Skip to content

[WIP] Plugin Experiment #336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0d0deb7
Pulled page rendering out into its own method
Michael-F-Bryan Jun 15, 2017
f946ef6
Pulled some more little bits out into their own helper functions
Michael-F-Bryan Jun 15, 2017
2568986
fixed a typo
Michael-F-Bryan Jun 15, 2017
b7aa78c
Minor refactoring
Michael-F-Bryan Jun 15, 2017
ff72598
Added a generic Plugin trait
Michael-F-Bryan Jun 15, 2017
41acfe6
Made the Renderer trait mutable so it can hold state
Michael-F-Bryan Jun 15, 2017
deab3ba
Tiny whitespace changes
Michael-F-Bryan Jun 15, 2017
fc2e167
Created an initial mdbook-core crate
Michael-F-Bryan Jun 16, 2017
7f3a279
Started setting up the config loading and stubbed out some top-level …
Michael-F-Bryan Jun 16, 2017
d09acdf
Added a generic Plugin trait
Michael-F-Bryan Jun 15, 2017
cca7acc
Made the Renderer trait mutable so it can hold state
Michael-F-Bryan Jun 15, 2017
ed4c963
Merge branch 'plugins' of github.com:Michael-F-Bryan/mdBook into plugins
Michael-F-Bryan Jun 16, 2017
c89a77b
Fixed ci so it'll work with mdbook-core as well
Michael-F-Bryan Jun 16, 2017
bb3f06a
Created an initial Book struct and ran rustfmt
Michael-F-Bryan Jun 16, 2017
1bf442f
Copied across the SUMMARY.md parser
Michael-F-Bryan Jun 16, 2017
5c62c72
Fleshing out the Loader::parse_summary() function
Michael-F-Bryan Jun 16, 2017
543e8fb
Partway through converting the SUMMARY.md parser to use the new datat…
Michael-F-Bryan Jun 17, 2017
6e63bdc
Added an unimplemented!() for the Loader::parse_summary() method (wil…
Michael-F-Bryan Jun 17, 2017
4b7de88
Fixed an issue where PathBuf doesn't implement Default on older versi…
Michael-F-Bryan Jun 17, 2017
859cfc8
Added a Visitor trait for doing manipulations on a Book
Michael-F-Bryan Jun 17, 2017
c1845bf
Added traits which plugins and renderers must implement and updated docs
Michael-F-Bryan Jun 17, 2017
7cb4064
Added some more stubs
Michael-F-Bryan Jun 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ serve = ["iron", "staticfile", "ws"]
doc = false
name = "mdbook"
path = "src/bin/mdbook.rs"

[workspace]
members = ["mdbook-core"]
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build --verbose
- cargo test --verbose
- cargo test --verbose --all

before_deploy:
# Generate artifacts for release
Expand Down
3 changes: 2 additions & 1 deletion ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ run_test_suite() {
# all the channels) to avoid significantly increasing the build times
if [ $TARGET = x86_64-unknown-linux-gnu ]; then
cargo build --target $TARGET --no-default-features --verbose
cargo test --target $TARGET --no-default-features --verbose
cargo test --target $TARGET --no-default-features --verbose --all
# note: we need the "--all" flag to tell travis to test both mdbook and mdbook-core
cargo clean
fi

Expand Down
16 changes: 16 additions & 0 deletions mdbook-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "mdbook-core"
version = "0.1.0"
authors = ["Michael Bryan <[email protected]>"]
workspace = ".."
description = "The core components of MDBook"

[dependencies]
error-chain = "*"
serde_derive = "*"
serde = "*"
toml = "*"
log = "*"

[dev-dependencies]
tempdir = "*"
112 changes: 112 additions & 0 deletions mdbook-core/src/book.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! The datatypes used to describe a `Book` in memory.

use std::path::PathBuf;
use std::fs::File;
use std::io::Read;

use errors::*;


/// An in-memory representation of the entire book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Book {
pub sections: Vec<BookItem>,
}

/// Any item which the book may contain.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BookItem {
// TODO: what on earth is this String for?
Chapter(String, Chapter),
Affix(Chapter),
Spacer,
}

/// A single chapter, which may or may not have sub-chapters.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Chapter {
/// The chapter name as specified in the `SUMMARY.md`.
pub name: String,
/// The file's location relative to the project root.
pub path: PathBuf,
/// The chapter's raw text.
pub contents: String,
/// Any sub-items in the chapter.
pub items: Vec<BookItem>,
}

impl Chapter {
pub fn new(name: String, path: PathBuf) -> Result<Chapter> {
let mut contents = String::new();
File::open(&path)?.read_to_string(&mut contents)?;

Ok(Chapter {
name: name,
path: path,
contents: contents,
items: Vec::new(),
})
}
}

/// Book walker implemented using the [Visitor Pattern] for performing
/// manipulations on a `Book`.
///
/// Each method of a `Visitor` is a hook which can potentially be overridden,
/// with the default methods simply recursively visiting each node in a `Book`
/// (e.g. the visit_section method by default calls visit::walk_section).
///
/// Each overridden visit method has full control over what happens with its
/// node, it can do its own traversal of the node's children, call visit::walk_*
/// to apply the default traversal algorithm, or prevent deeper traversal by
/// doing nothing.
///
/// > **Note:** The idea for implementing this was shamelessly stolen from
/// [syn].
///
/// [syn]: https://docs.serde.rs/syn/visit/trait.Visitor.html
/// [Visitor Pattern]: https://en.wikipedia.org/wiki/Visitor_pattern
pub trait Visitor: Sized {
fn visit_book(&mut self, book: &mut Book) {
visit::walk_book(self, book);
}
fn visit_section(&mut self, section: &mut BookItem) {
visit::walk_section(self, section);
}
fn visit_chapter(&mut self, ch: &mut Chapter) {
visit::walk_chapter(self, ch);
}
}


/// Helper functions which may be called by a `Visitor` to continue the default
/// traversal.
pub mod visit {
use super::{Chapter, Book, BookItem, Visitor};

/// A function a `Visitor` may call to make sure the rest of the `Book` gets
/// visited.
pub fn walk_book<V: Visitor>(visitor: &mut V, book: &mut Book) {
for section in book.sections.iter_mut() {
visitor.visit_section(section);
}
}

/// A function a `Visitor` may call to make sure the `Chapter` inside this
/// `BookItem` (if there is one) gets visited.
pub fn walk_section<V: Visitor>(visitor: &mut V, section: &mut BookItem) {
match *section {
BookItem::Chapter(_, ref mut ch) |
BookItem::Affix(ref mut ch) => visitor.visit_chapter(ch),
_ => {},
}
}

/// A function a `Visitor` may call to make sure the rest of the items in a
/// `Chapter` get visited.
pub fn walk_chapter<V: Visitor>(visitor: &mut V, chapter: &mut Chapter) {
for item in chapter.items.iter_mut() {
visitor.visit_section(item);
}
}
}
121 changes: 121 additions & 0 deletions mdbook-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Configuration management.

use std::path::{Path, PathBuf};
use std::fs::File;
use std::collections::HashMap;
use std::io::Read;
use std::ops::Deref;

use toml;
use errors::*;


pub type RendererConfig = HashMap<String, String>;

/// Try to load the config file from the provided directory, automatically
/// detecting the supported formats.
pub fn load_config<P: AsRef<Path>>(root: P) -> Result<Config> {
// TODO: add a `Config::from_json()` call here if the toml one fails
let toml_path = root.as_ref().join("book.toml");
debug!("[*] Attempting to load the config file from {}", toml_path.display());

Config::from_toml(&toml_path)
}

/// Configuration struct for a `mdbook` project directory.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[serde(default)]
pub struct Config {
source: PathBuf,
#[serde(rename = "renderer")]
renderers: HashMap<String, RendererConfig>,
title: String,
description: Option<String>,
author: Option<String>,
}

impl Config {
fn from_toml<P: AsRef<Path>>(path: P) -> Result<Config> {
let path = path.as_ref();
if !path.exists() {
bail!("The configuration file doesn't exist");
}

if !path.is_file() {
bail!("The provided path doesn't point to a file");
}

let mut contents = String::new();
File::open(&path)
.chain_err(|| "Couldn't open the config file for reading")?
.read_to_string(&mut contents)?;

toml::from_str(&contents).chain_err(|| "Config parsing failed")
}

pub fn source_directory(&self) -> &Path {
&self.source
}

pub fn title(&self) -> &str {
&self.title
}

pub fn author(&self) -> Option<&str> {
self.author.as_ref().map(Deref::deref)
}
}

impl Default for Config {
fn default() -> Config {
Config {
source: PathBuf::from("src"),
renderers: Default::default(),
author: None,
description: None,
title: "Example Book".to_owned(),
}
}
}


#[cfg(test)]
mod tests {
use super::*;
use toml;

#[test]
fn deserialize_basic_config() {
let src = r#"
[renderer.html]
"#;

let got: Config = toml::from_str(src).unwrap();
println!("{:#?}", got);

assert_eq!(got.source, PathBuf::from("src"));
assert!(got.renderers.contains_key("html"));
}

/// This test will read from the `book.toml` in the `book-example`, making
/// sure that the `Config::from_toml()` method works and that we maintain
/// backwards compatibility.
#[test]
fn read_a_working_config_toml() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("book-example")
.join("book.toml");
println!("{}", path.display());

let got = Config::from_toml(&path).unwrap();

assert_eq!(got.title, "mdBook Documentation");
assert_eq!(
got.description,
Some("Create book from markdown files. Like Gitbook but implemented in Rust".to_string())
);
assert_eq!(got.author, Some("Mathieu David".to_string()));
}
}
42 changes: 42 additions & 0 deletions mdbook-core/src/extensions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Traits which must be implemented by plugins and renderers.

use std::path::Path;

use book::Book;
use config::Config;
use errors::*;

/// The trait for rendering a `Book`.
pub trait Renderer {
/// Get the Renderer's name.
fn name(&self) -> &str;

/// Render the book to disk in the specified directory.
fn render(&mut self, book: &Book, config: &Config, output_directory: &Path);
}

/// A plugin for doing pre/post processing.
pub trait Plugin {
// TODO: How can a plugin apply renderer-specific operations?
// e.g. check for dead/broken links in the HTML renderer

/// A function which is run on the book's raw content immediately after
/// being loaded from disk.
///
/// This allows plugin creators to do any special preprocessing before it
/// reaches the markdown parser (e.g. MathJax substitution). The plugin may
/// or may not decide to make changes.
fn preprocess_book(&mut self, book: &mut Book) -> Result<()> {
Ok(())
}

/// The plugin function called after `mdbook` has loaded the book into
/// memory and just before the renderer writes it to disk.
///
/// This is typically when you would go through and update links or add
/// in a TOC. You'll typically want to use the `book::Visitor` trait to make
/// this easier.
fn postprocess_book(&mut self, _book: &mut Book) -> Result<()> {
Ok(())
}
}
59 changes: 59 additions & 0 deletions mdbook-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! This crate contains the core components of `mdbook`, allowing third parties
//! to seamlessly integrate with the rest of the project while generating a
//! rendered version of your document.
//!
//!
//! # Getting Started
//!
//! ```rust,no_run
//! use mdbook_core::Runner;
//! # use mdbook_core::errors::*;
//! # fn run() -> Result<()> {
//! let mut runner = Runner::new("/path/to/book/directory/")?;
//! runner.build()?;
//! # Ok(())
//! # }
//! # fn main() {run().unwrap()}
//! ```

// #![deny(missing_docs,
// missing_debug_implementations,
// missing_copy_implementations,
// trivial_casts,
// trivial_numeric_casts,
// unsafe_code,
// unused_import_braces,
// unused_qualifications,
// unstable_features)]

#[macro_use]
extern crate log;
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde_derive;
extern crate serde;

extern crate toml;

#[cfg(test)]
extern crate tempdir;

pub mod runner;
pub mod config;
pub mod loader;
pub mod book;
pub mod extensions;

pub use runner::Runner;
pub use book::Book;


/// Error types generated by `error-chain`.
pub mod errors {
error_chain!{
foreign_links {
Io(::std::io::Error) #[doc = "A `std::io::Error`"];
}
}
}
Loading