diff --git a/.gitignore b/.gitignore index fbe2812f..734e5a61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ +*.swp +.#* Cargo.lock target - -book-test +TAGS +src/tests/book-minimal/book +src/tests/book-minimal-with-assets/book +src/tests/book-wonderland-multilang/book book-example/book diff --git a/Cargo.toml b/Cargo.toml index b203f8fc..3922fc47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mdbook" -version = "0.0.15" +version = "0.0.16" authors = ["Mathieu David "] description = "create books from markdown files (like Gitbook)" documentation = "http://azerupi.github.io/mdBook/index.html" @@ -9,20 +9,26 @@ keywords = ["book", "gitbook", "rustbook", "markdown"] license = "MPL-2.0" readme = "README.md" build = "build.rs" +include = ["data"] exclude = [ "book-example/*", - "src/theme/stylus", + "data/html-template/_stylus", ] + [dependencies] clap = "2.19.2" handlebars = { version = "0.23.0", features = ["serde_type"] } serde = "0.8" serde_json = "0.8" pulldown-cmark = "0.0.8" +regex = "0.1" +glob = "0.2" log = "0.3" env_logger = "0.3" toml = { version = "0.2", features = ["serde"] } +phf = "0.7" +includedir = "0.2" # Watch feature notify = { version = "2.5.5", optional = true } @@ -34,6 +40,9 @@ iron = { version = "0.4", optional = true } staticfile = { version = "0.3", optional = true } ws = { version = "0.5.1", optional = true} +[build-dependencies] +includedir_codegen = "0.2" + # Tests [dev-dependencies] tempdir = "0.3.4" diff --git a/doc/assets/structs.png b/book-example/assets/images/structs-v0-0-15.png similarity index 100% rename from doc/assets/structs.png rename to book-example/assets/images/structs-v0-0-15.png diff --git a/doc/assets/structs.pum b/book-example/assets/images/structs-v0-0-15.pum similarity index 100% rename from doc/assets/structs.pum rename to book-example/assets/images/structs-v0-0-15.pum diff --git a/book-example/assets/images/structs-v0-0-16.png b/book-example/assets/images/structs-v0-0-16.png new file mode 100644 index 00000000..9eb2cced Binary files /dev/null and b/book-example/assets/images/structs-v0-0-16.png differ diff --git a/book-example/assets/images/structs-v0-0-16.pum b/book-example/assets/images/structs-v0-0-16.pum new file mode 100644 index 00000000..2ecab4b9 --- /dev/null +++ b/book-example/assets/images/structs-v0-0-16.pum @@ -0,0 +1,114 @@ +@startuml + +namespace book { + +class MDBook { + project_root: PathBuf, + template_dir: PathBuf, + dest_base: PathBuf, + render_intent: RenderIntent, + + translations: HashMap, + + indent_spaces: i32, + livereload: bool, + + new(project_root) +} + +class book::Book { + config: BookConfig, + toc: Vec, + + new(project_root) +} + +class book::Chapter { + title: String, + path: PathBuf, + dest_path: Option, + authors: Option>, + translators: Option>, + description: Option, + css_class: Option, + + new(title, path) +} + +} + +namespace book::bookconfig { + +class BookConfig { + dest: PathBuf, + src: PathBuf, + + title: String, + subtitle: Option, + description: Option, + language: Language, + authors: Vec, + translators: Option>, + publisher: Option, + number_format: NumberFormat, + section_names: Vec, + is_main_book: bool, + is_multilang: bool, + + new(project_root) +} + +class Author { + name: String, + file_as: String, + email: Option, + + new(name) +} + +class Language { + name: String, + code: String, + + new(name, code) +} + +class Publisher { + name: String, + url: Option, + logo_src: Option, + + new(name) +} + +enum NumberFormat { + Arabic + Roman + Word +} + +} + +namespace book::toc { + +class TocContent { + chapter: Chapter, + sub_items: Option>, + section: Option>, +} + +enum TocItem { + Numbered "TocContent", + Unnumbered "TocContent", + Unlisted "TocContent", + Spacer, +} + +} + +class Renderer { + build(&self, project_root: &PathBuf), + render(&self, book_project: &MDBook), +} + +@enduml diff --git a/book-example/book.toml b/book-example/book.toml index cac456db..996e7309 100644 --- a/book-example/book.toml +++ b/book-example/book.toml @@ -1,3 +1,5 @@ title = "mdBook Documentation" -description = "Create book from markdown files. Like Gitbook but implemented in Rust" -author = "Mathieu David" +description = "Create books from markdown files. Like Gitbook but implemented in Rust." + +[[authors]] +name = "Mathieu David" diff --git a/book-example/src/SUMMARY.md b/book-example/src/SUMMARY.md index 8d7bdcdd..43ede57c 100644 --- a/book-example/src/SUMMARY.md +++ b/book-example/src/SUMMARY.md @@ -1,7 +1,5 @@ # Summary -[Introduction](misc/introduction.md) - - [mdBook](README.md) - [Command Line Tool](cli/cli-tool.md) - [init](cli/init.md) @@ -18,5 +16,6 @@ - [MathJax Support](format/mathjax.md) - [Rust code specific features](format/rust.md) - [Rust Library](lib/lib.md) +- [Structs](structs/structs.md) ----------- [Contributors](misc/contributors.md) diff --git a/book-example/src/misc/introduction.md b/book-example/src/misc/introduction.md deleted file mode 100644 index 36495382..00000000 --- a/book-example/src/misc/introduction.md +++ /dev/null @@ -1,3 +0,0 @@ -# Introduction - -A frontmatter chapter. diff --git a/book-example/src/structs/structs.md b/book-example/src/structs/structs.md new file mode 100644 index 00000000..49eacf4a --- /dev/null +++ b/book-example/src/structs/structs.md @@ -0,0 +1,6 @@ +# Structs + +![structs reorganized](images/structs-v0-0-16.png) + +Diagram with [plantuml](http://plantuml.com) + diff --git a/build.rs b/build.rs index d3177444..b558bb18 100644 --- a/build.rs +++ b/build.rs @@ -1,23 +1,36 @@ // build.rs +extern crate includedir_codegen; + +use includedir_codegen::Compression; + use std::process::Command; use std::env; use std::path::Path; fn main() { + includedir_codegen::start("FILES") + .dir("data", Compression::Gzip) + .build("data.rs") + .unwrap(); + + // TODO this using cargo as a Makefile. This is only for development, it + // doesn't have to be part of the production auto-build. Use either a + // Makefile or an npm command if stylus comes from npm anyway. + if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") { // Compile stylus stylesheet to css let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let theme_dir = Path::new(&manifest_dir).join("src/theme/"); - let stylus_dir = theme_dir.join("stylus/book.styl"); + let template_dir = Path::new(&manifest_dir).join("data/html-template/"); + let stylus_dir = template_dir.join("_stylus/book.styl"); if !Command::new("stylus") .arg(format!("{}", stylus_dir.to_str().unwrap())) .arg("--out") - .arg(format!("{}", theme_dir.to_str().unwrap())) + .arg(format!("{}", template_dir.to_str().unwrap())) .arg("--use") .arg("nib") .status().unwrap() diff --git a/src/theme/index.hbs b/data/html-template/_layouts/index-old.hbs similarity index 100% rename from src/theme/index.hbs rename to data/html-template/_layouts/index-old.hbs diff --git a/data/html-template/_layouts/page.hbs b/data/html-template/_layouts/page.hbs new file mode 100644 index 00000000..ec5a88ff --- /dev/null +++ b/data/html-template/_layouts/page.hbs @@ -0,0 +1,99 @@ + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ {{{ content }}} +
+ + + {{#previous}} + + {{/previous}} + + {{#next}} + + {{/next}} + +
+ + {{#previous}} + + {{/previous}} + + {{#next}} + + {{/next}} + +
+ + + + + diff --git a/src/theme/stylus/book.styl b/data/html-template/_stylus/book.styl similarity index 100% rename from src/theme/stylus/book.styl rename to data/html-template/_stylus/book.styl diff --git a/src/theme/stylus/general.styl b/data/html-template/_stylus/general.styl similarity index 100% rename from src/theme/stylus/general.styl rename to data/html-template/_stylus/general.styl diff --git a/src/theme/stylus/menu.styl b/data/html-template/_stylus/menu.styl similarity index 100% rename from src/theme/stylus/menu.styl rename to data/html-template/_stylus/menu.styl diff --git a/src/theme/stylus/nav-icons.styl b/data/html-template/_stylus/nav-icons.styl similarity index 100% rename from src/theme/stylus/nav-icons.styl rename to data/html-template/_stylus/nav-icons.styl diff --git a/src/theme/stylus/page.styl b/data/html-template/_stylus/page.styl similarity index 100% rename from src/theme/stylus/page.styl rename to data/html-template/_stylus/page.styl diff --git a/src/theme/stylus/print.styl b/data/html-template/_stylus/print.styl similarity index 100% rename from src/theme/stylus/print.styl rename to data/html-template/_stylus/print.styl diff --git a/src/theme/stylus/sidebar.styl b/data/html-template/_stylus/sidebar.styl similarity index 100% rename from src/theme/stylus/sidebar.styl rename to data/html-template/_stylus/sidebar.styl diff --git a/src/theme/stylus/theme-popup.styl b/data/html-template/_stylus/theme-popup.styl similarity index 100% rename from src/theme/stylus/theme-popup.styl rename to data/html-template/_stylus/theme-popup.styl diff --git a/src/theme/stylus/themes/base.styl b/data/html-template/_stylus/themes/base.styl similarity index 100% rename from src/theme/stylus/themes/base.styl rename to data/html-template/_stylus/themes/base.styl diff --git a/src/theme/stylus/themes/coal.styl b/data/html-template/_stylus/themes/coal.styl similarity index 100% rename from src/theme/stylus/themes/coal.styl rename to data/html-template/_stylus/themes/coal.styl diff --git a/src/theme/stylus/themes/index.styl b/data/html-template/_stylus/themes/index.styl similarity index 100% rename from src/theme/stylus/themes/index.styl rename to data/html-template/_stylus/themes/index.styl diff --git a/src/theme/stylus/themes/light.styl b/data/html-template/_stylus/themes/light.styl similarity index 100% rename from src/theme/stylus/themes/light.styl rename to data/html-template/_stylus/themes/light.styl diff --git a/src/theme/stylus/themes/navy.styl b/data/html-template/_stylus/themes/navy.styl similarity index 100% rename from src/theme/stylus/themes/navy.styl rename to data/html-template/_stylus/themes/navy.styl diff --git a/src/theme/stylus/themes/rust.styl b/data/html-template/_stylus/themes/rust.styl similarity index 100% rename from src/theme/stylus/themes/rust.styl rename to data/html-template/_stylus/themes/rust.styl diff --git a/src/theme/stylus/variables.styl b/data/html-template/_stylus/variables.styl similarity index 100% rename from src/theme/stylus/variables.styl rename to data/html-template/_stylus/variables.styl diff --git a/src/theme/book.css b/data/html-template/css/book.css similarity index 100% rename from src/theme/book.css rename to data/html-template/css/book.css diff --git a/src/theme/_FontAwesome/css/font-awesome.min.css b/data/html-template/css/font-awesome.min.css similarity index 100% rename from src/theme/_FontAwesome/css/font-awesome.min.css rename to data/html-template/css/font-awesome.min.css diff --git a/src/theme/highlight.css b/data/html-template/css/highlight.css similarity index 100% rename from src/theme/highlight.css rename to data/html-template/css/highlight.css diff --git a/src/theme/tomorrow-night.css b/data/html-template/css/tomorrow-night.css similarity index 100% rename from src/theme/tomorrow-night.css rename to data/html-template/css/tomorrow-night.css diff --git a/src/theme/_FontAwesome/fonts/FontAwesome.otf b/data/html-template/fonts/FontAwesome.otf similarity index 100% rename from src/theme/_FontAwesome/fonts/FontAwesome.otf rename to data/html-template/fonts/FontAwesome.otf diff --git a/src/theme/_FontAwesome/fonts/fontawesome-webfont.eot b/data/html-template/fonts/fontawesome-webfont.eot similarity index 100% rename from src/theme/_FontAwesome/fonts/fontawesome-webfont.eot rename to data/html-template/fonts/fontawesome-webfont.eot diff --git a/src/theme/_FontAwesome/fonts/fontawesome-webfont.svg b/data/html-template/fonts/fontawesome-webfont.svg similarity index 100% rename from src/theme/_FontAwesome/fonts/fontawesome-webfont.svg rename to data/html-template/fonts/fontawesome-webfont.svg diff --git a/src/theme/_FontAwesome/fonts/fontawesome-webfont.ttf b/data/html-template/fonts/fontawesome-webfont.ttf similarity index 100% rename from src/theme/_FontAwesome/fonts/fontawesome-webfont.ttf rename to data/html-template/fonts/fontawesome-webfont.ttf diff --git a/src/theme/_FontAwesome/fonts/fontawesome-webfont.woff b/data/html-template/fonts/fontawesome-webfont.woff similarity index 100% rename from src/theme/_FontAwesome/fonts/fontawesome-webfont.woff rename to data/html-template/fonts/fontawesome-webfont.woff diff --git a/src/theme/_FontAwesome/fonts/fontawesome-webfont.woff2 b/data/html-template/fonts/fontawesome-webfont.woff2 similarity index 100% rename from src/theme/_FontAwesome/fonts/fontawesome-webfont.woff2 rename to data/html-template/fonts/fontawesome-webfont.woff2 diff --git a/src/theme/favicon.png b/data/html-template/images/favicon.png similarity index 100% rename from src/theme/favicon.png rename to data/html-template/images/favicon.png diff --git a/src/theme/book.js b/data/html-template/js/book.js similarity index 100% rename from src/theme/book.js rename to data/html-template/js/book.js diff --git a/src/theme/highlight.js b/data/html-template/js/highlight.js similarity index 100% rename from src/theme/highlight.js rename to data/html-template/js/highlight.js diff --git a/src/theme/jquery-2.1.4.min.js b/data/html-template/js/jquery-2.1.4.min.js similarity index 100% rename from src/theme/jquery-2.1.4.min.js rename to data/html-template/js/jquery-2.1.4.min.js diff --git a/doc/assets/bookdata.graphml b/doc/assets/bookdata.graphml deleted file mode 100644 index a1722780..00000000 --- a/doc/assets/bookdata.graphml +++ /dev/null @@ -1,614 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Config - - - - - - - - - - - - Book Content - - - - - - - - - - - CLI Args - - - - - - - - - - - - - - - - book.toml - - - - - - - - - - - - - - - - SUMMARY.md - - - - - - - - - - - - - - - - markdown -chapters - - - - - - - - - - - - - - - - template assets - - - - - - - - - - - - - - - - BookConfig - lang -project_root -book_dest -book_src -template_path - - - - - - - - - - - - - - - - - - - Book - config -metadata -toc - - - - - - - - - - - - - - - - - - - Renderer - render(book) - - - - - - - - - - - - - - - - - - - MDBook - project_root -books -renderer - - - - - - - - - - - - - - - - - - - images - - - - - - - - - - - - - - - - BookMetadata - title -author -publisher - - - - - - - - - - - - - - - - - - - Vec<Chapter> - title -file - - - - - - - - - - - - - - - - - - - YAML headers -(optional) - - - - - - - - - - - - - - - - - - summary -parser - - - - - - - - - - - - - - - - - - Vec<TocItem> - content -sub_items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - behaviour control, -paths, target format - - - - - - - - - - - - - - paths - - - - - - - - - - - - - - metadata - - - - - - - - - - - - - - - - - - - - chapter list - - - - - - - - - - - - - - chapter attributes -chapter content - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - renderer specific -data - - - - - - - - - - - - template path - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/assets/bookdata.png b/doc/assets/bookdata.png deleted file mode 100644 index f206399a..00000000 Binary files a/doc/assets/bookdata.png and /dev/null differ diff --git a/doc/assets/structs-reorganized.png b/doc/assets/structs-reorganized.png deleted file mode 100644 index cd33bb9c..00000000 Binary files a/doc/assets/structs-reorganized.png and /dev/null differ diff --git a/doc/assets/structs-reorganized.pum b/doc/assets/structs-reorganized.pum deleted file mode 100644 index 6dc4de93..00000000 --- a/doc/assets/structs-reorganized.pum +++ /dev/null @@ -1,129 +0,0 @@ -@startuml - -class book::MDBook { - project_root : PathBuf - - books : HashMap<&'a str, Book> - - renderer : Box - livereload : Option - indent_spaces: i32 - multilingual: bool - - new(root) -} - -class book::book.Book { - config : BookConfig - metadata : BookMetadata - - chapters: Vec - - new(title) -} - -class book::bookconfig.BookConfig { - lang : Language - - project_root : PathBuf - book_dest : PathBuf - book_src : PathBuf - template_path : PathBuf - - new(root) -} - -class book::chapter.Chapter { -title -file -author -description -css_class -index : Vec - -new(title, file) -} - -namespace book::toc { - -class TocItem { - content : TocContent - sub_items: Vec - new(content) -} - -enum TocContent { - Frontmatter "Chapter" - Mainmatter "Chapter" - Backmatter "Chapter" - Insert "Chapter" - Spacer -} - -} - -namespace book::metadata { - -class BookMetadata { - title - subtitle - description - publisher - language - authors - translators - number_format - section_names - new(title) -} - -class Author { - name - email - new(name) -} - -class Language { - name - code -} - -class Publisher { - name - url - logo_src -} - -enum NumberFormat { - Arabic - Roman - Word -} - -} - -class renderer::html_handlebars::HtmlHandlebars { - new() - render(book: MDBook) -} - -class theme::Theme { - index - css - favicon - js - highlight_css - tomorrow_night_css - highlight_js - jquery - new(src) -} - -book::book-[hidden]->book::bookconfig -book::book-[hidden]->book::chapter -book::book-[hidden]->book::toc -book::book-[hidden]->book::metadata - -renderer::html_handlebars::HtmlHandlebars-[hidden]->theme::Theme - -@enduml diff --git a/doc/doc.md b/doc/doc.md deleted file mode 100644 index 961f4926..00000000 --- a/doc/doc.md +++ /dev/null @@ -1,94 +0,0 @@ -# Doc - -Diagrams are with [yEd](http://www.yworks.com/products/yed) -and [plantuml](http://plantuml.com). - -## Data - -`MDBook::new(root)` parses CLI args and `book.toml` to create: - -- app config settings -- `Book` for each language - -Each `Book` is given their config setting with their source- and destination -paths. - -The renderer can then render each book. - -To render the TOC, renderer gets a Vec from summary parser. - -The renderer walks through the Vec. It can match content kinds in an enum and -this way knows whether to render: - -- front- back- or mainmatter -- spacer elements (vertical space in TOC but no chapter output) -- insert chapters (no TOC link, but the chapter is part of the reading sequence) - -![book data](assets/bookdata.png) - -### Renderer - -Takes a book, which knows: - -- metadata -- toc with chapters -- config for paths -- template assets (`template_path`) - -For generating pages: - -Book metadata, `BookMetadata` (title, author, publisher, etc.). Just recognize -those properties which can be easily anticipated. - -If Renderer needs more specific data, it can be supplied in `book.toml`. It's -the Renderer's job to open that and parse it out. - -Chapters are represented in a `Vec`, each item has the chapter content -as payload. - -If the user wants to store attributes that are not anticipated with structs, -they can go in a hashmap with string keys, let them be accessible from the -templates with helpers. - -For generating output: - -- template assets, `template-path`, renderer does whatever it wants with it -- config (root, dest, etc. folders) - -Renderer is seleceted by CLI or default (html). Each book is passed to this -renderer. - -### Config - -Takes data from: - -- CLI args -- book.toml - -## Structs - -### Reorganized - -![structs reorganized](assets/structs-reorganized.png) - -### Currently - -![structs](assets/structs.png) - -## Notes - -Take config paths for as many things as possible. Let the user organize their -project folder differently, or allow `mdbook` to function in existing projects -with already established folders. - -Add config path for `SUMMARY.md`. Default is good to be in `src/`, it allows -chapter links to work when reading the file on Github. - -The init command should copy the assets folder by default, it is better to make -this choice for new users. - -The specific assets (CSS, templates, etc.) are closely coupled with the book -content when the user is writing it. If the templates change when mdbook -develops, this changes the output in a way the user doesn't expect, maybe even -breaking their book. - diff --git a/src/bin/mdbook.rs b/src/bin/mdbook.rs index 2d2bb0b0..b8698c6a 100644 --- a/src/bin/mdbook.rs +++ b/src/bin/mdbook.rs @@ -29,14 +29,9 @@ use std::path::{Path, PathBuf}; use clap::{App, ArgMatches, SubCommand, AppSettings}; -// Uses for the Watch feature -#[cfg(feature = "watch")] -use notify::Watcher; -#[cfg(feature = "watch")] -use std::sync::mpsc::channel; - - use mdbook::MDBook; +use mdbook::renderer::{Renderer, HtmlHandlebars}; +use mdbook::utils; const NAME: &'static str = "mdbook"; @@ -55,7 +50,7 @@ fn main() { .about("Create boilerplate structure and files in the directory") // the {n} denotes a newline which will properly aligned in all help messages .arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'") - .arg_from_usage("--theme 'Copies the default theme into your source folder'") + .arg_from_usage("--copy-assets 'Copies the default assets (css, layout template, etc.) into your project folder'") .arg_from_usage("--force 'skip confirmation prompts'")) .subcommand(SubCommand::with_name("build") .about("Build the book from the markdown files") @@ -92,7 +87,6 @@ fn main() { } } - // Simple function that user comfirmation fn confirm() -> bool { io::stdout().flush().unwrap(); @@ -104,25 +98,24 @@ fn confirm() -> bool { } } - // Init command implementation fn init(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir); + let mut book_project = MDBook::new(&book_dir); - // Call the function that does the initialization - try!(book.init()); + book_project.read_config(); + book_project.parse_books(); - // If flag `--theme` is present, copy theme to src - if args.is_present("theme") { + // If flag `--copy-assets` is present, copy embedded assets to project root + if args.is_present("copy-assets") { // Skip this if `--force` is present - if !args.is_present("force") { + if book_project.get_project_root().join("assets").exists() && !args.is_present("force") { // Print warning - print!("\nCopying the default theme to {:?}", book.get_src()); - println!("could potentially overwrite files already present in that directory."); - print!("\nAre you sure you want to continue? (y/n) "); + println!("\nCopying the default assets to {:?}", book_project.get_project_root()); + println!("This will overwrite files already present in that directory."); + print!("Are you sure you want to continue? (y/n) "); // Read answer from user and exit if it's not 'yes' if !confirm() { @@ -132,20 +125,21 @@ fn init(args: &ArgMatches) -> Result<(), Box> { } } - // Call the function that copies the theme - try!(book.copy_theme()); - println!("\nTheme copied."); + // Copy the assets + try!(utils::fs::copy_data("data/**/*", + "data/", + vec![], + &book_project.get_project_root().join("assets"))); + + println!("\nAssets copied."); } - // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` - let is_dest_inside_root = book.get_dest().starts_with(book.get_root()); - - if !args.is_present("force") && is_dest_inside_root { + if !args.is_present("force") { println!("\nDo you want a .gitignore to be created? (y/n)"); if confirm() { - book.create_gitignore(); + utils::fs::create_gitignore(&book_project); println!("\n.gitignore created."); } } @@ -155,115 +149,40 @@ fn init(args: &ArgMatches) -> Result<(), Box> { Ok(()) } - // Build command implementation fn build(args: &ArgMatches) -> Result<(), Box> { let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config(); - try!(book.build()); + // TODO figure out render format intent when we acutally have different renderers + + let renderer = HtmlHandlebars::new(); + try!(renderer.build(&book_dir)); Ok(()) } - // Watch command implementation #[cfg(feature = "watch")] fn watch(args: &ArgMatches) -> Result<(), Box> { - let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config(); - - trigger_on_change(&mut book, |event, book| { - if let Some(path) = event.path { - println!("File changed: {:?}\nBuilding book...\n", path); - match book.build() { - Err(e) => println!("Error while building: {:?}", e), - _ => {}, - } - println!(""); - } - }); - + // TODO watch + println!("watch"); Ok(()) } - -// Watch command implementation +// Serve command implementation #[cfg(feature = "serve")] fn serve(args: &ArgMatches) -> Result<(), Box> { - const RELOAD_COMMAND: &'static str = "reload"; - - let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config(); - let port = args.value_of("port").unwrap_or("3000"); - let ws_port = args.value_of("ws-port").unwrap_or("3001"); - let interface = args.value_of("interface").unwrap_or("localhost"); - let public_address = args.value_of("address").unwrap_or(interface); - - let address = format!("{}:{}", interface, port); - let ws_address = format!("{}:{}", interface, ws_port); - - book.set_livereload(format!(r#" - - "#, public_address, ws_port, RELOAD_COMMAND).to_owned()); - - try!(book.build()); - - let staticfile = staticfile::Static::new(book.get_dest()); - let iron = iron::Iron::new(staticfile); - let _iron = iron.http(&*address).unwrap(); - - let ws_server = ws::WebSocket::new(|_| { - |_| { - Ok(()) - } - }).unwrap(); - - let broadcaster = ws_server.broadcaster(); - - std::thread::spawn(move || { - ws_server.listen(&*ws_address).unwrap(); - }); - - println!("\nServing on {}", address); - - trigger_on_change(&mut book, move |event, book| { - if let Some(path) = event.path { - println!("File changed: {:?}\nBuilding book...\n", path); - match book.build() { - Err(e) => println!("Error while building: {:?}", e), - _ => broadcaster.send(RELOAD_COMMAND).unwrap(), - } - println!(""); - } - }); - + // TODO serve + println!("serve"); Ok(()) } - fn test(args: &ArgMatches) -> Result<(), Box> { - let book_dir = get_book_dir(args); - let mut book = MDBook::new(&book_dir).read_config(); - - try!(book.test()); - + // TODO test + println!("test"); Ok(()) } - fn get_book_dir(args: &ArgMatches) -> PathBuf { if let Some(dir) = args.value_of("dir") { // Check if path is relative from current dir, or absolute... @@ -277,58 +196,3 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf { env::current_dir().unwrap() } } - - -// Calls the closure when a book source file is changed. This is blocking! -#[cfg(feature = "watch")] -fn trigger_on_change(book: &mut MDBook, closure: F) -> () - where F: Fn(notify::Event, &mut MDBook) -> () -{ - // Create a channel to receive the events. - let (tx, rx) = channel(); - - let w: Result = notify::Watcher::new(tx); - - match w { - Ok(mut watcher) => { - // Add the source directory to the watcher - if let Err(e) = watcher.watch(book.get_src()) { - println!("Error while watching {:?}:\n {:?}", book.get_src(), e); - ::std::process::exit(0); - }; - - // Add the book.json file to the watcher if it exists, because it's not - // located in the source directory - if let Err(_) = watcher.watch(book.get_root().join("book.json")) { - // do nothing if book.json is not found - } - - let mut previous_time = time::get_time(); - - println!("\nListening for changes...\n"); - - loop { - match rx.recv() { - Ok(event) => { - // Skip the event if an event has already been issued in the last second - let time = time::get_time(); - if time - previous_time < time::Duration::seconds(1) { - continue; - } else { - previous_time = time; - } - - closure(event, book); - }, - Err(e) => { - println!("An error occured: {:?}", e); - }, - } - } - }, - Err(e) => { - println!("Error while trying to watch the files:\n\n\t{:?}", e); - ::std::process::exit(0); - }, - } -} diff --git a/src/book/book.rs b/src/book/book.rs index 6b6ce03b..3bfbb218 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -1,80 +1,118 @@ -use book::metadata::BookMetadata; -use book::chapter::Chapter; +use std::fs::File; +use std::path::{Path, PathBuf}; +use book::bookconfig::BookConfig; +use book::toc::{TocItem, TocContent}; -/// The `Book` struct contains the metadata and chapters for one language of the book. -/// Multiple `Book` structs are combined in the `MDBook` struct to support multi-language books. +use utils::fs::create_with_str; +use parse::construct_tocitems; + +/// The `Book` struct contains the metadata (config) and chapters (toc) for one +/// language of the book. Multiple `Book` structs are combined in the `MDBook` +/// struct to support multi-language books. #[derive(Debug, Clone)] pub struct Book { - metadata: BookMetadata, + pub config: BookConfig, + pub toc: Vec, +} - frontmatter: Vec, - mainmatter: Vec, - backmatter: Vec, +impl Default for Book { + fn default() -> Book { + Book { + config: BookConfig::default(), + toc: vec![], + } + } } impl Book { - /// Creates a new book with the given title, chapters are added with the - /// `add_frontmatter_chapter`, `add_mainmatter_chapter`, - /// `add_backmatter_chapter` methods - pub fn new(title: &str) -> Self { - Book { - metadata: BookMetadata::new(title), - frontmatter: Vec::new(), - mainmatter: Vec::new(), - backmatter: Vec::new(), + /// Creates a new book + pub fn new(project_root: &PathBuf) -> Book { + let conf = BookConfig::new(project_root); + let mut book = Book::default(); + book.config = conf; + book + } + + /// Parses in the SUMMARY.md or creates one + pub fn parse_or_create_summary_file(&mut self, first_as_index: bool) -> Result<&mut Self, String> { + + let summary_path = self.config.src.join("SUMMARY.md"); + if !summary_path.exists() { + try!(create_with_str(&summary_path, "# Summary")); } + + // parse SUMMARY.md to toc items + self.toc = match construct_tocitems(&summary_path, first_as_index) { + Ok(x) => x, + Err(e) => { return Err(format!("Error constructing the TOC: {:?}", e)); } + }; + + Ok(self) } - /// Adds a new mainmatter chapter - pub fn add_mainmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { - self.mainmatter.push(chapter); - self + /// Walks through the TOC array and calls parse_or_create() on each + pub fn parse_or_create_chapter_files(&mut self) -> Result<&mut Self, String> { + self.toc = self.process_them(&self.toc); + Ok(self) } - /// Adds a new frontmatter chapter - pub fn add_frontmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { - self.frontmatter.push(chapter); - self + fn process_them(&self, items: &Vec) -> Vec { + items.iter().map(|i| + match i { + &TocItem::Numbered(ref c) => TocItem::Numbered(self.process_toccontent(c)), + &TocItem::Unnumbered(ref c) => TocItem::Unnumbered(self.process_toccontent(c)), + &TocItem::Unlisted(ref c) => TocItem::Unlisted(self.process_toccontent(c)), + &TocItem::Spacer => TocItem::Spacer, + } + ).collect::>() } - /// Adds a new backmatter chapter - pub fn add_backmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { - self.backmatter.push(chapter); - self - } - - - /// This method takes a slice `&[x, y, z]` as parameter and returns the corresponding chapter. - /// For example, to retrieve chapter 2.3 we would use: - /// ``` - /// #extern crate mdbook; - /// #use mdbook::book::Book; - /// #fn main() { - /// #let book = Book::new("Test"); - /// let chapter_2_3 = book.get_chapter(&[2, 3]); - /// #} - /// ``` - pub fn get_chapter(&self, section: &[usize]) -> Option<&Chapter> { - match section.len() { - 0 => None, - 1 => self.mainmatter.get(section[0]), - _ => { - self.mainmatter - .get(section[0]) - .and_then(|ch| ch.get_sub_chapter(§ion[1..])) - }, + fn process_toccontent(&self, c: &TocContent) -> TocContent { + let mut content: TocContent = c.clone(); + if let Ok(ch) = content.chapter.clone().parse_or_create_using(&self.config.src) { + content.chapter = ch.to_owned(); } + if let Some(s) = content.sub_items { + let subs = self.process_them(&s); + content.sub_items = Some(subs); + } + content } - /// Returns a mutable reference to the metadata for modification - pub fn mut_metadata(&mut self) -> &mut BookMetadata { - &mut self.metadata - } + // TODO update - // Returns a reference to the metadata - pub fn metadata(&self) -> &BookMetadata { - &self.metadata - } + // /// This method takes a slice `&[x, y, z]` as parameter and returns the corresponding chapter. + // /// For example, to retrieve chapter 2.3 we would use: + // /// ``` + // /// #extern crate mdbook; + // /// #use mdbook::book::Book; + // /// #fn main() { + // /// #let book = Book::new("Test"); + // /// let chapter_2_3 = book.get_chapter(&[2, 3]); + // /// #} + // /// ``` + // pub fn get_chapter(&self, section: &[usize]) -> Option<&Chapter> { + // match section.len() { + // 0 => None, + // 1 => self.mainmatter.get(section[0]), + // _ => { + // self.mainmatter + // .get(section[0]) + // .and_then(|ch| ch.get_sub_chapter(§ion[1..])) + // }, + // } + // } + + // /// Returns a mutable reference to the metadata for modification + // pub fn mut_metadata(&mut self) -> &mut BookMetadata { + // &mut self.metadata + // } + + // // Returns a reference to the metadata + // pub fn metadata(&self) -> &BookMetadata { + // &self.metadata + // } } + diff --git a/src/book/bookconfig.rs b/src/book/bookconfig.rs index 50bcb76d..89ff86bb 100644 --- a/src/book/bookconfig.rs +++ b/src/book/bookconfig.rs @@ -3,169 +3,214 @@ extern crate toml; use std::process::exit; use std::fs::File; use std::io::Read; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::collections::BTreeMap; use std::str::FromStr; use serde_json; +use utils; + #[derive(Debug, Clone)] pub struct BookConfig { - root: PathBuf, + + // Paths + pub dest: PathBuf, pub src: PathBuf, - pub theme_path: PathBuf, + // Metadata + + /// The title of the book. pub title: String, - pub author: String, - pub description: String, + /// The subtitle, when titles are in the form of "The Immense Journey: An + /// Imaginative Naturalist Explores the Mysteries of Man and Nature" + pub subtitle: Option, + /// A brief description or summary of the book. + pub description: Option, + pub language: Language, + pub authors: Vec, + pub translators: Option>, + /// Publisher's info + pub publisher: Option, + /// Chapter numbering scheme + pub number_format: NumberFormat, + /// Section names for nested Vec structures, defaults to `[ "Chapter", "Section", "Subsection" ]` + pub section_names: Vec, + /// Whether this is the main book, in the case of translations. + pub is_main_book: bool, + pub is_multilang: bool, +} - pub indent_spaces: i32, - multilingual: bool, +impl Default for BookConfig { + fn default() -> BookConfig { + BookConfig { + dest: PathBuf::from("book".to_string()), + src: PathBuf::from("src".to_string()), + + title: "Untitled".to_string(), + subtitle: None, + description: None, + language: Language::default(), + authors: vec![Author::new("The Author").file_as("Author, The")], + translators: None, + publisher: None, + number_format: NumberFormat::Arabic, + section_names: vec!["Chapter".to_string(), + "Section".to_string(), + "Subsection".to_string()], + is_main_book: false, + is_multilang: false, + } + } } impl BookConfig { - pub fn new(root: &Path) -> Self { - BookConfig { - root: root.to_owned(), - dest: root.join("book"), - src: root.join("src"), - theme_path: root.join("theme"), - title: String::new(), - author: String::new(), - description: String::new(), + pub fn new(project_root: &PathBuf) -> BookConfig { + let mut conf = BookConfig::default(); - indent_spaces: 4, // indentation used for SUMMARY.md - multilingual: false, - } - } + // join paths to project_root + // Prefer "" to "." and "src" to "./src", avoid "././src" - pub fn read_config(&mut self, root: &Path) -> &mut Self { - - debug!("[fn]: read_config"); - - let read_file = |path: PathBuf| -> String { - let mut data = String::new(); - let mut f: File = match File::open(&path) { - Ok(x) => x, - Err(_) => { - error!("[*]: Failed to open {:?}", &path); - exit(2); - } - }; - if let Err(_) = f.read_to_string(&mut data) { - error!("[*]: Failed to read {:?}", &path); - exit(2); - } - data - }; - - // Read book.toml or book.json if exists - - if Path::new(root.join("book.toml").as_os_str()).exists() { - - debug!("[*]: Reading config"); - let data = read_file(root.join("book.toml")); - self.parse_from_toml_string(&data); - - } else if Path::new(root.join("book.json").as_os_str()).exists() { - - debug!("[*]: Reading config"); - let data = read_file(root.join("book.json")); - self.parse_from_json_string(&data); - - } else { - debug!("[*]: No book.toml or book.json was found, using defaults."); + let mut pr = project_root.clone(); + if pr.as_os_str() == OsStr::new(".") { + pr = PathBuf::from("".to_string()); } - self - } - - pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self { - - let mut parser = toml::Parser::new(&data); - - let config = match parser.parse() { - Some(x) => {x}, - None => { - error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors); - exit(2); - } - }; - - self.parse_from_btreemap(&config); - - self - } - - /// Parses the string to JSON and converts it to BTreeMap. - pub fn parse_from_json_string(&mut self, data: &String) -> &mut Self { - - let c: serde_json::Value = match serde_json::from_str(&data) { - Ok(x) => x, - Err(e) => { - error!("[*]: JSON parse errors in book.json: {:?}", e); - exit(2); - } - }; - - let config = json_object_to_btreemap(&c.as_object().unwrap()); - self.parse_from_btreemap(&config); - - self + conf.dest = pr.join(&conf.dest); + conf.src = pr.join(&conf.src); + + conf } + /// Parses recognized keys from a BTreeMap one by one. Not trying to + /// directly de-serialize to `BookConfig` so that we can provide some + /// convenient shorthands for the user. + /// + /// `book.toml` is a user interface, not an app data store, we never have to + /// write data back to it. + /// + /// Parses author when given as an array, or when given as a hash key to + /// make declaring just an author name easy. + /// + /// Both of these express a single author: + /// + /// ```toml + /// [[authors]] + /// name = "Marcus Aurelius Antoninus" + /// ``` + /// + /// Or: + /// + /// ```toml + /// name = "Marcus Aurelius Antoninus" + /// ``` + /// pub fn parse_from_btreemap(&mut self, config: &BTreeMap) -> &mut Self { - // Title, author, description - if let Some(a) = config.get("title") { - self.title = a.to_string().replace("\"", ""); - } - if let Some(a) = config.get("author") { - self.author = a.to_string().replace("\"", ""); - } - if let Some(a) = config.get("description") { - self.description = a.to_string().replace("\"", ""); - } + // Paths // Destination folder if let Some(a) = config.get("dest") { - let mut dest = PathBuf::from(&a.to_string().replace("\"", "")); - - // If path is relative make it absolute from the parent directory of src - if dest.is_relative() { - dest = self.get_root().join(&dest); - } + let dest = PathBuf::from(&a.to_string().replace("\"", "")); self.set_dest(&dest); } // Source folder if let Some(a) = config.get("src") { - let mut src = PathBuf::from(&a.to_string().replace("\"", "")); - if src.is_relative() { - src = self.get_root().join(&src); - } + let src = PathBuf::from(&a.to_string().replace("\"", "")); self.set_src(&src); } - // Theme path folder - if let Some(a) = config.get("theme_path") { - let mut theme_path = PathBuf::from(&a.to_string().replace("\"", "")); - if theme_path.is_relative() { - theme_path = self.get_root().join(&theme_path); - } - self.set_theme_path(&theme_path); + // Metadata + + let extract_authors_from_slice = |x: &[toml::Value]| -> Vec { + x.iter() + .filter_map(|x| x.as_table()) + .map(|x| Author::from(x.to_owned())) + .collect::>() + }; + + if let Some(a) = config.get("title") { + self.title = a.to_string().replace("\"", ""); } - self - } + if let Some(a) = config.get("subtitle") { + self.subtitle = Some(a.to_string().replace("\"", "")); + } - pub fn get_root(&self) -> &Path { - &self.root - } + if let Some(a) = config.get("description") { + self.description = Some(a.to_string().replace("\"", "")); + } + + if let Some(a) = config.get("language") { + if let Some(b) = a.as_table() { + self.language = Language::from(b.to_owned()); + } + } + + // Author name as a hash key. + if let Some(a) = config.get("author") { + if let Some(b) = a.as_str() { + self.authors = vec![Author::new(b)]; + } + } + + // Authors as an array of tables. This will override the above. + if let Some(a) = config.get("authors") { + if let Some(b) = a.as_slice() { + self.authors = extract_authors_from_slice(b); + } + } + + // Translator name as a hash key. + if let Some(a) = config.get("translator") { + if let Some(b) = a.as_str() { + self.translators = Some(vec![Author::new(b)]); + } + } + + // Translators as an array of tables. This will override the above. + if let Some(a) = config.get("translators") { + if let Some(b) = a.as_slice() { + self.translators = Some(extract_authors_from_slice(b)); + } + } + + if let Some(a) = config.get("publisher") { + if let Some(b) = a.as_table() { + self.publisher = Some(Publisher::from(b.to_owned())); + } + } + + if let Some(a) = config.get("number_format") { + if let Some(b) = a.as_str() { + self.number_format = match b.to_lowercase().as_ref() { + "arabic" => NumberFormat::Arabic, + "roman" => NumberFormat::Roman, + "word" => NumberFormat::Word, + _ => NumberFormat::Arabic, + }; + } + } + + if let Some(a) = config.get("section_names") { + if let Some(b) = a.as_slice() { + self.section_names = + b.iter() + .filter_map(|x| x.as_str()) + .map(|x| x.to_string()) + .collect::>(); + } + } + + if let Some(a) = config.get("is_main_book") { + if let Some(b) = a.as_bool() { + self.is_main_book = b; + } + } - pub fn set_root(&mut self, root: &Path) -> &mut Self { - self.root = root.to_owned(); self } @@ -173,7 +218,7 @@ impl BookConfig { &self.dest } - pub fn set_dest(&mut self, dest: &Path) -> &mut Self { + pub fn set_dest(&mut self, dest: &Path) -> &mut BookConfig { self.dest = dest.to_owned(); self } @@ -182,47 +227,152 @@ impl BookConfig { &self.src } - pub fn set_src(&mut self, src: &Path) -> &mut Self { + pub fn set_src(&mut self, src: &Path) -> &mut BookConfig { self.src = src.to_owned(); self } - pub fn get_theme_path(&self) -> &Path { - &self.theme_path +} + +#[derive(Debug, Clone)] +pub struct Author { + /// Author's name, such as "Howard Philip Lovecraft" + name: String, + /// Author's name in the form of "Lovecraft, Howard Philip", an ebook metadata field used for sorting + file_as: String, + email: Option, +} + +impl Author { + + pub fn new(name: &str) -> Self { + Author { + name: name.to_owned(), + file_as: utils::last_name_first(name), + email: None, + } } - pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self { - self.theme_path = theme_path.to_owned(); + pub fn file_as(mut self, file_as: &str) -> Self { + self.file_as = file_as.to_owned(); + self + } + + pub fn with_email(mut self, email: &str) -> Self { + self.email = Some(email.to_owned()); self } } -pub fn json_object_to_btreemap(json: &serde_json::Map) -> BTreeMap { - let mut config: BTreeMap = BTreeMap::new(); - - for (key, value) in json.iter() { - config.insert( - String::from_str(key).unwrap(), - json_value_to_toml_value(value.to_owned()) - ); - } - - config -} - -pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value { - match json { - serde_json::Value::Null => toml::Value::String("".to_string()), - serde_json::Value::Bool(x) => toml::Value::Boolean(x), - serde_json::Value::I64(x) => toml::Value::Integer(x), - serde_json::Value::U64(x) => toml::Value::Integer(x as i64), - serde_json::Value::F64(x) => toml::Value::Float(x), - serde_json::Value::String(x) => toml::Value::String(x), - serde_json::Value::Array(x) => { - toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect()) - }, - serde_json::Value::Object(x) => { - toml::Value::Table(json_object_to_btreemap(&x)) - }, +impl From for Author { + fn from(data: toml::Table) -> Author { + let mut author = Author::new("The Author"); + if let Some(x) = data.get("name") { + author.name = x.to_string().replace("\"", ""); + } + if let Some(x) = data.get("file_as") { + author.file_as = x.to_string().replace("\"", ""); + } else { + author.file_as = utils::last_name_first(&author.name); + } + if let Some(x) = data.get("email") { + author.email = Some(x.to_string().replace("\"", "")); + } + author } } + +#[derive(Debug, Clone)] +pub struct Language { + pub name: String, + pub code: String, +} + +impl Default for Language { + fn default() -> Self { + Language { + name: String::from("English"), + code: String::from("en"), + } + } +} + +impl Language { + pub fn new(name: &str, code: &str) -> Language { + Language{ + name: name.to_string(), + code: code.to_string(), + } + } +} + +impl From for Language { + fn from(data: toml::Table) -> Language { + let mut language = Language::default(); + if let Some(x) = data.get("name") { + language.name = x.to_string().replace("\"", ""); + } + if let Some(x) = data.get("code") { + language.code = x.to_string().replace("\"", ""); + } + language + } +} + +#[derive(Debug, Clone)] +pub struct Publisher { + /// name of the publisher organization + name: String, + /// link to the sublisher's site + url: Option, + /// path to publisher's logo image + logo_src: Option, +} + +impl Default for Publisher { + fn default() -> Publisher { + Publisher { + name: "The Publisher".to_string(), + url: None, + logo_src: None, + } + } +} + +impl Publisher { + pub fn new(name: &str) -> Publisher { + Publisher { + name: name.to_string(), + url: None, + logo_src: None, + } + } +} + +impl From for Publisher { + fn from(data: toml::Table) -> Publisher { + let mut publisher = Publisher::default(); + if let Some(x) = data.get("name") { + publisher.name = x.to_string().replace("\"", ""); + } + if let Some(x) = data.get("url") { + publisher.url = Some(x.to_string()); + } + if let Some(x) = data.get("logo_src") { + publisher.logo_src = Some(PathBuf::from(x.to_string())); + } + publisher + } +} + +/// NumberFormat when rendering chapter titles. +#[derive(Debug, Clone)] +pub enum NumberFormat { + /// 19 + Arabic, + /// XIX + Roman, + /// Nineteen + Word, +} + diff --git a/src/book/bookitem.rs b/src/book/bookitem.rs deleted file mode 100644 index 2d5bf976..00000000 --- a/src/book/bookitem.rs +++ /dev/null @@ -1,82 +0,0 @@ -use serde::{Serialize, Serializer}; -use std::path::PathBuf; - -#[derive(Debug, Clone)] -pub enum BookItem { - Chapter(String, Chapter), // String = section - Affix(Chapter), - Spacer, -} - -#[derive(Debug, Clone)] -pub struct Chapter { - pub name: String, - pub path: PathBuf, - pub sub_items: Vec, -} - -#[derive(Debug, Clone)] -pub struct BookItems<'a> { - pub items: &'a [BookItem], - pub current_index: usize, - pub stack: Vec<(&'a [BookItem], usize)>, -} - - -impl Chapter { - pub fn new(name: String, path: PathBuf) -> Self { - - Chapter { - name: name, - path: path, - sub_items: vec![], - } - } -} - - -impl Serialize for Chapter { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { - let mut state = try!(serializer.serialize_struct("Chapter", 2)); - try!(serializer.serialize_struct_elt(&mut state, "name", self.name.clone())); - try!(serializer.serialize_struct_elt(&mut state, "path", self.path.clone())); - serializer.serialize_struct_end(state) - } -} - - - -// Shamelessly copied from Rustbook -// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs) -impl<'a> Iterator for BookItems<'a> { - type Item = &'a BookItem; - - fn next(&mut self) -> Option<&'a BookItem> { - loop { - if self.current_index >= self.items.len() { - match self.stack.pop() { - None => return None, - Some((parent_items, parent_idx)) => { - self.items = parent_items; - self.current_index = parent_idx + 1; - }, - } - } else { - let cur = self.items.get(self.current_index).unwrap(); - - match *cur { - BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => { - self.stack.push((self.items, self.current_index)); - self.items = &ch.sub_items[..]; - self.current_index = 0; - }, - BookItem::Spacer => { - self.current_index += 1; - }, - } - - return Some(cur); - } - } - } -} diff --git a/src/book/chapter.rs b/src/book/chapter.rs index 7afb3610..b2ef07a5 100644 --- a/src/book/chapter.rs +++ b/src/book/chapter.rs @@ -1,71 +1,215 @@ +extern crate regex; +extern crate toml; + +use regex::Regex; + use std::path::{Path, PathBuf}; -use book::metadata::Author; +use std::fs::File; +use std::error::Error; +use std::io::{self, Read}; +use std::collections::BTreeMap; -/// The Chapter struct holds the title of the chapter as written in the SUMMARY.md file, -/// the location of the markdown file containing the content and eventually sub-chapters +use utils; +use book::bookconfig::Author; -/// TODO use in template: author, description, index, class +use utils::fs::create_with_str; +/// The Chapter struct holds the title of the chapter as written in the +/// SUMMARY.md file, the location of the markdown file and other metadata. +/// +/// If the markdown file starts with a TOML header, it will be parsed to set the +/// chapter's properties. A TOML header should start and end with `+++` lines: +/// +/// ``` +/// +++ +/// title = "The Library of Babel" +/// author = "Jorge Luis Borges" +/// translator = "James E. Irby" +/// +++ +/// +/// # Babel +/// +/// The universe (which others call the Library) is composed of an indefinite and +/// perhaps infinite number of hexagonal galleries, with vast air shafts between, +/// surrounded by very low railings. From any of the hexagons one can see, +/// interminably, the upper and lower floors. +/// ``` #[derive(Debug, Clone)] pub struct Chapter { + /// The title of the chapter. - title: String, - /// Path to chapter's markdown file. - file: PathBuf, + pub title: String, - /// TODO The author of the chapter, or the book. - author: Author, - /// TODO The description of the chapter. - description: String, - /// TODO Index number of the chapter in its level. This is the Vec index + 1. - index: i32, - /// TODO CSS class that will be added to the page-level wrap div to allow customized chapter styles. - class: String, + /// Path to the chapter's markdown file, relative to the book's source + /// directory. + /// + /// `book.src.join(chapter.path)` points to the Markdown file, and + /// `book.dest.join(chapter.path).with_extension("html")` points to the + /// output html file. This way if the user had a custom folder structure in + /// their source folder, this is re-created in the destination folder. + pub path: PathBuf, - sub_chapters: Vec, + /// Optional destination path to write to. Used when changing the first + /// chapter's path to index.html. + pub dest_path: Option, + + /// The author of the chapter, or the book. + pub authors: Option>, + + /// The translators of the chapter, or the book. + pub translators: Option>, + + /// The description of the chapter. + pub description: Option, + + /// CSS class that will be added to the page-level wrap div to allow + /// customized chapter styles. + pub css_class: Option, +} + +impl Default for Chapter { + fn default() -> Chapter { + Chapter { + title: "Untitled".to_string(), + path: PathBuf::from("src".to_string()).join("untitled.md"), + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None, + } + } } impl Chapter { - /// Creates a new chapter with the given title and source file and no sub-chapters - pub fn new(title: &str, file: &Path) -> Self { - Chapter { - title: title.to_owned(), - file: file.to_owned(), - sub_chapters: Vec::new(), + pub fn new(title: String, path: PathBuf) -> Chapter { + let mut chapter = Chapter::default(); + chapter.title = title; + chapter.path = path; + chapter + } - // TODO placeholder values for now - author: Author::new(""), - description: "".to_string(), - index: 0, - class: "".to_string(), + pub fn parse_or_create_using(&mut self, book_src_dir: &PathBuf) -> Result<&mut Self, String> { + + debug!("[fn] Chapter::parse_or_create() : {:?}", &self); + + let src_path = &book_src_dir.join(&self.path).to_owned(); + if !src_path.exists() { + debug!("[*] Creating: {:?}", src_path); + match create_with_str(src_path, &format!("# {}", self.title)) { + Ok(_) => { return Ok(self); }, + Err(e) => { + return Err(format!("Could not create: {:?}", src_path)); + }, + } } - } - /// This function takes a slice `&[x,y,z]` and returns the corresponding sub-chapter if it exists. - /// - /// For example: `chapter.get_sub_chapter(&[1,3])` will return the third sub-chapter of the first sub-chapter. - pub fn get_sub_chapter(&self, section: &[usize]) -> Option<&Chapter> { - match section.len() { - 0 => None, - 1 => self.sub_chapters.get(section[0]), - _ => { - // The lengt of the slice is more than one, this means that we want a sub-chapter of a sub-chapter - // We call `get_sub_chapter` recursively until we are deep enough and return the asked sub-chapter - self.sub_chapters - .get(section[0]) - .and_then(|ch| ch.get_sub_chapter(§ion[1..])) - }, + let mut text = String::new(); + match File::open(src_path) { + Err(e) => { return Err(format!("Read error: {:?}", e)); }, + Ok(mut f) => { + f.read_to_string(&mut text); + } } + + let re: Regex = Regex::new(r"(?ms)^\+\+\+\n(?P.*)\n\+\+\+\n").unwrap(); + + match re.captures(&text) { + Some(caps) => { + let toml = caps.name("toml").unwrap(); + match utils::toml_str_to_btreemap(&toml) { + Ok(x) => {self.parse_from_btreemap(&x);}, + Err(e) => { + error!("[*] Errors while parsing TOML: {:?}", e); + return Err(e); + } + } + } + None => {}, + } + + Ok(self) } - pub fn title(&self) -> &str { - &self.title + pub fn parse_from_btreemap(&mut self, data: &BTreeMap) -> &mut Self { + + let extract_authors_from_slice = |x: &[toml::Value]| -> Vec { + x.iter() + .filter_map(|x| x.as_table()) + .map(|x| Author::from(x.to_owned())) + .collect::>() + }; + + if let Some(a) = data.get("title") { + self.title = a.to_string().replace("\"", ""); + } + + if let Some(a) = data.get("description") { + self.description = Some(a.to_string().replace("\"", "")); + } + + if let Some(a) = data.get("css_class") { + self.css_class = Some(a.to_string()); + } + + // Author name as a hash key. + if let Some(a) = data.get("author") { + if let Some(b) = a.as_str() { + self.authors = Some(vec![Author::new(b)]); + } + } + + // Authors as an array of tables. This will override the above. + if let Some(a) = data.get("authors") { + if let Some(b) = a.as_slice() { + self.authors = Some(extract_authors_from_slice(b)); + } + } + + // Translator name as a hash key. + if let Some(a) = data.get("translator") { + if let Some(b) = a.as_str() { + self.translators = Some(vec![Author::new(b)]); + } + } + + // Translators as an array of tables. This will override the above. + if let Some(a) = data.get("translators") { + if let Some(b) = a.as_slice() { + self.translators = Some(extract_authors_from_slice(b)); + } + } + + self } - pub fn file(&self) -> &Path { - &self.file - } - pub fn sub_chapters(&self) -> &[Chapter] { - &self.sub_chapters + + /// Reads in the chapter's content from the markdown file. Chapter doesn't + /// know the book's src folder, hence the `book_src_dir` argument. + pub fn read_content_using(&self, book_src_dir: &PathBuf) -> Result> { + + let src_path = book_src_dir.join(&self.path); + + if !src_path.exists() { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + format!("Doesn't exist: {:?}", src_path)) + )); + } + + debug!("[*]: Opening file: {:?}", src_path); + + let mut f = try!(File::open(&src_path)); + let mut content: String = String::new(); + + debug!("[*]: Reading file"); + try!(f.read_to_string(&mut content)); + + // Render markdown using the pulldown-cmark crate + content = utils::strip_toml_header(&content); + content = utils::render_markdown(&content); + + Ok(content) } + } diff --git a/src/book/metadata.rs b/src/book/metadata.rs deleted file mode 100644 index 6fe837cd..00000000 --- a/src/book/metadata.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::PathBuf; - -/// TODO use in template: subtitle, description, publisher, number_format, section_names - -#[derive(Debug, Clone)] -pub struct BookMetadata { - /// The title of the book. - pub title: String, - - /// TODO The subtitle, when titles are in the form of "The Immense Journey: An - /// Imaginative Naturalist Explores the Mysteries of Man and Nature" - pub subtitle: String, - - /// TODO A brief description or summary. - pub description: String, - - /// TODO Publisher's info - pub publisher: Publisher, - - pub language: Language, - - authors: Vec, - translators: Vec, - - /// TODO Chapter numbering scheme - number_format: NumberFormat, - /// TODO Section names for nested Vec structures, such as `[ - /// "Part", "Chapter", "Section" ]` - section_names: Vec, -} - -#[derive(Debug, Clone)] -pub struct Author { - name: String, - email: Option, -} - -#[derive(Debug, Clone)] -pub struct Language { - name: String, - code: String, -} - -/// TODO use Publisher in template. - -#[derive(Debug, Clone)] -pub struct Publisher { - name: String, - /// link to the sublisher's site - url: String, - /// path to publisher's logo image - logo_src: PathBuf, -} - -impl Publisher { - pub fn default() -> Publisher { - Publisher { - name: "".to_string(), - url: "".to_string(), - logo_src: PathBuf::new(), - } - } -} - -/// TODO use NumberFormat when rendering chapter titles. - -#[derive(Debug, Clone)] -pub enum NumberFormat { - /// 19 - Arabic, - /// XIX - Roman, - /// Nineteen - Word, -} - -impl BookMetadata { - pub fn new(title: &str) -> Self { - BookMetadata { - title: title.to_owned(), - description: String::new(), - - language: Language::default(), - - authors: Vec::new(), - translators: Vec::new(), - - // TODO placeholder values for now - subtitle: "".to_string(), - publisher: Publisher::default(), - number_format: NumberFormat::Arabic, - section_names: vec![], - } - } - - pub fn set_description(&mut self, description: &str) -> &mut Self { - self.description = description.to_owned(); - self - } - - pub fn add_author(&mut self, author: Author) -> &mut Self { - self.authors.push(author); - self - } -} - -impl Author { - pub fn new(name: &str) -> Self { - Author { - name: name.to_owned(), - email: None, - } - } - - pub fn with_email(mut self, email: &str) -> Self { - self.email = Some(email.to_owned()); - self - } -} - - -impl Default for Language { - fn default() -> Self { - Language { - name: String::from("English"), - code: String::from("en"), - } - } -} diff --git a/src/book/mod.rs b/src/book/mod.rs index 712dad76..6d1ee063 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,123 +1,144 @@ -pub mod bookitem; -pub mod bookconfig; -pub mod metadata; -pub mod chapter; +extern crate toml; + pub mod book; +pub mod bookconfig; +pub mod toc; +pub mod chapter; -pub use self::metadata::{Author, Language, BookMetadata}; -pub use self::chapter::Chapter; pub use self::book::Book; +use renderer::{Renderer, HtmlHandlebars}; +use utils; -pub mod bookconfig_test; - -pub use self::bookitem::{BookItem, BookItems}; -pub use self::bookconfig::BookConfig; - +use std::env; +use std::process::exit; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::fs::{self, File}; +use std::io::Read; use std::error::Error; -use std::io; -use std::io::Write; -use std::io::ErrorKind; -use std::process::Command; -use std::collections::HashMap; +use std::collections::{HashMap, BTreeMap}; -use {theme, parse, utils}; -use renderer::{Renderer, HtmlHandlebars}; +#[derive(Debug, Clone)] +pub struct MDBook { + /// Top-level directory of the book project, as an absolute path. Defaults + /// to the current directory. `set_project_root()` converts relative paths + /// to absolute. + project_root: PathBuf, -pub struct MDBook<'a> { - root: PathBuf, - dest: PathBuf, - src: PathBuf, - theme_path: PathBuf, + /// Path to the template for the renderer, relative to `project_root`. + /// The `render_intent` determines its default value. + /// + /// A book doesn't necessarily has to have the template files. When not + /// found in the book's folder, the embedded static assets will be used. + /// + /// Html Handlebars: `project_root` + `assets/html-template`. + template_dir: PathBuf, - pub title: String, - pub author: String, - pub description: String, + /// Output base for all books, relative to `project_root`. Defaults to + /// `book`. + dest_base: PathBuf, - pub content: Vec, - books: HashMap<&'a str, Book>, - renderer: Box, + /// Informs other functions which renderer has been selected, either by + /// default or CLI argument. + render_intent: RenderIntent, - livereload: Option, + // TODO Identify and cross-link translations either by file name, or an id + // string. + + /// The book, or books in case of translations, accessible with a String + /// key. The keys can be two-letter codes of the translation such as 'en' or + /// 'fr', but this is not enforced. + /// + /// The String keys will be sub-folders where the translation's Markdown + /// sources are expected. + /// + /// Each translation should have its own SUMMARY.md file, in its source + /// folder with the chapter files. + /// + /// In the case of a single language, it is the sole item in the HashMap, + /// and its source is not expected to be under a sub-folder, just simply in + /// `./src`. + /// + /// Translations have to be declared in `book.toml` in their separate + /// blocks. In this case `is_main_book = true` has to be set to mark the + /// main book to avoid ambiguity. + /// + /// For a single language, the book's properties can be set on the main + /// block: + /// + /// ```toml + /// livereload = true + /// title = "Alice in Wonderland" + /// author = "Lewis Carroll" + /// ``` + /// + /// For multiple languages, declare them in blocks: + /// + /// ```toml + /// livereload = true + /// + /// [translations.en] + /// title = "Alice in Wonderland" + /// author = "Lewis Carroll" + /// language = { name = "English", code = "en" } + /// is_main_book = true + /// + /// [translations.fr] + /// title = "Alice au pays des merveilles" + /// author = "Lewis Carroll" + /// translator = "Henri Bué" + /// language = { name = "Français", code = "fr" } + /// + /// [translations.hu] + /// title = "Alice Csodaországban" + /// author = "Lewis Carroll" + /// translator = "Kosztolányi Dezső" + /// language = { name = "Hungarian", code = "hu" } + /// ``` + pub translations: HashMap, + + /// Space indentation in SUMMARY.md, defaults to 4 spaces. + pub indent_spaces: i32, + + /// Whether to include the livereload snippet in the output html. + pub livereload: bool, } -impl<'a> MDBook<'a> { - /// Create a new `MDBook` struct with root directory `root` - /// - /// Default directory paths: - /// - /// - source: `root/src` - /// - output: `root/book` - /// - theme: `root/theme` - /// - /// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest) - - pub fn new(root: &Path) -> MDBook { - - if !root.exists() || !root.is_dir() { - warn!("{:?} No directory with that name", root); - } - - MDBook { - root: root.to_owned(), - dest: root.join("book"), - src: root.join("src"), - theme_path: root.join("theme"), - - title: String::new(), - author: String::new(), - description: String::new(), - - content: vec![], - books: HashMap::new(), - renderer: Box::new(HtmlHandlebars::new()), - - livereload: None, - } +impl Default for MDBook { + fn default() -> MDBook { + let mut proj: MDBook = MDBook { + project_root: PathBuf::from("".to_string()), + template_dir: PathBuf::from("".to_string()), + dest_base: PathBuf::from("book".to_string()), + render_intent: RenderIntent::HtmlHandlebars, + translations: HashMap::new(), + indent_spaces: 4, + livereload: false, + }; + proj.set_project_root(&env::current_dir().unwrap()); + // sets default template_dir + proj.set_render_intent(RenderIntent::HtmlHandlebars); + proj } +} - /// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html): - /// `(section: String, bookitem: &BookItem)` - /// - /// ```no_run - /// # extern crate mdbook; - /// # use mdbook::MDBook; - /// # use mdbook::BookItem; - /// # use std::path::Path; - /// # fn main() { - /// # let mut book = MDBook::new(Path::new("mybook")); - /// for item in book.iter() { - /// match item { - /// &BookItem::Chapter(ref section, ref chapter) => {}, - /// &BookItem::Affix(ref chapter) => {}, - /// &BookItem::Spacer => {}, - /// } - /// } - /// - /// // would print something like this: - /// // 1. Chapter 1 - /// // 1.1 Sub Chapter - /// // 1.2 Sub Chapter - /// // 2. Chapter 2 - /// // - /// // etc. - /// # } - /// ``` +#[derive(Debug, Clone)] +pub enum RenderIntent { + HtmlHandlebars, +} - pub fn iter(&self) -> BookItems { - BookItems { - items: &self.content[..], - current_index: 0, - stack: Vec::new(), - } +impl MDBook { + + /// Create a new `MDBook` struct with top-level project directory `project_root` + pub fn new(project_root: &PathBuf) -> MDBook { + MDBook::default().set_project_root(project_root).clone() } /// `init()` creates some boilerplate files and directories to get you started with your book. /// /// ```text - /// book-test/ + /// book-example/ /// ├── book /// └── src /// ├── chapter_1.md @@ -126,358 +147,348 @@ impl<'a> MDBook<'a> { /// /// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a /// `chapter_1.md` to the source directory. - pub fn init(&mut self) -> Result<(), Box> { debug!("[fn]: init"); - if !self.root.exists() { - fs::create_dir_all(&self.root).unwrap(); - info!("{:?} created", &self.root); + if !self.project_root.exists() { + fs::create_dir_all(&self.project_root).unwrap(); + info!("{:?} created", &self.project_root); } - { - - if !self.dest.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.dest); - try!(fs::create_dir_all(&self.dest)); - } - - if !self.src.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.src); - try!(fs::create_dir_all(&self.src)); - } - - let summary = self.src.join("SUMMARY.md"); - - if !summary.exists() { - - // Summary does not exist, create it - - debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md")); - let mut f = try!(File::create(&self.src.join("SUMMARY.md"))); - - debug!("[*]: Writing to SUMMARY.md"); - - try!(writeln!(f, "# Summary")); - try!(writeln!(f, "")); - try!(writeln!(f, "- [Chapter 1](./chapter_1.md)")); - } - } - - // parse SUMMARY.md, and create the missing item related file - try!(self.parse_summary()); - - debug!("[*]: constructing paths for missing files"); - for item in self.iter() { - debug!("[*]: item: {:?}", item); - match *item { - BookItem::Spacer => continue, - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) => { - if ch.path != PathBuf::new() { - let path = self.src.join(&ch.path); - - if !path.exists() { - debug!("[*]: {:?} does not exist, trying to create file", path); - try!(::std::fs::create_dir_all(path.parent().unwrap())); - let mut f = try!(File::create(path)); - - // debug!("[*]: Writing to {:?}", path); - try!(writeln!(f, "# {}", ch.name)); - } - } - }, - } - } + // Read book.toml if exists and populate .translations + self.read_config(); debug!("[*]: init done"); Ok(()) } - pub fn create_gitignore(&self) { - let gitignore = self.get_gitignore(); - - if !gitignore.exists() { - // Gitignore does not exist, create it - - // Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it - // is not, `strip_prefix` will return an Error. - if !self.get_dest().starts_with(&self.root) { - return; - } - - let relative = self.get_dest() - .strip_prefix(&self.root) - .expect("Destination is not relative to root."); - let relative = relative.to_str() - .expect("Path could not be yielded into a string slice."); - - debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore); - - let mut f = File::create(&gitignore).expect("Could not create file."); - - debug!("[*]: Writing to .gitignore"); - - writeln!(f, "{}", relative).expect("Could not write to file."); - } - } - - /// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to - /// construct the book's structure in the form of a `Vec` and then calls `render()` - /// method of the current renderer. - /// - /// It is the renderer who generates all the output files. - pub fn build(&mut self) -> Result<(), Box> { - debug!("[fn]: build"); - - try!(self.init()); - - // Clean output directory - try!(utils::fs::remove_dir_content(&self.dest)); - - try!(self.renderer.render(&self)); - - Ok(()) - } - - - pub fn get_gitignore(&self) -> PathBuf { - self.root.join(".gitignore") - } - - pub fn copy_theme(&self) -> Result<(), Box> { - debug!("[fn]: copy_theme"); - - let theme_dir = self.src.join("theme"); - - if !theme_dir.exists() { - debug!("[*]: {:?} does not exist, trying to create directory", theme_dir); - try!(fs::create_dir(&theme_dir)); - } - - // index.hbs - let mut index = try!(File::create(&theme_dir.join("index.hbs"))); - try!(index.write_all(theme::INDEX)); - - // book.css - let mut css = try!(File::create(&theme_dir.join("book.css"))); - try!(css.write_all(theme::CSS)); - - // favicon.png - let mut favicon = try!(File::create(&theme_dir.join("favicon.png"))); - try!(favicon.write_all(theme::FAVICON)); - - // book.js - let mut js = try!(File::create(&theme_dir.join("book.js"))); - try!(js.write_all(theme::JS)); - - // highlight.css - let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css"))); - try!(highlight_css.write_all(theme::HIGHLIGHT_CSS)); - - // highlight.js - let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js"))); - try!(highlight_js.write_all(theme::HIGHLIGHT_JS)); - - Ok(()) - } - - /// Parses the `book.json` file (if it exists) to extract the configuration parameters. - /// The `book.json` file should be in the root directory of the book. - /// The root directory is the one specified when creating a new `MDBook` + /// Parses the `book.toml` file (if it exists) to extract the configuration parameters. + /// The `book.toml` file should be in the root directory of the book project. + /// The project root directory is the one specified when creating a new `MDBook` /// /// ```no_run /// # extern crate mdbook; /// # use mdbook::MDBook; /// # use std::path::Path; /// # fn main() { - /// let mut book = MDBook::new(Path::new("root_dir")); + /// let mut book = MDBook::new(Path::new("project_root_dir")); /// # } /// ``` /// - /// In this example, `root_dir` will be the root directory of our book and is specified in function + /// In this example, `project_root_dir` will be the root directory of our book and is specified in function /// of the current working directory by using a relative path instead of an absolute path. + pub fn read_config(&mut self) -> &mut Self { - pub fn read_config(mut self) -> Self { + debug!("[fn]: read_config"); - let config = BookConfig::new(&self.root) - .read_config(&self.root) - .to_owned(); + // TODO refactor to a helper that returns Result? - self.title = config.title; - self.description = config.description; - self.author = config.author; + // TODO Maybe some error handling instead of exit(2), although it is a + // clear indication for the user that something is wrong and we can't + // fix it for them. - self.dest = config.dest; - self.src = config.src; - self.theme_path = config.theme_path; + let read_file = |path: PathBuf| -> String { + let mut data = String::new(); + let mut f: File = match File::open(&path) { + Ok(x) => x, + Err(_) => { + error!("[*]: Failed to open {:?}", &path); + exit(2); + } + }; + if let Err(_) = f.read_to_string(&mut data) { + error!("[*]: Failed to read {:?}", &path); + exit(2); + } + data + }; + + // Read book.toml or book.json if exists to a BTreeMap + + if Path::new(self.project_root.join("book.toml").as_os_str()).exists() { + + debug!("[*]: Reading config"); + let text = read_file(self.project_root.join("book.toml")); + + match utils::toml_str_to_btreemap(&text) { + Ok(x) => {self.parse_from_btreemap(&x);}, + Err(e) => { + error!("[*] Errors while parsing TOML: {:?}", e); + exit(2); + } + } + + } else if Path::new(self.project_root.join("book.json").as_os_str()).exists() { + + debug!("[*]: Reading config"); + let text = read_file(self.project_root.join("book.json")); + + match utils::json_str_to_btreemap(&text) { + Ok(x) => {self.parse_from_btreemap(&x);}, + Err(e) => { + error!("[*] Errors while parsing JSON: {:?}", e); + exit(2); + } + } + + } else { + debug!("[*]: No book.toml or book.json was found, using defaults."); + } self } - /// You can change the default renderer to another one by using this method. The only requirement - /// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html) + /// Configures MDBook properties and translations. /// - /// ```no_run - /// extern crate mdbook; - /// use mdbook::MDBook; - /// use mdbook::renderer::HtmlHandlebars; - /// # use std::path::Path; + /// After parsing properties for MDBook struct, it removes them from the + /// config (template_dir, livereload, etc.). The remaining keys on the main + /// block will be interpreted as properties of the main book. /// - /// fn main() { - /// let mut book = MDBook::new(Path::new("mybook")) - /// .set_renderer(Box::new(HtmlHandlebars::new())); + /// `project_root` is ignored. /// - /// // In this example we replace the default renderer by the default renderer... - /// // Don't forget to put your renderer in a Box - /// } - /// ``` - /// - /// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()` + /// - dest_base + /// - render_intent + /// - template_dir + /// - indent_spaces + /// - livereload + pub fn parse_from_btreemap(&mut self, conf: &BTreeMap) -> &mut Self { - pub fn set_renderer(mut self, renderer: Box) -> Self { - self.renderer = renderer; - self - } + let mut config = conf.clone(); - pub fn test(&mut self) -> Result<(), Box> { - // read in the chapters - try!(self.parse_summary()); - for item in self.iter() { + if config.contains_key("project_root") { + config.remove("project_root"); + } - match *item { - BookItem::Chapter(_, ref ch) => { - if ch.path != PathBuf::new() { + if let Some(a) = config.get("dest_base") { + self.set_dest_base(&PathBuf::from(&a.to_string())); + } + config.remove("dest_base"); - let path = self.get_src().join(&ch.path); - - println!("[*]: Testing file: {:?}", path); - - let output_result = Command::new("rustdoc") - .arg(&path) - .arg("--test") - .output(); - let output = try!(output_result); - - if !output.status.success() { - return Err(Box::new(io::Error::new(ErrorKind::Other, format!( - "{}\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr)))) as Box); - } - } - }, - _ => {}, + if let Some(a) = config.get("render_intent") { + if a.to_string() == "html".to_string() { + self.set_render_intent(RenderIntent::HtmlHandlebars); + } else { + // offer some real choices later on... + self.set_render_intent(RenderIntent::HtmlHandlebars); } } - Ok(()) - } + config.remove("render_intent"); - pub fn get_root(&self) -> &Path { - &self.root - } + // Parsing template_dir must be after render_intent, otherwise + // .set_render_intent() will always override template_dir with its + // default setting. + if let Some(a) = config.get("template_dir") { + self.set_template_dir(&PathBuf::from(&a.to_string())); + } + config.remove("template_dir"); - pub fn set_dest(mut self, dest: &Path) -> Self { + if let Some(a) = config.get("indent_spaces") { + if let Some(b) = a.as_integer() { + self.indent_spaces = b as i32; + } + } + config.remove("indent_spaces"); - // Handle absolute and relative paths - match dest.is_absolute() { - true => { - self.dest = dest.to_owned(); - }, - false => { - let dest = self.root.join(dest).to_owned(); - self.dest = dest; - }, + if let Some(a) = config.get("livereload") { + if let Some(b) = a.as_bool() { + self.livereload = b; + } + } + config.remove("livereload"); + + // If there is a 'translations' table, configugre each book from that. + // If there isn't, take the rest of the config as one book. + + // If there is only one book, leave its source and destination folder as + // the default `./src` and `./book`. If there are more, join their hash + // keys to the default source and destination folder such as `/src/en` + // and `./book/en`. This may be overridden if set specifically. + + if let Some(a) = config.get("translations") { + if let Some(b) = a.as_table() { + + let is_multilang: bool = b.iter().count() > 1; + + for (key, conf) in b.iter() { + let mut book = Book::new(&self.project_root); + + if let Some(c) = conf.as_slice() { + if let Some(d) = c[0].as_table() { + if is_multilang { + book.config.src = book.config.src.join(key); + book.config.dest = book.config.dest.join(key); + } + book.config.is_multilang = is_multilang; + book.config.parse_from_btreemap(&d); + self.translations.insert(key.to_owned(), book); + } + } + } + } + } else { + let mut book = Book::new(&self.project_root); + + book.config.parse_from_btreemap(&config); + let key = book.config.language.code.clone(); + self.translations.insert(key, book); } self } - pub fn get_dest(&self) -> &Path { - &self.dest - } + pub fn parse_books(&mut self) -> &mut Self { + debug!("[fn]: parse_books"); - pub fn set_src(mut self, src: &Path) -> Self { + for key in self.translations.clone().keys() { + if let Some(mut b) = self.translations.clone().get_mut(key) { - // Handle absolute and relative paths - match src.is_absolute() { - true => { - self.src = src.to_owned(); - }, - false => { - let src = self.root.join(src).to_owned(); - self.src = src; - }, + // TODO error handling could be better here + + let first_as_index = match self.render_intent { + RenderIntent::HtmlHandlebars => true, + }; + + match b.parse_or_create_summary_file(first_as_index) { + Ok(_) => {}, + Err(e) => {println!("{}", e);}, + } + + match b.parse_or_create_chapter_files() { + Ok(_) => {}, + Err(e) => {println!("{}", e);}, + } + + self.translations.remove(key); + self.translations.insert(key.to_owned(), b.clone()); + } } self } - pub fn get_src(&self) -> &Path { - &self.src + pub fn get_project_root(&self) -> &Path { + &self.project_root } - pub fn set_title(mut self, title: &str) -> Self { - self.title = title.to_owned(); - self - } - - pub fn get_title(&self) -> &str { - &self.title - } - - pub fn set_author(mut self, author: &str) -> Self { - self.author = author.to_owned(); - self - } - - pub fn get_author(&self) -> &str { - &self.author - } - - pub fn set_description(mut self, description: &str) -> Self { - self.description = description.to_owned(); - self - } - - pub fn get_description(&self) -> &str { - &self.description - } - - pub fn set_livereload(&mut self, livereload: String) -> &mut Self { - self.livereload = Some(livereload); - self - } - - pub fn unset_livereload(&mut self) -> &Self { - self.livereload = None; - self - } - - pub fn get_livereload(&self) -> Option<&String> { - match self.livereload { - Some(ref livereload) => Some(&livereload), - None => None, + pub fn set_project_root(&mut self, path: &PathBuf) -> &mut MDBook { + if path.is_absolute() { + self.project_root = path.to_owned(); + } else { + self.project_root = env::current_dir().unwrap().join(path).to_owned(); } - } - - pub fn set_theme_path(mut self, theme_path: &Path) -> Self { - self.theme_path = match theme_path.is_absolute() { - true => theme_path.to_owned(), - false => self.root.join(theme_path).to_owned(), - }; self } - pub fn get_theme_path(&self) -> &Path { - &self.theme_path + pub fn get_template_dir(&self) -> PathBuf { + self.project_root.join(&self.template_dir) } - // Construct book - fn parse_summary(&mut self) -> Result<(), Box> { - // When append becomes stable, use self.content.append() ... - self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md"))); - Ok(()) + pub fn set_template_dir(&mut self, path: &PathBuf) -> &mut MDBook { + if path.as_os_str() == OsStr::new(".") { + self.template_dir = PathBuf::from("".to_string()); + } else { + self.template_dir = path.to_owned(); + } + self } + + pub fn get_dest_base(&self) -> PathBuf { + self.project_root.join(&self.dest_base) + } + + pub fn set_dest_base(&mut self, path: &PathBuf) -> &mut MDBook { + if path.as_os_str() == OsStr::new(".") { + self.dest_base = PathBuf::from("".to_string()); + } else { + self.dest_base = path.to_owned(); + } + self + } + + pub fn get_render_intent(&self) -> &RenderIntent { + &self.render_intent + } + + pub fn set_render_intent(&mut self, intent: RenderIntent) -> &mut MDBook { + self.render_intent = intent; + match self.render_intent { + RenderIntent::HtmlHandlebars => { + self.set_template_dir(&PathBuf::from("assets").join("html-template")); + }, + } + self + } + + // TODO update + + // pub fn test(&mut self) -> Result<(), Box> { + // // read in the chapters + // try!(self.parse_summary()); + // for item in self.iter() { + + // match *item { + // BookItem::Chapter(_, ref ch) => { + // if ch.path != PathBuf::new() { + + // let path = self.get_src().join(&ch.path); + + // println!("[*]: Testing file: {:?}", path); + + // let output_result = Command::new("rustdoc") + // .arg(&path) + // .arg("--test") + // .output(); + // let output = try!(output_result); + + // if !output.status.success() { + // return Err(Box::new(io::Error::new(ErrorKind::Other, format!( + // "{}\n{}", + // String::from_utf8_lossy(&output.stdout), + // String::from_utf8_lossy(&output.stderr)))) as Box); + // } + // } + // }, + // _ => {}, + // } + // } + // Ok(()) + // } + + // /// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html): + // /// `(section: String, bookitem: &BookItem)` + // /// + // /// ```no_run + // /// # extern crate mdbook; + // /// # use mdbook::MDBook; + // /// # use mdbook::BookItem; + // /// # use std::path::Path; + // /// # fn main() { + // /// # let mut book = MDBook::new(Path::new("mybook")); + // /// for item in book.iter() { + // /// match item { + // /// &BookItem::Chapter(ref section, ref chapter) => {}, + // /// &BookItem::Affix(ref chapter) => {}, + // /// &BookItem::Spacer => {}, + // /// } + // /// } + // /// + // /// // would print something like this: + // /// // 1. Chapter 1 + // /// // 1.1 Sub Chapter + // /// // 1.2 Sub Chapter + // /// // 2. Chapter 2 + // /// // + // /// // etc. + // /// # } + // /// ``` + + // pub fn iter(&self) -> BookItems { + // BookItems { + // items: &self.content[..], + // current_index: 0, + // stack: Vec::new(), + // } + // } + } diff --git a/src/book/toc.rs b/src/book/toc.rs new file mode 100644 index 00000000..84beaf74 --- /dev/null +++ b/src/book/toc.rs @@ -0,0 +1,76 @@ +use book::chapter::Chapter; + +/// A Table of Contents is a `Vec`, where an item is an enum that +/// qualifies its content. +#[derive(Debug, Clone)] +pub enum TocItem { + Numbered(TocContent), + Unnumbered(TocContent), + Unlisted(TocContent), + Spacer, +} + +/// An entry in the TOC with content. Its payload is the Chapter. This struct +/// knows the section index of the entry, or contains optional sub-entries as +/// `Vec`. +#[derive(Debug, Clone)] +pub struct TocContent { + pub chapter: Chapter, + pub sub_items: Option>, + /// Section indexes of the chapter + pub section: Option>, +} + +impl Default for TocContent { + fn default() -> TocContent { + TocContent { + chapter: Chapter::default(), + sub_items: None, + section: None, + } + } +} + +impl TocContent { + + pub fn new(chapter: Chapter) -> TocContent { + let mut toc = TocContent::default(); + toc.chapter = chapter; + toc + } + + pub fn new_with_section(chapter: Chapter, section: Vec) -> TocContent { + let mut toc = TocContent::default(); + toc.chapter = chapter; + toc.section = Some(section); + toc + } + + pub fn section_as_string(&self) -> String { + if let Some(ref sec) = self.section { + let a = sec.iter().map(|x| x.to_string()).collect::>(); + format!("{}.", a.join(".")) + } else { + "".to_string() + } + } + + // TODO update + + // /// This function takes a slice `&[x,y,z]` and returns the corresponding sub-chapter if it exists. + // /// + // /// For example: `chapter.get_sub_chapter(&[1,3])` will return the third sub-chapter of the first sub-chapter. + // pub fn get_sub_chapter(&self, section: &[usize]) -> Option<&Chapter> { + // match section.len() { + // 0 => None, + // 1 => self.sub_chapters.get(section[0]), + // _ => { + // // The lengt of the slice is more than one, this means that we want a sub-chapter of a sub-chapter + // // We call `get_sub_chapter` recursively until we are deep enough and return the asked sub-chapter + // self.sub_chapters + // .get(section[0]) + // .and_then(|ch| ch.get_sub_chapter(§ion[1..])) + // }, + // } + // } +} diff --git a/src/lib.rs b/src/lib.rs index f85de104..50d382f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,19 +69,23 @@ //! //! Make sure to take a look at it. +extern crate includedir; +extern crate phf; + +include!(concat!(env!("OUT_DIR"), "/data.rs")); + extern crate serde; extern crate serde_json; extern crate handlebars; extern crate pulldown_cmark; +extern crate regex; +extern crate glob; #[macro_use] extern crate log; pub mod book; mod parse; pub mod renderer; -pub mod theme; pub mod utils; +pub mod tests; pub use book::MDBook; -pub use book::BookItem; -pub use book::BookConfig; -pub use renderer::Renderer; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index c8c8aab7..3f7b5b6c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,3 +1,3 @@ -pub use self::summary::construct_bookitems; +pub use self::summary::construct_tocitems; pub mod summary; diff --git a/src/parse/summary.rs b/src/parse/summary.rs index a4c87a9c..bbc42806 100644 --- a/src/parse/summary.rs +++ b/src/parse/summary.rs @@ -1,26 +1,41 @@ use std::path::PathBuf; use std::fs::File; use std::io::{Read, Result, Error, ErrorKind}; -use book::bookitem::{BookItem, Chapter}; -pub fn construct_bookitems(path: &PathBuf) -> Result> { - debug!("[fn]: construct_bookitems"); +use book::chapter::Chapter; +use book::toc::{TocItem, TocContent}; + +pub fn construct_tocitems(summary_path: &PathBuf, first_as_index: bool) -> Result> { + debug!("[fn]: construct_tocitems"); let mut summary = String::new(); - try!(try!(File::open(path)).read_to_string(&mut summary)); + try!(try!(File::open(summary_path)).read_to_string(&mut summary)); debug!("[*]: Parse SUMMARY.md"); - let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0])); + + let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0], first_as_index)); debug!("[*]: Done parsing SUMMARY.md"); Ok(top_items) } -fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec) -> Result> { +pub fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec, first_as_index: bool) -> Result> { debug!("[fn]: parse_level"); - let mut items: Vec = vec![]; + let mut items: Vec = vec![]; + + let mut found_first = false; + + let ohnoes = r#"Your SUMMARY.md is messed up + +Unnumbered and Spacer items can only exist on the root level. + +Unnumbered items can only exist before or after Numbered items, since these +items are in the frontmatter of a book. + +There can be no Numbered items after Unnumbered items, as they are in the +backmatter."#; // Construct the book recursively while !summary.is_empty() { - let item: BookItem; + let item: TocItem; // Indentation level of the line to parse let level = try!(level(summary[0], 4)); @@ -35,58 +50,58 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec { + let sec = section.clone(); + a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false))); + items.push(TocItem::Numbered(a)); - // Remove the last number from the section, because we got back to our level.. - section.pop(); - continue; - } else { - return Err(Error::new(ErrorKind::Other, - format!("Your summary.md is messed up\n\n - Prefix, \ - Suffix and Spacer elements can only exist on the root level.\n - \ - Prefix elements can only exist before any chapter and there can be \ - no chapters after suffix elements."))); + // Remove the last number from the section, because we got + // back to our level... + section.pop(); + continue; + }, + TocItem::Unnumbered(mut a) => { + let sec = section.clone(); + a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false))); + items.push(TocItem::Unnumbered(a)); + section.pop(); + continue; + }, + TocItem::Unlisted(mut a) => { + let sec = section.clone(); + a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false))); + items.push(TocItem::Unlisted(a)); + section.pop(); + continue; + }, + _ => { + return Err(Error::new(ErrorKind::Other, ohnoes)); + } }; } else { // level and current_level are the same, parse the line item = if let Some(parsed_item) = parse_line(summary[0]) { - // Eliminate possible errors and set section to -1 after suffix + // Eliminate possible errors and set section to -1 after unnumbered match parsed_item { - // error if level != 0 and BookItem is != Chapter - BookItem::Affix(_) | BookItem::Spacer if level > 0 => { - return Err(Error::new(ErrorKind::Other, - format!("Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements can only exist on the \ - root level.\n - Prefix \ - elements can only exist before any chapter and there can be \ - no chapters after suffix elements."))) + + // error if level != 0 and TocItem is != Numbered + TocItem::Unnumbered(_) | TocItem::Spacer if level > 0 => { + return Err(Error::new(ErrorKind::Other, ohnoes)) }, - // error if BookItem == Chapter and section == -1 - BookItem::Chapter(_, _) if section[0] == -1 => { - return Err(Error::new(ErrorKind::Other, - format!("Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements can only exist on the \ - root level.\n - Prefix \ - elements can only exist before any chapter and there can be \ - no chapters after suffix elements."))) + // error if TocItem == Numbered or Unlisted and section == -1 + TocItem::Numbered(_) | TocItem::Unlisted(_) if section[0] == -1 => { + return Err(Error::new(ErrorKind::Other, ohnoes)) }, - // Set section = -1 after suffix - BookItem::Affix(_) if section[0] > 0 => { + // Set section = -1 after unnumbered + TocItem::Unnumbered(_) if section[0] > 0 => { section[0] = -1; }, @@ -94,12 +109,14 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec { + TocItem::Numbered(mut content) => { // Increment section let len = section.len() - 1; section[len] += 1; - let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + "."); - BookItem::Chapter(s, ch) + + content.section = Some(section.clone()); + + TocItem::Numbered(content) }, _ => parsed_item, } @@ -112,13 +129,33 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec { + found_first = true; + content.chapter.dest_path = Some(PathBuf::from("index.html".to_string())); + TocItem::Numbered(content) + }, + TocItem::Unnumbered(mut content) => { + found_first = true; + content.chapter.dest_path = Some(PathBuf::from("index.html".to_string())); + TocItem::Unnumbered(content) + }, + TocItem::Unlisted(content) => { + TocItem::Unlisted(content) + }, + TocItem::Spacer => TocItem::Spacer, + }; + items.push(i); + } else { + items.push(item); + } } debug!("[*]: Level: {:?}", items); Ok(items) } - fn level(line: &str, spaces_in_tab: i32) -> Result { debug!("[fn]: level"); let mut spaces = 0; @@ -147,8 +184,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result { Ok(level) } - -fn parse_line(l: &str) -> Option { +fn parse_line(l: &str) -> Option { debug!("[fn]: parse_line"); // Remove leading and trailing spaces or tabs @@ -157,7 +193,7 @@ fn parse_line(l: &str) -> Option { // Spacers are "------" if line.starts_with("--") { debug!("[*]: Line is spacer"); - return Some(BookItem::Spacer); + return Some(TocItem::Spacer); } if let Some(c) = line.chars().nth(0) { @@ -166,8 +202,9 @@ fn parse_line(l: &str) -> Option { '-' | '*' => { debug!("[*]: Line is list element"); - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path))); + if let Some((title, path)) = read_link(line) { + let chapter = Chapter::new(title, path); + return Some(TocItem::Numbered(TocContent::new(chapter))); } else { return None; } @@ -176,8 +213,9 @@ fn parse_line(l: &str) -> Option { '[' => { debug!("[*]: Line is a link element"); - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Affix(Chapter::new(name, path))); + if let Some((title, path)) = read_link(line) { + let chapter = Chapter::new(title, path); + return Some(TocItem::Unnumbered(TocContent::new(chapter))); } else { return None; } @@ -209,7 +247,7 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> { return None; } - let name = line[start_delimitor + 1..end_delimitor].to_owned(); + let title = line[start_delimitor + 1..end_delimitor].to_owned(); start_delimitor = end_delimitor + 1; if let Some(i) = line[start_delimitor..].find(')') { @@ -221,5 +259,5 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> { let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned()); - Some((name, path)) + Some((title, path)) } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 27fb214e..a9f5eb0e 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,10 +1,13 @@ use renderer::html_handlebars::helpers; use renderer::Renderer; -use book::MDBook; -use book::bookitem::BookItem; -use {utils, theme}; +use book::{MDBook, Book}; +use book::chapter::Chapter; +use book::toc::{TocItem, TocContent}; +use utils; +use FILES; use std::path::{Path, PathBuf}; +use std::ffi::OsStr; use std::fs::{self, File}; use std::error::Error; use std::io::{self, Read, Write}; @@ -15,7 +18,6 @@ use handlebars::Handlebars; use serde_json; use serde_json::value::ToJson; - pub struct HtmlHandlebars; impl HtmlHandlebars { @@ -25,308 +27,390 @@ impl HtmlHandlebars { } impl Renderer for HtmlHandlebars { - fn render(&self, book: &MDBook) -> Result<(), Box> { + + /// Prepares the project and calls `render()`. + fn build(&self, project_root: &PathBuf) -> Result<(), Box> { + debug!("[fn]: build"); + + let mut book_project = MDBook::new(&project_root); + + book_project.read_config(); + book_project.parse_books(); + + // Clean output directory + try!(utils::fs::remove_dir_content(&book_project.get_dest_base())); + + try!(self.render(&book_project)); + + Ok(()) + } + + /// Renders the chapters and copies static assets. + fn render(&self, book_project: &MDBook) -> Result<(), Box> { + + debug!("[*]: Check if book's base output folder exists"); + if let Err(_) = fs::create_dir_all(&book_project.get_dest_base()) { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "Unexpected error when constructing path") + )); + } + + // TODO write print version html + + // TODO livereload + + // Copy book's static assets + + if book_project.get_project_root().join("assets").exists() { + + let a = book_project.get_project_root().join("assets"); + let base = a.to_str().unwrap(); + + let b = a.join("**").join("*"); + let include_glob = b.to_str().unwrap(); + + let c = a.join("_*"); + let exclude_glob = c.to_str().unwrap(); + + // anyone wants to catch errors? + utils::fs::copy_files(include_glob, + base, + vec![exclude_glob], + &book_project.get_dest_base()); + } + + // Copy template's static assets + + // If there is a template dir in the books's project folder, copy asset + // files from there, otherwise copy from embedded assets. + + if book_project.get_template_dir().exists() { + + let a = book_project.get_template_dir(); + let base = a.to_str().unwrap(); + + let b = a.join("**").join("*"); + let include_glob = b.to_str().unwrap(); + + let c = a.join("_*"); + let exclude_glob = c.to_str().unwrap(); + + // don't try!(), copy_files() will send error values when trying to copy folders that are part of the file glob + // + // Error { + // repr: Custom( + // Custom { + // kind: Other, + // error: StringError( + // "Err(Error { repr: Custom(Custom { kind: InvalidInput, error: StringError(\"the source path is not an existing regular file\") }) })\n" + // ) + // } + // ) + // } + + // anyone wants to catch errors? + utils::fs::copy_files(include_glob, + base, + vec![exclude_glob], + &book_project.get_dest_base()); + + } else { + try!(utils::fs::copy_data("data/html-template/**/*", + "data/html-template/", + vec!["data/html-template/_*"], + &book_project.get_dest_base())); + } + debug!("[fn]: render"); let mut handlebars = Handlebars::new(); - // Load theme - let theme = theme::Theme::new(book.get_theme_path()); + // Render the chapters of each book + for (key, book) in &book_project.translations { - // Register template - debug!("[*]: Register handlebars template"); - try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index)))); + // Read in the page template + let tmpl_path: &PathBuf = &book_project.get_template_dir().join("_layouts").join("page.hbs"); + let s = if tmpl_path.exists() { + try!(utils::fs::file_to_string(&tmpl_path)) + } else { + try!(utils::fs::get_data_file("data/html-template/_layouts/page.hbs")) + }; - // Register helpers - debug!("[*]: Register handlebars helpers"); - handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc)); - handlebars.register_helper("previous", Box::new(helpers::navigation::previous)); - handlebars.register_helper("next", Box::new(helpers::navigation::next)); + // Register template + debug!("[*]: Register handlebars template"); + try!(handlebars.register_template_string("page", s)); - let mut data = try!(make_data(book)); + // Register helpers + debug!("[*]: Register handlebars helpers"); + handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc)); + handlebars.register_helper("previous", Box::new(helpers::navigation::previous)); + handlebars.register_helper("next", Box::new(helpers::navigation::next)); - // Print version - let mut print_content: String = String::new(); + // Check if book's dest directory exists - // Check if dest directory exists - debug!("[*]: Check if destination directory exists"); - if let Err(_) = fs::create_dir_all(book.get_dest()) { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, - "Unexpected error when constructing destination path"))); - } + // If this is a single book, config.dest default is + // `project_root/book`, and the earlier check will cover this. - // Render a file for every entry in the book - let mut index = true; - for item in book.iter() { + // If this is multi-language book, config.dest will + // `project_book/book/key`, and so we check here for each book. - match *item { - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) => { - if ch.path != PathBuf::new() { + debug!("[*]: Check if book's destination directory exists"); + if let Err(_) = fs::create_dir_all(book.config.get_dest()) { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "Unexpected error when constructing destination path") + )); + } - let path = book.get_src().join(&ch.path); + // If this is the main book of a multi-language book, add an + // index.html to the project dest folder - debug!("[*]: Opening file: {:?}", path); - let mut f = try!(File::open(&path)); - let mut content: String = String::new(); + if book.config.is_multilang && book.config.is_main_book { + match book.toc[0] { + TocItem::Numbered(ref i) | + TocItem::Unnumbered(ref i) | + TocItem::Unlisted(ref i) => { + let mut chapter: Chapter = i.chapter.clone(); + chapter.dest_path = Some(PathBuf::from("index.html".to_string())); - debug!("[*]: Reading file"); - try!(f.read_to_string(&mut content)); + // almost the same as process_chapter(), but we have to + // manipulate path_to_root in data and rendered_path + + let mut content = try!(chapter.read_content_using(&book.config.src)); // Parse for playpen links - if let Some(p) = path.parent() { + if let Some(p) = book.config.get_src().join(&chapter.path).parent() { content = helpers::playpen::render_playpen(&content, p); } - // Render markdown using the pulldown-cmark crate - content = utils::render_markdown(&content); - print_content.push_str(&content); + let mut data = try!(make_data(&book, &chapter, &content)); - // Remove content from previous file and render content for this one - data.remove("path"); - match ch.path.to_str() { - Some(p) => { - data.insert("path".to_owned(), p.to_json()); - }, - None => { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, - "Could not convert path to str"))) - }, - } - - - // Remove content from previous file and render content for this one - data.remove("content"); - data.insert("content".to_owned(), content.to_json()); - - // Remove path to root from previous file and render content for this one data.remove("path_to_root"); - data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&ch.path).to_json()); + data.insert("path_to_root".to_owned(), "".to_json()); // Rendere the handlebars template with the data debug!("[*]: Render template"); - let rendered = try!(handlebars.render("index", &data)); + let rendered_content = try!(handlebars.render("page", &data)); + + let p = chapter.dest_path.unwrap(); + let rendered_path = &book_project.get_dest_base().join(&p); + + debug!("[*]: Create file {:?}", rendered_path); - debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html")); // Write to file - let mut file = - try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html"))); - info!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html")); + let mut file = try!(utils::fs::create_file(rendered_path)); + info!("[*] Creating {:?} ✓", rendered_path); - try!(file.write_all(&rendered.into_bytes())); - - // Create an index.html from the first element in SUMMARY.md - if index { - debug!("[*]: index.html"); - - let mut index_file = try!(File::create(book.get_dest().join("index.html"))); - let mut content = String::new(); - let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html")))) - .read_to_string(&mut content); - - // This could cause a problem when someone displays code containing - // on the front page, however this case should be very very rare... - content = content.lines() - .filter(|line| !line.contains(" {}, + try!(file.write_all(&rendered_content.into_bytes())); + }, + TocItem::Spacer => {}, + } } + + // Render a file for every entry in the book + try!(self.process_items(&book.toc, &book, &handlebars)); } - // Print version - - // Remove content from previous file and render content for this one - data.remove("path"); - data.insert("path".to_owned(), "print.md".to_json()); - - // Remove content from previous file and render content for this one - data.remove("content"); - data.insert("content".to_owned(), print_content.to_json()); - - // Remove path to root from previous file and render content for this one - data.remove("path_to_root"); - data.insert("path_to_root".to_owned(), utils::fs::path_to_root(Path::new("print.md")).to_json()); - - // Rendere the handlebars template with the data - debug!("[*]: Render template"); - let rendered = try!(handlebars.render("index", &data)); - let mut file = try!(utils::fs::create_file(&book.get_dest().join("print").with_extension("html"))); - try!(file.write_all(&rendered.into_bytes())); - info!("[*] Creating print.html ✓"); - - // Copy static files (js, css, images, ...) - - debug!("[*] Copy static files"); - // JavaScript - let mut js_file = if let Ok(f) = File::create(book.get_dest().join("book.js")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.js"))); - }; - try!(js_file.write_all(&theme.js)); - - // Css - let mut css_file = if let Ok(f) = File::create(book.get_dest().join("book.css")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.css"))); - }; - try!(css_file.write_all(&theme.css)); - - // Favicon - let mut favicon_file = if let Ok(f) = File::create(book.get_dest().join("favicon.png")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create favicon.png"))); - }; - try!(favicon_file.write_all(&theme.favicon)); - - // JQuery local fallback - let mut jquery = if let Ok(f) = File::create(book.get_dest().join("jquery.js")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create jquery.js"))); - }; - try!(jquery.write_all(&theme.jquery)); - - // syntax highlighting - let mut highlight_css = if let Ok(f) = File::create(book.get_dest().join("highlight.css")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.css"))); - }; - try!(highlight_css.write_all(&theme.highlight_css)); - - let mut tomorrow_night_css = if let Ok(f) = File::create(book.get_dest().join("tomorrow-night.css")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create tomorrow-night.css"))); - }; - try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css)); - - let mut highlight_js = if let Ok(f) = File::create(book.get_dest().join("highlight.js")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.js"))); - }; - try!(highlight_js.write_all(&theme.highlight_js)); - - // Font Awesome local fallback - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/css/font-awesome.css")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/fontawesome-webfont.eot")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_EOT)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/fontawesome-webfont.svg")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_SVG)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/fontawesome-webfont.ttf")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_TTF)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/fontawesome-webfont.woff")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/fontawesome-webfont.woff2")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2)); - let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest() - .join("_FontAwesome/fonts/FontAwesome.ttf")) { - f - } else { - return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf"))); - }; - try!(font_awesome.write_all(theme::FONT_AWESOME_TTF)); - - // Copy all remaining files - try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"])); - Ok(()) } } -fn make_data(book: &MDBook) -> Result, Box> { +impl HtmlHandlebars { + + fn process_items(&self, + items: &Vec, + book: &Book, + handlebars: &Handlebars) + -> Result<(), Box> { + + for item in items.iter() { + match *item { + TocItem::Numbered(ref i) | + TocItem::Unnumbered(ref i) | + TocItem::Unlisted(ref i) => { + // FIXME chapters with path "" are interpreted as draft now, + // not rendered here, and displayed gray in the TOC. Either + // path should be instead an Option or all chapter output + // should be used from setting dest_path, which is already + // Option but currently only used for rendering a chapter as + // index.html. + if i.chapter.path.as_os_str().len() > 0 { + try!(self.process_chapter(&i.chapter, book, handlebars)); + } + + if let Some(ref subs) = i.sub_items { + try!(self.process_items(&subs, book, handlebars)); + } + + }, + _ => {}, + } + } + + Ok(()) + } + + fn process_chapter(&self, + chapter: &Chapter, + book: &Book, + handlebars: &Handlebars) + -> Result<(), Box> { + + let mut content = try!(chapter.read_content_using(&book.config.src)); + + // Parse for playpen links + if let Some(p) = book.config.get_src().join(&chapter.path).parent() { + content = helpers::playpen::render_playpen(&content, p); + } + + let data = try!(make_data(book, chapter, &content)); + + // Rendere the handlebars template with the data + debug!("[*]: Render template"); + let rendered_content = try!(handlebars.render("page", &data)); + + let p = match chapter.dest_path.clone() { + Some(x) => x, + None => chapter.path.with_extension("html") + }; + + let rendered_path = &book.config.get_dest().join(&p); + + debug!("[*]: Create file {:?}", rendered_path); + + // Write to file + let mut file = try!(utils::fs::create_file(rendered_path)); + info!("[*] Creating {:?} ✓", rendered_path); + + try!(file.write_all(&rendered_content.into_bytes())); + + Ok(()) + } +} + +fn make_data(book: &Book, + chapter: &Chapter, + content: &str) + -> Result, Box> { + debug!("[fn]: make_data"); let mut data = serde_json::Map::new(); + + // Book data + data.insert("language".to_owned(), "en".to_json()); - data.insert("title".to_owned(), book.get_title().to_json()); - data.insert("description".to_owned(), book.get_description().to_json()); - data.insert("favicon".to_owned(), "favicon.png".to_json()); - if let Some(livereload) = book.get_livereload() { - data.insert("livereload".to_owned(), livereload.to_json()); + data.insert("title".to_owned(), book.config.title.to_json()); + data.insert("description".to_owned(), book.config.description.to_json()); + + // Chapter data + + let mut path = if let Some(ref dest_path) = chapter.dest_path { + PathBuf::from(dest_path) + } else { + chapter.path.clone() + }; + + if book.config.is_multilang && path.as_os_str().len() > 0 { + let p = PathBuf::from(&book.config.language.code); + path = p.join(path); } - let mut chapters = vec![]; - - for item in book.iter() { - // Create the data to inject in the template - let mut chapter = BTreeMap::new(); - - match *item { - BookItem::Affix(ref ch) => { - chapter.insert("name".to_owned(), ch.name.to_json()); - match ch.path.to_str() { - Some(p) => { - chapter.insert("path".to_owned(), p.to_json()); - }, - None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))), - } - }, - BookItem::Chapter(ref s, ref ch) => { - chapter.insert("section".to_owned(), s.to_json()); - chapter.insert("name".to_owned(), ch.name.to_json()); - match ch.path.to_str() { - Some(p) => { - chapter.insert("path".to_owned(), p.to_json()); - }, - None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))), - } - }, - BookItem::Spacer => { - chapter.insert("spacer".to_owned(), "_spacer_".to_json()); - }, - - } - - chapters.push(chapter); + match path.to_str() { + Some(p) => { + data.insert("path".to_owned(), p.to_json()); + }, + None => { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "Could not convert path to str") + )) + }, } + data.insert("content".to_owned(), content.to_json()); + + data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&path).to_json()); + + let chapters = try!(items_to_chapters(&book.toc, &book)); + data.insert("chapters".to_owned(), chapters.to_json()); - debug!("[*]: JSON constructed"); Ok(data) } + +fn items_to_chapters(items: &Vec, book: &Book) + -> Result>, Box> { + + let mut chapters = vec![]; + + for item in items.iter() { + + match *item { + TocItem::Numbered(ref i) | + TocItem::Unnumbered(ref i) => { + match process_chapter_and_subs(i, book) { + Ok(mut x) => chapters.append(&mut x), + Err(e) => return Err(e), + } + }, + TocItem::Spacer => { + let mut chapter = serde_json::Map::new(); + chapter.insert("spacer".to_owned(), "_spacer_".to_json()); + chapters.push(chapter); + }, + TocItem::Unlisted(_) => {}, + } + } + + Ok(chapters) +} + +fn process_chapter_and_subs(i: &TocContent, book: &Book) + -> Result>, Box> { + + let mut chapters = vec![]; + + // Create the data to inject in the template + let mut chapter = serde_json::Map::new(); + let ch = &i.chapter; + + if let Some(_) = i.section { + let s = i.section_as_string(); + chapter.insert("section".to_owned(), s.to_json()); + } + + chapter.insert("title".to_owned(), ch.title.to_json()); + + let mut path = if let Some(ref dest_path) = ch.dest_path { + PathBuf::from(dest_path) + } else { + ch.path.clone() + }; + + if book.config.is_multilang && path.as_os_str().len() > 0 { + let p = PathBuf::from(&book.config.language.code); + path = p.join(path); + } + + match path.to_str() { + Some(p) => { + chapter.insert("path".to_owned(), p.to_json()); + }, + None => { + return Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "Could not convert path to str") + )) + }, + } + + chapters.push(chapter); + + if let Some(ref subs) = i.sub_items { + let mut sub_chs = try!(items_to_chapters(&subs, book)); + chapters.append(&mut sub_chs); + } + + Ok(chapters) +} diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs index 58c90100..d0e132d9 100644 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ b/src/renderer/html_handlebars/helpers/navigation.rs @@ -47,7 +47,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext let mut previous_chapter = BTreeMap::new(); // Chapter title - match previous.get("name") { + match previous.get("title") { Some(n) => { debug!("[*]: Inserting title: {}", n); previous_chapter.insert("title".to_owned(), n.to_json()) @@ -105,9 +105,6 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext Ok(()) } - - - pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { debug!("[fn]: next (handlebars helper)"); @@ -151,7 +148,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> // Create new BTreeMap to extend the context: 'title' and 'link' let mut next_chapter = BTreeMap::new(); - match item.get("name") { + match item.get("title") { Some(n) => { debug!("[*]: Inserting title: {}", n); next_chapter.insert("title".to_owned(), n.to_json()); diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index 07a6b36d..6c8e9625 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -97,11 +97,11 @@ impl HelperDef for RenderToc { try!(rc.writer.write(" ".as_bytes())); } - if let Some(name) = item.get("name") { + if let Some(title) = item.get("title") { // Render only inline code blocks // filter all events that are not inline code blocks - let parser = Parser::new(&name).filter(|event| { + let parser = Parser::new(&title).filter(|event| { match event { &Event::Start(Tag::Code) | &Event::End(Tag::Code) => true, @@ -112,7 +112,7 @@ impl HelperDef for RenderToc { }); // render markdown to html - let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2); + let mut markdown_parsed_name = String::with_capacity(title.len() * 3 / 2); html::push_html(&mut markdown_parsed_name, parser); // write to the handlebars template diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index e76ffb48..ef348969 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -2,8 +2,23 @@ pub use self::html_handlebars::HtmlHandlebars; mod html_handlebars; +use book::MDBook; use std::error::Error; +use std::path::PathBuf; pub trait Renderer { - fn render(&self, book: &::book::MDBook) -> Result<(), Box>; + + /// Responsible for creating an `MDBook` struct from path, preparing the + /// project and calling `render()`, doing what is necessary for the + /// particular output format. + /// + /// This involves parsing config options from `book.toml` and parsing the + /// `SUMMARY.md` of each translation to a nested `Vec`. + /// + /// Finally it calls `render()` to process the chapters and static assets. + fn build(&self, project_root: &PathBuf) -> Result<(), Box>; + + /// Responsible for rendering the chapters and copying static assets. + fn render(&self, book_project: &MDBook) -> Result<(), Box>; + } diff --git a/src/tests/book-minimal-with-assets/assets/html-template/_layouts/page.hbs b/src/tests/book-minimal-with-assets/assets/html-template/_layouts/page.hbs new file mode 100644 index 00000000..ec5a88ff --- /dev/null +++ b/src/tests/book-minimal-with-assets/assets/html-template/_layouts/page.hbs @@ -0,0 +1,99 @@ + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ {{{ content }}} +
+ + + {{#previous}} + + {{/previous}} + + {{#next}} + + {{/next}} + +
+ + {{#previous}} + + {{/previous}} + + {{#next}} + + {{/next}} + +
+ + + + + diff --git a/src/tests/book-minimal-with-assets/assets/html-template/css/book.css b/src/tests/book-minimal-with-assets/assets/html-template/css/book.css new file mode 100644 index 00000000..3646bc65 --- /dev/null +++ b/src/tests/book-minimal-with-assets/assets/html-template/css/book.css @@ -0,0 +1,828 @@ +html, +body { + font-family: "Open Sans", sans-serif; + color: #333; +} +.left { + float: left; +} +.right { + float: right; +} +.hidden { + display: none; +} +h2, +h3 { + margin-top: 2.5em; +} +h4, +h5 { + margin-top: 2em; +} +.header + .header h3, +.header + .header h4, +.header + .header h5 { + margin-top: 1em; +} +table { + margin: 0 auto; + border-collapse: collapse; +} +table td { + padding: 3px 20px; + border: 1px solid; +} +table thead td { + font-weight: 700; +} +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: 300px; + overflow-y: auto; + padding: 10px 10px; + font-size: 0.875em; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + -webkit-transition: left 0.5s; + -moz-transition: left 0.5s; + -o-transition: left 0.5s; + -ms-transition: left 0.5s; + transition: left 0.5s; +} +@media only screen and (max-width: 1060px) { + .sidebar { + left: -300px; + } +} +.sidebar code { + line-height: 2em; +} +.sidebar-hidden .sidebar { + left: -300px; +} +.sidebar-visible .sidebar { + left: 0; +} +.chapter { + list-style: none outside none; + padding-left: 0; + line-height: 2.2em; +} +.chapter li a { + padding: 5px 0; + text-decoration: none; +} +.chapter li a:hover { + text-decoration: none; +} +.chapter .spacer { + width: 100%; + height: 3px; + margin: 10px 0px; +} +.section { + list-style: none outside none; + padding-left: 20px; + line-height: 1.9em; +} +.section li { + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} +.page-wrapper { + position: absolute; + left: 315px; + right: 0; + top: 0; + bottom: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + min-height: 100%; + -webkit-transition: left 0.5s; + -moz-transition: left 0.5s; + -o-transition: left 0.5s; + -ms-transition: left 0.5s; + transition: left 0.5s; +} +@media only screen and (max-width: 1060px) { + .page-wrapper { + left: 15px; + padding-right: 15px; + } +} +.sidebar-hidden .page-wrapper { + left: 15px; +} +.sidebar-visible .page-wrapper { + left: 315px; +} +.page { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + padding-right: 15px; +} +@media only screen and (max-width: 400px) { + .page { + /* Only prevent horizontal scrolling on screens with less than 100px for the content + A better way would be to somehow prevent horizontal scrolling all the time, but this causes scrolling problems on iOS Safari. + Also, would be better to only enable horizontal scrolling when it is needed (content does not fit on page) but I have no idea how to do that. */ + overflow-x: hidden; + } +} +.content { + margin-left: auto; + margin-right: auto; + max-width: 750px; + padding-bottom: 50px; +} +.content a { + text-decoration: none; +} +.content a:hover { + text-decoration: underline; +} +.content img { + max-width: 100%; +} +.menu-bar { + position: relative; + height: 50px; +} +.menu-bar i { + position: relative; + margin: 0 10px; + z-index: 10; + line-height: 50px; + -webkit-transition: color 0.5s; + -moz-transition: color 0.5s; + -o-transition: color 0.5s; + -ms-transition: color 0.5s; + transition: color 0.5s; +} +.menu-bar i:hover { + cursor: pointer; +} +.menu-bar .left-buttons { + float: left; +} +.menu-bar .right-buttons { + float: right; +} +.menu-title { + display: inline-block; + font-weight: 200; + font-size: 20px; + line-height: 50px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + text-align: center; + margin: 0; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: opacity 0.5s ease-in-out; + -moz-transition: opacity 0.5s ease-in-out; + -o-transition: opacity 0.5s ease-in-out; + -ms-transition: opacity 0.5s ease-in-out; + transition: opacity 0.5s ease-in-out; +} +.menu-bar:hover .menu-title { + opacity: 1; + -ms-filter: none; + filter: none; +} +.nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + position: fixed; + top: 50px /* Height of menu-bar */; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-pack: center; + -moz-box-pack: center; + -o-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -ms-flex-line-pack: center; + -webkit-align-content: center; + align-content: center; + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -o-box-orient: vertical; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-transition: color 0.5s; + -moz-transition: color 0.5s; + -o-transition: color 0.5s; + -ms-transition: color 0.5s; + transition: color 0.5s; +} +.mobile-nav-chapters { + display: none; +} +.nav-chapters:hover { + text-decoration: none; +} +.sidebar-hidden .previous { + left: 0; +} +.sidebar-visible .nav-chapters .previous { + left: 300px; +} +.sidebar-visible .mobile-nav-chapters .previous { + left: 0; +} +.next { + right: 15px; +} +.theme-popup { + position: relative; + left: 10px; + z-index: 1000; + -webkit-border-radius: 4px; + border-radius: 4px; + font-size: 0.7em; +} +.theme-popup .theme { + margin: 0; + padding: 2px 10px; + line-height: 25px; + white-space: nowrap; +} +.theme-popup .theme:hover:first-child, +.theme-popup .theme:hover:last-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +@media only screen and (max-width: 1250px) { + .nav-chapters { + display: none; + } + .mobile-nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + max-width: 150px; + min-width: 90px; + -webkit-box-pack: center; + -moz-box-pack: center; + -o-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -ms-flex-line-pack: center; + -webkit-align-content: center; + align-content: center; + position: relative; + display: inline-block; + margin-bottom: 50px; + -webkit-border-radius: 5px; + border-radius: 5px; + } + .next { + float: right; + } + .previous { + float: left; + } +} +.light { + color: #333; + background-color: #fff; +/* Inline code */ +} +.light .content .header:link, +.light .content .header:visited { + color: #333; + pointer: cursor; +} +.light .content .header:link:hover, +.light .content .header:visited:hover { + text-decoration: none; +} +.light .sidebar { + background-color: #fafafa; + color: #364149; +} +.light .chapter li { + color: #aaa; +} +.light .chapter li a { + color: #364149; +} +.light .chapter li .active, +.light .chapter li a:hover { +/* Animate color change */ + color: #008cff; +} +.light .chapter .spacer { + background-color: #f4f4f4; +} +.light .menu-bar, +.light .menu-bar:visited, +.light .nav-chapters, +.light .nav-chapters:visited, +.light .mobile-nav-chapters, +.light .mobile-nav-chapters:visited { + color: #ccc; +} +.light .menu-bar i:hover, +.light .nav-chapters:hover, +.light .mobile-nav-chapters i:hover { + color: #333; +} +.light .mobile-nav-chapters i:hover { + color: #364149; +} +.light .mobile-nav-chapters { + background-color: #fafafa; +} +.light .content a:link, +.light a:visited { + color: #4183c4; +} +.light .theme-popup { + color: #333; + background: #fafafa; + border: 1px solid #ccc; +} +.light .theme-popup .theme:hover { + background-color: #e6e6e6; +} +.light .theme-popup .default { + color: #ccc; +} +.light blockquote { + margin: 20px 0; + padding: 0 20px; + color: #333; + background-color: #f2f7f9; + border-top: 0.1em solid #e1edf1; + border-bottom: 0.1em solid #e1edf1; +} +.light table td { + border-color: #f2f2f2; +} +.light table tbody tr:nth-child(2n) { + background: #f7f7f7; +} +.light table thead { + background: #ccc; +} +.light table thead td { + border: none; +} +.light table thead tr { + border: 1px #ccc solid; +} +.light :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.light pre { + position: relative; +} +.light pre > .buttons { + position: absolute; + right: 5px; + top: 5px; + color: #364149; + cursor: pointer; +} +.light pre > .buttons :hover { + color: #008cff; +} +.light pre > .buttons i { + margin-left: 8px; +} +.light pre > .result { + margin-top: 10px; +} +.coal { + color: #98a3ad; + background-color: #141617; +/* Inline code */ +} +.coal .content .header:link, +.coal .content .header:visited { + color: #98a3ad; + pointer: cursor; +} +.coal .content .header:link:hover, +.coal .content .header:visited:hover { + text-decoration: none; +} +.coal .sidebar { + background-color: #292c2f; + color: #a1adb8; +} +.coal .chapter li { + color: #505254; +} +.coal .chapter li a { + color: #a1adb8; +} +.coal .chapter li .active, +.coal .chapter li a:hover { +/* Animate color change */ + color: #3473ad; +} +.coal .chapter .spacer { + background-color: #393939; +} +.coal .menu-bar, +.coal .menu-bar:visited, +.coal .nav-chapters, +.coal .nav-chapters:visited, +.coal .mobile-nav-chapters, +.coal .mobile-nav-chapters:visited { + color: #43484d; +} +.coal .menu-bar i:hover, +.coal .nav-chapters:hover, +.coal .mobile-nav-chapters i:hover { + color: #b3c0cc; +} +.coal .mobile-nav-chapters i:hover { + color: #a1adb8; +} +.coal .mobile-nav-chapters { + background-color: #292c2f; +} +.coal .content a:link, +.coal a:visited { + color: #2b79a2; +} +.coal .theme-popup { + color: #98a3ad; + background: #141617; + border: 1px solid #43484d; +} +.coal .theme-popup .theme:hover { + background-color: #1f2124; +} +.coal .theme-popup .default { + color: #43484d; +} +.coal blockquote { + margin: 20px 0; + padding: 0 20px; + color: #98a3ad; + background-color: #242637; + border-top: 0.1em solid #2c2f44; + border-bottom: 0.1em solid #2c2f44; +} +.coal table td { + border-color: #1f2223; +} +.coal table tbody tr:nth-child(2n) { + background: #1b1d1e; +} +.coal table thead { + background: #3f4649; +} +.coal table thead td { + border: none; +} +.coal table thead tr { + border: 1px #3f4649 solid; +} +.coal :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.coal pre { + position: relative; +} +.coal pre > .buttons { + position: absolute; + right: 5px; + top: 5px; + color: #a1adb8; + cursor: pointer; +} +.coal pre > .buttons :hover { + color: #3473ad; +} +.coal pre > .buttons i { + margin-left: 8px; +} +.coal pre > .result { + margin-top: 10px; +} +.navy { + color: #bcbdd0; + background-color: #161923; +/* Inline code */ +} +.navy .content .header:link, +.navy .content .header:visited { + color: #bcbdd0; + pointer: cursor; +} +.navy .content .header:link:hover, +.navy .content .header:visited:hover { + text-decoration: none; +} +.navy .sidebar { + background-color: #282d3f; + color: #c8c9db; +} +.navy .chapter li { + color: #505274; +} +.navy .chapter li a { + color: #c8c9db; +} +.navy .chapter li .active, +.navy .chapter li a:hover { +/* Animate color change */ + color: #2b79a2; +} +.navy .chapter .spacer { + background-color: #2d334f; +} +.navy .menu-bar, +.navy .menu-bar:visited, +.navy .nav-chapters, +.navy .nav-chapters:visited, +.navy .mobile-nav-chapters, +.navy .mobile-nav-chapters:visited { + color: #737480; +} +.navy .menu-bar i:hover, +.navy .nav-chapters:hover, +.navy .mobile-nav-chapters i:hover { + color: #b7b9cc; +} +.navy .mobile-nav-chapters i:hover { + color: #c8c9db; +} +.navy .mobile-nav-chapters { + background-color: #282d3f; +} +.navy .content a:link, +.navy a:visited { + color: #2b79a2; +} +.navy .theme-popup { + color: #bcbdd0; + background: #161923; + border: 1px solid #737480; +} +.navy .theme-popup .theme:hover { + background-color: #282e40; +} +.navy .theme-popup .default { + color: #737480; +} +.navy blockquote { + margin: 20px 0; + padding: 0 20px; + color: #bcbdd0; + background-color: #262933; + border-top: 0.1em solid #2f333f; + border-bottom: 0.1em solid #2f333f; +} +.navy table td { + border-color: #1f2331; +} +.navy table tbody tr:nth-child(2n) { + background: #1b1f2b; +} +.navy table thead { + background: #39415b; +} +.navy table thead td { + border: none; +} +.navy table thead tr { + border: 1px #39415b solid; +} +.navy :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.navy pre { + position: relative; +} +.navy pre > .buttons { + position: absolute; + right: 5px; + top: 5px; + color: #c8c9db; + cursor: pointer; +} +.navy pre > .buttons :hover { + color: #2b79a2; +} +.navy pre > .buttons i { + margin-left: 8px; +} +.navy pre > .result { + margin-top: 10px; +} +.rust { + color: #262625; + background-color: #e1e1db; +/* Inline code */ +} +.rust .content .header:link, +.rust .content .header:visited { + color: #262625; + pointer: cursor; +} +.rust .content .header:link:hover, +.rust .content .header:visited:hover { + text-decoration: none; +} +.rust .sidebar { + background-color: #3b2e2a; + color: #c8c9db; +} +.rust .chapter li { + color: #505254; +} +.rust .chapter li a { + color: #c8c9db; +} +.rust .chapter li .active, +.rust .chapter li a:hover { +/* Animate color change */ + color: #e69f67; +} +.rust .chapter .spacer { + background-color: #45373a; +} +.rust .menu-bar, +.rust .menu-bar:visited, +.rust .nav-chapters, +.rust .nav-chapters:visited, +.rust .mobile-nav-chapters, +.rust .mobile-nav-chapters:visited { + color: #737480; +} +.rust .menu-bar i:hover, +.rust .nav-chapters:hover, +.rust .mobile-nav-chapters i:hover { + color: #262625; +} +.rust .mobile-nav-chapters i:hover { + color: #c8c9db; +} +.rust .mobile-nav-chapters { + background-color: #3b2e2a; +} +.rust .content a:link, +.rust a:visited { + color: #2b79a2; +} +.rust .theme-popup { + color: #262625; + background: #e1e1db; + border: 1px solid #b38f6b; +} +.rust .theme-popup .theme:hover { + background-color: #99908a; +} +.rust .theme-popup .default { + color: #737480; +} +.rust blockquote { + margin: 20px 0; + padding: 0 20px; + color: #262625; + background-color: #c1c1bb; + border-top: 0.1em solid #b8b8b1; + border-bottom: 0.1em solid #b8b8b1; +} +.rust table td { + border-color: #d7d7cf; +} +.rust table tbody tr:nth-child(2n) { + background: #dbdbd4; +} +.rust table thead { + background: #b3a497; +} +.rust table thead td { + border: none; +} +.rust table thead tr { + border: 1px #b3a497 solid; +} +.rust :not(pre) > .hljs { + display: inline-block; + vertical-align: middle; + padding: 0.1em 0.3em; + -webkit-border-radius: 3px; + border-radius: 3px; +} +.rust pre { + position: relative; +} +.rust pre > .buttons { + position: absolute; + right: 5px; + top: 5px; + color: #c8c9db; + cursor: pointer; +} +.rust pre > .buttons :hover { + color: #e69f67; +} +.rust pre > .buttons i { + margin-left: 8px; +} +.rust pre > .result { + margin-top: 10px; +} +@media only print { + #sidebar, + #menu-bar, + .nav-chapters, + .mobile-nav-chapters { + display: none; + } + #page-wrapper { + left: 0; + overflow-y: initial; + } + #content { + max-width: none; + margin: 0; + padding: 0; + } + .page { + overflow-y: initial; + } + code { + background-color: #666; + -webkit-border-radius: 5px; + border-radius: 5px; +/* Force background to be printed in Chrome */ + -webkit-print-color-adjust: exact; + } + a, + a:visited, + a:active, + a:hover { + color: #4183c4; + text-decoration: none; + } + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-inside: avoid; + page-break-after: avoid; +/*break-after: avoid*/ + } + pre, + code { + page-break-inside: avoid; + white-space: pre-wrap /* CSS 3 */; + white-space: -moz-pre-wrap /* Mozilla, since 1999 */; + white-space: -pre-wrap /* Opera 4-6 */; + white-space: -o-pre-wrap /* Opera 7 */; + word-wrap: break-word /* Internet Explorer 5.5+ */; + } +} diff --git a/src/tests/book-minimal-with-assets/book.toml b/src/tests/book-minimal-with-assets/book.toml new file mode 100644 index 00000000..fb8ee0b4 --- /dev/null +++ b/src/tests/book-minimal-with-assets/book.toml @@ -0,0 +1,3 @@ +title = "Labyrinths" +subtitle = "Selected Stories and Other Writings" +author = "Jorge Luis Borges" diff --git a/src/tests/book-minimal-with-assets/src/SUMMARY.md b/src/tests/book-minimal-with-assets/src/SUMMARY.md new file mode 100644 index 00000000..727197c6 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/SUMMARY.md @@ -0,0 +1,11 @@ +# Labyrinths + +[Introduction](introduction.md) + +- [Fictions](fictions.md) + - [Ruins](fictions/ruins.md) + - [Babel](fictions/babel.md) +- [Essays](essays.md) + - [Kafka](essays/kafka.md) + +[Chronology](chronology.md) diff --git a/src/tests/book-minimal-with-assets/src/chronology.md b/src/tests/book-minimal-with-assets/src/chronology.md new file mode 100644 index 00000000..94eceeee --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/chronology.md @@ -0,0 +1 @@ +# Chronology diff --git a/src/tests/book-minimal-with-assets/src/essays.md b/src/tests/book-minimal-with-assets/src/essays.md new file mode 100644 index 00000000..c018e668 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/essays.md @@ -0,0 +1 @@ +# Essays diff --git a/src/tests/book-minimal-with-assets/src/essays/kafka.md b/src/tests/book-minimal-with-assets/src/essays/kafka.md new file mode 100644 index 00000000..2eb9d103 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/essays/kafka.md @@ -0,0 +1 @@ +# Kafka diff --git a/src/tests/book-minimal-with-assets/src/fictions.md b/src/tests/book-minimal-with-assets/src/fictions.md new file mode 100644 index 00000000..c04e57bc --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/fictions.md @@ -0,0 +1 @@ +# Fictions diff --git a/src/tests/book-minimal-with-assets/src/fictions/babel.md b/src/tests/book-minimal-with-assets/src/fictions/babel.md new file mode 100644 index 00000000..3c50a9c3 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/fictions/babel.md @@ -0,0 +1,12 @@ ++++ +title = "The Library of Babel" +author = "Jorge Luis Borges" +translator = "James E. Irby" ++++ + +# Babel + +The universe (which others call the Library) is composed of an indefinite and +perhaps infinite number of hexagonal galleries, with vast air shafts between, +surrounded by very low railings. From any of the hexagons one can see, +interminably, the upper and lower floors. diff --git a/src/tests/book-minimal-with-assets/src/fictions/ruins.md b/src/tests/book-minimal-with-assets/src/fictions/ruins.md new file mode 100644 index 00000000..833fd716 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/fictions/ruins.md @@ -0,0 +1,5 @@ ++++ +title = "The Circular Ruins" ++++ + +# Ruins diff --git a/src/tests/book-minimal-with-assets/src/introduction.md b/src/tests/book-minimal-with-assets/src/introduction.md new file mode 100644 index 00000000..e10b99d0 --- /dev/null +++ b/src/tests/book-minimal-with-assets/src/introduction.md @@ -0,0 +1 @@ +# Introduction diff --git a/src/tests/book-minimal/book.toml b/src/tests/book-minimal/book.toml new file mode 100644 index 00000000..fb8ee0b4 --- /dev/null +++ b/src/tests/book-minimal/book.toml @@ -0,0 +1,3 @@ +title = "Labyrinths" +subtitle = "Selected Stories and Other Writings" +author = "Jorge Luis Borges" diff --git a/src/tests/book-minimal/src/SUMMARY.md b/src/tests/book-minimal/src/SUMMARY.md new file mode 100644 index 00000000..727197c6 --- /dev/null +++ b/src/tests/book-minimal/src/SUMMARY.md @@ -0,0 +1,11 @@ +# Labyrinths + +[Introduction](introduction.md) + +- [Fictions](fictions.md) + - [Ruins](fictions/ruins.md) + - [Babel](fictions/babel.md) +- [Essays](essays.md) + - [Kafka](essays/kafka.md) + +[Chronology](chronology.md) diff --git a/src/tests/book-minimal/src/chronology.md b/src/tests/book-minimal/src/chronology.md new file mode 100644 index 00000000..94eceeee --- /dev/null +++ b/src/tests/book-minimal/src/chronology.md @@ -0,0 +1 @@ +# Chronology diff --git a/src/tests/book-minimal/src/essays.md b/src/tests/book-minimal/src/essays.md new file mode 100644 index 00000000..c018e668 --- /dev/null +++ b/src/tests/book-minimal/src/essays.md @@ -0,0 +1 @@ +# Essays diff --git a/src/tests/book-minimal/src/essays/kafka.md b/src/tests/book-minimal/src/essays/kafka.md new file mode 100644 index 00000000..2eb9d103 --- /dev/null +++ b/src/tests/book-minimal/src/essays/kafka.md @@ -0,0 +1 @@ +# Kafka diff --git a/src/tests/book-minimal/src/fictions.md b/src/tests/book-minimal/src/fictions.md new file mode 100644 index 00000000..c04e57bc --- /dev/null +++ b/src/tests/book-minimal/src/fictions.md @@ -0,0 +1 @@ +# Fictions diff --git a/src/tests/book-minimal/src/fictions/babel.md b/src/tests/book-minimal/src/fictions/babel.md new file mode 100644 index 00000000..3c50a9c3 --- /dev/null +++ b/src/tests/book-minimal/src/fictions/babel.md @@ -0,0 +1,12 @@ ++++ +title = "The Library of Babel" +author = "Jorge Luis Borges" +translator = "James E. Irby" ++++ + +# Babel + +The universe (which others call the Library) is composed of an indefinite and +perhaps infinite number of hexagonal galleries, with vast air shafts between, +surrounded by very low railings. From any of the hexagons one can see, +interminably, the upper and lower floors. diff --git a/src/tests/book-minimal/src/fictions/ruins.md b/src/tests/book-minimal/src/fictions/ruins.md new file mode 100644 index 00000000..833fd716 --- /dev/null +++ b/src/tests/book-minimal/src/fictions/ruins.md @@ -0,0 +1,5 @@ ++++ +title = "The Circular Ruins" ++++ + +# Ruins diff --git a/src/tests/book-minimal/src/introduction.md b/src/tests/book-minimal/src/introduction.md new file mode 100644 index 00000000..e10b99d0 --- /dev/null +++ b/src/tests/book-minimal/src/introduction.md @@ -0,0 +1 @@ +# Introduction diff --git a/src/tests/book-wonderland-multilang/assets/images/Queen.jpg b/src/tests/book-wonderland-multilang/assets/images/Queen.jpg new file mode 100644 index 00000000..74421205 Binary files /dev/null and b/src/tests/book-wonderland-multilang/assets/images/Queen.jpg differ diff --git a/src/tests/book-wonderland-multilang/assets/images/Rabbit.png b/src/tests/book-wonderland-multilang/assets/images/Rabbit.png new file mode 100644 index 00000000..bc86c6a6 Binary files /dev/null and b/src/tests/book-wonderland-multilang/assets/images/Rabbit.png differ diff --git a/src/tests/book-wonderland-multilang/assets/images/Tail.png b/src/tests/book-wonderland-multilang/assets/images/Tail.png new file mode 100644 index 00000000..d45b8990 Binary files /dev/null and b/src/tests/book-wonderland-multilang/assets/images/Tail.png differ diff --git a/src/tests/book-wonderland-multilang/assets/images/Tears.png b/src/tests/book-wonderland-multilang/assets/images/Tears.png new file mode 100644 index 00000000..fc93a58c Binary files /dev/null and b/src/tests/book-wonderland-multilang/assets/images/Tears.png differ diff --git a/src/tests/book-wonderland-multilang/book.toml b/src/tests/book-wonderland-multilang/book.toml new file mode 100644 index 00000000..7a60e311 --- /dev/null +++ b/src/tests/book-wonderland-multilang/book.toml @@ -0,0 +1,17 @@ +# Source: https://en.wikisource.org/wiki/Alice%27s_Adventures_in_Wonderland_(1866)" + +[[translations.en]] +title = "Alice's Adventures in Wonderland" +author = "Lewis Carroll" +language = { name = "English", code = "en" } +is_main_book = true + +[[translations.fr]] +title = "Alice au pays des merveilles" +author = "Lewis Carroll" +language = { name = "Français", code = "fr" } + +[[translations.hu]] +title = "Alice Csodaországban" +author = "Lewis Carroll" +language = { name = "Magyar", code = "hu" } diff --git a/src/tests/book-wonderland-multilang/src/en/SUMMARY.md b/src/tests/book-wonderland-multilang/src/en/SUMMARY.md new file mode 100644 index 00000000..41d71540 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/en/SUMMARY.md @@ -0,0 +1,17 @@ +# Alice's Adventures in Wonderland + +[Titlepage](titlepage.md) + +- [Down The Rabbit-Hole](rabbit-hole.md) +- [The Pool of Tears](tears.md) +- [A Caucus-Race and a Long Tale](long-tale.md) +- [The Rabbit Sends in a Little Bill]() +- [Advice From a Caterpillar]() +- [Pig and Pepper]() +- [A Mad Tea-Party]() +- [The Queen's Croquet-Ground]() +- [The Mock-Turtle's Story]() +- [The Lobster Quadrille]() +- [Who Stole The Tarts?]() +- [Alice's Evidence]() + diff --git a/src/tests/book-wonderland-multilang/src/en/long-tale.md b/src/tests/book-wonderland-multilang/src/en/long-tale.md new file mode 100644 index 00000000..dec8091d --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/en/long-tale.md @@ -0,0 +1,15 @@ +# A Caucus-Race and a Long Tale + +![Tail](images/Tail.png) + +They were indeed a queer-looking party that assembled on the bank—the birds with +draggled feathers, the animals with their fur clinging close to them, and all +dripping wet, cross, and uncomfortable. + +The first question of course was, how to get dry again: they had a consultation +about this, and after a few minutes it seemed quite natural to Alice to find +herself talking familiarly with them, as if she had known them all her life. +Indeed, she had quite a long argument with the Lory, who at last turned sulky, +and would only say, "I am older than you, and must know better;" and this Alice +would not allow, without knowing how old it was, and as the Lory positively +refused to tell its age, there was no more to be said. diff --git a/src/tests/book-wonderland-multilang/src/en/rabbit-hole.md b/src/tests/book-wonderland-multilang/src/en/rabbit-hole.md new file mode 100644 index 00000000..585e8a22 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/en/rabbit-hole.md @@ -0,0 +1,13 @@ +# Down The Rabbit-Hole + +![Rabbit](images/Rabbit.png) + +Alice was beginning to get very tired of sitting by her sister on the bank, and +of having nothing to do: once or twice she had peeped into the book her sister +was reading, but it had no pictures or conversations in it, "and what is the use +of a book," thought Alice, "without pictures or conversations?" + +So she was considering in her own mind, (as well as she could, for the hot day +made her feel very sleepy and stupid,) whether the pleasure of making a +daisy-chain would be worth the trouble of getting up and picking the daisies, +when suddenly a white rabbit with pink eyes ran close by her. diff --git a/src/tests/book-wonderland-multilang/src/en/tears.md b/src/tests/book-wonderland-multilang/src/en/tears.md new file mode 100644 index 00000000..7d2523e3 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/en/tears.md @@ -0,0 +1,18 @@ +# The Pool of Tears + +![Tears](images/Tears.png) + +"Curiouser and curiouser!" cried Alice (she was so much surprised, that for the +moment she quite forgot how to speak good English); "now I'm opening out like +the largest telescope that ever was! Good-bye, feet!" (for when she looked down +at her feet, they seemed to be almost out of sight, they were getting so far +off). "Oh, my poor little feet, I wonder who will put on your shoes and +stockings for you now, dears? I'm sure I shan't be able! I shall be a great deal +too far off to trouble myself about you: you must manage the best way you +can;—but I must be kind to them," thought Alice, "or perhaps they won't walk the +way I want to go! Let me see: I'll give them a new pair of boots every +Christmas." + +And she went on planning to herself how she would manage it. "They must go by +the carrier," she thought; "and how funny it'll seem, sending presents to one's +own feet! And how odd the directions will look! diff --git a/src/tests/book-wonderland-multilang/src/en/titlepage.md b/src/tests/book-wonderland-multilang/src/en/titlepage.md new file mode 100644 index 00000000..58639ff0 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/en/titlepage.md @@ -0,0 +1,10 @@ +# Alice's Adventures in Wonderland + +![Queen of Hearts](images/Queen.jpg) + +All in the golden afternoon +Full leisurely we glide; +For both our oars, with little skill, +By little arms are plied, +While little hands make vain pretence +Our wanderings to guide. diff --git a/src/tests/book-wonderland-multilang/src/fr/SUMMARY.md b/src/tests/book-wonderland-multilang/src/fr/SUMMARY.md new file mode 100644 index 00000000..0ca6d6d6 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/fr/SUMMARY.md @@ -0,0 +1,17 @@ +# Alice au pays des merveilles + +[Titre](titre.md) + +- [Au fond du terrier](terrier.md) +- [La mare aux larmes](larmes.md) +- [La course cocasse](cocasse.md) +- [L'habitation du lapin blanc]() +- [Conseils d'une chenille]() +- [Porc et poivre]() +- [Un thé de fous]() +- [Le croquet de la reine]() +- [Histoire de la fausse-tortue]() +- [Le quadrille de homards]() +- [Qui a volé les tartes ?]() +- [Déposition d'Alice]() + diff --git a/src/tests/book-wonderland-multilang/src/fr/cocasse.md b/src/tests/book-wonderland-multilang/src/fr/cocasse.md new file mode 100644 index 00000000..cb74ab3a --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/fr/cocasse.md @@ -0,0 +1,17 @@ +# La course cocasse + +![Cocasse](images/Tail.png) + +Ils formaient une assemblée bien grotesque ces êtres singuliers réunis sur le +bord de la mare ; les uns avaient leurs plumes tout en désordre, les autres le +poil plaqué contre le corps. Tous étaient trempés, de mauvaise humeur, et fort +mal à l’aise. + +« Comment faire pour nous sécher ? » ce fut la première question, cela va sans +dire. Au bout de quelques instants, il sembla tout naturel à Alice de causer +familièrement avec ces animaux, comme si elle les connaissait depuis son +berceau. Elle eut même une longue discussion avec le Lory, qui, à la fin, lui +fit la mine et lui dit d’un air boudeur : « Je suis plus âgé que vous, et je +dois par conséquent en savoir plus long. » Alice ne voulut pas accepter cette +conclusion avant de savoir l’âge du Lory, et comme celui-ci refusa tout net de +le lui dire, cela mit un terme au débat. diff --git a/src/tests/book-wonderland-multilang/src/fr/larmes.md b/src/tests/book-wonderland-multilang/src/fr/larmes.md new file mode 100644 index 00000000..87d0c91c --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/fr/larmes.md @@ -0,0 +1,18 @@ +# La mare aux larmes + +![Larmes](images/Tears.png) + +« De plus très-curieux en plus très-curieux ! » s’écria Alice (sa surprise était +si grande qu’elle ne pouvait s’exprimer correctement) : « Voilà que je m’allonge +comme le plus grand télescope qui fût jamais ! Adieu mes pieds ! » (Elle venait +de baisser les yeux, et ses pieds lui semblaient s’éloigner à perte de vue.) « +Oh ! mes pauvres petits pieds ! Qui vous mettra vos bas et vos souliers +maintenant, mes mignons ? Quant à moi, je ne le pourrai certainement pas ! Je +serai bien trop loin pour m’occuper de vous : arrangez-vous du mieux que vous +pourrez. — Il faut cependant que je sois bonne pour eux, » pensa Alice, « sans +cela ils refuseront peut-être d’aller du côté que je voudrai. Ah ! je sais ce +que je ferai : je leur donnerai une belle paire de bottines à Noël. » + +Puis elle chercha dans son esprit comment elle s’y prendrait. « Il faudra les +envoyer par le messager, » pensa-t-elle ; « quelle étrange chose d’envoyer des +présents à ses pieds ! Et l’adresse donc ! C’est cela qui sera drôle. diff --git a/src/tests/book-wonderland-multilang/src/fr/terrier.md b/src/tests/book-wonderland-multilang/src/fr/terrier.md new file mode 100644 index 00000000..55cb3e7f --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/fr/terrier.md @@ -0,0 +1,13 @@ +# Au fond du terrier + +![Terrier](images/Rabbit.png) + +Alice, assise auprès de sa sœur sur le gazon, commençait à s’ennuyer de rester +là à ne rien faire ; une ou deux fois elle avait jeté les yeux sur le livre que +lisait sa sœur ; mais quoi ! pas d’images, pas de dialogues ! « La belle avance, +» pensait Alice, « qu’un livre sans images, sans causeries ! » + +Elle s’était mise à réfléchir, (tant bien que mal, car la chaleur du jour +l’endormait et la rendait lourde,) se demandant si le plaisir de faire une +couronne de marguerites valait bien la peine de se lever et de cueillir les +fleurs, quand tout à coup un lapin blanc aux yeux roses passa près d’elle. diff --git a/src/tests/book-wonderland-multilang/src/fr/titre.md b/src/tests/book-wonderland-multilang/src/fr/titre.md new file mode 100644 index 00000000..099db953 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/fr/titre.md @@ -0,0 +1,18 @@ +# Alice au pays des merveilles + +![Queen of Hearts](images/Queen.jpg) + +[L’Auteur désire exprimer ici sa reconnaissance envers le Traducteur de ce qu’il +a remplacé par des parodies de sa composition quelques parodies de morceaux de +poésie anglais, qui n’avaient de valeur que pour des enfants anglais ; et aussi, +de ce qu’il a su donner en jeux de mots français les équivalents des jeux de +mots anglais, dont la traduction n’était pas possible.] + +Notre barque glisse sur l’onde +Que dorent de brûlants rayons ; +Sa marche lente et vagabonde +Témoigne que des bras mignons, +Pleins d’ardeur, mais encore novices, +Tout fiers de ce nouveau travail, +Mènent au gré de leurs caprices +Les rames et le gouvernail. diff --git a/src/tests/book-wonderland-multilang/src/hu/SUMMARY.md b/src/tests/book-wonderland-multilang/src/hu/SUMMARY.md new file mode 100644 index 00000000..2e9daf5a --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/hu/SUMMARY.md @@ -0,0 +1,17 @@ +# Alice Csodaországban + +[Címoldal](cimoldal.md) + +- [Lenn, a Nyuszi barlangjában](nyuszi.md) +- [Könnytó](konnyto.md) +- [Körbecsukó meg az egér hosszú tarka farka](tarka-farka.md) +- [Gyíkocska]() +- [A hernyó tanácsot ad]() +- [Békétlenség, bors és baj]() +- [Bolondok uzsonnája]() +- [A királyi krokettpálya]() +- [Az Ál-Teknőc története]() +- [Homár-humor]() +- [Ki lopta el a lepényt?]() +- [Alice tanúvallomása]() + diff --git a/src/tests/book-wonderland-multilang/src/hu/cimoldal.md b/src/tests/book-wonderland-multilang/src/hu/cimoldal.md new file mode 100644 index 00000000..48177b68 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/hu/cimoldal.md @@ -0,0 +1,10 @@ +# Alice Csodaországban + +![Queen of Hearts](images/Queen.jpg) + +Egész aranyló délután +csak szeltük a vizet, +két ügyetlen, parányi kar +buzgón evezgetett, +küszködtek a kormánnyal +a parányi kis kezek. diff --git a/src/tests/book-wonderland-multilang/src/hu/konnyto.md b/src/tests/book-wonderland-multilang/src/hu/konnyto.md new file mode 100644 index 00000000..0b88fe12 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/hu/konnyto.md @@ -0,0 +1,10 @@ +# Könnytó + +![Könnytó](images/Tears.png) + +-- Egyre murisabb! - kiáltott fel Alice. Úgy meglepődött, hogy egyszeriben +elfelejtett szépen beszélni. + +-- Most hát olyan hosszúra nyúltam, mint a világ legnagyobb távcsöve. No, +szervusztok, lábaim. Tudniillik lenézett a lábaira. Alig látta őket, olyan +messzire estek tőle. diff --git a/src/tests/book-wonderland-multilang/src/hu/nyuszi.md b/src/tests/book-wonderland-multilang/src/hu/nyuszi.md new file mode 100644 index 00000000..24896e19 --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/hu/nyuszi.md @@ -0,0 +1,14 @@ +# Lenn, a Nyuszi barlangjában + +![Nyuszi](images/Rabbit.png) + +Alice már elunta, hogy tétlenül üldögéljen nénje mellett az árokparton. +Egyszer-egyszer bepislantott abba a könyvbe, amelyet a nénje olvasott, de nem +voltak benne se képek, se versek. + +Mit ér egy könyv - gondolta Alice - képek meg versek nélkül? + +Hát csak ült-üldögélt, s azon törte a fejét, már amennyire tőle telt - mert a +rekkenő hőségtől egészen elálmosodott és megbutult -, hogy jobb volna fölkelni +pitypangot szedni és füzért fonni belőle, amikor hirtelenül elsurrant mellette a +piros szemű fehér Nyuszi. diff --git a/src/tests/book-wonderland-multilang/src/hu/tarka-farka.md b/src/tests/book-wonderland-multilang/src/hu/tarka-farka.md new file mode 100644 index 00000000..f3617efe --- /dev/null +++ b/src/tests/book-wonderland-multilang/src/hu/tarka-farka.md @@ -0,0 +1,17 @@ +# Körbecsukó meg az egér hosszú tarka farka + +![Tarka-farka](images/Tail.png) + +Mondhatom, furcsán festett ez a társaság a parton. A madarak tolla csatakos +volt, a többi állat szőre meg odatapadt a testéhez, valamennyien +csurogtak-csöpögtek, és búnak eresztették a fejüket. + +Természetesen az volt a legnagyobb gondjuk, hogyan lehetne megszáradniuk. Ezen +tanakodtak, s Alice néhány perc múlva már kedélyesen tereferélt velük, mintha +réges-rég ismerte volna őket. Sőt a Papagájjal össze is veszett, hogy az végül +duzzogva hátat fordított neki, s csak ezt hajtogatta: + +- Én öregebb vagyok, szívem, ennélfogva nekem jobban kell tudnom. + +Alice ezt nem akarta elismerni mindaddig, amíg a Papagáj meg nem mondja: hány +éves. De a Papagáj ezt kereken megtagadta. Hát a vita itt megrekedt. diff --git a/src/tests/bookconfig_test.rs b/src/tests/bookconfig_test.rs new file mode 100644 index 00000000..35a2c180 --- /dev/null +++ b/src/tests/bookconfig_test.rs @@ -0,0 +1,16 @@ +#[cfg(test)] + +use std::path::PathBuf; + +use book::bookconfig::BookConfig; + +#[test] +fn it_creates_paths_joined_to_project_root() { + let result = BookConfig::new(&PathBuf::from("./there".to_string())); + + let mut expected = BookConfig::default(); + expected.dest = PathBuf::from("./there".to_string()).join("book"); + expected.src = PathBuf::from("./there".to_string()).join("src"); + + assert_eq!(format!("{:?}", result), format!("{:?}", expected)); +} diff --git a/src/tests/chapter_test.rs b/src/tests/chapter_test.rs new file mode 100644 index 00000000..7ce4e08a --- /dev/null +++ b/src/tests/chapter_test.rs @@ -0,0 +1,22 @@ +#[cfg(test)] + +use std::path::PathBuf; + +use book::bookconfig::Author; +use book::chapter::Chapter; + +#[test] +fn it_parses_when_exists() { + let src_path = PathBuf::from(".").join("src").join("tests").join("chapters"); + let path = PathBuf::from("at-the-mountains-of-madness.md"); + + let mut result = Chapter::new("Mountains".to_string(), path.clone()); + result.parse_or_create_using(&src_path); + + let mut expected = Chapter::new("Mountains".to_string(), path.clone()); + + // test that the author is parsed from the TOML header + expected.authors = Some(vec![Author::new("H.P. Lovecraft")]); + + assert_eq!(format!("{:?}", result), format!("{:?}", expected)); +} diff --git a/src/tests/chapters/at-the-mountains-of-madness.md b/src/tests/chapters/at-the-mountains-of-madness.md new file mode 100644 index 00000000..0e55c08c --- /dev/null +++ b/src/tests/chapters/at-the-mountains-of-madness.md @@ -0,0 +1,30 @@ ++++ +author = "H.P. Lovecraft" ++++ + +# At the Mountains of Madness + +> "I have seen the dark universe yawning\\ +> Where the black planets roll without aim,\\ +> Where they roll in their horror unheeded,\\ +> without knowledge or lustre or name." +> +> Nemesis, 1917 + +Yet now the sway of reason seemed irrefutably shaken, for this +Cyclopean maze of squared, curved, and angled blocks had features +which cut off all comfortable refuge. It was, very clearly, the +blasphemous city of the mirage in stark, objective, and +ineluctable reality. That damnable portent had had a material +basis after all—there had been some horizontal stratum of ice +dust in the upper air, and this shocking stone survival had +projected its image across the mountains according to the simple +laws of reflection, Of course, the phantom had been twisted and +exaggerated, and had contained things which the real source did +not contain; yet now, as we saw that real source, we thought it +even more hideous and menacing than its distant image. + +[At the Mountains of Madness][1] by *H.P. Lovecraft*, 1931 + +[1]: http://en.wikisource.org/wiki/At_the_Mountains_of_Madness/Chapter_5 + diff --git a/src/tests/fs_test.rs b/src/tests/fs_test.rs new file mode 100644 index 00000000..240f185b --- /dev/null +++ b/src/tests/fs_test.rs @@ -0,0 +1,50 @@ +#[cfg(test)] + +use std::fs::{self, File}; +use std::io::Read; +use std::path::Path; + +use utils; + +#[test] +fn it_copies_data_file() { + let dest_path = Path::new("the place was dark").join("and dusty and half-lost").join("book.css"); + utils::fs::copy_data_file("data/html-template/css/books.css", &dest_path); + + let mut file = match File::open(&dest_path) { + Ok(f) => f, + Err(e) => { + println!("Failed to open {:?}", dest_path); + return; + }, + }; + + let mut content = String::new(); + if let Err(e) = file.read_to_string(&mut content) { + println!("Failed to read {:?}", dest_path); + return; + } + + assert!(content.as_str().contains("Open Sans")); +} + +#[test] +fn it_copies_data_by_pattern() { + let dest_base = Path::new("in tangles of old alleys").join("near the quays"); + + if let Err(e) = utils::fs::copy_data("data/html-template/**/*", + "data/html-template/", + vec!["data/html-template/_*"], + &dest_base) { + println!("Error: {:#?}", e); + return; + } + + assert!(dest_base.join("css").join("book.css").exists()); + assert!(!dest_base.join("_layouts").exists()); + + let p = Path::new("in tangles of old alleys"); + if p.exists() { + fs::remove_dir_all(p); + } +} diff --git a/src/tests/hbs_renderer_multilang.rs b/src/tests/hbs_renderer_multilang.rs new file mode 100644 index 00000000..aff66a74 --- /dev/null +++ b/src/tests/hbs_renderer_multilang.rs @@ -0,0 +1,53 @@ +#[cfg(test)] + +use std::path::{Path, PathBuf}; + +use book::MDBook; +use renderer::{Renderer, HtmlHandlebars}; +use utils; + +#[test] +fn it_renders_multilanguage_book() { + let path = PathBuf::from(".").join("src").join("tests").join("book-wonderland-multilang"); + + let renderer = HtmlHandlebars::new(); + if let Err(e) = renderer.build(&path) { + println!("{:#?}", e); + } + + let mut proj = MDBook::new(&path); + proj.read_config(); + proj.parse_books(); + + let mut book_path: &Path = proj.translations.get("en").unwrap().config.get_dest(); + let mut chapter_path: PathBuf = PathBuf::from("".to_string()); + let mut s: String = String::new(); + + // Test if index.html in the project dest folder is the main book's first chapter + + chapter_path = proj.get_dest_base().join("index.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("Alice's Adventures in Wonderland")); + assert!(s.contains("

Alice's Adventures in Wonderland

")); + + // Test if each translation was rendered + + chapter_path = book_path.join("tears.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("

The Pool of Tears

")); + + book_path = proj.translations.get("fr").unwrap().config.get_dest(); + chapter_path = book_path.join("larmes.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("

La mare aux larmes

")); + + book_path = proj.translations.get("hu").unwrap().config.get_dest(); + chapter_path = book_path.join("konnyto.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("

Könnytó

")); + + // Test if book's asset files were copied + + assert!(proj.get_dest_base().join("images").join("Queen.jpg").exists()); + +} diff --git a/src/tests/hbs_renderer_test.rs b/src/tests/hbs_renderer_test.rs new file mode 100644 index 00000000..1252e995 --- /dev/null +++ b/src/tests/hbs_renderer_test.rs @@ -0,0 +1,79 @@ +#[cfg(test)] + +use std::path::{Path, PathBuf}; + +use book::MDBook; +use renderer::{Renderer, HtmlHandlebars}; +use utils; + +#[test] +fn it_renders_html_from_minimal_book() { + let path = PathBuf::from(".").join("src").join("tests").join("book-minimal"); + + let renderer = HtmlHandlebars::new(); + if let Err(e) = renderer.build(&path) { + println!("{:#?}", e); + } + + let mut proj = MDBook::new(&path); + proj.read_config(); + proj.parse_books(); + + let book_path: &Path = proj.translations.get("en").unwrap().config.get_dest(); + let mut chapter_path: PathBuf = PathBuf::from("".to_string()); + let mut s: String = String::new(); + + // Test if "Library of Babel" was rendered + + chapter_path = book_path.join("fictions").join("babel").with_extension("html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("The Library of Babel")); + + // Test if first chapter "Introduction" was rendered as index.html + + chapter_path = book_path.join("index.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains("

Introduction

")); + + // Test if next link from "Introduction" is "Fictions" + + chapter_path = book_path.join("index.html"); + s = utils::fs::file_to_string(&chapter_path).unwrap(); + assert!(s.contains(" {x}, + None => { + error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors); + exit(2); + } + }; + + result.parse_from_btreemap(&config); + + let mut expected = MDBook::new(&PathBuf::from(".")); + expected.indent_spaces = 2; + + { + let mut conf = BookConfig::new(&PathBuf::from(".")); + conf.title = "Alice's Adventures in Wonderland".to_string(); + conf.authors = vec![Author::new("Lewis Carroll")]; + conf.src = expected.get_project_root().join("src").join("en"); + conf.dest = expected.get_project_root().join("book").join("en"); + + let mut book = Book::default(); + book.config = conf; + + expected.translations.insert("en".to_string(), book); + } + + { + let mut conf = BookConfig::new(&PathBuf::from(".")); + conf.title = "Alice Csodaországban".to_string(); + conf.authors = vec![Author::new("Lewis Carroll")]; + conf.translators = Some(vec![Author::new("Kosztolányi Dezső")]); + conf.src = expected.get_project_root().join("src").join("hu"); + conf.dest = expected.get_project_root().join("book").join("hu"); + + let mut book = Book::default(); + book.config = conf; + + expected.translations.insert("hu".to_string(), book); + } + + // Hashmaps are unordered. They don't always print their keys in the same order. + + assert_eq!( + format!("{:#?} {:#?} {:#?}", result.indent_spaces, result.translations.get("en").unwrap(), result.translations.get("hu").unwrap()), + format!("{:#?} {:#?} {:#?}", expected.indent_spaces, expected.translations.get("en").unwrap(), expected.translations.get("hu").unwrap()) + ); +} + +#[test] +fn it_parses_config_for_a_single_book() { + let text = r#" +indent_spaces = 2 +title = "Alice Csodaországban" +language = { name = "Hungarian", code = "hu" } + +[[authors]] +name = "Lewis Carroll" + +[[translators]] +name = "Kosztolányi Dezső" +"#; + + let mut result = MDBook::new(&PathBuf::from(".")); + + let mut parser = toml::Parser::new(&text); + + let config = match parser.parse() { + Some(x) => {x}, + None => { + error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors); + exit(2); + } + }; + + result.parse_from_btreemap(&config); + + let mut expected = MDBook::new(&PathBuf::from(".")); + expected.indent_spaces = 2; + + { + let mut book = Book::new(&PathBuf::from(result.get_project_root())); + book.config.title = "Alice Csodaországban".to_string(); + book.config.authors = vec![Author::new("Lewis Carroll")]; + book.config.translators = Some(vec![Author::new("Kosztolányi Dezső")]); + book.config.language.name = "Hungarian".to_string(); + book.config.language.code = "hu".to_string(); + + expected.translations.insert("hu".to_string(), book); + } + + assert_eq!( + format!("{:?} {:?}", result.indent_spaces, result.translations.get("hu").unwrap()), + format!("{:?} {:?}", expected.indent_spaces, expected.translations.get("hu").unwrap()) + ); +} + +#[test] +fn it_parses_toc_and_chapters_in_minimal_book() { + let path = PathBuf::from(".").join("src").join("tests").join("book-minimal"); + let mut result = MDBook::new(&path); + + result.read_config(); + result.parse_books(); + + let mut babel = Chapter::default(); + if let TocItem::Numbered(ref fictions) = result.translations.get("en").unwrap().toc[1] { + if let TocItem::Numbered(ref ch) = fictions.clone().sub_items.unwrap()[1] { + babel = ch.chapter.clone(); + } + } + + assert_eq!(format!("{:?}", babel.translators.unwrap()), format!("{:?}", vec![Author::new("James E. Irby")])); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 00000000..b84de45d --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,11 @@ +#[cfg(test)] + +pub mod mdbook_test; +pub mod bookconfig_test; +pub mod chapter_test; +pub mod summary_test; +pub mod utils_test; +pub mod fs_test; +pub mod hbs_renderer_test; +pub mod hbs_renderer_multilang; +pub mod toc_test; diff --git a/src/tests/summary_test.rs b/src/tests/summary_test.rs new file mode 100644 index 00000000..648160a9 --- /dev/null +++ b/src/tests/summary_test.rs @@ -0,0 +1,400 @@ +#[cfg(test)] + +use std::path::PathBuf; + +use parse::summary::parse_level; + +#[test] +fn it_parses_summary_to_tocitems() { + let text = r#" +# Summary + +[Introduction](misc/introduction.md) + +- [mdBook](README.md) +- [Command Line Tool](cli/cli-tool.md) + - [init](cli/init.md) + - [build](cli/build.md) + - [watch](cli/watch.md) + - [serve](cli/serve.md) + - [test](cli/test.md) +- [Format](format/format.md) + - [SUMMARY.md](format/summary.md) + - [Configuration](format/config.md) + - [Theme](format/theme/theme.md) + - [index.hbs](format/theme/index-hbs.md) + - [Syntax highlighting](format/theme/syntax-highlighting.md) + - [MathJax Support](format/mathjax.md) + - [Rust code specific features](format/rust.md) +- [Rust Library](lib/lib.md) +----------- +[Contributors](misc/contributors.md) +"#; + + let result = parse_level(&mut text.split('\n').collect(), 0, vec![0], true).unwrap(); + + let expected = r#"[ + Unnumbered( + TocContent { + chapter: Chapter { + title: "Introduction", + path: "misc/introduction.md", + dest_path: Some( + "index.html" + ), + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: None + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "mdBook", + path: "README.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 1 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Command Line Tool", + path: "cli/cli-tool.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: Some( + [ + Numbered( + TocContent { + chapter: Chapter { + title: "init", + path: "cli/init.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 2, + 1 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "build", + path: "cli/build.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 2, + 2 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "watch", + path: "cli/watch.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 2, + 3 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "serve", + path: "cli/serve.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 2, + 4 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "test", + path: "cli/test.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 2, + 5 + ] + ) + } + ) + ] + ), + section: Some( + [ + 2 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Format", + path: "format/format.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: Some( + [ + Numbered( + TocContent { + chapter: Chapter { + title: "SUMMARY.md", + path: "format/summary.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 1 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Configuration", + path: "format/config.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 2 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Theme", + path: "format/theme/theme.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: Some( + [ + Numbered( + TocContent { + chapter: Chapter { + title: "index.hbs", + path: "format/theme/index-hbs.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 3, + 1 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Syntax highlighting", + path: "format/theme/syntax-highlighting.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 3, + 2 + ] + ) + } + ) + ] + ), + section: Some( + [ + 3, + 3 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "MathJax Support", + path: "format/mathjax.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 4 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Rust code specific features", + path: "format/rust.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 3, + 5 + ] + ) + } + ) + ] + ), + section: Some( + [ + 3 + ] + ) + } + ), + Numbered( + TocContent { + chapter: Chapter { + title: "Rust Library", + path: "lib/lib.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: Some( + [ + 4 + ] + ) + } + ), + Spacer, + Unnumbered( + TocContent { + chapter: Chapter { + title: "Contributors", + path: "misc/contributors.md", + dest_path: None, + authors: None, + translators: None, + description: None, + css_class: None + }, + sub_items: None, + section: None + } + ) +]"#; + + assert_eq!(expected, format!("{:#?}", result)); +} diff --git a/src/tests/toc_test.rs b/src/tests/toc_test.rs new file mode 100644 index 00000000..4b64139c --- /dev/null +++ b/src/tests/toc_test.rs @@ -0,0 +1,13 @@ +#[cfg(test)] + +use book::chapter::Chapter; +use book::toc::TocContent; + +#[test] +fn it_should_produce_the_section_as_string() { + let mut c = TocContent::default(); + c.section = Some(vec![1, 9, 4]); + let result = c.section_as_string(); + let expected = "1.9.4.".to_string(); + assert_eq!(result, expected); +} diff --git a/src/book/bookconfig_test.rs b/src/tests/utils_test.rs similarity index 84% rename from src/book/bookconfig_test.rs rename to src/tests/utils_test.rs index 34122628..76ef7447 100644 --- a/src/book/bookconfig_test.rs +++ b/src/tests/utils_test.rs @@ -1,56 +1,21 @@ #[cfg(test)] -use std::path::Path; +use std::collections::BTreeMap; + use serde_json; -use book::bookconfig::*; + +use utils::*; #[test] -fn it_parses_json_config() { - let text = r#" -{ - "title": "mdBook Documentation", - "description": "Create book from markdown files. Like Gitbook but implemented in Rust", - "author": "Mathieu David" -}"#; - - // TODO don't require path argument, take pwd - let mut config = BookConfig::new(Path::new(".")); - - config.parse_from_json_string(&text.to_string()); - - let mut expected = BookConfig::new(Path::new(".")); - expected.title = "mdBook Documentation".to_string(); - expected.author = "Mathieu David".to_string(); - expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string(); - - assert_eq!(format!("{:#?}", config), format!("{:#?}", expected)); -} - -#[test] -fn it_parses_toml_config() { - let text = r#" -title = "mdBook Documentation" -description = "Create book from markdown files. Like Gitbook but implemented in Rust" -author = "Mathieu David" -"#; - - // TODO don't require path argument, take pwd - let mut config = BookConfig::new(Path::new(".")); - - config.parse_from_toml_string(&text.to_string()); - - let mut expected = BookConfig::new(Path::new(".")); - expected.title = "mdBook Documentation".to_string(); - expected.author = "Mathieu David".to_string(); - expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string(); - - assert_eq!(format!("{:#?}", config), format!("{:#?}", expected)); +fn it_puts_last_name_first() { + let name = "Howard Philip Lovecraft"; + assert_eq!(last_name_first(name), "Lovecraft, Howard Philip"); } #[test] fn it_parses_json_nested_array_to_toml() { - // Example from: + // JSON example from: // toml-0.2.1/tests/valid/arrays-nested.json let text = r#" @@ -131,11 +96,10 @@ fn it_parses_json_nested_array_to_toml() { assert_eq!(format!("{:#?}", result), expected); } - #[test] fn it_parses_json_arrays_to_toml() { - // Example from: + // JSON example from: // toml-0.2.1/tests/valid/arrays.json let text = r#" diff --git a/src/theme/mod.rs b/src/theme/mod.rs deleted file mode 100644 index 130dd115..00000000 --- a/src/theme/mod.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::path::Path; -use std::fs::File; -use std::io::Read; - - -pub static INDEX: &'static [u8] = include_bytes!("index.hbs"); -pub static CSS: &'static [u8] = include_bytes!("book.css"); -pub static FAVICON: &'static [u8] = include_bytes!("favicon.png"); -pub static JS: &'static [u8] = include_bytes!("book.js"); -pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js"); -pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css"); -pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css"); -pub static JQUERY: &'static [u8] = include_bytes!("jquery-2.1.4.min.js"); -pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css"); -pub static FONT_AWESOME_EOT: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot"); -pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg"); -pub static FONT_AWESOME_TTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf"); -pub static FONT_AWESOME_WOFF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff"); -pub static FONT_AWESOME_WOFF2: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2"); -pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf"); - -/// The `Theme` struct should be used instead of the static variables because the `new()` method -/// will look if the user has a theme directory in his source folder and use the users theme instead -/// of the default. -/// -/// You should exceptionnaly use the static variables only if you need the default theme even if the -/// user has specified another theme. -pub struct Theme { - pub index: Vec, - pub css: Vec, - pub favicon: Vec, - pub js: Vec, - pub highlight_css: Vec, - pub tomorrow_night_css: Vec, - pub highlight_js: Vec, - pub jquery: Vec, -} - -impl Theme { - pub fn new(src: &Path) -> Self { - - // Default theme - let mut theme = Theme { - index: INDEX.to_owned(), - css: CSS.to_owned(), - favicon: FAVICON.to_owned(), - js: JS.to_owned(), - highlight_css: HIGHLIGHT_CSS.to_owned(), - tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(), - highlight_js: HIGHLIGHT_JS.to_owned(), - jquery: JQUERY.to_owned(), - }; - - // Check if the given path exists - if !src.exists() || !src.is_dir() { - return theme; - } - - // Check for individual files if they exist - - // index.hbs - if let Ok(mut f) = File::open(&src.join("index.hbs")) { - theme.index.clear(); // Reset the value, because read_to_string appends... - let _ = f.read_to_end(&mut theme.index); - } - - // book.js - if let Ok(mut f) = File::open(&src.join("book.js")) { - theme.js.clear(); - let _ = f.read_to_end(&mut theme.js); - } - - // book.css - if let Ok(mut f) = File::open(&src.join("book.css")) { - theme.css.clear(); - let _ = f.read_to_end(&mut theme.css); - } - - // favicon.png - if let Ok(mut f) = File::open(&src.join("favicon.png")) { - theme.favicon.clear(); - let _ = f.read_to_end(&mut theme.favicon); - } - - // highlight.js - if let Ok(mut f) = File::open(&src.join("highlight.js")) { - theme.highlight_js.clear(); - let _ = f.read_to_end(&mut theme.highlight_js); - } - - // highlight.css - if let Ok(mut f) = File::open(&src.join("highlight.css")) { - theme.highlight_css.clear(); - let _ = f.read_to_end(&mut theme.highlight_css); - } - - // tomorrow-night.css - if let Ok(mut f) = File::open(&src.join("tomorrow-night.css")) { - theme.tomorrow_night_css.clear(); - let _ = f.read_to_end(&mut theme.tomorrow_night_css); - } - - theme - } -} diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 858f1e83..36b520de 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -1,10 +1,15 @@ -use std::path::{Path, Component}; +use book::MDBook; + +use std::path::{Path, PathBuf, Component}; use std::error::Error; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::fs::{self, File}; -/// Takes a path to a file and try to read the file into a String +use glob::{glob, Pattern}; +use FILES; + +/// Takes a path to a file and try to read the file into a String pub fn file_to_string(path: &Path) -> Result> { let mut file = match File::open(path) { Ok(f) => f, @@ -24,6 +29,156 @@ pub fn file_to_string(path: &Path) -> Result> { Ok(content) } +/// Returns the contents of a static asset file by its path as &str. The path +/// should include "data/". +pub fn get_data_file(path: &str) -> Result> { + let content = match FILES.get(&path) { + Ok(x) => String::from_utf8(x.into_owned()).unwrap_or("".to_string()), + Err(e) => return Err(Box::new(e)), + }; + Ok(content) +} + +/// Writes the content of a data file from the embedded static assets to the +/// given destination path. Necessary folders will be created. +pub fn copy_data_file(src_path: &str, dest_path: &Path) -> Result<(), Box> { + let content = match FILES.get(&src_path) { + Ok(x) => x.into_owned(), + Err(e) => return Err(Box::new(e)), + }; + + let mut f: File = try!(create_file(dest_path)); + + match f.write_all(&content) { + Ok(x) => Ok(x), + Err(e) => Err(Box::new(e)) + } +} + +/// Writes selected data files from the embedded static assets to the given +/// destination path. +/// +/// `include_base` will be removed from the source path. This way the path +/// relative to the `dest_path` can be controlled. +/// +/// The following will copy all files under "data/html-template/", excluding +/// folders that start with "_", take the "data/html-template/" part off the +/// source path, and write the entries to "assets" folder. +/// +/// I.e. "data/html-template/css/book.css" will be written to +/// "assets/css/book.css". +/// +/// ```no_run +/// utils::fs::copy_data("data/html-template/**/*", +/// "data/html-template/", +/// vec!["data/html-template/_*"], +/// &Path::new("assets")); +/// ``` +pub fn copy_data(include_glob: &str, + include_base: &str, + exclude_globs: Vec<&str>, + dest_base: &Path) + -> Result<(), Box> { + + let results = FILES.file_names() + // narrow to files that match any of the include patterns + .filter(|x| glob_matches(x, &vec![include_glob])) + // exclude those which match any of the exclude patterns + .filter(|x| !glob_matches(x, &exclude_globs)) + // copy each to the destination such that `include_base` is removed from the source path + .map(|x| { + let mut s: &str = &x.replace(include_base, ""); + s = s.trim_left_matches("/"); + + let p = Path::new(s); + let dest_path = dest_base.join(p); + + copy_data_file(x, &dest_path) + }) + // only error results should remain + .filter(|x| !x.is_ok()); + + // collect errors as a String + let mut s = String::new(); + for i in results { + s.push_str(&format!("{:?}\n", i)); + } + + if s.len() > 1 as usize { + Err(Box::new(io::Error::new(io::ErrorKind::Other, s))) + } else { + Ok(()) + } +} + +/// Is there a match in any of the glob patterns? +pub fn glob_matches(text: &str, globs: &Vec<&str>) -> bool { + let patterns = globs.iter().map(|x| Pattern::new(x).unwrap()); + for pat in patterns { + if pat.matches(text) { + return true; + } + } + false +} + +/// Same logic as `copy_data()` but operating on actual files instead of +/// embedded static assets. +pub fn copy_files(include_glob: &str, + include_base: &str, + exclude_globs: Vec<&str>, + dest_base: &Path) + -> Result<(), Box> { + + let pathbufs: Vec = try!(glob(include_glob)) + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .collect::>(); + + let files = pathbufs.iter().filter_map(|x| x.to_str()); + + let results = + // narrow to files that match any of the include patterns + files.filter(|x| glob_matches(x, &vec![include_glob])) + // exclude those which match any of the exclude patterns + .filter(|x| !glob_matches(x, &exclude_globs)) + // copy each to the destination such that `include_base` is removed from the source path + .map(|x| { + let mut s: &str = &x.replace(include_base, ""); + s = s.trim_left_matches("/"); + + let p = Path::new(s); + let dest_path = dest_base.join(p); + + // make sure parent exists + if let Some(p) = dest_path.parent() { + try!(fs::create_dir_all(p)); + } + + if dest_path.is_dir() { + // if it is an already created dir + Ok(0) + } else { + // this will error on folders, so don't try!() on results + fs::copy(&x, &dest_path) + } + }) + // only error results should remain + .filter(|x| !x.is_ok()); + + // collect errors as a String + let mut s = String::new(); + for i in results { + s.push_str(&format!("{:?}\n", i)); + } + + if s.len() > 1 as usize { + Err(Box::new(io::Error::new(io::ErrorKind::Other, s))) + } else { + Ok(()) + } +} + /// Takes a path and returns a path containing just enough `../` to point to the root of the given path. /// /// This is mostly interesting for a relative path to point back to the directory from where the @@ -44,7 +199,6 @@ pub fn file_to_string(path: &Path) -> Result> { /// **note:** it's not very fool-proof, if you find a situation where it doesn't return the correct /// path. Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) or a /// [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it. - pub fn path_to_root(path: &Path) -> String { debug!("[fn]: path_to_root"); // Remove filename and add "../" for every directory @@ -64,11 +218,8 @@ pub fn path_to_root(path: &Path) -> String { }) } - - /// This function creates a file and returns it. But before creating the file it checks every /// directory in the path to see if it exists, and if it does not it will be created. - pub fn create_file(path: &Path) -> Result> { debug!("[fn]: create_file"); @@ -91,9 +242,14 @@ pub fn create_file(path: &Path) -> Result> { Ok(f) } -/// Removes all the content of a directory but not the directory itself +// TODO why not just delete the folder and re-create it? +/// Removes all the content of a directory but not the directory itself pub fn remove_dir_content(dir: &Path) -> Result<(), Box> { + if !dir.exists() { + return Ok(()); + } + for item in try!(fs::read_dir(dir)) { if let Ok(item) = item { let item = item.path(); @@ -107,11 +263,8 @@ pub fn remove_dir_content(dir: &Path) -> Result<(), Box> { Ok(()) } -/// -/// /// Copies all files of a directory to another one except the files with the extensions given in the /// `ext_blacklist` array - pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str]) -> Result<(), Box> { debug!("[fn] copy_files_except_ext"); // Check that from and to are different @@ -161,6 +314,53 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl Ok(()) } +pub fn create_with_str(path: &PathBuf, text: &str) -> Result { + match File::create(path) { + Err(e) => { + return Err(format!("File doesn't exist, error in creating: {:?}", e)); + }, + Ok(mut f) => { + let s = text.as_bytes(); + match f.write_all(s) { + Ok(_) => Ok(f), + Err(e) => Err(format!("File doesn't exist, error in writing: {:?}", e)) + } + }, + } +} + +/// Creates .gitignore in the project root folder. +pub fn create_gitignore(proj: &MDBook) { + let gitignore = proj.get_project_root().join(".gitignore"); + + if gitignore.exists() { + return; + } + + // Gitignore does not exist, create it + + // Figure out what is the user's output folder (can be default "book" or + // custom config). This will be a full path, so remove the project_root from + // it. + let a = proj.get_project_root(); + let b = proj.get_dest_base(); + let c = b.strip_prefix(&a).unwrap(); + let relative_dest = c.to_str().expect("Path could not be yielded into a string slice."); + + debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore); + + let mut f = File::create(&gitignore).expect("Could not create file."); + + let text = format!("*.swp +.#* +*~ +.DS_Store +{}", relative_dest); + + debug!("[*]: Writing to .gitignore"); + + f.write_all(&text.into_bytes()).expect("Could not write to file."); +} // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 59db1999..f3c17b55 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,12 +1,19 @@ +extern crate regex; +extern crate toml; + +use regex::Regex; + +use std::str::FromStr; +use std::error::Error; +use std::collections::BTreeMap; + +use serde_json; + pub mod fs; use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; - -/// -/// /// Wrapper around the pulldown-cmark parser and renderer to render markdown - pub fn render_markdown(text: &str) -> String { let mut s = String::with_capacity(text.len() * 3 / 2); @@ -18,3 +25,71 @@ pub fn render_markdown(text: &str) -> String { html::push_html(&mut s, p); s } + +pub fn last_name_first(name: &str) -> String { + let mut s = name.split_whitespace().collect::>(); + let last = s.pop().unwrap(); + format!("{}, {}", last, s.join(" ")) +} + +pub fn toml_str_to_btreemap(text: &str) -> Result, String> { + let mut parser = toml::Parser::new(text); + match parser.parse() { + Some(x) => Ok(x), + None => Err(format!("{:#?}", parser.errors)), + } +} + +/// Parses the string to JSON and converts it to BTreeMap. +pub fn json_str_to_btreemap(text: &str) -> Result, String> { + let c: serde_json::Value = match serde_json::from_str(text) { + Ok(x) => x, + Err(e) => return Err(format!("{:#?}", e)), + }; + + Ok(json_object_to_btreemap(&c.as_object().unwrap())) +} + +pub fn json_object_to_btreemap(json: &serde_json::Map) -> BTreeMap { + let mut config: BTreeMap = BTreeMap::new(); + + for (key, value) in json.iter() { + config.insert( + String::from_str(key).unwrap(), + json_value_to_toml_value(value.to_owned()) + ); + } + + config +} + +pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value { + match json { + serde_json::Value::Null => toml::Value::String("".to_string()), + serde_json::Value::Bool(x) => toml::Value::Boolean(x), + serde_json::Value::I64(x) => toml::Value::Integer(x), + serde_json::Value::U64(x) => toml::Value::Integer(x as i64), + serde_json::Value::F64(x) => toml::Value::Float(x), + serde_json::Value::String(x) => toml::Value::String(x), + serde_json::Value::Array(x) => { + toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect()) + }, + serde_json::Value::Object(x) => { + toml::Value::Table(json_object_to_btreemap(&x)) + }, + } +} + +pub fn strip_toml_header(text: &str) -> String { + let re: Regex = Regex::new(r"(?ms)^\+\+\+\n.*\n\+\+\+\n").unwrap(); + let mut out = text.to_owned(); + match re.captures(text) { + Some(caps) => { + if let Some(s) = caps.at(0) { + out = text.replace(s, ""); + } + }, + None => {} + } + out +}