Merge pull request #1543 from GitbookIO/dream

React for rendering website with plugins
This commit is contained in:
Samy Pessé
2016-12-22 10:18:38 +01:00
committed by GitHub
581 changed files with 13636 additions and 6861 deletions
+2 -13
View File
@@ -1,15 +1,4 @@
{
"rules": {
"indent": [ 2, 4 ],
"quotes": [ 2, "single" ],
"linebreak-style": [ 2, "unix" ],
"semi": [ 2, "always" ],
"no-unused-vars": [ 2, {
"vars": "all",
"args": "none"
} ],
"spaced-comment": [ 2, "always" ]
},
"env": {
"node": true,
"browser": true,
@@ -18,5 +7,5 @@
"globals": {
"expect": true
},
"extends": "eslint:recommended"
}
"extends": "gitbook"
}
+8 -2
View File
@@ -22,14 +22,20 @@ 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
# yarn
yarn.lock
# vim swapfile
*.swp
# GitBook with babel
/packages/gitbook-core/lib
/packages/gitbook-plugin/lib
# Output of documentation
_book
book.pdf
book.epub
book.mobi
+11
View File
@@ -2,6 +2,17 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 4.0.0
- **Breaking Changes:** Most plugins and themes should be incompatible with this version
- GitBook rendering is now done using React, which brings better APIs for plugins and a more interactive website.
- *Performances*: Installation of plugins is much faster thanks to `ied`
- *Performances*: Git conrefs are faster over large books
- *Website Feature:* new default plugin `copy-code` to copy code blocks content in one click
- *Website Feature:* new default plugin `heading-anchors` to have clickable headings
- *Website Feature:* `fontsettings` is now only controlling the font size, and can work on multiple themes
- *Plugin API*: hook `page` can no longer modify the content, only the page's attributes
- *Plugin API*: plugins can no longer export resources, instead use the `_assets` folder
## 3.2.2
- Fix catching parsing errors leading to possible missing pages in generated book
- Rollback markdown parser to `kramed` until `markup-it` is stable enough
+51
View File
@@ -0,0 +1,51 @@
# Contributing
Want to contribute to GitBook? That would be awesome!
- [Send feedback](#send-feedback)
- [Reporting Bugs](#reporting-bugs)
- [Asking Questions](#asking-questions)
- [Submitting Pull Requests](#submitting-pull-requests)
- [Running Tests](#running-tests)
## Send feedback
We’ve done our best to test but your feedback is really important. If you encounter problems or have feedback about GitBook products (GitBook.com, GitBook Editor, or the Toolchain), please log an issue in the [GitbookIO/feedback](https://github.com/GitbookIO/feedback/issues) repository.
## Reporting Bugs
If you run into any weird behavior while using GitBook, feel free to open a new issue in this repository! To be most helpful, please include the steps to reproduce the bug as best you can, including the output of command `gitbook -V`.
## Asking Questions
Questions are very welcome :smile:! Previous questions that folks have asked are tagged with a [`question`](https://github.com/GitBookIO/gitbook/issues?q=is%3Aissue+is%3Aclosed+label%3Aquestion) label, so before opening a new issue double-check that someone hasn't asked it before. But if you don't see anything, or if you're not sure if it's the same, err on the side of asking!
We've also got a [GitBook Community Slack](https://slack.gitbook.com/) where you can ask questions and get answers from other people using GitBook.
## Submitting Pull Requests
All pull requests are super welcomed and greatly appreciated! Easy issues are marked with an [`easy-one`](https://github.com/GitBookIO/gitbook/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy-one) label if you're looking for a simple place to get familiar with the code base.
Please include tests and docs with every pull request!
## Running Tests
To run the examples, you need to have the GitBook repository cloned to your computer. After that, you need to `cd` into the directory where you cloned it, and install the dependencies from `npm`.
```
npm install
```
Then you'll need to bootstrap it:
```
npm run bootstrap
```
Which will also compile the source files. Then run the tests with:
```
npm test
```
+1 -4
View File
@@ -5,11 +5,8 @@ module.exports = {
root: './docs',
title: 'GitBook Toolchain Documentation',
// Enforce use of GitBook v3
gitbook: '3.1.1',
// Use the "official" theme
plugins: ['theme-official@2.1.1', '-sharing', '-fontsettings', 'sitemap'],
plugins: ['sitemap'],
variables: {
version: pkg.version
+7
View File
@@ -42,6 +42,13 @@
* [Test your plugin](plugins/testing.md)
* [Theming](themes/README.md)
### Plugin Development
* [Introduction](./api/README.md)
* [Node](./api/node.md)
* [Connect to the context](./api/connect.md)
* [Components](./api/components.md)
--
* [FAQ](faq.md)
-26
View File
@@ -1,26 +0,0 @@
{% extends template.self %}
{% block header_nav %}
<a href="https://github.com/GitbookIO/gitbook/blob/master/docs/{{ file.path }}" target="_blank" class="btn btn-link pull-right hidden-xs">
<i class="octicon octicon-mark-github"></i> Edit on GitHub
</a>
<a href="{{ "faq.md"|resolveFile }}" class="btn btn-link pull-right hidden-xs">
F.A.Q
</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 %}
+24
View File
@@ -0,0 +1,24 @@
# Plugin Architecture
A GitBook plugin is a NPM package that follows a defined convention.
`gitbook-plugin` is a command line utility to help you create, test and release plugins.
### Bootstrap your first plugin
Install `gitbook-plugin` from NPM:
```
$ npm install gitbook-plugin -g
```
Then create your plugin:
```
$ gitbook-plugin create
```
You will be asked for a plugin name, and a few other things to complete the creation process.
### Publish your plugin
+86
View File
@@ -0,0 +1,86 @@
# Components
## Injection
Plugins can inject components by registering React components to some roles.
#### Register a components
During the initialization phase of your plugin, dispatch the `GitBook.registerComponent` action:
```js
dispatch(GitBook.registerComponent(MyCustomButton, { role: 'toolbar:buttons:left' }));
```
#### Roles
Custom roles can be use for interopability with other plugins, but GitBook and the default theme set a convention for common roles:
| Role | Description | Props |
| ---- | ----------- | ----- |
| `page:container` | DIV container for the page's content | `{ page: Page }` |
| `summary:container` | DIV container for the whole summary | `{ summary: Summary }` |
| `summary:parts` | DIV container for summary's parts | `{ parts: List<SummaryPart> }` |
| `summary:part` | DIV for a specific part | `{ part: SummaryPart }` |
| `summary:articles` | UL container for a part's articles | `{ articles: List<SummaryArticle> }` |
| `summary:article` | LI for a specific article | `{ article: SummaryArticle }` |
## Default Components
#### `GitBook.Head`
Extends the meta tags of the page. This is an alias for [react-helmet](https://github.com/nfl/react-helmet).
```js
<GitBook.Head
title="My page"
/>
```
#### `GitBook.ImportCSS`
Import a CSS file by resolving the path correctly according to the current page:
```js
<GitBook.ImportCSS href="myfile.css" />
```
#### `GitBook.ImportJS`
Import a JS file by resolving the path correctly according to the current page:
```js
<GitBook.ImportJS src="mylib.js" />
```
#### `GitBook.InjectedComponent`
Inject a component matching a specific role:
```js
<GitBook.InjectedComponent matching={{ role: 'mycustomrole' }} props={{ someProp: 1 }}>
<b>Inner content</b>
</GitBook.InjectedComponent>
```
#### `GitBook.InjectedComponentSet`
Same API as `InjectedComponentSet` but render the matching components in chain instead of composed:
```js
<GitBook.InjectedComponentSet matching={{ role: 'mytoolbar' }} />
```
**Warning:** Children are discarded.
#### `GitBook.FlexLayout` and `GitBook.FlexBox`
A simple wrapper that provides a Flexbox layout with the given direction and style. Any additional props you set on the Flexbox are rendered.
```js
<GitBook.FlexLayout column>
<GitBook.FlexBox>First column</GitBook.FlexBox>
<GitBook.FlexBox>Second column</GitBook.FlexBox>
</GitBook.FlexLayout>
```
+33
View File
@@ -0,0 +1,33 @@
# Connect to the context
`GitBook.connect(Component, [mapStateToProps], [mapActionsToProps])` connects a react component to the GitBook context.
It does not modify the component class passed to it.
Instead, it returns a new, connected component class, for you to use.
### `mapStateToProps(state, [ownProps]): stateProps`
If specified, the component will subscribe to GitBook store updates. Any time it updates, `mapStateToProps` will be called. Its result must be a plain object, and it will be merged into the component’s props.
If you omit it, the component will not be subscribed to the GitBook store. If `ownProps` is specified as a second argument, its value will be the props passed to your component, and `mapStateToProps` will be additionally re-invoked whenever the component receives new props (e.g. if props received from a parent component have shallowly changed, and you use the `ownProps` argument, `mapStateToProps` is re-evaluated).
For example to render the title of the current page:
```js
const GitBook = require('gitbook-core');
let PageTitle = React.createClass({
render() {
const { page } = this.props;
return <h1>{page.title}</h1>;
}
});
function mapStateToProps(state) {
return { page: state.page };
}
PageTitle = GitBook.connect(PageTitle, mapStateToProps);
```
### `mapActionsToProps(actions, [dispatch])`
+44
View File
@@ -0,0 +1,44 @@
# Internationalize your plugin
GitBook has built-in support for internationalization. Plugins can register new languages and provide the right messages for different languages.
### Register locale and messages
The first step is to register messages for a language:
```js
module.exports = GitBook.createPlugin({
init: (dispatch, getState, { I18n }) => {
dispatch(I18n.registerLocale('en-US', {
MY_PLUGIN_MESSAGE: 'Hello World'
}));
}
});
```
### Render a message in a component
`GitBook.connect` adds a `i18n` prop to access the internationalization:
```js
const GitBook = require('gitbook-core');
const { React } = GitBook;
const MyButton = React.createClass({
propTypes: {
i18n: GitBook.Shapes.i18n
},
render() {
const { i18n } = this.props;
return (
<GitBook.Button>
{i18.t('MY_PLUGIN_MESSAGE')}
</GitBook.Button>
);
}
});
module.exports = GitBook.connect(MyButton);
```
+23
View File
@@ -0,0 +1,23 @@
# Navigation
### Listen to url change
Listen for changes to the current location:
```js
const onLocationChanged = (location) => {
console.log(location.pathname);
console.log(location.query);
console.log(location.hash);
};
module.exports = GitBook.createPlugin({
init: (dispatch, getState, { Navigation }) => {
dispatch(Navigation.listen(onLocationChanged));
}
});
```
The `onLocationChanged` will be triggered for initial state.
### Changing the url
+97
View File
@@ -0,0 +1,97 @@
# Node APIs
GitBooks provides different Node APIs and contexts to plugins. These APIs can vary according to the GitBook version being used, your plugin should specify the `engines.gitbook` field in `package.json` accordingly.
#### Book instance
The `Book` interface is the central point of GitBook, it centralize all access read methods.
```js
// Read configuration from book.json
var value = book.config.get('title', 'Default Value');
// Resolve a filename to an absolute path
var filepath = book.resolve('README.md');
// Render an inline markup string
book.renderInline('markdown', 'This is **Markdown**')
.then(function(str) { ... })
// Render a markup string (block mode)
book.renderBlock('markdown', '* This is **Markdown**')
.then(function(str) { ... })
```
#### Output instance
The `Output` class represent the output/write process.
```js
// Return root folder for the output
var root = output.root();
// Resolve a file in the output folder
var filepath = output.resolve('myimage.png');
// Convert a filename to an URL (returns a path to an html file)
var fileurl = output.toURL('mychapter/README.md');
// Write a file in the output folder
output.writeFile('hello.txt', 'Hello World')
.then(function() { ... });
// Copy a file to the output folder
output.copyFile('./myfile.jpg', 'cover.jpg')
.then(function() { ... });
// Verify that a file exists
output.hasFile('hello.txt')
.then(function(exists) { ... });
```
#### Page instance
A page instance represent the current parsed page.
```js
// Title of the page (from SUMMARY)
page.title
// Content of the page (Markdown/Asciidoc/HTML according to the stage)
page.content
// Relative path in the book
page.path
// Absolute path to the file
page.rawPath
// Type of parser used for this file
page.type ('markdown' or 'asciidoc')
```
#### Context for Blocks and Filters
Blocks and filters have access to the same context, this context is bind to the template engine execution:
```js
{
// Current templating syntax
"ctx": {
// For example, after a {% set message = "hello" %}
"message": "hello"
},
// Book instance
"book" <Book>,
// Output instance
"output": <Output>
}
```
For example a filter or block function can access the current book using: `this.book`.
#### Context for Hooks
Hooks only have access to the `<Book>` instance using `this.book`.
+4
View File
@@ -0,0 +1,4 @@
{
"lerna": "2.0.0-beta.29",
"version": "4.0.0"
}
-22
View File
@@ -1,22 +0,0 @@
var decodeConfig = require('./decodeConfig');
/**
Decode changes from a JS API to a output object.
Only the configuration can be edited by plugin's hooks
@param {Output} output
@param {Object} result: result from API
@return {Output}
*/
function decodeGlobal(output, result) {
var book = output.getBook();
var config = book.getConfig();
// Update config
config = decodeConfig(config, result.config);
book = book.set('config', config);
return output.set('book', book);
}
module.exports = decodeGlobal;
-44
View File
@@ -1,44 +0,0 @@
var deprecate = require('./deprecate');
/**
Decode changes from a JS API to a page object.
Only the content can be edited by plugin's hooks.
@param {Output} output
@param {Page} page: page instance to edit
@param {Object} result: result from API
@return {Page}
*/
function decodePage(output, page, result) {
var originalContent = page.getContent();
// No returned value
// Existing content will be used
if (!result) {
return page;
}
deprecate.disable('page.sections');
// GitBook 3
// Use returned page.content if different from original content
if (result.content != originalContent) {
page = page.set('content', result.content);
}
// GitBook 2 compatibility
// Finally, use page.sections
else if (result.sections) {
page = page.set('content',
result.sections.map(function(section) {
return section.content;
}).join('\n')
);
}
deprecate.enable('page.sections');
return page;
}
module.exports = decodePage;
-122
View File
@@ -1,122 +0,0 @@
var is = require('is');
var objectPath = require('object-path');
var logged = {};
var disabled = {};
/**
Log a deprecated notice
@param {Book|Output} book
@param {String} key
@param {String} message
*/
function logNotice(book, key, message) {
if (logged[key] || disabled[key]) return;
logged[key] = true;
var logger = book.getLogger();
logger.warn.ln(message);
}
/**
Deprecate a function
@param {Book|Output} book
@param {String} key: unique identitifer for the deprecated
@param {Function} fn
@param {String} msg: message to print when called
@return {Function}
*/
function deprecateMethod(book, key, fn, msg) {
return function() {
logNotice(book, key, msg);
return fn.apply(this, arguments);
};
}
/**
Deprecate a property of an object
@param {Book|Output} book
@param {String} key: unique identitifer for the deprecated
@param {Object} instance
@param {String|Function} property
@param {String} msg: message to print when called
@return {Function}
*/
function deprecateField(book, key, instance, property, value, msg) {
var store = undefined;
var prepare = function() {
if (!is.undefined(store)) return;
if (is.fn(value)) store = value();
else store = value;
};
var getter = function(){
prepare();
logNotice(book, key, msg);
return store;
};
var setter = function(v) {
prepare();
logNotice(book, key, msg);
store = v;
return store;
};
Object.defineProperty(instance, property, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
/**
Enable a deprecation
@param {String} key: unique identitifer
*/
function enableDeprecation(key) {
disabled[key] = false;
}
/**
Disable a deprecation
@param {String} key: unique identitifer
*/
function disableDeprecation(key) {
disabled[key] = true;
}
/**
Deprecate a method in favor of another one
@param {Book} book
@param {String} key
@param {Object} instance
@param {String} oldName
@param {String} newName
*/
function deprecateRenamedMethod(book, key, instance, oldName, newName, msg) {
msg = msg || ('"' + oldName + '" is deprecated, use "' + newName + '()" instead');
var fn = objectPath.get(instance, newName);
instance[oldName] = deprecateMethod(book, key, fn, msg);
}
module.exports = {
method: deprecateMethod,
renamedMethod: deprecateRenamedMethod,
field: deprecateField,
enable: enableDeprecation,
disable: disableDeprecation
};
-257
View File
@@ -1,257 +0,0 @@
var path = require('path');
var Promise = require('../utils/promise');
var PathUtils = require('../utils/path');
var fs = require('../utils/fs');
var Plugins = require('../plugins');
var deprecate = require('./deprecate');
var fileToURL = require('../output/helper/fileToURL');
var defaultBlocks = require('../constants/defaultBlocks');
var gitbook = require('../gitbook');
var parsers = require('../parsers');
var encodeConfig = require('./encodeConfig');
var encodeSummary = require('./encodeSummary');
var encodeNavigation = require('./encodeNavigation');
var encodePage = require('./encodePage');
/**
Encode a global context into a JS object
It's the context for page's hook, etc
@param {Output} output
@return {Object}
*/
function encodeGlobal(output) {
var book = output.getBook();
var bookFS = book.getContentFS();
var logger = output.getLogger();
var outputFolder = output.getRoot();
var plugins = output.getPlugins();
var blocks = Plugins.listBlocks(plugins);
var result = {
log: logger,
config: encodeConfig(output, book.getConfig()),
summary: encodeSummary(output, book.getSummary()),
/**
Check if the book is a multilingual book
@return {Boolean}
*/
isMultilingual: function() {
return book.isMultilingual();
},
/**
Check if the book is a language book for a multilingual book
@return {Boolean}
*/
isLanguageBook: function() {
return book.isLanguageBook();
},
/**
Read a file from the book
@param {String} fileName
@return {Promise<Buffer>}
*/
readFile: function(fileName) {
return bookFS.read(fileName);
},
/**
Read a file from the book as a string
@param {String} fileName
@return {Promise<String>}
*/
readFileAsString: function(fileName) {
return bookFS.readAsString(fileName);
},
/**
Resolve a file from the book root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(book.getContentRoot(), fileName);
},
/**
Resolve a page by it path
@param {String} filePath
@return {String}
*/
getPageByPath: function(filePath) {
var page = output.getPage(filePath);
if (!page) return undefined;
return encodePage(output, page);
},
/**
Render a block of text (markdown/asciidoc)
@param {String} type
@param {String} text
@return {Promise<String>}
*/
renderBlock: function(type, text) {
var parser = parsers.get(type);
return parser.parsePage(text)
.get('content');
},
/**
Render an inline text (markdown/asciidoc)
@param {String} type
@param {String} text
@return {Promise<String>}
*/
renderInline: function(type, text) {
var parser = parsers.get(type);
return parser.parseInline(text)
.get('content');
},
template: {
/**
Apply a templating block and returns its result
@param {String} name
@param {Object} blockData
@return {Promise|Object}
*/
applyBlock: function(name, blockData) {
var block = blocks.get(name) || defaultBlocks.get(name);
return Promise(block.applyBlock(blockData, result));
}
},
output: {
/**
Name of the generator being used
{String}
*/
name: output.getGenerator(),
/**
Return absolute path to the root folder of output
@return {String}
*/
root: function() {
return outputFolder;
},
/**
Resolve a file from the output root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(outputFolder, fileName);
},
/**
Convert a filepath into an url
@return {String}
*/
toURL: function(filePath) {
return fileToURL(output, filePath);
},
/**
Check that a file exists.
@param {String} fileName
@return {Promise}
*/
hasFile: function(fileName, content) {
return Promise()
.then(function() {
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
return fs.exists(filePath);
});
},
/**
Write a file to the output folder,
It creates the required folder
@param {String} fileName
@param {Buffer} content
@return {Promise}
*/
writeFile: function(fileName, content) {
return Promise()
.then(function() {
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
return fs.ensureFile(filePath)
.then(function() {
return fs.writeFile(filePath, content);
});
});
},
/**
Copy a file to the output folder
It creates the required folder.
@param {String} inputFile
@param {String} outputFile
@param {Buffer} content
@return {Promise}
*/
copyFile: function(inputFile, outputFile, content) {
return Promise()
.then(function() {
var outputFilePath = PathUtils.resolveInRoot(outputFolder, outputFile);
return fs.ensureFile(outputFilePath)
.then(function() {
return fs.copy(inputFile, outputFilePath);
});
});
}
},
gitbook: {
version: gitbook.version
}
};
// Deprecated properties
deprecate.renamedMethod(output, 'this.isSubBook', result, 'isSubBook', 'isLanguageBook');
deprecate.renamedMethod(output, 'this.contentLink', result, 'contentLink', 'output.toURL');
deprecate.field(output, 'this.generator', result, 'generator',
output.getGenerator(), '"this.generator" property is deprecated, use "this.output.name" instead');
deprecate.field(output, 'this.navigation', result, 'navigation', function() {
return encodeNavigation(output);
}, '"navigation" property is deprecated');
deprecate.field(output, 'this.book', result, 'book',
result, '"book" property is deprecated, use "this" directly instead');
deprecate.field(output, 'this.options', result, 'options',
result.config.values, '"options" property is deprecated, use config.get(key) instead');
return result;
}
module.exports = encodeGlobal;
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
encodePage: require('./encodePage'),
decodePage: require('./decodePage'),
encodeGlobal: require('./encodeGlobal'),
decodeGlobal: require('./decodeGlobal')
};
-26
View File
@@ -1,26 +0,0 @@
var Modifiers = require('./modifiers');
module.exports = {
Parse: require('./parse'),
// Models
Book: require('./models/book'),
FS: require('./models/fs'),
File: require('./models/file'),
Summary: require('./models/summary'),
Glossary: require('./models/glossary'),
Config: require('./models/config'),
Page: require('./models/page'),
PluginDependency: require('./models/pluginDependency'),
// Modifiers
SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config,
// Constants
CONFIG_FILES: require('./constants/configFiles.js'),
IGNORE_FILES: require('./constants/ignoreFiles.js'),
DEFAULT_PLUGINS: require('./constants/defaultPlugins'),
EXTENSIONS_MARKDOWN: require('./constants/extsMarkdown'),
EXTENSIONS_ASCIIDOC: require('./constants/extsAsciidoc')
};
-23
View File
@@ -1,23 +0,0 @@
var path = require('path');
var Book = require('../models/book');
var createNodeFS = require('../fs/node');
/**
Return a book instance to work on from
command line args/kwargs
@param {Array} args
@param {Object} kwargs
@return {Book}
*/
function getBook(args, kwargs) {
var input = path.resolve(args[0] || process.cwd());
var logLevel = kwargs.log;
var fs = createNodeFS(input);
var book = Book.createForFS(fs);
return book.setLogLevel(logLevel);
}
module.exports = getBook;
-17
View File
@@ -1,17 +0,0 @@
var path = require('path');
/**
Return path to output folder
@param {Array} args
@return {String}
*/
function getOutputFolder(args) {
var bookRoot = path.resolve(args[0] || process.cwd());
var defaultOutputRoot = path.join(bookRoot, '_book');
var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot;
return outputFolder;
}
module.exports = getOutputFolder;
-17
View File
@@ -1,17 +0,0 @@
var path = require('path');
var options = require('./options');
var initBook = require('../init');
module.exports = {
name: 'init [book]',
description: 'setup and create files for chapters',
options: [
options.log
],
exec: function(args, kwargs) {
var bookRoot = path.resolve(process.cwd(), args[0] || './');
return initBook(bookRoot);
}
};
-128
View File
@@ -1,128 +0,0 @@
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');
function Server() {
this.running = null;
this.dir = null;
this.port = 0;
this.sockets = [];
}
util.inherits(Server, events.EventEmitter);
/**
Return true if the server is running
@return {Boolean}
*/
Server.prototype.isRunning = function() {
return !!this.running;
};
/**
Stop the server
@return {Promise}
*/
Server.prototype.stop = function() {
var that = this;
if (!this.isRunning()) return Promise();
var d = Promise.defer();
this.running.close(function(err) {
that.running = null;
that.emit('state', false);
if (err) d.reject(err);
else d.resolve();
});
for (var i = 0; i < this.sockets.length; i++) {
this.sockets[i].destroy();
}
return d.promise;
};
/**
Start the server
@return {Promise}
*/
Server.prototype.start = function(dir, port) {
var that = this, pre = Promise();
port = port || 8004;
if (that.isRunning()) pre = this.stop();
return pre
.then(function() {
var d = Promise.defer();
that.running = http.createServer(function(req, res){
// Render error
function error(err) {
res.statusCode = err.status || 500;
res.end(err.message);
}
// Redirect to directory's index.html
function redirect() {
var resultURL = urlTransform(req.url, function(parsed) {
parsed.pathname += '/';
return parsed;
});
res.statusCode = 301;
res.setHeader('Location', resultURL);
res.end('Redirecting to ' + resultURL);
}
res.setHeader('X-Current-Location', req.url);
// Send file
send(req, url.parse(req.url).pathname, {
root: dir
})
.on('error', error)
.on('directory', redirect)
.pipe(res);
});
that.running.on('connection', function (socket) {
that.sockets.push(socket);
socket.setTimeout(4000);
socket.on('close', function () {
that.sockets.splice(that.sockets.indexOf(socket), 1);
});
});
that.running.listen(port, function(err) {
if (err) return d.reject(err);
that.port = port;
that.dir = dir;
that.emit('state', true);
d.resolve();
});
return d.promise;
});
};
/**
urlTransform is a helper function that allows a function to transform
a url string in it's parsed form and returns the new url as a string
@param {String} uri
@param {Function} fn
@return {String}
*/
function urlTransform(uri, fn) {
return url.format(fn(url.parse(uri)));
}
module.exports = Server;
-6
View File
@@ -1,6 +0,0 @@
var Immutable = require('immutable');
var jsonSchemaDefaults = require('json-schema-defaults');
var schema = require('./configSchema');
module.exports = Immutable.fromJS(jsonSchemaDefaults(schema));
-51
View File
@@ -1,51 +0,0 @@
var Immutable = require('immutable');
var TemplateBlock = require('../models/templateBlock');
module.exports = Immutable.Map({
html: TemplateBlock({
name: 'html',
process: function(blk) {
return blk;
}
}),
code: TemplateBlock({
name: 'code',
process: function(blk) {
return {
html: false,
body: blk.body
};
}
}),
markdown: TemplateBlock({
name: 'markdown',
process: function(blk) {
return this.book.renderInline('markdown', blk.body)
.then(function(out) {
return { body: out };
});
}
}),
asciidoc: TemplateBlock({
name: 'asciidoc',
process: function(blk) {
return this.book.renderInline('asciidoc', blk.body)
.then(function(out) {
return { body: out };
});
}
}),
markup: TemplateBlock({
name: 'markup',
process: function(blk) {
return this.book.renderInline(this.ctx.file.type, blk.body)
.then(function(out) {
return { body: out };
});
}
})
});
-10
View File
@@ -1,10 +0,0 @@
var extend = require('extend');
var common = require('./browser');
module.exports = extend({
initBook: require('./init'),
createNodeFS: require('./fs/node'),
Output: require('./output'),
commands: require('./cli')
}, common);
-39
View File
@@ -1,39 +0,0 @@
var extend = require('extend');
var gitbook = require('../gitbook');
var encodeSummary = require('./encodeSummary');
var encodeGlossary = require('./encodeGlossary');
var encodeReadme = require('./encodeReadme');
var encodeLanguages = require('./encodeLanguages');
/**
Encode a book to JSON
@param {Book}
@return {Object}
*/
function encodeBookToJson(book) {
var config = book.getConfig();
var language = book.getLanguage();
var variables = config.getValue('variables', {});
return {
summary: encodeSummary(book.getSummary()),
glossary: encodeGlossary(book.getGlossary()),
readme: encodeReadme(book.getReadme()),
config: book.getConfig().getValues().toJS(),
languages: book.isMultilingual()? encodeLanguages(book.getLanguages()) : undefined,
gitbook: {
version: gitbook.version,
time: gitbook.START_TIME
},
book: extend({
language: language? language : undefined
}, variables.toJS())
};
}
module.exports = encodeBookToJson;
-22
View File
@@ -1,22 +0,0 @@
var encodeBook = require('./encodeBook');
var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile');
/**
* Return a JSON representation of a book with a specific file
*
* @param {Book} output
* @param {Page} page
* @return {Object}
*/
function encodeBookWithPage(book, page) {
var file = page.getFile();
var result = encodeBook(book);
result.page = encodePage(page, book.getSummary());
result.file = encodeFile(file);
return result;
}
module.exports = encodeBookWithPage;
-21
View File
@@ -1,21 +0,0 @@
/**
Return a JSON representation of a file
@param {File} file
@return {Object}
*/
function encodeFileToJson(file) {
var filePath = file.getPath();
if (!filePath) {
return undefined;
}
return {
path: filePath,
mtime: file.getMTime(),
type: file.getType()
};
}
module.exports = encodeFileToJson;
-21
View File
@@ -1,21 +0,0 @@
var encodeFile = require('./encodeFile');
var encodeGlossaryEntry = require('./encodeGlossaryEntry');
/**
Encode a glossary to JSON
@param {Glossary}
@return {Object}
*/
function encodeGlossary(glossary) {
var file = glossary.getFile();
var entries = glossary.getEntries();
return {
file: encodeFile(file),
entries: entries
.map(encodeGlossaryEntry).toJS()
};
}
module.exports = encodeGlossary;
-26
View File
@@ -1,26 +0,0 @@
var encodeFile = require('./encodeFile');
/**
Encode a languages listing to JSON
@param {Languages}
@return {Object}
*/
function encodeLanguages(languages) {
var file = languages.getFile();
var list = languages.getList();
return {
file: encodeFile(file),
list: list
.valueSeq()
.map(function(lang) {
return {
id: lang.getID(),
title: lang.getTitle()
};
}).toJS()
};
}
module.exports = encodeLanguages;
-25
View File
@@ -1,25 +0,0 @@
var encodeBook = require('./encodeBook');
/**
* Encode an output to JSON
*
* @param {Output}
* @return {Object}
*/
function encodeOutputToJson(output) {
var book = output.getBook();
var generator = output.getGenerator();
var options = output.getOptions();
var result = encodeBook(book);
result.output = {
name: generator
};
result.options = options.toJS();
return result;
}
module.exports = encodeOutputToJson;
-23
View File
@@ -1,23 +0,0 @@
var encodeOutput = require('./encodeOutput');
var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile');
/**
* Return a JSON representation of a book with a specific file
*
* @param {Book} output
* @param {Page} page
* @return {Object}
*/
function encodeOutputWithPage(output, page) {
var file = page.getFile();
var book = output.getBook();
var result = encodeOutput(output);
result.page = encodePage(page, book.getSummary());
result.file = encodeFile(file);
return result;
}
module.exports = encodeOutputWithPage;
-39
View File
@@ -1,39 +0,0 @@
var encodeSummaryArticle = require('./encodeSummaryArticle');
/**
Return a JSON representation of a page
@param {Page} page
@param {Summary} summary
@return {Object}
*/
function encodePage(page, summary) {
var file = page.getFile();
var attributes = page.getAttributes();
var article = summary.getByPath(file.getPath());
var result = attributes.toJS();
if (article) {
result.title = article.getTitle();
result.level = article.getLevel();
result.depth = article.getDepth();
var nextArticle = summary.getNextArticle(article);
if (nextArticle) {
result.next = encodeSummaryArticle(nextArticle);
}
var prevArticle = summary.getPrevArticle(article);
if (prevArticle) {
result.previous = encodeSummaryArticle(prevArticle);
}
}
result.content = page.getContent();
result.dir = page.getDir();
return result;
}
module.exports = encodePage;
-17
View File
@@ -1,17 +0,0 @@
var encodeFile = require('./encodeFile');
/**
Encode a readme to JSON
@param {Readme}
@return {Object}
*/
function encodeReadme(readme) {
var file = readme.getFile();
return {
file: encodeFile(file)
};
}
module.exports = encodeReadme;
-20
View File
@@ -1,20 +0,0 @@
var encodeFile = require('./encodeFile');
var encodeSummaryPart = require('./encodeSummaryPart');
/**
Encode a summary to JSON
@param {Summary}
@return {Object}
*/
function encodeSummary(summary) {
var file = summary.getFile();
var parts = summary.getParts();
return {
file: encodeFile(file),
parts: parts.map(encodeSummaryPart).toJS()
};
}
module.exports = encodeSummary;
-28
View File
@@ -1,28 +0,0 @@
/**
Encode a SummaryArticle to JSON
@param {SummaryArticle}
@return {Object}
*/
function encodeSummaryArticle(article, recursive) {
var articles = undefined;
if (recursive !== false) {
articles = article.getArticles()
.map(encodeSummaryArticle)
.toJS();
}
return {
title: article.getTitle(),
level: article.getLevel(),
depth: article.getDepth(),
anchor: article.getAnchor(),
url: article.getUrl(),
path: article.getPath(),
ref: article.getRef(),
articles: articles
};
}
module.exports = encodeSummaryArticle;
-17
View File
@@ -1,17 +0,0 @@
var encodeSummaryArticle = require('./encodeSummaryArticle');
/**
Encode a SummaryPart to JSON
@param {SummaryPart}
@return {Object}
*/
function encodeSummaryPart(part) {
return {
title: part.getTitle(),
articles: part.getArticles()
.map(encodeSummaryArticle).toJS()
};
}
module.exports = encodeSummaryPart;
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
encodeOutput: require('./encodeOutput'),
encodeBookWithPage: require('./encodeBookWithPage'),
encodeOutputWithPage: require('./encodeOutputWithPage'),
encodeBook: require('./encodeBook'),
encodeFile: require('./encodeFile'),
encodePage: require('./encodePage'),
encodeSummary: require('./encodeSummary'),
encodeSummaryArticle: require('./encodeSummaryArticle'),
encodeReadme: require('./encodeReadme'),
encodeLanguages: require('./encodeLanguages')
};
-205
View File
@@ -1,205 +0,0 @@
var nunjucks = require('nunjucks');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
describe('TemplateBlock', function() {
var TemplateBlock = require('../templateBlock');
describe('create', function() {
it('must initialize a simple TemplateBlock from a function', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// Check basic templateBlock properties
expect(templateBlock.getName()).toBe('sayhello');
expect(templateBlock.getEndTag()).toBe('endsayhello');
expect(templateBlock.getBlocks().size).toBe(0);
expect(templateBlock.getExtensionName()).toBe('BlocksayhelloExtension');
// Check result of applying block
return Promise()
.then(function() {
return templateBlock.applyBlock();
})
.then(function(result) {
expect(result.name).toBe('sayhello');
expect(result.body).toBe('<p>Hello, World!</p>');
});
});
});
describe('getShortcuts', function() {
it('must return undefined if no shortcuts', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
expect(templateBlock.getShortcuts()).toNotExist();
});
it('must return complete shortcut', function() {
var templateBlock = TemplateBlock.create('sayhello', {
process: function(block) {
return '<p>Hello, World!</p>';
},
shortcuts: {
parsers: ['markdown'],
start: '$',
end: '-'
}
});
var shortcut = templateBlock.getShortcuts();
expect(shortcut).toBeDefined();
expect(shortcut.getStart()).toEqual('$');
expect(shortcut.getEnd()).toEqual('-');
expect(shortcut.getStartTag()).toEqual('sayhello');
expect(shortcut.getEndTag()).toEqual('endsayhello');
});
});
describe('toNunjucksExt()', function() {
it('should replace by block anchor', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return 'Hello';
});
var blocks = {};
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt({}, blocks);
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
blocks = Immutable.fromJS(blocks);
expect(blocks.size).toBe(1);
var blockId = blocks.keySeq().get(0);
var block = blocks.get(blockId);
expect(res).toBe('{{-%' + blockId + '%-}}');
expect(block.get('body')).toBe('Hello');
expect(block.get('name')).toBe('sayhello');
});
});
it('must create a valid nunjucks extension', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p>Hello, World!</p>');
});
});
it('must apply block arguments correctly', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!</'+block.kwargs.tag+'>',
parse: true
};
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello name="Samy", tag="p" %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p>Hello, Samy!</p>');
});
});
it('must accept an async function', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return Promise()
.then(function() {
return {
body: 'Hello ' + block.body,
parse: true
};
});
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello %}Samy{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('Hello Samy');
});
});
it('must handle nested blocks', function() {
var templateBlock = new TemplateBlock({
name: 'yoda',
blocks: Immutable.List(['start', 'end']),
process: function(block) {
var nested = {};
block.blocks.forEach(function(blk) {
nested[blk.name] = blk.body.trim();
});
return {
body: '<p class="yoda">'+nested.end+' '+nested.start+'</p>',
parse: true
};
}
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% yoda %}{% start %}this sentence should be{% end %}inverted{% endyoda %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p class="yoda">inverted this sentence should be</p>');
});
});
});
});
-364
View File
@@ -1,364 +0,0 @@
var path = require('path');
var Immutable = require('immutable');
var Logger = require('../utils/logger');
var FS = require('./fs');
var Config = require('./config');
var Readme = require('./readme');
var Summary = require('./summary');
var Glossary = require('./glossary');
var Languages = require('./languages');
var Ignore = require('./ignore');
var Book = Immutable.Record({
// Logger for outptu message
logger: Logger(),
// Filesystem binded to the book scope to read files/directories
fs: FS(),
// Ignore files parser
ignore: Ignore(),
// Structure files
config: Config(),
readme: Readme(),
summary: Summary(),
glossary: Glossary(),
languages: Languages(),
// ID of the language for language books
language: String(),
// List of children, if multilingual (String -> Book)
books: Immutable.OrderedMap()
});
Book.prototype.getLogger = function() {
return this.get('logger');
};
Book.prototype.getFS = function() {
return this.get('fs');
};
Book.prototype.getIgnore = function() {
return this.get('ignore');
};
Book.prototype.getConfig = function() {
return this.get('config');
};
Book.prototype.getReadme = function() {
return this.get('readme');
};
Book.prototype.getSummary = function() {
return this.get('summary');
};
Book.prototype.getGlossary = function() {
return this.get('glossary');
};
Book.prototype.getLanguages = function() {
return this.get('languages');
};
Book.prototype.getBooks = function() {
return this.get('books');
};
Book.prototype.getLanguage = function() {
return this.get('language');
};
/**
Return FS instance to access the content
@return {FS}
*/
Book.prototype.getContentFS = function() {
var fs = this.getFS();
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
return FS.reduceScope(fs, rootFolder);
}
return fs;
};
/**
Return root of the book
@return {String}
*/
Book.prototype.getRoot = function() {
var fs = this.getFS();
return fs.getRoot();
};
/**
Return root for content of the book
@return {String}
*/
Book.prototype.getContentRoot = function() {
var fs = this.getContentFS();
return fs.getRoot();
};
/**
Check if a file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isFileIgnored = function(filename) {
var ignore = this.getIgnore();
var language = this.getLanguage();
// Ignore is always relative to the root of the main book
if (language) {
filename = path.join(language, filename);
}
return ignore.isFileIgnored(filename);
};
/**
Check if a content file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isContentFileIgnored = function(filename) {
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
filename = path.join(rootFolder, filename);
}
return this.isFileIgnored(filename);
};
/**
Return a page from a book by its path
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.getPage = function(ref) {
return this.getPages().get(ref);
};
/**
Is this book the parent of language's books
@return {Boolean}
*/
Book.prototype.isMultilingual = function() {
return (this.getLanguages().getCount() > 0);
};
/**
Return true if book is associated to a language
@return {Boolean}
*/
Book.prototype.isLanguageBook = function() {
return Boolean(this.getLanguage());
};
/**
Return a languages book
@param {String} language
@return {Book}
*/
Book.prototype.getLanguageBook = function(language) {
var books = this.getBooks();
return books.get(language);
};
/**
Add a new language book
@param {String} language
@param {Book} book
@return {Book}
*/
Book.prototype.addLanguageBook = function(language, book) {
var books = this.getBooks();
books = books.set(language, book);
return this.set('books', books);
};
/**
Set the summary for this book
@param {Summary}
@return {Book}
*/
Book.prototype.setSummary = function(summary) {
return this.set('summary', summary);
};
/**
Set the readme for this book
@param {Readme}
@return {Book}
*/
Book.prototype.setReadme = function(readme) {
return this.set('readme', readme);
};
/**
Set the configuration for this book
@param {Config}
@return {Book}
*/
Book.prototype.setConfig = function(config) {
return this.set('config', config);
};
/**
Set the ignore instance for this book
@param {Ignore}
@return {Book}
*/
Book.prototype.setIgnore = function(ignore) {
return this.set('ignore', ignore);
};
/**
Change log level
@param {String} level
@return {Book}
*/
Book.prototype.setLogLevel = function(level) {
this.getLogger().setLevel(level);
return this;
};
/**
Create a book using a filesystem
@param {FS} fs
@return {Book}
*/
Book.createForFS = function createForFS(fs) {
return new Book({
fs: fs
});
};
/**
Infers the default extension for files
@return {String}
*/
Book.prototype.getDefaultExt = function() {
// Inferring sources
var clues = [
this.getReadme(),
this.getSummary(),
this.getGlossary()
];
// List their extensions
var exts = clues.map(function (clue) {
var file = clue.getFile();
if (file.exists()) {
return file.getParser().getExtensions().first();
} else {
return null;
}
});
// Adds the general default extension
exts.push('.md');
// Choose the first non null
return exts.find(function (e) { return e !== null; });
};
/**
Infer the default path for a Readme.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultReadmePath = function(absolute) {
var defaultPath = 'README'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Summary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultSummaryPath = function(absolute) {
var defaultPath = 'SUMMARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Glossary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultGlossaryPath = function(absolute) {
var defaultPath = 'GLOSSARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Create a language book from a parent
@param {Book} parent
@param {String} language
@return {Book}
*/
Book.createFromParent = function createFromParent(parent, language) {
var ignore = parent.getIgnore();
var config = parent.getConfig();
// Set language in configuration
config = config.setValue('language', language);
return new Book({
// Inherits config. logegr and list of ignored files
logger: parent.getLogger(),
config: config,
ignore: ignore,
language: language,
fs: FS.reduceScope(parent.getContentFS(), language)
});
};
module.exports = Book;
-42
View File
@@ -1,42 +0,0 @@
var Immutable = require('immutable');
var IgnoreMutable = require('ignore');
/*
Immutable version of node-ignore
*/
var Ignore = Immutable.Record({
ignore: new IgnoreMutable()
}, 'Ignore');
Ignore.prototype.getIgnore = function() {
return this.get('ignore');
};
/**
Test if a file is ignored by these rules
@param {String} filePath
@return {Boolean}
*/
Ignore.prototype.isFileIgnored = function(filename) {
var ignore = this.getIgnore();
return ignore.filter([filename]).length == 0;
};
/**
Add rules
@param {String}
@return {Ignore}
*/
Ignore.prototype.add = function(rule) {
var ignore = this.getIgnore();
var newIgnore = new IgnoreMutable();
newIgnore.add(ignore);
newIgnore.add(rule);
return this.set('ignore', newIgnore);
};
module.exports = Ignore;
-107
View File
@@ -1,107 +0,0 @@
var Immutable = require('immutable');
var Book = require('./book');
var LocationUtils = require('../utils/location');
var Output = Immutable.Record({
book: Book(),
// Name of the generator being used
generator: String(),
// Map of plugins to use (String -> Plugin)
plugins: Immutable.OrderedMap(),
// Map pages to generation (String -> Page)
pages: Immutable.OrderedMap(),
// List assets (String)
assets: Immutable.List(),
// Option for the generation
options: Immutable.Map(),
// Internal state for the generation
state: Immutable.Map()
});
Output.prototype.getBook = function() {
return this.get('book');
};
Output.prototype.getGenerator = function() {
return this.get('generator');
};
Output.prototype.getPlugins = function() {
return this.get('plugins');
};
Output.prototype.getPages = function() {
return this.get('pages');
};
Output.prototype.getOptions = function() {
return this.get('options');
};
Output.prototype.getAssets = function() {
return this.get('assets');
};
Output.prototype.getState = function() {
return this.get('state');
};
/**
Return a page byt its file path
@param {String} filePath
@return {Page|undefined}
*/
Output.prototype.getPage = function(filePath) {
filePath = LocationUtils.normalize(filePath);
var pages = this.getPages();
return pages.get(filePath);
};
/**
Get root folder for output
@return {String}
*/
Output.prototype.getRoot = function() {
return this.getOptions().get('root');
};
/**
Update state of output
@param {Map} newState
@return {Output}
*/
Output.prototype.setState = function(newState) {
return this.set('state', newState);
};
/**
Update options
@param {Map} newOptions
@return {Output}
*/
Output.prototype.setOptions = function(newOptions) {
return this.set('options', newOptions);
};
/**
Return logegr for this output (same as book)
@return {Logger}
*/
Output.prototype.getLogger = function() {
return this.getBook().getLogger();
};
module.exports = Output;
-70
View File
@@ -1,70 +0,0 @@
var Immutable = require('immutable');
var yaml = require('js-yaml');
var File = require('./file');
var Page = Immutable.Record({
file: File(),
// Attributes extracted from the YAML header
attributes: Immutable.Map(),
// Content of the page
content: String(),
// Direction of the text
dir: String('ltr')
});
Page.prototype.getFile = function() {
return this.get('file');
};
Page.prototype.getAttributes = function() {
return this.get('attributes');
};
Page.prototype.getContent = function() {
return this.get('content');
};
Page.prototype.getDir = function() {
return this.get('dir');
};
/**
* Return page as text
* @return {String}
*/
Page.prototype.toText = function() {
var attrs = this.getAttributes();
var content = this.getContent();
if (attrs.size === 0) {
return content;
}
var frontMatter = '---\n' + yaml.safeDump(attrs.toJS(), { skipInvalid: true }) + '---\n\n';
return (frontMatter + content);
};
/**
* Return path of the page
* @return {String}
*/
Page.prototype.getPath = function() {
return this.getFile().getPath();
};
/**
* Create a page for a file
* @param {File} file
* @return {Page}
*/
Page.createForFile = function(file) {
return new Page({
file: file
});
};
module.exports = Page;
-169
View File
@@ -1,169 +0,0 @@
var Immutable = require('immutable');
var TemplateBlock = require('./templateBlock');
var PluginDependency = require('./pluginDependency');
var THEME_PREFIX = require('../constants/themePrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({
name: String(),
// Requirement version (ex: ">1.0.0")
version: String(DEFAULT_VERSION),
// Path to load this plugin
path: String(),
// Depth of this plugin in the dependency tree
depth: Number(0),
// Parent depending on this plugin
parent: String(),
// Content of the "package.json"
package: Immutable.Map(),
// Content of the package itself
content: Immutable.Map()
}, 'Plugin');
Plugin.prototype.getName = function() {
return this.get('name');
};
Plugin.prototype.getPath = function() {
return this.get('path');
};
Plugin.prototype.getVersion = function() {
return this.get('version');
};
Plugin.prototype.getPackage = function() {
return this.get('package');
};
Plugin.prototype.getContent = function() {
return this.get('content');
};
Plugin.prototype.getDepth = function() {
return this.get('depth');
};
Plugin.prototype.getParent = function() {
return this.get('parent');
};
/**
* Return the ID on NPM for this plugin
* @return {String}
*/
Plugin.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName());
};
/**
* Check if a plugin is loaded
* @return {Boolean}
*/
Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0);
};
/**
* Check if a plugin is a theme given its name
* @return {Boolean}
*/
Plugin.prototype.isTheme = function() {
var name = this.getName();
return (name && name.indexOf(THEME_PREFIX) === 0);
};
/**
* Return map of hooks
* @return {Map<String:Function>}
*/
Plugin.prototype.getHooks = function() {
return this.getContent().get('hooks') || Immutable.Map();
};
/**
* Return infos about resources for a specific type
* @param {String} type
* @return {Map<String:Mixed>}
*/
Plugin.prototype.getResources = function(type) {
if (type != 'website' && type != 'ebook') {
throw new Error('Invalid assets type ' + type);
}
var content = this.getContent();
return (content.get(type)
|| (type == 'website'? content.get('book') : null)
|| Immutable.Map());
};
/**
* Return map of filters
* @return {Map<String:Function>}
*/
Plugin.prototype.getFilters = function() {
return this.getContent().get('filters');
};
/**
* Return map of blocks
* @return {Map<String:TemplateBlock>}
*/
Plugin.prototype.getBlocks = function() {
var blocks = this.getContent().get('blocks');
blocks = blocks || Immutable.Map();
return blocks
.map(function(block, blockName) {
return TemplateBlock.create(blockName, block);
});
};
/**
* Return a specific hook
* @param {String} name
* @return {Function|undefined}
*/
Plugin.prototype.getHook = function(name) {
return this.getHooks().get(name);
};
/**
* Create a plugin from a string
* @param {String}
* @return {Plugin}
*/
Plugin.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
return new Plugin({
name: name,
version: version || DEFAULT_VERSION
});
};
/**
* Create a plugin from a dependency
* @param {PluginDependency}
* @return {Plugin}
*/
Plugin.createFromDep = function(dep) {
return new Plugin({
name: dep.getName(),
version: dep.getVersion()
});
};
Plugin.nameToNpmID = PluginDependency.nameToNpmID;
module.exports = Plugin;
-281
View File
@@ -1,281 +0,0 @@
var is = require('is');
var extend = require('extend');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var genKey = require('../utils/genKey');
var TemplateShortcut = require('./templateShortcut');
var NODE_ENDARGS = '%%endargs%%';
var TemplateBlock = Immutable.Record({
// Name of block, also the start tag
name: String(),
// End tag, default to "end<name>"
end: String(),
// Function to process the block content
process: Function(),
// List of String, for inner block tags
blocks: Immutable.List(),
// List of shortcuts to replace with this block
shortcuts: Immutable.Map()
}, 'TemplateBlock');
TemplateBlock.prototype.getName = function() {
return this.get('name');
};
TemplateBlock.prototype.getEndTag = function() {
return this.get('end') || ('end' + this.getName());
};
TemplateBlock.prototype.getProcess = function() {
return this.get('process');
};
TemplateBlock.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
* Return shortcuts associated with this block or undefined
* @return {TemplateShortcut|undefined}
*/
TemplateBlock.prototype.getShortcuts = function() {
var shortcuts = this.get('shortcuts');
if (shortcuts.size === 0) {
return undefined;
}
return TemplateShortcut.createForBlock(this, shortcuts);
};
/**
* Return name for the nunjucks extension
* @return {String}
*/
TemplateBlock.prototype.getExtensionName = function() {
return 'Block' + this.getName() + 'Extension';
};
/**
* Return a nunjucks extension to represents this block
* @return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
blocksOutput = blocksOutput || {};
var that = this;
var name = this.getName();
var endTag = this.getEndTag();
var blocks = this.getBlocks().toJS();
function Ext() {
this.tags = [name];
this.parse = function(parser, nodes) {
var lastBlockName = null;
var lastBlockArgs = null;
var allBlocks = blocks.concat([endTag]);
// Parse first block
var tok = parser.nextToken();
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(tok.value);
var args = new nodes.NodeList();
var bodies = [];
var blockNamesNode = new nodes.Array(tok.lineno, tok.colno);
var blockArgCounts = new nodes.Array(tok.lineno, tok.colno);
// Parse while we found "end<block>"
do {
// Read body
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
// Handle body with previous block name and args
blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName));
blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length));
bodies.push(currentBody);
// Append arguments of this block as arguments of the run function
lastBlockArgs.children.forEach(function(child) {
args.addChild(child);
});
// Read new block
lastBlockName = parser.nextToken().value;
// Parse signature and move to the end of the block
if (lastBlockName != endTag) {
lastBlockArgs = parser.parseSignature(null, true);
}
parser.advanceAfterBlockEnd(lastBlockName);
} while (lastBlockName != endTag);
args.addChild(blockNamesNode);
args.addChild(blockArgCounts);
args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS));
return new nodes.CallExtensionAsync(this, 'run', args, bodies);
};
this.run = function(context) {
var fnArgs = Array.prototype.slice.call(arguments, 1);
var args;
var blocks = [];
var bodies = [];
var blockNames;
var blockArgCounts;
var callback;
// Extract callback
callback = fnArgs.pop();
// Detect end of arguments
var endArgIndex = fnArgs.indexOf(NODE_ENDARGS);
// Extract arguments and bodies
args = fnArgs.slice(0, endArgIndex);
bodies = fnArgs.slice(endArgIndex + 1);
// Extract block counts
blockArgCounts = args.pop();
blockNames = args.pop();
// Recreate list of blocks
blockNames.forEach(function(name, i) {
var countArgs = blockArgCounts[i];
var blockBody = bodies.shift();
var blockArgs = countArgs > 0? args.slice(0, countArgs) : [];
args = args.slice(countArgs);
var blockKwargs = extractKwargs(blockArgs);
blocks.push({
name: name,
body: blockBody(),
args: blockArgs,
kwargs: blockKwargs
});
});
var mainBlock = blocks.shift();
mainBlock.blocks = blocks;
Promise()
.then(function() {
var ctx = extend({
ctx: context
}, mainContext || {});
return that.applyBlock(mainBlock, ctx);
})
.then(function(result) {
return that.blockResultToHtml(result, blocksOutput);
})
.nodeify(callback);
};
}
return Ext;
};
/**
* Apply a block to a content
* @param {Object} inner
* @param {Object} context
* @return {Promise<String>|String}
*/
TemplateBlock.prototype.applyBlock = function(inner, context) {
var processFn = this.getProcess();
inner = inner || {};
inner.args = inner.args || [];
inner.kwargs = inner.kwargs || {};
inner.blocks = inner.blocks || [];
var r = processFn.call(context, inner);
if (Promise.isPromiseAlike(r)) {
return r.then(this.normalizeBlockResult.bind(this));
} else {
return this.normalizeBlockResult(r);
}
};
/**
* Normalize result from a block process function
* @param {Object|String} result
* @return {Object}
*/
TemplateBlock.prototype.normalizeBlockResult = function(result) {
if (is.string(result)) {
result = { body: result };
}
result.name = this.getName();
return result;
};
/**
* Convert a block result to HTML
* @param {Object} result
* @param {Object} blocksOutput: stored post processing blocks in this object
* @return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
var indexedKey;
var toIndex = (!result.parse) || (result.post !== undefined);
if (toIndex) {
indexedKey = genKey();
blocksOutput[indexedKey] = result;
}
// Parsable block, just return it
if (result.parse) {
return result.body;
}
// Return it as a position marker
return '{{-%' + indexedKey + '%-}}';
};
/**
* Create a template block from a function or an object
* @param {String} blockName
* @param {Object} block
* @return {TemplateBlock}
*/
TemplateBlock.create = function(blockName, block) {
if (is.fn(block)) {
block = new Immutable.Map({
process: block
});
}
block = new TemplateBlock(block);
block = block.set('name', blockName);
return block;
};
/**
* Extract kwargs from an arguments array
* @param {Array} args
* @return {Object}
*/
function extractKwargs(args) {
var last = args[args.length - 1];
return (is.object(last) && last.__keywords)? args.pop() : {};
}
module.exports = TemplateBlock;
-139
View File
@@ -1,139 +0,0 @@
var nunjucks = require('nunjucks');
var Immutable = require('immutable');
var TemplateEngine = Immutable.Record({
// Map of {TemplateBlock}
blocks: Immutable.Map(),
// Map of Extension
extensions: Immutable.Map(),
// Map of filters: {String} name -> {Function} fn
filters: Immutable.Map(),
// Map of globals: {String} name -> {Mixed}
globals: Immutable.Map(),
// Context for filters / blocks
context: Object(),
// Nunjucks loader
loader: nunjucks.FileSystemLoader('views')
}, 'TemplateEngine');
TemplateEngine.prototype.getBlocks = function() {
return this.get('blocks');
};
TemplateEngine.prototype.getGlobals = function() {
return this.get('globals');
};
TemplateEngine.prototype.getFilters = function() {
return this.get('filters');
};
TemplateEngine.prototype.getShortcuts = function() {
return this.get('shortcuts');
};
TemplateEngine.prototype.getLoader = function() {
return this.get('loader');
};
TemplateEngine.prototype.getContext = function() {
return this.get('context');
};
TemplateEngine.prototype.getExtensions = function() {
return this.get('extensions');
};
/**
Return a block by its name (or undefined)
@param {String} name
@return {TemplateBlock}
*/
TemplateEngine.prototype.getBlock = function(name) {
var blocks = this.getBlocks();
return blocks.find(function(block) {
return block.getName() === name;
});
};
/**
Return a nunjucks environment from this configuration
@return {Nunjucks.Environment}
*/
TemplateEngine.prototype.toNunjucks = function(blocksOutput) {
var loader = this.getLoader();
var blocks = this.getBlocks();
var filters = this.getFilters();
var globals = this.getGlobals();
var extensions = this.getExtensions();
var context = this.getContext();
var env = new nunjucks.Environment(
loader,
{
// Escaping is done after by the asciidoc/markdown parser
autoescape: false,
// Syntax
tags: {
blockStart: '{%',
blockEnd: '%}',
variableStart: '{{',
variableEnd: '}}',
commentStart: '{###',
commentEnd: '###}'
}
}
);
// Add filters
filters.forEach(function(filterFn, filterName) {
env.addFilter(filterName, filterFn.bind(context));
});
// Add blocks
blocks.forEach(function(block) {
var extName = block.getExtensionName();
var Ext = block.toNunjucksExt(context, blocksOutput);
env.addExtension(extName, new Ext());
});
// Add globals
globals.forEach(function(globalValue, globalName) {
env.addGlobal(globalName, globalValue);
});
// Add other extensions
extensions.forEach(function(ext, extName) {
env.addExtension(extName, ext);
});
return env;
};
/**
Create a template engine
@param {Object} def
@return {TemplateEngine}
*/
TemplateEngine.create = function(def) {
return new TemplateEngine({
blocks: Immutable.List(def.blocks || []),
extensions: Immutable.Map(def.extensions || {}),
filters: Immutable.Map(def.filters || {}),
globals: Immutable.Map(def.globals || {}),
context: def.context,
loader: def.loader
});
};
module.exports = TemplateEngine;
-42
View File
@@ -1,42 +0,0 @@
var Immutable = require('immutable');
var TemplateOutput = Immutable.Record({
// Text content of the template
content: String(),
// Map of blocks to replace / post process
blocks: Immutable.Map()
}, 'TemplateOutput');
TemplateOutput.prototype.getContent = function() {
return this.get('content');
};
TemplateOutput.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
* Update content of this output
* @param {String} content
* @return {TemplateContent}
*/
TemplateOutput.prototype.setContent = function(content) {
return this.set('content', content);
};
/**
* Create a TemplateOutput from a text content
* and an object containing block definition
* @param {String} content
* @param {Object} blocks
* @return {TemplateOutput}
*/
TemplateOutput.create = function(content, blocks) {
return new TemplateOutput({
content: content,
blocks: Immutable.fromJS(blocks)
});
};
module.exports = TemplateOutput;
-45
View File
@@ -1,45 +0,0 @@
var Templating = require('../templating');
var TemplateEngine = require('../models/templateEngine');
var Api = require('../api');
var Plugins = require('../plugins');
var defaultBlocks = require('../constants/defaultBlocks');
var defaultFilters = require('../constants/defaultFilters');
/**
Create template engine for an output.
It adds default filters/blocks, then add the ones from plugins
@param {Output} output
@return {TemplateEngine}
*/
function createTemplateEngine(output) {
var plugins = output.getPlugins();
var book = output.getBook();
var rootFolder = book.getContentRoot();
var logger = book.getLogger();
var filters = Plugins.listFilters(plugins);
var blocks = Plugins.listBlocks(plugins);
// Extend with default
blocks = defaultBlocks.merge(blocks);
filters = defaultFilters.merge(filters);
// Create loader
var transformFn = Templating.replaceShortcuts.bind(null, blocks);
var loader = new Templating.ConrefsLoader(rootFolder, transformFn, logger);
// Create API context
var context = Api.encodeGlobal(output);
return new TemplateEngine({
filters: filters,
blocks: blocks,
loader: loader,
context: context
});
}
module.exports = createTemplateEngine;
-41
View File
@@ -1,41 +0,0 @@
var juice = require('juice');
var WebsiteGenerator = require('../website');
var JSONUtils = require('../../json');
var Templating = require('../../templating');
var Promise = require('../../utils/promise');
/**
Generate PDF header/footer templates
@param {Output} output
@param {String} type
@return {String}
*/
function getPDFTemplate(output, type) {
var filePath = 'pdf_' + type + '.html';
var outputRoot = output.getRoot();
var engine = WebsiteGenerator.createTemplateEngine(output, filePath);
// Generate context
var context = JSONUtils.encodeOutput(output);
context.page = {
num: '_PAGENUM_',
title: '_SECTION_'
};
// Render the theme
return Templating.renderFile(engine, 'ebook/' + filePath, context)
// Inline css and assets
.then(function(tplOut) {
return Promise.nfcall(juice.juiceResources, tplOut.getContent(), {
webResources: {
relativeTo: outputRoot
}
});
});
}
module.exports = getPDFTemplate;
-91
View File
@@ -1,91 +0,0 @@
var path = require('path');
var WebsiteGenerator = require('../website');
var JSONUtils = require('../../json');
var Templating = require('../../templating');
var Promise = require('../../utils/promise');
var error = require('../../utils/error');
var command = require('../../utils/command');
var writeFile = require('../helper/writeFile');
var getConvertOptions = require('./getConvertOptions');
var SUMMARY_FILE = 'SUMMARY.html';
/**
Write the SUMMARY.html
@param {Output}
@return {Output}
*/
function writeSummary(output) {
var options = output.getOptions();
var prefix = options.get('prefix');
var filePath = SUMMARY_FILE;
var engine = WebsiteGenerator.createTemplateEngine(output, filePath);
var context = JSONUtils.encodeOutput(output);
// Render the theme
return Templating.renderFile(engine, prefix + '/summary.html', context)
// Write it to the disk
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
}
/**
Generate the ebook file as "index.pdf"
@param {Output}
@return {Output}
*/
function runEbookConvert(output) {
var logger = output.getLogger();
var options = output.getOptions();
var format = options.get('format');
var outputFolder = output.getRoot();
if (!format) {
return Promise(output);
}
return getConvertOptions(output)
.then(function(options) {
var cmd = [
'ebook-convert',
path.resolve(outputFolder, SUMMARY_FILE),
path.resolve(outputFolder, 'index.' + format),
command.optionsToShellArgs(options)
].join(' ');
return command.exec(cmd)
.progress(function(data) {
logger.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);
});
})
.thenResolve(output);
}
/**
Finish the generation, generates the SUMMARY.html
@param {Output}
@return {Output}
*/
function onFinish(output) {
return writeSummary(output)
.then(runEbookConvert);
}
module.exports = onFinish;
-17
View File
@@ -1,17 +0,0 @@
var Immutable = require('immutable');
var Options = Immutable.Record({
// Root folder for the output
root: String(),
// Prefix for generation
prefix: String('ebook'),
// Format to generate using ebook-convert
format: String(),
// Force use of absolute urls ("index.html" instead of "/")
directoryIndex: Boolean(false)
});
module.exports = Options;
-79
View File
@@ -1,79 +0,0 @@
var path = require('path');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var timing = require('../utils/timing');
var Templating = require('../templating');
var JSONUtils = require('../json');
var createTemplateEngine = require('./createTemplateEngine');
var callPageHook = require('./callPageHook');
/**
* Prepare and generate HTML for a page
*
* @param {Output} output
* @param {Page} page
* @return {Promise<Page>}
*/
function generatePage(output, page) {
var book = output.getBook();
var engine = createTemplateEngine(output);
return timing.measure(
'page.generate',
Promise(page)
.then(function(resultPage) {
var file = resultPage.getFile();
var filePath = file.getPath();
var parser = file.getParser();
var context = JSONUtils.encodeOutputWithPage(output, resultPage);
if (!parser) {
return Promise.reject(error.FileNotParsableError({
filename: filePath
}));
}
// Call hook "page:before"
return callPageHook('page:before', output, resultPage)
// Escape code blocks with raw tags
.then(function(currentPage) {
return parser.preparePage(currentPage.getContent());
})
// Render templating syntax
.then(function(content) {
var absoluteFilePath = path.join(book.getContentRoot(), filePath);
return Templating.render(engine, absoluteFilePath, content, context);
})
.then(function(output) {
var content = output.getContent();
return parser.parsePage(content)
.then(function(result) {
return output.setContent(result.content);
});
})
// Post processing for templating syntax
.then(function(output) {
return Templating.postRender(engine, output);
})
// Return new page
.then(function(content) {
return resultPage.set('content', content);
})
// Call final hook
.then(function(currentPage) {
return callPageHook('page', output, currentPage);
});
})
);
}
module.exports = generatePage;
-73
View File
@@ -1,73 +0,0 @@
var Modifiers = require('./modifiers');
var resolveFileToURL = require('./helper/resolveFileToURL');
var Api = require('../api');
var Plugins = require('../plugins');
var Promise = require('../utils/promise');
var defaultBlocks = require('../constants/defaultBlocks');
var fileToOutput = require('./helper/fileToOutput');
var CODEBLOCK = 'code';
/**
* Return default modifier to prepare a page for
* rendering.
*
* @return {Array<Modifier>}
*/
function getModifiers(output, page) {
var book = output.getBook();
var plugins = output.getPlugins();
var glossary = book.getGlossary();
var file = page.getFile();
// Glossary entries
var entries = glossary.getEntries();
var glossaryFile = glossary.getFile();
var glossaryFilename = fileToOutput(output, glossaryFile.getPath());
// Current file path
var currentFilePath = file.getPath();
// Get TemplateBlock for highlighting
var blocks = Plugins.listBlocks(plugins);
var code = blocks.get(CODEBLOCK) || defaultBlocks.get(CODEBLOCK);
// Current context
var context = Api.encodeGlobal(output);
return [
// Normalize IDs on headings
Modifiers.addHeadingId,
// Annotate text with glossary entries
Modifiers.annotateText.bind(null, entries, glossaryFilename),
// Resolve images
Modifiers.resolveImages.bind(null, currentFilePath),
// Resolve links (.md -> .html)
Modifiers.resolveLinks.bind(null,
currentFilePath,
resolveFileToURL.bind(null, output)
),
// Highlight code blocks using "code" block
Modifiers.highlightCode.bind(null, function(lang, source) {
return Promise(code.applyBlock({
body: source,
kwargs: {
language: lang
}
}, context))
.then(function(result) {
if (result.html === false) {
return { text: result.body };
} else {
return { html: result.body };
}
});
})
];
}
module.exports = getModifiers;
-32
View File
@@ -1,32 +0,0 @@
var path = require('path');
var PathUtils = require('../../utils/path');
var LocationUtils = require('../../utils/location');
var OUTPUT_EXTENSION = '.html';
/**
* Convert a filePath (absolute) to a filename for output
*
* @param {Output} output
* @param {String} filePath
* @return {String}
*/
function fileToOutput(output, filePath) {
var book = output.getBook();
var readme = book.getReadme();
var fileReadme = readme.getFile();
if (
path.basename(filePath, path.extname(filePath)) == 'README' ||
(fileReadme.exists() && filePath == fileReadme.getPath())
) {
filePath = path.join(path.dirname(filePath), 'index' + OUTPUT_EXTENSION);
} else {
filePath = PathUtils.setExtension(filePath, OUTPUT_EXTENSION);
}
return LocationUtils.normalize(filePath);
}
module.exports = fileToOutput;
-31
View File
@@ -1,31 +0,0 @@
var path = require('path');
var LocationUtils = require('../../utils/location');
var fileToOutput = require('./fileToOutput');
/**
Convert a filePath (absolute) to an url (without hostname).
It returns an absolute path.
"README.md" -> "/"
"test/hello.md" -> "test/hello.html"
"test/README.md" -> "test/"
@param {Output} output
@param {String} filePath
@return {String}
*/
function fileToURL(output, filePath) {
var options = output.getOptions();
var directoryIndex = options.get('directoryIndex');
filePath = fileToOutput(output, filePath);
if (directoryIndex && path.basename(filePath) == 'index.html') {
filePath = path.dirname(filePath) + '/';
}
return LocationUtils.normalize(filePath);
}
module.exports = fileToURL;
-26
View File
@@ -1,26 +0,0 @@
var LocationUtils = require('../../utils/location');
var fileToURL = require('./fileToURL');
/**
* Resolve an absolute path (extracted from a link)
*
* @param {Output} output
* @param {String} filePath
* @return {String}
*/
function resolveFileToURL(output, filePath) {
// Convert /test.png -> test.png
filePath = LocationUtils.toAbsolute(filePath, '', '');
var page = output.getPage(filePath);
// if file is a page, return correct .html url
if (page) {
filePath = fileToURL(output, filePath);
}
return LocationUtils.normalize(filePath);
}
module.exports = resolveFileToURL;
@@ -1,40 +0,0 @@
var cheerio = require('cheerio');
var tmp = require('tmp');
var path = require('path');
var URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png';
describe('fetchRemoteImages', function() {
var dir;
var fetchRemoteImages = require('../fetchRemoteImages');
beforeEach(function() {
dir = tmp.dirSync();
});
it('should download image file', function() {
var $ = cheerio.load('<img src="' + URL + '" />');
return fetchRemoteImages(dir.name, 'index.html', $)
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(src);
});
});
it('should download image file and replace with relative path', function() {
var $ = cheerio.load('<img src="' + URL + '" />');
return fetchRemoteImages(dir.name, 'test/index.html', $)
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(path.join('test', src));
});
});
});
@@ -1,60 +0,0 @@
var cheerio = require('cheerio');
var Promise = require('../../../utils/promise');
var highlightCode = require('../highlightCode');
describe('highlightCode', function() {
function doHighlight(lang, code) {
return {
text: '' + (lang || '') + '$' + code
};
}
function doHighlightAsync(lang, code) {
return Promise()
.then(function() {
return doHighlight(lang, code);
});
}
it('should call it for normal code element', function() {
var $ = cheerio.load('<p>This is a <code>test</code></p>');
return highlightCode(doHighlight, $)
.then(function() {
var $code = $('code');
expect($code.text()).toBe('$test');
});
});
it('should call it for markdown code block', function() {
var $ = cheerio.load('<pre><code class="lang-js">test</code></pre>');
return highlightCode(doHighlight, $)
.then(function() {
var $code = $('code');
expect($code.text()).toBe('js$test');
});
});
it('should call it for asciidoc code block', function() {
var $ = cheerio.load('<pre><code class="language-python">test</code></pre>');
return highlightCode(doHighlight, $)
.then(function() {
var $code = $('code');
expect($code.text()).toBe('python$test');
});
});
it('should accept async highlighter', function() {
var $ = cheerio.load('<pre><code class="language-python">test</code></pre>');
return highlightCode(doHighlightAsync, $)
.then(function() {
var $code = $('code');
expect($code.text()).toBe('python$test');
});
});
});
@@ -1,25 +0,0 @@
var cheerio = require('cheerio');
var tmp = require('tmp');
var inlinePng = require('../inlinePng');
describe('inlinePng', function() {
var dir;
beforeEach(function() {
dir = tmp.dirSync();
});
it('should write an inline PNG using data URI as a file', function() {
var $ = cheerio.load('<img alt="GitBook Logo 20x20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUEAYAAADdGcFOAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAF+klEQVRIDY3Wf5CVVR3H8c9z791fyI9dQwdQ4TTI7wEWnQZZAa/mJE4Z0OaKUuN1KoaykZxUGGHay+iIVFMoEYrUPhDCKEKW2ChT8dA0RCSxWi6EW3sYYpcfxq5C+4O9957O+7m7O/qHQ9/XzH1+nHuec57z8wkWTsKw0y6N/LxXN6KzTnEUHi8eP/l3YStSU/MdsYvBbGh8six2YXcbcgc++QkfTQkWz/81KtqDA0hlUoWnsX+5uxe5X365BB9my2bjrHNHccLk16BpS9CExjcmXMDbD6wehdyEjxbjz1uK1zn9qga6dcfnMLXeXY/qjuQqTF4W1MKke8ZgeNhjMCxMPIWSd4OF78C55CFI/1kF6WwXpMqjkAZ/CKniNDrCsmU4lE1YbPlgR2x7R39FF23D4mq3A1+Z35PGTNs1E1XhxcGQOh6HNPwXkK56BVJhOaRg/pvoHXNxHFw410B25EYE2RMvI0i/twFJvXcrFObykEa+DmnQGLwYqR0l2a6JqItaj8C/4E2QxtZCofkC8tF1t8HZc/fAZaLnIF2xEsoEtW1w7vBSSFtfhDTnCki9cSi81Ain1uko2Ld+Dmf2rkUq0/5t+PYbFtPQdkjzNiAXTWtDEF49FgkzJInAVPwNyhzcDOmrdZCm/Rn+ebWtcPs+/U24hmg2XL0rRkPPELh9R8fDtXR2oC/VuZbGaci79Ajkb6lZgfyYtyzy/X9s6T/pO/ZfN/RdNxxIwTWM2wbX8KVmuIaEqmKm6zEondwGpd0SyOy5DrJ//TFkX9kMhd3XQHbEVCSsm4OECV5HIv2p15CwfWPSntoHRbv2Q1HzSvSlSqZwATIuBxk/zZBOBbdB+u9hSKU3Q7pwAjInZkFm6U8hu7MSMqe/Dqn8fUj5GVCmpxK+4N/F1LMa0p5eSOPqIPP7NGSunAI/+R6GnzQzIBt8A1LC/QZ+6HwLst1rITv0n5CtXgSZ78yFTNkR+FdeDZneJkip3fAtsQ5Scilkek7CH9dAmjIWvkK7IXXOh6/IzZDNPQdZXR1TQmdjKv0ZfEu0YKDpNflpyG5aDtnRv8VAuu3dBV+huyBbvgdS97tQNLQc0mfugKy5Cb4BipPIXvsUpK5N8Mvao/Bd3QDZRH9Rrtj3Cl6FHwPFMLmNkKrj8BnHoT+XX6f2wl+XxFS4Ab7C72Dgf7bi+5DpTkNm8kQMpCs/BzIlz8LfPxnzLdh3EjwMX4GX4Ju4GNb9A1L7k/D3J8b6kv2LFCtmCmcgUzoJsr2z4MfwFsh87xikZefg188fYaAhpPUxm3ge/vFnYkoED0HqeQiyJYcwkNGWnoNv6s9C1p1Bf/389VYoCjohW7UfMms3wXdpBv7+FEiPLIHs4DIMNERUNhbSpY3wk6QOsqlCDVx2xCrInMpBmfNPQOnzKxBkkrugdOl9GKigSZZCUWIm/GqwDtLUI5D+WAOlb9wKP0YvQLbjZSjsaYaL/n0/FA3fDtnCGihK5UYjCK+ZDr+TDIKLdm2Fs1UOzo76F5wO74XSZj0S6d7RCMLkCshcXALZxaWQRjXDZQ62oRAdCeG/Ju5HELX2QFH3C0hkRy6GovyfwF58AoVbguOxyB2H7/I34Gf11yANnQSp7Vr4MbQH0vg7kbNNp5AM3UrIVDchnz56B1Jm573wW9gZSFVPwO/hefg5FsIvN09CchtQCIOFw/F5U8ii3CZn4cqo7C8YlXEPYkx9cacZl00+iwnprrtwVdj1Q/gXmAs/pu6LZc9XQOGgSvh19n2cDZN341g2EcfxTEGwH/RewqlMsUfbbWIGLjUG+j/j9nokD1beiOvLS5dhjr30Gu6ZnivgdtM/6VJvY1+6pBHbH+h9CX84vfMxNJtisYVFlys+WNCIZJNmIsjohlhNSQC3f8R55H+y/hjkN8GPR9ndCLJxT4/3n0Px51ay8XQnNrYfDJHf//Fc0oMrEZSeeQGJ7+Z+gKCgLbHNWgXnB9FlYt5JaN38JIINC95EakjtAqQeuUx21c5B6tEFf0fSfbEFQf28Z6D6y+X/H0jf40QQJhYwAAAAAElFTkSuQmCC"/>');
return inlinePng(dir.name, 'index.html', $)
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(src);
});
});
});
@@ -1,104 +0,0 @@
var path = require('path');
var cheerio = require('cheerio');
var resolveLinks = require('../resolveLinks');
describe('resolveLinks', function() {
function resolveFileBasic(href) {
return 'fakeDir/' + href;
}
function resolveFileCustom(href) {
if (path.extname(href) == '.md') {
return href.slice(0, -3) + '.html';
}
return href;
}
describe('Absolute path', function() {
var TEST = '<p>This is a <a href="/test/cool.md"></a></p>';
it('should resolve path starting by "/" in root directory', function() {
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileBasic, $)
.then(function() {
var link = $('a');
expect(link.attr('href')).toBe('fakeDir/test/cool.md');
});
});
it('should resolve path starting by "/" in child directory', function() {
var $ = cheerio.load(TEST);
return resolveLinks('afolder/hello.md', resolveFileBasic, $)
.then(function() {
var link = $('a');
expect(link.attr('href')).toBe('../fakeDir/test/cool.md');
});
});
});
describe('Anchor', function() {
it('should prevent anchors in resolution', function() {
var TEST = '<p>This is a <a href="test/cool.md#an-anchor"></a></p>';
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileCustom, $)
.then(function() {
var link = $('a');
expect(link.attr('href')).toBe('test/cool.html#an-anchor');
});
});
it('should ignore pure anchor links', function() {
var TEST = '<p>This is a <a href="#an-anchor"></a></p>';
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileCustom, $)
.then(function() {
var link = $('a');
expect(link.attr('href')).toBe('#an-anchor');
});
});
});
describe('Custom Resolver', function() {
var TEST = '<p>This is a <a href="/test/cool.md"></a> <a href="afile.png"></a></p>';
it('should resolve path correctly for absolute path', function() {
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileCustom, $)
.then(function() {
var link = $('a').first();
expect(link.attr('href')).toBe('test/cool.html');
});
});
it('should resolve path correctly for absolute path (2)', function() {
var $ = cheerio.load(TEST);
return resolveLinks('afodler/hello.md', resolveFileCustom, $)
.then(function() {
var link = $('a').first();
expect(link.attr('href')).toBe('../test/cool.html');
});
});
});
describe('External link', function() {
var TEST = '<p>This is a <a href="http://www.github.com">external link</a></p>';
it('should have target="_blank" attribute', function() {
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileBasic, $)
.then(function() {
var link = $('a');
expect(link.attr('target')).toBe('_blank');
});
});
});
});
@@ -1,25 +0,0 @@
var cheerio = require('cheerio');
var tmp = require('tmp');
describe('svgToImg', function() {
var dir;
var svgToImg = require('../svgToImg');
beforeEach(function() {
dir = tmp.dirSync();
});
it('should write svg as a file', function() {
var $ = cheerio.load('<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 svgToImg(dir.name, 'index.html', $)
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(src);
});
});
});
@@ -1,33 +0,0 @@
var cheerio = require('cheerio');
var tmp = require('tmp');
var path = require('path');
var svgToImg = require('../svgToImg');
var svgToPng = require('../svgToPng');
describe('svgToPng', function() {
var dir;
beforeEach(function() {
dir = tmp.dirSync();
});
it('should write svg as png file', function() {
var $ = cheerio.load('<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>');
var fileName = 'index.html';
return svgToImg(dir.name, fileName, $)
.then(function() {
return svgToPng(dir.name, fileName, $);
})
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(src);
expect(path.extname(src)).toBe('.png');
});
});
});
-44
View File
@@ -1,44 +0,0 @@
var path = require('path');
var crc = require('crc');
var editHTMLElement = require('./editHTMLElement');
var fs = require('../../utils/fs');
var LocationUtils = require('../../utils/location');
/**
Fetch all remote images
@param {String} rootFolder
@param {String} currentFile
@param {HTMLDom} $
@return {Promise}
*/
function fetchRemoteImages(rootFolder, currentFile, $) {
var currentDirectory = path.dirname(currentFile);
return editHTMLElement($, 'img', function($img) {
var src = $img.attr('src');
var extension = path.extname(src);
if (!LocationUtils.isExternal(src)) {
return;
}
// We avoid generating twice the same PNG
var hash = crc.crc32(src).toString(16);
var fileName = hash + extension;
var filePath = path.join(rootFolder, fileName);
return fs.assertFile(filePath, function() {
return fs.download(src, filePath);
})
.then(function() {
// Convert to relative
src = LocationUtils.relative(currentDirectory, fileName);
$img.replaceWith('<img src="' + src + '" />');
});
});
}
module.exports = fetchRemoteImages;
-58
View File
@@ -1,58 +0,0 @@
var is = require('is');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
var editHTMLElement = require('./editHTMLElement');
/**
Return language for a code blocks from a list of class names
@param {Array<String>}
@return {String}
*/
function getLanguageForClass(classNames) {
return Immutable.List(classNames)
.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;
})
.find(function(cl) {
return Boolean(cl);
});
}
/**
Highlight all code elements
@param {Function(lang, body) -> String} highlight
@param {HTMLDom} $
@return {Promise}
*/
function highlightCode(highlight, $) {
return editHTMLElement($, 'code', function($code) {
var classNames = ($code.attr('class') || '').split(' ');
var lang = getLanguageForClass(classNames);
var source = $code.text();
return Promise(highlight(lang, source))
.then(function(r) {
if (is.string(r.html)) {
$code.html(r.html);
} else {
$code.text(r.text);
}
});
});
}
module.exports = highlightCode;
-25
View File
@@ -1,25 +0,0 @@
var cheerio = require('cheerio');
var Promise = require('../../utils/promise');
/**
Apply a list of operations to a page and
output the new page.
@param {Page}
@param {List|Array<Transformation>}
@return {Promise<Page>}
*/
function modifyHTML(page, operations) {
var html = page.getContent();
var $ = cheerio.load(html);
return Promise.forEach(operations, function(op) {
return op($);
})
.then(function() {
var resultHTML = $.html();
return page.set('content', resultHTML);
});
}
module.exports = modifyHTML;
-53
View File
@@ -1,53 +0,0 @@
var path = require('path');
var url = require('url');
var LocationUtils = require('../../utils/location');
var editHTMLElement = require('./editHTMLElement');
/**
Resolve all HTML links:
- /test.md in hello -> ../test.html
@param {String} currentFile
@param {Function(String) -> String} resolveFile
@param {HTMLDom} $
*/
function resolveLinks(currentFile, resolveFile, $) {
var currentDirectory = path.dirname(currentFile);
return editHTMLElement($, 'a', function($a) {
var href = $a.attr('href');
// Don't change a tag without href
if (!href) {
return;
}
if (LocationUtils.isExternal(href)) {
$a.attr('target', '_blank');
return;
}
// Split anchor
var parsed = url.parse(href);
href = parsed.pathname || '';
if (href) {
// Calcul absolute path for this
href = LocationUtils.toAbsolute(href, currentDirectory, '.');
// Resolve file
href = resolveFile(href);
// Convert back to relative
href = LocationUtils.relative(currentDirectory, href);
}
// Add back anchor
href = href + (parsed.hash || '');
$a.attr('href', href);
});
}
module.exports = resolveLinks;
-26
View File
@@ -1,26 +0,0 @@
var Parse = require('../parse');
var Promise = require('../utils/promise');
/**
List and prepare all pages
@param {Output}
@return {Promise<Output>}
*/
function preparePages(output) {
var book = output.getBook();
var logger = book.getLogger();
if (book.isMultilingual()) {
return Promise(output);
}
return Parse.parsePagesList(book)
.then(function(pages) {
logger.info.ln('found', pages.size, 'pages');
return output.set('pages', pages);
});
}
module.exports = preparePages;
-38
View File
@@ -1,38 +0,0 @@
var createMockOutput = require('../../__tests__/createMock');
var prepareI18n = require('../prepareI18n');
var createTemplateEngine = require('../createTemplateEngine');
var WebsiteGenerator = require('../');
describe('i18n', function() {
it('should correctly use english as default language', function() {
return createMockOutput(WebsiteGenerator, {
'README.md': 'Hello World'
})
.then(function(output) {
return prepareI18n(output);
})
.then(function(output) {
var engine = createTemplateEngine(output, 'README.md');
var t = engine.getFilters().get('t');
expect(t('SUMMARY_INTRODUCTION')).toEqual('Introduction');
});
});
it('should correctly use language from book.json', function() {
return createMockOutput(WebsiteGenerator, {
'README.md': 'Hello World',
'book.json': JSON.stringify({ language: 'fr' })
})
.then(function(output) {
return prepareI18n(output);
})
.then(function(output) {
var engine = createTemplateEngine(output, 'README.md');
var t = engine.getFilters().get('t');
expect(t('GITBOOK_LINK')).toEqual('Publié avec GitBook');
});
});
});
-117
View File
@@ -1,117 +0,0 @@
var path = require('path');
var ASSET_FOLDER = require('../../constants/pluginAssetsFolder');
var Promise = require('../../utils/promise');
var fs = require('../../utils/fs');
/**
Copy all assets from plugins.
Assets are files stored in "_assets"
nd resources declared in the plugin itself.
@param {Output}
@return {Promise}
*/
function copyPluginAssets(output) {
var book = output.getBook();
// Don't copy plugins assets for language book
// It'll be resolved to the parent folder
if (book.isLanguageBook()) {
return Promise(output);
}
var plugins = output.getPlugins()
// We reverse the order of plugins to copy
// so that first plugins can replace assets from other plugins.
.reverse();
return Promise.forEach(plugins, function(plugin) {
return copyAssets(output, plugin)
.then(function() {
return copyResources(output, plugin);
});
})
.thenResolve(output);
}
/**
Copy assets from a plugin
@param {Plugin}
@return {Promise}
*/
function copyAssets(output, plugin) {
var logger = output.getLogger();
var pluginRoot = plugin.getPath();
var options = output.getOptions();
var outputRoot = options.get('root');
var assetOutputFolder = path.join(outputRoot, 'gitbook');
var prefix = options.get('prefix');
var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix);
if (!fs.existsSync(assetFolder)) {
return Promise();
}
logger.debug.ln('copy assets from theme', assetFolder);
return fs.copyDir(
assetFolder,
assetOutputFolder,
{
deleteFirst: false,
overwrite: true,
confirm: true
}
);
}
/**
Copy resources from a plugin
@param {Plugin}
@return {Promise}
*/
function copyResources(output, plugin) {
var logger = output.getLogger();
var options = output.getOptions();
var outputRoot = options.get('root');
var state = output.getState();
var resources = state.getResources();
var pluginRoot = plugin.getPath();
var pluginResources = resources.get(plugin.getName());
var assetsFolder = pluginResources.get('assets');
var assetOutputFolder = path.join(outputRoot, 'gitbook', plugin.getNpmID());
if (!assetsFolder) {
return Promise();
}
// Resolve assets folder
assetsFolder = path.resolve(pluginRoot, assetsFolder);
if (!fs.existsSync(assetsFolder)) {
logger.warn.ln('assets folder for plugin "' + plugin.getName() + '" doesn\'t exist');
return Promise();
}
logger.debug.ln('copy resources from plugin', assetsFolder);
return fs.copyDir(
assetsFolder,
assetOutputFolder,
{
deleteFirst: false,
overwrite: true,
confirm: true
}
);
}
module.exports = copyPluginAssets;
-151
View File
@@ -1,151 +0,0 @@
var path = require('path');
var nunjucks = require('nunjucks');
var DoExtension = require('nunjucks-do')(nunjucks);
var Api = require('../../api');
var deprecate = require('../../api/deprecate');
var JSONUtils = require('../../json');
var LocationUtils = require('../../utils/location');
var fs = require('../../utils/fs');
var PathUtils = require('../../utils/path');
var TemplateEngine = require('../../models/templateEngine');
var templatesFolder = require('../../constants/templatesFolder');
var defaultFilters = require('../../constants/defaultFilters');
var Templating = require('../../templating');
var listSearchPaths = require('./listSearchPaths');
var fileToURL = require('../helper/fileToURL');
var resolveFileToURL = require('../helper/resolveFileToURL');
/**
* Directory for a theme with the templates
*/
function templateFolder(dir) {
return path.join(dir, templatesFolder);
}
/**
* Create templating engine to render themes
*
* @param {Output} output
* @param {String} currentFile
* @return {TemplateEngine}
*/
function createTemplateEngine(output, currentFile) {
var book = output.getBook();
var state = output.getState();
var i18n = state.getI18n();
var config = book.getConfig();
var summary = book.getSummary();
var outputFolder = output.getRoot();
// Search paths for templates
var searchPaths = listSearchPaths(output);
var tplSearchPaths = searchPaths.map(templateFolder);
// Create loader
var loader = new Templating.ThemesLoader(tplSearchPaths);
// Get languages
var language = config.getValue('language');
// Create API context
var context = Api.encodeGlobal(output);
/**
* Check if a file exists
* @param {String} fileName
* @return {Boolean}
*/
function fileExists(fileName) {
if (!fileName) {
return false;
}
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
return fs.existsSync(filePath);
}
/**
* Return an article by its path
* @param {String} filePath
* @return {Object|undefined}
*/
function getArticleByPath(filePath) {
var article = summary.getByPath(filePath);
if (!article) return undefined;
return JSONUtils.encodeSummaryArticle(article);
}
/**
* Return a page by its path
* @param {String} filePath
* @return {Object|undefined}
*/
function getPageByPath(filePath) {
var page = output.getPage(filePath);
if (!page) return undefined;
return JSONUtils.encodePage(page, summary);
}
return TemplateEngine.create({
loader: loader,
context: context,
globals: {
getArticleByPath: getArticleByPath,
getPageByPath: getPageByPath,
fileExists: fileExists
},
filters: defaultFilters.merge({
/**
* Translate a sentence
*/
t: function t(s) {
return i18n.t(language, s);
},
/**
* Resolve an absolute file path into a
* relative path.
* it also resolve pages
*/
resolveFile: function(filePath) {
filePath = resolveFileToURL(output, filePath);
return LocationUtils.relativeForFile(currentFile, filePath);
},
resolveAsset: function(filePath) {
filePath = LocationUtils.toAbsolute(filePath, '', '');
filePath = path.join('gitbook', filePath);
filePath = LocationUtils.relativeForFile(currentFile, filePath);
// Use assets from parent if language book
if (book.isLanguageBook()) {
filePath = path.join('../', filePath);
}
return LocationUtils.normalize(filePath);
},
fileExists: deprecate.method(book, 'fileExists', fileExists, 'Filter "fileExists" is deprecated, use "fileExists(filename)" '),
getArticleByPath: deprecate.method(book, 'getArticleByPath', fileExists, 'Filter "getArticleByPath" is deprecated, use "getArticleByPath(filename)" '),
contentURL: function(filePath) {
return fileToURL(output, filePath);
}
}),
extensions: {
'DoExtension': new DoExtension()
}
});
}
module.exports = createTemplateEngine;
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
name: 'website',
State: require('./state'),
Options: require('./options'),
onInit: require('./onInit'),
onFinish: require('./onFinish'),
onPage: require('./onPage'),
onAsset: require('./onAsset'),
createTemplateEngine: require('./createTemplateEngine')
};
-23
View File
@@ -1,23 +0,0 @@
/**
List search paths for templates / i18n, etc
@param {Output} output
@return {List<String>}
*/
function listSearchPaths(output) {
var book = output.getBook();
var plugins = output.getPlugins();
var searchPaths = plugins
.valueSeq()
.map(function(plugin) {
return plugin.getPath();
})
.toList();
return searchPaths.unshift(book.getContentRoot());
}
module.exports = listSearchPaths;
-28
View File
@@ -1,28 +0,0 @@
var path = require('path');
var fs = require('../../utils/fs');
/**
Copy an asset to the output folder
@param {Output} output
@param {Page} page
*/
function onAsset(output, asset) {
var book = output.getBook();
var options = output.getOptions();
var bookFS = book.getContentFS();
var outputFolder = options.get('root');
var outputPath = path.resolve(outputFolder, asset);
return fs.ensureFile(outputPath)
.then(function() {
return bookFS.readAsStream(asset)
.then(function(stream) {
return fs.writeStream(outputPath, stream);
});
})
.thenResolve(output);
}
module.exports = onAsset;
-35
View File
@@ -1,35 +0,0 @@
var Promise = require('../../utils/promise');
var JSONUtils = require('../../json');
var Templating = require('../../templating');
var writeFile = require('../helper/writeFile');
var createTemplateEngine = require('./createTemplateEngine');
/**
Finish the generation, write the languages index
@param {Output}
@return {Output}
*/
function onFinish(output) {
var book = output.getBook();
var options = output.getOptions();
var prefix = options.get('prefix');
if (!book.isMultilingual()) {
return Promise(output);
}
var filePath = 'index.html';
var engine = createTemplateEngine(output, filePath);
var context = JSONUtils.encodeOutput(output);
// Render the theme
return Templating.renderFile(engine, prefix + '/languages.html', context)
// Write it to the disk
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
}
module.exports = onFinish;
-20
View File
@@ -1,20 +0,0 @@
var Promise = require('../../utils/promise');
var copyPluginAssets = require('./copyPluginAssets');
var prepareI18n = require('./prepareI18n');
var prepareResources = require('./prepareResources');
/**
Initialize the generator
@param {Output}
@return {Output}
*/
function onInit(output) {
return Promise(output)
.then(prepareI18n)
.then(prepareResources)
.then(copyPluginAssets);
}
module.exports = onInit;
-76
View File
@@ -1,76 +0,0 @@
var path = require('path');
var omit = require('omit-keys');
var Templating = require('../../templating');
var Plugins = require('../../plugins');
var JSONUtils = require('../../json');
var LocationUtils = require('../../utils/location');
var Modifiers = require('../modifiers');
var writeFile = require('../helper/writeFile');
var getModifiers = require('../getModifiers');
var createTemplateEngine = require('./createTemplateEngine');
var fileToOutput = require('../helper/fileToOutput');
/**
* Write a page as a json file
*
* @param {Output} output
* @param {Page} page
*/
function onPage(output, page) {
var options = output.getOptions();
var prefix = options.get('prefix');
var file = page.getFile();
var book = output.getBook();
var plugins = output.getPlugins();
var state = output.getState();
var resources = state.getResources();
var engine = createTemplateEngine(output, page.getPath());
// Output file path
var filePath = fileToOutput(output, file.getPath());
// Calcul relative path to the root
var outputDirName = path.dirname(filePath);
var basePath = LocationUtils.normalize(path.relative(outputDirName, './'));
return Modifiers.modifyHTML(page, getModifiers(output, page))
.then(function(resultPage) {
// Generate the context
var context = JSONUtils.encodeOutputWithPage(output, resultPage);
context.plugins = {
resources: Plugins.listResources(plugins, resources).toJS()
};
context.template = {
getJSContext: function() {
return {
page: omit(context.page, 'content'),
config: context.config,
file: context.file,
gitbook: context.gitbook,
basePath: basePath,
book: {
language: book.getLanguage()
}
};
}
};
// We should probabbly move it to "template" or a "site" namespace
context.basePath = basePath;
// Render the theme
return Templating.renderFile(engine, prefix + '/page.html', context)
// Write it to the disk
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
});
}
module.exports = onPage;
-14
View File
@@ -1,14 +0,0 @@
var Immutable = require('immutable');
var Options = Immutable.Record({
// Root folder for the output
root: String(),
// Prefix for generation
prefix: String('website'),
// Use directory index url instead of "index.html"
directoryIndex: Boolean(true)
});
module.exports = Options;
-30
View File
@@ -1,30 +0,0 @@
var path = require('path');
var fs = require('../../utils/fs');
var Promise = require('../../utils/promise');
var listSearchPaths = require('./listSearchPaths');
/**
* Prepare i18n, load translations from plugins and book
*
* @param {Output}
* @return {Promise<Output>}
*/
function prepareI18n(output) {
var state = output.getState();
var i18n = state.getI18n();
var searchPaths = listSearchPaths(output);
searchPaths
.reverse()
.forEach(function(searchPath) {
var i18nRoot = path.resolve(searchPath, '_i18n');
if (!fs.existsSync(i18nRoot)) return;
i18n.load(i18nRoot);
});
return Promise(output);
}
module.exports = prepareI18n;
-54
View File
@@ -1,54 +0,0 @@
var is = require('is');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
var Api = require('../../api');
/**
Prepare plugins resources, add all output corresponding type resources
@param {Output}
@return {Promise<Output>}
*/
function prepareResources(output) {
var plugins = output.getPlugins();
var options = output.getOptions();
var type = options.get('prefix');
var state = output.getState();
var context = Api.encodeGlobal(output);
var result = Immutable.Map();
return Promise.forEach(plugins, function(plugin) {
var pluginResources = plugin.getResources(type);
return Promise()
.then(function() {
// Apply resources if is a function
if (is.fn(pluginResources)) {
return Promise()
.then(pluginResources.bind(context));
}
else {
return pluginResources;
}
})
.then(function(resources) {
result = result.set(plugin.getName(), Immutable.Map(resources));
});
})
.then(function() {
// Set output resources
state = state.merge({
resources: result
});
output = output.merge({
state: state
});
return output;
});
}
module.exports = prepareResources;
-43
View File
@@ -1,43 +0,0 @@
var timing = require('../utils/timing');
/**
List all assets in a book
Assets are file not ignored and not a page
@param {Book} book
@param {List<String>} pages
@param
*/
function listAssets(book, pages) {
var fs = book.getContentFS();
var summary = book.getSummary();
var summaryFile = summary.getFile().getPath();
var glossary = book.getGlossary();
var glossaryFile = glossary.getFile().getPath();
var langs = book.getLanguages();
var langsFile = langs.getFile().getPath();
var config = book.getConfig();
var configFile = config.getFile().getPath();
function filterFile(file) {
return !(
file === summaryFile ||
file === glossaryFile ||
file === langsFile ||
file === configFile ||
book.isContentFileIgnored(file) ||
pages.has(file)
);
}
return timing.measure(
'parse.listAssets',
fs.listAllFiles('.', filterFile)
);
}
module.exports = listAssets;
-19
View File
@@ -1,19 +0,0 @@
var path = require('path');
var Book = require('../../models/book');
var createNodeFS = require('../../fs/node');
var findForBook = require('../findForBook');
describe('findForBook', function() {
var fs = createNodeFS(
path.resolve(__dirname, '../../..')
);
var book = Book.createForFS(fs);
it('should list default plugins', function() {
return findForBook(book)
.then(function(plugins) {
expect(plugins.has('fontsettings')).toBeTruthy();
});
});
});
-29
View File
@@ -1,29 +0,0 @@
var path = require('path');
var PluginDependency = require('../../models/pluginDependency');
var Book = require('../../models/book');
var NodeFS = require('../../fs/node');
var installPlugin = require('../installPlugin');
var Parse = require('../../parse');
describe('installPlugin', function() {
var book;
this.timeout(30000);
before(function() {
var fs = NodeFS(path.resolve(__dirname, '../../../'));
var baseBook = Book.createForFS(fs);
return Parse.parseConfig(baseBook)
.then(function(_book) {
book = _book;
});
});
it('must install a plugin from NPM', function() {
var dep = PluginDependency.createFromString('ga');
return installPlugin(book, dep);
});
});
-30
View File
@@ -1,30 +0,0 @@
var path = require('path');
var Book = require('../../models/book');
var NodeFS = require('../../fs/node');
var installPlugins = require('../installPlugins');
var Parse = require('../../parse');
describe('installPlugins', function() {
var book;
this.timeout(30000);
before(function() {
var fs = NodeFS(path.resolve(__dirname, '../../../'));
var baseBook = Book.createForFS(fs);
return Parse.parseConfig(baseBook)
.then(function(_book) {
book = _book;
});
});
it('must install all plugins from NPM', function() {
return installPlugins(book)
.then(function(n) {
expect(n).toBe(2);
});
});
});
-38
View File
@@ -1,38 +0,0 @@
var PluginDependency = require('../../models/pluginDependency');
var listDependencies = require('../listDependencies');
var toNames = require('../toNames');
describe('listDependencies', function() {
it('must list default', function() {
var deps = PluginDependency.listFromString('ga,great');
var plugins = listDependencies(deps);
var names = toNames(plugins);
expect(names).toEqual([
'ga', 'great',
'highlight', 'search', 'lunr', 'sharing', 'fontsettings',
'theme-default' ]);
});
it('must list from array with -', function() {
var deps = PluginDependency.listFromString('ga,-great');
var plugins = listDependencies(deps);
var names = toNames(plugins);
expect(names).toEqual([
'ga',
'highlight', 'search', 'lunr', 'sharing', 'fontsettings',
'theme-default' ]);
});
it('must remove default plugins using -', function() {
var deps = PluginDependency.listFromString('ga,-search');
var plugins = listDependencies(deps);
var names = toNames(plugins);
expect(names).toEqual([
'ga',
'highlight', 'lunr', 'sharing', 'fontsettings',
'theme-default' ]);
});
});
-91
View File
@@ -1,91 +0,0 @@
var readInstalled = require('read-installed');
var Immutable = require('immutable');
var path = require('path');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var Plugin = require('../models/plugin');
var PREFIX = require('../constants/pluginPrefix');
/**
* Validate if a package name is a GitBook plugin
*
* @return {Boolean}
*/
function validateId(name) {
return name && name.indexOf(PREFIX) === 0;
}
/**
* List all packages installed inside a folder
*
* @param {String} folder
* @return {OrderedMap<String:Plugin>}
*/
function findInstalled(folder) {
var options = {
dev: false,
log: function() {},
depth: 4
};
var results = Immutable.OrderedMap();
function onPackage(pkg, parent) {
if (!pkg.name) return;
var name = pkg.name;
var version = pkg.version;
var pkgPath = pkg.realPath;
var depth = pkg.depth;
var dependencies = pkg.dependencies;
var pluginName = name.slice(PREFIX.length);
if (!validateId(name)){
if (parent) return;
} else {
results = results.set(pluginName, Plugin({
name: pluginName,
version: version,
path: pkgPath,
depth: depth,
parent: parent
}));
}
Immutable.Map(dependencies).forEach(function(dep) {
onPackage(dep, pluginName);
});
}
// Search for gitbook-plugins in node_modules folder
var node_modules = path.join(folder, 'node_modules');
// List all folders in node_modules
return fs.readdir(node_modules)
.fail(function() {
return Promise([]);
})
.then(function(modules) {
return Promise.serie(modules, function(module) {
// Not a gitbook-plugin
if (!validateId(module)) {
return Promise();
}
// Read gitbook-plugin package details
var module_folder = path.join(node_modules, module);
return Promise.nfcall(readInstalled, module_folder, options)
.then(function(data) {
onPackage(data);
});
});
})
.then(function() {
// Return installed plugins
return results;
});
}
module.exports = findInstalled;
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
loadForBook: require('./loadForBook'),
validateConfig: require('./validateConfig'),
installPlugins: require('./installPlugins'),
listResources: require('./listResources'),
listBlocks: require('./listBlocks'),
listFilters: require('./listFilters')
};
-47
View File
@@ -1,47 +0,0 @@
var npmi = require('npmi');
var Promise = require('../utils/promise');
var resolveVersion = require('./resolveVersion');
/**
Install a plugin for a book
@param {Book}
@param {PluginDependency}
@return {Promise}
*/
function installPlugin(book, plugin) {
var logger = book.getLogger();
var installFolder = book.getRoot();
var name = plugin.getName();
var requirement = plugin.getVersion();
logger.info.ln('');
logger.info.ln('installing plugin "' + name + '"');
// Find a version to install
return resolveVersion(plugin)
.then(function(version) {
if (!version) {
throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"');
}
logger.info.ln('install plugin "' + name +'" (' + requirement + ') from NPM with version', version);
return Promise.nfcall(npmi, {
'name': plugin.getNpmID(),
'version': version,
'path': installFolder,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': installFolder
}
});
})
.then(function() {
logger.info.ok('plugin "' + name + '" installed with success');
});
}
module.exports = installPlugin;
-18
View File
@@ -1,18 +0,0 @@
var Immutable = require('immutable');
/**
List blocks from a list of plugins
@param {OrderedMap<String:Plugin>}
@return {Map<String:TemplateBlock>}
*/
function listBlocks(plugins) {
return plugins
.reverse()
.reduce(function(result, plugin) {
var blocks = plugin.getBlocks();
return result.merge(blocks);
}, Immutable.Map());
}
module.exports = listBlocks;
-17
View File
@@ -1,17 +0,0 @@
var Immutable = require('immutable');
/**
List filters from a list of plugins
@param {OrderedMap<String:Plugin>}
@return {Map<String:Function>}
*/
function listFilters(plugins) {
return plugins
.reverse()
.reduce(function(result, plugin) {
return result.merge(plugin.getFilters());
}, Immutable.Map());
}
module.exports = listFilters;
-45
View File
@@ -1,45 +0,0 @@
var Immutable = require('immutable');
var path = require('path');
var LocationUtils = require('../utils/location');
var PLUGIN_RESOURCES = require('../constants/pluginResources');
/**
List all resources from a list of plugins
@param {OrderedMap<String:Plugin>}
@param {String} type
@return {Map<String:List<{url, path}>}
*/
function listResources(plugins, resources) {
return plugins.reduce(function(result, plugin) {
var npmId = plugin.getNpmID();
var pluginResources = resources.get(plugin.getName());
PLUGIN_RESOURCES.forEach(function(resourceType) {
var assets = pluginResources.get(resourceType);
if (!assets) return;
var list = result.get(resourceType) || Immutable.List();
assets = assets.map(function(assetFile) {
if (LocationUtils.isExternal(assetFile)) {
return {
url: assetFile
};
} else {
return {
path: LocationUtils.normalize(path.join(npmId, assetFile))
};
}
});
list = list.concat(assets);
result = result.set(resourceType, list);
});
return result;
}, Immutable.Map());
}
module.exports = listResources;
-22
View File
@@ -1,22 +0,0 @@
var path = require('path');
var resolve = require('resolve');
var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
/**
* Resolve the root folder containing for node_modules
* since gitbook can be used as a library and dependency can be flattened.
*
* @return {String} folderPath
*/
function locateRootFolder() {
var firstDefaultPlugin = DEFAULT_PLUGINS.first();
var pluginPath = resolve.sync(firstDefaultPlugin.getNpmID() + '/package.json', {
basedir: __dirname
});
var nodeModules = path.resolve(pluginPath, '../../..');
return nodeModules;
}
module.exports = locateRootFolder;
-71
View File
@@ -1,71 +0,0 @@
var Immutable = require('immutable');
var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var mergeDefaults = require('../utils/mergeDefaults');
/**
Validate one plugin for a book and update book's confiration
@param {Book}
@param {Plugin}
@return {Book}
*/
function validatePluginConfig(book, plugin) {
var config = book.getConfig();
var packageInfos = plugin.getPackage();
var configKey = [
'pluginsConfig',
plugin.getName()
].join('.');
var pluginConfig = config.getValue(configKey, {}).toJS();
var schema = (packageInfos.get('gitbook') || Immutable.Map()).toJS();
if (!schema) return book;
// Normalize schema
schema.id = '/' + configKey;
schema.type = 'object';
// Validate and throw if invalid
var v = new jsonschema.Validator();
var result = v.validate(pluginConfig, schema, {
propertyName: configKey
});
// Throw error
if (result.errors.length > 0) {
throw new error.ConfigurationError(new Error(result.errors[0].stack));
}
// Insert default values
var defaults = jsonSchemaDefaults(schema);
pluginConfig = mergeDefaults(pluginConfig, defaults);
// Update configuration
config = config.setValue(configKey, pluginConfig);
// Return new book
return book.set('config', config);
}
/**
Validate a book configuration for plugins and
returns an update configuration with default values.
@param {Book}
@param {OrderedMap<String:Plugin>}
@return {Promise<Book>}
*/
function validateConfig(book, plugins) {
return Promise.reduce(plugins, function(newBook, plugin) {
return validatePluginConfig(newBook, plugin);
}, book);
}
module.exports = validateConfig;
-98
View File
@@ -1,98 +0,0 @@
var path = require('path');
var TemplateEngine = require('../../models/templateEngine');
var renderTemplate = require('../render');
var ConrefsLoader = require('../conrefsLoader');
describe('ConrefsLoader', function() {
var dirName = __dirname + '/';
var fileName = path.join(dirName, 'test.md');
describe('Git', function() {
var engine = TemplateEngine({
loader: new ConrefsLoader(dirName)
});
it('should include content from git', function() {
return renderTemplate(engine, fileName, '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('Hello from git');
});
});
it('should handle deep inclusion (1)', function() {
return renderTemplate(engine, fileName, '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('First Hello. Hello from git');
});
});
it('should handle deep inclusion (2)', function() {
return renderTemplate(engine, fileName, '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('First Hello. Hello from git');
});
});
});
describe('Local', function() {
var engine = TemplateEngine({
loader: new ConrefsLoader(dirName)
});
describe('Relative', function() {
it('should resolve basic relative filepath', function() {
return renderTemplate(engine, fileName, '{% include "include.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('Hello World');
});
});
it('should resolve basic parent filepath', function() {
return renderTemplate(engine, path.join(dirName, 'hello/test.md'), '{% include "../include.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('Hello World');
});
});
});
describe('Absolute', function() {
it('should resolve absolute filepath', function() {
return renderTemplate(engine, fileName, '{% include "/include.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('Hello World');
});
});
it('should resolve absolute filepath when in a directory', function() {
return renderTemplate(engine, path.join(dirName, 'hello/test.md'), '{% include "/include.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('Hello World');
});
});
});
});
describe('transform', function() {
function transform(filePath, source) {
expect(filePath).toBeA('string');
expect(source).toBeA('string');
expect(filePath).toBe(path.resolve(__dirname, 'include.md'));
expect(source).toBe('Hello World');
return 'test-' + source + '-endtest';
}
var engine = TemplateEngine({
loader: new ConrefsLoader(dirName, transform)
});
it('should transform included content', function() {
return renderTemplate(engine, fileName, '{% include "include.md" %}')
.then(function(out) {
expect(out.getContent()).toBe('test-Hello World-endtest');
});
});
});
});
-51
View File
@@ -1,51 +0,0 @@
var TemplateEngine = require('../../models/templateEngine');
var TemplateBlock = require('../../models/templateBlock');
var renderTemplate = require('../render');
var postRender = require('../postRender');
describe('postRender', function() {
var testPost;
var engine = TemplateEngine.create({
blocks: [
TemplateBlock.create('lower', function(blk) {
return blk.body.toLowerCase();
}),
TemplateBlock.create('prefix', function(blk) {
return {
body: '_' + blk.body + '_',
post: function() {
testPost = true;
}
};
})
]
});
it('should correctly replace block', function() {
return renderTemplate(engine, 'README.md', 'Hello {% lower %}Samy{% endlower %}')
.then(function(output) {
expect(output.getContent()).toMatch(/Hello \{\{\-([\S]+)\-\}\}/);
expect(output.getBlocks().size).toBe(1);
return postRender(engine, output);
})
.then(function(result) {
expect(result).toBe('Hello samy');
});
});
it('should correctly replace blocks', function() {
return renderTemplate(engine, 'README.md', 'Hello {% lower %}Samy{% endlower %}{% prefix %}Pesse{% endprefix %}')
.then(function(output) {
expect(output.getContent()).toMatch(/Hello \{\{\-([\S]+)\-\}\}\{\{\-([\S]+)\-\}\}/);
expect(output.getBlocks().size).toBe(2);
return postRender(engine, output);
})
.then(function(result) {
expect(result).toBe('Hello samy_Pesse_');
expect(testPost).toBe(true);
});
});
});
@@ -1,27 +0,0 @@
var Immutable = require('immutable');
var TemplateBlock = require('../../models/templateBlock');
var replaceShortcuts = require('../replaceShortcuts');
describe('replaceShortcuts', function() {
var blocks = Immutable.List([
TemplateBlock.create('math', {
shortcuts: {
start: '$$',
end: '$$',
parsers: ['markdown']
}
})
]);
it('should correctly replace inline matches by block', function() {
var content = replaceShortcuts(blocks, 'test.md', 'Hello $$a = b$$');
expect(content).toBe('Hello {% math %}a = b{% endmath %}');
});
it('should correctly replace block matches', function() {
var content = replaceShortcuts(blocks, 'test.md', 'Hello\n$$\na = b\n$$\n');
expect(content).toBe('Hello\n{% math %}\na = b\n{% endmath %}\n');
});
});
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
render: require('./render'),
renderFile: require('./renderFile'),
postRender: require('./postRender'),
replaceShortcuts: require('./replaceShortcuts'),
ConrefsLoader: require('./conrefsLoader'),
ThemesLoader: require('./themesLoader')
};
-53
View File
@@ -1,53 +0,0 @@
var Promise = require('../utils/promise');
/**
* Replace position markers of blocks by body after processing
* This is done to avoid that markdown/asciidoc processer parse the block content
*
* @param {String} content
* @return {Object} {blocks: Set, content: String}
*/
function replaceBlocks(content, blocks) {
var newContent = content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) {
var replacedWith = match;
var block = blocks.get(key);
if (block) {
replacedWith = replaceBlocks(block.get('body'), blocks);
}
return replacedWith;
});
return newContent;
}
/**
* Post render a template:
* - Execute "post" for blocks
* - Replace block content
*
* @param {TemplateEngine} engine
* @param {TemplateOutput} content
* @return {Promise<String>}
*/
function postRender(engine, output) {
var content = output.getContent();
var blocks = output.getBlocks();
var result = replaceBlocks(content, blocks);
return Promise.forEach(blocks, function(block) {
var post = block.get('post');
if (!post) {
return;
}
return post();
})
.thenResolve(result);
}
module.exports = postRender;
-115
View File
@@ -1,115 +0,0 @@
var Immutable = require('immutable');
var nunjucks = require('nunjucks');
var fs = require('fs');
var path = require('path');
var PathUtils = require('../utils/path');
var ThemesLoader = nunjucks.Loader.extend({
init: function(searchPaths) {
this.searchPaths = Immutable.List(searchPaths)
.map(path.normalize);
},
/**
* Read source of a resolved filepath
* @param {String}
* @return {Object}
*/
getSource: function(fullpath) {
if (!fullpath) return null;
fullpath = this.resolve(null, fullpath);
var templateName = this.getTemplateName(fullpath);
if(!fullpath) {
return null;
}
var src = fs.readFileSync(fullpath, 'utf-8');
src = '{% do %}var template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' +
src +
'\n{% do %}template.self = template.stack.pop();{% enddo %}';
return {
src: src,
path: fullpath,
noCache: true
};
},
/**
* Nunjucks calls "isRelative" to determine when to call "resolve".
* We handle absolute paths ourselves in ".resolve" so we always return true
*/
isRelative: function() {
return true;
},
/**
* Get original search path containing a template
* @param {String} filepath
* @return {String} searchPath
*/
getSearchPath: function(filepath) {
return this.searchPaths
.sortBy(function(s) {
return -s.length;
})
.find(function(basePath) {
return (filepath && filepath.indexOf(basePath) === 0);
});
},
/**
* Get template name from a filepath
* @param {String} filepath
* @return {String} name
*/
getTemplateName: function(filepath) {
var originalSearchPath = this.getSearchPath(filepath);
return originalSearchPath? path.relative(originalSearchPath, filepath) : null;
},
/**
* Resolve a template from a current template
* @param {String|null} from
* @param {String} to
* @return {String|null}
*/
resolve: function(from, to) {
var searchPaths = this.searchPaths;
// Relative template like "./test.html"
if (PathUtils.isPureRelative(to) && from) {
return path.resolve(path.dirname(from), to);
}
// Determine in which search folder we currently are
var originalSearchPath = this.getSearchPath(from);
var originalFilename = this.getTemplateName(from);
// If we are including same file from a different search path
// Slice the search paths to avoid including from previous ones
if (originalFilename == to) {
var currentIndex = searchPaths.indexOf(originalSearchPath);
searchPaths = searchPaths.slice(currentIndex + 1);
}
// Absolute template to resolve in root folder
var resultFolder = searchPaths.find(function(basePath) {
var p = path.resolve(basePath, to);
return (
p.indexOf(basePath) === 0
&& fs.existsSync(p)
);
});
if (!resultFolder) return null;
return path.resolve(resultFolder, to);
}
});
module.exports = ThemesLoader;

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