Merge pull request #1109 from GitbookIO/3.0.0

Version 3.0.0
This commit is contained in:
Samy Pessé
2016-02-26 09:41:26 +01:00
281 changed files with 7667 additions and 34223 deletions
+2 -1
View File
@@ -1 +1,2 @@
theme/**/*
docs/**/*
test/node_modules/**/*
+1
View File
@@ -0,0 +1 @@
./docs
+8 -1
View File
@@ -22,7 +22,14 @@ build/Release
# Dependency directory
# Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules
/node_modules
# vim swapfile
*.swp
# Output of documentation
docs/_book
book.pdf
book.epub
book.mobi
+2
View File
@@ -6,3 +6,5 @@ node_js:
- "0.12"
before_install:
- npm install svgexport -g
after_success:
- npm run lint
+14
View File
@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 3.x.x (unreleased)
- Summary can contain external links and anchors (Fix [#776](https://github.com/GitbookIO/gitbook/issues/776))
- Summary can contain differents entitled sections
- Glossary is generated as a normal page
- Headings are no longer annotated with glossary terms
- Themes are now published as a plugin, with ability to extend it from the book source
- `links.sidebar` configuration is no longer supported, use summary sections instead
- `pdf.headerTemplate` and `pdf.footerTemplate` have been replaced by a template in theme/book: `_layout/ebook/pdf_header.html` and `_layout/ebook/pdf_footer.html`
- Markdown parser is now using CommonMark
- Root folder for the book can be specified in a `.gitbook` file
- Multi-lingual books share assets folder
- YAML front matter is parsed and page's description can be specified in it
- Fix `uk` translation
## 2.6.7
- Fix bug with filenames including spaces
- Add Turkish and Catalan translations
+1
View File
@@ -1,4 +1,5 @@
#! /usr/bin/env node
/* eslint-disable no-console */
var color = require('bash-color');
+22
View File
@@ -0,0 +1,22 @@
# GitBook Format Documentation
> This documentation is for GitBook version **{{ book.version }}**
GitBook is a command line tool (and Node.js library) for building beautiful books using GitHub/Git and Markdown (or AsciiDoc). Here is an example: [Learn Javascript](https://www.gitbook.com/book/GitBookIO/javascript). This documentation has been generated using GitBook.
GitBook can output your content as a website ([customizable](themes.md) and [extensibles](plugins.md)) or as an ebook (PDF, ePub or Mobi).
[GitBook.com](https://www.gitbook.com) is the online platform to create and host books built using the GitBook format. It offers hosting, collaboration features and an [easy-to-use editor](https://www.gitbook.com/editor).
### Help and Support
We're always happy to help out with your books or any other questions you might have. You can ask a question on the following contact form at [gitbook.com/contact](https://www.gitbook.com/contact) or signal an issue on [GitHub](https://github.com/GitbookIO/gitbook).
### FAQ
There are questions that are asked quite often, [check this out before creating an issue](faq.md).
### Contribute to this documentation
You can contribute to improve this documentation on [GitHub](https://github.com/GitbookIO/gitbook) by signaling issues or proposing changes.
+33
View File
@@ -0,0 +1,33 @@
# Summary
### Getting Started
* [About this documentation](README.md)
* [Installation and Setup](setup.md)
### Your Content
* [Directory structure](structure.md)
* [Pages and Summary](pages.md)
* [Glossary](lexicon.md)
* [Multi-Lingual](languages.md)
* [Configuration](config.md)
* [AsciiDoc](asciidoc.md)
### Miscellaneous
* [Templating](templating.md)
* [Content References](conrefs.md)
* [Variables](variables.md)
### Customization
* [Plugins](plugins.md)
* [Theming](themes.md)
--
* [FAQ](faq.md)
* [Release notes](https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md)
+23
View File
@@ -0,0 +1,23 @@
{% extends template.theme %}
{% 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">
<i class="octicon octicon-mark-github"></i> Edit on GitHub
</a>
<a href="https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md" target="_blank" class="btn btn-link pull-right hidden-xs">
{{ book.version }}
</a>
{% endblock %}
{% block page %}
{{ super() }}
<hr>
<div class="btn-group btn-group-justified">
{% if page.previous and page.previous.path %}
<a class="btn" href="{{ page.previous.path|resolveFile }}"><b>Previous:</b> {{ page.previous.title }}</a>
{% endif %}
{% if page.next and page.next.path %}
<a class="btn" href="{{ page.next.path|resolveFile }}"><b>Next:</b> {{ page.next.title }}</a>
{% endif %}
</div>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
# AsciiDoc
Since version `2.0.0`, GitBook can also accept AsciiDoc as an input format.
Please refer to the [AsciiDoc Syntax Quick Reference](http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/) for more informations about the format.
Just like for markdown, GitBook is using some special files to extract structures: `README.adoc`, `SUMMARY.adoc`, `LANGS.adoc` and `GLOSSARY.adoc`.
### README.adoc
This is the main entry of your book: the introduction. This file is **non optional**.
### SUMMARY.adoc
This file defines the list of chapters and subchapters. Just like [for markdown](./pages.md), the `SUMMARY.adoc`'s format is simply a list of links, the name of the link is used as the chapter's name, and the target is a path to that chapter's file.
Subchapters are defined simply by adding a nested list to a parent chapter.
```asciidoc
= Summary
. link:chapter-1/README.adoc[Chapter 1]
.. link:chapter-1/ARTICLE1.adoc[Article 1]
.. link:chapter-1/ARTICLE2.adoc[Article 2]
... link:chapter-1/ARTICLE-1-2-1.adoc[Article 1.2.1]
. link:chapter-2/README.adoc[Chapter 2]
. link:chapter-3/README.adoc[Chapter 3]
. link:chapter-4/README.adoc[Chapter 4]
.. Unfinished article
. Unfinished Chapter
```
### LANGS.adoc
For [Multi-Languages](./languages.md) books, this file is used to define the different supported languages and translations.
This file is following the same syntax as the `SUMMARY.adoc`:
```asciidoc
= Languages
. link:en/[English]
. link:fr/[French]
```
### GLOSSARY.adoc
This file is used to define terms. [See the glossary section](./lexicon.md).
```asciidoc
= Glossary
== Magic
Sufficiently advanced technology, beyond the understanding of the observer producing a sense of wonder.
== PHP
An atrocious language, invented for the sole purpose of inflicting pain and suffering amongst the programming wizards of this world.
```
+11
View File
@@ -0,0 +1,11 @@
var pkg = require('../package.json');
module.exports = {
title: 'GitBook Documentation',
plugins: ['theme-official'],
theme: 'official',
variables: {
version: pkg.version
}
};
+37
View File
@@ -0,0 +1,37 @@
# Configuration
GitBook allows you to customize your book using a flexible configuration. These options are specified in a `book.json` file.
### Configuration Settings
| Variable | Description |
| -------- | ----------- |
| `title` | Title of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
| `description` | Description of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
| `author` | Name of the author. On GitBook.com this field is pre-filled. |
| `isbn` | ISBN of the book |
| `language` | ISO code of the book's language, default value is `en` |
| `direction` | `rtl` or `ltr`, default value depends on the value of `language` |
| `gitbook` | [SemVer](http://semver.org) condition to validate which GitBook version should be used |
| `plugins` | List of plugins to load, See [the plugins section](plugins.md) for more details |
| `pluginsConfig` |Configuration for plugins, See [the plugins section](plugins.md) for more details |
### PDF Options
PDF Output can be customized using a set of options in the `book.json`:
| Variable | Description |
| -------- | ----------- |
| `pdf.pageNumbers` | Add page numbers to the bottom of every page (default is `true`) |
| `pdf.fontSize` | Base font size (default is `12`) |
| `pdf.fontFamily` | Base font family (default is `Arial`) |
| `pdf.paperSize` | Paper size, options are `'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'` (default is `a4`) |
| `pdf.margin.top` | Top margin (default is `56`) |
| `pdf.margin.bottom` | Bottom margin (default is `56`) |
| `pdf.margin.right` | Right margin (default is `62`) |
| `pdf.margin.left` | Left margin (default is `62`) |
### Plugins
Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins.md) for more details.
+55
View File
@@ -0,0 +1,55 @@
# Content References
Content referencing (conref) is a convenient mechanism for reuse of content from other files or books.
### Importing local files
Importing an other file's content is really easy using the `include` tag:
```
{% include "./test.md" %}
```
### Importing file from another book
GitBook can also resolve the include path by using git:
```
{% include "git+https://github.com/GitbookIO/documentation.git/README.md#0.0.1" %}
```
The format of git url is:
```
git+https://user@hostname/project/blah.git/file#commit-ish
```
The real git url part should finish with `.git`, the filename to import is extracted after the `.git` till the fragment of the url.
The `commit-ish` can be any tag, sha, or branch which can be supplied as an argument to `git checkout`. The default is `master`.
### Inheritance
Template inheritance is a way to make it easy to reuse templates. When writing a template, you can define "blocks" that child templates can override. The inheritance chain can be as long as you like.
`block` defines a section on the template and identifies it with a name. Base templates can specify blocks and child templates can override them with new content.
```
{% extends "./mypage.md" %}
{% block pageContent %}
# This is my page content
{% endblock %}
```
In the file `mypage.md`, you should specify the blocks that can be extent:
```
{% block pageContent %}
This is the default content
{% endblock %}
# License
{% import "./LICENSE" %}
```
+33
View File
@@ -0,0 +1,33 @@
# GitBook FAQ
#### 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.
#### What can I use to edit my content?
Any text editor should work! But we advise using the [GitBook Editor](https://www.gitbook.com/editor). [GitBook.com](https://www.gitbook.com) also provides a web version of this editor.
---
#### Should I use an `.html` or `.md` extensions in my links?
You should always use `.md` extensions for your relative links, GitBook will automatically replace these links by the right value when the pointing file is referenced in the Table of Contents.
#### Can I create a GitBook in a sub-directory of my repository?
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 support Math equations?
GitBook supports math equations and TeX thanks to plugins. There are currently 2 official plugins to display math: [mathjax](https://plugins.gitbook.com/plugin/mathjax) and [katex](https://plugins.gitbook.com/plugin/katex).
#### Can I customize/theme the output?
Yes, both the website and ebook outputs can be customized using [themes](themes.md).
#### Can I add interactive content (videos, etc)?
GitBook is very [extensible](plugins.md). You can use [existing plugins](https://plugins.gitbook.com) or create your own!
+15
View File
@@ -0,0 +1,15 @@
# Multi-Languages
GitBook supports building books written in multiple languages. Each language should be a sub-directory following the normal GitBook format, and a file named `LANGS.md` should be present at the root of the repository with the following format:
```markdown
* [English](en/)
* [French](fr/)
* [Español](es/)
```
### Configuration for each language
When a language book (ex: `en`) has a `book.json`, its configuration will extend the main configuration.
The only exception is plugins, plugins are specify globally relative to the book, and language specific plugins can not be specified.
+13
View File
@@ -0,0 +1,13 @@
# Glossary
Allows you to specify terms and their respective definitions to be displayed as annotations. Based on those terms, gitbook will automatically build an index and highlight those terms in pages.
The `GLOSSARY.md` format is very simple :
```markdown
# Term
Definition for this term
# Another term
With it's definition, this can contain bold text and all other kinds of inline markup ...
```
+50
View File
@@ -0,0 +1,50 @@
# Pages and Summary
GitBook uses a `SUMMARY.md` file to define the structure of chapters and subchapters of the book. The `SUMMARY.md` file is used to generate the book's table of contents.
### Summary
The `SUMMARY.md`'s format is simply a list of links, the title of the link is used as the chapter's title, and the target is a path to that chapter's file.
Subchapters are defined simply by adding a nested list to a parent chapter.
##### Simple example
```markdown
# Summary
* [Part I](part1/README.md)
* [Writing is nice](part1/writing.md)
* [GitBook is nice](part1/gitbook.md)
* [Part II](part2/README.md)
* [We love feedback](part2/feedback_please.md)
* [Better tools for authors](part2/better_tools.md)
```
##### Example with subchapters split into parts
```markdown
# Summary
### Part 1
* [Writing is nice](part1/writing.md)
* [GitBook is nice](part1/gitbook.md)
### Part 2
* [We love feedback](part2/feedback_please.md)
* [Better tools for authors](part2/better_tools.md)
```
### Front Matter
Pages can contain an optional front matter. It can be used to define the page's description. The front matter must be the first thing in the file and must take the form of valid YAML set between triple-dashed lines. Here is a basic example:
```yaml
---
description: This is a short description of my page
---
# The content of my page
```
+26
View File
@@ -0,0 +1,26 @@
# Plugins
Plugins are the best way to extend GitBook functionalities (ebook and website). There exist plugins to do a lot of things: bring math formulas display support, track visits using Google Analytic, etc.
### How to find plugins?
Plugins can be easily searched on [plugins.gitbook.com](https://plugins.gitbook.com).
### How to install a plugin?
Once you find a plugin that you want to install, you need to add it to your `book.json`:
```
{
"plugins": ["myPlugin", "anotherPlugin"]
}
```
You can also specify a specific version using: `"myPlugin@0.3.1"`. By default GitBook will resolve the latest version of the plugin compatbile with the current GitBook version.
Plugins are automatically installed on [GitBook.com](https://www.gitbook.com). Locally, run `gitbook install` to install and prepare all plugins for your books.
### Configuring plugins
PLugins specific configurations are stored in `pluginsConfig`. You have to refer to the documentation of the plugin itself for details about the available options.
+45
View File
@@ -0,0 +1,45 @@
# Setup and Installation of GitBook
Getting GitBook installed and ready-to-go should only take a few minutes.
### GitBook.com
[GitBook.com](https://www.gitbook.com) is an easy to use solution to write, publish and host books. It's best and easier solution for publishing your content and collaborate on it.
It integrates well with the [GitBook Editor](https://www.gitbook.com/editor).
### Local Installation
##### Requirements
Installing GitBook is easy and straight-forward, but there are a few requirements youll need to make sure your system has before you start.
* NodeJS (v4.0.0 and above are adviced)
* Windows, Linux, Unix, or Mac OS X
##### Install with NPM
The best way to install GitBook is via **NPM**. At the terminal prompt, simply run the following command to install GitBook:
```
$ npm install gitbook-cli -g
```
`gitbook-cli` is an utility to install and use multiple versions of GitBook on the same system. It will automatically install the required version to build a book.
##### Using pre-releases
`gitbook-cli` makes it easy to install and test other versions of GitBook with your book:
```
$ gitbook install beta
```
##### Debugging
You can use the options `--log=debug` and `--debug` to get better error messages (with stack trace). For example:
```
$ gitbook build ./ --log=debug --debug
```
+62
View File
@@ -0,0 +1,62 @@
# Directory structure
GitBook uses a very simple and obvious directory sttructure:
```
.
├── book.json
├── README.md
├── SUMMARY.md
├── chapter-1/
| ├── README.md
| └── something.md
└── chapter-2/
├── README.md
└── something.md
```
An overview of what each of these does:
| File | Description |
| -------- | ----------- |
| `book.json` | Stores [configuration](config.md) data (__optional__) |
| `README.md` | Preface / Introduction for your book (**required**) |
| `SUMMARY.md` | Table of Contents |
### Static files and Images
A static file is a file that is not listed in the `SUMMARY.md`. All static files, not [ignored](#ignore), are copied to the output.
### Ignoring files & folders {#ignore}
GitBook will read the `.gitignore`, `.bookignore` and `.ignore` files to get a list of files and folders to skip.
The format inside those files, follows the same convention as `.gitignore`:
```markdown
# This is a comment
# Ignore the file test.md
test.md
# Ignore everything in the directory "bin"
bin/*
```
### Project documentation / Sub-directory {#subdirectory}
For project documentaiton, it sometimes better to use a diretcory (like `docs/`) to store the prject's documentation. You can use a `.gitbook` file to indicate to GitBook in which folder the book is stored:
```
.
├── .gitbook
└── docs/
├── README.md
└── SUMMARY.md
```
With `.gitbook` containing:
```
./docs/
```
+90
View File
@@ -0,0 +1,90 @@
# Templating
GitBook uses the Nunjucks templating language to process pages and theme's templates.
The Nunjucks syntax is very similar to **Jinja2** or **Liquid**.
### Variables
A variable looks up a value from the template context. If you wanted to simply display a variable, you would do:
```twig
{{ username }}
```
This looks up username from the context and displays it. Variable names can have dots in them which lookup properties, just like javascript. You can also use the square bracket syntax.
```twig
{{ foo.bar }}
{{ foo["bar"] }}
```
If a value is undefined, nothing is displayed. The following all output nothing if foo is undefined: `{{ foo }}`, `{{ foo.bar }}`, `{{ foo.bar.baz }}`.
GitBook provides a set of [context variables](variables.md).
### Filters
Filters are essentially functions that can be applied to variables. They are called with a pipe operator (`|`) and can take arguments.
```twig
{{ foo | title }}
{{ foo | join(",") }}
{{ foo | replace("foo", "bar") | capitalize }}
```
The third example shows how you can chain filters. It would display "Bar", by first replacing "foo" with "bar" and then capitalizing it.
### Tags
##### if
`if` tests a condition and lets you selectively display content. It behaves exactly as javascript's if behaves.
```twig
{% if variable %}
It is true
{% endif %}
```
If variable is defined and evaluates to true, "It is true" will be displayed. Otherwise, nothing will be.
You can specify alternate conditions with elif and else:
```twig
{% if hungry %}
I am hungry
{% elif tired %}
I am tired
{% else %}
I am good!
{% endif %}
```
##### for
`for` iterates over arrays and dictionaries.
```twig
# Chapters about GitBook
{% for article in glossary.terms['gitbook'].articles %}
* [{{ article.title }}]({{ article.path }})
{% endfor %}
```
##### set
`set` lets you create/modify a variable.
```twig
{% set softwareVersion = "1.0.0" %}
Current version is {{ softwareVersion }}.
[Download it](website.com/download/{{ softwareVersion }})
```
##### include and block
Inclusion and inheritance is detailled in the [ConRefs](conrefs.md) section.
+26
View File
@@ -0,0 +1,26 @@
# 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`.
> **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.
| Folder | Description |
| -------- | ----------- |
| `_layouts` | Main folder containing all the templates |
| `_layouts/website/page.html` | Template for a normal page |
| `_layouts/ebook/page.html` | Template for a normal page during ebook generation (PDF< ePub, Mobi) |
### 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
### Publish a theme
Themes are published as plugins ([see related docs](plugins.md)) with a `theme-` prefix. For example the theme `awesome` will be loaded from `theme-awesome` plugin, and then from `gitbook-plugin-theme-awesome` NPM package.
+64
View File
@@ -0,0 +1,64 @@
# Variables
The following is a reference of the available data during book's parsing and theme generation.
### Global Variables
| Variable | Description |
| -------- | ----------- |
| `book` | Bookwide information + configuration settings from `book.json`. See below for details. |
| `gitbook` | GitBook specific information |
| `page` | Current page specific information |
| `file` | File associated with the current page specific information |
| `summary` | Information about the table of contents |
| `languages` | List of languages for multi-lingual books |
| `config` | Dump of the `book.json` |
### Book Variables
| Variable | Description |
| -------- | ----------- |
| `book.[CONFIGURATION_DATA]` | All the `variables` set via the `book.json` are available through the book variable. |
| `book.language` | Current language for a multilingual book |
### GitBook Variables
| Variable | Description |
| -------- | ----------- |
| `gitbook.time` | The current time (when you run the `gitbook` command). |
| `gitbook.version` | Version of GitBook used to generate the book |
### File Variables
| Variable | Description |
| -------- | ----------- |
| `file.path` | The path to the raw page |
| `file.mtime` | Modified Time, Time when file data last modified |
| `file.type` | The name of the parser used to compile this file (ex: `markdown`, `asciidoc`, etc) |
#### Page Variables
| Variable | Description |
| -------- | ----------- |
| `page.title` | Title of the page |
| `page.previous` | Previous page in the Table of Contents (can be `null`) |
| `page.next` | Next page in the Table of Contents (can be `null`) |
| `page.dir` | Text direction, based on configuration or detected from content (`rtl` or `ltr`) |
#### Table of Contents Variables
| Variable | Description |
| -------- | ----------- |
| `summary.parts` | List of sections in the Table of Contents |
Thw whole table of contents (`SUMMARY.md`) can be accessed:
`summary.parts[0].articles[0].title` will return the title of the first article.
#### Multi-lingual book Variable
| Variable | Description |
| -------- | ----------- |
| `languages.list` | List of languages for this book |
Languages are defined by `{ id: 'en', title: 'English' }`.
-47
View File
@@ -1,47 +0,0 @@
var _ = require('lodash');
var gulp = require('gulp');
var gutil = require('gulp-util');
var less = require('gulp-less');
var rename = require('gulp-rename');
var minifyCSS = require('gulp-minify-css');
var browserify = require('browserify');
var mergeStream = require('merge-stream');
var source = require('vinyl-source-stream');
gulp.task('css', function() {
var merged = mergeStream();
_.each({
'ebook.less': 'ebook/ebook.css',
'pdf.less': 'ebook/pdf.css',
'mobi.less': 'ebook/mobi.css',
'epub.less': 'ebook/epub.css',
'website.less': 'website/style.css'
}, function(out, input) {
gutil.log('compiling', input, 'into', out);
merged.add(gulp.src('theme/stylesheets/'+input)
.pipe(less())
.pipe(minifyCSS())
.pipe(rename(out))
.pipe(gulp.dest('theme/assets/')));
});
return merged;
});
gulp.task('js', function() {
return browserify('./theme/javascript/index.js')
.bundle()
.pipe(source('app.js'))
.pipe(gulp.dest('./theme/assets/website'));
});
gulp.task('assets', function() {
return gulp.src('./node_modules/font-awesome/fonts/*')
.pipe(gulp.dest('theme/assets/website/fonts/fontawesome/'));
});
gulp.task('default', ['css', 'js', 'assets'], function() {
});
+69
View File
@@ -0,0 +1,69 @@
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;
+99
View File
@@ -0,0 +1,99 @@
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;
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
Readme: require('./readme'),
Summary: require('./summary'),
Glossary: require('./glossary'),
Langs: require('./langs')
};
+81
View File
@@ -0,0 +1,81 @@
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;
+26
View File
@@ -0,0 +1,26 @@
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;
+339
View File
@@ -0,0 +1,339 @@
var _ = require('lodash');
var util = require('util');
var url = require('url');
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) {
var parts = url.parse(this.ref);
if (!this.isExternal()) {
this.path = parts.pathname;
this.anchor = parts.hash;
}
}
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;
};
// 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;
+238 -703
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
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());
return Book.setup(nodeFS, input, {
logLevel: kwargs.log
})
.then(function(book) {
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[1] || ('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
};
+187
View File
@@ -0,0 +1,187 @@
/* eslint-disable no-console */
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: '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() {
return Book.setup(helper.nodeFS, input, {
'logLevel': kwargs.log
})
.then(function(book) {
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();
});
}
}
]
};
+20 -19
View File
@@ -1,9 +1,10 @@
var Q = require("q");
var events = require("events");
var http = require("http");
var send = require("send");
var util = require("util");
var url = require("url");
var events = require('events');
var http = require('http');
var send = require('send');
var util = require('util');
var url = require('url');
var Promise = require('../utils/promise');
var Server = function() {
this.running = null;
@@ -21,12 +22,12 @@ Server.prototype.isRunning = function() {
// Stop the server
Server.prototype.stop = function() {
var that = this;
if (!this.isRunning()) return Q();
if (!this.isRunning()) return Promise();
var d = Q.defer();
var d = Promise.defer();
this.running.close(function(err) {
that.running = null;
that.emit("state", false);
that.emit('state', false);
if (err) d.reject(err);
else d.resolve();
@@ -40,13 +41,13 @@ Server.prototype.stop = function() {
};
Server.prototype.start = function(dir, port) {
var that = this, pre = Q();
var that = this, pre = Promise();
port = port || 8004;
if (that.isRunning()) pre = this.stop();
return pre
.then(function() {
var d = Q.defer();
var d = Promise.defer();
that.running = http.createServer(function(req, res){
// Render error
@@ -55,25 +56,25 @@ Server.prototype.start = function(dir, port) {
res.end(err.message);
}
// Redirect to directory"s index.html
// Redirect to directory's index.html
function redirect() {
res.statusCode = 301;
res.setHeader("Location", req.url + "/");
res.end("Redirecting to " + req.url + "/");
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
}
// Send file
send(req, url.parse(req.url).pathname)
.root(dir)
.on("error", error)
.on("directory", redirect)
.on('error', error)
.on('directory', redirect)
.pipe(res);
});
that.running.on("connection", function (socket) {
that.running.on('connection', function (socket) {
that.sockets.push(socket);
socket.setTimeout(4000);
socket.on("close", function () {
socket.on('close', function () {
that.sockets.splice(that.sockets.indexOf(socket), 1);
});
});
@@ -83,7 +84,7 @@ Server.prototype.start = function(dir, port) {
that.port = port;
that.dir = dir;
that.emit("state", true);
that.emit('state', true);
d.resolve();
});
+42
View File
@@ -0,0 +1,42 @@
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
function watch(dir) {
var d = Promise.defer();
dir = path.resolve(dir);
var toWatch = [
'book.json', 'book.js'
];
// Watch all parsable files
_.each(parsers.extensions, function(ext) {
toWatch.push('**/*'+ext);
});
var watcher = chokidar.watch(toWatch, {
cwd: dir,
ignored: '_book/**',
ignoreInitial: true
});
watcher.once('all', function(e, filepath) {
watcher.close();
d.resolve(filepath);
});
watcher.once('error', function(err) {
watcher.close();
d.reject(err);
});
return d.promise;
}
module.exports = watch;
+132
View File
@@ -0,0 +1,132 @@
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;
+67
View File
@@ -0,0 +1,67 @@
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
};
+188
View File
@@ -0,0 +1,188 @@
module.exports = {
'$schema': 'http://json-schema.org/schema#',
'id': 'https://gitbook.com/schemas/book.json',
'title': 'GitBook Configuration',
'type': 'object',
'properties': {
'title': {
'type': 'string',
'title': 'Title of the book, default is extracted from README'
},
'title': {
'type': 'string',
'title': 'Description of the book, default is extracted from README'
},
'isbn': {
'type': 'string',
'title': 'ISBN for published book'
},
'author': {
'type': 'string',
'title': 'Name of the author'
},
'gitbook': {
'type': 'string',
'default': '*',
'title': 'GitBook version to match'
},
'direction': {
'type': 'string',
'enum': ['ltr', 'rtl'],
'title': 'Direction of texts, default is detected in the pages'
},
'theme': {
'type': 'string',
'default': 'default',
'title': 'Name of the theme plugin to use'
},
'variables': {
'type': 'object',
'title': 'Templating context variables'
},
'plugins': {
'oneOf': [
{ '$ref': '#/definitions/pluginsArray' },
{ '$ref': '#/definitions/pluginsString' }
],
'default': []
},
'pluginsConfig': {
'type': 'object',
'title': 'Configuration for plugins'
},
'structure': {
'type': 'object',
'properties': {
'langs': {
'default': 'LANGS.md',
'type': 'string',
'title': 'File to use as languages index',
'pattern': '^[0-9a-zA-Z ... ]+$'
},
'readme': {
'default': 'README.md',
'type': 'string',
'title': 'File to use as preface',
'pattern': '^[0-9a-zA-Z ... ]+$'
},
'glossary': {
'default': 'GLOSSARY.md',
'type': 'string',
'title': 'File to use as glossary index',
'pattern': '^[0-9a-zA-Z ... ]+$'
},
'summary': {
'default': 'SUMMARY.md',
'type': 'string',
'title': 'File to use as table of contents',
'pattern': '^[0-9a-zA-Z ... ]+$'
}
},
'additionalProperties': false
},
'pdf': {
'type': 'object',
'title': 'PDF specific configurations',
'properties': {
'pageNumbers': {
'type': 'boolean',
'default': true,
'title': 'Add page numbers to the bottom of every page'
},
'fontSize': {
'type': 'integer',
'minimum': 8,
'maximum': 30,
'default': 12,
'title': 'Font size for the PDF output'
},
'fontFamily': {
'type': 'string',
'default': 'Arial',
'title': 'Font family for the PDF output'
},
'paperSize': {
'type': 'string',
'enum': ['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'],
'default': 'a4',
'title': 'Paper size for the PDF'
},
'chapterMark': {
'type': 'string',
'enum': ['pagebreak', 'rule', 'both', 'none'],
'default': 'pagebreak',
'title': 'How to mark detected chapters'
},
'pageBreaksBefore': {
'type': 'string',
'default': '/',
'title': 'An XPath expression. Page breaks are inserted before the specified elements. To disable use the expression: "/"'
},
'margin': {
'type': 'object',
'properties': {
'right': {
'type': 'integer',
'title': 'Right Margin',
'minimum': 0,
'maximum': 100,
'default': 62
},
'left': {
'type': 'integer',
'title': 'Left Margin',
'minimum': 0,
'maximum': 100,
'default': 62
},
'top': {
'type': 'integer',
'title': 'Top Margin',
'minimum': 0,
'maximum': 100,
'default': 56
},
'bottom': {
'type': 'integer',
'title': 'Bottom Margin',
'minimum': 0,
'maximum': 100,
'default': 56
}
}
}
}
}
},
'required': [],
'definitions': {
'pluginsArray': {
'type': 'array',
'items': {
'oneOf': [
{ '$ref': '#/definitions/pluginObject' },
{ '$ref': '#/definitions/pluginString' }
]
}
},
'pluginsString': {
'type': 'string'
},
'pluginString': {
'type': 'string'
},
'pluginObject': {
'type': 'object',
'properties': {
'name': {
'type': 'string'
},
'version': {
'type': 'string'
}
},
'additionalProperties': false,
'required': ['name']
}
}
};
+28
View File
@@ -0,0 +1,28 @@
var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var schema = require('./schema');
var error = require('../utils/error');
// Validate a book.json content
// And return a mix with the default value
function validate(bookJson) {
var v = new jsonschema.Validator();
var result = v.validate(bookJson, schema, {
propertyName: 'config'
});
// Throw error
if (result.errors.length > 0) {
throw new error.ConfigurationError(new Error(result.errors[0].stack));
}
// Insert default values
var defaults = jsonSchemaDefaults(schema);
return mergeDefaults(bookJson, defaults);
}
module.exports = {
validate: validate
};
-109
View File
@@ -1,109 +0,0 @@
var path = require('path');
module.exports = {
// Options that can't be extend
'configFile': 'book',
'generator': 'website',
'extension': null,
// Book metadats (somes are extracted from the README by default)
'title': null,
'description': null,
'isbn': null,
'language': 'en',
'direction': null,
'author': null,
// version of gitbook to use
'gitbook': '*',
// Structure
'structure': {
'langs': 'LANGS.md',
'readme': 'README.md',
'glossary': 'GLOSSARY.md',
'summary': 'SUMMARY.md'
},
// CSS Styles
'styles': {
'website': 'styles/website.css',
'print': 'styles/print.css',
'ebook': 'styles/ebook.css',
'pdf': 'styles/pdf.css',
'mobi': 'styles/mobi.css',
'epub': 'styles/epub.css'
},
// Plugins list, can contain '-name' for removing default plugins
'plugins': [],
// Global configuration for plugins
'pluginsConfig': {},
// Variables for templating
'variables': {},
// Set another theme with your own layout
// It's recommended to use plugins or add more options for default theme, though
// See https://github.com/GitbookIO/gitbook/issues/209
'theme': path.resolve(__dirname, '../theme'),
// Links in template (null: default, false: remove, string: new value)
'links': {
// Custom links at top of sidebar
'sidebar': {
// 'Custom link name': 'https://customlink.com'
},
// Sharing links
'sharing': {
'google': null,
'facebook': null,
'twitter': null,
'weibo': null,
'all': null
}
},
// Options for PDF generation
'pdf': {
// Add toc at the end of the file
'toc': true,
// Add page numbers to the bottom of every page
'pageNumbers': false,
// Font for the file content
'fontSize': 12,
'fontFamily': 'Arial',
// Paper size for the pdf
// Choices are [ua0, ua1, ua2, ua3, ua4, ua5, ua6, ub0, ub1, ub2, ub3, ub4, ub5, ub6, ulegal, uletter]
'paperSize': 'a4',
// How to mark detected chapters.
// Choices are “pagebreak”, “rule”, 'both' or “none”.
'chapterMark' : 'pagebreak',
// An XPath expression. Page breaks are inserted before the specified elements.
// To disable use the expression: '/'
'pageBreaksBefore': '/',
// Margin (in pts)
// Note: 72 pts equals 1 inch
'margin': {
'right': 62,
'left': 62,
'top': 56,
'bottom': 56
},
// Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
'headerTemplate': null,
// Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
'footerTemplate': null
}
};
-210
View File
@@ -1,210 +0,0 @@
var _ = require('lodash');
var Q = require('q');
var path = require('path');
var semver = require('semver');
var pkg = require('../package.json');
var i18n = require('./utils/i18n');
var version = require('./version');
var DEFAULT_CONFIG = require('./config_default');
// Default plugins added to each books
var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings'];
// Check if a plugin is a default plugin
// Plugin should be in the list
// And version from book.json specified for this plugin should be satisfied
function isDefaultPlugin(name, version) {
if (!_.contains(DEFAULT_PLUGINS, name)) return false;
try {
var pluginPkg = require('gitbook-plugin-'+name+'/package.json');
return semver.satisfies(pluginPkg.version, version || '*');
} catch(e) {
return false;
}
}
// Normalize a list of plugins to use
function normalizePluginsList(plugins, addDefaults) {
// 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
'isDefault': isDefaultPlugin(name, version)
};
});
// 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
if (addDefaults !== false) {
_.each(DEFAULT_PLUGINS, function(plugin) {
if (_.find(plugins, { name: plugin })) {
return;
}
plugins.push({
'name': plugin,
'isDefault': true
});
});
}
// 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;
}
var Configuration = function(book, options) {
this.book = book;
this.replace(options);
};
// Read and parse the configuration
Configuration.prototype.load = function() {
var that = this;
return Q()
.then(function() {
var configPath, _config;
try {
configPath = require.resolve(
that.book.resolve(that.options.configFile)
);
// Invalidate node.js cache for livreloading
delete require.cache[configPath];
_config = require(configPath);
that.options = _.merge(
that.options,
_.omit(_config, 'configFile', 'defaultsPlugins', 'generator', 'extension')
);
}
catch(err) {
if (err instanceof SyntaxError) return Q.reject(err);
return Q();
}
})
.then(function() {
if (!that.book.isSubBook()) {
if (!version.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(pkg.version, 'patch'), that.options.gitbook)) {
that.book.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(pkg.version.split('.'))+'.x.x')+'\' is more adequate');
}
}
that.options.output = path.resolve(that.options.output || that.book.resolve('_book'));
that.options.plugins = normalizePluginsList(that.options.plugins);
that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
that.options.plugins = _.uniq(that.options.plugins, 'name');
// Default value for text direction (from language)
if (!that.options.direction) {
var lang = i18n.getCatalog(that.options.language);
if (lang) that.options.direction = lang.direction;
}
that.options.gitbook = pkg.version;
});
};
// Extend the configuration
Configuration.prototype.extend = function(options) {
_.extend(this.options, options);
};
// Replace the whole configuration
Configuration.prototype.replace = function(options) {
var that = this;
this.options = _.cloneDeep(DEFAULT_CONFIG);
this.options = _.merge(this.options, 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;
}
});
// options.originalOutput == book.parent.options.output
Object.defineProperty(this.options, 'originalOutput', {
get: function () {
return that.book.parent? that.book.parent.options.output : undefined;
}
});
};
// Dump configuration as json object
Configuration.prototype.dump = function() {
return _.cloneDeep(this.options);
};
// Get structure file
Configuration.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 normalized language
Configuration.prototype.normalizeLanguage = function() {
return i18n.normalizeLanguage(this.options.language);
};
// Return a configuration
Configuration.prototype.get = function(key, def) {
return _.get(this.options, key, def);
};
// Update a configuration
Configuration.prototype.set = function(key, value) {
return _.set(this.options, key, value);
};
// Default configuration
Configuration.DEFAULT = DEFAULT_CONFIG;
module.exports= Configuration;
-73
View File
@@ -1,73 +0,0 @@
var _ = require('lodash');
var path = require('path');
var nunjucks = require('nunjucks');
var git = require('./utils/git');
var fs = require('./utils/fs');
var pathUtil = require('./utils/path');
// The loader should handle relative and git url
var BookLoader = nunjucks.Loader.extend({
async: true,
init: function(book, opts) {
this.opts = _.defaults(opts || {}, {
interpolate: _.identity
});
this.book = book;
},
getSource: function(fileurl, callback) {
var that = this;
git.resolveFile(fileurl)
.then(function(filepath) {
// Is local file
if (!filepath) filepath = path.resolve(fileurl);
else that.book.log.debug.ln('resolve from git', fileurl, 'to', filepath);
// Read file from absolute path
return fs.readFile(filepath)
.then(function(source) {
return that.opts.interpolate(filepath, source.toString());
})
.then(function(source) {
return {
src: source,
path: filepath,
// We disable cache sincde content is modified (shortcuts, ...)
noCache: true
};
});
})
.nodeify(callback);
},
resolve: function(from, to) {
// If origin is in the book, we enforce result file to be in the book
if (this.book.fileIsInBook(from)) {
return this.book.resolve(
this.book.relative(path.dirname(from)),
to
);
}
// If origin is in a git repository, we resolve file in the git repository
var gitRoot = git.resolveRoot(from);
if (gitRoot) {
return pathUtil.resolveInRoot(gitRoot, to);
}
// If origin is not in the book (include from a git content ref)
return path.resolve(path.dirname(from), to);
},
// Handle all files as relative, so that nunjucks pass responsability to 'resolve'
// Only git urls are considered as absolute
isRelative: function(filename) {
return !git.checkUrl(filename);
}
});
module.exports = BookLoader;
+106
View File
@@ -0,0 +1,106 @@
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;
+66
View File
@@ -0,0 +1,66 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
var fs = require('../utils/fs');
var Promise = require('../utils/promise');
var BaseFS = require('./');
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) {
return fs.readdir(folder)
.then(function(files) {
return _.chain(files)
.map(function(file) {
if (file == '.' || file == '..') return;
var stat = fs.statSync(path.join(folder, file));
if (stat.isDirectory()) file = file + path.sep;
return file;
})
.compact()
.value();
});
};
// 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;
-76
View File
@@ -1,76 +0,0 @@
var _ = require('lodash');
var path = require('path');
var Q = require('q');
var fs = require('./utils/fs');
var BaseGenerator = function(book) {
this.book = book;
Object.defineProperty(this, 'options', {
get: function () {
return this.book.options;
}
});
_.bindAll(this);
};
BaseGenerator.prototype.callHook = function(name, data) {
return this.book.callHook(name, data);
};
// Prepare the genertor
BaseGenerator.prototype.prepare = function() {
var that = this;
return that.callHook('init');
};
// Write a parsed file to the output
BaseGenerator.prototype.convertFile = function(input) {
return Q.reject(new Error('Could not convert '+input));
};
// Copy file to the output (non parsable)
BaseGenerator.prototype.transferFile = function(input) {
return fs.copy(
this.book.resolve(input),
path.join(this.options.output, input)
);
};
// Copy a folder to the output
BaseGenerator.prototype.transferFolder = function(input) {
return fs.mkdirp(
path.join(this.book.options.output, input)
);
};
// Copy the cover picture
BaseGenerator.prototype.copyCover = function() {
var that = this;
return Q.all([
fs.copy(that.book.resolve('cover.jpg'), path.join(that.options.output, 'cover.jpg')),
fs.copy(that.book.resolve('cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
])
.fail(function() {
// If orignaly from multi-lang, try copy from parent
if (!that.book.isSubBook()) return;
return Q.all([
fs.copy(path.join(that.book.parentRoot(), 'cover.jpg'), path.join(that.options.output, 'cover.jpg')),
fs.copy(path.join(that.book.parentRoot(), 'cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
]);
})
.fail(function() {
return Q();
});
};
// At teh end of the generation
BaseGenerator.prototype.finish = function() {
return Q.reject(new Error('Could not finish generation'));
};
module.exports = BaseGenerator;
-172
View File
@@ -1,172 +0,0 @@
var util = require('util');
var path = require('path');
var Q = require('q');
var _ = require('lodash');
var juice = require('juice');
var exec = require('child_process').exec;
var fs = require('../utils/fs');
var stringUtils = require('../utils/string');
var BaseGenerator = require('./website');
var Generator = function(book, format) {
BaseGenerator.apply(this, arguments);
// eBook format
this.ebookFormat = format;
// Resources namespace
this.namespace = 'ebook';
// Styles to use
this.styles = _.compact(['print', 'ebook', this.ebookFormat]);
// Convert images (svg -> png)
this.convertImages = true;
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.prepareTemplates = function() {
this.templates.page = this.book.plugins.template('ebook:page') || path.resolve(this.options.theme, 'templates/ebook/page.html');
this.templates.summary = this.book.plugins.template('ebook:summary') || path.resolve(this.options.theme, 'templates/ebook/summary.html');
this.templates.glossary = this.book.plugins.template('ebook:glossary') || path.resolve(this.options.theme, 'templates/ebook/glossary.html');
return Q();
};
// Generate table of contents
Generator.prototype.writeSummary = function() {
var that = this;
that.book.log.info.ln('write SUMMARY.html');
return this._writeTemplate(this.templates.summary, {}, path.join(this.options.output, 'SUMMARY.html'));
};
// Return template for footer/header with inlined css
Generator.prototype.getPDFTemplate = function(id) {
var tpl = this.options.pdf[id+'Template'];
var defaultTpl = path.resolve(this.options.theme, 'templates/ebook/'+id+'.html');
var defaultCSS = path.resolve(this.options.theme, 'assets/ebook/pdf.css');
// Default template from theme
if (!tpl && fs.existsSync(defaultTpl)) {
tpl = fs.readFileSync(defaultTpl, { encoding: 'utf-8' });
}
// Inline CSS using juice
var stylesheets = [];
// From theme
if (fs.existsSync(defaultCSS)) {
stylesheets.push(fs.readFileSync(defaultCSS, { encoding: 'utf-8' }));
}
// Custom PDF style
if (this.styles.pdf) {
stylesheets.push(fs.readFileSync(this.book.resolveOutput(this.styles.pdf), { encoding: 'utf-8' }));
}
tpl = juice(tpl, {
extraCss: stylesheets.join('\n\n')
});
return tpl;
};
Generator.prototype.finish = function() {
var that = this;
return Q()
.then(this.copyAssets)
.then(this.copyCover)
.then(this.writeGlossary)
.then(this.writeSummary)
.then(function() {
if (!that.ebookFormat) return Q();
if (!that.options.cover && fs.existsSync(path.join(that.options.output, 'cover.jpg'))) {
that.options.cover = path.join(that.options.output, 'cover.jpg');
}
var d = Q.defer();
var _options = {
'--cover': that.options.cover,
'--title': that.options.title,
'--comments': that.options.description,
'--isbn': that.options.isbn,
'--authors': that.options.author,
'--language': that.options.language,
'--book-producer': 'GitBook',
'--publisher': 'GitBook',
'--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]',
'--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]',
'--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]',
'--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]',
'--no-chapters-in-toc': true,
'--max-levels': '1',
'--breadth-first': true
};
if (that.ebookFormat == 'pdf') {
var pdfOptions = that.options.pdf;
_.extend(_options, {
'--chapter-mark': String(pdfOptions.chapterMark),
'--page-breaks-before': String(pdfOptions.pageBreaksBefore),
'--margin-left': String(pdfOptions.margin.left),
'--margin-right': String(pdfOptions.margin.right),
'--margin-top': String(pdfOptions.margin.top),
'--margin-bottom': String(pdfOptions.margin.bottom),
'--pdf-default-font-size': String(pdfOptions.fontSize),
'--pdf-mono-font-size': String(pdfOptions.fontSize),
'--paper-size': String(pdfOptions.paperSize),
'--pdf-page-numbers': Boolean(pdfOptions.pageNumbers),
'--pdf-header-template': that.getPDFTemplate('header'),
'--pdf-footer-template': that.getPDFTemplate('footer'),
'--pdf-sans-family': String(pdfOptions.fontFamily)
});
} else if (that.ebookFormat == 'epub') {
_.extend(_options, {
'--dont-split-on-page-breaks': true
});
}
var command = [
'ebook-convert',
path.join(that.options.output, 'SUMMARY.html'),
path.join(that.options.output, 'index.'+that.ebookFormat),
stringUtils.optionsToShellArgs(_options)
].join(' ');
that.book.log.info('start conversion to', that.ebookFormat, '....');
var child = exec(command, function (error, stdout) {
if (error) {
that.book.log.info.fail();
if (error.code == 127) {
error.message = 'Need to install ebook-convert from Calibre';
} else {
error.message = error.message + ' '+stdout;
}
return d.reject(error);
}
that.book.log.info.ok();
d.resolve();
});
child.stdout.on('data', function (data) {
that.book.log.debug(data);
});
child.stderr.on('data', function (data) {
that.book.log.debug(data);
});
return d.promise;
});
};
module.exports = Generator;
-11
View File
@@ -1,11 +0,0 @@
var _ = require("lodash");
var EbookGenerator = require("./ebook");
module.exports = {
json: require("./json"),
website: require("./website"),
ebook: EbookGenerator,
pdf: _.partialRight(EbookGenerator, "pdf"),
mobi: _.partialRight(EbookGenerator, "mobi"),
epub: _.partialRight(EbookGenerator, "epub")
};
-76
View File
@@ -1,76 +0,0 @@
var util = require('util');
var path = require('path');
var Q = require('q');
var _ = require('lodash');
var fs = require('../utils/fs');
var BaseGenerator = require('../generator');
var links = require('../utils/links');
var Generator = function() {
BaseGenerator.apply(this, arguments);
};
util.inherits(Generator, BaseGenerator);
// Ignore some methods
Generator.prototype.transferFile = function() { };
// Convert an input file
Generator.prototype.convertFile = function(input) {
var that = this;
return that.book.parsePage(input)
.then(function(page) {
var json = {
progress: page.progress,
sections: page.sections
};
var output = links.changeExtension(page.path, '.json');
output = path.join(that.options.output, output);
return fs.writeFile(
output,
JSON.stringify(json, null, 4)
);
});
};
// Finish generation
Generator.prototype.finish = function() {
return this.writeReadme();
};
// Write README.json
Generator.prototype.writeReadme = function() {
var that = this;
var mainLang, langs, readme;
return Q()
.then(function() {
langs = that.book.langs;
mainLang = langs.length > 0? _.first(langs).lang : null;
readme = links.changeExtension(that.book.readmeFile, '.json');
// Read readme from main language
return fs.readFile(
mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme)
);
})
.then(function(content) {
// Extend it with infos about the languages
var json = JSON.parse(content);
_.extend(json, {
langs: langs
});
// Write it as README.json
return fs.writeFile(
path.join(that.options.output, 'README.json'),
JSON.stringify(json, null, 4)
);
});
};
module.exports = Generator;
-268
View File
@@ -1,268 +0,0 @@
var util = require('util');
var path = require('path');
var Q = require('q');
var _ = require('lodash');
var nunjucks = require('nunjucks');
var AutoEscapeExtension = require('nunjucks-autoescape')(nunjucks);
var FilterExtension = require('nunjucks-filter')(nunjucks);
var fs = require('../utils/fs');
var BaseGenerator = require('../generator');
var links = require('../utils/links');
var i18n = require('../utils/i18n');
var pkg = require('../../package.json');
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Revision
this.revision = new Date();
// Resources namespace
this.namespace = 'website';
// Style to integrates in the output
this.styles = ['website'];
// Convert images (svg -> png)
this.convertImages = false;
// Templates
this.templates = {};
};
util.inherits(Generator, BaseGenerator);
// Prepare the genertor
Generator.prototype.prepare = function() {
return BaseGenerator.prototype.prepare.apply(this)
.then(this.prepareStyles)
.then(this.prepareTemplates)
.then(this.prepareTemplateEngine);
};
// Prepare all styles
Generator.prototype.prepareStyles = function() {
var that = this;
this.styles = _.chain(this.styles)
.map(function(style) {
var stylePath = that.options.styles[style];
var styleExists = (
fs.existsSync(that.book.resolveOutput(stylePath)) ||
fs.existsSync(that.book.resolve(stylePath))
);
if (stylePath && styleExists) {
return [style, stylePath];
}
return null;
})
.compact()
.object()
.value();
return Q();
};
// Prepare templates
Generator.prototype.prepareTemplates = function() {
this.templates.page = this.book.plugins.template('site:page') || path.resolve(this.options.theme, 'templates/website/page.html');
this.templates.langs = this.book.plugins.template('site:langs') || path.resolve(this.options.theme, 'templates/website/langs.html');
this.templates.glossary = this.book.plugins.template('site:glossary') || path.resolve(this.options.theme, 'templates/website/glossary.html');
return Q();
};
// Prepare template engine
Generator.prototype.prepareTemplateEngine = function() {
var that = this;
return Q()
.then(function() {
var language = that.book.config.normalizeLanguage();
if (!i18n.hasLocale(language)) {
that.book.log.warn.ln('Language "'+language+'" is not available as a layout locales (en, '+i18n.getLocales().join(', ')+')');
}
var folders = _.chain(that.templates)
.values()
.map(path.dirname)
.uniq()
.value();
that.env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(folders),
{
autoescape: true
}
);
// Add filter
that.env.addFilter('contentLink', that.book.contentLink.bind(that.book));
that.env.addFilter('lvl', function(lvl) {
return lvl.split('.').length;
});
// Add extension
that.env.addExtension('AutoEscapeExtension', new AutoEscapeExtension(that.env));
that.env.addExtension('FilterExtension', new FilterExtension(that.env));
});
};
// Finis generation
Generator.prototype.finish = function() {
return this.copyAssets()
.then(this.copyCover)
.then(this.writeGlossary)
.then(this.writeLangsIndex);
};
// Convert an input file
Generator.prototype.convertFile = function(input) {
var that = this;
return that.book.parsePage(input, {
convertImages: that.convertImages,
interpolateTemplate: function(page) {
return that.callHook('page:before', page);
},
interpolateContent: function(page) {
return that.callHook('page', page);
}
})
.then(function(page) {
var relativeOutput = that.book.contentPath(page.path);
var output = path.join(that.options.output, relativeOutput);
var basePath = path.relative(path.dirname(output), that.options.output) || '.';
if (process.platform === 'win32') basePath = basePath.replace(/\\/g, '/');
that.book.log.debug.ln('write parsed file', page.path, 'to', relativeOutput);
return that._writeTemplate(that.templates.page, {
progress: page.progress,
_input: page.path,
content: page.sections,
basePath: basePath,
staticBase: links.join(basePath, 'gitbook')
}, output);
});
};
// Write the index for langs
Generator.prototype.writeLangsIndex = function() {
if (!this.book.langs.length) return Q();
return this._writeTemplate(this.templates.langs, {
langs: this.book.langs
}, path.join(this.options.output, 'index.html'));
};
// Write glossary
Generator.prototype.writeGlossary = function() {
// No glossary
if (this.book.glossary.length === 0) return Q();
return this._writeTemplate(this.templates.glossary, {}, path.join(this.options.output, 'GLOSSARY.html'));
};
// Convert a page into a normalized data set
Generator.prototype.normalizePage = function(page) {
var that = this;
var _callHook = function(name) {
return that.callHook(name, page)
.then(function(_page) {
page = _page;
return page;
});
};
return Q()
.then(function() {
return _callHook('page');
})
.then(function() {
return page;
});
};
// Generate a template
Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
var that = this;
interpolate = interpolate || _.identity;
return Q()
.then(function() {
return that.env.render(
tpl,
_.extend({
gitbook: {
version: pkg.version
},
styles: that.styles,
revision: that.revision,
title: that.options.title,
description: that.options.description,
language: that.book.config.normalizeLanguage(),
innerlanguage: that.book.isSubBook()? that.book.config.get('language') : null,
glossary: that.book.glossary,
summary: that.book.summary,
allNavigation: that.book.navigation,
plugins: {
resources: that.book.plugins.resources(that.namespace)
},
pluginsConfig: JSON.stringify(that.options.pluginsConfig),
htmlSnippet: _.partial(_.partialRight(that.book.plugins.html, that, options), that.namespace),
options: that.options,
basePath: '.',
staticBase: path.join('.', 'gitbook'),
'__': that.book.i18n.bind(that.book)
}, options)
);
})
.then(interpolate)
.then(function(html) {
return fs.writeFile(
output,
html
);
});
};
// Copy assets
Generator.prototype.copyAssets = function() {
var that = this;
// Copy gitbook assets
return fs.copy(
path.join(that.options.theme, 'assets/'+this.namespace),
path.join(that.options.output, 'gitbook')
)
// Copy plugins assets
.then(function() {
return Q.all(
_.map(that.book.plugins.list, function(plugin) {
var pluginAssets = path.join(that.options.output, 'gitbook/plugins/', plugin.name);
return plugin.copyAssets(pluginAssets, that.namespace);
})
);
});
};
module.exports = Generator;
+16 -2
View File
@@ -4,7 +4,9 @@ var pkg = require('../package.json');
var VERSION = pkg.version;
var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
// Test if current current gitbook version satisfies a condition
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)
function satisfies(condition) {
// Test with real version
@@ -14,6 +16,18 @@ 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 = {
satisfies: satisfies
version: pkg.version,
satisfies: satisfies,
getContext: getContext
};
+2 -210
View File
@@ -1,215 +1,7 @@
/* eslint no-console: 0 */
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var tinylr = require('tiny-lr');
var color = require('bash-color');
var Book = require('./book');
var initBook = require('./init');
var Server = require('./utils/server');
var stringUtils = require('./utils/string');
var watch = require('./utils/watch');
var logger = require('./utils/logger');
var LOG_OPTION = {
name: 'log',
description: 'Minimum log level to display',
values: _.chain(logger.LEVELS).keys().map(stringUtils.toLowerCase).value(),
defaults: 'info'
};
var FORMAT_OPTION = {
name: 'format',
description: 'Format to build to',
values: ['website', 'json', 'ebook'],
defaults: 'website'
};
// Export init to gitbook library
Book.init = initBook;
var cli = require('./cli');
module.exports = {
Book: Book,
LOG_LEVELS: logger.LEVELS,
commands: _.flatten([
{
name: 'build [book] [output]',
description: 'build a book',
options: [
FORMAT_OPTION,
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var output = args[1] || path.join(input, '_book');
var book = new Book(input, _.extend({}, {
'config': {
'output': output
},
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generate(kwargs.format);
})
.then(function(){
console.log('');
console.log(color.green('Done, without error'));
});
}
},
_.map(['pdf', 'epub', 'mobi'], function(ebookType) {
return {
name: ebookType+' [book] [output]',
description: 'build a book to '+ebookType,
options: [
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var output = args[1];
var book = new Book(input, _.extend({}, {
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generateFile(output, {
ebookFormat: ebookType
});
})
.then(function(){
console.log('');
console.log(color.green('Done, without error'));
});
}
};
}),
{
name: 'serve [book]',
description: 'Build then serve a gitbook 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
},
FORMAT_OPTION,
LOG_OPTION
],
exec: function(args, kwargs) {
var input = args[0] || process.cwd();
var server = new Server();
// Init livereload server
var lrServer = tinylr({});
var lrPath;
var generate = function() {
if (server.isRunning()) console.log('Stopping server');
return server.stop()
.then(function() {
var book = new Book(input, _.extend({}, {
'config': {
'defaultsPlugins': ['livereload']
},
'logLevel': kwargs.log
}));
return book.parse()
.then(function() {
return book.generate(kwargs.format);
})
.thenResolve(book);
})
.then(function(book) {
console.log();
console.log('Starting server ...');
return server.start(book.options.output, kwargs.port)
.then(function() {
console.log('Serving book on http://localhost:'+kwargs.port);
if (lrPath) {
// trigger livereload
lrServer.changed({
body: {
files: [lrPath]
}
});
}
if (!kwargs.watch) return;
return watch(book.root)
.then(function(filepath) {
// set livereload path
lrPath = filepath;
console.log('Restart after change in file', filepath);
console.log('');
return generate();
});
});
});
};
return Q.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();
});
}
},
{
name: 'install [book]',
description: 'install plugins dependencies',
exec: function(args) {
var input = args[0] || process.cwd();
var book = new Book(input);
return book.config.load()
.then(function() {
return book.plugins.install();
})
.then(function(){
console.log('');
console.log(color.green('Done, without error'));
});
}
},
{
name: 'init [directory]',
description: 'create files and folders based on contents of SUMMARY.md',
exec: function(args) {
return initBook(args[0] || process.cwd())
.then(function(){
console.log('');
console.log(color.green('Done, without error'));
});
}
}
])
commands: cli.commands
};
-83
View File
@@ -1,83 +0,0 @@
var _ = require('lodash');
var Q = require('q');
var path = require('path');
var Book = require('./book');
var fs = require('./utils/fs');
// Initialize folder structure for a book
// Read SUMMARY to created the right chapter
function initBook(root, opts) {
var book = new Book(root, opts);
var extensionToUse = '.md';
var chaptersPaths = function(chapters) {
return _.reduce(chapters || [], function(accu, chapter) {
var o = {
title: chapter.title
};
if (chapter.path) o.path = chapter.path;
return accu.concat(
[o].concat(chaptersPaths(chapter.articles))
);
}, []);
};
book.log.info.ln('init book at', root);
return fs.mkdirp(root)
.then(function() {
book.log.info.ln('detect structure from SUMMARY (if it exists)');
return book.parseSummary();
})
.fail(function() {
return Q();
})
.then(function() {
var summary = book.summaryFile || 'SUMMARY.md';
var chapters = book.summary.chapters || [];
extensionToUse = path.extname(summary);
if (chapters.length === 0) {
chapters = [
{
title: 'Summary',
path: 'SUMMARY'+extensionToUse
},
{
title: 'Introduction',
path: 'README'+extensionToUse
}
];
}
return Q(chaptersPaths(chapters));
})
.then(function(chapters) {
// Create files that don't exist
return Q.all(_.map(chapters, function(chapter) {
if (!chapter.path) return Q();
var absolutePath = path.resolve(book.root, chapter.path);
return fs.exists(absolutePath)
.then(function(exists) {
if(exists) {
book.log.info.ln('found', chapter.path);
return;
} else {
book.log.info.ln('create', chapter.path);
}
return fs.mkdirp(path.dirname(absolutePath))
.then(function() {
return fs.writeFile(absolutePath, '# '+chapter.title+'\n');
});
});
}));
})
.then(function() {
book.log.info.ln('initialization is finished');
});
}
module.exports = initBook;
+140
View File
@@ -0,0 +1,140 @@
var util = require('util');
var path = require('path');
var crc = require('crc');
var FolderOutput = require('./folder')();
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var imagesUtil = require('../utils/images');
var location = require('../utils/location');
var DEFAULT_ASSETS_FOLDER = 'assets';
/*
Mixin to inline all the assets in a book:
- Outline <svg> tags
- Download remote images
- Convert .svg images as png
*/
module.exports = function assetsInliner(Base) {
Base = Base || FolderOutput;
function AssetsInliner() {
Base.apply(this, arguments);
// Map of svg already converted
this.svgs = {};
this.inlineSvgs = {};
// Map of images already downloaded
this.downloaded = {};
}
util.inherits(AssetsInliner, Base);
// Output a SVG buffer as a file
AssetsInliner.prototype.onOutputSVG = function(page, svg) {
this.log.debug.ln('output svg from', page.path);
// Convert svg buffer to a png file
return this.convertSVGBuffer(svg)
// Return relative path from the page
.then(function(filename) {
return page.relative('/' + filename);
});
};
// Output an image as a file
AssetsInliner.prototype.onOutputImage = function(page, src) {
var that = this;
return Promise()
// Download file if external
.then(function() {
if (!location.isExternal(src)) return;
return that.downloadAsset(src)
.then(function(_asset) {
src = '/' + _asset;
});
})
.then(function() {
// Resolve src to a relative filepath to the book's root
src = page.resolveLocal(src);
// Already a PNG/JPG/.. ?
if (path.extname(src).toLowerCase() != '.svg') {
return src;
}
// Convert SVG to PNG
return that.convertSVGFile(that.resolve(src));
})
// Return relative path from the page
.then(function(filename) {
return page.relative(filename);
});
};
// Download an asset if not already download; returns the output file
AssetsInliner.prototype.downloadAsset = function(src) {
if (this.downloaded[src]) return Promise(this.downloaded[src]);
var that = this;
var ext = path.extname(src);
var hash = crc.crc32(src).toString(16);
// Create new file
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext)
.then(function(filename) {
that.downloaded[src] = filename;
that.log.debug.ln('downloading asset', src);
return fs.download(src, that.resolve(filename))
.thenResolve(filename);
});
};
// Convert a .svg into an .png
// Return the output filename for the .png
AssetsInliner.prototype.convertSVGFile = function(src) {
if (this.svgs[src]) return Promise(this.svgs[src]);
var that = this;
var hash = crc.crc32(src).toString(16);
// Create new file
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
.then(function(filename) {
that.svgs[src] = filename;
return imagesUtil.convertSVGToPNG(src, that.resolve(filename))
.thenResolve(filename);
});
};
// Convert an inline svg into an .png
// Return the output filename for the .png
AssetsInliner.prototype.convertSVGBuffer = function(buf) {
var that = this;
var hash = crc.crc32(buf).toString(16);
// Already converted?
if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]);
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
.then(function(filename) {
that.inlineSvgs[hash] = filename;
return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename))
.thenResolve(filename);
});
};
return AssetsInliner;
};
+274
View File
@@ -0,0 +1,274 @@
var _ = require('lodash');
var Ignore = require('ignore');
var path = require('path');
var Promise = require('../utils/promise');
var pathUtil = require('../utils/path');
var location = require('../utils/location');
var PluginsManager = require('../plugins');
var TemplateEngine = require('../template');
/*
Output is like a stream interface for a parsed book
to output "something".
The process is mostly on the behavior of "onPage" and "onAsset"
*/
function Output(book, opts, parent) {
_.bindAll(this);
this.parent = parent;
this.opts = _.defaults({}, opts || {}, {
directoryIndex: true
});
this.book = book;
this.log = this.book.log;
// Create plugins manager
this.plugins = new PluginsManager(this.book);
// Create template engine
this.template = new TemplateEngine(this);
// Files to ignore in output
this.ignore = Ignore();
}
// Default extension for output
Output.prototype.defaultExtension = '.html';
// Start the generation, for a parsed book
Output.prototype.generate = function() {
var that = this;
var isMultilingual = this.book.isMultilingual();
return Promise()
// Load all plugins
.then(function() {
return that.plugins.loadAll()
.then(function() {
that.template.addFilters(that.plugins.getFilters());
that.template.addBlocks(that.plugins.getBlocks());
});
})
// Transform the configuration
.then(function() {
return that.plugins.hook('config', that.book.config.dump())
.then(function(cfg) {
that.book.config.replace(cfg);
});
})
// Initialize the generation
.then(function() {
return that.plugins.hook('init');
})
.then(function() {
that.log.info.ln('preparing the generation');
return that.prepare();
})
// Process all files
.then(function() {
that.log.debug.ln('listing files');
return that.book.fs.listAllFiles(that.book.root);
})
// We want to process assets first, then pages
// Since pages can have logic based on existance of assets
.then(function(files) {
// Split into pages/assets
var byTypes = _.chain(files)
.filter(that.ignore.createFilter())
// Ignore file present in a language book
.filter(function(filename) {
return !(isMultilingual && that.book.isInLanguageBook(filename));
})
.groupBy(function(filename) {
return (that.book.hasPage(filename)? 'page' : 'asset');
})
.value();
return Promise.serie(byTypes.asset, function(filename) {
that.log.debug.ln('copy asset', filename);
return that.onAsset(filename);
})
.then(function() {
return Promise.serie(byTypes.page, function(filename) {
that.log.debug.ln('process page', filename);
return that.onPage(that.book.getPage(filename));
});
});
})
// Generate sub-books
.then(function() {
if (!that.book.isMultilingual()) return;
return Promise.serie(that.book.books, function(subbook) {
that.log.info.ln('');
that.log.info.ln('start generation of language "' + path.relative(that.book.root, subbook.root) + '"');
var out = that.onLanguageBook(subbook);
return out.generate();
});
})
// Finish the generation
.then(function() {
return that.plugins.hook('finish:before');
})
.then(function() {
that.log.debug.ln('finishing the generation');
return that.finish();
})
.then(function() {
return that.plugins.hook('finish');
})
.then(function() {
if (!that.book.isLanguageBook()) that.log.info.ln('');
that.log.info.ok('generation finished with success!');
});
};
// Prepare the generation
Output.prototype.prepare = function() {
this.ignore.addPattern(_.compact([
'.gitignore',
'.ignore',
'.bookignore',
'node_modules',
// The configuration file should not be copied in the output
this.book.config.path,
// Structure file to ignore
this.book.summary.path,
this.book.langs.path
]));
};
// Write a page (parsable file), ex: markdown, etc
Output.prototype.onPage = function(page) {
return page.toHTML(this);
};
// Copy an asset file (non-parsable), ex: images, etc
Output.prototype.onAsset = function(filename) {
};
// Finish the generation
Output.prototype.finish = function() {
};
// Resolve an HTML link
Output.prototype.onRelativeLink = function(currentPage, href) {
var to = currentPage.followPage(href);
// Replace by an .html link
if (to) {
href = to.path;
// Recalcul as relative link
href = currentPage.relative(href);
// Replace .md by .html
href = this.outputUrl(href);
}
return href;
};
// Output a SVG buffer as a file
Output.prototype.onOutputSVG = function(page, svg) {
return null;
};
// Output an image as a file
// Normalize the relative link
Output.prototype.onOutputImage = function(page, imgFile) {
imgFile = page.resolveLocal(imgFile);
return page.relative(imgFile);
};
// Read a template by its source URL
Output.prototype.onGetTemplate = function(sourceUrl) {
throw new Error('template not found '+sourceUrl);
};
// Generate a source URL for a template
Output.prototype.onResolveTemplate = function(from, to) {
return path.resolve(path.dirname(from), to);
};
// Prepare output for a language book
Output.prototype.onLanguageBook = function(book) {
return new this.constructor(book, this.opts, this);
};
// ---- Utilities ----
// Return a default context for templates
Output.prototype.getContext = function() {
return _.extend(
{},
this.book.getContext(),
this.book.langs.getContext(),
this.book.summary.getContext(),
this.book.glossary.getContext(),
this.book.config.getContext()
);
};
// Resolve a file path in the context of a specific page
// Result is an "absolute path relative to the output folder"
Output.prototype.resolveForPage = function(page, href) {
if (_.isString(page)) page = this.book.getPage(page);
href = page.relative(href);
return this.onRelativeLink(page, href);
};
// Filename for output
// READMEs are replaced by index.html
// /test/README.md -> /test/index.html
Output.prototype.outputPath = function(filename, ext) {
ext = ext || this.defaultExtension;
var output = filename;
if (
path.basename(filename, path.extname(filename)) == 'README' ||
output == this.book.readme.path
) {
output = path.join(path.dirname(output), 'index'+ext);
} else {
output = pathUtil.setExtension(output, ext);
}
return output;
};
// Filename for output
// /test/index.html -> /test/
Output.prototype.outputUrl = function(filename, ext) {
var href = this.outputPath(filename, ext);
if (path.basename(href) == 'index.html' && this.opts.directoryIndex) {
href = path.dirname(href) + '/';
}
return location.normalize(href);
};
module.exports = Output;
+67
View File
@@ -0,0 +1,67 @@
var path = require('path');
var util = require('util');
var folderOutput = require('./folder');
var Git = require('../utils/git');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
var location = require('../utils/location');
/*
Mixin for output to resolve git conrefs
*/
module.exports = function conrefsLoader(Base) {
Base = folderOutput(Base);
function ConrefsLoader() {
Base.apply(this, arguments);
this.git = new Git();
}
util.inherits(ConrefsLoader, Base);
// Read a template by its source URL
ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
var that = this;
return this.git.resolve(sourceURL)
.then(function(filepath) {
// Is local file
if (!filepath) {
filepath = that.book.resolve(sourceURL);
} else {
that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
}
// Read file from absolute path
return fs.readFile(filepath)
.then(function(source) {
return {
src: source.toString('utf8'),
path: filepath
};
});
});
};
// Generate a source URL for a template
ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
// If origin is in the book, we enforce result file to be in the book
if (this.book.isInBook(from)) {
var href = location.toAbsolute(to, path.dirname(from), '');
return this.book.resolve(href);
}
// If origin is in a git repository, we resolve file in the git repository
var gitRoot = this.git.resolveRoot(from);
if (gitRoot) {
return pathUtil.resolveInRoot(gitRoot, to);
}
// If origin is not in the book (include from a git content ref)
return path.resolve(path.dirname(from), to);
};
return ConrefsLoader;
};
+190
View File
@@ -0,0 +1,190 @@
var _ = require('lodash');
var util = require('util');
var juice = require('juice');
var command = require('../utils/command');
var fs = require('../utils/fs');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var WebsiteOutput = require('./website');
var assetsInliner = require('./assets-inliner');
function _EbookOutput() {
WebsiteOutput.apply(this, arguments);
// ebook-convert does not support link like "./"
this.opts.directoryIndex = false;
}
util.inherits(_EbookOutput, WebsiteOutput);
var EbookOutput = assetsInliner(_EbookOutput);
EbookOutput.prototype.name = 'ebook';
// Finish generation, create ebook using ebook-convert
EbookOutput.prototype.finish = function() {
var that = this;
if (that.book.isMultilingual()) {
return EbookOutput.super_.prototype.finish.apply(that);
}
return Promise()
.then(function() {
return EbookOutput.super_.prototype.finish.apply(that);
})
// Generate SUMMARY.html
.then(function() {
return that.render('summary', that.getContext())
.then(function(html) {
return that.writeFile(
'SUMMARY.html',
html
);
});
})
// Start ebook-convert
.then(function() {
return that.ebookConvertOption();
})
.then(function(options) {
if (!that.opts.format) return;
var cmd = [
'ebook-convert',
that.resolve('SUMMARY.html'),
that.resolve('index.'+that.opts.format),
command.optionsToShellArgs(options)
].join(' ');
return command.exec(cmd)
.progress(function(data) {
that.book.log.debug(data);
})
.fail(function(err) {
if (err.code == 127) {
throw error.RequireInstallError({
cmd: 'ebook-convert',
install: 'Install it from Calibre: https://calibre-ebook.com'
});
}
throw error.EbookError(err);
});
});
};
// Generate header/footer for PDF
EbookOutput.prototype.getPDFTemplate = function(tpl) {
var that = this;
var context = _.extend(
{
// Nunjucks context mapping to ebook-convert templating
page: {
num: '_PAGENUM_',
title: '_TITLE_',
section: '_SECTION_'
}
},
this.getContext()
);
return this.render('pdf_'+tpl, context)
// Inline css, include css relative to the output folder
.then(function(output) {
return Promise.nfcall(juice.juiceResources, output, {
webResources: {
relativeTo: that.root()
}
});
});
};
// Locate the cover file to use
// Use configuration or search a "cover.jpg" file
// For multi-lingual book, it can use the one from the main book
EbookOutput.prototype.locateCover = function() {
var cover = this.book.config.get('cover', 'cover.jpg');
// Resolve to absolute
cover = this.resolve(cover);
// Cover doesn't exist and multilingual?
if (!fs.existsSync(cover)) {
if (this.parent) return this.parent.locateCover()
else return undefined;
}
return cover;
};
// Generate options for ebook-convert
EbookOutput.prototype.ebookConvertOption = function() {
var that = this;
var options = {
'--cover': this.locateCover(),
'--title': that.book.config.get('title'),
'--comments': that.book.config.get('description'),
'--isbn': that.book.config.get('isbn'),
'--authors': that.book.config.get('author'),
'--language': that.book.config.get('language'),
'--book-producer': 'GitBook',
'--publisher': 'GitBook',
'--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]',
'--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]',
'--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]',
'--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]',
'--no-chapters-in-toc': true,
'--max-levels': '1',
'--breadth-first': true
};
if (that.opts.format == 'epub') {
options = _.extend(options, {
'--dont-split-on-page-breaks': true
});
}
if (that.opts.format != 'pdf') return Promise(options);
var pdfOptions = that.book.config.get('pdf');
options = _.extend(options, {
'--chapter-mark': String(pdfOptions.chapterMark),
'--page-breaks-before': String(pdfOptions.pageBreaksBefore),
'--margin-left': String(pdfOptions.margin.left),
'--margin-right': String(pdfOptions.margin.right),
'--margin-top': String(pdfOptions.margin.top),
'--margin-bottom': String(pdfOptions.margin.bottom),
'--pdf-default-font-size': String(pdfOptions.fontSize),
'--pdf-mono-font-size': String(pdfOptions.fontSize),
'--paper-size': String(pdfOptions.paperSize),
'--pdf-page-numbers': Boolean(pdfOptions.pageNumbers),
'--pdf-header-template': that.getPDFTemplate('header'),
'--pdf-footer-template': that.getPDFTemplate('footer'),
'--pdf-sans-family': String(pdfOptions.fontFamily)
});
return that.getPDFTemplate('header')
.then(function(tpl) {
options['--pdf-header-template'] = tpl;
return that.getPDFTemplate('footer');
})
.then(function(tpl) {
options['--pdf-footer-template'] = tpl;
return options;
});
};
// Don't write multi-lingual index for wbook
EbookOutput.prototype.outputMultilingualIndex = function() {
};
module.exports = EbookOutput;
+152
View File
@@ -0,0 +1,152 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
var Output = require('./base');
var fs = require('../utils/fs');
var pathUtil = require('../utils/path');
var Promise = require('../utils/promise');
/*
This output requires the native fs module to output
book as a directory (mapping assets and pages)
*/
module.exports = function folderOutput(Base) {
Base = Base || Output;
function FolderOutput() {
Base.apply(this, arguments);
this.opts.root = path.resolve(this.opts.root || this.book.resolve('_book'));
}
util.inherits(FolderOutput, Base);
// Copy an asset file (non-parsable), ex: images, etc
FolderOutput.prototype.onAsset = function(filename) {
return this.copyFile(
this.book.resolve(filename),
filename
);
};
// Prepare the generation by creating the output folder
FolderOutput.prototype.prepare = function() {
var that = this;
return Promise()
.then(function() {
return FolderOutput.super_.prototype.prepare.apply(that);
})
// Cleanup output folder
.then(function() {
that.log.debug.ln('removing previous output directory');
return fs.rmDir(that.root())
.fail(function() {
return Promise();
});
})
// Create output folder
.then(function() {
that.log.debug.ln('creating output directory');
return fs.mkdirp(that.root());
})
// Add output folder to ignored files
.then(function() {
that.ignore.addPattern([
path.relative(that.book.root, that.root())
]);
});
};
// Prepare output for a language book
FolderOutput.prototype.onLanguageBook = function(book) {
return new this.constructor(book, _.extend({}, this.opts, {
// Language output should be output in sub-directory of output
root: path.resolve(this.root(), book.language)
}), this);
};
// ----- Utility methods -----
// Return path to the root folder
FolderOutput.prototype.root = function() {
return this.opts.root;
};
// Resolve a file in the output directory
FolderOutput.prototype.resolve = function(filename) {
return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
};
// Copy a file to the output
FolderOutput.prototype.copyFile = function(from, to) {
var that = this;
return Promise()
.then(function() {
to = that.resolve(to);
var folder = path.dirname(to);
// Ensure folder exists
return fs.mkdirp(folder);
})
.then(function() {
return fs.copy(from, to);
});
};
// Write a file/buffer to the output folder
FolderOutput.prototype.writeFile = function(filename, buf) {
var that = this;
return Promise()
.then(function() {
filename = that.resolve(filename);
var folder = path.dirname(filename);
// Ensure folder exists
return fs.mkdirp(folder);
})
// Write the file
.then(function() {
return fs.writeFile(filename, buf);
});
};
// Return true if a file exists in the output folder
FolderOutput.prototype.hasFile = function(filename) {
var that = this;
return Promise()
.then(function() {
return fs.exists(that.resolve(filename));
});
};
// Create a new unique file
// Returns its filename
FolderOutput.prototype.createNewFile = function(base, filename) {
var that = this;
if (!filename) {
filename = path.basename(filename);
base = path.dirname(base);
}
return fs.uniqueFilename(this.resolve(base), filename)
.then(function(out) {
out = path.join(base, out);
return fs.ensure(that.resolve(out))
.thenResolve(out);
});
};
return FolderOutput;
};
+47
View File
@@ -0,0 +1,47 @@
var conrefsLoader = require('./conrefs');
var JSONOutput = conrefsLoader();
JSONOutput.prototype.name = 'json';
// Don't copy asset on JSON output
JSONOutput.prototype.onAsset = function(filename) {};
// Write a page (parsable file)
JSONOutput.prototype.onPage = function(page) {
var that = this;
// Parse the page
return page.toHTML(this)
// Write as json
.then(function() {
var json = page.getContext();
// Delete some private properties
delete json.config;
// Specify JSON output version
json.version = '2';
return that.writeFile(
page.withExtension('.json'),
JSON.stringify(json, null, 4)
);
});
};
// At the end of generation, generate README.json for multilingual books
JSONOutput.prototype.finish = function() {
if (!this.book.isMultilingual()) return;
// Copy README.json from main book
var mainLanguage = this.book.langs.getDefault().id;
return this.copyFile(
this.resolve(mainLanguage, 'README.json'),
'README.json'
);
};
module.exports = JSONOutput;
+270
View File
@@ -0,0 +1,270 @@
var _ = require('lodash');
var path = require('path');
var util = require('util');
var nunjucks = require('nunjucks');
var I18n = require('i18n-t');
var Promise = require('../utils/promise');
var location = require('../utils/location');
var fs = require('../utils/fs');
var defaultFilters = require('../template/filters');
var conrefsLoader = require('./conrefs');
var Output = require('./base');
// Tranform a theme ID into a plugin
function themeID(plugin) {
return 'theme-' + plugin;
}
// Directory for a theme with the templates
function templatesPath(dir) {
return path.join(dir, '_layouts');
}
function _WebsiteOutput() {
Output.apply(this, arguments);
// Nunjucks environment
this.env;
// Plugin instance for the main theme
this.theme;
// Plugin instance for the default theme
this.defaultTheme;
// Resources loaded from plugins
this.resources;
// i18n for themes
this.i18n = new I18n();
}
util.inherits(_WebsiteOutput, Output);
var WebsiteOutput = conrefsLoader(_WebsiteOutput);
// Name of the generator
// It's being used as a prefix for templates
WebsiteOutput.prototype.name = 'website';
// Load and setup the theme
WebsiteOutput.prototype.prepare = function() {
var that = this;
return Promise()
.then(function() {
return WebsiteOutput.super_.prototype.prepare.apply(that);
})
.then(function() {
var themeName = that.book.config.get('theme');
that.theme = that.plugins.get(themeID(themeName));
that.themeDefault = that.plugins.get(themeID('default'));
if (!that.theme) {
throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
}
if (that.themeDefault.root != that.theme.root) {
that.log.info.ln('build using theme "' + themeName + '"');
}
// This list is ordered to give priority to templates in the book
var searchPaths = _.chain([
// The book itself can contains a "_layouts" folder
that.book.root,
// Installed plugin (it can be identical to themeDefault.root)
that.theme.root,
// Is default theme still installed
that.themeDefault? that.themeDefault.root : null
])
.compact()
.uniq()
.value();
// Load i18n
_.each(searchPaths.concat().reverse(), function(searchPath) {
var i18nRoot = path.resolve(searchPath, '_i18n');
if (!fs.existsSync(i18nRoot)) return;
that.i18n.load(i18nRoot);
});
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
// Add GitBook default filters
_.each(defaultFilters, function(fn, filter) {
that.env.addFilter(filter, fn);
});
// Translate using _i18n locales
that.env.addFilter('t', function(s) {
return that.i18n.t(that.book.config.get('language'), s);
});
// Transform an absolute path into a relative path
// using this.ctx.page.path
that.env.addFilter('resolveFile', function(href) {
return location.normalize(that.resolveForPage(this.ctx.file.path, href));
});
// Test if a file exists
that.env.addFilter('fileExists', function(href) {
return fs.existsSync(that.resolve(href));
});
// Transform a '.md' into a '.html' (README -> index)
that.env.addFilter('contentURL', function(s) {
return location.normalize(that.outputUrl(s));
});
// Relase path to an asset
that.env.addFilter('resolveAsset', function(href) {
href = path.join('gitbook', href);
// Resolve for current file
if (this.ctx.file) {
href = that.resolveForPage(this.ctx.file.path, '/' + href);
}
// Use assets from parent
if (that.book.isLanguageBook()) {
href = path.join('../', href);
}
return location.normalize(href);
});
})
// Copy assets from themes before copying files from book
.then(function() {
if (that.book.isLanguageBook()) return;
return Promise.serie([
// Assets from the book are already copied
// The order is reversed from the template's one
// Is default theme still installed
that.themeDefault && that.themeDefault.root != that.theme.root?
that.themeDefault.root : null,
// Installed plugin (it can be identical to themeDefault.root)
that.theme.root
], function(folder) {
if (!folder) return;
// Copy assets only if exists (don't fail otherwise)
var assetFolder = path.join(folder, '_assets', that.name);
if (!fs.existsSync(assetFolder)) return;
that.log.debug.ln('copy assets from theme', assetFolder);
return fs.copyDir(
assetFolder,
that.resolve('gitbook'),
{
deleteFirst: false, // Delete "to" before
overwrite: true,
confirm: true
}
);
});
})
// Load resources for plugins
.then(function() {
return that.plugins.getResources(that.name)
.then(function(resources) {
that.resources = resources;
});
});
};
// Write a page (parsable file)
WebsiteOutput.prototype.onPage = function(page) {
var that = this;
// Parse the page
return page.toHTML(this)
// Render the page template with the same context as the json output
.then(function() {
return that.render('page', page.getContext());
})
// Write the HTML file
.then(function(html) {
return that.writeFile(
that.outputPath(page.path),
html
);
});
};
// Finish generation, create ebook using ebook-convert
WebsiteOutput.prototype.finish = function() {
var that = this;
return Promise()
.then(function() {
return WebsiteOutput.super_.prototype.finish.apply(that);
})
// Copy assets from plugins
.then(function() {
if (that.book.isLanguageBook()) return;
return that.plugins.copyResources(that.name, that.resolve('gitbook'));
})
// Generate homepage to select languages
.then(function() {
if (!that.book.isMultilingual()) return;
return that.outputMultilingualIndex();
});
};
// ----- Utilities ----
// Write multi-languages index
WebsiteOutput.prototype.outputMultilingualIndex = function() {
var that = this;
return that.render('languages', that.getContext())
.then(function(html) {
return that.writeFile(
'index.html',
html
);
});
};
// Render a template using nunjucks
// Templates are stored in `_layouts` folders
WebsiteOutput.prototype.render = function(tpl, context) {
var filename = this.templateName(tpl);
context = _.extend(context, {
template: {
// Same template but in the default theme
default: this.themeDefault? path.resolve(templatesPath(this.themeDefault.root), filename) : null,
// Same template but in the theme
theme: path.resolve(templatesPath(this.theme.root), filename)
},
plugins: {
resources: this.resources
},
options: this.opts
});
return Promise.nfcall(this.env.render.bind(this.env), filename, context);
};
// Return a complete name for a template
WebsiteOutput.prototype.templateName = function(name) {
return path.join(this.name, name+'.html');
};
module.exports = WebsiteOutput;
+280
View File
@@ -0,0 +1,280 @@
var _ = require('lodash');
var url = require('url');
var cheerio = require('cheerio');
var domSerializer = require('dom-serializer');
var slug = require('github-slugid');
var Promise = require('../utils/promise');
var location = require('../utils/location');
// Selector to ignore
var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6';
function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
this.opts = _.defaults(opts || {}, {
// Called once the description has been found
onDescription: function(description) { },
// Calcul new href for a relative link
onRelativeLink: _.identity,
// Output an image
onImage: _.identity,
// Syntax highlighting
onCodeBlock: _.identity,
// Output a svg, if returns null the svg is kept inlined
onOutputSVG: _.constant(null),
// Words to annotate
annotations: [],
// When an annotation is applied
onAnnotation: function () { }
});
this.$ = cheerio.load(htmlString, {
// We should parse html without trying to normalize too much
xmlMode: false,
// SVG need some attributes to use uppercases
lowerCaseAttributeNames: false,
lowerCaseTags: false
});
}
// Transform a query of elements in the page
HTMLPipeline.prototype._transform = function(query, fn) {
var that = this;
var $elements = this.$(query);
return Promise.serie($elements, function(el) {
var $el = that.$(el);
return fn.call(that, $el);
});
};
// Normalize links
HTMLPipeline.prototype.transformLinks = function() {
return this._transform('a', function($a) {
var href = $a.attr('href');
if (!href) return;
if (location.isAnchor(href)) {
// Don't "change" anchor links
} else if (location.isRelative(href)) {
// Preserve anchor
var parsed = url.parse(href);
var filename = this.opts.onRelativeLink(parsed.pathname);
$a.attr('href', filename + (parsed.hash || ''));
} else {
// External links
$a.attr('target', '_blank');
}
});
};
// Normalize images
HTMLPipeline.prototype.transformImages = function() {
return this._transform('img', function($img) {
return Promise(this.opts.onImage($img.attr('src')))
.then(function(filename) {
$img.attr('src', filename);
});
});
};
// Normalize code blocks
HTMLPipeline.prototype.transformCodeBlocks = function() {
return this._transform('code', function($code) {
// Extract language
var lang = _.chain(
($code.attr('class') || '').split(' ')
)
.map(function(cl) {
// Markdown
if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
// Asciidoc
if (cl.search('language-') === 0) return cl.slice('language-'.length);
return null;
})
.compact()
.first()
.value();
var source = $code.text();
return Promise(this.opts.onCodeBlock(source, lang))
.then(function(blk) {
if (blk.html === false) {
$code.text(blk.body);
} else {
$code.html(blk.body);
}
});
});
};
// Add ID to headings
HTMLPipeline.prototype.transformHeadings = function() {
var that = this;
this.$('h1,h2,h3,h4,h5,h6').each(function() {
var $h = that.$(this);
// Already has an ID?
if ($h.attr('id')) return;
$h.attr('id', slug($h.text()));
});
};
// Outline SVG from the HML
HTMLPipeline.prototype.transformSvgs = function() {
var that = this;
return this._transform('svg', function($svg) {
var content = [
'<?xml version="1.0" encoding="UTF-8"?>',
renderDOM(that.$, $svg)
].join('\n');
return Promise(that.opts.onOutputSVG(content))
.then(function(filename) {
if (!filename) return;
$svg.replaceWith(that.$('<img>').attr('src', filename));
});
});
};
// Annotate the content
HTMLPipeline.prototype.applyAnnotations = function() {
var that = this;
_.each(this.opts.annotations, function(annotation) {
var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' );
that.$('*').each(function() {
var $this = that.$(this);
if (
$this.is(ANNOTATION_IGNORE) ||
$this.parents(ANNOTATION_IGNORE).length > 0
) return;
replaceText(that.$, this, searchRegex, function(match) {
that.opts.onAnnotation(annotation);
return '<a href="' + that.opts.onRelativeLink(annotation.href) + '" '
+ 'class="glossary-term" title="'+_.escape(annotation.description)+'">'
+ match
+ '</a>';
});
});
});
};
// Extract page description from html
// This can totally be improved
HTMLPipeline.prototype.extractDescription = function() {
var $p = this.$('p').first();
var description = $p.text().trim().slice(0, 155);
this.opts.onDescription(description);
};
// Write content to the pipeline
HTMLPipeline.prototype.output = function() {
var that = this;
return Promise()
.then(this.extractDescription)
.then(this.transformImages)
.then(this.transformHeadings)
.then(this.transformCodeBlocks)
.then(this.transformSvgs)
.then(this.applyAnnotations)
// Transform of links should be applied after annotations
// because annotations are created as links
.then(this.transformLinks)
.then(function() {
return renderDOM(that.$);
});
};
// Render a cheerio DOM as html
function renderDOM($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
}
// Replace text in an element
function replaceText($, el, search, replace, text_only ) {
return $(el).each(function(){
var node = this.firstChild,
val,
new_val,
// Elements to be removed at the end.
remove = [];
// Only continue if firstChild exists.
if ( node ) {
// Loop over all childNodes.
while (node) {
// Only process text nodes.
if ( node.nodeType === 3 ) {
// The original node value.
val = node.nodeValue;
// The new value.
new_val = val.replace( search, replace );
// Only replace text if the new value is actually different!
if ( new_val !== val ) {
if ( !text_only && /</.test( new_val ) ) {
// The new value contains HTML, set it in a slower but far more
// robust way.
$(node).before( new_val );
// Don't remove the node yet, or the loop will lose its place.
remove.push( node );
} else {
// The new value contains no HTML, so it can be set in this
// very fast, simple way.
node.nodeValue = new_val;
}
}
}
node = node.nextSibling;
}
}
// Time to remove those elements!
if (remove.length) $(remove).remove();
});
}
function pregQuote( str ) {
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
}
module.exports = HTMLPipeline;
+250
View File
@@ -0,0 +1,250 @@
var _ = require('lodash');
var path = require('path');
var direction = require('direction');
var fm = require('front-matter');
var error = require('../utils/error');
var pathUtil = require('../utils/path');
var location = require('../utils/location');
var parsers = require('../parsers');
var gitbook = require('../gitbook');
var pluginCompatibility = require('../plugins/compatibility');
var HTMLPipeline = require('./html');
/*
A page represent a parsable file in the book (Markdown, Asciidoc, etc)
*/
function Page(book, filename) {
if (!(this instanceof Page)) return new Page(book, filename);
var extension;
_.bindAll(this);
this.book = book;
this.log = this.book.log;
// Current content
this.content = '';
// Short description for the page
this.description = '';
// Relative path to the page
this.path = location.normalize(filename);
// Absolute path to the page
this.rawPath = this.book.resolve(filename);
// Last modification date
this.mtime = 0;
// Can we parse it?
extension = path.extname(this.path);
this.parser = parsers.get(extension);
if (!this.parser) throw error.ParsingError(new Error('Can\'t parse file "'+this.path+'"'));
this.type = this.parser.name;
}
// Return the filename of the page with another extension
// "README.md" -> "README.html"
Page.prototype.withExtension = function(ext) {
return pathUtil.setExtension(this.path, ext);
};
// Resolve a filename relative to this page
// It returns a path relative to the book root folder
Page.prototype.resolveLocal = function() {
var dir = path.dirname(this.path);
var file = path.join.apply(path, _.toArray(arguments));
return location.toAbsolute(file, dir, '');
};
// Resolve a filename relative to this page
// It returns an absolute path for the FS
Page.prototype.resolve = function() {
return this.book.resolve(this.resolveLocal.apply(this, arguments));
};
// Convert an absolute path (in the book) to a relative path from this page
Page.prototype.relative = function(name) {
// Convert /test.png -> test.png
name = location.toAbsolute(name, '', '');
return location.relative(
this.resolve('.') + '/',
this.book.resolve(name)
);
};
// Return a page result of a relative page from this page
Page.prototype.followPage = function(filename) {
var absPath = this.resolveLocal(filename);
return this.book.getPage(absPath);
};
// Update content of the page
Page.prototype.update = function(content) {
this.content = content;
};
// Read the page as a string
Page.prototype.read = function() {
var that = this;
return this.book.statFile(this.path)
.then(function(stat) {
that.mtime = stat.mtime;
return that.book.readFile(that.path);
})
.then(this.update);
};
// Return templating context for this page
// This is used both for themes and page parsing
Page.prototype.getContext = function() {
var article = this.book.summary.getArticle(this);
var next = article? article.next() : null;
var prev = article? article.prev() : null;
// Detect text direction in this page
var dir = this.book.config.get('direction');
if (!dir) {
dir = direction(this.content);
if (dir == 'neutral') dir = null;
}
return _.extend(
{
file: {
path: this.path,
mtime: this.mtime,
type: this.type
},
page: {
title: article? article.title : null,
description: this.description,
next: next? next.getContext() : null,
previous: prev? prev.getContext() : null,
level: article? article.level : null,
depth: article? article.depth : 0,
content: this.content,
dir: dir
}
},
gitbook.getContext(),
this.book.getContext(),
this.book.langs.getContext(),
this.book.summary.getContext(),
this.book.glossary.getContext(),
this.book.config.getContext()
);
};
// Parse the page and return its content
Page.prototype.toHTML = function(output) {
var that = this;
this.log.debug.ln('start parsing file', this.path);
// Call a hook in the output
// using an utility to "keep" compatibility with gitbook 2
function hook(name) {
return pluginCompatibility.pageHook(that, function(ctx) {
return output.plugins.hook(name, ctx);
})
.then(function(result) {
if(_.isString(result)) that.update(result);
});
}
return this.read()
// Parse yaml front matter
.then(function() {
var parsed = fm(that.content);
// Extend page with the fontmatter attribute
that.description = parsed.attributes.description || '';
// Keep only the body
that.update(parsed.body);
})
.then(function() {
return hook('page:before');
})
// Pre-process page with parser
.then(function() {
return that.parser.page.prepare(that.content)
.then(that.update);
})
// Render template
.then(function() {
return output.template.render(that.content, that.getContext(), {
path: that.path
})
.then(that.update);
})
// Render markup using the parser
.then(function() {
return that.parser.page(that.content)
.then(function(out) {
that.update(out.content);
});
})
// Post process templating
.then(function() {
return output.template.postProcess(that.content)
.then(that.update);
})
// Normalize HTML output
.then(function() {
var pipelineOpts = {
onRelativeLink: _.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that),
// Use 'code' template block
onCodeBlock: function(source, lang) {
return output.template.applyBlock('code', {
body: source,
kwargs: {
language: lang
}
});
},
// Extract description from page's content if no frontmatter
onDescription: function(description) {
if (that.description) return;
that.description = description;
},
// Convert glossary entries to annotations
annotations: that.book.glossary.annotations()
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
return pipeline.output()
.then(that.update);
})
.then(function() {
return hook('page');
})
// Return content itself
.then(function() {
return that.content;
});
};
module.exports = Page;
+60
View File
@@ -0,0 +1,60 @@
var _ = require('lodash');
var path = require('path');
var markdownParser = require('gitbook-markdown');
var asciidocParser = require('gitbook-asciidoc');
var Promise = require('./utils/promise');
// This list is ordered by priority of parsers to use
var PARSERS = [
createParser(markdownParser, {
name: 'markdown',
extensions: ['.md', '.markdown', '.mdown']
}),
createParser(asciidocParser, {
name: 'asciidoc',
extensions: ['.adoc', '.asciidoc']
})
];
// Prepare and compose a parser
function createParser(parser, base) {
var nparser = base;
nparser.glossary = Promise.wrapfn(parser.glossary);
nparser.glossary.toText = Promise.wrapfn(parser.glossary.toText);
nparser.summary = Promise.wrapfn(parser.summary);
nparser.summary.toText = Promise.wrapfn(parser.summary.toText);
nparser.langs = Promise.wrapfn(parser.langs);
nparser.langs.toText = Promise.wrapfn(parser.langs.toText);
nparser.readme = Promise.wrapfn(parser.readme);
nparser.page = Promise.wrapfn(parser.page);
nparser.page.prepare = Promise.wrapfn(parser.page.prepare || _.identity);
return nparser;
}
// Return a specific parser according to an extension
function getParser(ext) {
return _.find(PARSERS, function(input) {
return input.name == ext || _.contains(input.extensions, ext);
});
}
// Return parser for a file
function getParserForFile(filename) {
return getParser(path.extname(filename));
}
module.exports = {
all: PARSERS,
extensions: _.flatten(_.pluck(PARSERS, 'extensions')),
get: getParser,
getForFile: getParserForFile
};
-241
View File
@@ -1,241 +0,0 @@
var _ = require('lodash');
var Q = require('q');
var path = require('path');
var url = require('url');
var fs = require('./utils/fs');
var resolve = require('resolve');
var mergeDefaults = require('merge-defaults');
var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var version = require('./version');
var PLUGIN_PREFIX = 'gitbook-plugin-';
// Return an absolute name for the plugin (the one on NPM)
function absoluteName(name) {
if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
return [PLUGIN_PREFIX, name].join('');
}
var Plugin = function(book, name) {
this.book = book;
this.name = absoluteName(name);
this.packageInfos = {};
this.infos = {};
// Bind methods
_.bindAll(this);
_.each([
absoluteName(name),
name
], function(_name) {
// Load from the book
if (this.load(_name, book.root)) return false;
// Load from default plugins
if (this.load(_name, __dirname)) return false;
}, this);
};
// Type of plugins resources
Plugin.RESOURCES = ['js', 'css'];
Plugin.HOOKS = [
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
];
// Return the reduce name for the plugin
// "gitbook-plugin-test" -> "test"
// Return a relative name for the plugin (the one on GitBook)
Plugin.prototype.reducedName = function() {
return this.name.replace(PLUGIN_PREFIX, '');
};
// Load from a name
Plugin.prototype.load = function(name, baseDir) {
try {
var res = resolve.sync(name+'/package.json', { basedir: baseDir });
this.baseDir = path.dirname(res);
this.packageInfos = require(res);
this.infos = require(resolve.sync(name, { basedir: baseDir }));
this.name = this.packageInfos.name;
return true;
} catch (e) {
this.packageInfos = {};
this.infos = {};
return false;
}
};
Plugin.prototype.normalizeResource = function(resource) {
// Parse the resource path
var parsed = url.parse(resource);
// This is a remote resource
// so we will simply link to using it's URL
if (parsed.protocol) {
return {
'url': resource
};
}
// This will be copied over from disk
// and shipped with the book's build
return { 'path': this.name+'/'+resource };
};
// Return resources
Plugin.prototype._getResources = function(base) {
base = base;
var book = this.infos[base];
// Compatibility with version 1.x.x
if (base == 'website') book = book || this.infos.book;
// Nothing specified, fallback to default
if (!book) {
return Q({});
}
// Dynamic function
if(typeof book === 'function') {
// Call giving it the context of our book
return Q().then(book.bind(this.book));
}
// Plain data object
return Q(_.cloneDeep(book));
};
// Normalize resources and return them
Plugin.prototype.getResources = function(base) {
var that = this;
return this._getResources(base)
.then(function(resources) {
_.each(Plugin.RESOURCES, function(resourceType) {
resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
});
return resources;
});
};
// Normalize filters and return them
Plugin.prototype.getFilters = function() {
return this.infos.filters || {};
};
// Normalize blocks and return them
Plugin.prototype.getBlocks = function() {
return this.infos.blocks || {};
};
// Test if it's a valid plugin
Plugin.prototype.isValid = function() {
var that = this;
var isValid = (
this.packageInfos &&
this.packageInfos.name &&
this.packageInfos.engines &&
this.packageInfos.engines.gitbook &&
version.satisfies(this.packageInfos.engines.gitbook)
);
// Valid hooks
_.each(this.infos.hooks, function(hook, hookName) {
if (_.contains(Plugin.HOOKS, hookName)) return;
that.book.log.warn.ln('Hook "'+hookName+'"" used by plugin "'+that.packageInfos.name+'" has been removed or is deprecated');
});
return isValid;
};
// Normalize, validate configuration for this plugin using its schema
// Throw an error when shcema is not respected
Plugin.prototype.validateConfig = function(config) {
var that = this;
return Q()
.then(function() {
var schema = that.packageInfos.gitbook || {};
if (!schema) return config;
// Normalize schema
schema.id = '/pluginsConfig.'+that.reducedName();
schema.type = 'object';
// Validate and throw if invalid
var v = new jsonschema.Validator();
var result = v.validate(config, schema, {
propertyName: 'pluginsConfig.'+that.reducedName()
});
// Throw error
if (result.errors.length > 0) {
throw new Error('Configuration Error: '+result.errors[0].stack);
}
// Insert default values
var defaults = jsonSchemaDefaults(schema);
return mergeDefaults(config, defaults);
});
};
// Resolve file path
Plugin.prototype.resolveFile = function(filename) {
return path.resolve(this.baseDir, filename);
};
// Resolve file path
Plugin.prototype.callHook = function(name, data) {
// Our book will be the context to apply
var context = this.book;
var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
data = data || {};
if (!hookFunc) return Q(data);
this.book.log.debug.ln('call hook', name);
if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
return Q()
.then(function() {
return hookFunc.apply(context, [data]);
});
};
// Copy plugin assets fodler
Plugin.prototype.copyAssets = function(out, base) {
var that = this;
return this.getResources(base)
.get('assets')
.then(function(assets) {
// Assets are undefined
if(!assets) return false;
return fs.copy(
that.resolveFile(assets),
out
).then(_.constant(true));
}, _.constant(false));
};
// Get config from book
Plugin.prototype.getConfig = function() {
return this.book.config.get('pluginsConfig.'+this.reducedName(), {});
};
// Set configuration for this plugin
Plugin.prototype.setConfig = function(values) {
return this.book.config.set('pluginsConfig.'+this.reducedName(), values);
};
module.exports = Plugin;
+57
View File
@@ -0,0 +1,57 @@
var _ = require('lodash');
var error = require('../utils/error');
/*
Return the context for a plugin.
It tries to keep compatibilities with GitBook v2
*/
function pluginCtx(plugin) {
var book = plugin.book;
var ctx = {
config: book.config,
log: plugin.log,
// Paths
resolve: book.resolve
};
// Deprecation
error.deprecateField(ctx, 'options', book.config.dump(), '"options" property is deprecated, use config.get(key) instead');
// Loop for template filters/blocks
error.deprecateField(ctx, 'book', ctx, '"book" property is deprecated, use "this" directly instead');
return ctx;
}
// Call a function "fn" with a context of page similar to the one in GitBook v2
function pageHook(page, fn) {
var ctx = {
type: page.type,
content: page.content,
path: page.path,
rawPath: page.rawPath
};
// Deprecate sections
error.deprecateField(ctx, 'sections', [
{ content: ctx.content }
], '"sections" property is deprecated, use page.content instead');
return fn(ctx)
.then(function(result) {
if (!result) return undefined;
if (result.content) {
return result.content;
}
if (result.sections) {
return _.pluck(result.sections, 'content').join('\n');
}
});
}
module.exports = {
pluginCtx: pluginCtx,
pageHook: pageHook
};
+155
View File
@@ -0,0 +1,155 @@
var _ = require('lodash');
var path = require('path');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var BookPlugin = require('./plugin');
var registry = require('./registry');
var pluginsConfig = require('../config/plugins');
/*
PluginsManager is an interface to work with multiple plugins at once:
- Extract assets from plugins
- Call hooks for all plugins, etc
*/
function PluginsManager(book) {
this.book = book;
this.log = this.book.log;
this.plugins = [];
_.bindAll(this);
}
// Return count of plugins loaded
PluginsManager.prototype.count = function() {
return _.size(this.plugins);
};
// Returns a plugin by its name
PluginsManager.prototype.get = function(name) {
return _.find(this.plugins, {
id: name
});
};
// Load a plugin, or a list of plugins
PluginsManager.prototype.load = function(name) {
var that = this;
if (_.isArray(name)) {
return Promise.serie(name, function(_name) {
return that.load(_name);
});
}
return Promise()
// Initiate and load the plugin
.then(function() {
var plugin;
if (!_.isString(name)) plugin = name;
else plugin = new BookPlugin(that.book, name);
if (that.get(plugin.id)) {
throw new Error('Plugin "'+plugin.id+'" is already loaded');
}
if (plugin.isLoaded()) return plugin;
else return plugin.load()
.thenResolve(plugin);
})
// Setup the plugin
.then(this._setup);
};
// Load all plugins from the book's configuration
PluginsManager.prototype.loadAll = function() {
var plugins = _.pluck(this.book.config.get('plugins'), 'name');
this.log.info.ln('loading', plugins.length, 'plugins');
return this.load(plugins);
};
// Setup a plugin
// Register its filter, blocks, etc
PluginsManager.prototype._setup = function(plugin) {
this.plugins.push(plugin);
};
// Install all plugins for the book
PluginsManager.prototype.install = function() {
var that = this;
var plugins = _.filter(this.book.config.get('plugins'), function(plugin) {
return !pluginsConfig.isDefaultPlugin(plugin.name);
});
if (plugins.length == 0) {
this.log.info.ln('nothing to install!');
return Promise(0);
}
this.log.info.ln('installing', plugins.length, 'plugins');
return Promise.serie(plugins, function(plugin) {
return registry.install(that.book, plugin.name, plugin.version);
})
.thenResolve(plugins.length);
};
// Call a hook on all plugins to transform an input
PluginsManager.prototype.hook = function(name, input) {
return Promise.reduce(this.plugins, function(current, plugin) {
return plugin.hook(name, current);
}, input);
};
// Extract all resources for a namespace
PluginsManager.prototype.getResources = function(namespace) {
return Promise.reduce(this.plugins, function(out, plugin) {
return plugin.getResources(namespace)
.then(function(pluginResources) {
_.each(BookPlugin.RESOURCES, function(resourceType) {
out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []);
});
return out;
});
}, {});
};
// Copy all resources for a plugin
PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
return Promise.serie(this.plugins, function(plugin) {
return plugin.getResources(namespace)
.then(function(resources) {
if (!resources.assets) return;
var input = path.resolve(plugin.root, resources.assets);
var output = path.resolve(outputRoot, plugin.npmId);
return fs.copyDir(input, output);
});
});
};
// Get all filters and blocks
PluginsManager.prototype.getFilters = function() {
return _.reduce(this.plugins, function(out, plugin) {
var filters = plugin.getFilters();
return _.extend(out, filters);
}, {});
};
PluginsManager.prototype.getBlocks = function() {
return _.reduce(this.plugins, function(out, plugin) {
var blocks = plugin.getBlocks();
return _.extend(out, blocks);
}, {});
};
module.exports = PluginsManager;
+300
View File
@@ -0,0 +1,300 @@
var _ = require('lodash');
var path = require('path');
var url = require('url');
var resolve = require('resolve');
var mergeDefaults = require('merge-defaults');
var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');
var compatibility = require('./compatibility');
var HOOKS = [
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
];
var RESOURCES = ['js', 'css'];
// Return true if an error is a "module not found"
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
return err.message.indexOf('Cannot find module') >= 0;
}
function BookPlugin(book, pluginId) {
this.book = book;
this.log = this.book.log.prefix(pluginId);
this.id = pluginId;
this.npmId = registry.npmId(pluginId);
this.root;
this.packageInfos = undefined;
this.content = undefined;
// Cache for resources
this._resources = {};
_.bindAll(this);
}
// Return true if plugin has been loaded correctly
BookPlugin.prototype.isLoaded = function() {
return Boolean(this.packageInfos && this.content);
};
// Bind a function to the plugin's context
BookPlugin.prototype.bind = function(fn) {
return fn.bind(compatibility.pluginCtx(this));
};
// Load this plugin
// An optional folder to search in can be passed
BookPlugin.prototype.load = function(folder) {
var that = this;
if (this.isLoaded()) {
return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
}
// Fodlers to search plugins in
var searchPaths = _.compact([
folder,
this.book.resolve('node_modules'),
__dirname
]);
// Try loading plugins from different location
var p = Promise.some(searchPaths, function(baseDir) {
// Locate plugin and load pacjage.json
try {
var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
that.root = path.dirname(res);
that.packageInfos = require(res);
} catch (err) {
if (!isModuleNotFound(err)) throw err;
that.packageInfos = undefined;
that.content = undefined;
return false;
}
// Load plugin JS content
try {
that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
} catch(err) {
// It's no big deal if the plugin doesn't have an "index.js"
// (For example: themes)
if (isModuleNotFound(err)) {
that.content = {};
} else {
throw new error.PluginError(err, {
plugin: that.id
});
}
}
return true;
})
.then(that.validate)
// Validate the configuration and update it
.then(function() {
var config = that.book.config.get(that.getConfigKey(), {});
return that.validateConfig(config);
})
.then(function(config) {
that.book.config.set(that.getConfigKey(), config);
});
this.log.info('loading plugin "' + this.id + '"... ');
return this.log.info.promise(p);
};
// Verify the definition of a plugin
// Also verify that the plugin accepts the current gitbook version
// This method throws erros if plugin is invalid
BookPlugin.prototype.validate = function() {
var isValid = (
this.packageInfos &&
this.packageInfos.name &&
this.packageInfos.engines &&
this.packageInfos.engines.gitbook
);
if (!this.isLoaded()) {
throw new Error('Couldn\'t locate plugin "' + this.id + '", Run \'gitbook install\' to install plugins from registry.');
}
if (!isValid) {
throw new Error('Invalid plugin "' + this.id + '"');
}
if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook);
}
};
// Normalize, validate configuration for this plugin using its schema
// Throw an error when shcema is not respected
BookPlugin.prototype.validateConfig = function(config) {
var that = this;
return Promise()
.then(function() {
var schema = that.packageInfos.gitbook || {};
if (!schema) return config;
// Normalize schema
schema.id = '/'+that.getConfigKey();
schema.type = 'object';
// Validate and throw if invalid
var v = new jsonschema.Validator();
var result = v.validate(config, schema, {
propertyName: that.getConfigKey()
});
// Throw error
if (result.errors.length > 0) {
throw new error.ConfigurationError(new Error(result.errors[0].stack));
}
// Insert default values
var defaults = jsonSchemaDefaults(schema);
return mergeDefaults(config, defaults);
});
};
// Return key for configuration
BookPlugin.prototype.getConfigKey = function() {
return 'pluginsConfig.'+this.id;
};
// Call a hook and returns its result
BookPlugin.prototype.hook = function(name, input) {
var that = this;
var hookFunc = this.content.hooks? this.content.hooks[name] : null;
input = input || {};
if (!hookFunc) return Promise(input);
this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"');
if (!_.contains(HOOKS, name)) {
this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
}
return Promise()
.then(function() {
return that.bind(hookFunc)(input);
});
};
// Return resources without normalization
BookPlugin.prototype._getResources = function(base) {
var that = this;
return Promise()
.then(function() {
if (that._resources[base]) return that._resources[base];
base = base;
var book = that.content[base];
// Compatibility with version 1.x.x
if (base == 'website') book = book || that.content.book;
// Nothing specified, fallback to default
if (!book) {
return Promise({});
}
// Dynamic function
if(typeof book === 'function') {
// Call giving it the context of our book
return that.bind(book)();
}
// Plain data object
return book;
})
.then(function(resources) {
that._resources[base] = resources;
return _.cloneDeep(resources);
});
};
// Normalize a specific resource
BookPlugin.prototype.normalizeResource = function(resource) {
// Parse the resource path
var parsed = url.parse(resource);
// This is a remote resource
// so we will simply link to using it's URL
if (parsed.protocol) {
return {
'url': resource
};
}
// This will be copied over from disk
// and shipped with the book's build
return { 'path': this.npmId+'/'+resource };
};
// Normalize resources and return them
BookPlugin.prototype.getResources = function(base) {
var that = this;
return this._getResources(base)
.then(function(resources) {
_.each(RESOURCES, function(resourceType) {
resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource);
});
return resources;
});
};
// Normalize filters and return them
BookPlugin.prototype.getFilters = function() {
var that = this;
return _.mapValues(this.content.filters || {}, function(fn, filter) {
return function() {
var ctx = _.extend(compatibility.pluginCtx(that), this);
return fn.apply(ctx, arguments);
};
});
};
// Normalize blocks and return them
BookPlugin.prototype.getBlocks = function() {
var that = this;
return _.mapValues(this.content.blocks || {}, function(block, blockName) {
block = _.isFunction(block)? { process: block } : block;
var fn = block.process;
block.process = function() {
var ctx = _.extend(compatibility.pluginCtx(that), this);
return fn.apply(ctx, arguments);
};
return block;
});
};
module.exports = BookPlugin;
module.exports.RESOURCES = RESOURCES;
+115
View File
@@ -0,0 +1,115 @@
var npm = require('npm');
var npmi = require('npmi');
var semver = require('semver');
var _ = require('lodash');
var Promise = require('../utils/promise');
var gitbook = require('../gitbook');
var PLUGIN_PREFIX = 'gitbook-plugin-';
// Return an absolute name for the plugin (the one on NPM)
function npmId(name) {
if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
return [PLUGIN_PREFIX, name].join('');
}
// Return a plugin ID 9the one on GitBook
function pluginId(name) {
return name.replace(PLUGIN_PREFIX, '');
}
// Validate an NPM plugin ID
function validateId(name) {
return name.indexOf(PLUGIN_PREFIX) === 0;
}
// Initialize NPM for operations
var initNPM = _.memoize(function() {
return Promise.nfcall(npm.load, {
silent: true,
loglevel: 'silent'
});
});
// Link a plugin for use in a specific book
function linkPlugin(book, pluginPath) {
book.log('linking', pluginPath);
}
// Resolve the latest version for a plugin
function resolveVersion(plugin) {
var npnName = npmId(plugin);
return initNPM()
.then(function() {
return Promise.nfcall(npm.commands.view, [npnName+'@*', 'engines'], true);
})
.then(function(versions) {
return _.chain(versions)
.pairs()
.map(function(v) {
return {
version: v[0],
gitbook: (v[1].engines || {}).gitbook
};
})
.filter(function(v) {
return v.gitbook && gitbook.satisfies(v.gitbook);
})
.sort(function(v1, v2) {
return semver.lt(v1.version, v2.version)? 1 : -1;
})
.pluck('version')
.first()
.value();
});
}
// Install a plugin in a book
function installPlugin(book, plugin, version) {
book.log.info.ln('installing plugin', plugin);
var npnName = npmId(plugin);
return Promise()
.then(function() {
if (version) return version;
book.log.info.ln('No version specified, resolve plugin "' + plugin + '"');
return resolveVersion(plugin);
})
// Install the plugin with the resolved version
.then(function(version) {
if (!version) {
throw new Error('Found no satisfactory version for plugin "' + plugin + '"');
}
book.log.info.ln('install plugin' + plugin +'" from npm ('+npnName+') with version', version);
return Promise.nfcall(npmi, {
'name': npnName,
'version': version,
'path': book.root,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': book.root
}
});
})
.then(function() {
book.log.info.ok('plugin "' + plugin + '" installed with success');
});
}
module.exports = {
npmId: npmId,
pluginId: pluginId,
validateId: validateId,
resolve: resolveVersion,
link: linkPlugin,
install: installPlugin
};
-230
View File
@@ -1,230 +0,0 @@
var _ = require('lodash');
var Q = require('q');
var npmi = require('npmi');
var npm = require('npm');
var semver = require('semver');
var Plugin = require('./plugin');
var version = require('./version');
var initNPM = _.memoize(function() {
return Q.nfcall(npm.load, { silent: true, loglevel: 'silent' });
});
var PluginsList = function(book, plugins) {
this.book = book;
this.log = this.book.log;
// List of Plugin objects
this.list = [];
// List of names of failed plugins
this.failed = [];
// Namespaces
this.namespaces = _.chain(['website', 'ebook'])
.map(function(namespace) {
return [
namespace,
{
html: {},
resources: _.chain(Plugin.RESOURCES)
.map(function(type) {
return [type, []];
})
.object()
.value()
}
];
})
.object()
.value();
// Bind methods
_.bindAll(this);
if (plugins) this.load(plugins);
};
// return count of plugins
PluginsList.prototype.count = function() {
return this.list.length;
};
// Add and load a plugin
PluginsList.prototype.load = function(plugin) {
var that = this;
if (_.isArray(plugin)) {
return _.reduce(plugin, function(prev, p) {
return prev.then(function() {
return that.load(p);
});
}, Q());
}
if (_.isObject(plugin) && !(plugin instanceof Plugin)) plugin = plugin.name;
if (_.isString(plugin)) plugin = new Plugin(this.book, plugin);
that.log.info('load plugin', plugin.name, '....');
if (!plugin.isValid()) {
that.log.info.fail();
that.failed.push(plugin.name);
return Q();
} else {
that.log.info.ok();
// Push in the list
that.list.push(plugin);
}
return Q()
// Validate and normalize configuration
.then(function() {
var config = plugin.getConfig();
return plugin.validateConfig(config);
})
.then(function(config) {
// Update configuration
plugin.setConfig(config);
// Extract filters
that.book.template.addFilters(plugin.getFilters());
// Extract blocks
that.book.template.addBlocks(plugin.getBlocks());
return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) {
return prev.then(function() {
return plugin.getResources(namespaceName)
.then(function(plResources) {
var namespace = that.namespaces[namespaceName];
// Extract js and css
_.each(Plugin.RESOURCES, function(resourceType) {
namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []);
});
// Map of html resources by name added by each plugin
_.each(plResources.html || {}, function(value, tag) {
// Turn into function if not one already
if (!_.isFunction(value)) value = _.constant(value);
namespace.html[tag] = namespace.html[tag] || [];
namespace.html[tag].push(value);
});
});
});
}, Q());
});
};
// Call a hook
PluginsList.prototype.hook = function(name, data) {
return _.reduce(this.list, function(prev, plugin) {
return prev.then(function(ret) {
return plugin.callHook(name, ret);
});
}, Q(data));
};
// Return a template from a plugin
PluginsList.prototype.template = function(name) {
var withTpl = _.find(this.list, function(plugin) {
return (
plugin.infos.templates &&
plugin.infos.templates[name]
);
});
if (!withTpl) return null;
return withTpl.resolveFile(withTpl.infos.templates[name]);
};
// Return an html snippet
PluginsList.prototype.html = function(namespace, tag, context, options) {
var htmlSnippets = this.namespaces[namespace].html[tag];
return _.map(htmlSnippets || [], function(code) {
return code.call(context, options);
}).join('\n');
};
// Return a resources map for a namespace
PluginsList.prototype.resources = function(namespace) {
return this.namespaces[namespace].resources;
};
// Install plugins from a book
PluginsList.prototype.install = function() {
var that = this;
// Remove defaults (no need to install)
var plugins = _.reject(that.book.options.plugins, {
isDefault: true
});
// Install plugins one by one
that.book.log.info.ln(plugins.length+' plugins to install');
return _.reduce(plugins, function(prev, plugin) {
return prev.then(function() {
var fullname = 'gitbook-plugin-'+plugin.name;
return Q()
// Resolve version if needed
.then(function() {
if (plugin.version) return plugin.version;
that.book.log.info.ln('No version specified, resolve plugin', plugin.name);
return initNPM()
.then(function() {
return Q.nfcall(npm.commands.view, [fullname+'@*', 'engines'], true);
})
.then(function(versions) {
return _.chain(versions)
.pairs()
.map(function(v) {
return {
version: v[0],
gitbook: (v[1].engines || {}).gitbook
};
})
.filter(function(v) {
return v.gitbook && version.satisfies(v.gitbook);
})
.sort(function(v1, v2) {
return semver.lt(v1.version, v2.version)? 1 : -1;
})
.pluck('version')
.first()
.value();
});
})
// Install the plugin with the resolved version
.then(function(version) {
if (!version) {
throw 'Found no satisfactory version for plugin '+plugin.name;
}
that.book.log.info.ln('install plugin', plugin.name, 'from npm ('+fullname+') with version', version);
return Q.nfcall(npmi, {
'name': fullname,
'version': version,
'path': that.book.root,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': that.book.root
}
});
})
.then(function() {
that.book.log.info.ok('plugin', plugin.name, 'installed with success');
});
});
}, Q());
};
module.exports = PluginsList;
+7 -2
View File
@@ -6,6 +6,11 @@ module.exports = {
html: _.identity,
// Highlight a code block
// This block can be extent by plugins
code: _.identity
// This block can be replaced by plugins
code: function(blk) {
return {
html: false,
body: blk.body
};
}
};
+15
View File
@@ -0,0 +1,15 @@
var moment = require('moment');
module.exports = {
// Format a date
// ex: 'MMMM Do YYYY, h:mm:ss a
date: function(time, format) {
return moment(time).format(format);
},
// Relative Time
dateFromNow: function(time) {
return moment(time).fromNow();
}
};
+156 -191
View File
@@ -1,47 +1,42 @@
var _ = require('lodash');
var Q = require('q');
var path = require('path');
var nunjucks = require('nunjucks');
var parsers = require('gitbook-parsers');
var escapeStringRegexp = require('escape-string-regexp');
var batch = require('./utils/batch');
var pkg = require('../package.json');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var parsers = require('../parsers');
var defaultBlocks = require('./blocks');
var BookLoader = require('./conrefs_loader');
var defaultFilters = require('./filters');
var Loader = require('./loader');
// Normalize result from a block
// Return extension name for a specific block
function blockExtName(name) {
return 'Block'+name+'Extension';
}
// Normalize the result of block process function
function normBlockResult(blk) {
if (_.isString(blk)) blk = { body: blk };
return blk;
}
var TemplateEngine = function(book) {
var that = this;
this.book = book;
function TemplateEngine(output) {
this.output = output;
this.book = output.book;
this.log = this.book.log;
// Template loader
this.loader = new BookLoader(book, {
// Replace shortcuts in imported files
interpolate: function(filepath, source) {
var parser = parsers.get(path.extname(filepath));
var type = parser? parser.name : null;
// Create file loader
this.loader = new Loader(this);
return that.applyShortcuts(type, source);
}
});
// Nunjucks env
// Create nunjucks instance
this.env = new nunjucks.Environment(
this.loader,
{
// Escaping is done after by the markdown parser
// Escaping is done after by the asciidoc/markdown parser
autoescape: false,
// Tags
// Syntax
tags: {
blockStart: '{%',
blockEnd: '%}',
@@ -65,79 +60,48 @@ var TemplateEngine = function(book) {
// Bind methods
_.bindAll(this);
// Add default blocks
// Add default blocks and filters
this.addBlocks(defaultBlocks);
};
// Process the result of block in a context
TemplateEngine.prototype.processBlock = function(blk) {
blk = _.defaults(blk, {
parse: false,
post: undefined
});
blk.id = _.uniqueId('blk');
var toAdd = (!blk.parse) || (blk.post !== undefined);
// Add to global map
if (toAdd) this.blockBodies[blk.id] = blk;
// Parsable block, just return it
if (blk.parse) {
return blk.body;
}
// Return it as a position marker
return '@%@'+blk.id+'@%@';
};
// Replace position markers of blocks by body after processing
// This is done to avoid that markdown/asciidoc processer parse the block content
TemplateEngine.prototype.replaceBlocks = function(content) {
var that = this;
return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) {
var blk = that.blockBodies[key];
if (!blk) return match;
var body = blk.body;
return body;
});
};
this.addFilters(defaultFilters);
}
// Bind a function to a context
// Filters and blocks are binded to this context
TemplateEngine.prototype.bindContext = function(func) {
var that = this;
return function() {
var ctx = {
ctx: this.ctx,
book: that.book,
generator: that.book.options.generator
};
return func.apply(ctx, arguments);
var ctx = {
ctx: this.ctx,
output: this.output,
generator: this.output.name
};
return _.bind(func, ctx);
};
// Add filter
// Interpolate a string content to replace shortcuts according to the filetype
TemplateEngine.prototype.interpolate = function(filepath, source) {
var parser = parsers.get(path.extname(filepath));
var type = parser? parser.name : null;
return this.applyShortcuts(type, source);
};
// Add a new custom filter
TemplateEngine.prototype.addFilter = function(filterName, func) {
try {
this.env.getFilter(filterName);
this.log.warn.ln('conflict in filters, \''+filterName+'\' is already set');
this.log.error.ln('conflict in filters, "'+filterName+'" is already set');
return false;
} catch(e) {
// Filter doesn't exist
}
this.log.debug.ln('add filter \''+filterName+'\'');
this.log.debug.ln('add filter "'+filterName+'"');
this.env.addFilter(filterName, this.bindContext(function() {
var ctx = this;
var args = Array.prototype.slice.apply(arguments);
var callback = _.last(args);
Q()
Promise()
.then(function() {
return func.apply(ctx, args.slice(0, -1));
})
@@ -146,29 +110,24 @@ TemplateEngine.prototype.addFilter = function(filterName, func) {
return true;
};
// Add multiple filters
// Add multiple filters at once
TemplateEngine.prototype.addFilters = function(filters) {
_.each(filters, function(filter, name) {
this.addFilter(name, filter);
}, this);
};
// Return nunjucks extension name of a block
TemplateEngine.prototype.blockExtName = function(name) {
return 'Block'+name+'Extension';
};
// Test if a block is defined
// Return true if a block is defined
TemplateEngine.prototype.hasBlock = function(name) {
return this.env.hasExtension(this.blockExtName(name));
return this.env.hasExtension(blockExtName(name));
};
// Remove a block
// Remove/Disable a block
TemplateEngine.prototype.removeBlock = function(name) {
if (!this.hasBlock(name)) return;
// Remove nunjucks extension
this.env.removeExtension(this.blockExtName(name));
this.env.removeExtension(blockExtName(name));
// Cleanup shortcuts
this.shortcuts = _.reject(this.shortcuts, {
@@ -177,22 +136,27 @@ TemplateEngine.prototype.removeBlock = function(name) {
};
// Add a block
// Using the extensions of nunjucks: https://mozilla.github.io/nunjucks/api.html#addextension
TemplateEngine.prototype.addBlock = function(name, block) {
var that = this, Ext, extName;
// Block can be a simple function
if (_.isFunction(block)) block = { process: block };
block = _.defaults(block || {}, {
shortcuts: [],
end: 'end'+name,
process: _.identity,
blocks: []
});
extName = this.blockExtName(name);
extName = blockExtName(name);
if (!block.process) {
throw new Error('Invalid block "' + name + '", it should have a "process" method');
}
if (this.hasBlock(name) && !defaultBlocks[name]) {
this.log.warn.ln('conflict in blocks, \''+name+'\' is already defined');
this.log.warn.ln('conflict in blocks, "'+name+'" is already defined');
}
// Cleanup previous block
@@ -215,7 +179,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
var args = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(tok.value);
while (1) {
do {
// Read body
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
@@ -232,14 +196,14 @@ TemplateEngine.prototype.addBlock = function(name, block) {
// Read new block
lastBlockName = parser.peekToken().value;
if (lastBlockName == block.end) {
break;
}
// Parse signature and move to the end of the block
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(lastBlockName);
}
if (lastBlockName != block.end) {
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(lastBlockName);
}
} while (lastBlockName != block.end)
parser.advanceAfterBlockEnd();
var bodies = [body];
@@ -282,7 +246,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
};
});
Q()
Promise()
.then(function() {
return that.applyBlock(name, {
body: body(),
@@ -292,7 +256,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
}, context);
})
// process the block returned
// Process the block returned
.then(that.processBlock)
.nodeify(callback);
};
@@ -301,10 +265,13 @@ TemplateEngine.prototype.addBlock = function(name, block) {
// Add the Extension
this.env.addExtension(extName, new Ext());
// Add shortcuts
if (!_.isArray(block.shortcuts)) block.shortcuts = [block.shortcuts];
// Add shortcuts if any
if (!_.isArray(block.shortcuts)) {
block.shortcuts = [block.shortcuts];
}
_.each(block.shortcuts, function(shortcut) {
this.log.debug.ln('add template shortcut from \''+shortcut.start+'\' to block \''+name+'\' for parsers ', shortcut.parsers);
this.log.debug.ln('add template shortcut from "'+shortcut.start+'" to block "'+name+'" for parsers ', shortcut.parsers);
this.shortcuts.push({
block: name,
parsers: shortcut.parsers,
@@ -318,7 +285,7 @@ TemplateEngine.prototype.addBlock = function(name, block) {
}, this);
};
// Add multiple blocks
// Add multiple blocks at once
TemplateEngine.prototype.addBlocks = function(blocks) {
_.each(blocks, function(block, name) {
this.addBlock(name, block);
@@ -331,7 +298,7 @@ TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
var func, block, r;
block = this.blocks[name];
if (!block) throw new Error('Block not found \''+name+'\'');
if (!block) throw new Error('Block not found "'+name+'"');
if (_.isString(blk)) {
blk = {
body: blk
@@ -348,13 +315,63 @@ TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
func = this.bindContext(block.process);
r = func.call(ctx || {}, blk);
if (Q.isPromise(r)) return r.then(normBlockResult);
if (Promise.isPromise(r)) return r.then(normBlockResult);
else return normBlockResult(r);
};
// Process the result of block in a context
TemplateEngine.prototype.processBlock = function(blk) {
blk = _.defaults(blk, {
parse: false,
post: undefined
});
blk.id = _.uniqueId('blk');
var toAdd = (!blk.parse) || (blk.post !== undefined);
// Add to global map
if (toAdd) this.blockBodies[blk.id] = blk;
// Parsable block, just return it
if (blk.parse) {
return blk.body;
}
// Return it as a position marker
return '@%@'+blk.id+'@%@';
};
// Render a string (without post processing)
TemplateEngine.prototype.render = function(content, context, options) {
options = _.defaults(options || {}, {
path: null
});
var filename = options.path;
// Setup path and type
if (options.path) {
options.path = this.book.resolve(options.path);
}
// Replace shortcuts
content = this.applyShortcuts(options.type, content);
return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
throw error.TemplateError(err, {
filename: filename || '<inline>'
});
});
};
// Render a string with post-processing
TemplateEngine.prototype.renderString = function(content, context, options) {
return this.render(content, context, options)
.then(this.postProcess);
};
// Apply a shortcut to a string
TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
if (!_.contains(shortcut.parsers, parser)) return content;
TemplateEngine.prototype.applyShortcut = function(content, shortcut) {
var regex = new RegExp(
escapeStringRegexp(shortcut.start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(shortcut.end),
'g'
@@ -364,100 +381,48 @@ TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
});
};
// Apply all shortcuts to some template string
// Replace position markers of blocks by body after processing
// This is done to avoid that markdown/asciidoc processer parse the block content
TemplateEngine.prototype.replaceBlocks = function(content) {
var that = this;
return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) {
var blk = that.blockBodies[key];
if (!blk) return match;
var body = blk.body;
return body;
});
};
// Apply all shortcuts to a template
TemplateEngine.prototype.applyShortcuts = function(type, content) {
return _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), type), content);
return _.chain(this.shortcuts)
.filter(function(shortcut) {
return _.contains(shortcut.parsers, type);
})
.reduce(this.applyShortcut, content)
.value();
};
// Render a string from the book
TemplateEngine.prototype.renderString = function(content, context, options) {
context = _.extend({}, context, {
// Variables from book.json
book: this.book.options.variables,
// Complete book.json
config: this.book.options,
// infos about gitbook
gitbook: {
version: pkg.version,
generator: this.book.options.generator
}
});
options = _.defaults(options || {}, {
path: null,
type: null
});
if (options.path) options.path = this.book.resolve(options.path);
if (!options.type && options.path) {
var parser = parsers.get(path.extname(options.path));
options.type = parser? parser.name : null;
}
// Replace shortcuts
content = this.applyShortcuts(options.type, content);
return Q.nfcall(this.env.renderString.bind(this.env), content, context, options)
.fail(function(err) {
if (_.isString(err)) err = new Error(err);
err.message = err.message.replace(/^Error: /, '');
throw err;
});
};
// Render a file from the book
TemplateEngine.prototype.renderFile = function(filename) {
var that = this;
return that.book.readFile(filename)
.then(function(content) {
return that.renderString(content, {}, {
path: filename
});
});
};
// Render a page from the book
TemplateEngine.prototype.renderPage = function(page) {
var that = this;
return that.book.statFile(page.path)
.then(function(stat) {
var context = {
// infos about the file
file: {
path: page.path,
mtime: stat.mtime
}
};
return that.renderString(page.content, context, {
path: page.path,
type: page.type
});
});
};
// Post process content
TemplateEngine.prototype.postProcess = function(content) {
var that = this;
return Q(content)
return Promise(content)
.then(that.replaceBlocks)
.then(function(_content) {
return batch.execEach(that.blockBodies, {
max: 20,
fn: function(blk, blkId) {
return Q()
.then(function() {
if (!blk.post) return Q();
return blk.post();
})
.then(function() {
delete that.blockBodies[blkId];
});
}
return Promise.serie(that.blockBodies, function(blk, blkId) {
return Promise()
.then(function() {
if (!blk.post) return;
return blk.post();
})
.then(function() {
delete that.blockBodies[blkId];
});
})
.thenResolve(_content);
});
+42
View File
@@ -0,0 +1,42 @@
var nunjucks = require('nunjucks');
var location = require('../utils/location');
/*
Simple nunjucks loader which is passing the reponsability to the Output
*/
var Loader = nunjucks.Loader.extend({
async: true,
init: function(engine, opts) {
this.engine = engine;
this.output = engine.output;
},
getSource: function(sourceURL, callback) {
var that = this;
this.output.onGetTemplate(sourceURL)
.then(function(out) {
// We disable cache since content is modified (shortcuts, ...)
out.noCache = true;
// Transform template before runnign it
out.source = that.engine.interpolate(out.path, out.source);
return out;
})
.nodeify(callback);
},
resolve: function(from, to) {
return this.output.onResolveTemplate(from, to);
},
// Handle all files as relative, so that nunjucks pass responsability to 'resolve'
isRelative: function(filename) {
return location.isRelative(filename);
}
});
module.exports = Loader;
-52
View File
@@ -1,52 +0,0 @@
var Q = require("q");
var _ = require("lodash");
// Execute a method for all element
function execEach(items, options) {
if (_.size(items) === 0) return Q();
var concurrents = 0, d = Q.defer(), pending = [];
options = _.defaults(options || {}, {
max: 100,
fn: function() {}
});
function startItem(item, i) {
if (concurrents >= options.max) {
pending.push([item, i]);
return;
}
concurrents++;
Q()
.then(function() {
return options.fn(item, i);
})
.then(function() {
concurrents--;
// Next pending
var next = pending.shift();
if (concurrents === 0 && !next) {
d.resolve();
} else if (next) {
startItem.apply(null, next);
}
})
.fail(function(err) {
pending = [];
d.reject(err);
});
}
_.each(items, startItem);
return d.promise;
}
module.exports = {
execEach: execEach
};
+80
View File
@@ -0,0 +1,80 @@
var _ = require('lodash');
var childProcess = require('child_process');
var spawn = require("spawn-cmd").spawn;
var Promise = require('./promise');
// Execute a command
function exec(command, options) {
var d = Promise.defer();
var child = childProcess.exec(command, options, function(err, stdout, stderr) {
if (!err) {
return d.resolve();
}
err.message = stdout.toString('utf8') + stderr.toString('utf8');
d.reject(err);
});
child.stdout.on('data', function (data) {
d.notify(data);
});
child.stderr.on('data', function (data) {
d.notify(data);
});
return d.promise;
}
// Spawn an executable
function spawnCmd(command, args, options) {
var d = Promise.defer();
var child = spawn(command, args, options);
child.on('error', function(error) {
return d.reject(error);
});
child.stdout.on('data', function (data) {
d.notify(data);
});
child.stderr.on('data', function (data) {
d.notify(data);
});
child.on('close', function(code) {
if (code === 0) {
d.resolve();
} else {
d.reject(new Error('Error with command "'+command+'"'));
}
});
return d.promise;
}
// Transform an option object to a command line string
function escapeShellArg(s) {
s = s.replace(/"/g, '\\"');
return '"' + s + '"';
}
function optionsToShellArgs(options) {
return _.chain(options)
.map(function(value, key) {
if (value === null || value === undefined || value === false) return null;
if (value === true) return key;
return key + '=' + escapeShellArg(value);
})
.compact()
.value()
.join(' ');
}
module.exports = {
exec: exec,
spawn: spawnCmd,
optionsToShellArgs: optionsToShellArgs
};
+105
View File
@@ -0,0 +1,105 @@
var _ = require('lodash');
var TypedError = require('error/typed');
var WrappedError = require('error/wrapped');
var deprecated = require('deprecated');
var Logger = require('./logger');
var log = new Logger();
// Enforce as an Error object, and cleanup message
function enforce(err) {
if (_.isString(err)) err = new Error(err);
err.message = err.message.replace(/^Error: /, '');
return err;
}
// Random error wrappers during parsing/generation
var ParsingError = WrappedError({
message: 'Parsing Error: {origMessage}',
type: 'parse'
});
var OutputError = WrappedError({
message: 'Output Error: {origMessage}',
type: 'generate'
});
// A file does not exists
var FileNotFoundError = TypedError({
type: 'file.not-found',
message: 'No "{filename}" file (or is ignored)',
filename: null
});
// A file is outside the scope
var FileOutOfScopeError = TypedError({
type: 'file.out-of-scope',
message: '"{filename}" not in "{root}"',
filename: null,
root: null,
code: 'EACCESS'
});
// A file is outside the scope
var RequireInstallError = TypedError({
type: 'install.required',
message: '"{cmd}" is not installed.\n{install}',
cmd: null,
code: 'ENOENT',
install: ''
});
// Error for nunjucks templates
var TemplateError = WrappedError({
message: 'Error compiling template "{filename}": {origMessage}',
type: 'template',
filename: null
});
// Error for nunjucks templates
var PluginError = WrappedError({
message: 'Error with plugin "{plugin}": {origMessage}',
type: 'plugin',
plugin: null
});
// Error with the book's configuration
var ConfigurationError = WrappedError({
message: 'Error with book\'s configuration: {origMessage}',
type: 'configuration'
});
// Error during ebook generation
var EbookError = WrappedError({
message: 'Error during ebook generation: {origMessage}\n{stdout}',
type: 'ebook',
stdout: ''
});
// Deprecate methods/fields
function deprecateMethod(fn, msg) {
return deprecated.method(msg, log.warn.ln, fn);
}
function deprecateField(obj, prop, value, msg) {
return deprecated.field(msg, log.warn.ln, obj, prop, value);
}
module.exports = {
enforce: enforce,
ParsingError: ParsingError,
OutputError: OutputError,
RequireInstallError: RequireInstallError,
FileNotFoundError: FileNotFoundError,
FileOutOfScopeError: FileOutOfScopeError,
TemplateError: TemplateError,
PluginError: PluginError,
ConfigurationError: ConfigurationError,
EbookError: EbookError,
deprecateMethod: deprecateMethod,
deprecateField: deprecateField
};
+76 -151
View File
@@ -1,71 +1,36 @@
var _ = require('lodash');
var Q = require('q');
var tmp = require('tmp');
var path = require('path');
var fs = require('graceful-fs');
var fsExtra = require('fs-extra');
var Ignore = require('fstream-ignore');
var mkdirp = require('mkdirp');
var destroy = require('destroy');
var rmdir = require('rmdir');
var tmp = require('tmp');
var request = require('request');
var path = require('path');
var cp = require('cp');
var cpr = require('cpr');
var fsUtils = {
tmp: {
file: function(opt) {
return Q.nfcall(tmp.file.bind(tmp), opt).get(0);
},
dir: function() {
return Q.nfcall(tmp.dir.bind(tmp)).get(0);
}
},
list: listFiles,
stat: Q.denodeify(fs.stat),
readdir: Q.denodeify(fs.readdir),
readFile: Q.denodeify(fs.readFile),
writeFile: writeFile,
writeStream: writeStream,
mkdirp: Q.denodeify(fsExtra.mkdirp),
copy: Q.denodeify(fsExtra.copy),
remove: Q.denodeify(fsExtra.remove),
symlink: Q.denodeify(fsExtra.symlink),
exists: function(path) {
var d = Q.defer();
fs.exists(path, d.resolve);
return d.promise;
},
findFile: findFile,
existsSync: fs.existsSync.bind(fs),
readFileSync: fs.readFileSync.bind(fs),
clean: cleanFolder,
getUniqueFilename: getUniqueFilename
};
// Write a file
function writeFile(filename, data, options) {
var d = Q.defer();
try {
fs.writeFileSync(filename, data, options);
} catch(err) {
d.reject(err);
}
d.resolve();
return d.promise;
}
var Promise = require('./promise');
// Write a stream to a file
function writeStream(filename, st) {
var d = Q.defer();
var d = Promise.defer();
var wstream = fs.createWriteStream(filename);
var cleanup = function() {
destroy(wstream);
wstream.removeAllListeners();
};
wstream.on('finish', function () {
cleanup();
d.resolve();
});
wstream.on('error', function (err) {
cleanup();
d.reject(err);
});
st.on('error', function(err) {
cleanup();
d.reject(err);
});
@@ -74,120 +39,80 @@ function writeStream(filename, st) {
return d.promise;
}
// Find a filename available
function getUniqueFilename(base, filename) {
if (!filename) {
filename = base;
base = '/';
}
// Return a promise resolved with a boolean
function fileExists(filename) {
var d = Promise.defer();
filename = path.resolve(base, filename);
fs.exists(filename, function(exists) {
d.resolve(exists);
});
return d.promise;
}
// Generate temporary file
function genTmpFile(opts) {
return Promise.nfcall(tmp.file, opts)
.get(0);
}
// Generate temporary dir
function genTmpDir(opts) {
return Promise.nfcall(tmp.dir, opts)
.get(0);
}
// Download an image
function download(uri, dest) {
return writeStream(dest, request(uri));
}
// Find a filename available in a folder
function uniqueFilename(base, filename) {
var ext = path.extname(filename);
filename = path.resolve(base, filename);
filename = path.join(path.dirname(filename), path.basename(filename, ext));
var _filename = filename+ext;
var i = 0;
while (fs.existsSync(filename)) {
_filename = filename+'_'+i+ext;
_filename = filename + '_' + i + ext;
i = i + 1;
}
return path.relative(base, _filename);
return Promise(path.relative(base, _filename));
}
// List files in a directory
function listFiles(root, options) {
options = _.defaults(options || {}, {
ignoreFiles: [],
ignoreRules: []
});
var d = Q.defer();
// Our list of files
var files = [];
var ig = Ignore({
path: root,
ignoreFiles: options.ignoreFiles
});
// Add extra rules to ignore common folders
ig.addIgnoreRules(options.ignoreRules, '__custom_stuff');
// Push each file to our list
ig.on('child', function (c) {
files.push(
c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '')
);
});
ig.on('end', function() {
// Normalize paths on Windows
if(process.platform === 'win32') {
return d.resolve(files.map(function(file) {
return file.replace(/\\/g, '/');
}));
}
// Simply return paths otherwise
return d.resolve(files);
});
ig.on('error', d.reject);
return d.promise;
// Create all required folder to create a file
function ensureFile(filename) {
var base = path.dirname(filename);
return Promise.nfcall(mkdirp, base);
}
// Clean a folder without removing .git and .svn
// Creates it if non existant
function cleanFolder(root) {
if (!fs.existsSync(root)) return fsUtils.mkdirp(root);
return listFiles(root, {
ignoreFiles: [],
ignoreRules: [
// Skip Git and SVN stuff
'.git/',
'.svn/'
]
})
.then(function(files) {
var d = Q.defer();
_.reduce(files, function(prev, file, i) {
return prev.then(function() {
var _file = path.join(root, file);
d.notify({
i: i+1,
count: files.length,
file: _file
});
return fsUtils.remove(_file);
});
}, Q())
.then(function() {
d.resolve();
}, function(err) {
d.reject(err);
});
return d.promise;
// Remove a folder
function rmDir(base) {
return Promise.nfcall(rmdir, base, {
fs: fs
});
}
// Find a file in a folder (case incensitive)
// Return the real filename
function findFile(root, filename) {
return Q.nfcall(fs.readdir, root)
.then(function(files) {
return _.find(files, function(file) {
return (file.toLowerCase() == filename.toLowerCase());
});
});
}
module.exports = fsUtils;
module.exports = {
exists: fileExists,
existsSync: fs.existsSync,
mkdirp: Promise.nfbind(mkdirp),
readFile: Promise.nfbind(fs.readFile),
writeFile: Promise.nfbind(fs.writeFile),
stat: Promise.nfbind(fs.stat),
statSync: fs.statSync,
readdir: Promise.nfbind(fs.readdir),
writeStream: writeStream,
copy: Promise.nfbind(cp),
copyDir: Promise.nfbind(cpr),
tmpFile: genTmpFile,
tmpDir: genTmpDir,
download: download,
uniqueFilename: uniqueFilename,
ensure: ensureFile,
rmDir: rmDir
};
+96 -94
View File
@@ -1,40 +1,118 @@
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var crc = require('crc');
var exec = Q.denodeify(require('child_process').exec);
var URI = require('urijs');
var pathUtil = require('./path');
var pathUtil = require('./path');
var Promise = require('./promise');
var command = require('./command');
var fs = require('./fs');
var GIT_PREFIX = 'git+';
var GIT_TMP = null;
function Git() {
this.tmpDir;
this.cloned = {};
}
// Return an unique ID for a combinaison host/ref
Git.prototype.repoID = function(host, ref) {
return crc.crc32(host+'#'+(ref || '')).toString(16);
};
// Allocate a temporary folder for cloning repos in it
Git.prototype.allocateDir = function() {
var that = this;
if (this.tmpDir) return Promise();
return fs.tmpDir()
.then(function(dir) {
that.tmpDir = dir;
});
};
// Clone a git repository if non existant
Git.prototype.clone = function(host, ref) {
var that = this;
return this.allocateDir()
// Return or clone the git repo
.then(function() {
// Unique ID for repo/ref combinaison
var repoId = that.repoID(host, ref);
// Absolute path to the folder
var repoPath = path.join(that.tmpDir, repoId);
if (that.cloned[repoId]) return repoPath;
// Clone repo
return command.exec('git clone '+host+' '+repoPath)
// Checkout reference if specified
.then(function() {
that.cloned[repoId] = true;
if (!ref) return;
return command.exec('git checkout '+ref, { cwd: repoPath });
})
.thenResolve(repoPath);
});
};
// Get file from a git repo
Git.prototype.resolve = function(giturl) {
// Path to a file in a git repo?
if (!Git.isUrl(giturl)) {
if (this.resolveRoot(giturl)) return Promise(giturl);
return Promise(null);
}
if (_.isString(giturl)) giturl = Git.parseUrl(giturl);
if (!giturl) return Promise(null);
// Clone or get from cache
return this.clone(giturl.host, giturl.ref)
.then(function(repo) {
return path.resolve(repo, giturl.filepath);
});
};
// Return root of git repo from a filepath
Git.prototype.resolveRoot = function(filepath) {
var relativeToGit, repoId;
// No git repo cloned, or file is not in a git repository
if (!this.tmpDir || !pathUtil.isInRoot(this.tmpDir, filepath)) return null;
// Extract first directory (is the repo id)
relativeToGit = path.relative(this.tmpDir, filepath);
repoId = _.first(relativeToGit.split(path.sep));
if (!repoId) return;
// Return an absolute file
return path.resolve(this.tmpDir, repoId);
};
// Check if an url is a git dependency url
function checkGitUrl(giturl) {
Git.isUrl = function(giturl) {
return (giturl.indexOf(GIT_PREFIX) === 0);
}
// Validates a SHA in hexadecimal
function validateSha(str) {
return (/[0-9a-f]{40}/).test(str);
}
};
// Parse and extract infos
function parseGitUrl(giturl) {
Git.parseUrl = function(giturl) {
var ref, uri, fileParts, filepath;
if (!checkGitUrl(giturl)) return null;
if (!Git.isUrl(giturl)) return null;
giturl = giturl.slice(GIT_PREFIX.length);
uri = new URI(giturl);
ref = uri.fragment() || 'master';
ref = uri.fragment() || null;
uri.fragment(null);
// Extract file inside the repo (after the .git)
fileParts =uri.path().split('.git');
fileParts = uri.path().split('.git');
filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : '';
if (filepath[0] == '/') filepath = filepath.slice(1);
@@ -43,85 +121,9 @@ function parseGitUrl(giturl) {
return {
host: uri.toString(),
ref: ref || 'master',
ref: ref,
filepath: filepath
};
}
// Clone a git repo from a specific ref
function cloneGitRepo(host, ref) {
var isBranch = false;
ref = ref || 'master';
if (!validateSha(ref)) isBranch = true;
return Q()
// Create temporary folder to store git repos
.then(function() {
if (GIT_TMP) return;
return fs.tmp.dir()
.then(function(_tmp) {
GIT_TMP = _tmp;
});
})
// Return or clone the git repo
.then(function() {
// Unique ID for repo/ref combinaison
var repoId = crc.crc32(host+'#'+ref).toString(16);
// Absolute path to the folder
var repoPath = path.resolve(GIT_TMP, repoId);
return fs.exists(repoPath)
.then(function(doExists) {
if (doExists) return;
// Clone repo
return exec('git clone '+host+' '+repoPath)
.then(function() {
return exec('git checkout '+ref, { cwd: repoPath });
});
})
.thenResolve(repoPath);
});
}
// Get file from a git repo
function resolveFileFromGit(giturl) {
if (_.isString(giturl)) giturl = parseGitUrl(giturl);
if (!giturl) return Q(null);
// Clone or get from cache
return cloneGitRepo(giturl.host, giturl.ref)
.then(function(repo) {
// Resolve relative path
return path.resolve(repo, giturl.filepath);
});
}
// Return root of git repo from a filepath
function resolveGitRoot(filepath) {
var relativeToGit, repoId;
// No git repo cloned, or file is not in a git repository
if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
// Extract first directory (is the repo id)
relativeToGit = path.relative(GIT_TMP, filepath);
repoId = _.first(relativeToGit.split(path.sep));
if (!repoId) return;
// Return an absolute file
return path.resolve(GIT_TMP, repoId);
}
module.exports = {
checkUrl: checkGitUrl,
parseUrl: parseGitUrl,
resolveFile: resolveFileFromGit,
resolveRoot: resolveGitRoot
};
module.exports = Git;
-80
View File
@@ -1,80 +0,0 @@
var _ = require('lodash');
var path = require('path');
var fs = require('fs');
var i18n = require('i18n');
var I18N_PATH = path.resolve(__dirname, '../../theme/i18n/');
var DEFAULT_LANGUAGE = 'en';
var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) {
return path.basename(lang, '.json');
});
i18n.configure({
locales: LOCALES,
directory: I18N_PATH,
defaultLocale: DEFAULT_LANGUAGE,
updateFiles: false
});
function compareLocales(lang, locale) {
var langMain = _.first(lang.split('-'));
var langSecond = _.last(lang.split('-'));
var localeMain = _.first(locale.split('-'));
var localeSecond = _.last(locale.split('-'));
if (locale == lang) return 100;
if (localeMain == langMain) return 50;
if (localeSecond == langSecond) return 20;
return 0;
}
var normalizeLanguage = _.memoize(function(lang) {
var language = _.chain(LOCALES)
.values()
.map(function(locale) {
return {
locale: locale,
score: compareLocales(lang, locale)
};
})
.filter(function(lang) {
return lang.score > 0;
})
.sortBy('score')
.pluck('locale')
.last()
.value();
return language || lang;
});
function translate(locale, phrase) {
var args = Array.prototype.slice.call(arguments, 2);
return i18n.__.apply({}, [{
locale: locale,
phrase: phrase
}].concat(args));
}
function getCatalog(locale) {
locale = normalizeLanguage(locale);
return i18n.getCatalog(locale);
}
function getLocales() {
return LOCALES;
}
function hasLocale(locale) {
return _.contains(LOCALES, locale);
}
module.exports = {
__: translate,
normalizeLanguage: normalizeLanguage,
getCatalog: getCatalog,
getLocales: getLocales,
hasLocale: hasLocale
};
+38 -31
View File
@@ -1,37 +1,44 @@
var _ = require("lodash");
var Q = require("q");
var fs = require("./fs");
var spawn = require("spawn-cmd").spawn;
var Promise = require('./promise');
var command = require('./command');
var fs = require('./fs');
var error = require('./error');
// Convert a svg file
var convertSVG = function(source, dest, options) {
if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source));
var d = Q.defer();
// Convert a svg file to a pmg
function convertSVGToPNG(source, dest, options) {
if (!fs.existsSync(source)) return Promise.reject(new error.FileNotFoundError({ filename: source }));
options = _.defaults(options || {}, {
});
//var command = shellescape(["svgexport", source, dest]);
var child = spawn("svgexport", [source, dest]);
child.on("error", function(error) {
if (error.code == "ENOENT") error = new Error("Need to install \"svgexport\" using \"npm install svgexport -g\"");
return d.reject(error);
});
child.on("close", function(code) {
if (code === 0 && fs.existsSync(dest)) {
d.resolve();
} else {
d.reject(new Error("Error converting "+source+" into "+dest));
return command.spawn('svgexport', [source, dest])
.fail(function(err) {
if (err.code == 'ENOENT') {
err = error.RequireInstallError({
cmd: 'svgexport',
install: 'Install it using: "npm install svgexport -g"'
});
}
});
throw err;
})
.then(function() {
if (fs.existsSync(dest)) return;
return d.promise;
};
throw new Error('Error converting '+source+' into '+dest);
});
}
// Convert a svg buffer to a png file
function convertSVGBufferToPNG(buf, dest) {
// Create a temporary SVG file to convert
return fs.tmpFile({
postfix: '.svg'
})
.then(function(tmpSvg) {
return fs.writeFile(tmpSvg, buf)
.then(function() {
return convertSVGToPNG(tmpSvg, dest);
});
});
}
module.exports = {
convertSVG: convertSVG,
INVALID: [".svg"]
};
convertSVGToPNG: convertSVGToPNG,
convertSVGBufferToPNG: convertSVGBufferToPNG
};
+19 -30
View File
@@ -1,7 +1,7 @@
var url = require('url');
var path = require('path');
// Is the link an external link
// Is the url an external url
function isExternal(href) {
try {
return Boolean(url.parse(href).protocol);
@@ -10,15 +10,9 @@ function isExternal(href) {
}
}
// Return true if the link is relative
// Inverse of isExternal
function isRelative(href) {
try {
var parsed = url.parse(href);
return !!(!parsed.protocol && parsed.path);
} catch(err) {
return true;
}
return !isExternal(href);
}
// Return true if the link is an achor
@@ -32,15 +26,20 @@ function isAnchor(href) {
}
// Normalize a path to be a link
function normalizeLink(s) {
function normalize(s) {
return s.replace(/\\/g, '/');
}
// Relative to absolute path
// Convert relative to absolute path
// dir: directory parent of the file currently in rendering process
// outdir: directory parent from the html output
function toAbsolute(_href, dir, outdir) {
if (isExternal(_href)) return _href;
outdir = outdir == undefined? dir : outdir;
_href = normalize(_href);
dir = normalize(dir);
outdir = normalize(outdir);
// Path "_href" inside the base folder
var hrefInRoot = path.normalize(path.join(dir, _href));
@@ -50,32 +49,22 @@ function toAbsolute(_href, dir, outdir) {
_href = path.relative(outdir, hrefInRoot);
// Normalize windows paths
_href = normalizeLink(_href);
_href = normalize(_href);
return _href;
}
// Join links
function join() {
var _href = path.join.apply(path, arguments);
return normalizeLink(_href);
};
// Change extension
function changeExtension(filename, newext) {
return path.join(
path.dirname(filename),
path.basename(filename, path.extname(filename))+newext
);
// Convert an absolute path to a relative path for a specific folder (dir)
// ('test/', 'hello.md') -> '../hello.md'
function relative(dir, file) {
return normalize(path.relative(dir, file));
}
module.exports = {
isAnchor: isAnchor,
isRelative: isRelative,
isExternal: isExternal,
isRelative: isRelative,
isAnchor: isAnchor,
normalize: normalize,
toAbsolute: toAbsolute,
join: join,
changeExtension: changeExtension,
normalize: normalizeLink
relative: relative
};
+104 -78
View File
@@ -1,6 +1,6 @@
var _ = require("lodash");
var util = require("util");
var color = require("bash-color");
var _ = require('lodash');
var util = require('util');
var color = require('bash-color');
var LEVELS = {
DEBUG: 0,
@@ -17,86 +17,112 @@ var COLORS = {
ERROR: color.red
};
module.exports = function(_write, logLevel) {
var logger = {};
var lastChar = "\n";
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
function Logger(write, logLevel, prefix) {
if (!(this instanceof Logger)) return new Logger(write, logLevel);
// Write a simple message
logger.write = function(msg) {
msg = msg.toString();
lastChar = _.last(msg);
return _write(msg);
};
this._write = write || function(msg) { process.stdout.write(msg); };
this.lastChar = '\n';
// Format a message
logger.format = function() {
return util.format.apply(util, arguments);
};
// Define log level
this.setLevel(logLevel);
// Write a line
logger.writeLn = function(msg) {
return this.write((msg || "")+"\n");
};
_.bindAll(this);
// Write a message with a certain level
logger.log = function(level) {
if (level < logLevel) return;
var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = logger.format.apply(logger, args);
if (lastChar == "\n") {
msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg;
}
return logger.write(msg);
};
logger.logLn = function() {
if (lastChar != "\n") logger.write("\n");
var args = Array.prototype.slice.apply(arguments);
args.push("\n");
logger.log.apply(logger, args);
};
// Write a OK
logger.ok = function(level) {
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = logger.format.apply(logger, args);
if (arguments.length > 1) {
logger.logLn(level, color.green(">> ") + msg.trim().replace(/\n/g, color.green("\n>> ")));
} else {
logger.log(level, color.green("OK"), "\n");
}
};
// Write an "FAIL"
logger.fail = function(level) {
return logger.log(level, color.red("ERROR")+"\n");
};
_.each(_.omit(LEVELS, "DISABLED"), function(level, levelKey) {
// Create easy-to-use method like "logger.debug.ln('....')"
_.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) {
levelKey = levelKey.toLowerCase();
logger[levelKey] = _.partial(logger.log, level);
logger[levelKey].ln = _.partial(logger.logLn, level);
logger[levelKey].ok = _.partial(logger.ok, level);
logger[levelKey].fail = _.partial(logger.fail, level);
logger[levelKey].promise = function(p) {
return p.
then(function(st) {
logger[levelKey].ok();
return st;
}, function(err) {
logger[levelKey].fail();
throw err;
});
};
});
this[levelKey] = _.partial(this.log, level);
this[levelKey].ln = _.partial(this.logLn, level);
this[levelKey].ok = _.partial(this.ok, level);
this[levelKey].fail = _.partial(this.fail, level);
this[levelKey].promise = _.partial(this.promise, level);
}, this);
}
return logger;
// Create a new logger prefixed from this logger
Logger.prototype.prefix = function(prefix) {
return (new Logger(this._write, this.logLevel, prefix));
};
module.exports.LEVELS = LEVELS;
module.exports.COLORS = COLORS;
// Change minimum level
Logger.prototype.setLevel = function(logLevel) {
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
this.logLevel = logLevel;
};
// Print a simple string
Logger.prototype.write = function(msg) {
msg = msg.toString();
this.lastChar = _.last(msg);
return this._write(msg);
};
// Format a string using the first argument as a printf-like format.
Logger.prototype.format = function() {
return util.format.apply(util, arguments);
};
// Print a line
Logger.prototype.writeLn = function(msg) {
return this.write((msg || '')+'\n');
};
// Log/Print a message if level is allowed
Logger.prototype.log = function(level) {
if (level < this.logLevel) return;
var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = this.format.apply(this, args);
if (this.lastChar == '\n') {
msg = COLORS[levelKey](levelKey.toLowerCase()+':')+' '+msg;
}
return this.write(msg);
};
// Log/Print a line if level is allowed
Logger.prototype.logLn = function() {
if (this.lastChar != '\n') this.write('\n');
var args = Array.prototype.slice.apply(arguments);
args.push('\n');
return this.log.apply(this, args);
};
// Log a confirmation [OK]
Logger.prototype.ok = function(level) {
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = this.format.apply(this, args);
if (arguments.length > 1) {
this.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
} else {
this.log(level, color.green('OK'), '\n');
}
};
// Log a "FAIL"
Logger.prototype.fail = function(level) {
return this.log(level, color.red('ERROR') + '\n');
};
// Log state of a promise
Logger.prototype.promise = function(level, p) {
var that = this;
return p.
then(function(st) {
that.ok(level);
return st;
}, function(err) {
that.fail(level);
throw err;
});
};
Logger.LEVELS = LEVELS;
Logger.COLORS = COLORS;
module.exports = Logger;
-79
View File
@@ -1,79 +0,0 @@
var _ = require("lodash");
// Cleans up an article/chapter object
// remove "articles" attributes
function clean(obj) {
return obj && _.omit(obj, ["articles"]);
}
function flattenChapters(chapters) {
return _.reduce(chapters, function(accu, chapter) {
return accu.concat([clean(chapter)].concat(flattenChapters(chapter.articles)));
}, []);
}
// Returns from a summary a map of
/*
{
"file/path.md": {
prev: ...,
next: ...,
},
...
}
*/
function navigation(summary, files) {
// Support single files as well as list
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
// List of all navNodes
// Flatten chapters
var navNodes = flattenChapters(summary.chapters);
// Mapping of prev/next for a give path
var mapping = _.chain(navNodes)
.map(function(current, i) {
var prev = null, next = null;
// Skip if no path
if(!current.exists) return null;
// Find prev
prev = _.chain(navNodes.slice(0, i))
.reverse()
.find(function(node) {
return node.exists && !node.external;
})
.value();
// Find next
next = _.chain(navNodes.slice(i+1))
.find(function(node) {
return node.exists && !node.external;
})
.value();
return [current.path, {
index: i,
title: current.title,
introduction: current.introduction,
prev: prev,
next: next,
level: current.level,
}];
})
.compact()
.object()
.value();
// Filter for only files we want
if(files) {
return _.pick(mapping, files);
}
return mapping;
}
// Exports
module.exports = navigation;
-397
View File
@@ -1,397 +0,0 @@
var Q = require('q');
var _ = require('lodash');
var url = require('url');
var path = require('path');
var cheerio = require('cheerio');
var domSerializer = require('dom-serializer');
var request = require('request');
var crc = require('crc');
var slug = require('github-slugid');
var links = require('./links');
var imgUtils = require('./images');
var fs = require('./fs');
var batch = require('./batch');
var parsableExtensions = require('gitbook-parsers').extensions;
// Map of images that have been converted
var imgConversionCache = {};
// Render a cheerio dom as html
function renderDom($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
}
function replaceText($, el, search, replace, text_only ) {
return $(el).each(function(){
var node = this.firstChild,
val,
new_val,
// Elements to be removed at the end.
remove = [];
// Only continue if firstChild exists.
if ( node ) {
// Loop over all childNodes.
while (node) {
// Only process text nodes.
if ( node.nodeType === 3 ) {
// The original node value.
val = node.nodeValue;
// The new value.
new_val = val.replace( search, replace );
// Only replace text if the new value is actually different!
if ( new_val !== val ) {
if ( !text_only && /</.test( new_val ) ) {
// The new value contains HTML, set it in a slower but far more
// robust way.
$(node).before( new_val );
// Don't remove the node yet, or the loop will lose its place.
remove.push( node );
} else {
// The new value contains no HTML, so it can be set in this
// very fast, simple way.
node.nodeValue = new_val;
}
}
}
node = node.nextSibling;
}
}
// Time to remove those elements!
if (remove.length) $(remove).remove();
});
}
function pregQuote( str ) {
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
}
// Adapt an html snippet to be relative to a base folder
function normalizeHtml(src, options) {
var $ = cheerio.load(src, {
// We should parse html without trying to normalize too much
xmlMode: false,
// SVG need some attributes to use uppercases
lowerCaseAttributeNames: false,
lowerCaseTags: false
});
var toConvert = [];
var svgContent = {};
var outputRoot = options.book.options.output;
imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {};
// Find svg images to extract and process
if (options.convertImages) {
$('svg').each(function() {
var content = renderDom($, $(this));
var svgId = _.uniqueId('svg');
var dest = svgId+'.svg';
// Generate filename
dest = '/'+fs.getUniqueFilename(outputRoot, dest);
svgContent[dest] = '<?xml version="1.0" encoding="UTF-8"?>'+content;
$(this).replaceWith($('<img>').attr('src', dest));
});
}
// Generate ID for headings
$('h1,h2,h3,h4,h5,h6').each(function() {
if ($(this).attr('id')) return;
$(this).attr('id', slug($(this).text()));
});
// Find images to normalize
$('img').each(function() {
var origin;
var src = $(this).attr('src');
if (!src) return;
var isExternal = links.isExternal(src);
// Transform as relative to the bases
if (links.isRelative(src)) {
src = links.toAbsolute(src, options.base, options.output);
}
// Convert if needed
if (options.convertImages) {
// If image is external and ebook, then downlaod the images
if (isExternal) {
origin = src;
src = '/'+crc.crc32(origin).toString(16)+path.extname(url.parse(origin).pathname);
src = links.toAbsolute(src, options.base, options.output);
isExternal = false;
}
var ext = path.extname(src);
var srcAbs = links.join('/', options.base, src);
// Test image extension
if (_.contains(imgUtils.INVALID, ext)) {
if (imgConversionCache[outputRoot][srcAbs]) {
// Already converted
src = imgConversionCache[outputRoot][srcAbs];
} else {
// Not converted yet
var dest = '';
// Replace extension
dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+'.png');
dest = dest[0] == '/'? dest.slice(1) : dest;
// Get a name that doesn't exists
dest = fs.getUniqueFilename(outputRoot, dest);
options.book.log.debug.ln('detect invalid image (will be converted to png):', srcAbs);
// Add to cache
imgConversionCache[outputRoot][srcAbs] = '/'+dest;
// Push to convert
toConvert.push({
origin: origin,
content: svgContent[srcAbs],
source: isExternal? srcAbs : path.join('./', srcAbs),
dest: path.join('./', dest)
});
src = links.join('/', dest);
}
// Reset as relative to output
src = links.toAbsolute(src, options.base, options.output);
}
else if (origin) {
// Need to downlaod image
toConvert.push({
origin: origin,
source: path.join('./', srcAbs)
});
}
}
$(this).attr('src', src);
});
// Normalize links
$('a').each(function() {
var href = $(this).attr('href');
if (!href) return;
if (links.isAnchor(href)) {
// Keep it as it is
} else if (links.isRelative(href)) {
var parts = url.parse(href);
var pathName = decodeURIComponent(parts.pathname);
var anchor = parts.hash || '';
// Calcul absolute path for this file (without the anchor)
var absolutePath = links.join(options.base, pathName);
// If is in navigation relative: transform as content
if (options.navigation[absolutePath]) {
absolutePath = options.book.contentLink(absolutePath);
}
// If md/adoc/rst files is not in summary
// or for ebook, signal all files that are outside the summary
else if (_.contains(parsableExtensions, path.extname(absolutePath)) ||
_.contains(['epub', 'pdf', 'mobi'], options.book.options.generator)) {
options.book.log.warn.ln('page', options.input, 'contains an hyperlink to resource outside spine \''+href+'\'');
}
// Transform as absolute
href = links.toAbsolute('/'+absolutePath, options.base, options.output)+anchor;
} else {
// External links
$(this).attr('target', '_blank');
}
// Transform extension
$(this).attr('href', href);
});
// Highlight code blocks
$('code').each(function() {
// Normalize language
var lang = _.chain(
($(this).attr('class') || '').split(' ')
)
.map(function(cl) {
// Markdown
if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
// Asciidoc
if (cl.search('language-') === 0) return cl.slice('language-'.length);
return null;
})
.compact()
.first()
.value();
var source = $(this).text();
var blk = options.book.template.applyBlock('code', {
body: source,
kwargs: {
language: lang
}
});
if (blk.html === false) $(this).text(blk.body);
else $(this).html(blk.body);
});
// Replace glossary terms
var glossary = _.sortBy(options.glossary, function(term) {
return -term.name.length;
});
_.each(glossary, function(term) {
var r = new RegExp( '\\b(' + pregQuote(term.name.toLowerCase()) + ')\\b' , 'gi' );
var includedInFiles = false;
$('*').each(function() {
// Ignore codeblocks
if (_.contains(['code', 'pre', 'a', 'script'], this.name.toLowerCase())) return;
replaceText($, this, r, function(match) {
// Add to files index in glossary
if (!includedInFiles) {
includedInFiles = true;
term.files = term.files || [];
term.files.push(options.navigation[options.input]);
}
return '<a href=\''+links.toAbsolute('/GLOSSARY.html', options.base, options.output) + '#' + term.id+'\' class=\'glossary-term\' title=\''+_.escape(term.description)+'\'>'+match+'</a>';
});
});
});
return {
html: renderDom($),
images: toConvert
};
}
// Convert svg images to png
function convertImages(images, options) {
if (!options.convertImages) return Q();
var downloaded = [];
options.book.log.debug.ln('convert ', images.length, 'images to png');
return batch.execEach(images, {
max: 100,
fn: function(image) {
var imgin = path.resolve(options.book.options.output, image.source);
return Q()
// Write image if need to be download
.then(function() {
if (!image.origin && !_.contains(downloaded, image.origin)) return;
options.book.log.debug('download image', image.origin, '...');
downloaded.push(image.origin);
return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin)))
.fail(function(err) {
if (!_.isError(err)) err = new Error(err);
err.message = 'Fail downloading '+image.origin+': '+err.message;
throw err;
});
})
// Write svg if content
.then(function() {
if (!image.content) return;
return fs.writeFile(imgin, image.content);
})
// Convert
.then(function() {
if (!image.dest) return;
var imgout = path.resolve(options.book.options.output, image.dest);
options.book.log.debug('convert image', image.source, 'to', image.dest, '...');
return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout));
});
}
})
.then(function() {
options.book.log.debug.ok(images.length+' images converted with success');
});
}
// Adapt page content to be relative to a base folder
function normalizePage(sections, options) {
options = _.defaults(options || {}, {
// Current book
book: null,
// Do we need to convert svg?
convertImages: false,
// Current file path
input: '.',
// Navigation to use to transform path
navigation: {},
// Directory parent of the file currently in rendering process
base: './',
// Directory parent from the html output
output: './',
// Glossary terms
glossary: []
});
// List of images to convert
var toConvert = [];
sections = _.map(sections, function(section) {
if (section.type != 'normal') return section;
var out = normalizeHtml(section.content, options);
toConvert = toConvert.concat(out.images);
section.content = out.html;
return section;
});
return Q()
.then(function() {
toConvert = _.uniq(toConvert, 'source');
return convertImages(toConvert, options);
})
.thenResolve(sections);
}
module.exports = {
normalize: normalizePage
};
+27 -9
View File
@@ -1,5 +1,12 @@
var _ = require("lodash");
var path = require("path");
var _ = require('lodash');
var path = require('path');
var error = require('./error');
// Normalize a filename
function normalizePath(filename) {
return path.normalize(filename);
}
// Return true if file path is inside a folder
function isInRoot(root, filename) {
@@ -10,31 +17,42 @@ function isInRoot(root, filename) {
// Resolve paths in a specific folder
// Throw error if file is outside this folder
function resolveInRoot(root) {
var input, result, err;
var input, result;
input = _.chain(arguments)
.toArray()
.slice(1)
.reduce(function(current, p) {
// Handle path relative to book root ("/README.md")
if (p[0] == "/" || p[0] == "\\") return p.slice(1);
if (p[0] == '/' || p[0] == '\\') return p.slice(1);
return current? path.join(current, p) : path.normalize(p);
}, "")
}, '')
.value();
result = path.resolve(root, input);
if (!isInRoot(root, result)) {
err = new Error("EACCESS: \"" + result + "\" not in \"" + root + "\"");
err.code = "EACCESS";
throw err;
throw new error.FileOutOfScopeError({
filename: result,
root: root
});
}
return result;
}
// Chnage extension
function setExtension(filename, ext) {
return path.join(
path.dirname(filename),
path.basename(filename, path.extname(filename)) + ext
);
}
module.exports = {
isInRoot: isInRoot,
resolveInRoot: resolveInRoot
resolveInRoot: resolveInRoot,
normalize: normalizePath,
setExtension: setExtension
};
-55
View File
@@ -1,55 +0,0 @@
var _ = require('lodash');
// Returns from a navigation and a current file, a snapshot of current detailed state
function calculProgress(navigation, current) {
var n = _.size(navigation);
var percent = 0, prevPercent = 0, currentChapter = null;
var done = true;
var chapters = _.chain(navigation)
// Transform as array
.map(function(nav, path) {
nav.path = path;
return nav;
})
// Sort entries
.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;
})
.value();
return {
// Previous percent
prevPercent: prevPercent,
// Current percent
percent: percent,
// List of chapter with progress
chapters: chapters,
// Current chapter
current: currentChapter
};
}
module.exports = calculProgress;
+62
View File
@@ -0,0 +1,62 @@
var Q = require('q');
var _ = require('lodash');
// Reduce an array to a promise
function reduce(arr, iter, base) {
return _.reduce(arr, function(prev, elem, i) {
return prev.then(function(val) {
return iter(val, elem, i);
});
}, Q(base));
}
// Transform an array
function serie(arr, iter, base) {
return reduce(arr, function(before, item, i) {
return Q(iter(item, i))
.then(function(r) {
before.push(r);
return before;
});
}, []);
}
// Iter over an array and return first result (not null)
function some(arr, iter) {
return _.reduce(arr, function(prev, elem, i) {
return prev.then(function(val) {
if (val) return val;
return iter(elem, i);
});
}, Q());
}
// Map an array using an async (promised) iterator
function map(arr, iter) {
return reduce(arr, function(prev, entry, i) {
return Q(iter(entry, i))
.then(function(out) {
prev.push(out);
return prev;
});
}, []);
}
// Wrap a fucntion in a promise
function wrap(func) {
return _.wrap(func, function(_func) {
var args = Array.prototype.slice.call(arguments, 1);
return Q()
.then(function() {
return _func.apply(null, args);
});
});
}
module.exports = Q;
module.exports.reduce = reduce;
module.exports.map = map;
module.exports.serie = serie;
module.exports.some = some;
module.exports.wrapfn = wrap;
-27
View File
@@ -1,27 +0,0 @@
var _ = require("lodash");
function escapeShellArg(arg) {
var ret = "";
ret = arg.replace(/"/g, '\\"');
return "\"" + ret + "\"";
}
function optionsToShellArgs(options) {
return _.chain(options)
.map(function(value, key) {
if (value === null || value === undefined || value === false) return null;
if (value === true) return key;
return key+"="+escapeShellArg(value);
})
.compact()
.value()
.join(" ");
}
module.exports = {
escapeShellArg: escapeShellArg,
optionsToShellArgs: optionsToShellArgs,
toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
};
-40
View File
@@ -1,40 +0,0 @@
var Q = require("q");
var _ = require("lodash");
var path = require("path");
var chokidar = require("chokidar");
var parsers = require("gitbook-parsers");
function watch(dir) {
var d = Q.defer();
dir = path.resolve(dir);
var toWatch = [
"book.json", "book.js"
];
_.each(parsers.extensions, function(ext) {
toWatch.push("**/*"+ext);
});
var watcher = chokidar.watch(toWatch, {
cwd: dir,
ignored: "_book/**",
ignoreInitial: true
});
watcher.once("all", function(e, filepath) {
watcher.close();
d.resolve(filepath);
});
watcher.once("error", function(err) {
watcher.close();
d.reject(err);
});
return d.promise;
}
module.exports = watch;
+28 -29
View File
@@ -1,25 +1,26 @@
{
"name": "gitbook",
"version": "2.6.7",
"version": "3.0.0-pre.0",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
"dependencies": {
"q": "1.0.1",
"lodash": "3.10.1",
"graceful-fs": "3.0.5",
"graceful-fs": "4.1.3",
"resolve": "0.6.3",
"fs-extra": "0.16.5",
"fstream-ignore": "1.0.2",
"gitbook-parsers": "0.8.9",
"mkdirp": "0.5.1",
"error": "7.0.2",
"gitbook-markdown": "1.0.3",
"gitbook-asciidoc": "1.0.2",
"gitbook-plugin-highlight": "1.0.3",
"gitbook-plugin-sharing": "1.0.1",
"gitbook-plugin-search": "1.1.0",
"gitbook-plugin-fontsettings": "1.0.2",
"nunjucks": "2.2.0",
"gitbook-plugin-theme-default": "1.0.0-pre.4",
"nunjucks": "2.3.0",
"nunjucks-autoescape": "1.0.0",
"nunjucks-filter": "1.0.0",
"i18n": "0.5.0",
"semver": "5.0.1",
"npmi": "0.1.1",
"cheerio": "0.19.0",
@@ -27,42 +28,40 @@
"chokidar": "~1.0.5",
"send": "0.2.0",
"tiny-lr": "0.2.1",
"tmp": "0.0.24",
"tmp": "0.0.28",
"crc": "3.2.1",
"bash-color": "0.0.3",
"urijs": "1.17.0",
"request": "2.51.0",
"request": "2.69.0",
"npm": "2.4.1",
"dom-serializer": "0.1.0",
"spawn-cmd": "0.0.2",
"escape-string-regexp": "1.0.3",
"juice": "1.5.0",
"jsonschema": "1.0.2",
"juice": "1.9.0",
"jsonschema": "1.1.0",
"json-schema-defaults": "0.1.1",
"merge-defaults": "0.2.1",
"github-slugid": "1.0.0"
"github-slugid": "1.0.0",
"destroy": "1.0.4",
"ignore": "2.2.19",
"deprecated": "0.0.1",
"rmdir": "1.2.0",
"cp": "0.2.0",
"cpr": "1.0.0",
"direction": "0.1.5",
"moment": "2.11.2",
"i18n-t": "1.0.0",
"front-matter": "2.0.6",
"spawn-cmd": "0.0.2"
},
"devDependencies": {
"eslint": "1.5.0",
"mocha": "2.3.2",
"should": "7.1.0",
"should-promised": "0.3.1",
"gulp": "^3.8.11",
"gulp-rename": "^1.2.2",
"gulp-uglify": "1.1.0",
"gulp-less": "3.0.2",
"gulp-minify-css": "1.0.0",
"gulp-util": "3.0.6",
"browserify": "11.0.1",
"merge-stream": "0.1.7",
"vinyl-source-stream": "1.1.0",
"jquery": "2.1.4",
"mousetrap": "1.5.3",
"font-awesome": "4.1.0",
"gitbook-markdown-css": "1.0.1"
"mocha": "2.4.5",
"should": "8.2.2"
},
"scripts": {
"test": "node_modules/.bin/mocha --reporter spec --timeout 15000"
"test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js",
"lint": "eslint ."
},
"repository": {
"type": "git",
+27
View File
@@ -0,0 +1,27 @@
// Utilities
require('./location');
require('./paths');
// Parsing
require('./locate');
require('./config');
require('./readme');
require('./summary');
require('./glossary');
require('./langs');
require('./parse');
require('./git');
require('./plugins');
require('./template');
require('./conrefs');
// Page and HTML generation
require('./page');
// Output
require('./assets-inliner');
require('./output-json');
require('./output-website');
require('./output-ebook');
+10 -17
View File
@@ -1,25 +1,18 @@
var _ = require('lodash');
var fs = require('fs');
var path = require('path');
var should = require('should');
var _ = require('lodash');
var cheerio = require('cheerio');
var should = require('should');
require('should-promised');
// Assertions to test if an Output has generated a file
should.Assertion.add('file', function(file, description) {
this.params = { actual: this.obj.toString(), operator: 'have file ' + file, message: description };
this.params = {
actual: this.obj.root(),
operator: 'have file ' + file,
message: description
};
this.obj.should.have.property('options').which.is.an.Object();
this.obj.options.should.have.property('output').which.is.a.String();
this.assert(fs.existsSync(path.resolve(this.obj.options.output, file)));
});
should.Assertion.add('jsonfile', function(file, description) {
this.params = { actual: this.obj.toString(), operator: 'have valid jsonfile ' + file, message: description };
this.obj.should.have.property('options').which.is.an.Object();
this.obj.options.should.have.property('output').which.is.a.String();
this.assert(JSON.parse(fs.readFileSync(path.resolve(this.obj.options.output, file), { encoding: 'utf-8' })));
this.obj.should.have.property('resolve').which.is.a.Function;
this.assert(fs.existsSync(this.obj.resolve(file)));
});
should.Assertion.add('html', function(rules, description) {
+86
View File
@@ -0,0 +1,86 @@
var cheerio = require('cheerio');
var path = require('path');
var mock = require('./mock');
var AssetsInliner = require('../lib/output/assets-inliner')();
describe('Assets Inliner Output', function() {
var output;
before(function() {
var SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" version="1.1"><rect width="200" height="100" stroke="black" stroke-width="6" fill="green"/></svg>';
return mock.outputDefaultBook(AssetsInliner, {
'README.md': '',
// SVGs
'svg_file.md': '![image](test.svg)',
'svg_inline.md': 'This is a svg: '+SVG,
'test.svg': '<?xml version="1.0" encoding="UTF-8"?>' + SVG,
// Relative
'folder/test.md': '![image](../test.svg)',
// Remote images
'remote_png.md': '![image](https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png)',
'remote_svg.md': '![image](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg)',
'SUMMARY.md': '* [svg inline](svg_inline.md)\n' +
'* [svg file](svg_file.md)\n' +
'* [remote png file](remote_png.md)\n' +
'* [remote svg file](remote_svg.md)\n' +
'* [relative image](folder/test.md)\n' +
'\n\n'
})
.then(function(_output) {
output = _output;
});
});
function testImageInPage(filename) {
var page = output.book.getPage(filename);
var $ = cheerio.load(page.content);
// Is there an image?
var $img = $('img');
$img.length.should.equal(1);
// Does the file exists
var src = $img.attr('src');
// Resolve the filename
src = page.resolveLocal(src);
output.should.have.file(src);
path.extname(src).should.equal('.png');
return src;
}
describe('SVG', function() {
it('should correctly convert SVG files to PNG', function() {
testImageInPage('svg_file.md');
});
it('should correctly convert inline SVG to PNG', function() {
testImageInPage('svg_inline.md');
});
});
describe('Remote Assets', function() {
it('should correctly download a PNG file', function() {
testImageInPage('remote_png.md');
});
it('should correctly download then convert a remote SVG to PNG', function() {
testImageInPage('remote_svg.md');
});
});
describe('Relative Images', function() {
it('should correctly resolve image', function() {
testImageInPage('folder/test.md');
});
});
});
-3
View File
@@ -1,3 +0,0 @@
# Readme
Default description for the book.
-1
View File
@@ -1 +0,0 @@
# Summary
-1
View File
@@ -1 +0,0 @@
# Readme
-1
View File
@@ -1 +0,0 @@
# Summary
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
"title": "js-config"
};
-1
View File
@@ -1 +0,0 @@
# Readme
-1
View File
@@ -1 +0,0 @@
# Summary
-3
View File
@@ -1,3 +0,0 @@
{
"title": "json-config"
}

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