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
target
book-test
TAGS
src/tests/book-minimal/book
src/tests/book-minimal-with-assets/book
src/tests/book-wonderland-multilang/book
book-example/book

View File

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

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"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"
description = "Create books from markdown files. Like Gitbook but implemented in Rust."
[[authors]]
name = "Mathieu David"

View File

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

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
extern crate includedir_codegen;
use includedir_codegen::Compression;
use std::process::Command;
use std::env;
use std::path::Path;
fn main() {
includedir_codegen::start("FILES")
.dir("data", Compression::Gzip)
.build("data.rs")
.unwrap();
// TODO this using cargo as a Makefile. This is only for development, it
// doesn't have to be part of the production auto-build. Use either a
// Makefile or an npm command if stylus comes from npm anyway.
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
// Compile stylus stylesheet to css
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
let stylus_dir = theme_dir.join("stylus/book.styl");
let template_dir = Path::new(&manifest_dir).join("data/html-template/");
let stylus_dir = template_dir.join("_stylus/book.styl");
if !Command::new("stylus")
.arg(format!("{}", stylus_dir.to_str().unwrap()))
.arg("--out")
.arg(format!("{}", theme_dir.to_str().unwrap()))
.arg(format!("{}", template_dir.to_str().unwrap()))
.arg("--use")
.arg("nib")
.status().unwrap()

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};
// Uses for the Watch feature
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;
use mdbook::MDBook;
use mdbook::renderer::{Renderer, HtmlHandlebars};
use mdbook::utils;
const NAME: &'static str = "mdbook";
@ -55,7 +50,7 @@ fn main() {
.about("Create boilerplate structure and files in the directory")
// the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--copy-assets 'Copies the default assets (css, layout template, etc.) into your project folder'")
.arg_from_usage("--force 'skip confirmation prompts'"))
.subcommand(SubCommand::with_name("build")
.about("Build the book from the markdown files")
@ -92,7 +87,6 @@ fn main() {
}
}
// Simple function that user comfirmation
fn confirm() -> bool {
io::stdout().flush().unwrap();
@ -104,25 +98,24 @@ fn confirm() -> bool {
}
}
// Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir);
let mut book_project = MDBook::new(&book_dir);
// Call the function that does the initialization
try!(book.init());
book_project.read_config();
book_project.parse_books();
// If flag `--theme` is present, copy theme to src
if args.is_present("theme") {
// If flag `--copy-assets` is present, copy embedded assets to project root
if args.is_present("copy-assets") {
// Skip this if `--force` is present
if !args.is_present("force") {
if book_project.get_project_root().join("assets").exists() && !args.is_present("force") {
// Print warning
print!("\nCopying the default theme to {:?}", book.get_src());
println!("could potentially overwrite files already present in that directory.");
print!("\nAre you sure you want to continue? (y/n) ");
println!("\nCopying the default assets to {:?}", book_project.get_project_root());
println!("This will overwrite files already present in that directory.");
print!("Are you sure you want to continue? (y/n) ");
// Read answer from user and exit if it's not 'yes'
if !confirm() {
@ -132,20 +125,21 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
}
}
// Call the function that copies the theme
try!(book.copy_theme());
println!("\nTheme copied.");
// Copy the assets
try!(utils::fs::copy_data("data/**/*",
"data/",
vec![],
&book_project.get_project_root().join("assets")));
println!("\nAssets copied.");
}
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
if !args.is_present("force") && is_dest_inside_root {
if !args.is_present("force") {
println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() {
book.create_gitignore();
utils::fs::create_gitignore(&book_project);
println!("\n.gitignore created.");
}
}
@ -155,115 +149,40 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
Ok(())
}
// Build command implementation
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
try!(book.build());
// TODO figure out render format intent when we acutally have different renderers
let renderer = HtmlHandlebars::new();
try!(renderer.build(&book_dir));
Ok(())
}
// Watch command implementation
#[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
trigger_on_change(&mut book, |event, book| {
if let Some(path) = event.path {
println!("File changed: {:?}\nBuilding book...\n", path);
match book.build() {
Err(e) => println!("Error while building: {:?}", e),
_ => {},
}
println!("");
}
});
// TODO watch
println!("watch");
Ok(())
}
// Watch command implementation
// Serve command implementation
#[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("ws-port").unwrap_or("3001");
let interface = args.value_of("interface").unwrap_or("localhost");
let public_address = args.value_of("address").unwrap_or(interface);
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);
book.set_livereload(format!(r#"
<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(|_| {
|_| {
// TODO serve
println!("serve");
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(())
}
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
try!(book.test());
// TODO test
println!("test");
Ok(())
}
fn get_book_dir(args: &ArgMatches) -> PathBuf {
if let Some(dir) = args.value_of("dir") {
// Check if path is relative from current dir, or absolute...
@ -277,58 +196,3 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
env::current_dir().unwrap()
}
}
// Calls the closure when a book source file is changed. This is blocking!
#[cfg(feature = "watch")]
fn trigger_on_change<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 book::chapter::Chapter;
use std::fs::File;
use std::path::{Path, PathBuf};
use book::bookconfig::BookConfig;
use book::toc::{TocItem, TocContent};
/// The `Book` struct contains the metadata and chapters for one language of the book.
/// Multiple `Book` structs are combined in the `MDBook` struct to support multi-language books.
use utils::fs::create_with_str;
use parse::construct_tocitems;
/// The `Book` struct contains the metadata (config) and chapters (toc) for one
/// language of the book. Multiple `Book` structs are combined in the `MDBook`
/// struct to support multi-language books.
#[derive(Debug, Clone)]
pub struct Book {
metadata: BookMetadata,
pub config: BookConfig,
pub toc: Vec<TocItem>,
}
frontmatter: Vec<Chapter>,
mainmatter: Vec<Chapter>,
backmatter: Vec<Chapter>,
impl Default for Book {
fn default() -> Book {
Book {
config: BookConfig::default(),
toc: vec![],
}
}
}
impl Book {
/// Creates a new book with the given title, chapters are added with the
/// `add_frontmatter_chapter`, `add_mainmatter_chapter`,
/// `add_backmatter_chapter` methods
pub fn new(title: &str) -> Self {
Book {
metadata: BookMetadata::new(title),
frontmatter: Vec::new(),
mainmatter: Vec::new(),
backmatter: Vec::new(),
}
/// Creates a new book
pub fn new(project_root: &PathBuf) -> Book {
let conf = BookConfig::new(project_root);
let mut book = Book::default();
book.config = conf;
book
}
/// Adds a new mainmatter chapter
pub fn add_mainmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
self.mainmatter.push(chapter);
self
/// 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"));
}
/// Adds a new frontmatter chapter
pub fn add_frontmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
self.frontmatter.push(chapter);
self
// 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 backmatter chapter
pub fn add_backmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
self.backmatter.push(chapter);
self
/// Walks through the TOC array and calls parse_or_create() on each
pub fn parse_or_create_chapter_files(&mut self) -> Result<&mut Self, String> {
self.toc = self.process_them(&self.toc);
Ok(self)
}
/// 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..]))
},
fn process_them(&self, items: &Vec<TocItem>) -> Vec<TocItem> {
items.iter().map(|i|
match i {
&TocItem::Numbered(ref c) => TocItem::Numbered(self.process_toccontent(c)),
&TocItem::Unnumbered(ref c) => TocItem::Unnumbered(self.process_toccontent(c)),
&TocItem::Unlisted(ref c) => TocItem::Unlisted(self.process_toccontent(c)),
&TocItem::Spacer => TocItem::Spacer,
}
).collect::<Vec<TocItem>>()
}
/// Returns a mutable reference to the metadata for modification
pub fn mut_metadata(&mut self) -> &mut BookMetadata {
&mut self.metadata
fn process_toccontent(&self, c: &TocContent) -> TocContent {
let mut content: TocContent = c.clone();
if let Ok(ch) = content.chapter.clone().parse_or_create_using(&self.config.src) {
content.chapter = ch.to_owned();
}
if let Some(s) = content.sub_items {
let subs = self.process_them(&s);
content.sub_items = Some(subs);
}
content
}
// Returns a reference to the metadata
pub fn metadata(&self) -> &BookMetadata {
&self.metadata
}
// TODO update
// /// 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..]))
// },
// }
// }
// /// 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::fs::File;
use std::io::Read;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::str::FromStr;
use serde_json;
use utils;
#[derive(Debug, Clone)]
pub struct BookConfig {
root: PathBuf,
// Paths
pub dest: PathBuf,
pub src: PathBuf,
pub theme_path: PathBuf,
// Metadata
/// The title of the book.
pub title: String,
pub author: String,
pub description: String,
/// The subtitle, when titles are in the form of "The Immense Journey: An
/// Imaginative Naturalist Explores the Mysteries of Man and Nature"
pub subtitle: Option<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,
multilingual: bool,
impl Default for BookConfig {
fn default() -> BookConfig {
BookConfig {
dest: PathBuf::from("book".to_string()),
src: PathBuf::from("src".to_string()),
title: "Untitled".to_string(),
subtitle: None,
description: None,
language: Language::default(),
authors: vec![Author::new("The Author").file_as("Author, The")],
translators: None,
publisher: None,
number_format: NumberFormat::Arabic,
section_names: vec!["Chapter".to_string(),
"Section".to_string(),
"Subsection".to_string()],
is_main_book: false,
is_multilang: false,
}
}
}
impl BookConfig {
pub fn new(root: &Path) -> Self {
BookConfig {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
theme_path: root.join("theme"),
title: String::new(),
author: String::new(),
description: String::new(),
pub fn new(project_root: &PathBuf) -> BookConfig {
let mut conf = BookConfig::default();
indent_spaces: 4, // indentation used for SUMMARY.md
multilingual: false,
}
// join paths to project_root
// Prefer "" to "." and "src" to "./src", avoid "././src"
let mut pr = project_root.clone();
if pr.as_os_str() == OsStr::new(".") {
pr = PathBuf::from("".to_string());
}
pub fn read_config(&mut self, root: &Path) -> &mut Self {
conf.dest = pr.join(&conf.dest);
conf.src = pr.join(&conf.src);
debug!("[fn]: read_config");
let read_file = |path: PathBuf| -> String {
let mut data = String::new();
let mut f: File = match File::open(&path) {
Ok(x) => x,
Err(_) => {
error!("[*]: Failed to open {:?}", &path);
exit(2);
}
};
if let Err(_) = f.read_to_string(&mut data) {
error!("[*]: Failed to read {:?}", &path);
exit(2);
}
data
};
// Read book.toml or book.json if exists
if Path::new(root.join("book.toml").as_os_str()).exists() {
debug!("[*]: Reading config");
let data = read_file(root.join("book.toml"));
self.parse_from_toml_string(&data);
} else if Path::new(root.join("book.json").as_os_str()).exists() {
debug!("[*]: Reading config");
let data = read_file(root.join("book.json"));
self.parse_from_json_string(&data);
} else {
debug!("[*]: No book.toml or book.json was found, using defaults.");
}
self
}
pub fn parse_from_toml_string(&mut self, data: &String) -> &mut Self {
let mut parser = toml::Parser::new(&data);
let config = match parser.parse() {
Some(x) => {x},
None => {
error!("[*]: Toml parse errors in book.toml: {:?}", parser.errors);
exit(2);
}
};
self.parse_from_btreemap(&config);
self
}
/// Parses the string to JSON and converts it to BTreeMap<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
conf
}
/// Parses recognized keys from a BTreeMap one by one. Not trying to
/// directly de-serialize to `BookConfig` so that we can provide some
/// convenient shorthands for the user.
///
/// `book.toml` is a user interface, not an app data store, we never have to
/// write data back to it.
///
/// Parses author when given as an array, or when given as a hash key to
/// make declaring just an author name easy.
///
/// Both of these express a single author:
///
/// ```toml
/// [[authors]]
/// name = "Marcus Aurelius Antoninus"
/// ```
///
/// Or:
///
/// ```toml
/// name = "Marcus Aurelius Antoninus"
/// ```
///
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
// Title, author, description
if let Some(a) = config.get("title") {
self.title = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("author") {
self.author = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("description") {
self.description = a.to_string().replace("\"", "");
}
// Paths
// Destination folder
if let Some(a) = config.get("dest") {
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
// If path is relative make it absolute from the parent directory of src
if dest.is_relative() {
dest = self.get_root().join(&dest);
}
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
self.set_dest(&dest);
}
// Source folder
if let Some(a) = config.get("src") {
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
if src.is_relative() {
src = self.get_root().join(&src);
}
let src = PathBuf::from(&a.to_string().replace("\"", ""));
self.set_src(&src);
}
// Theme path folder
if let Some(a) = config.get("theme_path") {
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
if theme_path.is_relative() {
theme_path = self.get_root().join(&theme_path);
}
self.set_theme_path(&theme_path);
// Metadata
let extract_authors_from_slice = |x: &[toml::Value]| -> Vec<Author> {
x.iter()
.filter_map(|x| x.as_table())
.map(|x| Author::from(x.to_owned()))
.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 {
&self.root
if let Some(a) = config.get("description") {
self.description = Some(a.to_string().replace("\"", ""));
}
if let Some(a) = config.get("language") {
if let Some(b) = a.as_table() {
self.language = Language::from(b.to_owned());
}
}
// Author name as a hash key.
if let Some(a) = config.get("author") {
if let Some(b) = a.as_str() {
self.authors = vec![Author::new(b)];
}
}
// Authors as an array of tables. This will override the above.
if let Some(a) = config.get("authors") {
if let Some(b) = a.as_slice() {
self.authors = extract_authors_from_slice(b);
}
}
// Translator name as a hash key.
if let Some(a) = config.get("translator") {
if let Some(b) = a.as_str() {
self.translators = Some(vec![Author::new(b)]);
}
}
// Translators as an array of tables. This will override the above.
if let Some(a) = config.get("translators") {
if let Some(b) = a.as_slice() {
self.translators = Some(extract_authors_from_slice(b));
}
}
if let Some(a) = config.get("publisher") {
if let Some(b) = a.as_table() {
self.publisher = Some(Publisher::from(b.to_owned()));
}
}
if let Some(a) = config.get("number_format") {
if let Some(b) = a.as_str() {
self.number_format = match b.to_lowercase().as_ref() {
"arabic" => NumberFormat::Arabic,
"roman" => NumberFormat::Roman,
"word" => NumberFormat::Word,
_ => NumberFormat::Arabic,
};
}
}
if let Some(a) = config.get("section_names") {
if let Some(b) = a.as_slice() {
self.section_names =
b.iter()
.filter_map(|x| x.as_str())
.map(|x| x.to_string())
.collect::<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
}
@ -173,7 +218,7 @@ impl BookConfig {
&self.dest
}
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
pub fn set_dest(&mut self, dest: &Path) -> &mut BookConfig {
self.dest = dest.to_owned();
self
}
@ -182,47 +227,152 @@ impl BookConfig {
&self.src
}
pub fn set_src(&mut self, src: &Path) -> &mut Self {
pub fn set_src(&mut self, src: &Path) -> &mut BookConfig {
self.src = src.to_owned();
self
}
pub fn get_theme_path(&self) -> &Path {
&self.theme_path
}
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
self.theme_path = theme_path.to_owned();
#[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 file_as(mut self, file_as: &str) -> Self {
self.file_as = file_as.to_owned();
self
}
pub fn with_email(mut self, email: &str) -> Self {
self.email = Some(email.to_owned());
self
}
}
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
for (key, value) in json.iter() {
config.insert(
String::from_str(key).unwrap(),
json_value_to_toml_value(value.to_owned())
);
impl From<toml::Table> for Author {
fn from(data: toml::Table) -> Author {
let mut author = Author::new("The Author");
if let Some(x) = data.get("name") {
author.name = x.to_string().replace("\"", "");
}
if let Some(x) = data.get("file_as") {
author.file_as = x.to_string().replace("\"", "");
} else {
author.file_as = utils::last_name_first(&author.name);
}
if let Some(x) = data.get("email") {
author.email = Some(x.to_string().replace("\"", ""));
}
author
}
}
config
#[derive(Debug, Clone)]
pub struct Language {
pub name: String,
pub code: String,
}
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
match json {
serde_json::Value::Null => toml::Value::String("".to_string()),
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
serde_json::Value::I64(x) => toml::Value::Integer(x),
serde_json::Value::U64(x) => toml::Value::Integer(x as i64),
serde_json::Value::F64(x) => toml::Value::Float(x),
serde_json::Value::String(x) => toml::Value::String(x),
serde_json::Value::Array(x) => {
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
},
serde_json::Value::Object(x) => {
toml::Value::Table(json_object_to_btreemap(&x))
},
impl 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 book::metadata::Author;
use std::fs::File;
use std::error::Error;
use std::io::{self, Read};
use std::collections::BTreeMap;
/// The Chapter struct holds the title of the chapter as written in the SUMMARY.md file,
/// the location of the markdown file containing the content and eventually sub-chapters
use utils;
use book::bookconfig::Author;
/// TODO use in template: author, description, index, class
use utils::fs::create_with_str;
/// The Chapter struct holds the title of the chapter as written in the
/// SUMMARY.md file, the location of the markdown file and other metadata.
///
/// If the markdown file starts with a TOML header, it will be parsed to set the
/// chapter's properties. A TOML header should start and end with `+++` lines:
///
/// ```
/// +++
/// title = "The Library of Babel"
/// author = "Jorge Luis Borges"
/// translator = "James E. Irby"
/// +++
///
/// # Babel
///
/// The universe (which others call the Library) is composed of an indefinite and
/// perhaps infinite number of hexagonal galleries, with vast air shafts between,
/// surrounded by very low railings. From any of the hexagons one can see,
/// interminably, the upper and lower floors.
/// ```
#[derive(Debug, Clone)]
pub struct Chapter {
/// The title of the chapter.
title: String,
/// Path to chapter's markdown file.
file: PathBuf,
pub title: String,
/// TODO The author of the chapter, or the book.
author: Author,
/// TODO The description of the chapter.
description: String,
/// TODO Index number of the chapter in its level. This is the Vec index + 1.
index: i32,
/// TODO CSS class that will be added to the page-level wrap div to allow customized chapter styles.
class: String,
/// Path to the chapter's markdown file, relative to the book's source
/// directory.
///
/// `book.src.join(chapter.path)` points to the Markdown file, and
/// `book.dest.join(chapter.path).with_extension("html")` points to the
/// output html file. This way if the user had a custom folder structure in
/// their source folder, this is re-created in the destination folder.
pub path: PathBuf,
sub_chapters: Vec<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 {
/// 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(),
// TODO placeholder values for now
author: Author::new(""),
description: "".to_string(),
index: 0,
class: "".to_string(),
}
pub fn new(title: String, path: PathBuf) -> Chapter {
let mut chapter = Chapter::default();
chapter.title = title;
chapter.path = path;
chapter
}
/// 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..]))
pub fn parse_or_create_using(&mut self, book_src_dir: &PathBuf) -> Result<&mut Self, String> {
debug!("[fn] Chapter::parse_or_create() : {:?}", &self);
let src_path = &book_src_dir.join(&self.path).to_owned();
if !src_path.exists() {
debug!("[*] Creating: {:?}", src_path);
match create_with_str(src_path, &format!("# {}", self.title)) {
Ok(_) => { return Ok(self); },
Err(e) => {
return Err(format!("Could not create: {:?}", src_path));
},
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn file(&self) -> &Path {
&self.file
}
pub fn sub_chapters(&self) -> &[Chapter] {
&self.sub_chapters
let mut text = String::new();
match File::open(src_path) {
Err(e) => { return Err(format!("Read error: {:?}", e)); },
Ok(mut f) => {
f.read_to_string(&mut text);
}
}
let re: Regex = Regex::new(r"(?ms)^\+\+\+\n(?P<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 parse_from_btreemap(&mut self, data: &BTreeMap<String, toml::Value>) -> &mut Self {
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
}
/// Reads in the chapter's content from the markdown file. Chapter doesn't
/// know the book's src folder, hence the `book_src_dir` argument.
pub fn read_content_using(&self, book_src_dir: &PathBuf) -> Result<String, Box<Error>> {
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;
pub mod bookconfig;
pub mod metadata;
pub mod chapter;
extern crate toml;
pub mod book;
pub mod bookconfig;
pub mod toc;
pub mod chapter;
pub use self::metadata::{Author, Language, BookMetadata};
pub use self::chapter::Chapter;
pub use self::book::Book;
use renderer::{Renderer, HtmlHandlebars};
use utils;
pub mod bookconfig_test;
pub use self::bookitem::{BookItem, BookItems};
pub use self::bookconfig::BookConfig;
use std::env;
use std::process::exit;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::Read;
use std::error::Error;
use std::io;
use std::io::Write;
use std::io::ErrorKind;
use std::process::Command;
use std::collections::HashMap;
use std::collections::{HashMap, BTreeMap};
use {theme, parse, utils};
use renderer::{Renderer, HtmlHandlebars};
#[derive(Debug, Clone)]
pub struct MDBook {
/// Top-level directory of the book project, as an absolute path. Defaults
/// to the current directory. `set_project_root()` converts relative paths
/// to absolute.
project_root: PathBuf,
pub struct MDBook<'a> {
root: PathBuf,
dest: PathBuf,
src: PathBuf,
theme_path: PathBuf,
pub title: String,
pub author: String,
pub description: String,
pub content: Vec<BookItem>,
books: HashMap<&'a str, Book>,
renderer: Box<Renderer>,
livereload: Option<String>,
}
impl<'a> MDBook<'a> {
/// Create a new `MDBook` struct with root directory `root`
/// Path to the template for the renderer, relative to `project_root`.
/// The `render_intent` determines its default value.
///
/// Default directory paths:
/// A book doesn't necessarily has to have the template files. When not
/// found in the book's folder, the embedded static assets will be used.
///
/// - source: `root/src`
/// - output: `root/book`
/// - theme: `root/theme`
/// Html Handlebars: `project_root` + `assets/html-template`.
template_dir: PathBuf,
/// Output base for all books, relative to `project_root`. Defaults to
/// `book`.
dest_base: PathBuf,
/// Informs other functions which renderer has been selected, either by
/// default or CLI argument.
render_intent: RenderIntent,
// 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.
///
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
pub fn new(root: &Path) -> MDBook {
if !root.exists() || !root.is_dir() {
warn!("{:?} No directory with that name", root);
}
MDBook {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
theme_path: root.join("theme"),
title: String::new(),
author: String::new(),
description: String::new(),
content: vec![],
books: HashMap::new(),
renderer: Box::new(HtmlHandlebars::new()),
livereload: None,
}
}
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
/// `(section: String, bookitem: &BookItem)`
/// The String keys will be sub-folders where the translation's Markdown
/// sources are expected.
///
/// ```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 => {},
/// }
/// }
/// Each translation should have its own SUMMARY.md file, in its source
/// folder with the chapter files.
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// # }
/// 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>,
pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
/// 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 Default for MDBook {
fn default() -> MDBook {
let mut proj: MDBook = MDBook {
project_root: PathBuf::from("".to_string()),
template_dir: PathBuf::from("".to_string()),
dest_base: PathBuf::from("book".to_string()),
render_intent: RenderIntent::HtmlHandlebars,
translations: HashMap::new(),
indent_spaces: 4,
livereload: false,
};
proj.set_project_root(&env::current_dir().unwrap());
// sets default template_dir
proj.set_render_intent(RenderIntent::HtmlHandlebars);
proj
}
}
#[derive(Debug, Clone)]
pub enum RenderIntent {
HtmlHandlebars,
}
impl MDBook {
/// Create a new `MDBook` struct with top-level project directory `project_root`
pub fn new(project_root: &PathBuf) -> MDBook {
MDBook::default().set_project_root(project_root).clone()
}
/// `init()` creates some boilerplate files and directories to get you started with your book.
///
/// ```text
/// book-test/
/// book-example/
/// ├── book
/// └── src
/// ├── chapter_1.md
@ -126,358 +147,348 @@ impl<'a> MDBook<'a> {
///
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
/// `chapter_1.md` to the source directory.
pub fn init(&mut self) -> Result<(), Box<Error>> {
debug!("[fn]: init");
if !self.root.exists() {
fs::create_dir_all(&self.root).unwrap();
info!("{:?} created", &self.root);
if !self.project_root.exists() {
fs::create_dir_all(&self.project_root).unwrap();
info!("{:?} created", &self.project_root);
}
{
if !self.dest.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
try!(fs::create_dir_all(&self.dest));
}
if !self.src.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
try!(fs::create_dir_all(&self.src));
}
let summary = self.src.join("SUMMARY.md");
if !summary.exists() {
// Summary does not exist, create it
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
let mut f = try!(File::create(&self.src.join("SUMMARY.md")));
debug!("[*]: Writing to SUMMARY.md");
try!(writeln!(f, "# Summary"));
try!(writeln!(f, ""));
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
}
}
// parse SUMMARY.md, and create the missing item related file
try!(self.parse_summary());
debug!("[*]: constructing paths for missing files");
for item in self.iter() {
debug!("[*]: item: {:?}", item);
match *item {
BookItem::Spacer => continue,
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = self.src.join(&ch.path);
if !path.exists() {
debug!("[*]: {:?} does not exist, trying to create file", path);
try!(::std::fs::create_dir_all(path.parent().unwrap()));
let mut f = try!(File::create(path));
// debug!("[*]: Writing to {:?}", path);
try!(writeln!(f, "# {}", ch.name));
}
}
},
}
}
// Read book.toml if exists and populate .translations
self.read_config();
debug!("[*]: init done");
Ok(())
}
pub fn create_gitignore(&self) {
let gitignore = self.get_gitignore();
if !gitignore.exists() {
// Gitignore does not exist, create it
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
// is not, `strip_prefix` will return an Error.
if !self.get_dest().starts_with(&self.root) {
return;
}
let relative = self.get_dest()
.strip_prefix(&self.root)
.expect("Destination is not relative to root.");
let relative = relative.to_str()
.expect("Path could not be yielded into a string slice.");
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
let mut f = File::create(&gitignore).expect("Could not create file.");
debug!("[*]: Writing to .gitignore");
writeln!(f, "{}", relative).expect("Could not write to file.");
}
}
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
/// construct the book's structure in the form of a `Vec<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`
/// Parses the `book.toml` file (if it exists) to extract the configuration parameters.
/// The `book.toml` file should be in the root directory of the book project.
/// The project root directory is the one specified when creating a new `MDBook`
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use std::path::Path;
/// # fn main() {
/// let mut book = MDBook::new(Path::new("root_dir"));
/// let mut book = MDBook::new(Path::new("project_root_dir"));
/// # }
/// ```
///
/// In this example, `root_dir` will be the root directory of our book and is specified in function
/// In this example, `project_root_dir` will be the root directory of our book and is specified in function
/// of the current working directory by using a relative path instead of an absolute path.
pub fn read_config(&mut self) -> &mut Self {
pub fn read_config(mut self) -> Self {
debug!("[fn]: read_config");
let config = BookConfig::new(&self.root)
.read_config(&self.root)
.to_owned();
// TODO refactor to a helper that returns Result?
self.title = config.title;
self.description = config.description;
self.author = config.author;
// TODO Maybe some error handling instead of exit(2), although it is a
// clear indication for the user that something is wrong and we can't
// fix it for them.
self.dest = config.dest;
self.src = config.src;
self.theme_path = config.theme_path;
self
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);
}
/// You can change the default renderer to another one by using this method. The only requirement
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
///
/// ```no_run
/// extern crate mdbook;
/// use mdbook::MDBook;
/// use mdbook::renderer::HtmlHandlebars;
/// # use std::path::Path;
///
/// fn main() {
/// 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...
/// // Don't forget to put your renderer in a Box
/// }
/// ```
///
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()`
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
self.renderer = renderer;
self
}
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(())
}
pub fn get_root(&self) -> &Path {
&self.root
}
pub fn set_dest(mut self, dest: &Path) -> Self {
// Handle absolute and relative paths
match dest.is_absolute() {
true => {
self.dest = dest.to_owned();
},
false => {
let dest = self.root.join(dest).to_owned();
self.dest = dest;
},
}
self
}
pub fn get_dest(&self) -> &Path {
&self.dest
}
pub fn set_src(mut self, src: &Path) -> Self {
// Handle absolute and relative paths
match src.is_absolute() {
true => {
self.src = src.to_owned();
},
false => {
let src = self.root.join(src).to_owned();
self.src = src;
},
}
self
}
pub fn get_src(&self) -> &Path {
&self.src
}
pub fn set_title(mut self, title: &str) -> Self {
self.title = title.to_owned();
self
}
pub fn get_title(&self) -> &str {
&self.title
}
pub fn set_author(mut self, author: &str) -> Self {
self.author = author.to_owned();
self
}
pub fn get_author(&self) -> &str {
&self.author
}
pub fn set_description(mut self, description: &str) -> Self {
self.description = description.to_owned();
self
}
pub fn get_description(&self) -> &str {
&self.description
}
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
self.livereload = Some(livereload);
self
}
pub fn unset_livereload(&mut self) -> &Self {
self.livereload = None;
self
}
pub fn get_livereload(&self) -> Option<&String> {
match self.livereload {
Some(ref livereload) => Some(&livereload),
None => None,
}
}
pub fn set_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(),
};
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
}
pub fn get_theme_path(&self) -> &Path {
&self.theme_path
/// Configures MDBook properties and translations.
///
/// After parsing properties for MDBook struct, it removes them from the
/// config (template_dir, livereload, etc.). The remaining keys on the main
/// block will be interpreted as properties of the main book.
///
/// `project_root` is ignored.
///
/// - dest_base
/// - render_intent
/// - template_dir
/// - indent_spaces
/// - livereload
pub fn parse_from_btreemap(&mut self, conf: &BTreeMap<String, toml::Value>) -> &mut Self {
let mut config = conf.clone();
if config.contains_key("project_root") {
config.remove("project_root");
}
// Construct book
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
// When append becomes stable, use self.content.append() ...
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
Ok(())
if let Some(a) = config.get("dest_base") {
self.set_dest_base(&PathBuf::from(&a.to_string()));
}
config.remove("dest_base");
if let Some(a) = config.get("render_intent") {
if a.to_string() == "html".to_string() {
self.set_render_intent(RenderIntent::HtmlHandlebars);
} else {
// offer some real choices later on...
self.set_render_intent(RenderIntent::HtmlHandlebars);
}
}
config.remove("render_intent");
// Parsing template_dir must be after render_intent, otherwise
// .set_render_intent() will always override template_dir with its
// default setting.
if let Some(a) = config.get("template_dir") {
self.set_template_dir(&PathBuf::from(&a.to_string()));
}
config.remove("template_dir");
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");
if let Some(a) = config.get("livereload") {
if let Some(b) = a.as_bool() {
self.livereload = b;
}
}
config.remove("livereload");
// If there is a 'translations' table, configugre each book from that.
// If there isn't, take the rest of the config as one book.
// If there is only one book, leave its source and destination folder as
// the default `./src` and `./book`. If there are more, join their hash
// keys to the default source and destination folder such as `/src/en`
// and `./book/en`. This may be overridden if set specifically.
if let Some(a) = config.get("translations") {
if let Some(b) = a.as_table() {
let is_multilang: bool = b.iter().count() > 1;
for (key, conf) in b.iter() {
let mut book = Book::new(&self.project_root);
if let Some(c) = conf.as_slice() {
if let Some(d) = c[0].as_table() {
if is_multilang {
book.config.src = book.config.src.join(key);
book.config.dest = book.config.dest.join(key);
}
book.config.is_multilang = is_multilang;
book.config.parse_from_btreemap(&d);
self.translations.insert(key.to_owned(), book);
}
}
}
}
} else {
let mut book = Book::new(&self.project_root);
book.config.parse_from_btreemap(&config);
let key = book.config.language.code.clone();
self.translations.insert(key, book);
}
self
}
pub fn parse_books(&mut self) -> &mut Self {
debug!("[fn]: parse_books");
for key in self.translations.clone().keys() {
if let Some(mut b) = self.translations.clone().get_mut(key) {
// TODO error handling could be better here
let first_as_index = match self.render_intent {
RenderIntent::HtmlHandlebars => true,
};
match b.parse_or_create_summary_file(first_as_index) {
Ok(_) => {},
Err(e) => {println!("{}", e);},
}
match b.parse_or_create_chapter_files() {
Ok(_) => {},
Err(e) => {println!("{}", e);},
}
self.translations.remove(key);
self.translations.insert(key.to_owned(), b.clone());
}
}
self
}
pub fn get_project_root(&self) -> &Path {
&self.project_root
}
pub fn set_project_root(&mut self, path: &PathBuf) -> &mut MDBook {
if path.is_absolute() {
self.project_root = path.to_owned();
} else {
self.project_root = env::current_dir().unwrap().join(path).to_owned();
}
self
}
pub fn get_template_dir(&self) -> PathBuf {
self.project_root.join(&self.template_dir)
}
pub fn set_template_dir(&mut self, path: &PathBuf) -> &mut MDBook {
if path.as_os_str() == OsStr::new(".") {
self.template_dir = PathBuf::from("".to_string());
} else {
self.template_dir = path.to_owned();
}
self
}
pub fn get_dest_base(&self) -> PathBuf {
self.project_root.join(&self.dest_base)
}
pub fn set_dest_base(&mut self, path: &PathBuf) -> &mut MDBook {
if path.as_os_str() == OsStr::new(".") {
self.dest_base = PathBuf::from("".to_string());
} else {
self.dest_base = path.to_owned();
}
self
}
pub fn get_render_intent(&self) -> &RenderIntent {
&self.render_intent
}
pub fn set_render_intent(&mut self, intent: RenderIntent) -> &mut MDBook {
self.render_intent = intent;
match self.render_intent {
RenderIntent::HtmlHandlebars => {
self.set_template_dir(&PathBuf::from("assets").join("html-template"));
},
}
self
}
// TODO update
// pub fn test(&mut self) -> Result<(), Box<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.
extern crate includedir;
extern crate phf;
include!(concat!(env!("OUT_DIR"), "/data.rs"));
extern crate serde;
extern crate serde_json;
extern crate handlebars;
extern crate pulldown_cmark;
extern crate regex;
extern crate glob;
#[macro_use] extern crate log;
pub mod book;
mod parse;
pub mod renderer;
pub mod theme;
pub mod utils;
pub mod tests;
pub use book::MDBook;
pub use book::BookItem;
pub use book::BookConfig;
pub use renderer::Renderer;

View File

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

View File

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

View File

@ -1,10 +1,13 @@
use renderer::html_handlebars::helpers;
use renderer::Renderer;
use book::MDBook;
use book::bookitem::BookItem;
use {utils, theme};
use book::{MDBook, Book};
use book::chapter::Chapter;
use book::toc::{TocItem, TocContent};
use utils;
use FILES;
use std::path::{Path, PathBuf};
use std::ffi::OsStr;
use std::fs::{self, File};
use std::error::Error;
use std::io::{self, Read, Write};
@ -15,7 +18,6 @@ use handlebars::Handlebars;
use serde_json;
use serde_json::value::ToJson;
pub struct HtmlHandlebars;
impl HtmlHandlebars {
@ -25,16 +27,118 @@ impl 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");
let mut handlebars = Handlebars::new();
// Load theme
let theme = theme::Theme::new(book.get_theme_path());
// Render the chapters of each book
for (key, book) in &book_project.translations {
// Read in the page template
let tmpl_path: &PathBuf = &book_project.get_template_dir().join("_layouts").join("page.hbs");
let s = if tmpl_path.exists() {
try!(utils::fs::file_to_string(&tmpl_path))
} else {
try!(utils::fs::get_data_file("data/html-template/_layouts/page.hbs"))
};
// Register template
debug!("[*]: Register handlebars template");
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
try!(handlebars.register_template_string("page", s));
// Register helpers
debug!("[*]: Register handlebars helpers");
@ -42,291 +146,271 @@ impl Renderer for HtmlHandlebars {
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));
// Check if book's dest directory exists
// Print version
let mut print_content: String = String::new();
// If this is a single book, config.dest default is
// `project_root/book`, and the earlier check will cover this.
// Check if dest directory exists
debug!("[*]: Check if destination directory exists");
if let Err(_) = fs::create_dir_all(book.get_dest()) {
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
"Unexpected error when constructing destination path")));
// If this is multi-language book, config.dest will
// `project_book/book/key`, and so we check here for each book.
debug!("[*]: Check if book's destination directory exists");
if let Err(_) = fs::create_dir_all(book.config.get_dest()) {
return Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"Unexpected error when constructing destination path")
));
}
// Render a file for every entry in the book
let mut index = true;
for item in book.iter() {
// If this is the main book of a multi-language book, add an
// index.html to the project dest folder
match *item {
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
if book.config.is_multilang && book.config.is_main_book {
match book.toc[0] {
TocItem::Numbered(ref i) |
TocItem::Unnumbered(ref i) |
TocItem::Unlisted(ref i) => {
let mut chapter: Chapter = i.chapter.clone();
chapter.dest_path = Some(PathBuf::from("index.html".to_string()));
let path = book.get_src().join(&ch.path);
// almost the same as process_chapter(), but we have to
// manipulate path_to_root in data and rendered_path
debug!("[*]: Opening file: {:?}", path);
let mut f = try!(File::open(&path));
let mut content: String = String::new();
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
let mut content = try!(chapter.read_content_using(&book.config.src));
// Parse for playpen links
if let Some(p) = path.parent() {
if let Some(p) = book.config.get_src().join(&chapter.path).parent() {
content = helpers::playpen::render_playpen(&content, p);
}
// Render markdown using the pulldown-cmark crate
content = utils::render_markdown(&content);
print_content.push_str(&content);
let mut data = try!(make_data(&book, &chapter, &content));
// Remove content from previous file and render content for this one
data.remove("path");
match ch.path.to_str() {
Some(p) => {
data.insert("path".to_owned(), p.to_json());
},
None => {
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
"Could not convert path to str")))
},
}
// Remove content from previous file and render content for this one
data.remove("content");
data.insert("content".to_owned(), content.to_json());
// Remove path to root from previous file and render content for this one
data.remove("path_to_root");
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&ch.path).to_json());
data.insert("path_to_root".to_owned(), "".to_json());
// Rendere the handlebars template with the data
debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data));
let rendered_content = try!(handlebars.render("page", &data));
let p = chapter.dest_path.unwrap();
let rendered_path = &book_project.get_dest_base().join(&p);
debug!("[*]: Create file {:?}", rendered_path);
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
// Write to file
let mut file =
try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
info!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
let mut file = try!(utils::fs::create_file(rendered_path));
info!("[*] Creating {:?} ✓", rendered_path);
try!(file.write_all(&rendered.into_bytes()));
// Create an index.html from the first element in SUMMARY.md
if index {
debug!("[*]: index.html");
let mut index_file = try!(File::create(book.get_dest().join("index.html")));
let mut content = String::new();
let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html"))))
.read_to_string(&mut content);
// This could cause a problem when someone displays code containing <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;
}
}
try!(file.write_all(&rendered_content.into_bytes()));
},
_ => {},
TocItem::Spacer => {},
}
}
// 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"]));
// Render a file for every entry in the book
try!(self.process_items(&book.toc, &book, &handlebars));
}
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");
let mut data = serde_json::Map::new();
// Book data
data.insert("language".to_owned(), "en".to_json());
data.insert("title".to_owned(), book.get_title().to_json());
data.insert("description".to_owned(), book.get_description().to_json());
data.insert("favicon".to_owned(), "favicon.png".to_json());
if let Some(livereload) = book.get_livereload() {
data.insert("livereload".to_owned(), livereload.to_json());
data.insert("title".to_owned(), book.config.title.to_json());
data.insert("description".to_owned(), book.config.description.to_json());
// Chapter data
let mut path = if let Some(ref dest_path) = chapter.dest_path {
PathBuf::from(dest_path)
} else {
chapter.path.clone()
};
if book.config.is_multilang && path.as_os_str().len() > 0 {
let p = PathBuf::from(&book.config.language.code);
path = p.join(path);
}
let mut chapters = vec![];
for item in book.iter() {
// Create the data to inject in the template
let mut chapter = BTreeMap::new();
match *item {
BookItem::Affix(ref ch) => {
chapter.insert("name".to_owned(), ch.name.to_json());
match ch.path.to_str() {
match path.to_str() {
Some(p) => {
chapter.insert("path".to_owned(), p.to_json());
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"))),
}
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());
debug!("[*]: JSON constructed");
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();
// Chapter title
match previous.get("name") {
match previous.get("title") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
previous_chapter.insert("title".to_owned(), n.to_json())
@ -105,9 +105,6 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
Ok(())
}
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: next (handlebars helper)");
@ -151,7 +148,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
// Create new BTreeMap to extend the context: 'title' and 'link'
let mut next_chapter = BTreeMap::new();
match item.get("name") {
match item.get("title") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
next_chapter.insert("title".to_owned(), n.to_json());

View File

@ -97,11 +97,11 @@ impl HelperDef for RenderToc {
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
// filter all events that are not inline code blocks
let parser = Parser::new(&name).filter(|event| {
let parser = Parser::new(&title).filter(|event| {
match event {
&Event::Start(Tag::Code) |
&Event::End(Tag::Code) => true,
@ -112,7 +112,7 @@ impl HelperDef for RenderToc {
});
// render markdown to html
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
let mut markdown_parsed_name = String::with_capacity(title.len() * 3 / 2);
html::push_html(&mut markdown_parsed_name, parser);
// write to the handlebars template

View File

@ -2,8 +2,23 @@ pub use self::html_handlebars::HtmlHandlebars;
mod html_handlebars;
use book::MDBook;
use std::error::Error;
use std::path::PathBuf;
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<(), Box<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