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
|
@ -1,5 +1,9 @@
|
||||||
|
*.swp
|
||||||
|
.#*
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
target
|
target
|
||||||
|
TAGS
|
||||||
book-test
|
src/tests/book-minimal/book
|
||||||
|
src/tests/book-minimal-with-assets/book
|
||||||
|
src/tests/book-wonderland-multilang/book
|
||||||
book-example/book
|
book-example/book
|
||||||
|
|
13
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
version = "0.0.15"
|
version = "0.0.16"
|
||||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||||
description = "create books from markdown files (like Gitbook)"
|
description = "create books from markdown files (like Gitbook)"
|
||||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||||
|
@ -9,20 +9,26 @@ keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
include = ["data"]
|
||||||
exclude = [
|
exclude = [
|
||||||
"book-example/*",
|
"book-example/*",
|
||||||
"src/theme/stylus",
|
"data/html-template/_stylus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.19.2"
|
clap = "2.19.2"
|
||||||
handlebars = { version = "0.23.0", features = ["serde_type"] }
|
handlebars = { version = "0.23.0", features = ["serde_type"] }
|
||||||
serde = "0.8"
|
serde = "0.8"
|
||||||
serde_json = "0.8"
|
serde_json = "0.8"
|
||||||
pulldown-cmark = "0.0.8"
|
pulldown-cmark = "0.0.8"
|
||||||
|
regex = "0.1"
|
||||||
|
glob = "0.2"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
env_logger = "0.3"
|
env_logger = "0.3"
|
||||||
toml = { version = "0.2", features = ["serde"] }
|
toml = { version = "0.2", features = ["serde"] }
|
||||||
|
phf = "0.7"
|
||||||
|
includedir = "0.2"
|
||||||
|
|
||||||
# Watch feature
|
# Watch feature
|
||||||
notify = { version = "2.5.5", optional = true }
|
notify = { version = "2.5.5", optional = true }
|
||||||
|
@ -34,6 +40,9 @@ iron = { version = "0.4", optional = true }
|
||||||
staticfile = { version = "0.3", optional = true }
|
staticfile = { version = "0.3", optional = true }
|
||||||
ws = { version = "0.5.1", optional = true}
|
ws = { version = "0.5.1", optional = true}
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
includedir_codegen = "0.2"
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempdir = "0.3.4"
|
tempdir = "0.3.4"
|
||||||
|
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 78 KiB |
|
@ -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
|
|
@ -1,3 +1,5 @@
|
||||||
title = "mdBook Documentation"
|
title = "mdBook Documentation"
|
||||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
description = "Create books from markdown files. Like Gitbook but implemented in Rust."
|
||||||
author = "Mathieu David"
|
|
||||||
|
[[authors]]
|
||||||
|
name = "Mathieu David"
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
[Introduction](misc/introduction.md)
|
|
||||||
|
|
||||||
- [mdBook](README.md)
|
- [mdBook](README.md)
|
||||||
- [Command Line Tool](cli/cli-tool.md)
|
- [Command Line Tool](cli/cli-tool.md)
|
||||||
- [init](cli/init.md)
|
- [init](cli/init.md)
|
||||||
|
@ -18,5 +16,6 @@
|
||||||
- [MathJax Support](format/mathjax.md)
|
- [MathJax Support](format/mathjax.md)
|
||||||
- [Rust code specific features](format/rust.md)
|
- [Rust code specific features](format/rust.md)
|
||||||
- [Rust Library](lib/lib.md)
|
- [Rust Library](lib/lib.md)
|
||||||
|
- [Structs](structs/structs.md)
|
||||||
-----------
|
-----------
|
||||||
[Contributors](misc/contributors.md)
|
[Contributors](misc/contributors.md)
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Introduction
|
|
||||||
|
|
||||||
A frontmatter chapter.
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Structs
|
||||||
|
|
||||||
|
![structs reorganized](images/structs-v0-0-16.png)
|
||||||
|
|
||||||
|
Diagram with [plantuml](http://plantuml.com)
|
||||||
|
|
19
build.rs
|
@ -1,23 +1,36 @@
|
||||||
// build.rs
|
// build.rs
|
||||||
|
|
||||||
|
extern crate includedir_codegen;
|
||||||
|
|
||||||
|
use includedir_codegen::Compression;
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
||||||
|
includedir_codegen::start("FILES")
|
||||||
|
.dir("data", Compression::Gzip)
|
||||||
|
.build("data.rs")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// TODO this using cargo as a Makefile. This is only for development, it
|
||||||
|
// doesn't have to be part of the production auto-build. Use either a
|
||||||
|
// Makefile or an npm command if stylus comes from npm anyway.
|
||||||
|
|
||||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||||
|
|
||||||
// Compile stylus stylesheet to css
|
// Compile stylus stylesheet to css
|
||||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||||
|
|
||||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
let template_dir = Path::new(&manifest_dir).join("data/html-template/");
|
||||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
let stylus_dir = template_dir.join("_stylus/book.styl");
|
||||||
|
|
||||||
if !Command::new("stylus")
|
if !Command::new("stylus")
|
||||||
.arg(format!("{}", stylus_dir.to_str().unwrap()))
|
.arg(format!("{}", stylus_dir.to_str().unwrap()))
|
||||||
.arg("--out")
|
.arg("--out")
|
||||||
.arg(format!("{}", theme_dir.to_str().unwrap()))
|
.arg(format!("{}", template_dir.to_str().unwrap()))
|
||||||
.arg("--use")
|
.arg("--use")
|
||||||
.arg("nib")
|
.arg("nib")
|
||||||
.status().unwrap()
|
.status().unwrap()
|
||||||
|
|
|
@ -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>
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 348 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -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<Chapter></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<TocItem></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>
|
|
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 58 KiB |
|
@ -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
|
|
94
doc/doc.md
|
@ -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.
|
|
||||||
|
|
|
@ -29,14 +29,9 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use clap::{App, ArgMatches, SubCommand, AppSettings};
|
use clap::{App, ArgMatches, SubCommand, AppSettings};
|
||||||
|
|
||||||
// Uses for the Watch feature
|
|
||||||
#[cfg(feature = "watch")]
|
|
||||||
use notify::Watcher;
|
|
||||||
#[cfg(feature = "watch")]
|
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
|
|
||||||
|
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
|
use mdbook::renderer::{Renderer, HtmlHandlebars};
|
||||||
|
use mdbook::utils;
|
||||||
|
|
||||||
const NAME: &'static str = "mdbook";
|
const NAME: &'static str = "mdbook";
|
||||||
|
|
||||||
|
@ -55,7 +50,7 @@ fn main() {
|
||||||
.about("Create boilerplate structure and files in the directory")
|
.about("Create boilerplate structure and files in the directory")
|
||||||
// the {n} denotes a newline which will properly aligned in all help messages
|
// the {n} denotes a newline which will properly aligned in all help messages
|
||||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
|
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
|
||||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
.arg_from_usage("--copy-assets 'Copies the default assets (css, layout template, etc.) into your project folder'")
|
||||||
.arg_from_usage("--force 'skip confirmation prompts'"))
|
.arg_from_usage("--force 'skip confirmation prompts'"))
|
||||||
.subcommand(SubCommand::with_name("build")
|
.subcommand(SubCommand::with_name("build")
|
||||||
.about("Build the book from the markdown files")
|
.about("Build the book from the markdown files")
|
||||||
|
@ -92,7 +87,6 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Simple function that user comfirmation
|
// Simple function that user comfirmation
|
||||||
fn confirm() -> bool {
|
fn confirm() -> bool {
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
@ -104,25 +98,24 @@ fn confirm() -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Init command implementation
|
// Init command implementation
|
||||||
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::new(&book_dir);
|
let mut book_project = MDBook::new(&book_dir);
|
||||||
|
|
||||||
// Call the function that does the initialization
|
book_project.read_config();
|
||||||
try!(book.init());
|
book_project.parse_books();
|
||||||
|
|
||||||
// If flag `--theme` is present, copy theme to src
|
// If flag `--copy-assets` is present, copy embedded assets to project root
|
||||||
if args.is_present("theme") {
|
if args.is_present("copy-assets") {
|
||||||
|
|
||||||
// Skip this if `--force` is present
|
// Skip this if `--force` is present
|
||||||
if !args.is_present("force") {
|
if book_project.get_project_root().join("assets").exists() && !args.is_present("force") {
|
||||||
// Print warning
|
// Print warning
|
||||||
print!("\nCopying the default theme to {:?}", book.get_src());
|
println!("\nCopying the default assets to {:?}", book_project.get_project_root());
|
||||||
println!("could potentially overwrite files already present in that directory.");
|
println!("This will overwrite files already present in that directory.");
|
||||||
print!("\nAre you sure you want to continue? (y/n) ");
|
print!("Are you sure you want to continue? (y/n) ");
|
||||||
|
|
||||||
// Read answer from user and exit if it's not 'yes'
|
// Read answer from user and exit if it's not 'yes'
|
||||||
if !confirm() {
|
if !confirm() {
|
||||||
|
@ -132,20 +125,21 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the function that copies the theme
|
// Copy the assets
|
||||||
try!(book.copy_theme());
|
try!(utils::fs::copy_data("data/**/*",
|
||||||
println!("\nTheme copied.");
|
"data/",
|
||||||
|
vec![],
|
||||||
|
&book_project.get_project_root().join("assets")));
|
||||||
|
|
||||||
|
println!("\nAssets copied.");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
if !args.is_present("force") {
|
||||||
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
|
|
||||||
|
|
||||||
if !args.is_present("force") && is_dest_inside_root {
|
|
||||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||||
|
|
||||||
if confirm() {
|
if confirm() {
|
||||||
book.create_gitignore();
|
utils::fs::create_gitignore(&book_project);
|
||||||
println!("\n.gitignore created.");
|
println!("\n.gitignore created.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,115 +149,40 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Build command implementation
|
// Build command implementation
|
||||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::new(&book_dir).read_config();
|
|
||||||
|
|
||||||
try!(book.build());
|
// TODO figure out render format intent when we acutally have different renderers
|
||||||
|
|
||||||
|
let renderer = HtmlHandlebars::new();
|
||||||
|
try!(renderer.build(&book_dir));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Watch command implementation
|
// Watch command implementation
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
// TODO watch
|
||||||
let mut book = MDBook::new(&book_dir).read_config();
|
println!("watch");
|
||||||
|
|
||||||
trigger_on_change(&mut book, |event, book| {
|
|
||||||
if let Some(path) = event.path {
|
|
||||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
|
||||||
match book.build() {
|
|
||||||
Err(e) => println!("Error while building: {:?}", e),
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
println!("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve command implementation
|
||||||
// Watch command implementation
|
|
||||||
#[cfg(feature = "serve")]
|
#[cfg(feature = "serve")]
|
||||||
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
const RELOAD_COMMAND: &'static str = "reload";
|
// TODO serve
|
||||||
|
println!("serve");
|
||||||
let book_dir = get_book_dir(args);
|
|
||||||
let mut book = MDBook::new(&book_dir).read_config();
|
|
||||||
let port = args.value_of("port").unwrap_or("3000");
|
|
||||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
|
||||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
|
||||||
let public_address = args.value_of("address").unwrap_or(interface);
|
|
||||||
|
|
||||||
let address = format!("{}:{}", interface, port);
|
|
||||||
let ws_address = format!("{}:{}", interface, ws_port);
|
|
||||||
|
|
||||||
book.set_livereload(format!(r#"
|
|
||||||
<script type="text/javascript">
|
|
||||||
var socket = new WebSocket("ws://{}:{}");
|
|
||||||
socket.onmessage = function (event) {{
|
|
||||||
if (event.data === "{}") {{
|
|
||||||
socket.close();
|
|
||||||
location.reload(true); // force reload from server (not from cache)
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
|
|
||||||
window.onbeforeunload = function() {{
|
|
||||||
socket.close();
|
|
||||||
}}
|
|
||||||
</script>
|
|
||||||
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
|
|
||||||
|
|
||||||
try!(book.build());
|
|
||||||
|
|
||||||
let staticfile = staticfile::Static::new(book.get_dest());
|
|
||||||
let iron = iron::Iron::new(staticfile);
|
|
||||||
let _iron = iron.http(&*address).unwrap();
|
|
||||||
|
|
||||||
let ws_server = ws::WebSocket::new(|_| {
|
|
||||||
|_| {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
|
|
||||||
let broadcaster = ws_server.broadcaster();
|
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
ws_server.listen(&*ws_address).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("\nServing on {}", address);
|
|
||||||
|
|
||||||
trigger_on_change(&mut book, move |event, book| {
|
|
||||||
if let Some(path) = event.path {
|
|
||||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
|
||||||
match book.build() {
|
|
||||||
Err(e) => println!("Error while building: {:?}", e),
|
|
||||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
|
||||||
}
|
|
||||||
println!("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
// TODO test
|
||||||
let mut book = MDBook::new(&book_dir).read_config();
|
println!("test");
|
||||||
|
|
||||||
try!(book.test());
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
if let Some(dir) = args.value_of("dir") {
|
if let Some(dir) = args.value_of("dir") {
|
||||||
// Check if path is relative from current dir, or absolute...
|
// Check if path is relative from current dir, or absolute...
|
||||||
|
@ -277,58 +196,3 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
env::current_dir().unwrap()
|
env::current_dir().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Calls the closure when a book source file is changed. This is blocking!
|
|
||||||
#[cfg(feature = "watch")]
|
|
||||||
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
|
||||||
where F: Fn(notify::Event, &mut MDBook) -> ()
|
|
||||||
{
|
|
||||||
// Create a channel to receive the events.
|
|
||||||
let (tx, rx) = channel();
|
|
||||||
|
|
||||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
|
||||||
|
|
||||||
match w {
|
|
||||||
Ok(mut watcher) => {
|
|
||||||
// Add the source directory to the watcher
|
|
||||||
if let Err(e) = watcher.watch(book.get_src()) {
|
|
||||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
|
||||||
::std::process::exit(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the book.json file to the watcher if it exists, because it's not
|
|
||||||
// located in the source directory
|
|
||||||
if let Err(_) = watcher.watch(book.get_root().join("book.json")) {
|
|
||||||
// do nothing if book.json is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut previous_time = time::get_time();
|
|
||||||
|
|
||||||
println!("\nListening for changes...\n");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match rx.recv() {
|
|
||||||
Ok(event) => {
|
|
||||||
// Skip the event if an event has already been issued in the last second
|
|
||||||
let time = time::get_time();
|
|
||||||
if time - previous_time < time::Duration::seconds(1) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
previous_time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
closure(event, book);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("An error occured: {:?}", e);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
|
||||||
::std::process::exit(0);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
152
src/book/book.rs
|
@ -1,80 +1,118 @@
|
||||||
use book::metadata::BookMetadata;
|
use std::fs::File;
|
||||||
use book::chapter::Chapter;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use book::bookconfig::BookConfig;
|
||||||
|
use book::toc::{TocItem, TocContent};
|
||||||
|
|
||||||
/// The `Book` struct contains the metadata and chapters for one language of the book.
|
use utils::fs::create_with_str;
|
||||||
/// Multiple `Book` structs are combined in the `MDBook` struct to support multi-language books.
|
use parse::construct_tocitems;
|
||||||
|
|
||||||
|
/// The `Book` struct contains the metadata (config) and chapters (toc) for one
|
||||||
|
/// language of the book. Multiple `Book` structs are combined in the `MDBook`
|
||||||
|
/// struct to support multi-language books.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
metadata: BookMetadata,
|
pub config: BookConfig,
|
||||||
|
pub toc: Vec<TocItem>,
|
||||||
|
}
|
||||||
|
|
||||||
frontmatter: Vec<Chapter>,
|
impl Default for Book {
|
||||||
mainmatter: Vec<Chapter>,
|
fn default() -> Book {
|
||||||
backmatter: Vec<Chapter>,
|
Book {
|
||||||
|
config: BookConfig::default(),
|
||||||
|
toc: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
impl Book {
|
||||||
/// Creates a new book with the given title, chapters are added with the
|
|
||||||
/// `add_frontmatter_chapter`, `add_mainmatter_chapter`,
|
|
||||||
/// `add_backmatter_chapter` methods
|
|
||||||
pub fn new(title: &str) -> Self {
|
|
||||||
Book {
|
|
||||||
metadata: BookMetadata::new(title),
|
|
||||||
|
|
||||||
frontmatter: Vec::new(),
|
/// Creates a new book
|
||||||
mainmatter: Vec::new(),
|
pub fn new(project_root: &PathBuf) -> Book {
|
||||||
backmatter: Vec::new(),
|
let conf = BookConfig::new(project_root);
|
||||||
}
|
let mut book = Book::default();
|
||||||
|
book.config = conf;
|
||||||
|
book
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new mainmatter chapter
|
/// Parses in the SUMMARY.md or creates one
|
||||||
pub fn add_mainmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
|
pub fn parse_or_create_summary_file(&mut self, first_as_index: bool) -> Result<&mut Self, String> {
|
||||||
self.mainmatter.push(chapter);
|
|
||||||
self
|
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
|
// parse SUMMARY.md to toc items
|
||||||
pub fn add_frontmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
|
self.toc = match construct_tocitems(&summary_path, first_as_index) {
|
||||||
self.frontmatter.push(chapter);
|
Ok(x) => x,
|
||||||
self
|
Err(e) => { return Err(format!("Error constructing the TOC: {:?}", e)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new backmatter chapter
|
/// Walks through the TOC array and calls parse_or_create() on each
|
||||||
pub fn add_backmatter_chapter(&mut self, chapter: Chapter) -> &mut Self {
|
pub fn parse_or_create_chapter_files(&mut self) -> Result<&mut Self, String> {
|
||||||
self.backmatter.push(chapter);
|
self.toc = self.process_them(&self.toc);
|
||||||
self
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_them(&self, items: &Vec<TocItem>) -> Vec<TocItem> {
|
||||||
/// This method takes a slice `&[x, y, z]` as parameter and returns the corresponding chapter.
|
items.iter().map(|i|
|
||||||
/// For example, to retrieve chapter 2.3 we would use:
|
match i {
|
||||||
/// ```
|
&TocItem::Numbered(ref c) => TocItem::Numbered(self.process_toccontent(c)),
|
||||||
/// #extern crate mdbook;
|
&TocItem::Unnumbered(ref c) => TocItem::Unnumbered(self.process_toccontent(c)),
|
||||||
/// #use mdbook::book::Book;
|
&TocItem::Unlisted(ref c) => TocItem::Unlisted(self.process_toccontent(c)),
|
||||||
/// #fn main() {
|
&TocItem::Spacer => TocItem::Spacer,
|
||||||
/// #let book = Book::new("Test");
|
|
||||||
/// let chapter_2_3 = book.get_chapter(&[2, 3]);
|
|
||||||
/// #}
|
|
||||||
/// ```
|
|
||||||
pub fn get_chapter(&self, section: &[usize]) -> Option<&Chapter> {
|
|
||||||
match section.len() {
|
|
||||||
0 => None,
|
|
||||||
1 => self.mainmatter.get(section[0]),
|
|
||||||
_ => {
|
|
||||||
self.mainmatter
|
|
||||||
.get(section[0])
|
|
||||||
.and_then(|ch| ch.get_sub_chapter(§ion[1..]))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
).collect::<Vec<TocItem>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the metadata for modification
|
fn process_toccontent(&self, c: &TocContent) -> TocContent {
|
||||||
pub fn mut_metadata(&mut self) -> &mut BookMetadata {
|
let mut content: TocContent = c.clone();
|
||||||
&mut self.metadata
|
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
|
// TODO update
|
||||||
pub fn metadata(&self) -> &BookMetadata {
|
|
||||||
&self.metadata
|
// /// This method takes a slice `&[x, y, z]` as parameter and returns the corresponding chapter.
|
||||||
}
|
// /// For example, to retrieve chapter 2.3 we would use:
|
||||||
|
// /// ```
|
||||||
|
// /// #extern crate mdbook;
|
||||||
|
// /// #use mdbook::book::Book;
|
||||||
|
// /// #fn main() {
|
||||||
|
// /// #let book = Book::new("Test");
|
||||||
|
// /// let chapter_2_3 = book.get_chapter(&[2, 3]);
|
||||||
|
// /// #}
|
||||||
|
// /// ```
|
||||||
|
// pub fn get_chapter(&self, section: &[usize]) -> Option<&Chapter> {
|
||||||
|
// match section.len() {
|
||||||
|
// 0 => None,
|
||||||
|
// 1 => self.mainmatter.get(section[0]),
|
||||||
|
// _ => {
|
||||||
|
// self.mainmatter
|
||||||
|
// .get(section[0])
|
||||||
|
// .and_then(|ch| ch.get_sub_chapter(§ion[1..]))
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// Returns a mutable reference to the metadata for modification
|
||||||
|
// pub fn mut_metadata(&mut self) -> &mut BookMetadata {
|
||||||
|
// &mut self.metadata
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Returns a reference to the metadata
|
||||||
|
// pub fn metadata(&self) -> &BookMetadata {
|
||||||
|
// &self.metadata
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,169 +3,214 @@ extern crate toml;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
use utils;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BookConfig {
|
pub struct BookConfig {
|
||||||
root: PathBuf,
|
|
||||||
|
// Paths
|
||||||
|
|
||||||
pub dest: PathBuf,
|
pub dest: PathBuf,
|
||||||
pub src: PathBuf,
|
pub src: PathBuf,
|
||||||
pub theme_path: PathBuf,
|
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
|
||||||
|
/// The title of the book.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub author: String,
|
/// The subtitle, when titles are in the form of "The Immense Journey: An
|
||||||
pub description: String,
|
/// Imaginative Naturalist Explores the Mysteries of Man and Nature"
|
||||||
|
pub subtitle: Option<String>,
|
||||||
|
/// A brief description or summary of the book.
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub language: Language,
|
||||||
|
pub authors: Vec<Author>,
|
||||||
|
pub translators: Option<Vec<Author>>,
|
||||||
|
/// Publisher's info
|
||||||
|
pub publisher: Option<Publisher>,
|
||||||
|
/// Chapter numbering scheme
|
||||||
|
pub number_format: NumberFormat,
|
||||||
|
/// Section names for nested Vec<Chapter> structures, defaults to `[ "Chapter", "Section", "Subsection" ]`
|
||||||
|
pub section_names: Vec<String>,
|
||||||
|
/// Whether this is the main book, in the case of translations.
|
||||||
|
pub is_main_book: bool,
|
||||||
|
pub is_multilang: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub indent_spaces: i32,
|
impl Default for BookConfig {
|
||||||
multilingual: bool,
|
fn default() -> BookConfig {
|
||||||
|
BookConfig {
|
||||||
|
dest: PathBuf::from("book".to_string()),
|
||||||
|
src: PathBuf::from("src".to_string()),
|
||||||
|
|
||||||
|
title: "Untitled".to_string(),
|
||||||
|
subtitle: None,
|
||||||
|
description: None,
|
||||||
|
language: Language::default(),
|
||||||
|
authors: vec![Author::new("The Author").file_as("Author, The")],
|
||||||
|
translators: None,
|
||||||
|
publisher: None,
|
||||||
|
number_format: NumberFormat::Arabic,
|
||||||
|
section_names: vec!["Chapter".to_string(),
|
||||||
|
"Section".to_string(),
|
||||||
|
"Subsection".to_string()],
|
||||||
|
is_main_book: false,
|
||||||
|
is_multilang: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BookConfig {
|
impl BookConfig {
|
||||||
pub fn new(root: &Path) -> Self {
|
|
||||||
BookConfig {
|
|
||||||
root: root.to_owned(),
|
|
||||||
dest: root.join("book"),
|
|
||||||
src: root.join("src"),
|
|
||||||
theme_path: root.join("theme"),
|
|
||||||
|
|
||||||
title: String::new(),
|
pub fn new(project_root: &PathBuf) -> BookConfig {
|
||||||
author: String::new(),
|
let mut conf = BookConfig::default();
|
||||||
description: String::new(),
|
|
||||||
|
|
||||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
// join paths to project_root
|
||||||
multilingual: false,
|
// Prefer "" to "." and "src" to "./src", avoid "././src"
|
||||||
}
|
|
||||||
|
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");
|
conf
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses recognized keys from a BTreeMap one by one. Not trying to
|
||||||
|
/// directly de-serialize to `BookConfig` so that we can provide some
|
||||||
|
/// convenient shorthands for the user.
|
||||||
|
///
|
||||||
|
/// `book.toml` is a user interface, not an app data store, we never have to
|
||||||
|
/// write data back to it.
|
||||||
|
///
|
||||||
|
/// Parses author when given as an array, or when given as a hash key to
|
||||||
|
/// make declaring just an author name easy.
|
||||||
|
///
|
||||||
|
/// Both of these express a single author:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// [[authors]]
|
||||||
|
/// name = "Marcus Aurelius Antoninus"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Or:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// name = "Marcus Aurelius Antoninus"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
||||||
|
|
||||||
// Title, author, description
|
// Paths
|
||||||
if let Some(a) = config.get("title") {
|
|
||||||
self.title = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
if let Some(a) = config.get("author") {
|
|
||||||
self.author = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
if let Some(a) = config.get("description") {
|
|
||||||
self.description = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destination folder
|
// Destination folder
|
||||||
if let Some(a) = config.get("dest") {
|
if let Some(a) = config.get("dest") {
|
||||||
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||||
|
|
||||||
// If path is relative make it absolute from the parent directory of src
|
|
||||||
if dest.is_relative() {
|
|
||||||
dest = self.get_root().join(&dest);
|
|
||||||
}
|
|
||||||
self.set_dest(&dest);
|
self.set_dest(&dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source folder
|
// Source folder
|
||||||
if let Some(a) = config.get("src") {
|
if let Some(a) = config.get("src") {
|
||||||
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
|
let src = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||||
if src.is_relative() {
|
|
||||||
src = self.get_root().join(&src);
|
|
||||||
}
|
|
||||||
self.set_src(&src);
|
self.set_src(&src);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Theme path folder
|
// Metadata
|
||||||
if let Some(a) = config.get("theme_path") {
|
|
||||||
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
|
let extract_authors_from_slice = |x: &[toml::Value]| -> Vec<Author> {
|
||||||
if theme_path.is_relative() {
|
x.iter()
|
||||||
theme_path = self.get_root().join(&theme_path);
|
.filter_map(|x| x.as_table())
|
||||||
}
|
.map(|x| Author::from(x.to_owned()))
|
||||||
self.set_theme_path(&theme_path);
|
.collect::<Vec<Author>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(a) = config.get("title") {
|
||||||
|
self.title = a.to_string().replace("\"", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
if let Some(a) = config.get("subtitle") {
|
||||||
|
self.subtitle = Some(a.to_string().replace("\"", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_root(&self) -> &Path {
|
if let Some(a) = config.get("description") {
|
||||||
&self.root
|
self.description = Some(a.to_string().replace("\"", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = config.get("language") {
|
||||||
|
if let Some(b) = a.as_table() {
|
||||||
|
self.language = Language::from(b.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author name as a hash key.
|
||||||
|
if let Some(a) = config.get("author") {
|
||||||
|
if let Some(b) = a.as_str() {
|
||||||
|
self.authors = vec![Author::new(b)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authors as an array of tables. This will override the above.
|
||||||
|
if let Some(a) = config.get("authors") {
|
||||||
|
if let Some(b) = a.as_slice() {
|
||||||
|
self.authors = extract_authors_from_slice(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translator name as a hash key.
|
||||||
|
if let Some(a) = config.get("translator") {
|
||||||
|
if let Some(b) = a.as_str() {
|
||||||
|
self.translators = Some(vec![Author::new(b)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translators as an array of tables. This will override the above.
|
||||||
|
if let Some(a) = config.get("translators") {
|
||||||
|
if let Some(b) = a.as_slice() {
|
||||||
|
self.translators = Some(extract_authors_from_slice(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = config.get("publisher") {
|
||||||
|
if let Some(b) = a.as_table() {
|
||||||
|
self.publisher = Some(Publisher::from(b.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = config.get("number_format") {
|
||||||
|
if let Some(b) = a.as_str() {
|
||||||
|
self.number_format = match b.to_lowercase().as_ref() {
|
||||||
|
"arabic" => NumberFormat::Arabic,
|
||||||
|
"roman" => NumberFormat::Roman,
|
||||||
|
"word" => NumberFormat::Word,
|
||||||
|
_ => NumberFormat::Arabic,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = config.get("section_names") {
|
||||||
|
if let Some(b) = a.as_slice() {
|
||||||
|
self.section_names =
|
||||||
|
b.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = config.get("is_main_book") {
|
||||||
|
if let Some(b) = a.as_bool() {
|
||||||
|
self.is_main_book = b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_root(&mut self, root: &Path) -> &mut Self {
|
|
||||||
self.root = root.to_owned();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +218,7 @@ impl BookConfig {
|
||||||
&self.dest
|
&self.dest
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
|
pub fn set_dest(&mut self, dest: &Path) -> &mut BookConfig {
|
||||||
self.dest = dest.to_owned();
|
self.dest = dest.to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -182,47 +227,152 @@ impl BookConfig {
|
||||||
&self.src
|
&self.src
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_src(&mut self, src: &Path) -> &mut Self {
|
pub fn set_src(&mut self, src: &Path) -> &mut BookConfig {
|
||||||
self.src = src.to_owned();
|
self.src = src.to_owned();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_theme_path(&self) -> &Path {
|
}
|
||||||
&self.theme_path
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Author {
|
||||||
|
/// Author's name, such as "Howard Philip Lovecraft"
|
||||||
|
name: String,
|
||||||
|
/// Author's name in the form of "Lovecraft, Howard Philip", an ebook metadata field used for sorting
|
||||||
|
file_as: String,
|
||||||
|
email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Author {
|
||||||
|
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
Author {
|
||||||
|
name: name.to_owned(),
|
||||||
|
file_as: utils::last_name_first(name),
|
||||||
|
email: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
|
pub fn file_as(mut self, file_as: &str) -> Self {
|
||||||
self.theme_path = theme_path.to_owned();
|
self.file_as = file_as.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_email(mut self, email: &str) -> Self {
|
||||||
|
self.email = Some(email.to_owned());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
|
impl From<toml::Table> for Author {
|
||||||
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
|
fn from(data: toml::Table) -> Author {
|
||||||
|
let mut author = Author::new("The Author");
|
||||||
for (key, value) in json.iter() {
|
if let Some(x) = data.get("name") {
|
||||||
config.insert(
|
author.name = x.to_string().replace("\"", "");
|
||||||
String::from_str(key).unwrap(),
|
|
||||||
json_value_to_toml_value(value.to_owned())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if let Some(x) = data.get("file_as") {
|
||||||
config
|
author.file_as = x.to_string().replace("\"", "");
|
||||||
}
|
} else {
|
||||||
|
author.file_as = utils::last_name_first(&author.name);
|
||||||
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
|
}
|
||||||
match json {
|
if let Some(x) = data.get("email") {
|
||||||
serde_json::Value::Null => toml::Value::String("".to_string()),
|
author.email = Some(x.to_string().replace("\"", ""));
|
||||||
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
|
}
|
||||||
serde_json::Value::I64(x) => toml::Value::Integer(x),
|
author
|
||||||
serde_json::Value::U64(x) => toml::Value::Integer(x as i64),
|
|
||||||
serde_json::Value::F64(x) => toml::Value::Float(x),
|
|
||||||
serde_json::Value::String(x) => toml::Value::String(x),
|
|
||||||
serde_json::Value::Array(x) => {
|
|
||||||
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
|
|
||||||
},
|
|
||||||
serde_json::Value::Object(x) => {
|
|
||||||
toml::Value::Table(json_object_to_btreemap(&x))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Language {
|
||||||
|
pub name: String,
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Language {
|
||||||
|
fn default() -> Self {
|
||||||
|
Language {
|
||||||
|
name: String::from("English"),
|
||||||
|
code: String::from("en"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Language {
|
||||||
|
pub fn new(name: &str, code: &str) -> Language {
|
||||||
|
Language{
|
||||||
|
name: name.to_string(),
|
||||||
|
code: code.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::Table> for Language {
|
||||||
|
fn from(data: toml::Table) -> Language {
|
||||||
|
let mut language = Language::default();
|
||||||
|
if let Some(x) = data.get("name") {
|
||||||
|
language.name = x.to_string().replace("\"", "");
|
||||||
|
}
|
||||||
|
if let Some(x) = data.get("code") {
|
||||||
|
language.code = x.to_string().replace("\"", "");
|
||||||
|
}
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Publisher {
|
||||||
|
/// name of the publisher organization
|
||||||
|
name: String,
|
||||||
|
/// link to the sublisher's site
|
||||||
|
url: Option<String>,
|
||||||
|
/// path to publisher's logo image
|
||||||
|
logo_src: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Publisher {
|
||||||
|
fn default() -> Publisher {
|
||||||
|
Publisher {
|
||||||
|
name: "The Publisher".to_string(),
|
||||||
|
url: None,
|
||||||
|
logo_src: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Publisher {
|
||||||
|
pub fn new(name: &str) -> Publisher {
|
||||||
|
Publisher {
|
||||||
|
name: name.to_string(),
|
||||||
|
url: None,
|
||||||
|
logo_src: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::Table> for Publisher {
|
||||||
|
fn from(data: toml::Table) -> Publisher {
|
||||||
|
let mut publisher = Publisher::default();
|
||||||
|
if let Some(x) = data.get("name") {
|
||||||
|
publisher.name = x.to_string().replace("\"", "");
|
||||||
|
}
|
||||||
|
if let Some(x) = data.get("url") {
|
||||||
|
publisher.url = Some(x.to_string());
|
||||||
|
}
|
||||||
|
if let Some(x) = data.get("logo_src") {
|
||||||
|
publisher.logo_src = Some(PathBuf::from(x.to_string()));
|
||||||
|
}
|
||||||
|
publisher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NumberFormat when rendering chapter titles.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NumberFormat {
|
||||||
|
/// 19
|
||||||
|
Arabic,
|
||||||
|
/// XIX
|
||||||
|
Roman,
|
||||||
|
/// Nineteen
|
||||||
|
Word,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +1,215 @@
|
||||||
|
extern crate regex;
|
||||||
|
extern crate toml;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use book::metadata::Author;
|
use std::fs::File;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
/// The Chapter struct holds the title of the chapter as written in the SUMMARY.md file,
|
use utils;
|
||||||
/// the location of the markdown file containing the content and eventually sub-chapters
|
use book::bookconfig::Author;
|
||||||
|
|
||||||
/// TODO use in template: author, description, index, class
|
use utils::fs::create_with_str;
|
||||||
|
|
||||||
|
/// The Chapter struct holds the title of the chapter as written in the
|
||||||
|
/// SUMMARY.md file, the location of the markdown file and other metadata.
|
||||||
|
///
|
||||||
|
/// If the markdown file starts with a TOML header, it will be parsed to set the
|
||||||
|
/// chapter's properties. A TOML header should start and end with `+++` lines:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// +++
|
||||||
|
/// title = "The Library of Babel"
|
||||||
|
/// author = "Jorge Luis Borges"
|
||||||
|
/// translator = "James E. Irby"
|
||||||
|
/// +++
|
||||||
|
///
|
||||||
|
/// # Babel
|
||||||
|
///
|
||||||
|
/// The universe (which others call the Library) is composed of an indefinite and
|
||||||
|
/// perhaps infinite number of hexagonal galleries, with vast air shafts between,
|
||||||
|
/// surrounded by very low railings. From any of the hexagons one can see,
|
||||||
|
/// interminably, the upper and lower floors.
|
||||||
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Chapter {
|
pub struct Chapter {
|
||||||
|
|
||||||
/// The title of the chapter.
|
/// The title of the chapter.
|
||||||
title: String,
|
pub title: String,
|
||||||
/// Path to chapter's markdown file.
|
|
||||||
file: PathBuf,
|
|
||||||
|
|
||||||
/// TODO The author of the chapter, or the book.
|
/// Path to the chapter's markdown file, relative to the book's source
|
||||||
author: Author,
|
/// directory.
|
||||||
/// TODO The description of the chapter.
|
///
|
||||||
description: String,
|
/// `book.src.join(chapter.path)` points to the Markdown file, and
|
||||||
/// TODO Index number of the chapter in its level. This is the Vec index + 1.
|
/// `book.dest.join(chapter.path).with_extension("html")` points to the
|
||||||
index: i32,
|
/// output html file. This way if the user had a custom folder structure in
|
||||||
/// TODO CSS class that will be added to the page-level wrap div to allow customized chapter styles.
|
/// their source folder, this is re-created in the destination folder.
|
||||||
class: String,
|
pub path: PathBuf,
|
||||||
|
|
||||||
sub_chapters: Vec<Chapter>,
|
/// Optional destination path to write to. Used when changing the first
|
||||||
|
/// chapter's path to index.html.
|
||||||
|
pub dest_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// The author of the chapter, or the book.
|
||||||
|
pub authors: Option<Vec<Author>>,
|
||||||
|
|
||||||
|
/// The translators of the chapter, or the book.
|
||||||
|
pub translators: Option<Vec<Author>>,
|
||||||
|
|
||||||
|
/// The description of the chapter.
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// CSS class that will be added to the page-level wrap div to allow
|
||||||
|
/// customized chapter styles.
|
||||||
|
pub css_class: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Chapter {
|
||||||
|
fn default() -> Chapter {
|
||||||
|
Chapter {
|
||||||
|
title: "Untitled".to_string(),
|
||||||
|
path: PathBuf::from("src".to_string()).join("untitled.md"),
|
||||||
|
dest_path: None,
|
||||||
|
authors: None,
|
||||||
|
translators: None,
|
||||||
|
description: None,
|
||||||
|
css_class: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chapter {
|
impl Chapter {
|
||||||
/// Creates a new chapter with the given title and source file and no sub-chapters
|
|
||||||
pub fn new(title: &str, file: &Path) -> Self {
|
|
||||||
Chapter {
|
|
||||||
title: title.to_owned(),
|
|
||||||
file: file.to_owned(),
|
|
||||||
|
|
||||||
sub_chapters: Vec::new(),
|
pub fn new(title: String, path: PathBuf) -> Chapter {
|
||||||
|
let mut chapter = Chapter::default();
|
||||||
// TODO placeholder values for now
|
chapter.title = title;
|
||||||
author: Author::new(""),
|
chapter.path = path;
|
||||||
description: "".to_string(),
|
chapter
|
||||||
index: 0,
|
|
||||||
class: "".to_string(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function takes a slice `&[x,y,z]` and returns the corresponding sub-chapter if it exists.
|
pub fn parse_or_create_using(&mut self, book_src_dir: &PathBuf) -> Result<&mut Self, String> {
|
||||||
///
|
|
||||||
/// For example: `chapter.get_sub_chapter(&[1,3])` will return the third sub-chapter of the first sub-chapter.
|
debug!("[fn] Chapter::parse_or_create() : {:?}", &self);
|
||||||
pub fn get_sub_chapter(&self, section: &[usize]) -> Option<&Chapter> {
|
|
||||||
match section.len() {
|
let src_path = &book_src_dir.join(&self.path).to_owned();
|
||||||
0 => None,
|
if !src_path.exists() {
|
||||||
1 => self.sub_chapters.get(section[0]),
|
debug!("[*] Creating: {:?}", src_path);
|
||||||
_ => {
|
match create_with_str(src_path, &format!("# {}", self.title)) {
|
||||||
// The lengt of the slice is more than one, this means that we want a sub-chapter of a sub-chapter
|
Ok(_) => { return Ok(self); },
|
||||||
// We call `get_sub_chapter` recursively until we are deep enough and return the asked sub-chapter
|
Err(e) => {
|
||||||
self.sub_chapters
|
return Err(format!("Could not create: {:?}", src_path));
|
||||||
.get(section[0])
|
|
||||||
.and_then(|ch| ch.get_sub_chapter(§ion[1..]))
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(&self) -> &str {
|
let mut text = String::new();
|
||||||
&self.title
|
match File::open(src_path) {
|
||||||
|
Err(e) => { return Err(format!("Read error: {:?}", e)); },
|
||||||
|
Ok(mut f) => {
|
||||||
|
f.read_to_string(&mut text);
|
||||||
}
|
}
|
||||||
pub fn file(&self) -> &Path {
|
|
||||||
&self.file
|
|
||||||
}
|
}
|
||||||
pub fn sub_chapters(&self) -> &[Chapter] {
|
|
||||||
&self.sub_chapters
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
845
src/book/mod.rs
|
@ -1,123 +1,144 @@
|
||||||
pub mod bookitem;
|
extern crate toml;
|
||||||
pub mod bookconfig;
|
|
||||||
pub mod metadata;
|
|
||||||
pub mod chapter;
|
|
||||||
pub mod book;
|
pub mod book;
|
||||||
|
pub mod bookconfig;
|
||||||
|
pub mod toc;
|
||||||
|
pub mod chapter;
|
||||||
|
|
||||||
pub use self::metadata::{Author, Language, BookMetadata};
|
|
||||||
pub use self::chapter::Chapter;
|
|
||||||
pub use self::book::Book;
|
pub use self::book::Book;
|
||||||
|
use renderer::{Renderer, HtmlHandlebars};
|
||||||
|
use utils;
|
||||||
|
|
||||||
pub mod bookconfig_test;
|
use std::env;
|
||||||
|
use std::process::exit;
|
||||||
pub use self::bookitem::{BookItem, BookItems};
|
use std::ffi::OsStr;
|
||||||
pub use self::bookconfig::BookConfig;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
|
use std::io::Read;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io;
|
use std::collections::{HashMap, BTreeMap};
|
||||||
use std::io::Write;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::process::Command;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use {theme, parse, utils};
|
#[derive(Debug, Clone)]
|
||||||
use renderer::{Renderer, HtmlHandlebars};
|
pub struct MDBook {
|
||||||
|
|
||||||
|
/// Top-level directory of the book project, as an absolute path. Defaults
|
||||||
|
/// to the current directory. `set_project_root()` converts relative paths
|
||||||
|
/// to absolute.
|
||||||
|
project_root: PathBuf,
|
||||||
|
|
||||||
pub struct MDBook<'a> {
|
/// Path to the template for the renderer, relative to `project_root`.
|
||||||
root: PathBuf,
|
/// The `render_intent` determines its default value.
|
||||||
dest: PathBuf,
|
///
|
||||||
src: PathBuf,
|
/// A book doesn't necessarily has to have the template files. When not
|
||||||
theme_path: PathBuf,
|
/// found in the book's folder, the embedded static assets will be used.
|
||||||
|
///
|
||||||
|
/// Html Handlebars: `project_root` + `assets/html-template`.
|
||||||
|
template_dir: PathBuf,
|
||||||
|
|
||||||
pub title: String,
|
/// Output base for all books, relative to `project_root`. Defaults to
|
||||||
pub author: String,
|
/// `book`.
|
||||||
pub description: String,
|
dest_base: PathBuf,
|
||||||
|
|
||||||
pub content: Vec<BookItem>,
|
/// Informs other functions which renderer has been selected, either by
|
||||||
books: HashMap<&'a str, Book>,
|
/// default or CLI argument.
|
||||||
renderer: Box<Renderer>,
|
render_intent: RenderIntent,
|
||||||
|
|
||||||
livereload: Option<String>,
|
// TODO Identify and cross-link translations either by file name, or an id
|
||||||
|
// string.
|
||||||
|
|
||||||
|
/// The book, or books in case of translations, accessible with a String
|
||||||
|
/// key. The keys can be two-letter codes of the translation such as 'en' or
|
||||||
|
/// 'fr', but this is not enforced.
|
||||||
|
///
|
||||||
|
/// The String keys will be sub-folders where the translation's Markdown
|
||||||
|
/// sources are expected.
|
||||||
|
///
|
||||||
|
/// Each translation should have its own SUMMARY.md file, in its source
|
||||||
|
/// folder with the chapter files.
|
||||||
|
///
|
||||||
|
/// In the case of a single language, it is the sole item in the HashMap,
|
||||||
|
/// and its source is not expected to be under a sub-folder, just simply in
|
||||||
|
/// `./src`.
|
||||||
|
///
|
||||||
|
/// Translations have to be declared in `book.toml` in their separate
|
||||||
|
/// blocks. In this case `is_main_book = true` has to be set to mark the
|
||||||
|
/// main book to avoid ambiguity.
|
||||||
|
///
|
||||||
|
/// For a single language, the book's properties can be set on the main
|
||||||
|
/// block:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// livereload = true
|
||||||
|
/// title = "Alice in Wonderland"
|
||||||
|
/// author = "Lewis Carroll"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// For multiple languages, declare them in blocks:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// livereload = true
|
||||||
|
///
|
||||||
|
/// [translations.en]
|
||||||
|
/// title = "Alice in Wonderland"
|
||||||
|
/// author = "Lewis Carroll"
|
||||||
|
/// language = { name = "English", code = "en" }
|
||||||
|
/// is_main_book = true
|
||||||
|
///
|
||||||
|
/// [translations.fr]
|
||||||
|
/// title = "Alice au pays des merveilles"
|
||||||
|
/// author = "Lewis Carroll"
|
||||||
|
/// translator = "Henri Bué"
|
||||||
|
/// language = { name = "Français", code = "fr" }
|
||||||
|
///
|
||||||
|
/// [translations.hu]
|
||||||
|
/// title = "Alice Csodaországban"
|
||||||
|
/// author = "Lewis Carroll"
|
||||||
|
/// translator = "Kosztolányi Dezső"
|
||||||
|
/// language = { name = "Hungarian", code = "hu" }
|
||||||
|
/// ```
|
||||||
|
pub translations: HashMap<String, Book>,
|
||||||
|
|
||||||
|
/// Space indentation in SUMMARY.md, defaults to 4 spaces.
|
||||||
|
pub indent_spaces: i32,
|
||||||
|
|
||||||
|
/// Whether to include the livereload snippet in the output html.
|
||||||
|
pub livereload: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MDBook<'a> {
|
impl Default for MDBook {
|
||||||
/// Create a new `MDBook` struct with root directory `root`
|
fn default() -> MDBook {
|
||||||
///
|
let mut proj: MDBook = MDBook {
|
||||||
/// Default directory paths:
|
project_root: PathBuf::from("".to_string()),
|
||||||
///
|
template_dir: PathBuf::from("".to_string()),
|
||||||
/// - source: `root/src`
|
dest_base: PathBuf::from("book".to_string()),
|
||||||
/// - output: `root/book`
|
render_intent: RenderIntent::HtmlHandlebars,
|
||||||
/// - theme: `root/theme`
|
translations: HashMap::new(),
|
||||||
///
|
indent_spaces: 4,
|
||||||
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
|
livereload: false,
|
||||||
|
};
|
||||||
pub fn new(root: &Path) -> MDBook {
|
proj.set_project_root(&env::current_dir().unwrap());
|
||||||
|
// sets default template_dir
|
||||||
if !root.exists() || !root.is_dir() {
|
proj.set_render_intent(RenderIntent::HtmlHandlebars);
|
||||||
warn!("{:?} No directory with that name", root);
|
proj
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MDBook {
|
#[derive(Debug, Clone)]
|
||||||
root: root.to_owned(),
|
pub enum RenderIntent {
|
||||||
dest: root.join("book"),
|
HtmlHandlebars,
|
||||||
src: root.join("src"),
|
}
|
||||||
theme_path: root.join("theme"),
|
|
||||||
|
|
||||||
title: String::new(),
|
impl MDBook {
|
||||||
author: String::new(),
|
|
||||||
description: String::new(),
|
|
||||||
|
|
||||||
content: vec![],
|
/// Create a new `MDBook` struct with top-level project directory `project_root`
|
||||||
books: HashMap::new(),
|
pub fn new(project_root: &PathBuf) -> MDBook {
|
||||||
renderer: Box::new(HtmlHandlebars::new()),
|
MDBook::default().set_project_root(project_root).clone()
|
||||||
|
|
||||||
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)`
|
|
||||||
///
|
|
||||||
/// ```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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// book-test/
|
/// book-example/
|
||||||
/// ├── book
|
/// ├── book
|
||||||
/// └── src
|
/// └── src
|
||||||
/// ├── chapter_1.md
|
/// ├── chapter_1.md
|
||||||
|
@ -126,358 +147,348 @@ impl<'a> MDBook<'a> {
|
||||||
///
|
///
|
||||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
||||||
/// `chapter_1.md` to the source directory.
|
/// `chapter_1.md` to the source directory.
|
||||||
|
|
||||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
debug!("[fn]: init");
|
debug!("[fn]: init");
|
||||||
|
|
||||||
if !self.root.exists() {
|
if !self.project_root.exists() {
|
||||||
fs::create_dir_all(&self.root).unwrap();
|
fs::create_dir_all(&self.project_root).unwrap();
|
||||||
info!("{:?} created", &self.root);
|
info!("{:?} created", &self.project_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Read book.toml if exists and populate .translations
|
||||||
|
self.read_config();
|
||||||
if !self.dest.exists() {
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
|
|
||||||
try!(fs::create_dir_all(&self.dest));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.src.exists() {
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
|
|
||||||
try!(fs::create_dir_all(&self.src));
|
|
||||||
}
|
|
||||||
|
|
||||||
let summary = self.src.join("SUMMARY.md");
|
|
||||||
|
|
||||||
if !summary.exists() {
|
|
||||||
|
|
||||||
// Summary does not exist, create it
|
|
||||||
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
|
|
||||||
let mut f = try!(File::create(&self.src.join("SUMMARY.md")));
|
|
||||||
|
|
||||||
debug!("[*]: Writing to SUMMARY.md");
|
|
||||||
|
|
||||||
try!(writeln!(f, "# Summary"));
|
|
||||||
try!(writeln!(f, ""));
|
|
||||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse SUMMARY.md, and create the missing item related file
|
|
||||||
try!(self.parse_summary());
|
|
||||||
|
|
||||||
debug!("[*]: constructing paths for missing files");
|
|
||||||
for item in self.iter() {
|
|
||||||
debug!("[*]: item: {:?}", item);
|
|
||||||
match *item {
|
|
||||||
BookItem::Spacer => continue,
|
|
||||||
BookItem::Chapter(_, ref ch) |
|
|
||||||
BookItem::Affix(ref ch) => {
|
|
||||||
if ch.path != PathBuf::new() {
|
|
||||||
let path = self.src.join(&ch.path);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
|
||||||
try!(::std::fs::create_dir_all(path.parent().unwrap()));
|
|
||||||
let mut f = try!(File::create(path));
|
|
||||||
|
|
||||||
// debug!("[*]: Writing to {:?}", path);
|
|
||||||
try!(writeln!(f, "# {}", ch.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("[*]: init done");
|
debug!("[*]: init done");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_gitignore(&self) {
|
/// Parses the `book.toml` file (if it exists) to extract the configuration parameters.
|
||||||
let gitignore = self.get_gitignore();
|
/// The `book.toml` file should be in the root directory of the book project.
|
||||||
|
/// The project root directory is the one specified when creating a new `MDBook`
|
||||||
if !gitignore.exists() {
|
|
||||||
// Gitignore does not exist, create it
|
|
||||||
|
|
||||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
|
|
||||||
// is not, `strip_prefix` will return an Error.
|
|
||||||
if !self.get_dest().starts_with(&self.root) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative = self.get_dest()
|
|
||||||
.strip_prefix(&self.root)
|
|
||||||
.expect("Destination is not relative to root.");
|
|
||||||
let relative = relative.to_str()
|
|
||||||
.expect("Path could not be yielded into a string slice.");
|
|
||||||
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
|
|
||||||
|
|
||||||
let mut f = File::create(&gitignore).expect("Could not create file.");
|
|
||||||
|
|
||||||
debug!("[*]: Writing to .gitignore");
|
|
||||||
|
|
||||||
writeln!(f, "{}", relative).expect("Could not write to file.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
|
|
||||||
/// construct the book's structure in the form of a `Vec<BookItem>` and then calls `render()`
|
|
||||||
/// method of the current renderer.
|
|
||||||
///
|
|
||||||
/// It is the renderer who generates all the output files.
|
|
||||||
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
|
||||||
debug!("[fn]: build");
|
|
||||||
|
|
||||||
try!(self.init());
|
|
||||||
|
|
||||||
// Clean output directory
|
|
||||||
try!(utils::fs::remove_dir_content(&self.dest));
|
|
||||||
|
|
||||||
try!(self.renderer.render(&self));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_gitignore(&self) -> PathBuf {
|
|
||||||
self.root.join(".gitignore")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
|
||||||
debug!("[fn]: copy_theme");
|
|
||||||
|
|
||||||
let theme_dir = self.src.join("theme");
|
|
||||||
|
|
||||||
if !theme_dir.exists() {
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
|
||||||
try!(fs::create_dir(&theme_dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
// index.hbs
|
|
||||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
|
||||||
try!(index.write_all(theme::INDEX));
|
|
||||||
|
|
||||||
// book.css
|
|
||||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
|
||||||
try!(css.write_all(theme::CSS));
|
|
||||||
|
|
||||||
// favicon.png
|
|
||||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
|
||||||
try!(favicon.write_all(theme::FAVICON));
|
|
||||||
|
|
||||||
// book.js
|
|
||||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
|
||||||
try!(js.write_all(theme::JS));
|
|
||||||
|
|
||||||
// highlight.css
|
|
||||||
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
|
|
||||||
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
|
|
||||||
|
|
||||||
// highlight.js
|
|
||||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
|
||||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the `book.json` file (if it exists) to extract the configuration parameters.
|
|
||||||
/// The `book.json` file should be in the root directory of the book.
|
|
||||||
/// The root directory is the one specified when creating a new `MDBook`
|
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # extern crate mdbook;
|
/// # extern crate mdbook;
|
||||||
/// # use mdbook::MDBook;
|
/// # use mdbook::MDBook;
|
||||||
/// # use std::path::Path;
|
/// # use std::path::Path;
|
||||||
/// # fn main() {
|
/// # fn main() {
|
||||||
/// let mut book = MDBook::new(Path::new("root_dir"));
|
/// let mut book = MDBook::new(Path::new("project_root_dir"));
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// In this example, `root_dir` will be the root directory of our book and is specified in function
|
/// In this example, `project_root_dir` will be the root directory of our book and is specified in function
|
||||||
/// of the current working directory by using a relative path instead of an absolute path.
|
/// of the current working directory by using a relative path instead of an absolute path.
|
||||||
|
pub fn read_config(&mut self) -> &mut Self {
|
||||||
|
|
||||||
pub fn read_config(mut self) -> Self {
|
debug!("[fn]: read_config");
|
||||||
|
|
||||||
let config = BookConfig::new(&self.root)
|
// TODO refactor to a helper that returns Result?
|
||||||
.read_config(&self.root)
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
self.title = config.title;
|
// TODO Maybe some error handling instead of exit(2), although it is a
|
||||||
self.description = config.description;
|
// clear indication for the user that something is wrong and we can't
|
||||||
self.author = config.author;
|
// fix it for them.
|
||||||
|
|
||||||
self.dest = config.dest;
|
let read_file = |path: PathBuf| -> String {
|
||||||
self.src = config.src;
|
let mut data = String::new();
|
||||||
self.theme_path = config.theme_path;
|
let mut f: File = match File::open(&path) {
|
||||||
|
Ok(x) => x,
|
||||||
self
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_theme_path(&self) -> &Path {
|
/// Configures MDBook properties and translations.
|
||||||
&self.theme_path
|
///
|
||||||
|
/// After parsing properties for MDBook struct, it removes them from the
|
||||||
|
/// config (template_dir, livereload, etc.). The remaining keys on the main
|
||||||
|
/// block will be interpreted as properties of the main book.
|
||||||
|
///
|
||||||
|
/// `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
|
if let Some(a) = config.get("dest_base") {
|
||||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
self.set_dest_base(&PathBuf::from(&a.to_string()));
|
||||||
// When append becomes stable, use self.content.append() ...
|
|
||||||
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
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(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(§ion[1..]))
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
12
src/lib.rs
|
@ -69,19 +69,23 @@
|
||||||
//!
|
//!
|
||||||
//! Make sure to take a look at it.
|
//! Make sure to take a look at it.
|
||||||
|
|
||||||
|
extern crate includedir;
|
||||||
|
extern crate phf;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/data.rs"));
|
||||||
|
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate handlebars;
|
extern crate handlebars;
|
||||||
extern crate pulldown_cmark;
|
extern crate pulldown_cmark;
|
||||||
|
extern crate regex;
|
||||||
|
extern crate glob;
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
pub mod book;
|
pub mod book;
|
||||||
mod parse;
|
mod parse;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod theme;
|
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
pub use book::MDBook;
|
pub use book::MDBook;
|
||||||
pub use book::BookItem;
|
|
||||||
pub use book::BookConfig;
|
|
||||||
pub use renderer::Renderer;
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
pub use self::summary::construct_bookitems;
|
pub use self::summary::construct_tocitems;
|
||||||
|
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
|
@ -1,26 +1,41 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Result, Error, ErrorKind};
|
use std::io::{Read, Result, Error, ErrorKind};
|
||||||
use book::bookitem::{BookItem, Chapter};
|
|
||||||
|
|
||||||
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
use book::chapter::Chapter;
|
||||||
debug!("[fn]: construct_bookitems");
|
use book::toc::{TocItem, TocContent};
|
||||||
|
|
||||||
|
pub fn construct_tocitems(summary_path: &PathBuf, first_as_index: bool) -> Result<Vec<TocItem>> {
|
||||||
|
debug!("[fn]: construct_tocitems");
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
try!(try!(File::open(path)).read_to_string(&mut summary));
|
try!(try!(File::open(summary_path)).read_to_string(&mut summary));
|
||||||
|
|
||||||
debug!("[*]: Parse SUMMARY.md");
|
debug!("[*]: Parse SUMMARY.md");
|
||||||
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
|
|
||||||
|
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0], first_as_index));
|
||||||
debug!("[*]: Done parsing SUMMARY.md");
|
debug!("[*]: Done parsing SUMMARY.md");
|
||||||
Ok(top_items)
|
Ok(top_items)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
|
pub fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>, first_as_index: bool) -> Result<Vec<TocItem>> {
|
||||||
debug!("[fn]: parse_level");
|
debug!("[fn]: parse_level");
|
||||||
let mut items: Vec<BookItem> = vec![];
|
let mut items: Vec<TocItem> = vec![];
|
||||||
|
|
||||||
|
let mut found_first = false;
|
||||||
|
|
||||||
|
let ohnoes = r#"Your SUMMARY.md is messed up
|
||||||
|
|
||||||
|
Unnumbered and Spacer items can only exist on the root level.
|
||||||
|
|
||||||
|
Unnumbered items can only exist before or after Numbered items, since these
|
||||||
|
items are in the frontmatter of a book.
|
||||||
|
|
||||||
|
There can be no Numbered items after Unnumbered items, as they are in the
|
||||||
|
backmatter."#;
|
||||||
|
|
||||||
// Construct the book recursively
|
// Construct the book recursively
|
||||||
while !summary.is_empty() {
|
while !summary.is_empty() {
|
||||||
let item: BookItem;
|
let item: TocItem;
|
||||||
// Indentation level of the line to parse
|
// Indentation level of the line to parse
|
||||||
let level = try!(level(summary[0], 4));
|
let level = try!(level(summary[0], 4));
|
||||||
|
|
||||||
|
@ -35,58 +50,58 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
// Level can not be root level !!
|
// Level can not be root level !!
|
||||||
// Add a sub-number to section
|
// Add a sub-number to section
|
||||||
section.push(0);
|
section.push(0);
|
||||||
|
|
||||||
let last = items.pop().expect("There should be at least one item since this can't be the root level");
|
let last = items.pop().expect("There should be at least one item since this can't be the root level");
|
||||||
|
|
||||||
item = if let BookItem::Chapter(ref s, ref ch) = last {
|
item = match last {
|
||||||
let mut ch = ch.clone();
|
TocItem::Numbered(mut a) => {
|
||||||
ch.sub_items = try!(parse_level(summary, level, section.clone()));
|
let sec = section.clone();
|
||||||
items.push(BookItem::Chapter(s.clone(), ch));
|
a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false)));
|
||||||
|
items.push(TocItem::Numbered(a));
|
||||||
|
|
||||||
// Remove the last number from the section, because we got back to our level..
|
// Remove the last number from the section, because we got
|
||||||
|
// back to our level...
|
||||||
section.pop();
|
section.pop();
|
||||||
continue;
|
continue;
|
||||||
} else {
|
},
|
||||||
return Err(Error::new(ErrorKind::Other,
|
TocItem::Unnumbered(mut a) => {
|
||||||
format!("Your summary.md is messed up\n\n
|
let sec = section.clone();
|
||||||
Prefix, \
|
a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false)));
|
||||||
Suffix and Spacer elements can only exist on the root level.\n
|
items.push(TocItem::Unnumbered(a));
|
||||||
\
|
section.pop();
|
||||||
Prefix elements can only exist before any chapter and there can be \
|
continue;
|
||||||
no chapters after suffix elements.")));
|
},
|
||||||
|
TocItem::Unlisted(mut a) => {
|
||||||
|
let sec = section.clone();
|
||||||
|
a.sub_items = Some(try!(parse_level(summary, level, sec.clone(), false)));
|
||||||
|
items.push(TocItem::Unlisted(a));
|
||||||
|
section.pop();
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(Error::new(ErrorKind::Other, ohnoes));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// level and current_level are the same, parse the line
|
// level and current_level are the same, parse the line
|
||||||
item = if let Some(parsed_item) = parse_line(summary[0]) {
|
item = if let Some(parsed_item) = parse_line(summary[0]) {
|
||||||
|
|
||||||
// Eliminate possible errors and set section to -1 after suffix
|
// Eliminate possible errors and set section to -1 after unnumbered
|
||||||
match parsed_item {
|
match parsed_item {
|
||||||
// error if level != 0 and BookItem is != Chapter
|
|
||||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
// error if level != 0 and TocItem is != Numbered
|
||||||
return Err(Error::new(ErrorKind::Other,
|
TocItem::Unnumbered(_) | TocItem::Spacer if level > 0 => {
|
||||||
format!("Your summary.md is messed up\n\n
|
return Err(Error::new(ErrorKind::Other, ohnoes))
|
||||||
\
|
|
||||||
Prefix, Suffix and Spacer elements can only exist on the \
|
|
||||||
root level.\n
|
|
||||||
Prefix \
|
|
||||||
elements can only exist before any chapter and there can be \
|
|
||||||
no chapters after suffix elements.")))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// error if BookItem == Chapter and section == -1
|
// error if TocItem == Numbered or Unlisted and section == -1
|
||||||
BookItem::Chapter(_, _) if section[0] == -1 => {
|
TocItem::Numbered(_) | TocItem::Unlisted(_) if section[0] == -1 => {
|
||||||
return Err(Error::new(ErrorKind::Other,
|
return Err(Error::new(ErrorKind::Other, ohnoes))
|
||||||
format!("Your summary.md is messed up\n\n
|
|
||||||
\
|
|
||||||
Prefix, Suffix and Spacer elements can only exist on the \
|
|
||||||
root level.\n
|
|
||||||
Prefix \
|
|
||||||
elements can only exist before any chapter and there can be \
|
|
||||||
no chapters after suffix elements.")))
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set section = -1 after suffix
|
// Set section = -1 after unnumbered
|
||||||
BookItem::Affix(_) if section[0] > 0 => {
|
TocItem::Unnumbered(_) if section[0] > 0 => {
|
||||||
section[0] = -1;
|
section[0] = -1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -94,12 +109,14 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
}
|
}
|
||||||
|
|
||||||
match parsed_item {
|
match parsed_item {
|
||||||
BookItem::Chapter(_, ch) => {
|
TocItem::Numbered(mut content) => {
|
||||||
// Increment section
|
// Increment section
|
||||||
let len = section.len() - 1;
|
let len = section.len() - 1;
|
||||||
section[len] += 1;
|
section[len] += 1;
|
||||||
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
|
||||||
BookItem::Chapter(s, ch)
|
content.section = Some(section.clone());
|
||||||
|
|
||||||
|
TocItem::Numbered(content)
|
||||||
},
|
},
|
||||||
_ => parsed_item,
|
_ => parsed_item,
|
||||||
}
|
}
|
||||||
|
@ -112,13 +129,33 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.remove(0);
|
summary.remove(0);
|
||||||
items.push(item)
|
|
||||||
|
if first_as_index && !found_first {
|
||||||
|
let i = match item {
|
||||||
|
TocItem::Numbered(mut content) => {
|
||||||
|
found_first = true;
|
||||||
|
content.chapter.dest_path = Some(PathBuf::from("index.html".to_string()));
|
||||||
|
TocItem::Numbered(content)
|
||||||
|
},
|
||||||
|
TocItem::Unnumbered(mut content) => {
|
||||||
|
found_first = true;
|
||||||
|
content.chapter.dest_path = Some(PathBuf::from("index.html".to_string()));
|
||||||
|
TocItem::Unnumbered(content)
|
||||||
|
},
|
||||||
|
TocItem::Unlisted(content) => {
|
||||||
|
TocItem::Unlisted(content)
|
||||||
|
},
|
||||||
|
TocItem::Spacer => TocItem::Spacer,
|
||||||
|
};
|
||||||
|
items.push(i);
|
||||||
|
} else {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
debug!("[*]: Level: {:?}", items);
|
debug!("[*]: Level: {:?}", items);
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||||
debug!("[fn]: level");
|
debug!("[fn]: level");
|
||||||
let mut spaces = 0;
|
let mut spaces = 0;
|
||||||
|
@ -147,8 +184,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||||
Ok(level)
|
Ok(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_line(l: &str) -> Option<TocItem> {
|
||||||
fn parse_line(l: &str) -> Option<BookItem> {
|
|
||||||
debug!("[fn]: parse_line");
|
debug!("[fn]: parse_line");
|
||||||
|
|
||||||
// Remove leading and trailing spaces or tabs
|
// Remove leading and trailing spaces or tabs
|
||||||
|
@ -157,7 +193,7 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||||
// Spacers are "------"
|
// Spacers are "------"
|
||||||
if line.starts_with("--") {
|
if line.starts_with("--") {
|
||||||
debug!("[*]: Line is spacer");
|
debug!("[*]: Line is spacer");
|
||||||
return Some(BookItem::Spacer);
|
return Some(TocItem::Spacer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = line.chars().nth(0) {
|
if let Some(c) = line.chars().nth(0) {
|
||||||
|
@ -166,8 +202,9 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||||
'-' | '*' => {
|
'-' | '*' => {
|
||||||
debug!("[*]: Line is list element");
|
debug!("[*]: Line is list element");
|
||||||
|
|
||||||
if let Some((name, path)) = read_link(line) {
|
if let Some((title, path)) = read_link(line) {
|
||||||
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)));
|
let chapter = Chapter::new(title, path);
|
||||||
|
return Some(TocItem::Numbered(TocContent::new(chapter)));
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -176,8 +213,9 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||||
'[' => {
|
'[' => {
|
||||||
debug!("[*]: Line is a link element");
|
debug!("[*]: Line is a link element");
|
||||||
|
|
||||||
if let Some((name, path)) = read_link(line) {
|
if let Some((title, path)) = read_link(line) {
|
||||||
return Some(BookItem::Affix(Chapter::new(name, path)));
|
let chapter = Chapter::new(title, path);
|
||||||
|
return Some(TocItem::Unnumbered(TocContent::new(chapter)));
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -209,7 +247,7 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = line[start_delimitor + 1..end_delimitor].to_owned();
|
let title = line[start_delimitor + 1..end_delimitor].to_owned();
|
||||||
|
|
||||||
start_delimitor = end_delimitor + 1;
|
start_delimitor = end_delimitor + 1;
|
||||||
if let Some(i) = line[start_delimitor..].find(')') {
|
if let Some(i) = line[start_delimitor..].find(')') {
|
||||||
|
@ -221,5 +259,5 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
|
||||||
|
|
||||||
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
|
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
|
||||||
|
|
||||||
Some((name, path))
|
Some((title, path))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use renderer::html_handlebars::helpers;
|
use renderer::html_handlebars::helpers;
|
||||||
use renderer::Renderer;
|
use renderer::Renderer;
|
||||||
use book::MDBook;
|
use book::{MDBook, Book};
|
||||||
use book::bookitem::BookItem;
|
use book::chapter::Chapter;
|
||||||
use {utils, theme};
|
use book::toc::{TocItem, TocContent};
|
||||||
|
use utils;
|
||||||
|
use FILES;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
@ -15,7 +18,6 @@ use handlebars::Handlebars;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_json::value::ToJson;
|
use serde_json::value::ToJson;
|
||||||
|
|
||||||
|
|
||||||
pub struct HtmlHandlebars;
|
pub struct HtmlHandlebars;
|
||||||
|
|
||||||
impl HtmlHandlebars {
|
impl HtmlHandlebars {
|
||||||
|
@ -25,16 +27,118 @@ impl HtmlHandlebars {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer for HtmlHandlebars {
|
impl Renderer for HtmlHandlebars {
|
||||||
fn render(&self, book: &MDBook) -> Result<(), Box<Error>> {
|
|
||||||
|
/// Prepares the project and calls `render()`.
|
||||||
|
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>> {
|
||||||
|
debug!("[fn]: build");
|
||||||
|
|
||||||
|
let mut book_project = MDBook::new(&project_root);
|
||||||
|
|
||||||
|
book_project.read_config();
|
||||||
|
book_project.parse_books();
|
||||||
|
|
||||||
|
// Clean output directory
|
||||||
|
try!(utils::fs::remove_dir_content(&book_project.get_dest_base()));
|
||||||
|
|
||||||
|
try!(self.render(&book_project));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the chapters and copies static assets.
|
||||||
|
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
|
debug!("[*]: Check if book's base output folder exists");
|
||||||
|
if let Err(_) = fs::create_dir_all(&book_project.get_dest_base()) {
|
||||||
|
return Err(Box::new(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Unexpected error when constructing path")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO write print version html
|
||||||
|
|
||||||
|
// TODO livereload
|
||||||
|
|
||||||
|
// Copy book's static assets
|
||||||
|
|
||||||
|
if book_project.get_project_root().join("assets").exists() {
|
||||||
|
|
||||||
|
let a = book_project.get_project_root().join("assets");
|
||||||
|
let base = a.to_str().unwrap();
|
||||||
|
|
||||||
|
let b = a.join("**").join("*");
|
||||||
|
let include_glob = b.to_str().unwrap();
|
||||||
|
|
||||||
|
let c = a.join("_*");
|
||||||
|
let exclude_glob = c.to_str().unwrap();
|
||||||
|
|
||||||
|
// anyone wants to catch errors?
|
||||||
|
utils::fs::copy_files(include_glob,
|
||||||
|
base,
|
||||||
|
vec![exclude_glob],
|
||||||
|
&book_project.get_dest_base());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy template's static assets
|
||||||
|
|
||||||
|
// If there is a template dir in the books's project folder, copy asset
|
||||||
|
// files from there, otherwise copy from embedded assets.
|
||||||
|
|
||||||
|
if book_project.get_template_dir().exists() {
|
||||||
|
|
||||||
|
let a = book_project.get_template_dir();
|
||||||
|
let base = a.to_str().unwrap();
|
||||||
|
|
||||||
|
let b = a.join("**").join("*");
|
||||||
|
let include_glob = b.to_str().unwrap();
|
||||||
|
|
||||||
|
let c = a.join("_*");
|
||||||
|
let exclude_glob = c.to_str().unwrap();
|
||||||
|
|
||||||
|
// don't try!(), copy_files() will send error values when trying to copy folders that are part of the file glob
|
||||||
|
//
|
||||||
|
// Error {
|
||||||
|
// repr: Custom(
|
||||||
|
// Custom {
|
||||||
|
// kind: Other,
|
||||||
|
// error: StringError(
|
||||||
|
// "Err(Error { repr: Custom(Custom { kind: InvalidInput, error: StringError(\"the source path is not an existing regular file\") }) })\n"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// anyone wants to catch errors?
|
||||||
|
utils::fs::copy_files(include_glob,
|
||||||
|
base,
|
||||||
|
vec![exclude_glob],
|
||||||
|
&book_project.get_dest_base());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
try!(utils::fs::copy_data("data/html-template/**/*",
|
||||||
|
"data/html-template/",
|
||||||
|
vec!["data/html-template/_*"],
|
||||||
|
&book_project.get_dest_base()));
|
||||||
|
}
|
||||||
|
|
||||||
debug!("[fn]: render");
|
debug!("[fn]: render");
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
|
|
||||||
// Load theme
|
// Render the chapters of each book
|
||||||
let theme = theme::Theme::new(book.get_theme_path());
|
for (key, book) in &book_project.translations {
|
||||||
|
|
||||||
|
// 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
|
// Register template
|
||||||
debug!("[*]: Register handlebars 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
|
// Register helpers
|
||||||
debug!("[*]: Register handlebars 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("previous", Box::new(helpers::navigation::previous));
|
||||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
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
|
// If this is a single book, config.dest default is
|
||||||
let mut print_content: String = String::new();
|
// `project_root/book`, and the earlier check will cover this.
|
||||||
|
|
||||||
// Check if dest directory exists
|
// If this is multi-language book, config.dest will
|
||||||
debug!("[*]: Check if destination directory exists");
|
// `project_book/book/key`, and so we check here for each book.
|
||||||
if let Err(_) = fs::create_dir_all(book.get_dest()) {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
debug!("[*]: Check if book's destination directory exists");
|
||||||
"Unexpected error when constructing destination path")));
|
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
|
// If this is the main book of a multi-language book, add an
|
||||||
let mut index = true;
|
// index.html to the project dest folder
|
||||||
for item in book.iter() {
|
|
||||||
|
|
||||||
match *item {
|
if book.config.is_multilang && book.config.is_main_book {
|
||||||
BookItem::Chapter(_, ref ch) |
|
match book.toc[0] {
|
||||||
BookItem::Affix(ref ch) => {
|
TocItem::Numbered(ref i) |
|
||||||
if ch.path != PathBuf::new() {
|
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 content = try!(chapter.read_content_using(&book.config.src));
|
||||||
let mut f = try!(File::open(&path));
|
|
||||||
let mut content: String = String::new();
|
|
||||||
|
|
||||||
debug!("[*]: Reading file");
|
|
||||||
try!(f.read_to_string(&mut content));
|
|
||||||
|
|
||||||
// Parse for playpen links
|
// Parse for playpen links
|
||||||
if let Some(p) = path.parent() {
|
if let Some(p) = book.config.get_src().join(&chapter.path).parent() {
|
||||||
content = helpers::playpen::render_playpen(&content, p);
|
content = helpers::playpen::render_playpen(&content, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render markdown using the pulldown-cmark crate
|
let mut data = try!(make_data(&book, &chapter, &content));
|
||||||
content = utils::render_markdown(&content);
|
|
||||||
print_content.push_str(&content);
|
|
||||||
|
|
||||||
// Remove content from previous file and render content for this one
|
|
||||||
data.remove("path");
|
|
||||||
match ch.path.to_str() {
|
|
||||||
Some(p) => {
|
|
||||||
data.insert("path".to_owned(), p.to_json());
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
|
||||||
"Could not convert path to str")))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Remove content from previous file and render content for this one
|
|
||||||
data.remove("content");
|
|
||||||
data.insert("content".to_owned(), content.to_json());
|
|
||||||
|
|
||||||
// Remove path to root from previous file and render content for this one
|
|
||||||
data.remove("path_to_root");
|
data.remove("path_to_root");
|
||||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&ch.path).to_json());
|
data.insert("path_to_root".to_owned(), "".to_json());
|
||||||
|
|
||||||
// Rendere the handlebars template with the data
|
// Rendere the handlebars template with the data
|
||||||
debug!("[*]: Render template");
|
debug!("[*]: Render template");
|
||||||
let rendered = try!(handlebars.render("index", &data));
|
let rendered_content = try!(handlebars.render("page", &data));
|
||||||
|
|
||||||
|
let p = chapter.dest_path.unwrap();
|
||||||
|
let rendered_path = &book_project.get_dest_base().join(&p);
|
||||||
|
|
||||||
|
debug!("[*]: Create file {:?}", rendered_path);
|
||||||
|
|
||||||
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
|
|
||||||
// Write to file
|
// Write to file
|
||||||
let mut file =
|
let mut file = try!(utils::fs::create_file(rendered_path));
|
||||||
try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
|
info!("[*] Creating {:?} ✓", rendered_path);
|
||||||
info!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
|
|
||||||
|
|
||||||
try!(file.write_all(&rendered.into_bytes()));
|
try!(file.write_all(&rendered_content.into_bytes()));
|
||||||
|
|
||||||
// Create an index.html from the first element in SUMMARY.md
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
_ => {},
|
TocItem::Spacer => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print version
|
// Render a file for every entry in the book
|
||||||
|
try!(self.process_items(&book.toc, &book, &handlebars));
|
||||||
// Remove content from previous file and render content for this one
|
}
|
||||||
data.remove("path");
|
|
||||||
data.insert("path".to_owned(), "print.md".to_json());
|
|
||||||
|
|
||||||
// Remove content from previous file and render content for this one
|
|
||||||
data.remove("content");
|
|
||||||
data.insert("content".to_owned(), print_content.to_json());
|
|
||||||
|
|
||||||
// Remove path to root from previous file and render content for this one
|
|
||||||
data.remove("path_to_root");
|
|
||||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(Path::new("print.md")).to_json());
|
|
||||||
|
|
||||||
// Rendere the handlebars template with the data
|
|
||||||
debug!("[*]: Render template");
|
|
||||||
let rendered = try!(handlebars.render("index", &data));
|
|
||||||
let mut file = try!(utils::fs::create_file(&book.get_dest().join("print").with_extension("html")));
|
|
||||||
try!(file.write_all(&rendered.into_bytes()));
|
|
||||||
info!("[*] Creating print.html ✓");
|
|
||||||
|
|
||||||
// Copy static files (js, css, images, ...)
|
|
||||||
|
|
||||||
debug!("[*] Copy static files");
|
|
||||||
// JavaScript
|
|
||||||
let mut js_file = if let Ok(f) = File::create(book.get_dest().join("book.js")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.js")));
|
|
||||||
};
|
|
||||||
try!(js_file.write_all(&theme.js));
|
|
||||||
|
|
||||||
// Css
|
|
||||||
let mut css_file = if let Ok(f) = File::create(book.get_dest().join("book.css")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.css")));
|
|
||||||
};
|
|
||||||
try!(css_file.write_all(&theme.css));
|
|
||||||
|
|
||||||
// Favicon
|
|
||||||
let mut favicon_file = if let Ok(f) = File::create(book.get_dest().join("favicon.png")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create favicon.png")));
|
|
||||||
};
|
|
||||||
try!(favicon_file.write_all(&theme.favicon));
|
|
||||||
|
|
||||||
// JQuery local fallback
|
|
||||||
let mut jquery = if let Ok(f) = File::create(book.get_dest().join("jquery.js")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create jquery.js")));
|
|
||||||
};
|
|
||||||
try!(jquery.write_all(&theme.jquery));
|
|
||||||
|
|
||||||
// syntax highlighting
|
|
||||||
let mut highlight_css = if let Ok(f) = File::create(book.get_dest().join("highlight.css")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.css")));
|
|
||||||
};
|
|
||||||
try!(highlight_css.write_all(&theme.highlight_css));
|
|
||||||
|
|
||||||
let mut tomorrow_night_css = if let Ok(f) = File::create(book.get_dest().join("tomorrow-night.css")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create tomorrow-night.css")));
|
|
||||||
};
|
|
||||||
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
|
|
||||||
|
|
||||||
let mut highlight_js = if let Ok(f) = File::create(book.get_dest().join("highlight.js")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.js")));
|
|
||||||
};
|
|
||||||
try!(highlight_js.write_all(&theme.highlight_js));
|
|
||||||
|
|
||||||
// Font Awesome local fallback
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/css/font-awesome.css")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/fontawesome-webfont.eot")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/fontawesome-webfont.svg")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/fontawesome-webfont.ttf")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff2")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
|
|
||||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
|
||||||
.join("_FontAwesome/fonts/FontAwesome.ttf")) {
|
|
||||||
f
|
|
||||||
} else {
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")));
|
|
||||||
};
|
|
||||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
|
||||||
|
|
||||||
// Copy all remaining files
|
|
||||||
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
|
impl HtmlHandlebars {
|
||||||
|
|
||||||
|
fn process_items(&self,
|
||||||
|
items: &Vec<TocItem>,
|
||||||
|
book: &Book,
|
||||||
|
handlebars: &Handlebars)
|
||||||
|
-> Result<(), Box<Error>> {
|
||||||
|
|
||||||
|
for item in items.iter() {
|
||||||
|
match *item {
|
||||||
|
TocItem::Numbered(ref i) |
|
||||||
|
TocItem::Unnumbered(ref i) |
|
||||||
|
TocItem::Unlisted(ref i) => {
|
||||||
|
// FIXME chapters with path "" are interpreted as draft now,
|
||||||
|
// not rendered here, and displayed gray in the TOC. Either
|
||||||
|
// path should be instead an Option or all chapter output
|
||||||
|
// should be used from setting dest_path, which is already
|
||||||
|
// Option but currently only used for rendering a chapter as
|
||||||
|
// index.html.
|
||||||
|
if i.chapter.path.as_os_str().len() > 0 {
|
||||||
|
try!(self.process_chapter(&i.chapter, book, handlebars));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref subs) = i.sub_items {
|
||||||
|
try!(self.process_items(&subs, book, handlebars));
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_chapter(&self,
|
||||||
|
chapter: &Chapter,
|
||||||
|
book: &Book,
|
||||||
|
handlebars: &Handlebars)
|
||||||
|
-> Result<(), Box<Error>> {
|
||||||
|
|
||||||
|
let mut content = try!(chapter.read_content_using(&book.config.src));
|
||||||
|
|
||||||
|
// Parse for playpen links
|
||||||
|
if let Some(p) = book.config.get_src().join(&chapter.path).parent() {
|
||||||
|
content = helpers::playpen::render_playpen(&content, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try!(make_data(book, chapter, &content));
|
||||||
|
|
||||||
|
// Rendere the handlebars template with the data
|
||||||
|
debug!("[*]: Render template");
|
||||||
|
let rendered_content = try!(handlebars.render("page", &data));
|
||||||
|
|
||||||
|
let p = match chapter.dest_path.clone() {
|
||||||
|
Some(x) => x,
|
||||||
|
None => chapter.path.with_extension("html")
|
||||||
|
};
|
||||||
|
|
||||||
|
let rendered_path = &book.config.get_dest().join(&p);
|
||||||
|
|
||||||
|
debug!("[*]: Create file {:?}", rendered_path);
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
let mut file = try!(utils::fs::create_file(rendered_path));
|
||||||
|
info!("[*] Creating {:?} ✓", rendered_path);
|
||||||
|
|
||||||
|
try!(file.write_all(&rendered_content.into_bytes()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_data(book: &Book,
|
||||||
|
chapter: &Chapter,
|
||||||
|
content: &str)
|
||||||
|
-> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
|
||||||
|
|
||||||
debug!("[fn]: make_data");
|
debug!("[fn]: make_data");
|
||||||
|
|
||||||
let mut data = serde_json::Map::new();
|
let mut data = serde_json::Map::new();
|
||||||
|
|
||||||
|
// Book data
|
||||||
|
|
||||||
data.insert("language".to_owned(), "en".to_json());
|
data.insert("language".to_owned(), "en".to_json());
|
||||||
data.insert("title".to_owned(), book.get_title().to_json());
|
data.insert("title".to_owned(), book.config.title.to_json());
|
||||||
data.insert("description".to_owned(), book.get_description().to_json());
|
data.insert("description".to_owned(), book.config.description.to_json());
|
||||||
data.insert("favicon".to_owned(), "favicon.png".to_json());
|
|
||||||
if let Some(livereload) = book.get_livereload() {
|
// Chapter data
|
||||||
data.insert("livereload".to_owned(), livereload.to_json());
|
|
||||||
|
let mut path = if let Some(ref dest_path) = chapter.dest_path {
|
||||||
|
PathBuf::from(dest_path)
|
||||||
|
} else {
|
||||||
|
chapter.path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if book.config.is_multilang && path.as_os_str().len() > 0 {
|
||||||
|
let p = PathBuf::from(&book.config.language.code);
|
||||||
|
path = p.join(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut chapters = vec![];
|
match path.to_str() {
|
||||||
|
|
||||||
for item in book.iter() {
|
|
||||||
// Create the data to inject in the template
|
|
||||||
let mut chapter = BTreeMap::new();
|
|
||||||
|
|
||||||
match *item {
|
|
||||||
BookItem::Affix(ref ch) => {
|
|
||||||
chapter.insert("name".to_owned(), ch.name.to_json());
|
|
||||||
match ch.path.to_str() {
|
|
||||||
Some(p) => {
|
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());
|
data.insert("chapters".to_owned(), chapters.to_json());
|
||||||
|
|
||||||
debug!("[*]: JSON constructed");
|
debug!("[*]: JSON constructed");
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn items_to_chapters(items: &Vec<TocItem>, book: &Book)
|
||||||
|
-> Result<Vec<serde_json::Map<String, serde_json::Value>>, Box<Error>> {
|
||||||
|
|
||||||
|
let mut chapters = vec![];
|
||||||
|
|
||||||
|
for item in items.iter() {
|
||||||
|
|
||||||
|
match *item {
|
||||||
|
TocItem::Numbered(ref i) |
|
||||||
|
TocItem::Unnumbered(ref i) => {
|
||||||
|
match process_chapter_and_subs(i, book) {
|
||||||
|
Ok(mut x) => chapters.append(&mut x),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TocItem::Spacer => {
|
||||||
|
let mut chapter = serde_json::Map::new();
|
||||||
|
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
|
||||||
|
chapters.push(chapter);
|
||||||
|
},
|
||||||
|
TocItem::Unlisted(_) => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_chapter_and_subs(i: &TocContent, book: &Book)
|
||||||
|
-> Result<Vec<serde_json::Map<String, serde_json::Value>>, Box<Error>> {
|
||||||
|
|
||||||
|
let mut chapters = vec![];
|
||||||
|
|
||||||
|
// Create the data to inject in the template
|
||||||
|
let mut chapter = serde_json::Map::new();
|
||||||
|
let ch = &i.chapter;
|
||||||
|
|
||||||
|
if let Some(_) = i.section {
|
||||||
|
let s = i.section_as_string();
|
||||||
|
chapter.insert("section".to_owned(), s.to_json());
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.insert("title".to_owned(), ch.title.to_json());
|
||||||
|
|
||||||
|
let mut path = if let Some(ref dest_path) = ch.dest_path {
|
||||||
|
PathBuf::from(dest_path)
|
||||||
|
} else {
|
||||||
|
ch.path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if book.config.is_multilang && path.as_os_str().len() > 0 {
|
||||||
|
let p = PathBuf::from(&book.config.language.code);
|
||||||
|
path = p.join(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
match path.to_str() {
|
||||||
|
Some(p) => {
|
||||||
|
chapter.insert("path".to_owned(), p.to_json());
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return Err(Box::new(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Could not convert path to str")
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.push(chapter);
|
||||||
|
|
||||||
|
if let Some(ref subs) = i.sub_items {
|
||||||
|
let mut sub_chs = try!(items_to_chapters(&subs, book));
|
||||||
|
chapters.append(&mut sub_chs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(chapters)
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||||
let mut previous_chapter = BTreeMap::new();
|
let mut previous_chapter = BTreeMap::new();
|
||||||
|
|
||||||
// Chapter title
|
// Chapter title
|
||||||
match previous.get("name") {
|
match previous.get("title") {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
debug!("[*]: Inserting title: {}", n);
|
debug!("[*]: Inserting title: {}", n);
|
||||||
previous_chapter.insert("title".to_owned(), n.to_json())
|
previous_chapter.insert("title".to_owned(), n.to_json())
|
||||||
|
@ -105,9 +105,6 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
debug!("[fn]: next (handlebars helper)");
|
debug!("[fn]: next (handlebars helper)");
|
||||||
|
|
||||||
|
@ -151,7 +148,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||||
let mut next_chapter = BTreeMap::new();
|
let mut next_chapter = BTreeMap::new();
|
||||||
|
|
||||||
match item.get("name") {
|
match item.get("title") {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
debug!("[*]: Inserting title: {}", n);
|
debug!("[*]: Inserting title: {}", n);
|
||||||
next_chapter.insert("title".to_owned(), n.to_json());
|
next_chapter.insert("title".to_owned(), n.to_json());
|
||||||
|
|
|
@ -97,11 +97,11 @@ impl HelperDef for RenderToc {
|
||||||
try!(rc.writer.write("</strong> ".as_bytes()));
|
try!(rc.writer.write("</strong> ".as_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = item.get("name") {
|
if let Some(title) = item.get("title") {
|
||||||
// Render only inline code blocks
|
// Render only inline code blocks
|
||||||
|
|
||||||
// filter all events that are not inline code blocks
|
// filter all events that are not inline code blocks
|
||||||
let parser = Parser::new(&name).filter(|event| {
|
let parser = Parser::new(&title).filter(|event| {
|
||||||
match event {
|
match event {
|
||||||
&Event::Start(Tag::Code) |
|
&Event::Start(Tag::Code) |
|
||||||
&Event::End(Tag::Code) => true,
|
&Event::End(Tag::Code) => true,
|
||||||
|
@ -112,7 +112,7 @@ impl HelperDef for RenderToc {
|
||||||
});
|
});
|
||||||
|
|
||||||
// render markdown to html
|
// render markdown to html
|
||||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
let mut markdown_parsed_name = String::with_capacity(title.len() * 3 / 2);
|
||||||
html::push_html(&mut markdown_parsed_name, parser);
|
html::push_html(&mut markdown_parsed_name, parser);
|
||||||
|
|
||||||
// write to the handlebars template
|
// write to the handlebars template
|
||||||
|
|
|
@ -2,8 +2,23 @@ pub use self::html_handlebars::HtmlHandlebars;
|
||||||
|
|
||||||
mod html_handlebars;
|
mod html_handlebars;
|
||||||
|
|
||||||
|
use book::MDBook;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
|
||||||
|
/// Responsible for creating an `MDBook` struct from path, preparing the
|
||||||
|
/// project and calling `render()`, doing what is necessary for the
|
||||||
|
/// particular output format.
|
||||||
|
///
|
||||||
|
/// This involves parsing config options from `book.toml` and parsing the
|
||||||
|
/// `SUMMARY.md` of each translation to a nested `Vec<TocItem>`.
|
||||||
|
///
|
||||||
|
/// Finally it calls `render()` to process the chapters and static assets.
|
||||||
|
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>>;
|
||||||
|
|
||||||
|
/// Responsible for rendering the chapters and copying static assets.
|
||||||
|
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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+ */;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Labyrinths"
|
||||||
|
subtitle = "Selected Stories and Other Writings"
|
||||||
|
author = "Jorge Luis Borges"
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
# Chronology
|
|
@ -0,0 +1 @@
|
||||||
|
# Essays
|
|
@ -0,0 +1 @@
|
||||||
|
# Kafka
|
|
@ -0,0 +1 @@
|
||||||
|
# Fictions
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
||||||
|
+++
|
||||||
|
title = "The Circular Ruins"
|
||||||
|
+++
|
||||||
|
|
||||||
|
# Ruins
|
|
@ -0,0 +1 @@
|
||||||
|
# Introduction
|
|
@ -0,0 +1,3 @@
|
||||||
|
title = "Labyrinths"
|
||||||
|
subtitle = "Selected Stories and Other Writings"
|
||||||
|
author = "Jorge Luis Borges"
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
||||||
|
# Chronology
|
|
@ -0,0 +1 @@
|
||||||
|
# Essays
|
|
@ -0,0 +1 @@
|
||||||
|
# Kafka
|
|
@ -0,0 +1 @@
|
||||||
|
# Fictions
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
||||||
|
+++
|
||||||
|
title = "The Circular Ruins"
|
||||||
|
+++
|
||||||
|
|
||||||
|
# Ruins
|
|
@ -0,0 +1 @@
|
||||||
|
# Introduction
|
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 140 KiB |
After Width: | Height: | Size: 343 KiB |
After Width: | Height: | Size: 162 KiB |
|
@ -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" }
|
|
@ -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]()
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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!
|
|
@ -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.
|
|
@ -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]()
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# La course cocasse
|
||||||
|
|
||||||
|
![Cocasse](images/Tail.png)
|
||||||
|
|
||||||
|
Ils formaient une assemblée bien grotesque ces êtres singuliers réunis sur le
|
||||||
|
bord de la mare ; les uns avaient leurs plumes tout en désordre, les autres le
|
||||||
|
poil plaqué contre le corps. Tous étaient trempés, de mauvaise humeur, et fort
|
||||||
|
mal à l’aise.
|
||||||
|
|
||||||
|
« Comment faire pour nous sécher ? » ce fut la première question, cela va sans
|
||||||
|
dire. Au bout de quelques instants, il sembla tout naturel à Alice de causer
|
||||||
|
familièrement avec ces animaux, comme si elle les connaissait depuis son
|
||||||
|
berceau. Elle eut même une longue discussion avec le Lory, qui, à la fin, lui
|
||||||
|
fit la mine et lui dit d’un air boudeur : « Je suis plus âgé que vous, et je
|
||||||
|
dois par conséquent en savoir plus long. » Alice ne voulut pas accepter cette
|
||||||
|
conclusion avant de savoir l’âge du Lory, et comme celui-ci refusa tout net de
|
||||||
|
le lui dire, cela mit un terme au débat.
|
|
@ -0,0 +1,18 @@
|
||||||
|
# La mare aux larmes
|
||||||
|
|
||||||
|
![Larmes](images/Tears.png)
|
||||||
|
|
||||||
|
« De plus très-curieux en plus très-curieux ! » s’écria Alice (sa surprise était
|
||||||
|
si grande qu’elle ne pouvait s’exprimer correctement) : « Voilà que je m’allonge
|
||||||
|
comme le plus grand télescope qui fût jamais ! Adieu mes pieds ! » (Elle venait
|
||||||
|
de baisser les yeux, et ses pieds lui semblaient s’éloigner à perte de vue.) «
|
||||||
|
Oh ! mes pauvres petits pieds ! Qui vous mettra vos bas et vos souliers
|
||||||
|
maintenant, mes mignons ? Quant à moi, je ne le pourrai certainement pas ! Je
|
||||||
|
serai bien trop loin pour m’occuper de vous : arrangez-vous du mieux que vous
|
||||||
|
pourrez. — Il faut cependant que je sois bonne pour eux, » pensa Alice, « sans
|
||||||
|
cela ils refuseront peut-être d’aller du côté que je voudrai. Ah ! je sais ce
|
||||||
|
que je ferai : je leur donnerai une belle paire de bottines à Noël. »
|
||||||
|
|
||||||
|
Puis elle chercha dans son esprit comment elle s’y prendrait. « Il faudra les
|
||||||
|
envoyer par le messager, » pensa-t-elle ; « quelle étrange chose d’envoyer des
|
||||||
|
présents à ses pieds ! Et l’adresse donc ! C’est cela qui sera drôle.
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Au fond du terrier
|
||||||
|
|
||||||
|
![Terrier](images/Rabbit.png)
|
||||||
|
|
||||||
|
Alice, assise auprès de sa sœur sur le gazon, commençait à s’ennuyer de rester
|
||||||
|
là à ne rien faire ; une ou deux fois elle avait jeté les yeux sur le livre que
|
||||||
|
lisait sa sœur ; mais quoi ! pas d’images, pas de dialogues ! « La belle avance,
|
||||||
|
» pensait Alice, « qu’un livre sans images, sans causeries ! »
|
||||||
|
|
||||||
|
Elle s’était mise à réfléchir, (tant bien que mal, car la chaleur du jour
|
||||||
|
l’endormait et la rendait lourde,) se demandant si le plaisir de faire une
|
||||||
|
couronne de marguerites valait bien la peine de se lever et de cueillir les
|
||||||
|
fleurs, quand tout à coup un lapin blanc aux yeux roses passa près d’elle.
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Alice au pays des merveilles
|
||||||
|
|
||||||
|
![Queen of Hearts](images/Queen.jpg)
|
||||||
|
|
||||||
|
[L’Auteur désire exprimer ici sa reconnaissance envers le Traducteur de ce qu’il
|
||||||
|
a remplacé par des parodies de sa composition quelques parodies de morceaux de
|
||||||
|
poésie anglais, qui n’avaient de valeur que pour des enfants anglais ; et aussi,
|
||||||
|
de ce qu’il a su donner en jeux de mots français les équivalents des jeux de
|
||||||
|
mots anglais, dont la traduction n’était pas possible.]
|
||||||
|
|
||||||
|
Notre barque glisse sur l’onde
|
||||||
|
Que dorent de brûlants rayons ;
|
||||||
|
Sa marche lente et vagabonde
|
||||||
|
Témoigne que des bras mignons,
|
||||||
|
Pleins d’ardeur, mais encore novices,
|
||||||
|
Tout fiers de ce nouveau travail,
|
||||||
|
Mènent au gré de leurs caprices
|
||||||
|
Les rames et le gouvernail.
|
|
@ -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]()
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|