mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 20:03:16 +00:00
Compare commits
329 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce50a1d50f | |||
| a76e018d36 | |||
| b06f1fd677 | |||
| 3d8b1880dc | |||
| 1a13c8c9bb | |||
| 852fae3a09 | |||
| d342d31c8e | |||
| 5b3f54fbc8 | |||
| 308ece784c | |||
| 0758ddf04e | |||
| f24d9aef73 | |||
| f8bb55f258 | |||
| 76daebeb84 | |||
| b0d366b7e8 | |||
| 3d0054dd64 | |||
| 13bc89ff44 | |||
| bb910f984e | |||
| e576fdea14 | |||
| c7864927ec | |||
| 54956de59f | |||
| 2cbd30d1f2 | |||
| 55614f1675 | |||
| 4f7421993f | |||
| 32ad7b530e | |||
| ee5d5010f6 | |||
| cf7e5884cd | |||
| 11a0a64d9e | |||
| b6051babac | |||
| 34947b5e20 | |||
| c621380b66 | |||
| c4b54033ce | |||
| 4d85d6eb6e | |||
| cc9274c9af | |||
| bbdf7efc34 | |||
| 38a986995a | |||
| ee8d35df7a | |||
| be411cd5ff | |||
| 2104019171 | |||
| 4e564707df | |||
| da8611e0c3 | |||
| 5d11641a5f | |||
| 24d38e4fcf | |||
| efb65a8413 | |||
| 3fc90554f5 | |||
| 4ebe28faae | |||
| 693171cb05 | |||
| 42a2d090aa | |||
| c84eaa83dd | |||
| 74c3ff80cb | |||
| 5893d3ee64 | |||
| 7f435c56a0 | |||
| 59b7100cf6 | |||
| 0923d1aa24 | |||
| ffe6c36f2f | |||
| 8c4752c5d8 | |||
| d6202e522b | |||
| 50a24c62c5 | |||
| 6b17d08892 | |||
| 79c91fd8c5 | |||
| 95764258ab | |||
| 41e6872557 | |||
| 30c96c37c2 | |||
| 20f1cf3dff | |||
| 8de0d6186e | |||
| edd7d8aefe | |||
| a8da4903f4 | |||
| c8df5033f7 | |||
| 412f418da3 | |||
| 3782d21069 | |||
| 82a29f4470 | |||
| 4a65326b24 | |||
| d75d8729be | |||
| c87060454e | |||
| bbba52c7ff | |||
| 33418bdb33 | |||
| de47db2b66 | |||
| fa6fcffbe1 | |||
| 93d3f11891 | |||
| 59479f10fc | |||
| 11e72253d4 | |||
| 37fca9c88a | |||
| 6d67048e98 | |||
| c516a5c1bd | |||
| 429431c13c | |||
| b80dfd7b9a | |||
| 4c144b6ceb | |||
| 718e32773a | |||
| fb75d5d3ee | |||
| 99bbcbebfc | |||
| 289e4b5dc1 | |||
| a974dcdab6 | |||
| 9a48620152 | |||
| e444b3a402 | |||
| fabf4f0c0a | |||
| d467888d6a | |||
| eb46311561 | |||
| 63839bb0a7 | |||
| 793fada0fb | |||
| 494ba78156 | |||
| 9816b27ebf | |||
| 1c43b7891b | |||
| 9d7739213e | |||
| 2ab5f6a305 | |||
| c73103969f | |||
| 6cbbdf5d5b | |||
| 3a48435497 | |||
| 084925de8b | |||
| 5bac2fe695 | |||
| 2c29fb241f | |||
| 77dc11bcc2 | |||
| 5154622566 | |||
| c681cd286a | |||
| c1d53ec11f | |||
| 36b49c66c6 | |||
| 80b8e340da | |||
| b51470db39 | |||
| a5f4596f8d | |||
| 5f3c3760c0 | |||
| 8b05885d57 | |||
| d9d7ab4ee5 | |||
| 30bce5f9bb | |||
| fbe0282738 | |||
| 5bad08b9a0 | |||
| 310691bca2 | |||
| 8f7967acd0 | |||
| faf18476b1 | |||
| 9acccad23a | |||
| db1a0aedf8 | |||
| 01a26fda25 | |||
| 0229272b0a | |||
| c817acff09 | |||
| a5eec81819 | |||
| 98f70879bd | |||
| ceb4627f4e | |||
| 8d9d4fe23f | |||
| 730298110c | |||
| 8578f7d0d1 | |||
| 07a5daefb0 | |||
| 0ed524bce5 | |||
| 94199af7b2 | |||
| b8d7a00f58 | |||
| aca963befc | |||
| 3ea4e334bf | |||
| 9955fa8c29 | |||
| fb37a6bcab | |||
| 4f8bdbb518 | |||
| 73c190891d | |||
| 93e701f471 | |||
| 68b9bf7e38 | |||
| 826f90505c | |||
| 7ae36f16c8 | |||
| 281f6d5535 | |||
| b4072a06c4 | |||
| cb3ee423c9 | |||
| 1360a744a7 | |||
| f3058598dc | |||
| 94db0f6ec9 | |||
| f08732095d | |||
| f946650ddb | |||
| 14fd6bbdce | |||
| 5059e8df34 | |||
| 3d405a1ac5 | |||
| 78e00db018 | |||
| e62bc31e63 | |||
| 4763f1342c | |||
| 9413e53b00 | |||
| 35d352bbeb | |||
| 8dd5e0c250 | |||
| 139e487998 | |||
| 9bb0a3fbd3 | |||
| 9a1a6f2625 | |||
| e781d6d2b1 | |||
| 5b342b50ce | |||
| 869822d180 | |||
| e50422bce5 | |||
| 999882e723 | |||
| d68dc82159 | |||
| cc9fdd0032 | |||
| 503cc4a729 | |||
| d585269228 | |||
| 50f535b910 | |||
| 978bdd149e | |||
| a9d558e891 | |||
| 8291eb58df | |||
| 7298dec353 | |||
| 7574b01863 | |||
| 98e739a12b | |||
| 262ce03e46 | |||
| 1906691a35 | |||
| 4ca23b84a4 | |||
| b05eb8440b | |||
| f4de7748f5 | |||
| 93c26a839b | |||
| 878e7441f2 | |||
| 27d387e3e1 | |||
| 7b7e4f60bc | |||
| 8bac111acf | |||
| 7a46d3b497 | |||
| 357aa92e31 | |||
| fa17f16716 | |||
| ce99c8c706 | |||
| be828cf004 | |||
| 1057bb15d3 | |||
| ea5e8891a8 | |||
| 57d83f082a | |||
| 8a3f80ba82 | |||
| 374ebd6f7a | |||
| ab2ebefe2b | |||
| c34a9f6213 | |||
| f5df41c75f | |||
| 0c2dc06cb7 | |||
| 4552ccd7d7 | |||
| 525bcd8295 | |||
| ac2ad8d8b5 | |||
| 14ce495e9e | |||
| 4aed2cf652 | |||
| 244fb0ca28 | |||
| f6e123f1ed | |||
| e1e4e7f011 | |||
| 814b6ff489 | |||
| 3aa17ab1f4 | |||
| 4646ed8f4a | |||
| 3f5c8acb3c | |||
| 93b257314a | |||
| 19a1db8cb4 | |||
| c3275a4aa9 | |||
| 927185d95c | |||
| a3df6c6f0c | |||
| e1fa977b5b | |||
| ce95f316b9 | |||
| a162af5a75 | |||
| fb0ea4610d | |||
| 4f86a9978b | |||
| cc78b880ce | |||
| 4336fdb241 | |||
| 87db7cf1d4 | |||
| 09ff802c7e | |||
| 71b59b1950 | |||
| a3300ad9c6 | |||
| 8f40d4affa | |||
| 560f3fcccb | |||
| ecaadf63c5 | |||
| d490a6e0b4 | |||
| 53ced205b8 | |||
| 61c4983949 | |||
| fb862f466f | |||
| 8d51300386 | |||
| c4b413563d | |||
| 8f852971f9 | |||
| 3c54c89464 | |||
| d07aa13822 | |||
| c218f7d0e3 | |||
| 72ad872e90 | |||
| 854f244d22 | |||
| 9acbb223db | |||
| c59fc687db | |||
| 0791b917f1 | |||
| ba182ce8e4 | |||
| 663291c515 | |||
| 99253eecea | |||
| 916a02cb36 | |||
| 114c4abcd1 | |||
| e1e3d11932 | |||
| df46217c78 | |||
| d18cc92396 | |||
| 8f63b4b790 | |||
| ce7de93936 | |||
| ef2a24fe6f | |||
| 038cd0155b | |||
| fb430ac1ed | |||
| 53cfa3703b | |||
| 7ed9e8d145 | |||
| 93e0b0cbea | |||
| 0118bd3168 | |||
| 07d5245a5f | |||
| 6b844213ec | |||
| dabdeb8a80 | |||
| ecd56315e6 | |||
| 15b9e839e9 | |||
| d7f7dcd47b | |||
| b1981e09b8 | |||
| c9edd0a795 | |||
| d83e0716bb | |||
| aed38afff3 | |||
| 1df522e6ec | |||
| bd4ef2354e | |||
| 32a49a6969 | |||
| 21806687e6 | |||
| 7ade91a091 | |||
| 4e41f96d89 | |||
| 325c32b4c9 | |||
| 7d6606a8e5 | |||
| c887d520eb | |||
| e6050fc558 | |||
| cae4853334 | |||
| 9ef2bebfe9 | |||
| 53294af810 | |||
| 7d21c815a5 | |||
| e07719a86c | |||
| fa5bee6ca3 | |||
| 3a43ef2c3f | |||
| 55deab03cd | |||
| edda84fefd | |||
| 10b191f631 | |||
| d5a575c5e0 | |||
| 82d8c3e2d9 | |||
| bc0c8dbcf3 | |||
| de0245ff40 | |||
| 7f56ff0281 | |||
| 2cfbf5411c | |||
| f70da1c82a | |||
| 0b4db49f57 | |||
| fbffd54aa2 | |||
| 4d19a33af2 | |||
| 9d67a0e656 | |||
| 093005bfd9 | |||
| 514883f0bd | |||
| 27681aad98 | |||
| 0d367da776 | |||
| c499a8a13a | |||
| 64a6de751d | |||
| d39f0d6ff0 | |||
| bdbfbf7300 | |||
| ccac3d2c3c | |||
| f07278490f | |||
| f1880fc53c | |||
| e8fe4a2dea | |||
| 1d79507665 | |||
| f05dc67887 |
+3
-1
@@ -1,2 +1,4 @@
|
||||
docs/**/*
|
||||
test/node_modules/**/*
|
||||
_book/**/*
|
||||
node_modules/**/*
|
||||
test/**/*
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"jest": true,
|
||||
"jasmine": true
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
}
|
||||
@@ -3,7 +3,6 @@ language: node_js
|
||||
node_js:
|
||||
- "stable"
|
||||
- "4.1"
|
||||
- "0.12"
|
||||
before_install:
|
||||
- npm install svgexport -g
|
||||
after_success:
|
||||
|
||||
+4
-3
@@ -7,9 +7,10 @@ Names below are ordered by first contribution.
|
||||
Author
|
||||
------
|
||||
|
||||
- Samy Pessé <samypesse@gmail.com> (@SamyPesse)
|
||||
- Aaron O'Mullan <aaron.omullan@gmail.com> (@AaronO)
|
||||
|
||||
- Samy Pessé <samy@gitbook.com> (@SamyPesse)
|
||||
- Aaron O'Mullan <aaron@gitbook.com> (@AaronO)
|
||||
- Johan Preynat <johan@gitbook.com> (@jpreynat)
|
||||
- Nicolas Gaborit <nicolas@gitbook.com> (@soreine)
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
@@ -15,6 +15,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Multi-lingual books share assets folder
|
||||
- YAML front matter is parsed and can extend page's properties
|
||||
- Fix `uk` translation
|
||||
- Fix heading ID including dashes
|
||||
- Fix error in code highlighting for unknown languages
|
||||
- Fix data-uri images being handled as external images
|
||||
- Accept SSH url as plugin version
|
||||
- Add templating blocks `markdown`, `asciidoc` and `markup`
|
||||
- Better search experience
|
||||
- Better default theme, more responsive and cleaner
|
||||
|
||||
## 2.6.7
|
||||
- Fix bug with filenames including spaces
|
||||
|
||||
@@ -12,7 +12,7 @@ You can publish and host books easily online using [gitbook.com](https://www.git
|
||||
|
||||
Check out the [GitBook Community Slack Channel](https://slack.gitbook.com), Stay updated by following [@GitBookIO](https://twitter.com/GitBookIO) on Twitter or [GitBook](https://www.facebook.com/gitbookcom) on Facebook.
|
||||
|
||||
Complete documentation is available at [help.gitbook.com](http://help.gitbook.com/).
|
||||
Complete documentation is available at [toolchain.gitbook.com](http://toolchain.gitbook.com/).
|
||||
|
||||

|
||||
|
||||
@@ -32,14 +32,14 @@ We're always happy to help out with your books or any other questions you might
|
||||
|
||||
## Features
|
||||
|
||||
* [Output as a website or ebook (pdf, epub, mobi)](http://help.gitbook.com/format/output.html)
|
||||
* [Multi-Languages](http://help.gitbook.com/format/languages.html)
|
||||
* [Glossary](http://help.gitbook.com/format/glossary.html)
|
||||
* [Cover](http://help.gitbook.com/format/cover.html)
|
||||
* [AsciiDoc Support](http://help.gitbook.com/format/asciidoc.html)
|
||||
* [Variables and Templating](http://help.gitbook.com/format/templating.html)
|
||||
* [Content References](http://help.gitbook.com/format/conrefs.html)
|
||||
* [Plugins](http://help.gitbook.com/format/plugins.html)
|
||||
* Write using [Markdown](http://toolchain.gitbook.com/syntax/markdown.html) or [AsciiDoc](http://toolchain.gitbook.com/syntax/asciidoc.html)
|
||||
* Output as a website or [ebook (pdf, epub, mobi)](http://toolchain.gitbook.com/ebook.html)
|
||||
* [Multi-Languages](http://toolchain.gitbook.com/languages.html)
|
||||
* [Lexicon / Glossary](http://toolchain.gitbook.com/lexicon.html)
|
||||
* [Cover](http://toolchain.gitbook.com/ebook.html)
|
||||
* [Variables and Templating](http://toolchain.gitbook.com/templating/)
|
||||
* [Content References](http://toolchain.gitbook.com/templating/conrefs.html)
|
||||
* [Plugins](http://toolchain.gitbook.com/plugins/)
|
||||
* [Beautiful default theme](https://github.com/GitbookIO/theme-default)
|
||||
|
||||
## Publish your book
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ init:
|
||||
# Test against these versions of Node.js.
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "0.12"
|
||||
- nodejs_version: "4.1"
|
||||
- nodejs_version: "5"
|
||||
- nodejs_version: "4"
|
||||
|
||||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
|
||||
@@ -3,14 +3,13 @@ var pkg = require('./package.json');
|
||||
module.exports = {
|
||||
// Documentation for GitBook is stored under "docs"
|
||||
root: './docs',
|
||||
title: 'GitBook Documentation',
|
||||
title: 'GitBook Toolchain Documentation',
|
||||
|
||||
// Enforce use of GitBook v3
|
||||
gitbook: pkg.version,
|
||||
gitbook: '>=3.0.0-pre.0',
|
||||
|
||||
// Use the "official" theme
|
||||
plugins: ['theme-official', 'sitemap'],
|
||||
theme: 'official',
|
||||
|
||||
variables: {
|
||||
version: pkg.version
|
||||
@@ -18,7 +17,7 @@ module.exports = {
|
||||
|
||||
pluginsConfig: {
|
||||
sitemap: {
|
||||
hostname: 'https://docs.gitbook.com'
|
||||
hostname: 'https://toolchain.gitbook.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# GitBook Format Documentation
|
||||
# GitBook Toolchain Documentation
|
||||
|
||||
This document aims to be a comprehensive guide to GitBook. It contains the full documentation for version **{{ book.version }}**. Help for GitBook.com specific questions can be found at [help.gitbook.com](https://help.gitbook.com).
|
||||
|
||||
|
||||
+18
-2
@@ -12,16 +12,32 @@
|
||||
* [Configuration](config.md)
|
||||
* [Glossary](lexicon.md)
|
||||
* [Multi-Lingual](languages.md)
|
||||
* [AsciiDoc](asciidoc.md)
|
||||
* [Markdown](syntax/markdown.md)
|
||||
* [Headings](syntax/markdown.md#headings)
|
||||
* [Paragraphs](syntax/markdown.md#paragraphs)
|
||||
* [Lists](syntax/markdown.md#lists)
|
||||
* [Links](syntax/markdown.md#links)
|
||||
* [Images](syntax/markdown.md#images)
|
||||
* [Blockquotes](syntax/markdown.md#blockquotes)
|
||||
* [Tables](syntax/markdown.md#tables)
|
||||
* [Code](syntax/markdown.md#code)
|
||||
* [HTML](syntax/markdown.md#html)
|
||||
* [Footnotes](syntax/markdown.md#footnotes)
|
||||
* [AsciiDoc](syntax/asciidoc.md)
|
||||
* [eBook and PDF](ebook.md)
|
||||
|
||||
### Customization
|
||||
|
||||
* [Cover](cover.md)
|
||||
* [Templating](templating/README.md)
|
||||
* [Content References](templating/conrefs.md)
|
||||
* [Variables](templating/variables.md)
|
||||
* [Builtin](templating/builtin.md)
|
||||
* [Plugins](plugins/README.md)
|
||||
* [Create a plugin](plugins/create.md)
|
||||
* [Hooks](plugins/hooks.md)
|
||||
* [Blocks](plugins/blocks.md)
|
||||
* [Filters](plugins/filters.md)
|
||||
* [API & Context](plugins/api.md)
|
||||
* [Test your plugin](plugins/testing.md)
|
||||
* [Theming](themes/README.md)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends template.theme %}
|
||||
{% extends template.self %}
|
||||
|
||||
{% block header_nav %}
|
||||
<a href="https://github.com/GitbookIO/gitbook/blob/master/docs/{{ file.path }}" target="_blank" class="btn btn-link pull-right hidden-xs">
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Cover
|
||||
|
||||
Covers are used for all the ebook formats, and to make books more elegant on GitBook.com.
|
||||
|
||||
To provide a cover, place a **cover.jpg** file at the root directory of your book. Adding a **cover_small.jpg** will specify a smaller version of the cover. The cover should be a **JPEG** file.
|
||||
|
||||
### Best Sizes
|
||||
|
||||
| | Big | Small |
|
||||
|:-------------------:|:-----------:|:-----------------:|
|
||||
| **File** | `cover.jpg` | `cover_small.jpg` |
|
||||
| **Size(in pixels)** | 1800x2360 | 200x262 |
|
||||
|
||||
### Guidelines
|
||||
|
||||
A good cover respects the following guidelines:
|
||||
|
||||
* No border
|
||||
* Clearly visible book title
|
||||
* Any important text should be visible in the small version
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generating eBooks and PDFs
|
||||
|
||||
GitBook can generates a website, but can also output content as ebook (ePub, Mobi, PDF).
|
||||
|
||||
```
|
||||
# Generate a PDF file
|
||||
$ gitbook pdf ./ ./mybook.pdf
|
||||
|
||||
# Generate an ePub file
|
||||
$ gitbook epub ./ ./mybook.epub
|
||||
|
||||
# Generate a Mobi file
|
||||
$ gitbook mobi ./ ./mybook.mobi
|
||||
```
|
||||
|
||||
### Installing ebook-convert
|
||||
|
||||
`ebook-convert` is required to generate ebooks (epub, mobi, pdf).
|
||||
|
||||
##### OS X
|
||||
|
||||
Download the [Calibre application](https://calibre-ebook.com/download). After moving the `calibre.app` to your Applications folder create a symbolic link to the ebook-convert tool:
|
||||
|
||||
```
|
||||
$ sudo ln -s ~/Applications/calibre.app/Contents/MacOS/ebook-convert /usr/bin
|
||||
```
|
||||
|
||||
You can replace `/usr/bin` with any directory that is in your $PATH.
|
||||
|
||||
### Cover
|
||||
|
||||
Covers are used for all the ebook formats. It's an important part of an ebook brandline.
|
||||
|
||||
A good cover should respect the following guidelines:
|
||||
|
||||
* Size of 1800x2360 (pixels)
|
||||
* No border
|
||||
* Clearly visible book title
|
||||
* Any important text should be visible in the small version
|
||||
|
||||
+22
@@ -1,5 +1,9 @@
|
||||
# GitBook FAQ
|
||||
|
||||
This page gathers common questions and answers concerning the GitBook format and toolchain.
|
||||
|
||||
Questions about GitBook.com and the Editor are gather into the [help.gitbook.com's FAQ](http://help.gitbook.com/faq.html).
|
||||
|
||||
#### How can I host/publish my book?
|
||||
|
||||
Books can easily be published and hosted on [GitBook.com](https://www.gitbook.com). But GitBook output can be hosted on any static file hosting solution.
|
||||
@@ -10,6 +14,20 @@ Any text editor should work! But we advise using the [GitBook Editor](https://ww
|
||||
|
||||
---
|
||||
|
||||
#### Does GitBook supports RTL/bi-directional text ?
|
||||
|
||||
The GitBook format supports right to left, and bi-directional writing. To enable it, you either need to specify a language (ex: `ar`), or force GitBook to use RTL in your `book.json`:
|
||||
|
||||
``` json
|
||||
{
|
||||
"language": "ar",
|
||||
"direction": "rtl"
|
||||
}
|
||||
```
|
||||
|
||||
With version 3.0 of GitBook, it's automatically detected according to the content.
|
||||
_Note that, while the output book will indeed respect RTL, the Editor doesn't support RTL writing yet_.
|
||||
|
||||
#### Should I use an `.html` or `.md` extensions in my links?
|
||||
|
||||
You should always use paths and the `.md` extensions when linking to your files, GitBook will automatically replace these paths by the appropriate link when the pointing file is referenced in the Table of Contents.
|
||||
@@ -18,6 +36,10 @@ You should always use paths and the `.md` extensions when linking to your files,
|
||||
|
||||
Yes, GitBooks can be created in [sub-directories](structure.md#subdirectory). GitBook.com and the CLI also looks by default in a serie of [folders](structure.md).
|
||||
|
||||
#### Does GitBook supports RTL languages?
|
||||
|
||||
Yes, GitBook automatically detect the direction in your pages (`rtl` or `ltr`) and adjust the layout accordingly. The direction can also be specified globally in the [book.json](config.md).
|
||||
|
||||
---
|
||||
|
||||
#### Does GitBook support Math equations?
|
||||
|
||||
+26
-2
@@ -23,7 +23,27 @@ Adding a nested list to a parent chapter will create subchapters.
|
||||
|
||||
Each chapter has a dedicated page (`part#/README.md`) and is split into subchapters.
|
||||
|
||||
##### Example with subchapters split into parts
|
||||
##### Anchors
|
||||
|
||||
Chapters in the Table of Contents can be pointing to specific part of a file using anchor.
|
||||
|
||||
```markdown
|
||||
# Summary
|
||||
|
||||
### Part I
|
||||
|
||||
* [Part I](part1/README.md)
|
||||
* [Writing is nice](part1/README.md#writing)
|
||||
* [GitBook is nice](part1/README.md#gitbook)
|
||||
* [Part II](part2/README.md)
|
||||
* [We love feedback](part2/README.md#feedback)
|
||||
* [Better tools for authors](part2/README.md#tools)
|
||||
```
|
||||
|
||||
|
||||
##### Parts
|
||||
|
||||
The Table of Contents can be divided into parts separated by headings or horizontal lines:
|
||||
|
||||
```markdown
|
||||
# Summary
|
||||
@@ -37,9 +57,13 @@ Each chapter has a dedicated page (`part#/README.md`) and is split into subchapt
|
||||
|
||||
* [We love feedback](part2/feedback_please.md)
|
||||
* [Better tools for authors](part2/better_tools.md)
|
||||
|
||||
----
|
||||
|
||||
* [Last part without title](part3/title.md)
|
||||
```
|
||||
|
||||
Here, parts are just groups of chapters and do not have dedicated pages, but will show in the navigation for example.
|
||||
Parts are just groups of chapters and do not have dedicated pages, but according to the theme, it will show in the navigation.
|
||||
|
||||
### Pages
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Context and APIs
|
||||
|
||||
GitBooks provides different APIs and contexts to plugins. These APIs can vary according to the GitBook version being used, your plugin should specify the `engines.gitbook` field in `package.json` accordingly.
|
||||
|
||||
#### Book instance
|
||||
|
||||
The `Book` class is the central point of GitBook, it centralize all access read methods. This class is defined in [book.js](https://github.com/GitbookIO/gitbook/blob/master/lib/book.js).
|
||||
|
||||
```js
|
||||
// Read configuration from book.json
|
||||
var value = book.config.get('title', 'Default Value');
|
||||
|
||||
// Resolve a filename to an absolute path
|
||||
var filepath = book.resolve('README.md');
|
||||
|
||||
// Render an inline markup string
|
||||
book.renderInline('markdown', 'This is **Markdown**')
|
||||
.then(function(str) { ... })
|
||||
|
||||
// Render a markup string (block mode)
|
||||
book.renderBlock('markdown', '* This is **Markdown**')
|
||||
.then(function(str) { ... })
|
||||
```
|
||||
|
||||
#### Output instance
|
||||
|
||||
The `Output` class represent the output/write process.
|
||||
|
||||
```js
|
||||
// Return root folder for the output
|
||||
var root = output.root();
|
||||
|
||||
// Resolve a file in the output folder
|
||||
var filepath = output.resolve('myimage.png');
|
||||
|
||||
// Convert a filename to an URL (returns a path to an html file)
|
||||
var fileurl = output.toURL('mychapter/README.md');
|
||||
|
||||
// Write a file in the output folder
|
||||
output.write('hello.txt', 'Hello World')
|
||||
.then(function() { ... });
|
||||
|
||||
// Copy a file to the output folder
|
||||
output.copyFile('./myfile.jpg', 'cover.jpg')
|
||||
.then(function() { ... });
|
||||
|
||||
// Verify that a file exists
|
||||
output.hasFile('hello.txt')
|
||||
.then(function(exists) { ... });
|
||||
```
|
||||
|
||||
#### Page instance
|
||||
|
||||
A page instance represent the current parsed page.
|
||||
|
||||
```js
|
||||
// Title of the page (from SUMMARY)
|
||||
page.title
|
||||
|
||||
// Content of the page (Markdown/Asciidoc/HTML according to the stage)
|
||||
page.content
|
||||
|
||||
// Relative path in the book
|
||||
page.path
|
||||
|
||||
// Absolute path to the file
|
||||
page.rawPath
|
||||
|
||||
// Type of parser used for this file
|
||||
page.type ('markdown' or 'asciidoc')
|
||||
```
|
||||
|
||||
#### Context for Blocks and Filters
|
||||
|
||||
Blocks and filters have access to the same context, this context is bind to the template engine execution:
|
||||
|
||||
```js
|
||||
{
|
||||
// Current templating syntax
|
||||
"ctx": {
|
||||
// For example, after a {% set message = "hello" %}
|
||||
"message": "hello"
|
||||
},
|
||||
|
||||
// Book instance
|
||||
"book" <Book>,
|
||||
|
||||
// Output instance
|
||||
"output": <Output>
|
||||
}
|
||||
```
|
||||
|
||||
For example a filter or block function can access the current book using: `this.book`.
|
||||
|
||||
#### Context for Hooks
|
||||
|
||||
Hooks only have access to the `<Book>` instance using `this.book`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Extend Blocks
|
||||
|
||||
Extending templating blocks is the best way to provide extra functionalities to authors.
|
||||
|
||||
The most common usage is to process the content within some tags at runtime. It's like [filters](./filters.md), but on steroids because you aren't confined to a single expression.
|
||||
|
||||
### Defining a new block
|
||||
|
||||
Blocks are defined by the plugin, blocks is a map of name associated with a block descriptor. The block descriptor needs to contain at least a `process` method.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
blocks: {
|
||||
tag1: {
|
||||
process: function(block) {
|
||||
return "Hello "+block.body+", How are you?";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The `process` should return the html content that will replace the tag. Refer to [Context and APIs](./api.md) to learn more about `this` and GitBook API.
|
||||
|
||||
### Handling block arguments
|
||||
|
||||
Arguments can be passed to blocks:
|
||||
|
||||
```
|
||||
{% tag1 "argument 1", "argument 2", name="Test" %}
|
||||
This is the body of the block.
|
||||
{% endtag1 %}
|
||||
```
|
||||
|
||||
And arguments are easily accessible in the `process` method:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
blocks: {
|
||||
tag1: {
|
||||
process: function(block) {
|
||||
// block.args equals ["argument 1", "argument 2"]
|
||||
// block.kwargs equals { "name": "Test" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Handling sub-blocks
|
||||
|
||||
A defined block can be parsed into different sub-blocks, for example let's consider the source:
|
||||
|
||||
```
|
||||
{% myTag %}
|
||||
Main body
|
||||
{% subblock1 %}
|
||||
Body of sub-block 1
|
||||
{% subblock 2 %}
|
||||
Body of sub-block 1
|
||||
{% endmyTag %}
|
||||
```
|
||||
+12
-1
@@ -8,13 +8,24 @@ A GitBook plugin is a node package published on NPM that follow a defined conven
|
||||
|
||||
The `package.json` is a manifest format for describing **Node.js modules**. GitBook plugins are built on top of Node modules. It declares dependencies, version, ownership, and other information required to run a plugin in GitBook. This document describes the schema in detail.
|
||||
|
||||
```
|
||||
A plugin manifest `package.json` can also contain details about the required configuration. The configuration schema is defined in the `gitbook` field of the `package.json` (This field follow the [JSON-Schema](http://json-schema.org) guidelines):
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "gitbook-plugin-mytest",
|
||||
"version": "0.0.1",
|
||||
"description": "This is my first GitBook plugin",
|
||||
"engines": {
|
||||
"gitbook": ">1.x.x"
|
||||
},
|
||||
"gitbook": {
|
||||
"properties": {
|
||||
"myConfigKey": {
|
||||
"type": "string",
|
||||
"default": "it's the default value",
|
||||
"description": "It defines my awesome config!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Extend Filters
|
||||
|
||||
Filters are essentially functions that can be applied to variables. They are called with a pipe operator (`|`) and can take arguments.
|
||||
|
||||
```
|
||||
{{ foo | title }}
|
||||
{{ foo | join(",") }}
|
||||
{{ foo | replace("foo", "bar") | capitalize }}
|
||||
```
|
||||
|
||||
### Defining a new filter
|
||||
|
||||
Plugins can extend filters by defining custom functions in their entry point under the `filters` scope.
|
||||
|
||||
A filter function takes as first argument the content to filter, and should return the new content.
|
||||
Refer to [Context and APIs](./api.md) to learn more about `this` and GitBook API.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
filters: {
|
||||
hello: function(name) {
|
||||
return 'Hello '+name;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The filter `hello` can then be used in the book:
|
||||
|
||||
```
|
||||
{{ "Aaron"|hello }}, how are you?
|
||||
```
|
||||
|
||||
### Handling block arguments
|
||||
|
||||
Arguments can be passed to filters:
|
||||
|
||||
```
|
||||
Hello {{ "Samy"|fullName("Pesse", man=true}} }}
|
||||
```
|
||||
|
||||
Arguments are passed to the function, named-arguments are passed as a last argument (object).
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
filters: {
|
||||
fullName: function(firstName, lastName, kwargs) {
|
||||
var name = firstName + ' ' + lastName;
|
||||
|
||||
if (kwargs.man) name = "Mr" + name;
|
||||
else name = "Mrs" + name;
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Hooks
|
||||
|
||||
Hooks is a method of augmenting or altering the behavior of the process, with custom callbacks.
|
||||
|
||||
### List of hooks
|
||||
|
||||
### Relative to the global pipeline
|
||||
|
||||
| Name | Description | Arguments |
|
||||
| ---- | ----------- | --------- |
|
||||
| `init` | Called after parsing the book, before generating output and pages. | None |
|
||||
| `finish:before` | Called after generating the pages, before copying assets, cover, ... | None |
|
||||
| `finish` | Called after everything else. | None |
|
||||
|
||||
### Relative to the page pipeline
|
||||
|
||||
> It is recommended using [templating](./templating.md) to extend page parsing.
|
||||
|
||||
| Name | Description | Arguments |
|
||||
| ---- | ----------- | --------- |
|
||||
| `page:before` | Called before running the templating engine on the page | Page Object |
|
||||
| `page` | Called before outputting and indexing the page. | Page Object |
|
||||
|
||||
##### Page Object
|
||||
|
||||
```js
|
||||
{
|
||||
// Parser named
|
||||
"type": "markdown",
|
||||
|
||||
// File Path relative to book root
|
||||
"path": "page.md",
|
||||
|
||||
// Absolute file path
|
||||
"rawpath": "/usr/...",
|
||||
|
||||
// Title of the page in the SUMMARY
|
||||
"title": "",
|
||||
|
||||
// Content of the page
|
||||
// Markdown/Asciidoc in "page:before"
|
||||
// HTML in "page"
|
||||
"content": "# Hello"
|
||||
}
|
||||
```
|
||||
|
||||
##### Example to add a title
|
||||
|
||||
In the `page:before` hook, `page.content` is the markdown/asciidoc content.
|
||||
|
||||
```js
|
||||
{
|
||||
"page:before": function(page) {
|
||||
page.content = "# Title\n" +page.content;
|
||||
return page;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Example to replace some html
|
||||
|
||||
In the `page` hook, `page.content` is the HTML generated from the markdown/asciidoc conversion.
|
||||
|
||||
```js
|
||||
{
|
||||
"page": function(page) {
|
||||
page.content = page.content.replace("<b>", "<strong>")
|
||||
.replace("</b>", "</strong>");
|
||||
return page;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Asynchronous Operations
|
||||
|
||||
Hooks callbacks can be asynchronous and return promises.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
{
|
||||
"init": function() {
|
||||
return writeSomeFile()
|
||||
.then(function() {
|
||||
return writeAnotherFile();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# Markdown
|
||||
|
||||
Most of the examples from this documentation are in Markdown. Markdown is default parser for GitBook, but one can also opt for the [AsciiDoc syntax](asciidoc.md).
|
||||
|
||||
Here’s an overview of Markdown syntax that you can use with GitBook (same as GitHub with some additions).
|
||||
|
||||
### Headings
|
||||
|
||||
To create a heading, add one to six `#` symbols before your heading text. The number of # you use will determine the size of the heading.
|
||||
|
||||
```markdown
|
||||
# This is an <h1> tag
|
||||
## This is an <h2> tag
|
||||
###### This is an <h6> tag
|
||||
```
|
||||
|
||||
GitBook supports a nice way for explicitly setting the header ID. If you follow the header text with an opening curly bracket (separated from the text with a least one space), a hash, the ID and a closing curly bracket, the ID is set on the header. If you use the trailing hash feature of atx style headers, the header ID has to go after the trailing hashes. For example:
|
||||
|
||||
```markdown
|
||||
Hello {#id}
|
||||
-----
|
||||
|
||||
# Hello {#id}
|
||||
|
||||
# Hello # {#id}
|
||||
```
|
||||
|
||||
### Paragraphs and Line Breaks {#paragraphs}
|
||||
|
||||
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. (A blank line is any line that looks like a blank line — a line containing nothing but spaces or tabs is considered blank.) Normal paragraphs should not be indented with spaces or tabs.
|
||||
|
||||
```
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
```
|
||||
|
||||
### Emphasis {#emphasis}
|
||||
|
||||
```markdown
|
||||
*This text will be italic*
|
||||
_This will also be italic_
|
||||
|
||||
**This text will be bold**
|
||||
__This will also be bold__
|
||||
|
||||
~~This text will be crossed out.~~
|
||||
|
||||
_You **can** combine them_
|
||||
```
|
||||
|
||||
### Lists {#lists}
|
||||
|
||||
Markdown supports ordered (numbered) and unordered (bulleted) lists.
|
||||
|
||||
##### Unordered
|
||||
|
||||
Unordered lists use asterisks, pluses, and hyphens — interchangably — as list markers:
|
||||
|
||||
```markdown
|
||||
* Item 1
|
||||
* Item 2
|
||||
* Item 2a
|
||||
* Item 2b
|
||||
```
|
||||
|
||||
##### Ordered
|
||||
|
||||
Ordered lists use numbers followed by periods:
|
||||
|
||||
```markdown
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
* Item 3a
|
||||
* Item 3b
|
||||
```
|
||||
|
||||
### Links {#links}
|
||||
|
||||
Markdown supports two style of links: inline and reference.
|
||||
|
||||
A simple link can be created by surrounding the text with square brackets and the link URL with parentheses:
|
||||
|
||||
```markdown
|
||||
This is [an example](http://example.com/ "Title") inline link with a title.
|
||||
|
||||
[This link](http://example.net/) has no title attribute.
|
||||
```
|
||||
|
||||
Links can point to relative paths, anchors or absolute urls.
|
||||
|
||||
|
||||
### References
|
||||
|
||||
There is another way to create links which does not interrupt the text flow. The URL and title are defined using a reference name and this reference name is then used in square brackets instead of the link URL:
|
||||
|
||||
```markdown
|
||||
This is [an example][id] reference-style link.
|
||||
```
|
||||
|
||||
Then, anywhere in the document, you define your link label like this, on a line by itself:
|
||||
|
||||
```markdown
|
||||
[id]: http://example.com/ "Optional Title Here"
|
||||
```
|
||||
|
||||
### Images {#images}
|
||||
|
||||
Images can be created in a similar way than links: just use an exclamation mark before the square brackets. The link text will become the alternative text of the image and the link URL specifies the image source:
|
||||
|
||||
```markdown
|
||||
An image: 
|
||||
```
|
||||
|
||||
### Blockquotes {#blockquotes}
|
||||
|
||||
A blockquote is started using the `>` marker followed by an optional space; all following lines that are also started with the blockquote marker belong to the blockquote. You can use any block-level elements inside a blockquote:
|
||||
|
||||
```markdown
|
||||
As Kanye West said:
|
||||
|
||||
> We're living the future so
|
||||
> the present is our past.
|
||||
```
|
||||
|
||||
### Tables {#tables}
|
||||
|
||||
You can create tables by assembling a list of words and dividing them with hyphens `-` (for the first row), and then separating each column with a pipe `|`:
|
||||
|
||||
```markdown
|
||||
| First Header | Second Header |
|
||||
| ------------- | ------------- |
|
||||
| Content Cell | Content Cell |
|
||||
| Content Cell | Content Cell |
|
||||
```
|
||||
|
||||
The pipes on either end of the table are optional. Cells can vary in width and do not need to be perfectly aligned within columns. There must be at least three hyphens in each column of the header row.
|
||||
|
||||
### Code {#code}
|
||||
|
||||
Markdown supports two different code block styles. One uses lines indented with either four spaces or one tab whereas the other uses lines with tilde characters as delimiters – therefore the content does not need to be indented:
|
||||
|
||||
```markdown
|
||||
This is a sample code block.
|
||||
|
||||
Continued here.
|
||||
|
||||
```
|
||||
|
||||
##### Fenced code blocks
|
||||
|
||||
You can create fenced code blocks by placing triple backticks ` ``` ` before and after the code block. We recommend placing a blank line before and after code blocks to make the raw formatting easier to read.
|
||||
|
||||
```
|
||||
function test() {
|
||||
console.log("notice the blank line before this function?");
|
||||
}
|
||||
```
|
||||
|
||||
##### Syntax highlighting
|
||||
|
||||
You can add an optional language identifier to enable syntax highlighting in your fenced code block.
|
||||
|
||||
For example, to syntax highlight Ruby code:
|
||||
|
||||
```ruby
|
||||
require 'redcarpet'
|
||||
markdown = Redcarpet.new("Hello World!")
|
||||
puts markdown.to_html
|
||||
```
|
||||
|
||||
##### Inline code
|
||||
|
||||
Text phrases can be marked up as code by surrounding them with backticks:
|
||||
|
||||
Use `gitbook` to convert the `text` in markdown
|
||||
syntax to HTML.
|
||||
|
||||
### Footnotes
|
||||
|
||||
GitBook supports a simple syntax for such footnotes. Footnotes are relative to each pages.
|
||||
|
||||
```markdown
|
||||
Text prior to footnote reference.[^2]
|
||||
|
||||
[^2]: Comment to include in footnote.
|
||||
```
|
||||
|
||||
### HTML
|
||||
|
||||
GitBook supports use of raw HTML in your text, Markdown syntax in HTML is not processed:
|
||||
|
||||
```
|
||||
<div>
|
||||
Markdown here will not be **parsed**
|
||||
</div>
|
||||
```
|
||||
|
||||
### Horizontal Rule
|
||||
|
||||
Horizontal Rules can be inserted using three or more asterisks, dashes or underscores, optionally separated by spaces or tabs, on an otherwise blank line:
|
||||
|
||||
```markdown
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
```
|
||||
|
||||
### Ignoring Markdown formatting
|
||||
|
||||
You can tell GitBook to ignore (or escape) Markdown formatting by using `\` before the Markdown character.
|
||||
|
||||
```
|
||||
Let's rename \*our-new-project\* to \*our-old-project\*.
|
||||
```
|
||||
@@ -87,3 +87,13 @@ Current version is {{ softwareVersion }}.
|
||||
##### include and block
|
||||
|
||||
Inclusion and inheritance is detailled in the [Content References](conrefs.md) section.
|
||||
|
||||
### Escaping
|
||||
|
||||
If you want GitBook to ignore any of the special templating tags, you can use raw and anything inside of it will be output as plain text.
|
||||
|
||||
``` twig
|
||||
{% raw %}
|
||||
this will {{ not be processed }}
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Builtin Templating Helpers
|
||||
|
||||
GitBook provides a serie of builtin filters and blocks to help you write templates.
|
||||
|
||||
### Filters
|
||||
|
||||
`value|default(default, [boolean])`If value is strictly undefined, return default, otherwise value. If boolean is true, any JavaScript falsy value will return default (false, "", etc)
|
||||
|
||||
`arr|sort(reverse, caseSens, attr)`
|
||||
Sort arr with JavaScript's arr.sort function. If reverse is true, result will be reversed. Sort is case-insensitive by default, but setting caseSens to true makes it case-sensitive. If attr is passed, will compare attr from each item.
|
||||
|
||||
### Blocks
|
||||
|
||||
`{% markdown %}Markdown string{% endmarkdown %}`
|
||||
Render inline markdown
|
||||
|
||||
`{% asciidoc %}AsciiDoc string{% endasciidoc %}`
|
||||
Render inline asciidoc
|
||||
@@ -10,8 +10,11 @@ The following is a reference of the available data during book's parsing and the
|
||||
| `gitbook` | GitBook specific information |
|
||||
| `page` | Current page specific information |
|
||||
| `file` | File associated with the current page specific information |
|
||||
| `readme` | Information about the Readme |
|
||||
| `glossary` | Information about the Glossary |
|
||||
| `summary` | Information about the table of contents |
|
||||
| `languages` | List of languages for multi-lingual books |
|
||||
| `output` | Information about the output generator |
|
||||
| `config` | Dump of the `book.json` |
|
||||
|
||||
### Book Variables
|
||||
@@ -62,3 +65,22 @@ The whole table of contents (`SUMMARY.md`) can be accessed:
|
||||
| `languages.list` | List of languages for this book |
|
||||
|
||||
Languages are defined by `{ id: 'en', title: 'English' }`.
|
||||
|
||||
### Output Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `output.name` | Name of the output generator, possible values are `website`, `json`, `ebook` |
|
||||
| `output.format` | When `output.name == "ebook"`, `format` defines the ebook format that will be generated, possible values are `pdf`, `epub` or `mobi` |
|
||||
|
||||
### Readme Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `readme.path` | Path to the Readme in the book |
|
||||
|
||||
### Glossary Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `glossary.path` | Path to the Glossary in the book |
|
||||
|
||||
Vendored
+3
-5
@@ -1,14 +1,12 @@
|
||||
# Theming
|
||||
|
||||
Since version 3.0.0, GitBook can be easily themed. Books are using by default the [theme-default](https://github.com/GitbookIO/theme-default).
|
||||
|
||||
The theme to use is specified in the [book's configuration](config.md) using key `theme`.
|
||||
Since version 3.0.0, GitBook can be easily themed. Books use the [theme-default](https://github.com/GitbookIO/theme-default) theme by default.
|
||||
|
||||
> **Caution**: Custom theming can block some plugins from working correctly.
|
||||
|
||||
### Structure of a theme
|
||||
|
||||
A theme is a folder containing templates and assets. All the templates are optionnal, since theme are always extending the default theme.
|
||||
A theme is a plugin containing templates and assets. Overriding any individual template is optional, since themes always extend the default theme.
|
||||
|
||||
| Folder | Description |
|
||||
| -------- | ----------- |
|
||||
@@ -19,7 +17,7 @@ A theme is a folder containing templates and assets. All the templates are optio
|
||||
|
||||
### Extend/Customize theme in a book
|
||||
|
||||
Authors can extend the templates of a theme directly from the book source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
|
||||
Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
|
||||
|
||||
### Publish a theme
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
var matchers = {
|
||||
/**
|
||||
Verify that a file exists in a directory
|
||||
*/
|
||||
toHaveFile: function () {
|
||||
return {
|
||||
compare: function (actual, expected) {
|
||||
var filePath = path.join(actual, expected);
|
||||
var exists = fs.existsSync(filePath);
|
||||
|
||||
return {
|
||||
pass: exists
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
jasmine.getEnv().beforeEach(function () {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
|
||||
jasmine.addMatchers(matchers);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
var gitbook = require('../gitbook');
|
||||
|
||||
describe('satisfies', function() {
|
||||
|
||||
it('should return true for *', function() {
|
||||
expect(gitbook.satisfies('*')).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
var tmp = require('tmp');
|
||||
var initBook = require('../init');
|
||||
|
||||
describe('initBook', function() {
|
||||
|
||||
pit('should create a README and SUMMARY for empty book', function() {
|
||||
var dir = tmp.dirSync();
|
||||
|
||||
return initBook(dir.name)
|
||||
.then(function() {
|
||||
expect(dir.name).toHaveFile('README.md');
|
||||
expect(dir.name).toHaveFile('SUMMARY.md');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
describe('GitBook', function() {
|
||||
it('should correctly export', function() {
|
||||
require('../');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
var Config = require('../models/config');
|
||||
|
||||
/**
|
||||
Decode changes from a JS API to a config object
|
||||
|
||||
@param {Config} config
|
||||
@param {Object} result: result from API
|
||||
@return {Config}
|
||||
*/
|
||||
function decodeGlobal(config, result) {
|
||||
var values = result.values;
|
||||
|
||||
delete values.generator;
|
||||
delete values.output;
|
||||
|
||||
return Config.updateValues(config, values);
|
||||
}
|
||||
|
||||
module.exports = decodeGlobal;
|
||||
@@ -0,0 +1,22 @@
|
||||
var decodeConfig = require('./decodeConfig');
|
||||
|
||||
/**
|
||||
Decode changes from a JS API to a output object.
|
||||
Only the configuration can be edited by plugin's hooks
|
||||
|
||||
@param {Output} output
|
||||
@param {Object} result: result from API
|
||||
@return {Output}
|
||||
*/
|
||||
function decodeGlobal(output, result) {
|
||||
var book = output.getBook();
|
||||
var config = book.getConfig();
|
||||
|
||||
// Update config
|
||||
config = decodeConfig(config, result.config);
|
||||
book = book.set('config', config);
|
||||
|
||||
return output.set('book', book);
|
||||
}
|
||||
|
||||
module.exports = decodeGlobal;
|
||||
@@ -0,0 +1,44 @@
|
||||
var deprecate = require('./deprecate');
|
||||
|
||||
/**
|
||||
Decode changes from a JS API to a page object.
|
||||
Only the content can be edited by plugin's hooks.
|
||||
|
||||
@param {Output} output
|
||||
@param {Page} page: page instance to edit
|
||||
@param {Object} result: result from API
|
||||
@return {Page}
|
||||
*/
|
||||
function decodePage(output, page, result) {
|
||||
var originalContent = page.getContent();
|
||||
|
||||
// No returned value
|
||||
// Existing content will be used
|
||||
if (!result) {
|
||||
return page;
|
||||
}
|
||||
|
||||
deprecate.disable('page.sections');
|
||||
|
||||
// GitBook 3
|
||||
// Use returned page.content if different from original content
|
||||
if (result.content != originalContent) {
|
||||
page = page.set('content', result.content);
|
||||
}
|
||||
|
||||
// GitBook 2 compatibility
|
||||
// Finally, use page.sections
|
||||
else if (result.sections) {
|
||||
page = page.set('content',
|
||||
result.sections.map(function(section) {
|
||||
return section.content;
|
||||
}).join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
deprecate.enable('page.sections');
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
module.exports = decodePage;
|
||||
@@ -0,0 +1,122 @@
|
||||
var is = require('is');
|
||||
var objectPath = require('object-path');
|
||||
|
||||
var logged = {};
|
||||
var disabled = {};
|
||||
|
||||
/**
|
||||
Log a deprecated notice
|
||||
|
||||
@param {Book|Output} book
|
||||
@param {String} key
|
||||
@param {String} message
|
||||
*/
|
||||
function logNotice(book, key, message) {
|
||||
if (logged[key] || disabled[key]) return;
|
||||
|
||||
logged[key] = true;
|
||||
|
||||
var logger = book.getLogger();
|
||||
logger.warn.ln(message);
|
||||
}
|
||||
|
||||
/**
|
||||
Deprecate a function
|
||||
|
||||
@param {Book|Output} book
|
||||
@param {String} key: unique identitifer for the deprecated
|
||||
@param {Function} fn
|
||||
@param {String} msg: message to print when called
|
||||
@return {Function}
|
||||
*/
|
||||
function deprecateMethod(book, key, fn, msg) {
|
||||
return function() {
|
||||
logNotice(book, key, msg);
|
||||
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
Deprecate a property of an object
|
||||
|
||||
@param {Book|Output} book
|
||||
@param {String} key: unique identitifer for the deprecated
|
||||
@param {Object} instance
|
||||
@param {String|Function} property
|
||||
@param {String} msg: message to print when called
|
||||
@return {Function}
|
||||
*/
|
||||
function deprecateField(book, key, instance, property, value, msg) {
|
||||
var store = undefined;
|
||||
|
||||
var prepare = function() {
|
||||
if (!is.undefined(store)) return;
|
||||
|
||||
if (is.fn(value)) store = value();
|
||||
else store = value;
|
||||
};
|
||||
|
||||
var getter = function(){
|
||||
prepare();
|
||||
|
||||
logNotice(book, key, msg);
|
||||
return store;
|
||||
};
|
||||
var setter = function(v) {
|
||||
prepare();
|
||||
|
||||
logNotice(book, key, msg);
|
||||
store = v;
|
||||
return store;
|
||||
};
|
||||
|
||||
Object.defineProperty(instance, property, {
|
||||
get: getter,
|
||||
set: setter,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Enable a deprecation
|
||||
|
||||
@param {String} key: unique identitifer
|
||||
*/
|
||||
function enableDeprecation(key) {
|
||||
disabled[key] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
Disable a deprecation
|
||||
|
||||
@param {String} key: unique identitifer
|
||||
*/
|
||||
function disableDeprecation(key) {
|
||||
disabled[key] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
Deprecate a method in favor of another one
|
||||
|
||||
@param {Book} book
|
||||
@param {String} key
|
||||
@param {Object} instance
|
||||
@param {String} oldName
|
||||
@param {String} newName
|
||||
*/
|
||||
function deprecateRenamedMethod(book, key, instance, oldName, newName, msg) {
|
||||
msg = msg || ('"' + oldName + '" is deprecated, use "' + newName + '()" instead');
|
||||
var fn = objectPath.get(instance, newName);
|
||||
|
||||
instance[oldName] = deprecateMethod(book, key, fn, msg);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
method: deprecateMethod,
|
||||
renamedMethod: deprecateRenamedMethod,
|
||||
field: deprecateField,
|
||||
enable: enableDeprecation,
|
||||
disable: disableDeprecation
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
var objectPath = require('object-path');
|
||||
var deprecate = require('./deprecate');
|
||||
|
||||
/**
|
||||
Encode a config object into a JS config api
|
||||
|
||||
@param {Output} output
|
||||
@param {Config} config
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeConfig(output, config) {
|
||||
var result = {
|
||||
values: config.getValues().toJS(),
|
||||
|
||||
get: function(key, defaultValue) {
|
||||
return objectPath.get(result.values, key, defaultValue);
|
||||
},
|
||||
|
||||
set: function(key, value) {
|
||||
return objectPath.set(result.values, key, value);
|
||||
}
|
||||
};
|
||||
|
||||
deprecate.field(output, 'config.options', result, 'options',
|
||||
result.values, '"config.options" property is deprecated, use "config.get(key)" instead');
|
||||
|
||||
deprecate.field(output, 'config.options.generator', result.values, 'generator',
|
||||
output.getGenerator(), '"options.generator" property is deprecated, use "output.name" instead');
|
||||
|
||||
deprecate.field(output, 'config.options.generator', result.values, 'output',
|
||||
output.getRoot(), '"options.output" property is deprecated, use "output.root()" instead');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodeConfig;
|
||||
@@ -0,0 +1,178 @@
|
||||
var path = require('path');
|
||||
var Promise = require('../utils/promise');
|
||||
var PathUtils = require('../utils/path');
|
||||
var fs = require('../utils/fs');
|
||||
|
||||
var Plugins = require('../plugins');
|
||||
var deprecate = require('./deprecate');
|
||||
var fileToURL = require('../output/helper/fileToURL');
|
||||
var defaultBlocks = require('../constants/defaultBlocks');
|
||||
var gitbook = require('../gitbook');
|
||||
|
||||
var encodeConfig = require('./encodeConfig');
|
||||
var encodeSummary = require('./encodeSummary');
|
||||
var encodeNavigation = require('./encodeNavigation');
|
||||
|
||||
/**
|
||||
Encode a global context into a JS object
|
||||
It's the context for page's hook, etc
|
||||
|
||||
@param {Output} output
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeGlobal(output) {
|
||||
var book = output.getBook();
|
||||
var bookFS = book.getContentFS();
|
||||
var logger = output.getLogger();
|
||||
var outputFolder = output.getRoot();
|
||||
var plugins = output.getPlugins();
|
||||
var blocks = Plugins.listBlocks(plugins);
|
||||
|
||||
var result = {
|
||||
log: logger,
|
||||
config: encodeConfig(output, book.getConfig()),
|
||||
summary: encodeSummary(output, book.getSummary()),
|
||||
|
||||
/**
|
||||
Check if the book is a multilingual book
|
||||
|
||||
@return {Boolean}
|
||||
*/
|
||||
isMultilingual: function() {
|
||||
return book.isMultilingual();
|
||||
},
|
||||
|
||||
/**
|
||||
Check if the book is a language book for a multilingual book
|
||||
|
||||
@return {Boolean}
|
||||
*/
|
||||
isLanguageBook: function() {
|
||||
return book.isLanguageBook();
|
||||
},
|
||||
|
||||
/**
|
||||
Read a file from the book
|
||||
|
||||
@param {String} fileName
|
||||
@return {Promise<Buffer>}
|
||||
*/
|
||||
readFile: function(fileName) {
|
||||
return bookFS.read(fileName);
|
||||
},
|
||||
|
||||
/**
|
||||
Read a file from the book as a string
|
||||
|
||||
@param {String} fileName
|
||||
@return {Promise<String>}
|
||||
*/
|
||||
readFileAsString: function(fileName) {
|
||||
return bookFS.readAsString(fileName);
|
||||
},
|
||||
|
||||
/**
|
||||
Resolve a file from the book root
|
||||
|
||||
@param {String} fileName
|
||||
@return {String}
|
||||
*/
|
||||
resolve: function(fileName) {
|
||||
return path.resolve(book.getContentRoot(), fileName);
|
||||
},
|
||||
|
||||
template: {
|
||||
/**
|
||||
Apply a templating block and returns its result
|
||||
|
||||
@param {String} name
|
||||
@param {Object} blockData
|
||||
@return {Promise|Object}
|
||||
*/
|
||||
applyBlock: function(name, blockData) {
|
||||
var block = blocks.get(name) || defaultBlocks.get(name);
|
||||
return Promise(block.applyBlock(blockData, result));
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
/**
|
||||
Name of the generator being used
|
||||
{String}
|
||||
*/
|
||||
name: output.getGenerator(),
|
||||
|
||||
/**
|
||||
Return absolute path to the root folder of output
|
||||
@return {String}
|
||||
*/
|
||||
root: function() {
|
||||
return outputFolder;
|
||||
},
|
||||
|
||||
/**
|
||||
Resolve a file from the output root
|
||||
|
||||
@param {String} fileName
|
||||
@return {String}
|
||||
*/
|
||||
resolve: function(fileName) {
|
||||
return path.resolve(outputFolder, fileName);
|
||||
},
|
||||
|
||||
/**
|
||||
Convert a filepath into an url
|
||||
@return {String}
|
||||
*/
|
||||
toURL: function(filePath) {
|
||||
return fileToURL(output, filePath);
|
||||
},
|
||||
|
||||
/**
|
||||
Write a file to the output folder,
|
||||
It creates the required folder
|
||||
|
||||
@param {String} fileName
|
||||
@param {Buffer} content
|
||||
@return {Promise}
|
||||
*/
|
||||
writeFile: function(fileName, content) {
|
||||
return Promise()
|
||||
.then(function() {
|
||||
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
|
||||
|
||||
return fs.ensureFile(filePath)
|
||||
.then(function() {
|
||||
return fs.writeFile(filePath, content);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
gitbook: {
|
||||
version: gitbook.version
|
||||
}
|
||||
};
|
||||
|
||||
// Deprecated properties
|
||||
|
||||
deprecate.renamedMethod(output, 'this.isSubBook', result, 'isSubBook', 'isLanguageBook');
|
||||
deprecate.renamedMethod(output, 'this.contentLink', result, 'contentLink', 'output.toURL');
|
||||
|
||||
deprecate.field(output, 'this.generator', result, 'generator',
|
||||
output.getGenerator(), '"this.generator" property is deprecated, use "this.output.name" instead');
|
||||
|
||||
deprecate.field(output, 'this.navigation', result, 'navigation', function() {
|
||||
return encodeNavigation(output);
|
||||
}, '"navigation" property is deprecated');
|
||||
|
||||
deprecate.field(output, 'this.book', result, 'book',
|
||||
result, '"book" property is deprecated, use "this" directly instead');
|
||||
|
||||
deprecate.field(output, 'this.options', result, 'options',
|
||||
result.config.values, '"options" property is deprecated, use config.get(key) instead');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodeGlobal;
|
||||
@@ -0,0 +1,64 @@
|
||||
var Immutable = require('immutable');
|
||||
|
||||
/**
|
||||
Encode an article for next/prev
|
||||
|
||||
@param {Map<String:Page>}
|
||||
@param {Article}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeArticle(pages, article) {
|
||||
var articlePath = article.getPath();
|
||||
|
||||
return {
|
||||
path: articlePath,
|
||||
title: article.getTitle(),
|
||||
level: article.getLevel(),
|
||||
exists: (articlePath && pages.has(articlePath)),
|
||||
external: article.isExternal()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
this.navigation is a deprecated property from GitBook v2
|
||||
|
||||
@param {Output}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeNavigation(output) {
|
||||
var book = output.getBook();
|
||||
var pages = output.getPages();
|
||||
var summary = book.getSummary();
|
||||
var articles = summary.getArticlesAsList();
|
||||
|
||||
|
||||
var navigation = articles
|
||||
.map(function(article, i) {
|
||||
var ref = article.getRef();
|
||||
if (!ref) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var prev = articles.get(i - 1);
|
||||
var next = articles.get(i + 1);
|
||||
|
||||
return [
|
||||
ref,
|
||||
{
|
||||
index: i,
|
||||
title: article.getTitle(),
|
||||
introduction: (i === 0),
|
||||
prev: prev? encodeArticle(pages, prev) : undefined,
|
||||
next: next? encodeArticle(pages, next) : undefined,
|
||||
level: article.getLevel()
|
||||
}
|
||||
];
|
||||
})
|
||||
.filter(function(e) {
|
||||
return Boolean(e);
|
||||
});
|
||||
|
||||
return Immutable.Map(navigation).toJS();
|
||||
}
|
||||
|
||||
module.exports = encodeNavigation;
|
||||
@@ -0,0 +1,39 @@
|
||||
var JSONUtils = require('../json');
|
||||
var deprecate = require('./deprecate');
|
||||
var encodeProgress = require('./encodeProgress');
|
||||
|
||||
/**
|
||||
Encode a page in a context to a JS API
|
||||
|
||||
@param {Output} output
|
||||
@param {Page} page
|
||||
@return {Object}
|
||||
*/
|
||||
function encodePage(output, page) {
|
||||
var book = output.getBook();
|
||||
var summary = book.getSummary();
|
||||
var fs = book.getContentFS();
|
||||
var file = page.getFile();
|
||||
|
||||
// JS Page is based on the JSON output
|
||||
var result = JSONUtils.encodePage(page, summary);
|
||||
|
||||
result.type = file.getType();
|
||||
result.path = file.getPath();
|
||||
result.rawPath = fs.resolve(result.path);
|
||||
|
||||
deprecate.field(output, 'page.progress', result, 'progress', function() {
|
||||
return encodeProgress(output, page);
|
||||
}, '"page.progress" property is deprecated');
|
||||
|
||||
deprecate.field(output, 'page.sections', result, 'sections', [
|
||||
{
|
||||
content: result.content,
|
||||
type: 'normal'
|
||||
}
|
||||
], '"sections" property is deprecated, use page.content instead');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodePage;
|
||||
@@ -0,0 +1,63 @@
|
||||
var Immutable = require('immutable');
|
||||
var encodeNavigation = require('./encodeNavigation');
|
||||
|
||||
/**
|
||||
page.progress is a deprecated property from GitBook v2
|
||||
|
||||
@param {Output}
|
||||
@param {Page}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeProgress(output, page) {
|
||||
var current = page.getPath();
|
||||
var navigation = encodeNavigation(output);
|
||||
navigation = Immutable.Map(navigation);
|
||||
|
||||
var n = navigation.size;
|
||||
var percent = 0, prevPercent = 0, currentChapter = null;
|
||||
var done = true;
|
||||
|
||||
var chapters = navigation
|
||||
.map(function(nav, chapterPath) {
|
||||
nav.path = chapterPath;
|
||||
return nav;
|
||||
})
|
||||
.valueSeq()
|
||||
.sortBy(function(nav) {
|
||||
return nav.index;
|
||||
})
|
||||
.map(function(nav, i) {
|
||||
// Calcul percent
|
||||
nav.percent = (i * 100) / Math.max((n - 1), 1);
|
||||
|
||||
// Is it done
|
||||
nav.done = done;
|
||||
if (nav.path == current) {
|
||||
currentChapter = nav;
|
||||
percent = nav.percent;
|
||||
done = false;
|
||||
} else if (done) {
|
||||
prevPercent = nav.percent;
|
||||
}
|
||||
|
||||
return nav;
|
||||
})
|
||||
.toJS();
|
||||
|
||||
return {
|
||||
// Previous percent
|
||||
prevPercent: prevPercent,
|
||||
|
||||
// Current percent
|
||||
percent: percent,
|
||||
|
||||
// List of chapter with progress
|
||||
chapters: chapters,
|
||||
|
||||
// Current chapter
|
||||
current: currentChapter
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeProgress;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
var encodeSummaryArticle = require('../json/encodeSummaryArticle');
|
||||
|
||||
/**
|
||||
Encode summary to provide an API to plugin
|
||||
|
||||
@param {Output} output
|
||||
@param {Config} config
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeSummary(output, summary) {
|
||||
var result = {
|
||||
/**
|
||||
Iterate over the summary, it stops when the "iter" returns false
|
||||
|
||||
@param {Function} iter
|
||||
*/
|
||||
walk: function (iter) {
|
||||
summary.getArticle(function(article) {
|
||||
var jsonArticle = encodeSummaryArticle(article, false);
|
||||
|
||||
return iter(jsonArticle);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Get an article by its level
|
||||
|
||||
@param {String} level
|
||||
@return {Object}
|
||||
*/
|
||||
getArticleByLevel: function(level) {
|
||||
var article = summary.getByLevel(level);
|
||||
return (article? encodeSummaryArticle(article) : undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
Get an article by its path
|
||||
|
||||
@param {String} level
|
||||
@return {Object}
|
||||
*/
|
||||
getArticleByPath: function(level) {
|
||||
var article = summary.getByPath(level);
|
||||
return (article? encodeSummaryArticle(article) : undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodeSummary;
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
encodePage: require('./encodePage'),
|
||||
decodePage: require('./decodePage'),
|
||||
|
||||
encodeGlobal: require('./encodeGlobal'),
|
||||
decodeGlobal: require('./decodeGlobal')
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
function BackboneFile(book) {
|
||||
if (!(this instanceof BackboneFile)) return new BackboneFile(book);
|
||||
|
||||
this.book = book;
|
||||
this.log = this.book.log;
|
||||
|
||||
// Filename in the book
|
||||
this.path = '';
|
||||
this.parser;
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
// Type of the backbone file
|
||||
BackboneFile.prototype.type = '';
|
||||
|
||||
// Parse a backbone file
|
||||
BackboneFile.prototype.parse = function() {
|
||||
// To be implemented by each child
|
||||
};
|
||||
|
||||
// Handle case where file doesn't exists
|
||||
BackboneFile.prototype.parseNotFound = function() {
|
||||
|
||||
};
|
||||
|
||||
// Return true if backbone file exists
|
||||
BackboneFile.prototype.exists = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Locate a backbone file, could be .md, .asciidoc, etc
|
||||
BackboneFile.prototype.locate = function() {
|
||||
var that = this;
|
||||
var filename = this.book.config.getStructure(this.type, true);
|
||||
this.log.debug.ln('locating', this.type, ':', filename);
|
||||
|
||||
return this.book.findParsableFile(filename)
|
||||
.then(function(result) {
|
||||
if (!result) return;
|
||||
|
||||
that.path = result.path;
|
||||
that.parser = result.parser;
|
||||
});
|
||||
};
|
||||
|
||||
// Read and parse the file
|
||||
BackboneFile.prototype.load = function() {
|
||||
var that = this;
|
||||
this.log.debug.ln('loading', this.type, ':', that.path);
|
||||
|
||||
return this.locate()
|
||||
.then(function() {
|
||||
if (!that.path) return that.parseNotFound();
|
||||
|
||||
that.log.debug.ln(that.type, 'located at', that.path);
|
||||
|
||||
return that.book.readFile(that.path)
|
||||
|
||||
// Parse it
|
||||
.then(function(content) {
|
||||
return that.parse(content);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BackboneFile;
|
||||
@@ -1,99 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
// Normalize a glossary entry name into a unique id
|
||||
function nameToId(name) {
|
||||
return name.toLowerCase()
|
||||
.replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
|
||||
.replace(/ /g, '_')
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
A glossary entry is represented by a name and a short description
|
||||
An unique id for the entry is generated using its name
|
||||
*/
|
||||
function GlossaryEntry(name, description) {
|
||||
if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
|
||||
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get: _.bind(this.getId, this)
|
||||
});
|
||||
}
|
||||
|
||||
// Normalizes a glossary entry's name to create an ID
|
||||
GlossaryEntry.prototype.getId = function() {
|
||||
return nameToId(this.name);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
A glossary is a list of entries stored in a GLOSSARY.md file
|
||||
*/
|
||||
function Glossary() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.entries = [];
|
||||
}
|
||||
util.inherits(Glossary, BackboneFile);
|
||||
|
||||
Glossary.prototype.type = 'glossary';
|
||||
|
||||
// Get templating context
|
||||
Glossary.prototype.getContext = function() {
|
||||
if (!this.path) return {};
|
||||
|
||||
return {
|
||||
glossary: {
|
||||
path: this.path
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Parse the readme content
|
||||
Glossary.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.glossary(content)
|
||||
.then(function(entries) {
|
||||
that.entries = _.map(entries, function(entry) {
|
||||
return new GlossaryEntry(entry.name, entry.description);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Return an entry by its id
|
||||
Glossary.prototype.get = function(id) {
|
||||
return _.find(this.entries, {
|
||||
id: id
|
||||
});
|
||||
};
|
||||
|
||||
// Find an entry by its name
|
||||
Glossary.prototype.find = function(name) {
|
||||
return this.get(nameToId(name));
|
||||
};
|
||||
|
||||
// Return false if glossary has entries (and exists)
|
||||
Glossary.prototype.isEmpty = function(id) {
|
||||
return _.size(this.entries) === 0;
|
||||
};
|
||||
|
||||
// Convert the glossary to a list of annotations
|
||||
Glossary.prototype.annotations = function() {
|
||||
return _.map(this.entries, function(entry) {
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
href: '/' + this.path + '#' + entry.id
|
||||
};
|
||||
}, this);
|
||||
};
|
||||
|
||||
module.exports = Glossary;
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
Readme: require('./readme'),
|
||||
Summary: require('./summary'),
|
||||
Glossary: require('./glossary'),
|
||||
Langs: require('./langs')
|
||||
};
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
function Language(title, folder) {
|
||||
var that = this;
|
||||
|
||||
this.title = title;
|
||||
this.folder = folder;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get: function() {
|
||||
return path.basename(that.folder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
A Langs is a list of languages stored in a LANGS.md file
|
||||
*/
|
||||
function Langs() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.languages = [];
|
||||
}
|
||||
util.inherits(Langs, BackboneFile);
|
||||
|
||||
Langs.prototype.type = 'langs';
|
||||
|
||||
// Parse the readme content
|
||||
Langs.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.langs(content)
|
||||
.then(function(langs) {
|
||||
that.languages = _.map(langs, function(entry) {
|
||||
return new Language(entry.title, entry.path);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Return the list of languages
|
||||
Langs.prototype.list = function() {
|
||||
return this.languages;
|
||||
};
|
||||
|
||||
// Return default/main language for the book
|
||||
Langs.prototype.getDefault = function() {
|
||||
return _.first(this.languages);
|
||||
};
|
||||
|
||||
// Return true if a language is the default one
|
||||
// "lang" cam be a string (id) or a Language entry
|
||||
Langs.prototype.isDefault = function(lang) {
|
||||
lang = lang.id || lang;
|
||||
return (this.cound() > 0 && this.getDefault().id == lang);
|
||||
};
|
||||
|
||||
// Return the count of languages
|
||||
Langs.prototype.count = function() {
|
||||
return _.size(this.languages);
|
||||
};
|
||||
|
||||
// Return templating context for the languages list
|
||||
Langs.prototype.getContext = function() {
|
||||
if (this.count() == 0) return {};
|
||||
|
||||
return {
|
||||
languages: {
|
||||
list: _.map(this.languages, function(lang) {
|
||||
return {
|
||||
id: lang.id,
|
||||
title: lang.title
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Langs;
|
||||
@@ -1,26 +0,0 @@
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
function Readme() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.title;
|
||||
this.description;
|
||||
}
|
||||
util.inherits(Readme, BackboneFile);
|
||||
|
||||
Readme.prototype.type = 'readme';
|
||||
|
||||
// Parse the readme content
|
||||
Readme.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.readme(content)
|
||||
.then(function(out) {
|
||||
that.title = out.title;
|
||||
that.description = out.description;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = Readme;
|
||||
@@ -1,348 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
|
||||
var location = require('../utils/location');
|
||||
var error = require('../utils/error');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
/*
|
||||
An article represent an entry in the Summary.
|
||||
It's defined by a title, a reference, and children articles,
|
||||
the reference (ref) can be a filename + anchor or an external file (optional)
|
||||
*/
|
||||
function TOCArticle(def, parent) {
|
||||
// Title
|
||||
this.title = def.title;
|
||||
|
||||
// Parent TOCPart or TOCArticle
|
||||
this.parent = parent;
|
||||
|
||||
// As string indicating the overall position
|
||||
// ex: '1.0.0'
|
||||
this.level;
|
||||
this._next;
|
||||
this._prev;
|
||||
|
||||
// When README has been automatically added
|
||||
this.isAutoIntro = def.isAutoIntro;
|
||||
this.isIntroduction = def.isIntroduction;
|
||||
|
||||
this.validate();
|
||||
|
||||
// Path can be a relative path or an url, or nothing
|
||||
this.ref = def.path;
|
||||
if (this.ref && !this.isExternal()) {
|
||||
var parts = this.ref.split('#');
|
||||
this.path = (parts.length > 1? parts.slice(0, -1).join('#') : this.ref);
|
||||
this.anchor = (parts.length > 1? '#' + _.last(parts) : null);
|
||||
|
||||
// Normalize path to remove ('./', etc)
|
||||
this.path = location.normalize(this.path);
|
||||
}
|
||||
|
||||
this.articles = _.map(def.articles || [], function(article) {
|
||||
if (article instanceof TOCArticle) return article;
|
||||
return new TOCArticle(article, this);
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Validate the article
|
||||
TOCArticle.prototype.validate = function() {
|
||||
if (!this.title) {
|
||||
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over all articles in this articles
|
||||
TOCArticle.prototype.walk = function(iter, base) {
|
||||
base = base || this.level;
|
||||
|
||||
_.each(this.articles, function(article, i) {
|
||||
var level = levelId(base, i);
|
||||
|
||||
if (iter(article, level) === false) {
|
||||
return false;
|
||||
}
|
||||
article.walk(iter, level);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for an article
|
||||
TOCArticle.prototype.getContext = function() {
|
||||
return {
|
||||
level: this.level,
|
||||
title: this.title,
|
||||
depth: this.depth(),
|
||||
path: this.isExternal()? undefined : this.path,
|
||||
anchor: this.isExternal()? undefined : this.anchor,
|
||||
url: this.isExternal()? this.ref : undefined
|
||||
};
|
||||
};
|
||||
|
||||
// Return true if is pointing to a file
|
||||
TOCArticle.prototype.hasLocation = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Return true if is pointing to an external location
|
||||
TOCArticle.prototype.isExternal = function() {
|
||||
return location.isExternal(this.ref);
|
||||
};
|
||||
|
||||
// Return true if this article is the introduction
|
||||
TOCArticle.prototype.isIntro = function() {
|
||||
return Boolean(this.isIntroduction);
|
||||
};
|
||||
|
||||
// Return true if has children
|
||||
TOCArticle.prototype.hasChildren = function() {
|
||||
return this.articles.length > 0;
|
||||
};
|
||||
|
||||
// Return true if has an article as parent
|
||||
TOCArticle.prototype.hasParent = function() {
|
||||
return !(this.parent instanceof TOCPart);
|
||||
};
|
||||
|
||||
// Return depth of this article
|
||||
TOCArticle.prototype.depth = function() {
|
||||
return this.level.split('.').length;
|
||||
};
|
||||
|
||||
// Return next article in the TOC
|
||||
TOCArticle.prototype.next = function() {
|
||||
return this._next;
|
||||
};
|
||||
|
||||
// Return previous article in the TOC
|
||||
TOCArticle.prototype.prev = function() {
|
||||
return this._prev;
|
||||
};
|
||||
|
||||
// Map over all articles
|
||||
TOCArticle.prototype.map = function(iter) {
|
||||
return _.map(this.articles, iter);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
A part of a ToC is a composed of a tree of articles and an optiona title
|
||||
*/
|
||||
function TOCPart(part, parent) {
|
||||
if (!(this instanceof TOCPart)) return new TOCPart(part, parent);
|
||||
|
||||
TOCArticle.apply(this, arguments);
|
||||
}
|
||||
util.inherits(TOCPart, TOCArticle);
|
||||
|
||||
// Validate the part
|
||||
TOCPart.prototype.validate = function() { };
|
||||
|
||||
// Return a sibling (next or prev) of this part
|
||||
TOCPart.prototype.sibling = function(direction) {
|
||||
var parts = this.parent.parts;
|
||||
var pos = _.findIndex(parts, this);
|
||||
|
||||
if (parts[pos + direction]) {
|
||||
return parts[pos + direction];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Iterate over all entries of the part
|
||||
TOCPart.prototype.walk = function(iter, base) {
|
||||
var articles = this.articles;
|
||||
|
||||
if (articles.length == 0) return;
|
||||
|
||||
// Has introduction?
|
||||
if (articles[0].isIntro()) {
|
||||
if (iter(articles[0], '0') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
articles = articles.slice(1);
|
||||
}
|
||||
|
||||
|
||||
_.each(articles, function(article, i) {
|
||||
var level = levelId(base, i);
|
||||
|
||||
if (iter(article, level) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
article.walk(iter, level);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for a part
|
||||
TOCPart.prototype.getContext = function(onArticle) {
|
||||
onArticle = onArticle || function(article) {
|
||||
return article.getContext();
|
||||
};
|
||||
|
||||
return {
|
||||
title: this.title,
|
||||
articles: this.map(onArticle)
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
A summary is composed of a list of parts, each composed wit a tree of articles.
|
||||
*/
|
||||
function Summary() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.parts = [];
|
||||
this._length = 0;
|
||||
}
|
||||
util.inherits(Summary, BackboneFile);
|
||||
|
||||
Summary.prototype.type = 'summary';
|
||||
|
||||
// Prepare summary when non existant
|
||||
Summary.prototype.parseNotFound = function() {
|
||||
this.update([]);
|
||||
};
|
||||
|
||||
// Parse the summary content
|
||||
Summary.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.summary(content)
|
||||
|
||||
.then(function(summary) {
|
||||
that.update(summary.parts);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for the summary
|
||||
Summary.prototype.getContext = function() {
|
||||
function onArticle(article) {
|
||||
var result = article.getContext();
|
||||
if (article.hasChildren()) {
|
||||
result.articles = article.map(onArticle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
summary: {
|
||||
parts: _.map(this.parts, function(part) {
|
||||
return part.getContext(onArticle);
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Iterate over all entries of the summary
|
||||
// iter is called with an TOCArticle
|
||||
Summary.prototype.walk = function(iter) {
|
||||
var hasMultipleParts = this.parts.length > 1;
|
||||
|
||||
_.each(this.parts, function(part, i) {
|
||||
part.walk(iter, hasMultipleParts? levelId('', i) : null);
|
||||
});
|
||||
};
|
||||
|
||||
// Find a specific article using a filter
|
||||
Summary.prototype.find = function(filter) {
|
||||
var result;
|
||||
|
||||
this.walk(function(article) {
|
||||
if (filter(article)) {
|
||||
result = article;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Flatten the list of articles
|
||||
Summary.prototype.flatten = function() {
|
||||
var result = [];
|
||||
|
||||
this.walk(function(article) {
|
||||
result.push(article);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Return the first TOCArticle for a specific page (or path)
|
||||
Summary.prototype.getArticle = function(page) {
|
||||
if (!_.isString(page)) page = page.path;
|
||||
|
||||
return this.find(function(article) {
|
||||
return article.path == page;
|
||||
});
|
||||
};
|
||||
|
||||
// Return the first TOCArticle for a specific level
|
||||
Summary.prototype.getArticleByLevel = function(lvl) {
|
||||
return this.find(function(article) {
|
||||
return article.level == lvl;
|
||||
});
|
||||
};
|
||||
|
||||
// Return the count of articles in the summary
|
||||
Summary.prototype.count = function() {
|
||||
return this._length;
|
||||
};
|
||||
|
||||
// Prepare the summary
|
||||
Summary.prototype.update = function(parts) {
|
||||
var that = this;
|
||||
|
||||
|
||||
that.parts = _.map(parts, function(part) {
|
||||
return new TOCPart(part, that);
|
||||
});
|
||||
|
||||
// Create first part if none
|
||||
if (that.parts.length == 0) {
|
||||
that.parts.push(new TOCPart({}, that));
|
||||
}
|
||||
|
||||
// Add README as first entry
|
||||
var firstArticle = that.parts[0].articles[0];
|
||||
if (!firstArticle || firstArticle.path != that.book.readme.path) {
|
||||
that.parts[0].articles.unshift(new TOCArticle({
|
||||
title: 'Introduction',
|
||||
path: that.book.readme.path,
|
||||
isAutoIntro: true
|
||||
}, that.parts[0]));
|
||||
}
|
||||
that.parts[0].articles[0].isIntroduction = true;
|
||||
|
||||
|
||||
// Update the count and indexing of "level"
|
||||
var prev = undefined;
|
||||
|
||||
that._length = 0;
|
||||
that.walk(function(article, level) {
|
||||
// Index level
|
||||
article.level = level;
|
||||
|
||||
// Chain articles
|
||||
article._prev = prev;
|
||||
if (prev) prev._next = article;
|
||||
|
||||
prev = article;
|
||||
|
||||
that._length += 1;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Return a level string from a base level and an index
|
||||
function levelId(base, i) {
|
||||
i = i + 1;
|
||||
return (base? [base || '', i] : [i]).join('.');
|
||||
}
|
||||
|
||||
module.exports = Summary;
|
||||
-388
@@ -1,388 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var Ignore = require('ignore');
|
||||
|
||||
var Config = require('./config');
|
||||
var Readme = require('./backbone/readme');
|
||||
var Glossary = require('./backbone/glossary');
|
||||
var Summary = require('./backbone/summary');
|
||||
var Langs = require('./backbone/langs');
|
||||
var Page = require('./page');
|
||||
var pathUtil = require('./utils/path');
|
||||
var error = require('./utils/error');
|
||||
var Promise = require('./utils/promise');
|
||||
var Logger = require('./utils/logger');
|
||||
var parsers = require('./parsers');
|
||||
var initBook = require('./init');
|
||||
|
||||
|
||||
/*
|
||||
The Book class is an interface for parsing books content.
|
||||
It does not require to run on Node.js, isnce it only depends on the fs implementation
|
||||
*/
|
||||
|
||||
function Book(opts) {
|
||||
if (!(this instanceof Book)) return new Book(opts);
|
||||
|
||||
this.opts = _.defaults(opts || {}, {
|
||||
fs: null,
|
||||
|
||||
// Root path for the book
|
||||
root: '',
|
||||
|
||||
// Extend book configuration
|
||||
config: {},
|
||||
|
||||
// Log function
|
||||
log: function(msg) {
|
||||
process.stdout.write(msg);
|
||||
},
|
||||
|
||||
// Log level
|
||||
logLevel: 'info'
|
||||
});
|
||||
|
||||
if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));
|
||||
|
||||
// Root path for the book
|
||||
this.root = opts.root;
|
||||
|
||||
// If multi-lingual, book can have a parent
|
||||
this.parent = opts.parent;
|
||||
if (this.parent) {
|
||||
this.language = path.relative(this.parent.root, this.root);
|
||||
}
|
||||
|
||||
// A book is linked to an fs, to access its content
|
||||
this.fs = opts.fs;
|
||||
|
||||
// Rules to ignore some files
|
||||
this.ignore = Ignore();
|
||||
this.ignore.addPattern([
|
||||
// Skip Git stuff
|
||||
'.git/',
|
||||
|
||||
// Skip OS X meta data
|
||||
'.DS_Store',
|
||||
|
||||
// Skip stuff installed by plugins
|
||||
'node_modules',
|
||||
|
||||
// Skip book outputs
|
||||
'_book',
|
||||
'*.pdf',
|
||||
'*.epub',
|
||||
'*.mobi'
|
||||
]);
|
||||
|
||||
// Create a logger for the book
|
||||
this.log = new Logger(opts.log, opts.logLevel);
|
||||
|
||||
// Create an interface to access the configuration
|
||||
this.config = new Config(this, opts.config);
|
||||
|
||||
// Interfaces for the book structure
|
||||
this.readme = new Readme(this);
|
||||
this.summary = new Summary(this);
|
||||
this.glossary = new Glossary(this);
|
||||
|
||||
// Multilinguals book
|
||||
this.langs = new Langs(this);
|
||||
this.books = [];
|
||||
|
||||
// List of page in the book
|
||||
this.pages = {};
|
||||
|
||||
// Deprecation for templates
|
||||
Object.defineProperty(this, 'options', {
|
||||
get: function () {
|
||||
this.log.warn.ln('"options" property is deprecated, use config.get(key) instead');
|
||||
var cfg = this.config.dump();
|
||||
error.deprecateField(cfg, 'book', (this.output? this.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
|
||||
|
||||
// options.generator
|
||||
cfg.generator = this.output? this.output.name : null;
|
||||
|
||||
// options.output
|
||||
cfg.output = this.output? this.output.root() : null;
|
||||
|
||||
return cfg;
|
||||
}
|
||||
});
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
// Loop for template filters/blocks
|
||||
error.deprecateField(this, 'book', this, '"book" property is deprecated, use "this" directly instead');
|
||||
}
|
||||
|
||||
// Return templating context for the book
|
||||
Book.prototype.getContext = function() {
|
||||
var variables = this.config.get('variables', {});
|
||||
|
||||
return {
|
||||
book: _.extend({
|
||||
language: this.language
|
||||
}, variables)
|
||||
};
|
||||
};
|
||||
|
||||
// Parse and prepare the configuration, fail if invalid
|
||||
Book.prototype.prepareConfig = function() {
|
||||
var that = this;
|
||||
|
||||
return this.config.load()
|
||||
.then(function() {
|
||||
var rootFolder = that.config.get('root');
|
||||
if (!rootFolder) return;
|
||||
|
||||
that.originalRoot = that.root;
|
||||
that.root = path.resolve(that.root, rootFolder);
|
||||
});
|
||||
};
|
||||
|
||||
// Resolve a path in the book source
|
||||
// Enforce that the output path is in the scope
|
||||
Book.prototype.resolve = function() {
|
||||
var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
|
||||
if (!this.isFileInScope(filename)) {
|
||||
throw error.FileOutOfScopeError({
|
||||
filename: filename,
|
||||
root: this.root
|
||||
});
|
||||
}
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
// Return false if a file is outside the book' scope
|
||||
Book.prototype.isFileInScope = function(filename) {
|
||||
filename = path.resolve(this.root, filename);
|
||||
|
||||
// Is the file in the scope of the parent?
|
||||
if (this.parent && this.parent.isFileInScope(filename)) return true;
|
||||
|
||||
// Is file in the root folder?
|
||||
return pathUtil.isInRoot(this.root, filename);
|
||||
};
|
||||
|
||||
// Parse .gitignore, etc to extract rules
|
||||
Book.prototype.parseIgnoreRules = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise.serie([
|
||||
'.ignore',
|
||||
'.gitignore',
|
||||
'.bookignore'
|
||||
], function(filename) {
|
||||
return that.readFile(filename)
|
||||
.then(function(content) {
|
||||
that.ignore.addPattern(content.toString().split(/\r?\n/));
|
||||
}, function() {
|
||||
return Promise();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Parse the whole book
|
||||
Book.prototype.parse = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(this.prepareConfig)
|
||||
.then(this.parseIgnoreRules)
|
||||
|
||||
// Parse languages
|
||||
.then(function() {
|
||||
return that.langs.load();
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
if (that.isMultilingual()) {
|
||||
if (that.isLanguageBook()) {
|
||||
throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
|
||||
}
|
||||
|
||||
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
|
||||
|
||||
// Create a new book for each language and parse it
|
||||
return Promise.serie(that.langs.list(), function(lang) {
|
||||
that.log.debug.ln('Preparing book for language', lang.id);
|
||||
var langBook = new Book(_.extend({}, that.opts, {
|
||||
parent: that,
|
||||
config: that.config.dump(),
|
||||
root: that.resolve(lang.id)
|
||||
}));
|
||||
|
||||
that.books.push(langBook);
|
||||
|
||||
return langBook.parse();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise()
|
||||
|
||||
// Parse the readme
|
||||
.then(that.readme.load)
|
||||
.then(function() {
|
||||
if (!that.readme.exists()) {
|
||||
throw new error.FileNotFoundError({ filename: 'README' });
|
||||
}
|
||||
|
||||
// Default configuration to infos extracted from readme
|
||||
if (!that.config.get('title')) that.config.set('title', that.readme.title);
|
||||
if (!that.config.get('description')) that.config.set('description', that.readme.description);
|
||||
})
|
||||
|
||||
// Parse the summary
|
||||
.then(that.summary.load)
|
||||
.then(function() {
|
||||
if (!that.summary.exists()) {
|
||||
that.log.warn.ln('no summary file in this book');
|
||||
}
|
||||
|
||||
// Index summary's articles
|
||||
that.summary.walk(function(article) {
|
||||
if (!article.hasLocation() || article.isExternal()) return;
|
||||
that.addPage(article.path);
|
||||
});
|
||||
})
|
||||
|
||||
// Parse the glossary
|
||||
.then(that.glossary.load)
|
||||
|
||||
// Add the glossary as a page
|
||||
.then(function() {
|
||||
if (!that.glossary.exists()) return;
|
||||
that.addPage(that.glossary.path);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Mark a filename as being parsable
|
||||
Book.prototype.addPage = function(filename) {
|
||||
if (this.hasPage(filename)) return this.getPage(filename);
|
||||
|
||||
filename = pathUtil.normalize(filename);
|
||||
this.pages[filename] = new Page(this, filename);
|
||||
return this.pages[filename];
|
||||
};
|
||||
|
||||
// Return a page by its filename (or undefined)
|
||||
Book.prototype.getPage = function(filename) {
|
||||
filename = pathUtil.normalize(filename);
|
||||
return this.pages[filename];
|
||||
};
|
||||
|
||||
|
||||
// Return true, if has a specific page
|
||||
Book.prototype.hasPage = function(filename) {
|
||||
return Boolean(this.getPage(filename));
|
||||
};
|
||||
|
||||
// Test if a file is ignored, return true if it is
|
||||
Book.prototype.isFileIgnored = function(filename) {
|
||||
return this.ignore.filter([filename]).length == 0;
|
||||
};
|
||||
|
||||
// Read a file in the book, throw error if ignored
|
||||
Book.prototype.readFile = function(filename) {
|
||||
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
|
||||
return this.fs.readAsString(this.resolve(filename));
|
||||
};
|
||||
|
||||
// Get stat infos about a file
|
||||
Book.prototype.statFile = function(filename) {
|
||||
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
|
||||
return this.fs.stat(this.resolve(filename));
|
||||
};
|
||||
|
||||
// Find a parsable file using a filename
|
||||
Book.prototype.findParsableFile = function(filename) {
|
||||
var that = this;
|
||||
|
||||
var ext = path.extname(filename);
|
||||
var basename = path.basename(filename, ext);
|
||||
|
||||
// Ordered list of extensions to test
|
||||
var exts = parsers.extensions;
|
||||
if (ext) exts = _.uniq([ext].concat(exts));
|
||||
|
||||
return _.reduce(exts, function(prev, ext) {
|
||||
return prev.then(function(output) {
|
||||
// Stop if already find a parser
|
||||
if (output) return output;
|
||||
|
||||
var filepath = basename+ext;
|
||||
|
||||
return that.fs.findFile(that.root, filepath)
|
||||
.then(function(realFilepath) {
|
||||
if (!realFilepath) return null;
|
||||
|
||||
return {
|
||||
parser: parsers.get(ext),
|
||||
path: realFilepath
|
||||
};
|
||||
});
|
||||
});
|
||||
}, Promise(null));
|
||||
};
|
||||
|
||||
// Return true if book is associated to a language
|
||||
Book.prototype.isLanguageBook = function() {
|
||||
return Boolean(this.parent);
|
||||
};
|
||||
Book.prototype.isSubBook = Book.prototype.isLanguageBook;
|
||||
|
||||
// Return true if the book is main instance of a multilingual book
|
||||
Book.prototype.isMultilingual = function() {
|
||||
return this.langs.count() > 0;
|
||||
};
|
||||
|
||||
// Return true if file is in the scope of this book
|
||||
Book.prototype.isInBook = function(filename) {
|
||||
return pathUtil.isInRoot(
|
||||
this.root,
|
||||
filename
|
||||
);
|
||||
};
|
||||
|
||||
// Return true if file is in the scope of a child book
|
||||
Book.prototype.isInLanguageBook = function(filename) {
|
||||
var that = this;
|
||||
|
||||
return _.some(this.langs.list(), function(lang) {
|
||||
return pathUtil.isInRoot(
|
||||
that.resolve(lang.id),
|
||||
that.resolve(filename)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// ----- DEPRECATED METHODS
|
||||
|
||||
Book.prototype.contentLink = error.deprecateMethod(function(s) {
|
||||
return this.output.toURL(s);
|
||||
}, '.contentLink() is deprecated, use ".output.toURL()" instead');
|
||||
|
||||
Book.prototype.contentPath = error.deprecateMethod(function(s) {
|
||||
return this.output.toURL(s);
|
||||
}, '.contentPath() is deprecated, use ".output.toURL()" instead');
|
||||
|
||||
Book.prototype.isSubBook = error.deprecateMethod(function() {
|
||||
return this.isLanguageBook();
|
||||
}, '.isSubBook() is deprecated, use ".isLanguageBook()" instead');
|
||||
|
||||
|
||||
// Initialize a book
|
||||
Book.init = function(fs, root, opts) {
|
||||
var book = new Book(_.extend(opts || {}, {
|
||||
root: root,
|
||||
fs: fs
|
||||
}));
|
||||
|
||||
return initBook(book);
|
||||
};
|
||||
|
||||
|
||||
module.exports = Book;
|
||||
@@ -0,0 +1,15 @@
|
||||
var Modifiers = require('./modifiers');
|
||||
|
||||
module.exports = {
|
||||
Parse: require('./parse'),
|
||||
|
||||
// Models
|
||||
Book: require('./models/book'),
|
||||
FS: require('./models/fs'),
|
||||
Summary: require('./models/summary'),
|
||||
Glossary: require('./models/glossary'),
|
||||
|
||||
// Modifiers
|
||||
SummaryModifier: Modifiers.Summary,
|
||||
ConfigModifier: Modifiers.Config
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
var Parse = require('../parse');
|
||||
var Output = require('../output');
|
||||
var timing = require('../utils/timing');
|
||||
|
||||
var options = require('./options');
|
||||
var getBook = require('./getBook');
|
||||
var getOutputFolder = require('./getOutputFolder');
|
||||
|
||||
|
||||
module.exports = {
|
||||
name: 'build [book] [output]',
|
||||
description: 'build a book',
|
||||
options: [
|
||||
options.log,
|
||||
options.format,
|
||||
options.timing
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var book = getBook(args, kwargs);
|
||||
var outputFolder = getOutputFolder(args);
|
||||
|
||||
var Generator = Output.getGenerator(kwargs.format);
|
||||
|
||||
return Parse.parseBook(book)
|
||||
.then(function(resultBook) {
|
||||
return Output.generate(Generator, resultBook, {
|
||||
root: outputFolder
|
||||
});
|
||||
})
|
||||
.fin(function() {
|
||||
if (kwargs.timing) timing.dump(book.getLogger());
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
var path = require('path');
|
||||
var tmp = require('tmp');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var fs = require('../utils/fs');
|
||||
var Parse = require('../parse');
|
||||
var Output = require('../output');
|
||||
|
||||
var options = require('./options');
|
||||
var getBook = require('./getBook');
|
||||
|
||||
|
||||
module.exports = function(format) {
|
||||
return {
|
||||
name: (format + ' [book] [output]'),
|
||||
description: 'build a book into an ebook file',
|
||||
options: [
|
||||
options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
// Output file will be stored in
|
||||
var outputFile = args[1] || ('book.' + format);
|
||||
|
||||
// Create temporary directory
|
||||
var outputFolder = tmp.dirSync().name;
|
||||
|
||||
var book = getBook(args, kwargs);
|
||||
var logger = book.getLogger();
|
||||
var Generator = Output.getGenerator('ebook');
|
||||
|
||||
return Parse.parseBook(book)
|
||||
.then(function(resultBook) {
|
||||
return Output.generate(Generator, resultBook, {
|
||||
root: outputFolder,
|
||||
format: format
|
||||
});
|
||||
})
|
||||
|
||||
// Extract ebook file
|
||||
.then(function(output) {
|
||||
var book = output.getBook();
|
||||
var languages = book.getLanguages();
|
||||
|
||||
if (book.isMultilingual()) {
|
||||
return Promise.ForEach(languages, function(lang) {
|
||||
var langID = lang.getID();
|
||||
|
||||
var langOutputFile = path.join(
|
||||
path.dirname(outputFile),
|
||||
path.basename(outputFile, format) + '_' + langID + '.' + format
|
||||
);
|
||||
|
||||
return fs.copy(
|
||||
path.resolve(outputFolder, langID, 'index.' + format),
|
||||
langOutputFile
|
||||
);
|
||||
})
|
||||
.thenResolve(languages.getCount());
|
||||
} else {
|
||||
return fs.copy(
|
||||
path.resolve(outputFolder, 'index.' + format),
|
||||
outputFile
|
||||
).thenResolve(1);
|
||||
}
|
||||
})
|
||||
|
||||
// Log end
|
||||
.then(function(count) {
|
||||
logger.info.ok(count + ' file(s) generated');
|
||||
|
||||
logger.debug('cleaning up... ');
|
||||
return logger.debug.promise(fs.rmDir(outputFolder));
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
var path = require('path');
|
||||
var Book = require('../models/book');
|
||||
var createNodeFS = require('../fs/node');
|
||||
|
||||
/**
|
||||
Return a book instance to work on from
|
||||
command line args/kwargs
|
||||
|
||||
@param {Array} args
|
||||
@param {Object} kwargs
|
||||
@return {Book}
|
||||
*/
|
||||
function getBook(args, kwargs) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
var logLevel = kwargs.log;
|
||||
|
||||
var fs = createNodeFS(input);
|
||||
var book = Book.createForFS(fs);
|
||||
|
||||
return book.setLogLevel(logLevel);
|
||||
}
|
||||
|
||||
module.exports = getBook;
|
||||
@@ -0,0 +1,17 @@
|
||||
var path = require('path');
|
||||
|
||||
/**
|
||||
Return path to output folder
|
||||
|
||||
@param {Array} args
|
||||
@return {String}
|
||||
*/
|
||||
function getOutputFolder(args) {
|
||||
var bookRoot = path.resolve(args[0] || process.cwd());
|
||||
var defaultOutputRoot = path.join(bookRoot, '_book');
|
||||
var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot;
|
||||
|
||||
return outputFolder;
|
||||
}
|
||||
|
||||
module.exports = getOutputFolder;
|
||||
@@ -1,140 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var Book = require('../book');
|
||||
var NodeFS = require('../fs/node');
|
||||
var Logger = require('../utils/logger');
|
||||
var Promise = require('../utils/promise');
|
||||
var fs = require('../utils/fs');
|
||||
var JSONOutput = require('../output/json');
|
||||
var WebsiteOutput = require('../output/website');
|
||||
var EBookOutput = require('../output/ebook');
|
||||
|
||||
var nodeFS = new NodeFS();
|
||||
|
||||
var LOG_OPTION = {
|
||||
name: 'log',
|
||||
description: 'Minimum log level to display',
|
||||
values: _.chain(Logger.LEVELS)
|
||||
.keys()
|
||||
.map(function(s) {
|
||||
return s.toLowerCase();
|
||||
})
|
||||
.value(),
|
||||
defaults: 'info'
|
||||
};
|
||||
|
||||
var FORMAT_OPTION = {
|
||||
name: 'format',
|
||||
description: 'Format to build to',
|
||||
values: ['website', 'json', 'ebook'],
|
||||
defaults: 'website'
|
||||
};
|
||||
|
||||
var FORMATS = {
|
||||
json: JSONOutput,
|
||||
website: WebsiteOutput,
|
||||
ebook: EBookOutput
|
||||
};
|
||||
|
||||
// Commands which is processing a book
|
||||
// the root of the book is the first argument (or current directory)
|
||||
function bookCmd(fn) {
|
||||
return function(args, kwargs) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
var book = new Book({
|
||||
fs: nodeFS,
|
||||
root: input,
|
||||
logLevel: kwargs.log
|
||||
});
|
||||
|
||||
return fn(book, args.slice(1), kwargs);
|
||||
};
|
||||
}
|
||||
|
||||
// Commands which is working on a Output instance
|
||||
function outputCmd(fn) {
|
||||
return bookCmd(function(book, args, kwargs) {
|
||||
var Out = FORMATS[kwargs.format];
|
||||
var outputFolder = undefined;
|
||||
|
||||
// Set output folder
|
||||
if (args[0]) {
|
||||
outputFolder = path.resolve(process.cwd(), args[0]);
|
||||
}
|
||||
|
||||
return fn(new Out(book, {
|
||||
root: outputFolder
|
||||
}), args);
|
||||
});
|
||||
}
|
||||
|
||||
// Command to generate an ebook
|
||||
function ebookCmd(format) {
|
||||
return {
|
||||
name: format + ' [book] [output] [file]',
|
||||
description: 'generates ebook '+format,
|
||||
options: [
|
||||
LOG_OPTION
|
||||
],
|
||||
exec: bookCmd(function(book, args, kwargs) {
|
||||
return fs.tmpDir()
|
||||
.then(function(dir) {
|
||||
var ext = '.'+format;
|
||||
var outputFile = path.resolve(process.cwd(), args[0] || ('book' + ext));
|
||||
var output = new EBookOutput(book, {
|
||||
root: dir,
|
||||
format: format
|
||||
});
|
||||
|
||||
return output.book.parse()
|
||||
.then(function() {
|
||||
return output.generate();
|
||||
})
|
||||
|
||||
// Copy the ebook files
|
||||
.then(function() {
|
||||
if (output.book.isMultilingual()) {
|
||||
return Promise.serie(output.book.langs.list(), function(lang) {
|
||||
var _outputFile = path.join(
|
||||
path.dirname(outputFile),
|
||||
path.basename(outputFile, ext) + '_' + lang.id + ext
|
||||
);
|
||||
|
||||
return fs.copy(
|
||||
path.resolve(dir, lang.id, 'index' + ext),
|
||||
_outputFile
|
||||
);
|
||||
})
|
||||
.thenResolve(output.book.langs.count());
|
||||
} else {
|
||||
return fs.copy(
|
||||
path.resolve(dir, 'index' + ext),
|
||||
outputFile
|
||||
).thenResolve(1);
|
||||
}
|
||||
})
|
||||
.then(function(n) {
|
||||
output.book.log.info.ok(n+' file(s) generated');
|
||||
|
||||
output.book.log.info('cleaning up... ');
|
||||
return output.book.log.info.promise(fs.rmDir(dir));
|
||||
});
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nodeFS: nodeFS,
|
||||
bookCmd: bookCmd,
|
||||
outputCmd: outputCmd,
|
||||
ebookCmd: ebookCmd,
|
||||
|
||||
options: {
|
||||
log: LOG_OPTION,
|
||||
format: FORMAT_OPTION
|
||||
},
|
||||
|
||||
FORMATS: FORMATS
|
||||
};
|
||||
+11
-198
@@ -1,199 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
var buildEbook = require('./buildEbook');
|
||||
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var tinylr = require('tiny-lr');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var PluginsManager = require('../plugins');
|
||||
var Book = require('../book');
|
||||
|
||||
var helper = require('./helper');
|
||||
var Server = require('./server');
|
||||
var watch = require('./watch');
|
||||
|
||||
module.exports = {
|
||||
commands: [
|
||||
{
|
||||
name: 'init [book]',
|
||||
description: 'setup and create files for chapters',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: function(args) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
return Book.init(helper.nodeFS, input);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'parse [book]',
|
||||
description: 'parse and returns debug information for a book',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: helper.bookCmd(function(book) {
|
||||
return book.parse()
|
||||
.then(function() {
|
||||
book.log.info.ln('Book located in:', book.root);
|
||||
book.log.info.ln('');
|
||||
|
||||
if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path);
|
||||
|
||||
if (book.isMultilingual()) {
|
||||
book.log.info.ln('Multilingual book detected:', book.langs.path);
|
||||
} else {
|
||||
book.log.info.ln('Readme:', book.readme.path);
|
||||
book.log.info.ln('Summary:', book.summary.path);
|
||||
if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path);
|
||||
|
||||
book.log.info.ln('Pages:');
|
||||
_.each(book.pages, function(page) {
|
||||
book.log.info.ln('\t-', page.path);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
name: 'install [book]',
|
||||
description: 'install all plugins dependencies',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: helper.bookCmd(function(book, args) {
|
||||
var plugins = new PluginsManager(book);
|
||||
|
||||
return book.config.load()
|
||||
.then(function() {
|
||||
return plugins.install();
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
name: 'build [book] [output]',
|
||||
description: 'build a book',
|
||||
options: [
|
||||
helper.options.log,
|
||||
helper.options.format
|
||||
],
|
||||
exec: helper.outputCmd(function(output, args, kwargs) {
|
||||
return output.book.parse()
|
||||
.then(function() {
|
||||
return output.generate();
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
helper.ebookCmd('pdf'),
|
||||
helper.ebookCmd('epub'),
|
||||
helper.ebookCmd('mobi'),
|
||||
|
||||
{
|
||||
name: 'serve [book]',
|
||||
description: 'Build then serve a book from a directory',
|
||||
options: [
|
||||
{
|
||||
name: 'port',
|
||||
description: 'Port for server to listen on',
|
||||
defaults: 4000
|
||||
},
|
||||
{
|
||||
name: 'lrport',
|
||||
description: 'Port for livereload server to listen on',
|
||||
defaults: 35729
|
||||
},
|
||||
{
|
||||
name: 'watch',
|
||||
description: 'Enable/disable file watcher',
|
||||
defaults: true
|
||||
},
|
||||
helper.options.format,
|
||||
helper.options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
var server = new Server();
|
||||
|
||||
// Init livereload server
|
||||
var lrServer = tinylr({});
|
||||
var port = kwargs.port;
|
||||
var lrPath;
|
||||
|
||||
var generate = function() {
|
||||
|
||||
// Stop server if running
|
||||
if (server.isRunning()) console.log('Stopping server');
|
||||
return server.stop()
|
||||
|
||||
// Generate the book
|
||||
.then(function() {
|
||||
var book = new Book({
|
||||
fs: helper.nodeFS,
|
||||
root: input,
|
||||
logLevel: kwargs.log
|
||||
});
|
||||
|
||||
return book.parse()
|
||||
.then(function() {
|
||||
// Add livereload plugin
|
||||
book.config.set('plugins',
|
||||
book.config.get('plugins')
|
||||
.concat([
|
||||
{ name: 'livereload' }
|
||||
])
|
||||
);
|
||||
|
||||
var Out = helper.FORMATS[kwargs.format];
|
||||
var output = new Out(book);
|
||||
|
||||
return output.generate()
|
||||
.thenResolve(output);
|
||||
});
|
||||
})
|
||||
|
||||
// Start server and watch changes
|
||||
.then(function(output) {
|
||||
console.log();
|
||||
console.log('Starting server ...');
|
||||
return server.start(output.root(), port)
|
||||
.then(function() {
|
||||
console.log('Serving book on http://localhost:'+port);
|
||||
|
||||
if (lrPath) {
|
||||
// trigger livereload
|
||||
lrServer.changed({
|
||||
body: {
|
||||
files: [lrPath]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!kwargs.watch) return;
|
||||
|
||||
return watch(output.book.root)
|
||||
.then(function(filepath) {
|
||||
// set livereload path
|
||||
lrPath = filepath;
|
||||
console.log('Restart after change in file', filepath);
|
||||
console.log('');
|
||||
return generate();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
|
||||
.then(function() {
|
||||
console.log('Live reload server started on port:', kwargs.lrport);
|
||||
console.log('Press CTRL+C to quit ...');
|
||||
console.log('');
|
||||
return generate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
module.exports = [
|
||||
require('./build'),
|
||||
require('./serve'),
|
||||
require('./install'),
|
||||
require('./parse'),
|
||||
require('./init'),
|
||||
buildEbook('pdf'),
|
||||
buildEbook('epub'),
|
||||
buildEbook('mobi')
|
||||
];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
var path = require('path');
|
||||
|
||||
var options = require('./options');
|
||||
var initBook = require('../init');
|
||||
|
||||
module.exports = {
|
||||
name: 'init [book]',
|
||||
description: 'setup and create files for chapters',
|
||||
options: [
|
||||
options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var bookRoot = path.resolve(process.cwd(), args[0] || './');
|
||||
|
||||
return initBook(bookRoot);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
var options = require('./options');
|
||||
var getBook = require('./getBook');
|
||||
|
||||
var Parse = require('../parse');
|
||||
var Plugins = require('../plugins');
|
||||
|
||||
module.exports = {
|
||||
name: 'install [book]',
|
||||
description: 'install all plugins dependencies',
|
||||
options: [
|
||||
options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var book = getBook(args, kwargs);
|
||||
|
||||
return Parse.parseConfig(book)
|
||||
.then(function(resultBook) {
|
||||
return Plugins.installPlugins(resultBook);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
var Logger = require('../utils/logger');
|
||||
|
||||
var logOptions = {
|
||||
name: 'log',
|
||||
description: 'Minimum log level to display',
|
||||
values: Logger.LEVELS
|
||||
.keySeq()
|
||||
.map(function(s) {
|
||||
return s.toLowerCase();
|
||||
}).toJS(),
|
||||
defaults: 'info'
|
||||
};
|
||||
|
||||
var formatOption = {
|
||||
name: 'format',
|
||||
description: 'Format to build to',
|
||||
values: ['website', 'json', 'ebook'],
|
||||
defaults: 'website'
|
||||
};
|
||||
|
||||
var timingOption = {
|
||||
name: 'timing',
|
||||
description: 'Print timing debug information',
|
||||
defaults: false
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
log: logOptions,
|
||||
format: formatOption,
|
||||
timing: timingOption
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
var options = require('./options');
|
||||
var getBook = require('./getBook');
|
||||
|
||||
var Parse = require('../parse');
|
||||
|
||||
function printBook(book) {
|
||||
var logger = book.getLogger();
|
||||
|
||||
var config = book.getConfig();
|
||||
var configFile = config.getFile();
|
||||
|
||||
var summary = book.getSummary();
|
||||
var summaryFile = summary.getFile();
|
||||
|
||||
var readme = book.getReadme();
|
||||
var readmeFile = readme.getFile();
|
||||
|
||||
var glossary = book.getGlossary();
|
||||
var glossaryFile = glossary.getFile();
|
||||
|
||||
if (configFile.exists()) {
|
||||
logger.info.ln('Configuration file is', configFile.getPath());
|
||||
}
|
||||
|
||||
if (readmeFile.exists()) {
|
||||
logger.info.ln('Introduction file is', readmeFile.getPath());
|
||||
}
|
||||
|
||||
if (glossaryFile.exists()) {
|
||||
logger.info.ln('Glossary file is', glossaryFile.getPath());
|
||||
}
|
||||
|
||||
if (summaryFile.exists()) {
|
||||
logger.info.ln('Table of Contents file is', summaryFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
function printMultingualBook(book) {
|
||||
var logger = book.getLogger();
|
||||
var languages = book.getLanguages();
|
||||
var books = book.getBooks();
|
||||
|
||||
logger.info.ln(languages.size + ' languages');
|
||||
|
||||
languages.forEach(function(lang) {
|
||||
logger.info.ln('Language:', lang.getTitle());
|
||||
printBook(books.get(lang.getID()));
|
||||
logger.info.ln('');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'parse [book]',
|
||||
description: 'parse and print debug information about a book',
|
||||
options: [
|
||||
options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var book = getBook(args, kwargs);
|
||||
var logger = book.getLogger();
|
||||
|
||||
return Parse.parseBook(book)
|
||||
.then(function(resultBook) {
|
||||
var rootFolder = book.getRoot();
|
||||
var contentFolder = book.getContentRoot();
|
||||
|
||||
logger.info.ln('Book located in:', rootFolder);
|
||||
if (contentFolder != rootFolder) {
|
||||
logger.info.ln('Content located in:', contentFolder);
|
||||
}
|
||||
|
||||
if (resultBook.isMultilingual()) {
|
||||
printMultingualBook(resultBook);
|
||||
} else {
|
||||
printBook(resultBook);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
var tinylr = require('tiny-lr');
|
||||
|
||||
var Parse = require('../parse');
|
||||
var Output = require('../output');
|
||||
var ConfigModifier = require('../modifiers').Config;
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
var options = require('./options');
|
||||
var getBook = require('./getBook');
|
||||
var getOutputFolder = require('./getOutputFolder');
|
||||
var Server = require('./server');
|
||||
var watch = require('./watch');
|
||||
|
||||
var server, lrServer, lrPath;
|
||||
|
||||
function waitForCtrlC() {
|
||||
var d = Promise.defer();
|
||||
|
||||
process.on('SIGINT', function() {
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
|
||||
function generateBook(args, kwargs) {
|
||||
var port = kwargs.port;
|
||||
var outputFolder = getOutputFolder(args);
|
||||
var book = getBook(args, kwargs);
|
||||
var Generator = Output.getGenerator(kwargs.format);
|
||||
|
||||
var hasWatch = kwargs['watch'];
|
||||
var hasLiveReloading = kwargs['live'];
|
||||
|
||||
// Stop server if running
|
||||
if (server.isRunning()) console.log('Stopping server');
|
||||
|
||||
return server.stop()
|
||||
.then(function() {
|
||||
return Parse.parseBook(book)
|
||||
.then(function(resultBook) {
|
||||
if (hasLiveReloading) {
|
||||
// Enable livereload plugin
|
||||
resultBook = ConfigModifier.addPlugin(resultBook, 'livereload');
|
||||
}
|
||||
|
||||
return Output.generate(Generator, resultBook, {
|
||||
root: outputFolder
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
console.log();
|
||||
console.log('Starting server ...');
|
||||
return server.start(outputFolder, port);
|
||||
})
|
||||
.then(function() {
|
||||
console.log('Serving book on http://localhost:'+port);
|
||||
|
||||
if (lrPath && hasLiveReloading) {
|
||||
// trigger livereload
|
||||
lrServer.changed({
|
||||
body: {
|
||||
files: [lrPath]
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
if (!hasWatch) {
|
||||
return waitForCtrlC();
|
||||
}
|
||||
|
||||
return watch(book.getRoot())
|
||||
.then(function(filepath) {
|
||||
// set livereload path
|
||||
lrPath = filepath;
|
||||
console.log('Restart after change in file', filepath);
|
||||
console.log('');
|
||||
return generateBook(args, kwargs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'serve [book] [output]',
|
||||
description: 'serve the book as a website for testing',
|
||||
options: [
|
||||
{
|
||||
name: 'port',
|
||||
description: 'Port for server to listen on',
|
||||
defaults: 4000
|
||||
},
|
||||
{
|
||||
name: 'lrport',
|
||||
description: 'Port for livereload server to listen on',
|
||||
defaults: 35729
|
||||
},
|
||||
{
|
||||
name: 'watch',
|
||||
description: 'Enable file watcher and live reloading',
|
||||
defaults: true
|
||||
},
|
||||
{
|
||||
name: 'live',
|
||||
description: 'Enable live reloading',
|
||||
defaults: true
|
||||
},
|
||||
options.log,
|
||||
options.format
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
server = new Server();
|
||||
var hasWatch = kwargs['watch'];
|
||||
var hasLiveReloading = kwargs['live'];
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
if (!hasWatch || !hasLiveReloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
lrServer = tinylr({});
|
||||
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
|
||||
.then(function() {
|
||||
console.log('Live reload server started on port:', kwargs.lrport);
|
||||
console.log('Press CTRL+C to quit ...');
|
||||
console.log('');
|
||||
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
return generateBook(args, kwargs);
|
||||
});
|
||||
}
|
||||
};
|
||||
+17
-4
@@ -6,20 +6,28 @@ var url = require('url');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
var Server = function() {
|
||||
function Server() {
|
||||
this.running = null;
|
||||
this.dir = null;
|
||||
this.port = 0;
|
||||
this.sockets = [];
|
||||
};
|
||||
}
|
||||
util.inherits(Server, events.EventEmitter);
|
||||
|
||||
// Return true if the server is running
|
||||
/**
|
||||
Return true if the server is running
|
||||
|
||||
@return {Boolean}
|
||||
*/
|
||||
Server.prototype.isRunning = function() {
|
||||
return !!this.running;
|
||||
};
|
||||
|
||||
// Stop the server
|
||||
/**
|
||||
Stop the server
|
||||
|
||||
@return {Promise}
|
||||
*/
|
||||
Server.prototype.stop = function() {
|
||||
var that = this;
|
||||
if (!this.isRunning()) return Promise();
|
||||
@@ -40,6 +48,11 @@ Server.prototype.stop = function() {
|
||||
return d.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
Start the server
|
||||
|
||||
@return {Promise}
|
||||
*/
|
||||
Server.prototype.start = function(dir, port) {
|
||||
var that = this, pre = Promise();
|
||||
port = port || 8004;
|
||||
|
||||
+8
-4
@@ -1,21 +1,25 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var chokidar = require('chokidar');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var parsers = require('../parsers');
|
||||
|
||||
// Watch a folder and resolve promise once a file is modified
|
||||
/**
|
||||
Watch a folder and resolve promise once a file is modified
|
||||
|
||||
@param {String} dir
|
||||
@return {Promise}
|
||||
*/
|
||||
function watch(dir) {
|
||||
var d = Promise.defer();
|
||||
dir = path.resolve(dir);
|
||||
|
||||
var toWatch = [
|
||||
'book.json', 'book.js'
|
||||
'book.json', 'book.js', '_layouts/**'
|
||||
];
|
||||
|
||||
// Watch all parsable files
|
||||
_.each(parsers.extensions, function(ext) {
|
||||
parsers.extensions.forEach(function(ext) {
|
||||
toWatch.push('**/*'+ext);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var semver = require('semver');
|
||||
|
||||
var gitbook = require('../gitbook');
|
||||
var Promise = require('../utils/promise');
|
||||
var validator = require('./validator');
|
||||
var plugins = require('./plugins');
|
||||
|
||||
// Config files to tested (sorted)
|
||||
var CONFIG_FILES = [
|
||||
'book.js',
|
||||
'book.json'
|
||||
];
|
||||
|
||||
/*
|
||||
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
|
||||
*/
|
||||
|
||||
function Config(book, baseConfig) {
|
||||
this.book = book;
|
||||
this.fs = book.fs;
|
||||
this.log = book.log;
|
||||
this.path = '';
|
||||
|
||||
this.baseConfig = baseConfig || {};
|
||||
this.replace({});
|
||||
}
|
||||
|
||||
// Load configuration of the book
|
||||
// and verify that the configuration is satisfying
|
||||
Config.prototype.load = function() {
|
||||
var that = this;
|
||||
var isLanguageBook = this.book.isLanguageBook();
|
||||
|
||||
// Try all potential configuration file
|
||||
return Promise.some(CONFIG_FILES, function(filename) {
|
||||
that.log.debug.ln('try loading configuration from', filename);
|
||||
|
||||
return that.fs.loadAsObject(that.book.resolve(filename))
|
||||
.then(function(_config) {
|
||||
that.log.debug.ln('configuration loaded from', filename);
|
||||
|
||||
that.path = filename;
|
||||
return that.replace(_config);
|
||||
})
|
||||
.fail(function(err) {
|
||||
if (err.code != 'MODULE_NOT_FOUND') throw(err);
|
||||
else return Promise(false);
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
if (!isLanguageBook) {
|
||||
if (!gitbook.satisfies(that.options.gitbook)) {
|
||||
throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
|
||||
}
|
||||
if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(gitbook.version, 'patch'), that.options.gitbook)) {
|
||||
that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate');
|
||||
}
|
||||
|
||||
that.options.plugins = plugins.toList(that.options.plugins);
|
||||
} else {
|
||||
// Multilingual book should inherits the plugins list from parent
|
||||
that.options.plugins = that.book.parent.config.get('plugins');
|
||||
}
|
||||
|
||||
that.options.gitbook = gitbook.version;
|
||||
});
|
||||
};
|
||||
|
||||
// Replace the whole configuration
|
||||
Config.prototype.replace = function(options) {
|
||||
var that = this;
|
||||
|
||||
// Extend base config
|
||||
options = _.defaults(_.cloneDeep(options), this.baseConfig);
|
||||
|
||||
// Validate the config
|
||||
this.options = validator.validate(options);
|
||||
|
||||
// options.input == book.root
|
||||
Object.defineProperty(this.options, 'input', {
|
||||
get: function () {
|
||||
return that.book.root;
|
||||
}
|
||||
});
|
||||
|
||||
// options.originalInput == book.parent.root
|
||||
Object.defineProperty(this.options, 'originalInput', {
|
||||
get: function () {
|
||||
return that.book.parent? that.book.parent.root : undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Return true if book has a configuration file
|
||||
Config.prototype.exists = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Return path to a structure file
|
||||
// Strip the extension by default
|
||||
Config.prototype.getStructure = function(name, dontStripExt) {
|
||||
var filename = this.options.structure[name];
|
||||
if (dontStripExt) return filename;
|
||||
|
||||
filename = filename.split('.').slice(0, -1).join('.');
|
||||
return filename;
|
||||
};
|
||||
|
||||
// Return a configuration using a key and a default value
|
||||
Config.prototype.get = function(key, def) {
|
||||
return _.get(this.options, key, def);
|
||||
};
|
||||
|
||||
// Update a configuration
|
||||
Config.prototype.set = function(key, value) {
|
||||
return _.set(this.options, key, value);
|
||||
};
|
||||
|
||||
// Return a dump of the configuration
|
||||
Config.prototype.dump = function() {
|
||||
return _.cloneDeep(this.options);
|
||||
};
|
||||
|
||||
// Return templating context
|
||||
Config.prototype.getContext = function() {
|
||||
return {
|
||||
config: this.book.config.dump()
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Config;
|
||||
@@ -1,67 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
// Default plugins added to each books
|
||||
var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings', 'theme-default'];
|
||||
|
||||
// Return true if a plugin is a default plugin
|
||||
function isDefaultPlugin(name, version) {
|
||||
return _.contains(DEFAULT_PLUGINS, name);
|
||||
}
|
||||
|
||||
// Normalize a list of plugins to use
|
||||
function normalizePluginsList(plugins) {
|
||||
// Normalize list to an array
|
||||
plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
|
||||
|
||||
// Remove empty parts
|
||||
plugins = _.compact(plugins);
|
||||
|
||||
// Divide as {name, version} to handle format like 'myplugin@1.0.0'
|
||||
plugins = _.map(plugins, function(plugin) {
|
||||
if (plugin.name) return plugin;
|
||||
|
||||
var parts = plugin.split('@');
|
||||
var name = parts[0];
|
||||
var version = parts[1];
|
||||
return {
|
||||
'name': name,
|
||||
'version': version // optional
|
||||
};
|
||||
});
|
||||
|
||||
// List plugins to remove
|
||||
var toremove = _.chain(plugins)
|
||||
.filter(function(plugin) {
|
||||
return plugin.name.length > 0 && plugin.name[0] == '-';
|
||||
})
|
||||
.map(function(plugin) {
|
||||
return plugin.name.slice(1);
|
||||
})
|
||||
.value();
|
||||
|
||||
// Merge with defaults
|
||||
_.each(DEFAULT_PLUGINS, function(plugin) {
|
||||
if (_.find(plugins, { name: plugin })) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.push({
|
||||
'name': plugin
|
||||
});
|
||||
});
|
||||
// Remove plugin that start with '-'
|
||||
plugins = _.filter(plugins, function(plugin) {
|
||||
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
|
||||
});
|
||||
|
||||
// Remove duplicates
|
||||
plugins = _.uniq(plugins, 'name');
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isDefaultPlugin: isDefaultPlugin,
|
||||
toList: normalizePluginsList
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
var Immutable = require('immutable');
|
||||
var jsonSchemaDefaults = require('json-schema-defaults');
|
||||
|
||||
var schema = require('./configSchema');
|
||||
|
||||
module.exports = Immutable.fromJS(jsonSchemaDefaults(schema));
|
||||
@@ -0,0 +1,5 @@
|
||||
// Configuration files to test (sorted)
|
||||
module.exports = [
|
||||
'book.js',
|
||||
'book.json'
|
||||
];
|
||||
@@ -0,0 +1,51 @@
|
||||
var Immutable = require('immutable');
|
||||
var TemplateBlock = require('../models/templateBlock');
|
||||
|
||||
module.exports = Immutable.Map({
|
||||
html: TemplateBlock({
|
||||
name: 'html',
|
||||
process: function(blk) {
|
||||
return blk;
|
||||
}
|
||||
}),
|
||||
|
||||
code: TemplateBlock({
|
||||
name: 'code',
|
||||
process: function(blk) {
|
||||
return {
|
||||
html: false,
|
||||
body: blk.body
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
markdown: TemplateBlock({
|
||||
name: 'markdown',
|
||||
process: function(blk) {
|
||||
return this.book.renderInline('markdown', blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
asciidoc: TemplateBlock({
|
||||
name: 'asciidoc',
|
||||
process: function(blk) {
|
||||
return this.book.renderInline('asciidoc', blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
markup: TemplateBlock({
|
||||
name: 'markup',
|
||||
process: function(blk) {
|
||||
return this.book.renderInline(this.ctx.file.type, blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
var Immutable = require('immutable');
|
||||
var moment = require('moment');
|
||||
|
||||
|
||||
module.exports = {
|
||||
module.exports = Immutable.Map({
|
||||
// Format a date
|
||||
// ex: 'MMMM Do YYYY, h:mm:ss a
|
||||
date: function(time, format) {
|
||||
@@ -12,4 +12,4 @@ module.exports = {
|
||||
dateFromNow: function(time) {
|
||||
return moment(time).fromNow();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
var Immutable = require('immutable');
|
||||
|
||||
/*
|
||||
List of default plugins for all books,
|
||||
default plugins should be installed in node dependencies of GitBook
|
||||
*/
|
||||
module.exports = Immutable.List([
|
||||
'highlight',
|
||||
'search',
|
||||
'lunr',
|
||||
'sharing',
|
||||
'fontsettings',
|
||||
'theme-default'
|
||||
]);
|
||||
@@ -0,0 +1,6 @@
|
||||
// Files containing ignore pattner (sorted by priority)
|
||||
module.exports = [
|
||||
'.ignore',
|
||||
'.gitignore',
|
||||
'.bookignore'
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
module.exports = '_assets';
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = [
|
||||
'init',
|
||||
'finish',
|
||||
'finish:before',
|
||||
'config',
|
||||
'page',
|
||||
'page:before'
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/*
|
||||
All GitBook plugins are NPM packages starting with this prefix.
|
||||
*/
|
||||
module.exports = 'gitbook-plugin-';
|
||||
@@ -0,0 +1,6 @@
|
||||
var Immutable = require('immutable');
|
||||
|
||||
module.exports = Immutable.List([
|
||||
'js',
|
||||
'css'
|
||||
]);
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
module.exports = '_layouts';
|
||||
@@ -0,0 +1,83 @@
|
||||
jest.autoMockOff();
|
||||
|
||||
describe('MockFS', function() {
|
||||
var createMockFS = require('../mock');
|
||||
var fs = createMockFS({
|
||||
'README.md': 'Hello World',
|
||||
'SUMMARY.md': '# Summary',
|
||||
'folder': {
|
||||
'test.md': 'Cool',
|
||||
'folder2': {
|
||||
'hello.md': 'Hello',
|
||||
'world.md': 'World'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('exists', function() {
|
||||
pit('must return true for a file', function() {
|
||||
return fs.exists('README.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
pit('must return false for a non existing file', function() {
|
||||
return fs.exists('README_NOTEXISTS.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
pit('must return true for a directory', function() {
|
||||
return fs.exists('folder')
|
||||
.then(function(result) {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
pit('must return true for a deep file', function() {
|
||||
return fs.exists('folder/test.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
pit('must return true for a deep file (2)', function() {
|
||||
return fs.exists('folder/folder2/hello.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readAsString', function() {
|
||||
pit('must return content for a file', function() {
|
||||
return fs.readAsString('README.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBe('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
pit('must return content for a deep file', function() {
|
||||
return fs.readAsString('folder/test.md')
|
||||
.then(function(result) {
|
||||
expect(result).toBe('Cool');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readDir', function() {
|
||||
pit('must return content for a directory', function() {
|
||||
return fs.readDir('./')
|
||||
.then(function(files) {
|
||||
expect(files.size).toBe(3);
|
||||
expect(files.includes('README.md')).toBeTruthy();
|
||||
expect(files.includes('SUMMARY.md')).toBeTruthy();
|
||||
expect(files.includes('folder/')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
/*
|
||||
A filesystem is an interface to read files
|
||||
GitBook can works with a virtual filesystem, for example in the browser.
|
||||
*/
|
||||
|
||||
// .readdir return files/folder as a list of string, folder ending with '/'
|
||||
function pathIsFolder(filename) {
|
||||
return _.last(filename) == '/' || _.last(filename) == '\\';
|
||||
}
|
||||
|
||||
|
||||
function FS() {
|
||||
|
||||
}
|
||||
|
||||
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
|
||||
FS.prototype.exists = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// Read a file and returns a promise with the content as a buffer
|
||||
FS.prototype.read = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// Read stat infos about a file
|
||||
FS.prototype.stat = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// List files/directories in a directory
|
||||
FS.prototype.readdir = function(folder) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
|
||||
// For optmization, it can be redefined:
|
||||
|
||||
// List files in a directory
|
||||
FS.prototype.listFiles = function(folder) {
|
||||
return this.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.reject(files, pathIsFolder);
|
||||
});
|
||||
};
|
||||
|
||||
// List all files in the fs
|
||||
FS.prototype.listAllFiles = function(folder) {
|
||||
var that = this;
|
||||
|
||||
return this.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.reduce(files, function(prev, file) {
|
||||
return prev.then(function(output) {
|
||||
var isDirectory = pathIsFolder(file);
|
||||
|
||||
if (!isDirectory) {
|
||||
output.push(file);
|
||||
return output;
|
||||
} else {
|
||||
return that.listAllFiles(path.join(folder, file))
|
||||
.then(function(files) {
|
||||
return output.concat(_.map(files, function(_file) {
|
||||
return path.join(file, _file);
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}, Promise([]));
|
||||
});
|
||||
};
|
||||
|
||||
// Read a file as a string (utf-8)
|
||||
FS.prototype.readAsString = function(filename) {
|
||||
return this.read(filename)
|
||||
.then(function(buf) {
|
||||
return buf.toString('utf-8');
|
||||
});
|
||||
};
|
||||
|
||||
// Find a file in a folder (case incensitive)
|
||||
// Return the real filename
|
||||
FS.prototype.findFile = function findFile(root, filename) {
|
||||
return this.listFiles(root)
|
||||
.then(function(files) {
|
||||
return _.find(files, function(file) {
|
||||
return (file.toLowerCase() == filename.toLowerCase());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Load a JSON file
|
||||
// By default, fs only supports JSON
|
||||
FS.prototype.loadAsObject = function(filename) {
|
||||
return this.readAsString(filename)
|
||||
.then(function(str) {
|
||||
return JSON.parse(str);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = FS;
|
||||
@@ -0,0 +1,95 @@
|
||||
var path = require('path');
|
||||
var is = require('is');
|
||||
var Buffer = require('buffer').Buffer;
|
||||
var Immutable = require('immutable');
|
||||
|
||||
var FS = require('../models/fs');
|
||||
var error = require('../utils/error');
|
||||
|
||||
/**
|
||||
Create a fake filesystem for unit testing GitBook.
|
||||
|
||||
@param {Map<String:String|Map>}
|
||||
*/
|
||||
function createMockFS(files) {
|
||||
files = Immutable.fromJS(files);
|
||||
var mtime = new Date();
|
||||
|
||||
function getFile(filePath) {
|
||||
var parts = path.normalize(filePath).split(path.sep);
|
||||
return parts.reduce(function(list, part, i) {
|
||||
if (!list) return null;
|
||||
|
||||
var file;
|
||||
|
||||
if (!part || part === '.') file = list;
|
||||
else file = list.get(part);
|
||||
|
||||
if (!file) return null;
|
||||
|
||||
if (is.string(file)) {
|
||||
if (i === (parts.length - 1)) return file;
|
||||
else return null;
|
||||
}
|
||||
|
||||
return file;
|
||||
}, files);
|
||||
}
|
||||
|
||||
function fsExists(filePath) {
|
||||
return Boolean(getFile(filePath) !== null);
|
||||
}
|
||||
|
||||
function fsReadFile(filePath) {
|
||||
var file = getFile(filePath);
|
||||
if (!is.string(file)) {
|
||||
throw error.FileNotFoundError({
|
||||
filename: filePath
|
||||
});
|
||||
}
|
||||
|
||||
return new Buffer(file, 'utf8');
|
||||
}
|
||||
|
||||
function fsStatFile(filePath) {
|
||||
var file = getFile(filePath);
|
||||
if (!file) {
|
||||
throw error.FileNotFoundError({
|
||||
filename: filePath
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
mtime: mtime
|
||||
};
|
||||
}
|
||||
|
||||
function fsReadDir(filePath) {
|
||||
var dir = getFile(filePath);
|
||||
if (!dir || is.string(dir)) {
|
||||
throw error.FileNotFoundError({
|
||||
filename: filePath
|
||||
});
|
||||
}
|
||||
|
||||
return dir
|
||||
.map(function(content, name) {
|
||||
if (!is.string(content)) {
|
||||
name = name + '/';
|
||||
}
|
||||
|
||||
return name;
|
||||
})
|
||||
.valueSeq();
|
||||
}
|
||||
|
||||
return FS.create({
|
||||
root: '',
|
||||
fsExists: fsExists,
|
||||
fsReadFile: fsReadFile,
|
||||
fsStatFile: fsStatFile,
|
||||
fsReadDir: fsReadDir
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = createMockFS;
|
||||
+27
-51
@@ -1,36 +1,16 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var path = require('path');
|
||||
var Immutable = require('immutable');
|
||||
var fresh = require('fresh-require');
|
||||
|
||||
var fs = require('../utils/fs');
|
||||
var Promise = require('../utils/promise');
|
||||
var BaseFS = require('./');
|
||||
var FS = require('../models/fs');
|
||||
|
||||
function NodeFS() {
|
||||
BaseFS.call(this);
|
||||
}
|
||||
util.inherits(NodeFS, BaseFS);
|
||||
|
||||
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
|
||||
NodeFS.prototype.exists = function(filename) {
|
||||
return fs.exists(filename);
|
||||
};
|
||||
|
||||
// Read a file and returns a promise with the content as a buffer
|
||||
NodeFS.prototype.read = function(filename) {
|
||||
return fs.readFile(filename);
|
||||
};
|
||||
|
||||
// Read stat infos about a file
|
||||
NodeFS.prototype.stat = function(filename) {
|
||||
return fs.stat(filename);
|
||||
};
|
||||
|
||||
// List files in a directory
|
||||
NodeFS.prototype.readdir = function(folder) {
|
||||
function fsReadDir(folder) {
|
||||
return fs.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.chain(files)
|
||||
files = Immutable.List(files);
|
||||
|
||||
return files
|
||||
.map(function(file) {
|
||||
if (file == '.' || file == '..') return;
|
||||
|
||||
@@ -38,29 +18,25 @@ NodeFS.prototype.readdir = function(folder) {
|
||||
if (stat.isDirectory()) file = file + path.sep;
|
||||
return file;
|
||||
})
|
||||
.compact()
|
||||
.value();
|
||||
.filter(function(file) {
|
||||
return Boolean(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fsLoadObject(filename) {
|
||||
return fresh(filename, require);
|
||||
}
|
||||
|
||||
module.exports = function createNodeFS(root) {
|
||||
return FS.create({
|
||||
root: root,
|
||||
|
||||
fsExists: fs.exists,
|
||||
fsReadFile: fs.readFile,
|
||||
fsStatFile: fs.stat,
|
||||
fsReadDir: fsReadDir,
|
||||
fsLoadObject: fsLoadObject,
|
||||
fsReadAsStream: fs.readStream
|
||||
});
|
||||
};
|
||||
|
||||
// Load a JSON/JS file
|
||||
NodeFS.prototype.loadAsObject = function(filename) {
|
||||
return Promise()
|
||||
.then(function() {
|
||||
var jsFile;
|
||||
|
||||
try {
|
||||
jsFile = require.resolve(filename);
|
||||
|
||||
// Invalidate node.js cache for livreloading
|
||||
delete require.cache[jsFile];
|
||||
|
||||
return require(jsFile);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = NodeFS;
|
||||
|
||||
+8
-13
@@ -6,8 +6,13 @@ var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
|
||||
|
||||
var START_TIME = new Date();
|
||||
|
||||
// Verify that this gitbook version satisfies a requirement
|
||||
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
|
||||
/**
|
||||
Verify that this gitbook version satisfies a requirement
|
||||
We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
|
||||
|
||||
@param {String} condition
|
||||
@return {Boolean}
|
||||
*/
|
||||
function satisfies(condition) {
|
||||
// Test with real version
|
||||
if (semver.satisfies(VERSION, condition)) return true;
|
||||
@@ -16,18 +21,8 @@ function satisfies(condition) {
|
||||
return semver.satisfies(VERSION_STABLE, condition);
|
||||
}
|
||||
|
||||
// Return templating/json context for gitbook itself
|
||||
function getContext() {
|
||||
return {
|
||||
gitbook: {
|
||||
version: pkg.version,
|
||||
time: START_TIME
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
version: pkg.version,
|
||||
satisfies: satisfies,
|
||||
getContext: getContext
|
||||
START_TIME: START_TIME
|
||||
};
|
||||
|
||||
+9
-6
@@ -1,7 +1,10 @@
|
||||
var Book = require('./book');
|
||||
var cli = require('./cli');
|
||||
var extend = require('extend');
|
||||
|
||||
module.exports = {
|
||||
Book: Book,
|
||||
commands: cli.commands
|
||||
};
|
||||
var common = require('./browser');
|
||||
|
||||
module.exports = extend({
|
||||
initBook: require('./init'),
|
||||
createNodeFS: require('./fs/node'),
|
||||
Output: require('./output'),
|
||||
commands: require('./cli')
|
||||
}, common);
|
||||
|
||||
+68
-52
@@ -1,66 +1,82 @@
|
||||
var path = require('path');
|
||||
|
||||
var createNodeFS = require('./fs/node');
|
||||
var fs = require('./utils/fs');
|
||||
var Promise = require('./utils/promise');
|
||||
var File = require('./models/file');
|
||||
var Readme = require('./models/readme');
|
||||
var Book = require('./models/book');
|
||||
var Parse = require('./parse');
|
||||
|
||||
// Initialize folder structure for a book
|
||||
// Read SUMMARY to created the right chapter
|
||||
function initBook(book) {
|
||||
var extensionToUse = '.md';
|
||||
/**
|
||||
Initialize folder structure for a book
|
||||
Read SUMMARY to created the right chapter
|
||||
|
||||
book.log.info.ln('init book at', book.root);
|
||||
return fs.mkdirp(book.root)
|
||||
@param {Book}
|
||||
@param {String}
|
||||
@return {Promise}
|
||||
*/
|
||||
function initBook(rootFolder) {
|
||||
var extension = '.md';
|
||||
|
||||
return fs.mkdirp(rootFolder)
|
||||
|
||||
// Parse the summary and readme
|
||||
.then(function() {
|
||||
return book.config.load();
|
||||
})
|
||||
.then(function() {
|
||||
book.log.info.ln('detect structure from SUMMARY (if it exists)');
|
||||
return book.summary.load();
|
||||
})
|
||||
.then(function() {
|
||||
var summary = book.summary.path || 'SUMMARY.md';
|
||||
var articles = book.summary.flatten();
|
||||
var fs = createNodeFS(rootFolder);
|
||||
var book = Book.createForFS(fs);
|
||||
|
||||
// Use extension of summary
|
||||
extensionToUse = path.extname(summary);
|
||||
return Parse.parseReadme(book)
|
||||
|
||||
// Readme doesn't have a path
|
||||
if (!articles[0].path) {
|
||||
articles[0].path = 'README' + extensionToUse;
|
||||
}
|
||||
|
||||
// Summary doesn't exists? create one
|
||||
if (!book.summary.path) {
|
||||
articles.push({
|
||||
title: 'Summary',
|
||||
path: 'SUMMARY'+extensionToUse
|
||||
});
|
||||
}
|
||||
|
||||
// Create files that don't exist
|
||||
return Promise.serie(articles, function(article) {
|
||||
if (!article.path) return;
|
||||
|
||||
var absolutePath = book.resolve(article.path);
|
||||
|
||||
return fs.exists(absolutePath)
|
||||
.then(function(exists) {
|
||||
if(exists) {
|
||||
book.log.info.ln('found', article.path);
|
||||
return;
|
||||
} else {
|
||||
book.log.info.ln('create', article.path);
|
||||
}
|
||||
|
||||
return fs.mkdirp(path.dirname(absolutePath))
|
||||
.then(function() {
|
||||
return fs.writeFile(absolutePath, '# '+article.title+'\n\n');
|
||||
});
|
||||
});
|
||||
// Setup default readme if doesn't found one
|
||||
.fail(function() {
|
||||
var readmeFile = File.createWithFilepath('README' + extension);
|
||||
var readme = Readme.create(readmeFile);
|
||||
return book.setReadme(readme);
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
book.log.info.ln('initialization is finished');
|
||||
.then(Parse.parseSummary)
|
||||
|
||||
.then(function(book) {
|
||||
var logger = book.getLogger();
|
||||
var summary = book.getSummary();
|
||||
var summaryFile = summary.getFile();
|
||||
var summaryFilename = summaryFile.getPath() || ('SUMMARY' + extension);
|
||||
|
||||
var articles = summary.getArticlesAsList();
|
||||
|
||||
// Write pages
|
||||
return Promise.forEach(articles, function(article) {
|
||||
var articlePath = article.getPath();
|
||||
var filePath = articlePath? path.join(rootFolder, articlePath) : null;
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fs.assertFile(filePath, function() {
|
||||
return fs.ensureFile(filePath)
|
||||
.then(function() {
|
||||
logger.info.ln('create', article.getPath());
|
||||
return fs.writeFile(filePath, '# ' + article.getTitle() + '\n\n');
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
// Write summary
|
||||
.then(function() {
|
||||
var filePath = path.join(rootFolder, summaryFilename);
|
||||
|
||||
return fs.ensureFile(filePath)
|
||||
.then(function() {
|
||||
logger.info.ln('create ' + path.basename(filePath));
|
||||
return fs.writeFile(filePath, summary.toText(extension));
|
||||
});
|
||||
})
|
||||
|
||||
// Log end
|
||||
.then(function() {
|
||||
logger.info.ln('initialization is finished');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
var extend = require('extend');
|
||||
|
||||
var gitbook = require('../gitbook');
|
||||
var encodeSummary = require('./encodeSummary');
|
||||
var encodeGlossary = require('./encodeGlossary');
|
||||
var encodeReadme = require('./encodeReadme');
|
||||
|
||||
/**
|
||||
Encode a book to JSON
|
||||
|
||||
@param {Book}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeBookToJson(book) {
|
||||
var config = book.getConfig();
|
||||
var language = book.getLanguage();
|
||||
|
||||
var variables = config.getValue('variables', {});
|
||||
|
||||
return {
|
||||
summary: encodeSummary(book.getSummary()),
|
||||
glossary: encodeGlossary(book.getGlossary()),
|
||||
readme: encodeReadme(book.getReadme()),
|
||||
config: book.getConfig().getValues().toJS(),
|
||||
gitbook: {
|
||||
version: gitbook.version,
|
||||
time: gitbook.START_TIME
|
||||
},
|
||||
book: extend({
|
||||
language: language? language : undefined
|
||||
}, variables.toJS())
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeBookToJson;
|
||||
@@ -0,0 +1,22 @@
|
||||
var encodeBook = require('./encodeBook');
|
||||
var encodePage = require('./encodePage');
|
||||
var encodeFile = require('./encodeFile');
|
||||
|
||||
/**
|
||||
Return a JSON representation of a book with a specific file
|
||||
|
||||
@param {Book} output
|
||||
@param {Page} page
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeBookWithPage(book, page) {
|
||||
var file = page.getFile();
|
||||
|
||||
var result = encodeBook(book);
|
||||
result.page = encodePage(page, book.getSummary());
|
||||
result.file = encodeFile(file);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodeBookWithPage;
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
/**
|
||||
Return a JSON representation of a file
|
||||
|
||||
@param {File} file
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeFileToJson(file) {
|
||||
var filePath = file.getPath();
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
mtime: file.getMTime(),
|
||||
type: file.getType()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeFileToJson;
|
||||
@@ -0,0 +1,21 @@
|
||||
var encodeFile = require('./encodeFile');
|
||||
var encodeGlossaryEntry = require('./encodeGlossaryEntry');
|
||||
|
||||
/**
|
||||
Encode a glossary to JSON
|
||||
|
||||
@param {Glossary}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeGlossary(glossary) {
|
||||
var file = glossary.getFile();
|
||||
var entries = glossary.getEntries();
|
||||
|
||||
return {
|
||||
file: encodeFile(file),
|
||||
entries: entries
|
||||
.map(encodeGlossaryEntry).toJS()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeGlossary;
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
/**
|
||||
Encode a SummaryArticle to JSON
|
||||
|
||||
@param {GlossaryEntry}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeGlossaryEntry(entry) {
|
||||
return {
|
||||
id: entry.getID(),
|
||||
name: entry.getName(),
|
||||
description: entry.getDescription()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeGlossaryEntry;
|
||||
@@ -0,0 +1,25 @@
|
||||
var encodeBook = require('./encodeBook');
|
||||
|
||||
/**
|
||||
Encode an output to JSON
|
||||
|
||||
@param {Output}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeOutputToJson(output) {
|
||||
var book = output.getBook();
|
||||
var generator = output.getGenerator();
|
||||
var options = output.getOptions();
|
||||
|
||||
var result = encodeBook(book);
|
||||
|
||||
result.output = {
|
||||
name: generator
|
||||
};
|
||||
|
||||
result.options = options.toJS();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodeOutputToJson;
|
||||
@@ -0,0 +1,39 @@
|
||||
var encodeSummaryArticle = require('./encodeSummaryArticle');
|
||||
|
||||
/**
|
||||
Return a JSON representation of a page
|
||||
|
||||
@param {Page} page
|
||||
@param {Summary} summary
|
||||
@return {Object}
|
||||
*/
|
||||
function encodePage(page, summary) {
|
||||
var file = page.getFile();
|
||||
var attributes = page.getAttributes();
|
||||
var article = summary.getByPath(file.getPath());
|
||||
|
||||
var result = attributes.toJS();
|
||||
|
||||
if (article) {
|
||||
result.title = article.getTitle();
|
||||
result.level = article.getLevel();
|
||||
result.depth = article.getDepth();
|
||||
|
||||
var nextArticle = summary.getNextArticle(article);
|
||||
if (nextArticle) {
|
||||
result.next = encodeSummaryArticle(nextArticle);
|
||||
}
|
||||
|
||||
var prevArticle = summary.getPrevArticle(article);
|
||||
if (prevArticle) {
|
||||
result.previous = encodeSummaryArticle(prevArticle);
|
||||
}
|
||||
}
|
||||
|
||||
result.content = page.getContent();
|
||||
result.dir = page.getDir();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = encodePage;
|
||||
@@ -0,0 +1,17 @@
|
||||
var encodeFile = require('./encodeFile');
|
||||
|
||||
/**
|
||||
Encode a readme to JSON
|
||||
|
||||
@param {Readme}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeReadme(readme) {
|
||||
var file = readme.getFile();
|
||||
|
||||
return {
|
||||
file: encodeFile(file)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeReadme;
|
||||
@@ -0,0 +1,20 @@
|
||||
var encodeFile = require('./encodeFile');
|
||||
var encodeSummaryPart = require('./encodeSummaryPart');
|
||||
|
||||
/**
|
||||
Encode a summary to JSON
|
||||
|
||||
@param {Summary}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeSummary(summary) {
|
||||
var file = summary.getFile();
|
||||
var parts = summary.getParts();
|
||||
|
||||
return {
|
||||
file: encodeFile(file),
|
||||
parts: parts.map(encodeSummaryPart).toJS()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeSummary;
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
/**
|
||||
Encode a SummaryArticle to JSON
|
||||
|
||||
@param {SummaryArticle}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeSummaryArticle(article, recursive) {
|
||||
var articles = undefined;
|
||||
if (recursive !== false) {
|
||||
articles = article.getArticles()
|
||||
.map(encodeSummaryArticle)
|
||||
.toJS();
|
||||
}
|
||||
|
||||
return {
|
||||
title: article.getTitle(),
|
||||
level: article.getLevel(),
|
||||
depth: article.getDepth(),
|
||||
anchor: article.getAnchor(),
|
||||
url: article.getUrl(),
|
||||
path: article.getPath(),
|
||||
ref: article.getRef(),
|
||||
articles: articles
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeSummaryArticle;
|
||||
@@ -0,0 +1,17 @@
|
||||
var encodeSummaryArticle = require('./encodeSummaryArticle');
|
||||
|
||||
/**
|
||||
Encode a SummaryPart to JSON
|
||||
|
||||
@param {SummaryPart}
|
||||
@return {Object}
|
||||
*/
|
||||
function encodeSummaryPart(part) {
|
||||
return {
|
||||
title: part.getTitle(),
|
||||
articles: part.getArticles()
|
||||
.map(encodeSummaryArticle).toJS()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = encodeSummaryPart;
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
module.exports = {
|
||||
encodeOutput: require('./encodeOutput'),
|
||||
encodeBookWithPage: require('./encodeBookWithPage'),
|
||||
encodeBook: require('./encodeBook'),
|
||||
encodeFile: require('./encodeFile'),
|
||||
encodePage: require('./encodePage'),
|
||||
encodeSummary: require('./encodeSummary'),
|
||||
encodeSummaryArticle: require('./encodeSummaryArticle'),
|
||||
encodeReadme: require('./encodeReadme')
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
jest.autoMockOff();
|
||||
|
||||
var Immutable = require('immutable');
|
||||
|
||||
describe('Config', function() {
|
||||
var Config = require('../config');
|
||||
|
||||
var config = Config.createWithValues({
|
||||
hello: {
|
||||
world: 1,
|
||||
test: 'Hello',
|
||||
isFalse: false
|
||||
}
|
||||
});
|
||||
|
||||
describe('getValue', function() {
|
||||
it('must return value as immutable', function() {
|
||||
var value = config.getValue('hello');
|
||||
expect(Immutable.Map.isMap(value)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('must return deep value', function() {
|
||||
var value = config.getValue('hello.world');
|
||||
expect(value).toBe(1);
|
||||
});
|
||||
|
||||
it('must return default value if non existant', function() {
|
||||
var value = config.getValue('hello.nonExistant', 'defaultValue');
|
||||
expect(value).toBe('defaultValue');
|
||||
});
|
||||
|
||||
it('must not return default value for falsy values', function() {
|
||||
var value = config.getValue('hello.isFalse', 'defaultValue');
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue', function() {
|
||||
it('must set value as immutable', function() {
|
||||
var testConfig = config.setValue('hello', {
|
||||
'cool': 1
|
||||
});
|
||||
var value = testConfig.getValue('hello');
|
||||
|
||||
expect(Immutable.Map.isMap(value)).toBeTruthy();
|
||||
expect(value.size).toBe(1);
|
||||
expect(value.has('cool')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('must set deep value', function() {
|
||||
var testConfig = config.setValue('hello.world', 2);
|
||||
var hello = testConfig.getValue('hello');
|
||||
var world = testConfig.getValue('hello.world');
|
||||
|
||||
expect(Immutable.Map.isMap(hello)).toBeTruthy();
|
||||
expect(hello.size).toBe(3);
|
||||
|
||||
expect(world).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
jest.autoMockOff();
|
||||
|
||||
describe('Glossary', function() {
|
||||
var File = require('../file');
|
||||
var Glossary = require('../glossary');
|
||||
var GlossaryEntry = require('../glossaryEntry');
|
||||
|
||||
var glossary = Glossary.createFromEntries(File(), [
|
||||
{
|
||||
name: 'Hello World',
|
||||
description: 'Awesome!'
|
||||
},
|
||||
{
|
||||
name: 'JavaScript',
|
||||
description: 'This is a cool language'
|
||||
}
|
||||
]);
|
||||
|
||||
describe('createFromEntries', function() {
|
||||
it('must add all entries', function() {
|
||||
var entries = glossary.getEntries();
|
||||
expect(entries.size).toBe(2);
|
||||
});
|
||||
|
||||
it('must add entries as GlossaryEntries', function() {
|
||||
var entries = glossary.getEntries();
|
||||
var entry = entries.get('hello-world');
|
||||
expect(entry instanceof GlossaryEntry).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toText', function() {
|
||||
pit('return as markdown', function() {
|
||||
return glossary.toText('.md')
|
||||
.then(function(text) {
|
||||
expect(text).toContain('# Glossary');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
jest.autoMockOff();
|
||||
|
||||
describe('GlossaryEntry', function() {
|
||||
var GlossaryEntry = require('../glossaryEntry');
|
||||
|
||||
describe('getID', function() {
|
||||
it('must return a normalized ID', function() {
|
||||
var entry = new GlossaryEntry({
|
||||
name: 'Hello World'
|
||||
});
|
||||
|
||||
expect(entry.getID()).toBe('hello-world');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
describe('Plugin', function() {
|
||||
var Plugin = require('../plugin');
|
||||
|
||||
describe('createFromString', function() {
|
||||
it('must parse name', function() {
|
||||
var plugin = Plugin.createFromString('hello');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('*');
|
||||
});
|
||||
|
||||
it('must parse version', function() {
|
||||
var plugin = Plugin.createFromString('hello@1.0.0');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLoaded', function() {
|
||||
it('must return false for empty plugin', function() {
|
||||
var plugin = Plugin.createFromString('hello');
|
||||
expect(plugin.isLoaded()).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
describe('PluginDependency', function() {
|
||||
var PluginDependency = require('../pluginDependency');
|
||||
|
||||
describe('createFromString', function() {
|
||||
it('must parse name', function() {
|
||||
var plugin = PluginDependency.createFromString('hello');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('*');
|
||||
});
|
||||
|
||||
it('must parse state', function() {
|
||||
var plugin = PluginDependency.createFromString('-hello');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
describe('Version', function() {
|
||||
it('must parse version', function() {
|
||||
var plugin = PluginDependency.createFromString('hello@1.0.0');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('must parse semver', function() {
|
||||
var plugin = PluginDependency.createFromString('hello@>=4.0.0');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('>=4.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GIT Version', function() {
|
||||
it('must handle HTTPS urls', function() {
|
||||
var plugin = PluginDependency.createFromString('hello@git+https://github.com/GitbookIO/plugin-ga.git');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('git+https://github.com/GitbookIO/plugin-ga.git');
|
||||
});
|
||||
|
||||
it('must handle SSH urls', function() {
|
||||
var plugin = PluginDependency.createFromString('hello@git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
|
||||
expect(plugin.getName()).toBe('hello');
|
||||
expect(plugin.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user