multilang

parses toc and chapters

renders html

hbs helpers and asset embedding

copy static assets by pattern

review

fix prev nav link

copy local assets when found

multilang renders

is_multilang as property

theme is template

cli init and build

bump version

structs diagram
This commit is contained in:
Gambhiro 2016-12-31 08:07:59 +00:00
parent 204a878ebf
commit e7ba6fac85
116 changed files with 4288 additions and 2387 deletions

8
.gitignore vendored
View File

@ -1,5 +1,9 @@
*.swp
.#*
Cargo.lock Cargo.lock
target target
TAGS
book-test src/tests/book-minimal/book
src/tests/book-minimal-with-assets/book
src/tests/book-wonderland-multilang/book
book-example/book book-example/book

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.0.15" version = "0.0.16"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"] authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
description = "create books from markdown files (like Gitbook)" description = "create books from markdown files (like Gitbook)"
documentation = "http://azerupi.github.io/mdBook/index.html" documentation = "http://azerupi.github.io/mdBook/index.html"
@ -9,20 +9,26 @@ keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0" license = "MPL-2.0"
readme = "README.md" readme = "README.md"
build = "build.rs" build = "build.rs"
include = ["data"]
exclude = [ exclude = [
"book-example/*", "book-example/*",
"src/theme/stylus", "data/html-template/_stylus",
] ]
[dependencies] [dependencies]
clap = "2.19.2" clap = "2.19.2"
handlebars = { version = "0.23.0", features = ["serde_type"] } handlebars = { version = "0.23.0", features = ["serde_type"] }
serde = "0.8" serde = "0.8"
serde_json = "0.8" serde_json = "0.8"
pulldown-cmark = "0.0.8" pulldown-cmark = "0.0.8"
regex = "0.1"
glob = "0.2"
log = "0.3" log = "0.3"
env_logger = "0.3" env_logger = "0.3"
toml = { version = "0.2", features = ["serde"] } toml = { version = "0.2", features = ["serde"] }
phf = "0.7"
includedir = "0.2"
# Watch feature # Watch feature
notify = { version = "2.5.5", optional = true } notify = { version = "2.5.5", optional = true }
@ -34,6 +40,9 @@ iron = { version = "0.4", optional = true }
staticfile = { version = "0.3", optional = true } staticfile = { version = "0.3", optional = true }
ws = { version = "0.5.1", optional = true} ws = { version = "0.5.1", optional = true}
[build-dependencies]
includedir_codegen = "0.2"
# Tests # Tests
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.4" tempdir = "0.3.4"

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,114 @@
@startuml
namespace book {
class MDBook {
project_root: PathBuf,
template_dir: PathBuf,
dest_base: PathBuf,
render_intent: RenderIntent,
translations: HashMap<String, Book>,
indent_spaces: i32,
livereload: bool,
new(project_root)
}
class book::Book {
config: BookConfig,
toc: Vec<TocItem>,
new(project_root)
}
class book::Chapter {
title: String,
path: PathBuf,
dest_path: Option<PathBuf>,
authors: Option<Vec<Author>>,
translators: Option<Vec<Author>>,
description: Option<String>,
css_class: Option<String>,
new(title, path)
}
}
namespace book::bookconfig {
class BookConfig {
dest: PathBuf,
src: PathBuf,
title: String,
subtitle: Option<String>,
description: Option<String>,
language: Language,
authors: Vec<Author>,
translators: Option<Vec<Author>>,
publisher: Option<Publisher>,
number_format: NumberFormat,
section_names: Vec<String>,
is_main_book: bool,
is_multilang: bool,
new(project_root)
}
class Author {
name: String,
file_as: String,
email: Option<String>,
new(name)
}
class Language {
name: String,
code: String,
new(name, code)
}
class Publisher {
name: String,
url: Option<String>,
logo_src: Option<PathBuf>,
new(name)
}
enum NumberFormat {
Arabic
Roman
Word
}
}
namespace book::toc {
class TocContent {
chapter: Chapter,
sub_items: Option<Vec<TocItem>>,
section: Option<Vec<i32>>,
}
enum TocItem {
Numbered "TocContent",
Unnumbered "TocContent",
Unlisted "TocContent",
Spacer,
}
}
class Renderer {
build(&self, project_root: &PathBuf),
render(&self, book_project: &MDBook),
}
@enduml

View File

@ -1,3 +1,5 @@
title = "mdBook Documentation" title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust" description = "Create books from markdown files. Like Gitbook but implemented in Rust."
author = "Mathieu David"
[[authors]]
name = "Mathieu David"

View File

@ -1,7 +1,5 @@
# Summary # Summary
[Introduction](misc/introduction.md)
- [mdBook](README.md) - [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md) - [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md) - [init](cli/init.md)
@ -18,5 +16,6 @@
- [MathJax Support](format/mathjax.md) - [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md) - [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md) - [Rust Library](lib/lib.md)
- [Structs](structs/structs.md)
----------- -----------
[Contributors](misc/contributors.md) [Contributors](misc/contributors.md)

View File

@ -1,3 +0,0 @@
# Introduction
A frontmatter chapter.

View File

@ -0,0 +1,6 @@
# Structs
![structs reorganized](images/structs-v0-0-16.png)
Diagram with [plantuml](http://plantuml.com)

View File

@ -1,23 +1,36 @@
// build.rs // build.rs
extern crate includedir_codegen;
use includedir_codegen::Compression;
use std::process::Command; use std::process::Command;
use std::env; use std::env;
use std::path::Path; use std::path::Path;
fn main() { 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") { if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
// Compile stylus stylesheet to css // Compile stylus stylesheet to css
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let theme_dir = Path::new(&manifest_dir).join("src/theme/"); let template_dir = Path::new(&manifest_dir).join("data/html-template/");
let stylus_dir = theme_dir.join("stylus/book.styl"); let stylus_dir = template_dir.join("_stylus/book.styl");
if !Command::new("stylus") if !Command::new("stylus")
.arg(format!("{}", stylus_dir.to_str().unwrap())) .arg(format!("{}", stylus_dir.to_str().unwrap()))
.arg("--out") .arg("--out")
.arg(format!("{}", theme_dir.to_str().unwrap())) .arg(format!("{}", template_dir.to_str().unwrap()))
.arg("--use") .arg("--use")
.arg("nib") .arg("nib")
.status().unwrap() .status().unwrap()

View File

@ -0,0 +1,99 @@
<!DOCTYPE HTML>
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="{{ path_to_root }}">
<link rel="shortcut icon" href="images/favicon.png">
<link rel="stylesheet" href="css/book.css">
<!-- TODO use OpenSans from local -->
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="css/highlight.css">
<link rel="stylesheet" href="css/tomorrow-night.css">
<!-- TODO use MathJax from local -->
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script src="js/jquery-2.1.4.min.js"></script>
</head>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
<div id="sidebar" class="sidebar">
{{#toc}}{{/toc}}
</div>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<h1 class="menu-title">{{ title }}</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</div>
</div>
<div id="content" class="content">
{{{ content }}}
</div>
<!-- Mobile navigation buttons -->
{{#previous}}
<a href="{{link}}" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
{{#previous}}
<a href="{{link}}" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
<script src="js/highlight.js"></script>
<script src="js/book.js"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,614 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
<!--Created by yEd 3.16.2.1-->
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
<key for="port" id="d1" yfiles.type="portgraphics"/>
<key for="port" id="d2" yfiles.type="portgeometry"/>
<key for="port" id="d3" yfiles.type="portuserdata"/>
<key attr.name="url" attr.type="string" for="node" id="d4"/>
<key attr.name="description" attr.type="string" for="node" id="d5"/>
<key for="node" id="d6" yfiles.type="nodegraphics"/>
<key for="graphml" id="d7" yfiles.type="resources"/>
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
<graph edgedefault="directed" id="G">
<data key="d0"/>
<node id="n0">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="100.75" width="245.0" x="55.75" y="32.0"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="n" textColor="#000000" verticalTextPosition="bottom" visible="true" width="42.501953125" x="101.2490234375" y="-21.96875">Config</y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n1">
<data key="d4"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="178.75" width="308.75" x="344.875" y="-46.0"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="sides" modelPosition="n" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.404296875" x="111.6728515625" y="-21.96875">Book Content</y:NodeLabel>
<y:Shape type="roundrectangle"/>
</y:ShapeNode>
</data>
</node>
<node id="n2">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="73.0" x="75.75" y="62.0"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.4296875" x="9.78515625" y="17.515625">CLI Args<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n3">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="73.0" x="207.75" y="62.5"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="64.134765625" x="4.4326171875" y="17.515625">book.toml<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n4">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="115.0" x="366.0" y="62.5"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.10546875" x="13.447265625" y="17.515625">SUMMARY.md<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n5">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="90.0" x="540.0" y="62.5"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.3046875" x="11.34765625" y="10.53125">markdown
chapters<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n6">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="128.0" x="528.0" y="577.7202141900937"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="100.181640625" x="13.9091796875" y="17.515625">template assets<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n7">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="119.0" width="128.0" x="89.0" y="257.0"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="72.3671875" x="27.81640625" y="4.0">BookConfig</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="73.84375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="91.451171875" x="2.0" y="29.96875">lang
project_root
book_dest
book_src
template_path<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n8">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="90.0" width="80.0" x="357.375" y="414.2175368139224"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="33.865234375" x="23.0673828125" y="4.0">Book</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="45.90625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="62.16015625" x="2.0" y="29.96875">config
metadata
toc<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n9">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="64.0" width="134.39999999999998" x="275.0" y="572.2202141900937"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="59.576171875" x="37.41191406249999" y="4.0">Renderer</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="82.46875" x="2.0" y="29.96875">render(book)<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n10">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="90.0" width="91.0" x="2.594879150390625" y="443.7175368139224"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.458984375" x="18.7705078125" y="4.0">MDBook</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="45.90625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="76.234375" x="2.0" y="29.96875">project_root
books
renderer<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n11">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="90.0" x="540.0" y="-20.5"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="47.62890625" x="21.185546875" y="17.515625">images<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n12">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="90.0" width="105.0" x="344.875" y="257.5"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="90.689453125" x="7.1552734375" y="4.0">BookMetadata</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="45.90625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="59.681640625" x="2.0" y="29.96875">title
author
publisher<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n13">
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="90.0" width="115.0" x="697.8658798197348" y="369.75"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.275390625" x="10.3623046875" y="4.0">Vec&lt;Chapter&gt;</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="27.4609375" x="2.0" y="29.96875">title
file<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<node id="n14">
<data key="d6">
<y:GenericNode configuration="ShinyPlateNode">
<y:Geometry height="53.0" width="115.0" x="395.0" y="-20.5"/>
<y:Fill color="#FF9900" transparent="false"/>
<y:BorderStyle hasColor="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.919921875" x="13.0400390625" y="10.53125">YAML headers
(optional)<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
</y:GenericNode>
</data>
</node>
<node id="n15">
<data key="d4"/>
<data key="d5"/>
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.0" width="110.0" x="520.0" y="257.5"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="34.265625" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.34375" x="22.328125" y="7.8671875">summary
parser<y:LabelModel>
<y:SmartNodeLabelModel distance="4.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
</y:ModelParameter>
</y:NodeLabel>
<y:Shape type="ellipse"/>
</y:ShapeNode>
</data>
</node>
<node id="n16">
<data key="d5"/>
<data key="d6">
<y:GenericNode configuration="com.yworks.entityRelationship.big_entity">
<y:Geometry height="90.0" width="115.0" x="515.0" y="369.75"/>
<y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
<y:BorderStyle color="#000000" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" backgroundColor="#B7C9E3" configuration="com.yworks.entityRelationship.label.name" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="94.884765625" x="10.0576171875" y="4.0">Vec&lt;TocItem&gt;</y:NodeLabel>
<y:NodeLabel alignment="left" autoSizePolicy="content" configuration="com.yworks.entityRelationship.label.attributes" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="top" visible="true" width="64.837890625" x="2.0" y="29.96875">content
sub_items<y:LabelModel>
<y:ErdAttributesNodeLabelModel/>
</y:LabelModel>
<y:ModelParameter>
<y:ErdAttributesNodeLabelModelParameter/>
</y:ModelParameter>
</y:NodeLabel>
<y:StyleProperties>
<y:Property class="java.lang.Boolean" name="y.view.ShadowNodePainter.SHADOW_PAINTING" value="true"/>
</y:StyleProperties>
</y:GenericNode>
</data>
</node>
<edge id="e0" source="n4" target="n4">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="423.5" y="89.0"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e1" source="n2" target="n7">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="-0.9141357421875003" ty="-42.31111111111111">
<y:Point x="132.33807373046875" y="181.30795288085938"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="right" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="left" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="125.921875" x="-112.17275522838912" y="92.57967131345038">behaviour control,
paths, target format<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="anywhere" side="left" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:Arc height="0.17399874329566956" ratio="0.003664793446660042" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e2" source="n3" target="n7">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="22.170099099683064" ty="-59.48682694471443">
<y:Point x="197.2362518310547" y="167.87789916992188"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="1.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="right" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.533203125" x="-38.136329861917886" y="103.87263713435809">paths<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" placement="anywhere" side="right" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:Arc height="-13.487004280090332" ratio="-0.29697197675704956" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e3" source="n3" target="n12">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="338.1900329589844" y="183.28660583496094"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="62.16015625" x="5.276900303809384" y="76.05124071206538">metadata<y:LabelModel>
<y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/>
</y:LabelModel>
<y:ModelParameter>
<y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.9422441392716253" segment="0"/>
</y:ModelParameter>
<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:Arc height="21.38491439819336" ratio="0.32557427883148193" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e4" source="n4" target="n15">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="511.2044982910156" y="176.3902587890625"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="71.634765625" x="-8.507094384363711" y="70.80742538314036">chapter list<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:Arc height="15.182718276977539" ratio="0.2471216768026352" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e5" source="n5" target="n15">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="600.8458251953125" y="186.82730102539062"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="left" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="112.603515625" x="13.477508337639165" y="56.54677946569382">chapter attributes
chapter content<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:Arc height="20.87361717224121" ratio="0.43092089891433716" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e6" source="n12" target="n8">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="397.375" y="380.8587646484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="0.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e7" source="n12" target="n12">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="397.375" y="302.5"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e8" source="n9" target="n9">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="342.20001220703125" y="604.22021484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e9" source="n9" target="n9">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="342.20001220703125" y="604.22021484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e10" source="n9" target="n9">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="342.20001220703125" y="604.22021484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e11" source="n8" target="n9">
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="-32.033203125">
<y:Point x="374.1595764160156" y="517.837646484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="4.865667343139648" ratio="0.15480542182922363" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e12" source="n3" target="n9">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="10.75" sy="1.0" tx="-41.46361445783125" ty="3.7797858099063433">
<y:Point x="255.0" y="608.0"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="right" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="105.326171875" x="-119.66308593750006" y="446.5169677734375">renderer specific
data<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e13" source="n6" target="n9">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="free" modelPosition="anywhere" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="89.265625" x="-95.627685546875" y="-24.204589843749886">template path<y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/>
</y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e14" source="n14" target="n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e15" source="n11" target="n5">
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e16" source="n7" target="n8">
<data key="d9"/>
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="-24.1875" ty="0.3449631860776208">
<y:Point x="254.77264404296875" y="400.8382873535156"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="-15.272879600524902" ratio="-0.23265674710273743" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e17" source="n13" target="n13">
<data key="d9"/>
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="755.3659057617188" y="414.75"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e18" source="n16" target="n8">
<data key="d9"/>
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="3.9375" ty="28.46996318607762">
<y:Point x="495.9642333984375" y="472.478271484375"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="23.108753204345703" ratio="0.496753990650177" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e19" source="n13" target="n13">
<data key="d9"/>
<data key="d10">
<y:ArcEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
<y:Point x="755.3659057617188" y="414.75"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:Arc height="0.0" ratio="1.0" type="fixedRatio"/>
</y:ArcEdge>
</data>
</edge>
<edge id="e20" source="n13" target="n16">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="none"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e21" source="n15" target="n16">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
</graph>
<data key="d7">
<y:Resources/>
</data>
</graphml>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,129 +0,0 @@
@startuml
class book::MDBook {
project_root : PathBuf
books : HashMap<&'a str, Book>
renderer : Box<Renderer>
livereload : Option<String>
indent_spaces: i32
multilingual: bool
new(root)
}
class book::book.Book {
config : BookConfig
metadata : BookMetadata
chapters: Vec<Chapter>
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<i32>
new(title, file)
}
namespace book::toc {
class TocItem {
content : TocContent
sub_items: Vec<TocItem>
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

View File

@ -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<TocItem> 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<TocItem>`, 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.

View File

@ -29,14 +29,9 @@ use std::path::{Path, PathBuf};
use clap::{App, ArgMatches, SubCommand, AppSettings}; 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::MDBook;
use mdbook::renderer::{Renderer, HtmlHandlebars};
use mdbook::utils;
const NAME: &'static str = "mdbook"; const NAME: &'static str = "mdbook";
@ -55,7 +50,7 @@ fn main() {
.about("Create boilerplate structure and files in the directory") .about("Create boilerplate structure and files in the directory")
// the {n} denotes a newline which will properly aligned in all help messages // 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("[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'")) .arg_from_usage("--force 'skip confirmation prompts'"))
.subcommand(SubCommand::with_name("build") .subcommand(SubCommand::with_name("build")
.about("Build the book from the markdown files") .about("Build the book from the markdown files")
@ -92,7 +87,6 @@ fn main() {
} }
} }
// Simple function that user comfirmation // Simple function that user comfirmation
fn confirm() -> bool { fn confirm() -> bool {
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
@ -104,25 +98,24 @@ fn confirm() -> bool {
} }
} }
// Init command implementation // Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> { fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args); 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 book_project.read_config();
try!(book.init()); book_project.parse_books();
// If flag `--theme` is present, copy theme to src // If flag `--copy-assets` is present, copy embedded assets to project root
if args.is_present("theme") { if args.is_present("copy-assets") {
// Skip this if `--force` is present // 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 warning
print!("\nCopying the default theme to {:?}", book.get_src()); println!("\nCopying the default assets to {:?}", book_project.get_project_root());
println!("could potentially overwrite files already present in that directory."); println!("This will overwrite files already present in that directory.");
print!("\nAre you sure you want to continue? (y/n) "); print!("Are you sure you want to continue? (y/n) ");
// Read answer from user and exit if it's not 'yes' // Read answer from user and exit if it's not 'yes'
if !confirm() { if !confirm() {
@ -132,20 +125,21 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
} }
} }
// Call the function that copies the theme // Copy the assets
try!(book.copy_theme()); try!(utils::fs::copy_data("data/**/*",
println!("\nTheme copied."); "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` if !args.is_present("force") {
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
if !args.is_present("force") && is_dest_inside_root {
println!("\nDo you want a .gitignore to be created? (y/n)"); println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() { if confirm() {
book.create_gitignore(); utils::fs::create_gitignore(&book_project);
println!("\n.gitignore created."); println!("\n.gitignore created.");
} }
} }
@ -155,115 +149,40 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
Ok(()) Ok(())
} }
// Build command implementation // Build command implementation
fn build(args: &ArgMatches) -> Result<(), Box<Error>> { fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args); 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(()) Ok(())
} }
// Watch command implementation // Watch command implementation
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> { fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args); // TODO watch
let mut book = MDBook::new(&book_dir).read_config(); println!("watch");
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!("");
}
});
Ok(()) Ok(())
} }
// Serve command implementation
// Watch command implementation
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> { fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
const RELOAD_COMMAND: &'static str = "reload"; // TODO serve
println!("serve");
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#"
<script type="text/javascript">
var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{
if (event.data === "{}") {{
socket.close();
location.reload(true); // force reload from server (not from cache)
}}
}};
window.onbeforeunload = function() {{
socket.close();
}}
</script>
"#, 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!("");
}
});
Ok(()) Ok(())
} }
fn test(args: &ArgMatches) -> Result<(), Box<Error>> { fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args); // TODO test
let mut book = MDBook::new(&book_dir).read_config(); println!("test");
try!(book.test());
Ok(()) Ok(())
} }
fn get_book_dir(args: &ArgMatches) -> PathBuf { fn get_book_dir(args: &ArgMatches) -> PathBuf {
if let Some(dir) = args.value_of("dir") { if let Some(dir) = args.value_of("dir") {
// Check if path is relative from current dir, or absolute... // 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() env::current_dir().unwrap()
} }
} }
// Calls the closure when a book source file is changed. This is blocking!
#[cfg(feature = "watch")]
fn trigger_on_change<F>(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::RecommendedWatcher, notify::Error> = 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);
},
}
}

View File

@ -1,80 +1,118 @@
use book::metadata::BookMetadata; use std::fs::File;
use book::chapter::Chapter; 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. use utils::fs::create_with_str;
/// Multiple `Book` structs are combined in the `MDBook` struct to support multi-language books. 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)] #[derive(Debug, Clone)]
pub struct Book { pub struct Book {
metadata: BookMetadata, pub config: BookConfig,
pub toc: Vec<TocItem>,
}
frontmatter: Vec<Chapter>, impl Default for Book {
mainmatter: Vec<Chapter>, fn default() -> Book {
backmatter: Vec<Chapter>, Book {
config: BookConfig::default(),
toc: vec![],
}
}
} }
impl Book { 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(), /// Creates a new book
mainmatter: Vec::new(), pub fn new(project_root: &PathBuf) -> Book {
backmatter: Vec::new(), 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 /// Walks through the TOC array and calls parse_or_create() on each
pub fn add_mainmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { pub fn parse_or_create_chapter_files(&mut self) -> Result<&mut Self, String> {
self.mainmatter.push(chapter); self.toc = self.process_them(&self.toc);
self Ok(self)
} }
/// Adds a new frontmatter chapter fn process_them(&self, items: &Vec<TocItem>) -> Vec<TocItem> {
pub fn add_frontmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { items.iter().map(|i|
self.frontmatter.push(chapter); match i {
self &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::<Vec<TocItem>>()
} }
/// Adds a new backmatter chapter fn process_toccontent(&self, c: &TocContent) -> TocContent {
pub fn add_backmatter_chapter(&mut self, chapter: Chapter) -> &mut Self { let mut content: TocContent = c.clone();
self.backmatter.push(chapter); if let Ok(ch) = content.chapter.clone().parse_or_create_using(&self.config.src) {
self content.chapter = ch.to_owned();
}
/// 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(&section[1..]))
},
} }
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 // TODO update
pub fn mut_metadata(&mut self) -> &mut BookMetadata {
&mut self.metadata
}
// Returns a reference to the metadata // /// This method takes a slice `&[x, y, z]` as parameter and returns the corresponding chapter.
pub fn metadata(&self) -> &BookMetadata { // /// For example, to retrieve chapter 2.3 we would use:
&self.metadata // /// ```
} // /// #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(&section[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
// }
} }

View File

@ -3,169 +3,214 @@ extern crate toml;
use std::process::exit; use std::process::exit;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::ffi::OsStr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr; use std::str::FromStr;
use serde_json; use serde_json;
use utils;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BookConfig { pub struct BookConfig {
root: PathBuf,
// Paths
pub dest: PathBuf, pub dest: PathBuf,
pub src: PathBuf, pub src: PathBuf,
pub theme_path: PathBuf,
// Metadata
/// The title of the book.
pub title: String, pub title: String,
pub author: String, /// The subtitle, when titles are in the form of "The Immense Journey: An
pub description: String, /// Imaginative Naturalist Explores the Mysteries of Man and Nature"
pub subtitle: Option<String>,
/// A brief description or summary of the book.
pub description: Option<String>,
pub language: Language,
pub authors: Vec<Author>,
pub translators: Option<Vec<Author>>,
/// Publisher's info
pub publisher: Option<Publisher>,
/// Chapter numbering scheme
pub number_format: NumberFormat,
/// Section names for nested Vec<Chapter> structures, defaults to `[ "Chapter", "Section", "Subsection" ]`
pub section_names: Vec<String>,
/// Whether this is the main book, in the case of translations.
pub is_main_book: bool,
pub is_multilang: bool,
}
pub indent_spaces: i32, impl Default for BookConfig {
multilingual: bool, 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 { 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(), pub fn new(project_root: &PathBuf) -> BookConfig {
author: String::new(), let mut conf = BookConfig::default();
description: String::new(),
indent_spaces: 4, // indentation used for SUMMARY.md // join paths to project_root
multilingual: false, // Prefer "" to "." and "src" to "./src", avoid "././src"
}
}
pub fn read_config(&mut self, root: &Path) -> &mut Self { let mut pr = project_root.clone();
if pr.as_os_str() == OsStr::new(".") {
debug!("[fn]: read_config"); pr = PathBuf::from("".to_string());
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.");
} }
self conf.dest = pr.join(&conf.dest);
} conf.src = pr.join(&conf.src);
pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self { conf
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<String, toml::Value>.
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
} }
/// 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<String, toml::Value>) -> &mut Self { pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
// Title, author, description // Paths
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("\"", "");
}
// Destination folder // Destination folder
if let Some(a) = config.get("dest") { if let Some(a) = config.get("dest") {
let mut dest = PathBuf::from(&a.to_string().replace("\"", "")); let 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);
}
self.set_dest(&dest); self.set_dest(&dest);
} }
// Source folder // Source folder
if let Some(a) = config.get("src") { if let Some(a) = config.get("src") {
let mut src = PathBuf::from(&a.to_string().replace("\"", "")); let src = PathBuf::from(&a.to_string().replace("\"", ""));
if src.is_relative() {
src = self.get_root().join(&src);
}
self.set_src(&src); self.set_src(&src);
} }
// Theme path folder // Metadata
if let Some(a) = config.get("theme_path") {
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", "")); let extract_authors_from_slice = |x: &[toml::Value]| -> Vec<Author> {
if theme_path.is_relative() { x.iter()
theme_path = self.get_root().join(&theme_path); .filter_map(|x| x.as_table())
} .map(|x| Author::from(x.to_owned()))
self.set_theme_path(&theme_path); .collect::<Vec<Author>>()
};
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 { if let Some(a) = config.get("description") {
&self.root 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::<Vec<String>>();
}
}
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 self
} }
@ -173,7 +218,7 @@ impl BookConfig {
&self.dest &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.dest = dest.to_owned();
self self
} }
@ -182,47 +227,152 @@ impl BookConfig {
&self.src &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.src = src.to_owned();
self 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<String>,
}
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 { pub fn file_as(mut self, file_as: &str) -> Self {
self.theme_path = theme_path.to_owned(); self.file_as = file_as.to_owned();
self
}
pub fn with_email(mut self, email: &str) -> Self {
self.email = Some(email.to_owned());
self self
} }
} }
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> { impl From<toml::Table> for Author {
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new(); fn from(data: toml::Table) -> Author {
let mut author = Author::new("The Author");
for (key, value) in json.iter() { if let Some(x) = data.get("name") {
config.insert( author.name = x.to_string().replace("\"", "");
String::from_str(key).unwrap(), }
json_value_to_toml_value(value.to_owned()) 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);
config }
} if let Some(x) = data.get("email") {
author.email = Some(x.to_string().replace("\"", ""));
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value { }
match json { author
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))
},
} }
} }
#[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<toml::Table> 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<String>,
/// path to publisher's logo image
logo_src: Option<PathBuf>,
}
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<toml::Table> 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,
}

View File

@ -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<BookItem>,
}
#[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<S>(&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);
}
}
}
}

View File

@ -1,71 +1,215 @@
extern crate regex;
extern crate toml;
use regex::Regex;
use std::path::{Path, PathBuf}; 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, use utils;
/// the location of the markdown file containing the content and eventually sub-chapters 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)] #[derive(Debug, Clone)]
pub struct Chapter { pub struct Chapter {
/// The title of the chapter. /// The title of the chapter.
title: String, pub title: String,
/// Path to chapter's markdown file.
file: PathBuf,
/// TODO The author of the chapter, or the book. /// Path to the chapter's markdown file, relative to the book's source
author: Author, /// directory.
/// TODO The description of the chapter. ///
description: String, /// `book.src.join(chapter.path)` points to the Markdown file, and
/// TODO Index number of the chapter in its level. This is the Vec index + 1. /// `book.dest.join(chapter.path).with_extension("html")` points to the
index: i32, /// output html file. This way if the user had a custom folder structure in
/// TODO CSS class that will be added to the page-level wrap div to allow customized chapter styles. /// their source folder, this is re-created in the destination folder.
class: String, pub path: PathBuf,
sub_chapters: Vec<Chapter>, /// Optional destination path to write to. Used when changing the first
/// chapter's path to index.html.
pub dest_path: Option<PathBuf>,
/// The author of the chapter, or the book.
pub authors: Option<Vec<Author>>,
/// The translators of the chapter, or the book.
pub translators: Option<Vec<Author>>,
/// The description of the chapter.
pub description: Option<String>,
/// CSS class that will be added to the page-level wrap div to allow
/// customized chapter styles.
pub css_class: Option<String>,
}
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 { 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 pub fn parse_or_create_using(&mut self, book_src_dir: &PathBuf) -> Result<&mut Self, String> {
author: Author::new(""),
description: "".to_string(), debug!("[fn] Chapter::parse_or_create() : {:?}", &self);
index: 0,
class: "".to_string(), 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. let mut text = String::new();
/// match File::open(src_path) {
/// For example: `chapter.get_sub_chapter(&[1,3])` will return the third sub-chapter of the first sub-chapter. Err(e) => { return Err(format!("Read error: {:?}", e)); },
pub fn get_sub_chapter(&self, section: &[usize]) -> Option<&Chapter> { Ok(mut f) => {
match section.len() { f.read_to_string(&mut text);
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(&section[1..]))
},
} }
let re: Regex = Regex::new(r"(?ms)^\+\+\+\n(?P<toml>.*)\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 { pub fn parse_from_btreemap(&mut self, data: &BTreeMap<String, toml::Value>) -> &mut Self {
&self.title
let extract_authors_from_slice = |x: &[toml::Value]| -> Vec<Author> {
x.iter()
.filter_map(|x| x.as_table())
.map(|x| Author::from(x.to_owned()))
.collect::<Vec<Author>>()
};
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 /// 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 sub_chapters(&self) -> &[Chapter] { pub fn read_content_using(&self, book_src_dir: &PathBuf) -> Result<String, Box<Error>> {
&self.sub_chapters
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)
} }
} }

View File

@ -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<Author>,
translators: Vec<Author>,
/// TODO Chapter numbering scheme
number_format: NumberFormat,
/// TODO Section names for nested Vec<Chapter> structures, such as `[
/// "Part", "Chapter", "Section" ]`
section_names: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Author {
name: String,
email: Option<String>,
}
#[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"),
}
}
}

View File

@ -1,123 +1,144 @@
pub mod bookitem; extern crate toml;
pub mod bookconfig;
pub mod metadata;
pub mod chapter;
pub mod book; 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; pub use self::book::Book;
use renderer::{Renderer, HtmlHandlebars};
use utils;
pub mod bookconfig_test; use std::env;
use std::process::exit;
pub use self::bookitem::{BookItem, BookItems}; use std::ffi::OsStr;
pub use self::bookconfig::BookConfig;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Read;
use std::error::Error; use std::error::Error;
use std::io; use std::collections::{HashMap, BTreeMap};
use std::io::Write;
use std::io::ErrorKind;
use std::process::Command;
use std::collections::HashMap;
use {theme, parse, utils}; #[derive(Debug, Clone)]
use renderer::{Renderer, HtmlHandlebars}; 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> { /// Path to the template for the renderer, relative to `project_root`.
root: PathBuf, /// The `render_intent` determines its default value.
dest: PathBuf, ///
src: PathBuf, /// A book doesn't necessarily has to have the template files. When not
theme_path: PathBuf, /// 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, /// Output base for all books, relative to `project_root`. Defaults to
pub author: String, /// `book`.
pub description: String, dest_base: PathBuf,
pub content: Vec<BookItem>, /// Informs other functions which renderer has been selected, either by
books: HashMap<&'a str, Book>, /// default or CLI argument.
renderer: Box<Renderer>, render_intent: RenderIntent,
livereload: Option<String>, // 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<String, Book>,
/// 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> { impl Default for MDBook {
/// Create a new `MDBook` struct with root directory `root` fn default() -> MDBook {
/// let mut proj: MDBook = MDBook {
/// Default directory paths: project_root: PathBuf::from("".to_string()),
/// template_dir: PathBuf::from("".to_string()),
/// - source: `root/src` dest_base: PathBuf::from("book".to_string()),
/// - output: `root/book` render_intent: RenderIntent::HtmlHandlebars,
/// - theme: `root/theme` translations: HashMap::new(),
/// indent_spaces: 4,
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest) livereload: false,
};
pub fn new(root: &Path) -> MDBook { proj.set_project_root(&env::current_dir().unwrap());
// sets default template_dir
if !root.exists() || !root.is_dir() { proj.set_render_intent(RenderIntent::HtmlHandlebars);
warn!("{:?} No directory with that name", root); proj
}
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,
}
} }
}
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html): #[derive(Debug, Clone)]
/// `(section: String, bookitem: &BookItem)` pub enum RenderIntent {
/// HtmlHandlebars,
/// ```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 { impl MDBook {
BookItems {
items: &self.content[..], /// Create a new `MDBook` struct with top-level project directory `project_root`
current_index: 0, pub fn new(project_root: &PathBuf) -> MDBook {
stack: Vec::new(), MDBook::default().set_project_root(project_root).clone()
}
} }
/// `init()` creates some boilerplate files and directories to get you started with your book. /// `init()` creates some boilerplate files and directories to get you started with your book.
/// ///
/// ```text /// ```text
/// book-test/ /// book-example/
/// ├── book /// ├── book
/// └── src /// └── src
/// ├── chapter_1.md /// ├── 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 /// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
/// `chapter_1.md` to the source directory. /// `chapter_1.md` to the source directory.
pub fn init(&mut self) -> Result<(), Box<Error>> { pub fn init(&mut self) -> Result<(), Box<Error>> {
debug!("[fn]: init"); debug!("[fn]: init");
if !self.root.exists() { if !self.project_root.exists() {
fs::create_dir_all(&self.root).unwrap(); fs::create_dir_all(&self.project_root).unwrap();
info!("{:?} created", &self.root); info!("{:?} created", &self.project_root);
} }
{ // Read book.toml if exists and populate .translations
self.read_config();
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));
}
}
},
}
}
debug!("[*]: init done"); debug!("[*]: init done");
Ok(()) Ok(())
} }
pub fn create_gitignore(&self) { /// Parses the `book.toml` file (if it exists) to extract the configuration parameters.
let gitignore = self.get_gitignore(); /// 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`
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<BookItem>` 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<Error>> {
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<Error>> {
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`
/// ///
/// ```no_run /// ```no_run
/// # extern crate mdbook; /// # extern crate mdbook;
/// # use mdbook::MDBook; /// # use mdbook::MDBook;
/// # use std::path::Path; /// # use std::path::Path;
/// # fn main() { /// # 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. /// 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) // TODO refactor to a helper that returns Result?
.read_config(&self.root)
.to_owned();
self.title = config.title; // TODO Maybe some error handling instead of exit(2), although it is a
self.description = config.description; // clear indication for the user that something is wrong and we can't
self.author = config.author; // fix it for them.
self.dest = config.dest; let read_file = |path: PathBuf| -> String {
self.src = config.src; let mut data = String::new();
self.theme_path = config.theme_path; 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 self
} }
/// You can change the default renderer to another one by using this method. The only requirement /// Configures MDBook properties and translations.
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
/// ///
/// ```no_run /// After parsing properties for MDBook struct, it removes them from the
/// extern crate mdbook; /// config (template_dir, livereload, etc.). The remaining keys on the main
/// use mdbook::MDBook; /// block will be interpreted as properties of the main book.
/// use mdbook::renderer::HtmlHandlebars;
/// # use std::path::Path;
/// ///
/// fn main() { /// `project_root` is ignored.
/// let mut book = MDBook::new(Path::new("mybook"))
/// .set_renderer(Box::new(HtmlHandlebars::new()));
/// ///
/// // In this example we replace the default renderer by the default renderer... /// - dest_base
/// // Don't forget to put your renderer in a Box /// - render_intent
/// } /// - template_dir
/// ``` /// - indent_spaces
/// /// - livereload
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()` pub fn parse_from_btreemap(&mut self, conf: &BTreeMap<String, toml::Value>) -> &mut Self {
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self { let mut config = conf.clone();
self.renderer = renderer;
self
}
pub fn test(&mut self) -> Result<(), Box<Error>> { if config.contains_key("project_root") {
// read in the chapters config.remove("project_root");
try!(self.parse_summary()); }
for item in self.iter() {
match *item { if let Some(a) = config.get("dest_base") {
BookItem::Chapter(_, ref ch) => { self.set_dest_base(&PathBuf::from(&a.to_string()));
if ch.path != PathBuf::new() { }
config.remove("dest_base");
let path = self.get_src().join(&ch.path); if let Some(a) = config.get("render_intent") {
if a.to_string() == "html".to_string() {
println!("[*]: Testing file: {:?}", path); self.set_render_intent(RenderIntent::HtmlHandlebars);
} else {
let output_result = Command::new("rustdoc") // offer some real choices later on...
.arg(&path) self.set_render_intent(RenderIntent::HtmlHandlebars);
.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<Error>);
}
}
},
_ => {},
} }
} }
Ok(()) config.remove("render_intent");
}
pub fn get_root(&self) -> &Path { // Parsing template_dir must be after render_intent, otherwise
&self.root // .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 if let Some(a) = config.get("livereload") {
match dest.is_absolute() { if let Some(b) = a.as_bool() {
true => { self.livereload = b;
self.dest = dest.to_owned(); }
}, }
false => { config.remove("livereload");
let dest = self.root.join(dest).to_owned();
self.dest = dest; // 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 self
} }
pub fn get_dest(&self) -> &Path { pub fn parse_books(&mut self) -> &mut Self {
&self.dest 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 // TODO error handling could be better here
match src.is_absolute() {
true => { let first_as_index = match self.render_intent {
self.src = src.to_owned(); RenderIntent::HtmlHandlebars => true,
}, };
false => {
let src = self.root.join(src).to_owned(); match b.parse_or_create_summary_file(first_as_index) {
self.src = src; 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 self
} }
pub fn get_src(&self) -> &Path { pub fn get_project_root(&self) -> &Path {
&self.src &self.project_root
} }
pub fn set_title(mut self, title: &str) -> Self { pub fn set_project_root(&mut self, path: &PathBuf) -> &mut MDBook {
self.title = title.to_owned(); if path.is_absolute() {
self self.project_root = path.to_owned();
} } else {
self.project_root = env::current_dir().unwrap().join(path).to_owned();
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_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 self
} }
pub fn get_theme_path(&self) -> &Path { pub fn get_template_dir(&self) -> PathBuf {
&self.theme_path self.project_root.join(&self.template_dir)
} }
// Construct book pub fn set_template_dir(&mut self, path: &PathBuf) -> &mut MDBook {
fn parse_summary(&mut self) -> Result<(), Box<Error>> { if path.as_os_str() == OsStr::new(".") {
// When append becomes stable, use self.content.append() ... self.template_dir = PathBuf::from("".to_string());
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md"))); } else {
Ok(()) 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<Error>> {
// // 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<Error>);
// }
// }
// },
// _ => {},
// }
// }
// 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(),
// }
// }
} }

76
src/book/toc.rs Normal file
View File

@ -0,0 +1,76 @@
use book::chapter::Chapter;
/// A Table of Contents is a `Vec<TocItem>`, 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<TocItem>`.
#[derive(Debug, Clone)]
pub struct TocContent {
pub chapter: Chapter,
pub sub_items: Option<Vec<TocItem>>,
/// Section indexes of the chapter
pub section: Option<Vec<i32>>,
}
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<i32>) -> 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::<Vec<String>>();
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(&section[1..]))
// },
// }
// }
}

View File

@ -69,19 +69,23 @@
//! //!
//! Make sure to take a look at it. //! 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;
extern crate serde_json; extern crate serde_json;
extern crate handlebars; extern crate handlebars;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate regex;
extern crate glob;
#[macro_use] extern crate log; #[macro_use] extern crate log;
pub mod book; pub mod book;
mod parse; mod parse;
pub mod renderer; pub mod renderer;
pub mod theme;
pub mod utils; pub mod utils;
pub mod tests;
pub use book::MDBook; pub use book::MDBook;
pub use book::BookItem;
pub use book::BookConfig;
pub use renderer::Renderer;

View File

@ -1,3 +1,3 @@
pub use self::summary::construct_bookitems; pub use self::summary::construct_tocitems;
pub mod summary; pub mod summary;

View File

@ -1,26 +1,41 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::File; use std::fs::File;
use std::io::{Read, Result, Error, ErrorKind}; use std::io::{Read, Result, Error, ErrorKind};
use book::bookitem::{BookItem, Chapter};
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> { use book::chapter::Chapter;
debug!("[fn]: construct_bookitems"); use book::toc::{TocItem, TocContent};
pub fn construct_tocitems(summary_path: &PathBuf, first_as_index: bool) -> Result<Vec<TocItem>> {
debug!("[fn]: construct_tocitems");
let mut summary = String::new(); 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"); 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"); debug!("[*]: Done parsing SUMMARY.md");
Ok(top_items) Ok(top_items)
} }
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> { pub fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>, first_as_index: bool) -> Result<Vec<TocItem>> {
debug!("[fn]: parse_level"); debug!("[fn]: parse_level");
let mut items: Vec<BookItem> = vec![]; let mut items: Vec<TocItem> = 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 // Construct the book recursively
while !summary.is_empty() { while !summary.is_empty() {
let item: BookItem; let item: TocItem;
// Indentation level of the line to parse // Indentation level of the line to parse
let level = try!(level(summary[0], 4)); let level = try!(level(summary[0], 4));
@ -35,58 +50,58 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
// Level can not be root level !! // Level can not be root level !!
// Add a sub-number to section // Add a sub-number to section
section.push(0); section.push(0);
let last = items.pop().expect("There should be at least one item since this can't be the root level"); let last = items.pop().expect("There should be at least one item since this can't be the root level");
item = if let BookItem::Chapter(ref s, ref ch) = last { item = match last {
let mut ch = ch.clone(); TocItem::Numbered(mut a) => {
ch.sub_items = try!(parse_level(summary, level, section.clone())); let sec = section.clone();
items.push(BookItem::Chapter(s.clone(), ch)); 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.. // Remove the last number from the section, because we got
section.pop(); // back to our level...
continue; section.pop();
} else { continue;
return Err(Error::new(ErrorKind::Other, },
format!("Your summary.md is messed up\n\n TocItem::Unnumbered(mut a) => {
Prefix, \ let sec = section.clone();
Suffix and Spacer elements can only exist on the root level.\n a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false)));
\ items.push(TocItem::Unnumbered(a));
Prefix elements can only exist before any chapter and there can be \ section.pop();
no chapters after suffix elements."))); 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 { } else {
// level and current_level are the same, parse the line // level and current_level are the same, parse the line
item = if let Some(parsed_item) = parse_line(summary[0]) { 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 { match parsed_item {
// error if level != 0 and BookItem is != Chapter
BookItem::Affix(_) | BookItem::Spacer if level > 0 => { // error if level != 0 and TocItem is != Numbered
return Err(Error::new(ErrorKind::Other, TocItem::Unnumbered(_) | TocItem::Spacer if level > 0 => {
format!("Your summary.md is messed up\n\n return Err(Error::new(ErrorKind::Other, ohnoes))
\
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 BookItem == Chapter and section == -1 // error if TocItem == Numbered or Unlisted and section == -1
BookItem::Chapter(_, _) if section[0] == -1 => { TocItem::Numbered(_) | TocItem::Unlisted(_) if section[0] == -1 => {
return Err(Error::new(ErrorKind::Other, return Err(Error::new(ErrorKind::Other, ohnoes))
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.")))
}, },
// Set section = -1 after suffix // Set section = -1 after unnumbered
BookItem::Affix(_) if section[0] > 0 => { TocItem::Unnumbered(_) if section[0] > 0 => {
section[0] = -1; section[0] = -1;
}, },
@ -94,12 +109,14 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
} }
match parsed_item { match parsed_item {
BookItem::Chapter(_, ch) => { TocItem::Numbered(mut content) => {
// Increment section // Increment section
let len = section.len() - 1; let len = section.len() - 1;
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, _ => parsed_item,
} }
@ -112,13 +129,33 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
} }
summary.remove(0); summary.remove(0);
items.push(item)
if first_as_index && !found_first {
let i = match item {
TocItem::Numbered(mut content) => {
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); debug!("[*]: Level: {:?}", items);
Ok(items) Ok(items)
} }
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> { fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
debug!("[fn]: level"); debug!("[fn]: level");
let mut spaces = 0; let mut spaces = 0;
@ -147,8 +184,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
Ok(level) Ok(level)
} }
fn parse_line(l: &str) -> Option<TocItem> {
fn parse_line(l: &str) -> Option<BookItem> {
debug!("[fn]: parse_line"); debug!("[fn]: parse_line");
// Remove leading and trailing spaces or tabs // Remove leading and trailing spaces or tabs
@ -157,7 +193,7 @@ fn parse_line(l: &str) -> Option<BookItem> {
// Spacers are "------" // Spacers are "------"
if line.starts_with("--") { if line.starts_with("--") {
debug!("[*]: Line is spacer"); debug!("[*]: Line is spacer");
return Some(BookItem::Spacer); return Some(TocItem::Spacer);
} }
if let Some(c) = line.chars().nth(0) { if let Some(c) = line.chars().nth(0) {
@ -166,8 +202,9 @@ fn parse_line(l: &str) -> Option<BookItem> {
'-' | '*' => { '-' | '*' => {
debug!("[*]: Line is list element"); debug!("[*]: Line is list element");
if let Some((name, path)) = read_link(line) { if let Some((title, path)) = read_link(line) {
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path))); let chapter = Chapter::new(title, path);
return Some(TocItem::Numbered(TocContent::new(chapter)));
} else { } else {
return None; return None;
} }
@ -176,8 +213,9 @@ fn parse_line(l: &str) -> Option<BookItem> {
'[' => { '[' => {
debug!("[*]: Line is a link element"); debug!("[*]: Line is a link element");
if let Some((name, path)) = read_link(line) { if let Some((title, path)) = read_link(line) {
return Some(BookItem::Affix(Chapter::new(name, path))); let chapter = Chapter::new(title, path);
return Some(TocItem::Unnumbered(TocContent::new(chapter)));
} else { } else {
return None; return None;
} }
@ -209,7 +247,7 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
return None; 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; start_delimitor = end_delimitor + 1;
if let Some(i) = line[start_delimitor..].find(')') { 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()); let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
Some((name, path)) Some((title, path))
} }

View File

@ -1,10 +1,13 @@
use renderer::html_handlebars::helpers; use renderer::html_handlebars::helpers;
use renderer::Renderer; use renderer::Renderer;
use book::MDBook; use book::{MDBook, Book};
use book::bookitem::BookItem; use book::chapter::Chapter;
use {utils, theme}; use book::toc::{TocItem, TocContent};
use utils;
use FILES;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::ffi::OsStr;
use std::fs::{self, File}; use std::fs::{self, File};
use std::error::Error; use std::error::Error;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
@ -15,7 +18,6 @@ use handlebars::Handlebars;
use serde_json; use serde_json;
use serde_json::value::ToJson; use serde_json::value::ToJson;
pub struct HtmlHandlebars; pub struct HtmlHandlebars;
impl HtmlHandlebars { impl HtmlHandlebars {
@ -25,308 +27,390 @@ impl HtmlHandlebars {
} }
impl Renderer for HtmlHandlebars { impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<(), Box<Error>> {
/// Prepares the project and calls `render()`.
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>> {
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<Error>> {
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"); debug!("[fn]: render");
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
// Load theme // Render the chapters of each book
let theme = theme::Theme::new(book.get_theme_path()); for (key, book) in &book_project.translations {
// Register template // Read in the page template
debug!("[*]: Register handlebars template"); let tmpl_path: &PathBuf = &book_project.get_template_dir().join("_layouts").join("page.hbs");
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index)))); 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 // Register template
debug!("[*]: Register handlebars helpers"); debug!("[*]: Register handlebars template");
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc)); try!(handlebars.register_template_string("page", s));
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
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 // Check if book's dest directory exists
let mut print_content: String = String::new();
// Check if dest directory exists // If this is a single book, config.dest default is
debug!("[*]: Check if destination directory exists"); // `project_root/book`, and the earlier check will cover this.
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")));
}
// Render a file for every entry in the book // If this is multi-language book, config.dest will
let mut index = true; // `project_book/book/key`, and so we check here for each book.
for item in book.iter() {
match *item { debug!("[*]: Check if book's destination directory exists");
BookItem::Chapter(_, ref ch) | if let Err(_) = fs::create_dir_all(book.config.get_dest()) {
BookItem::Affix(ref ch) => { return Err(Box::new(io::Error::new(
if ch.path != PathBuf::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); if book.config.is_multilang && book.config.is_main_book {
let mut f = try!(File::open(&path)); match book.toc[0] {
let mut content: String = String::new(); 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"); // almost the same as process_chapter(), but we have to
try!(f.read_to_string(&mut content)); // manipulate path_to_root in data and rendered_path
let mut content = try!(chapter.read_content_using(&book.config.src));
// Parse for playpen links // 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); content = helpers::playpen::render_playpen(&content, p);
} }
// Render markdown using the pulldown-cmark crate let mut data = try!(make_data(&book, &chapter, &content));
content = utils::render_markdown(&content);
print_content.push_str(&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.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 // Rendere the handlebars template with the data
debug!("[*]: Render template"); 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 // Write to file
let mut file = let mut file = try!(utils::fs::create_file(rendered_path));
try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html"))); info!("[*] Creating {:?} ✓", rendered_path);
info!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
try!(file.write_all(&rendered.into_bytes())); try!(file.write_all(&rendered_content.into_bytes()));
},
// Create an index.html from the first element in SUMMARY.md TocItem::Spacer => {},
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 <base href=...>
// on the front page, however this case should be very very rare...
content = content.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
try!(index_file.write_all(content.as_bytes()));
info!("[*] Creating index.html from {:?} ✓",
book.get_dest().join(&ch.path.with_extension("html")));
index = false;
}
}
},
_ => {},
} }
// 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(()) Ok(())
} }
} }
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> { impl HtmlHandlebars {
fn process_items(&self,
items: &Vec<TocItem>,
book: &Book,
handlebars: &Handlebars)
-> Result<(), Box<Error>> {
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<Error>> {
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<serde_json::Map<String, serde_json::Value>, Box<Error>> {
debug!("[fn]: make_data"); debug!("[fn]: make_data");
let mut data = serde_json::Map::new(); let mut data = serde_json::Map::new();
// Book data
data.insert("language".to_owned(), "en".to_json()); data.insert("language".to_owned(), "en".to_json());
data.insert("title".to_owned(), book.get_title().to_json()); data.insert("title".to_owned(), book.config.title.to_json());
data.insert("description".to_owned(), book.get_description().to_json()); data.insert("description".to_owned(), book.config.description.to_json());
data.insert("favicon".to_owned(), "favicon.png".to_json());
if let Some(livereload) = book.get_livereload() { // Chapter data
data.insert("livereload".to_owned(), livereload.to_json());
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![]; match path.to_str() {
Some(p) => {
for item in book.iter() { data.insert("path".to_owned(), p.to_json());
// Create the data to inject in the template },
let mut chapter = BTreeMap::new(); None => {
return Err(Box::new(io::Error::new(
match *item { io::ErrorKind::Other,
BookItem::Affix(ref ch) => { "Could not convert path to str")
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);
} }
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()); data.insert("chapters".to_owned(), chapters.to_json());
debug!("[*]: JSON constructed"); debug!("[*]: JSON constructed");
Ok(data) Ok(data)
} }
fn items_to_chapters(items: &Vec<TocItem>, book: &Book)
-> Result<Vec<serde_json::Map<String, serde_json::Value>>, Box<Error>> {
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<Vec<serde_json::Map<String, serde_json::Value>>, Box<Error>> {
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)
}

View File

@ -47,7 +47,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
let mut previous_chapter = BTreeMap::new(); let mut previous_chapter = BTreeMap::new();
// Chapter title // Chapter title
match previous.get("name") { match previous.get("title") {
Some(n) => { Some(n) => {
debug!("[*]: Inserting title: {}", n); debug!("[*]: Inserting title: {}", n);
previous_chapter.insert("title".to_owned(), n.to_json()) 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(()) Ok(())
} }
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> { pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: next (handlebars helper)"); 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' // Create new BTreeMap to extend the context: 'title' and 'link'
let mut next_chapter = BTreeMap::new(); let mut next_chapter = BTreeMap::new();
match item.get("name") { match item.get("title") {
Some(n) => { Some(n) => {
debug!("[*]: Inserting title: {}", n); debug!("[*]: Inserting title: {}", n);
next_chapter.insert("title".to_owned(), n.to_json()); next_chapter.insert("title".to_owned(), n.to_json());

View File

@ -97,11 +97,11 @@ impl HelperDef for RenderToc {
try!(rc.writer.write("</strong> ".as_bytes())); try!(rc.writer.write("</strong> ".as_bytes()));
} }
if let Some(name) = item.get("name") { if let Some(title) = item.get("title") {
// Render only inline code blocks // Render only inline code blocks
// filter all events that are not 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 { match event {
&Event::Start(Tag::Code) | &Event::Start(Tag::Code) |
&Event::End(Tag::Code) => true, &Event::End(Tag::Code) => true,
@ -112,7 +112,7 @@ impl HelperDef for RenderToc {
}); });
// render markdown to html // 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); html::push_html(&mut markdown_parsed_name, parser);
// write to the handlebars template // write to the handlebars template

View File

@ -2,8 +2,23 @@ pub use self::html_handlebars::HtmlHandlebars;
mod html_handlebars; mod html_handlebars;
use book::MDBook;
use std::error::Error; use std::error::Error;
use std::path::PathBuf;
pub trait Renderer { pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
/// 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<TocItem>`.
///
/// Finally it calls `render()` to process the chapters and static assets.
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>>;
/// Responsible for rendering the chapters and copying static assets.
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>>;
} }

View File

@ -0,0 +1,99 @@
<!DOCTYPE HTML>
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="{{ path_to_root }}">
<link rel="shortcut icon" href="images/favicon.png">
<link rel="stylesheet" href="css/book.css">
<!-- TODO use OpenSans from local -->
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="css/highlight.css">
<link rel="stylesheet" href="css/tomorrow-night.css">
<!-- TODO use MathJax from local -->
<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script src="js/jquery-2.1.4.min.js"></script>
</head>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
<div id="sidebar" class="sidebar">
{{#toc}}{{/toc}}
</div>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<h1 class="menu-title">{{ title }}</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</div>
</div>
<div id="content" class="content">
{{{ content }}}
</div>
<!-- Mobile navigation buttons -->
{{#previous}}
<a href="{{link}}" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
{{#previous}}
<a href="{{link}}" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
<script src="js/highlight.js"></script>
<script src="js/book.js"></script>
</body>
</html>

View File

@ -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+ */;
}
}

View File

@ -0,0 +1,3 @@
title = "Labyrinths"
subtitle = "Selected Stories and Other Writings"
author = "Jorge Luis Borges"

View File

@ -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)

View File

@ -0,0 +1 @@
# Chronology

View File

@ -0,0 +1 @@
# Essays

View File

@ -0,0 +1 @@
# Kafka

View File

@ -0,0 +1 @@
# Fictions

View File

@ -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.

View File

@ -0,0 +1,5 @@
+++
title = "The Circular Ruins"
+++
# Ruins

View File

@ -0,0 +1 @@
# Introduction

View File

@ -0,0 +1,3 @@
title = "Labyrinths"
subtitle = "Selected Stories and Other Writings"
author = "Jorge Luis Borges"

View File

@ -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)

View File

@ -0,0 +1 @@
# Chronology

View File

@ -0,0 +1 @@
# Essays

View File

@ -0,0 +1 @@
# Kafka

View File

@ -0,0 +1 @@
# Fictions

View File

@ -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.

View File

@ -0,0 +1,5 @@
+++
title = "The Circular Ruins"
+++
# Ruins

View File

@ -0,0 +1 @@
# Introduction

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -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" }

View File

@ -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]()

View File

@ -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.

View File

@ -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.

View File

@ -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!

View File

@ -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.

View File

@ -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]()

View File

@ -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 à laise.
« 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 dun 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.

View File

@ -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 quelle ne pouvait sexprimer correctement) : « Voilà que je mallonge
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 moccuper 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 daller 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 sy prendrait. « Il faudra les
envoyer par le messager, » pensa-t-elle ; « quelle étrange chose denvoyer des
présents à ses pieds ! Et ladresse donc ! Cest cela qui sera drôle.

View File

@ -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 à sennuyer 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 dimages, pas de dialogues ! « La belle avance,
» pensait Alice, « quun livre sans images, sans causeries ! »
Elle sétait mise à réfléchir, (tant bien que mal, car la chaleur du jour
lendormait 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 delle.

View File

@ -0,0 +1,18 @@
# Alice au pays des merveilles
![Queen of Hearts](images/Queen.jpg)
[LAuteur désire exprimer ici sa reconnaissance envers le Traducteur de ce quil
a remplacé par des parodies de sa composition quelques parodies de morceaux de
poésie anglais, qui navaient de valeur que pour des enfants anglais ; et aussi,
de ce quil 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 londe
Que dorent de brûlants rayons ;
Sa marche lente et vagabonde
Témoigne que des bras mignons,
Pleins dardeur, mais encore novices,
Tout fiers de ce nouveau travail,
Mènent au gré de leurs caprices
Les rames et le gouvernail.

View File

@ -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]()

View File

@ -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.

View File

@ -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.

Some files were not shown because too many files have changed in this diff Show More