Compare commits

...

81 Commits

Author SHA1 Message Date
Johan Preynat ad425b9772 Bump version to 3.2.2 2016-10-01 22:35:54 +02:00
Johan Preynat 5238fa352c Rollback to use gitbook-markdown@1.3.2 2016-10-01 22:30:43 +02:00
Johan Preynat 32183b6650 Merge branch 'fix-3.1-version' 2016-10-01 21:49:06 +02:00
Johan Preynat cb3d6a7f01 Correctly handle page parsing errors 2016-10-01 21:47:25 +02:00
Johan Preynat c978f0ab21 Add comment 2016-10-01 21:47:08 +02:00
Samy Pessé 33ff60cee7 Add changes for 3.2.1 2016-09-28 15:56:36 +02:00
Samy Pesse 33d7e022e6 Bump version to 3.2.1 2016-09-28 13:21:24 +02:00
Samy Pesse 09782b964b Update theme-default@1.0.6 2016-09-28 13:10:47 +02:00
Johan Preynat 7938ee8ec6 Use nunjucks@2.5.2 2016-09-28 10:32:14 +02:00
Peter B Smith 2e8e4f63d7 fixing a typo of the word "publishing" (#1536) 2016-09-28 09:49:29 +02:00
jdaviescoates b9fa6c3051 Update examples.md (#1522)
By adding the Loomio and Enspiral handbook examples
2016-09-08 20:38:59 +02:00
Samy Pessé 9c071dade5 Merge pull request #1481 from mcveat/patch-1
Fixed typo
2016-08-04 00:19:11 +02:00
Piotr Adamski 56e44651fa Fixed typo 2016-08-03 22:08:12 +02:00
Samy Pessé 42fac98992 Fix command for getting gitbook version 2016-08-01 09:47:31 +02:00
Samy Pessé 4f5513add0 Create template for GitHub issues 2016-07-25 20:39:31 +02:00
Samy Pesse 90cbfc0596 Add OSX testing on Travis 2016-07-15 11:38:58 +02:00
Samy Pessé 38ab9fa05a Bump version to 3.2.0 2016-07-11 16:48:13 +02:00
Samy Pessé 12e20c5fcc Update theme-default@1.0.5 2016-07-11 16:47:16 +02:00
Samy Pessé 6b103f4532 Fix #1408: fix i18n in website generator and add tests for it 2016-07-11 16:27:22 +02:00
Soreine d4fa5c18a8 editPlugin and getPluginConfig 2016-07-11 15:18:35 +02:00
Soreine 2a116dfdb0 Fix reducedObject when default property is undefined 2016-07-11 15:18:35 +02:00
Samy Pessé 0349f54f3c Export file model 2016-07-07 16:41:05 +02:00
Samy Pessé 695bda312c Bump version to 3.2.0-pre.1 2016-07-05 17:15:48 +02:00
Samy Pessé 144bd4c97a Update gitbook-markdown@2.0.1 2016-07-05 11:07:51 +02:00
Samy Pessé e105983cf8 Add back property "output" to page/template context 2016-07-04 18:08:44 +02:00
Soreine ea3afbed75 Update gitbook-plugin-lunr@1.2.0 2016-07-01 17:52:15 +02:00
Soreine d563abe3df Reduce size of logo to give room to version number 2016-06-30 15:37:31 +02:00
Soreine 70fd4ded29 Update theme-official for doc 2016-06-30 12:15:50 +02:00
Samy Pessé 9ce9388dfc Bump version to 3.2.0-pre.0 2016-06-30 11:09:18 +02:00
Samy Pessé c575ad83ae Update gitbook-markdown@2.0.0 2016-06-30 10:51:46 +02:00
Samy Pessé 936fa1545b Add tests for locationUtils.flatten 2016-06-29 10:32:49 +02:00
Samy Pessé c9c2dde2b6 Add method "isFile" and "isReadme" in SummaryArticle 2016-06-29 10:31:06 +02:00
Samy Pessé d9a1d387c7 Merge pull request #1397 from mpfeil/master
add open and browser args to serve command
2016-06-23 11:40:05 +02:00
Matthias Pfeil bf29d187c8 disable open by default 2016-06-20 17:24:08 +02:00
Matthias Pfeil d86f9b2667 add open and browser args to serve command 2016-06-17 16:00:53 +02:00
Samy Pesse aca4313fc3 Enable long stack support when process.env.CI is true 2016-06-11 12:38:19 +02:00
Samy Pesse 4cb5bb06fb Bump version to 3.1.1 2016-06-10 22:19:09 +02:00
Samy Pesse 24988b75ea Fix extsAsciidoc.js 2016-06-10 22:14:04 +02:00
Samy Pesse 775e858d05 Expose markdown/asciidoc extensions to gitbook module 2016-06-10 22:13:31 +02:00
Samy Pesse 1e3d871c1e Merge branch 'fix/plugins_loading' 2016-06-10 22:08:16 +02:00
Samy Pesse 94a4310c1e Adapt Plugins.loadForBook to use correct order 2016-06-10 22:04:19 +02:00
Samy Pessé e8ee57be83 Start fixing order of plugins in dependencies 2016-06-10 18:34:24 +02:00
Samy Pessé 50a132cb2c Add method "createChildLevel" to SummaryPart 2016-06-10 18:11:22 +02:00
Samy Pessé 59be32a54f Add method .createChildLevel for SummaryArticle 2016-06-10 18:08:56 +02:00
Soreine 4ae7cdb5d1 Mention autocover plugin in docs 2016-06-10 18:03:40 +02:00
Samy Pessé 990a4efeb5 Update gitbook-markdown and gitbook-asciidoc 2016-06-10 17:53:10 +02:00
Samy Pessé 9c927dcf13 Fix #1374 and #1360: apply transform of conrefsLoader using a string, not a buffer 2016-06-10 17:45:30 +02:00
Samy Pessé 7a08b3f109 Fix #1377: fix listing of plugins when using gitbook as a library 2016-06-10 17:15:04 +02:00
Samy Pessé 94d4234cb8 Merge branch 'fix/plugins_order' 2016-06-10 16:44:13 +02:00
Samy Pessé fbc851bc4e Fix listAll to add default plugins after normal ones 2016-06-10 16:43:32 +02:00
Samy Pessé e92b661a2a Adapt sortPlugins to only sort an ordered map of plugins 2016-06-10 16:37:44 +02:00
Soreine b9fe60915f Add tests for insertPart 2016-06-10 15:40:51 +02:00
Samy Pessé 1bf7d092f0 Merge pull request #1363 from todvora/master
Fixed target attribute for external links
2016-06-10 00:28:50 +02:00
Samy Pesse 826d472511 Fix documentation about themes 2016-06-08 20:06:50 +02:00
Samy Pessé a6b1f4bd87 Compare list of plugins sorted to array of names in tests 2016-06-08 15:08:11 +02:00
Samy Pessé 004f91f02c sortPlugins should not be async 2016-06-08 15:02:34 +02:00
Samy Pessé e6c6ec4293 Improve testing for sortPlugins 2016-06-08 14:57:54 +02:00
Soreine a1d3acf065 Fix serve cli using new ConfigModifier.addPlugin 2016-06-08 10:42:27 +02:00
Soreine a732660f59 docs: cover remove extraneous line 2016-06-08 10:35:12 +02:00
Nicolas Gaborit e1bb0638f8 docs: cover, fix formatting 2016-06-08 10:16:54 +02:00
Soreine 788c0c7e7e Adds cover.jpg info 2016-06-08 10:12:42 +02:00
Tomas Dvorak 762fe16a17 fixed target attribute for external links 2016-06-07 18:34:36 +02:00
Samy Pessé 15123805d1 Fix linting error 2016-06-07 16:24:22 +02:00
Samy Pessé 23aecb4f2b Improve output of page to text with frontmatter 2016-06-07 16:23:12 +02:00
Samy Pessé f1d200b81b Rollback frontmatter parsing to js-yaml to be browserify compatible 2016-06-07 14:32:12 +02:00
Samy Pessé b566711ee1 Add method toText to Page 2016-06-07 12:51:35 +02:00
Samy Pessé 22da5aac11 Switch frontmatter parsing to gray-matter 2016-06-07 12:47:16 +02:00
Samy Pessé f7b95102c5 Expose Page model 2016-06-07 12:36:32 +02:00
Samy Pessé ca70c934d6 Split page parsing into parsePageFromString and parsePage
Add tests for parsePageFromString
2016-06-07 12:31:21 +02:00
Samy Pessé 1cc48fa314 Use Immutable instead of lodash for mergeDefaults 2016-06-07 12:06:29 +02:00
Samy Pessé b63b5574d3 Only merge objects in mergeDefaults 2016-06-07 11:40:54 +02:00
Samy Pessé 67106e8698 Improve plugins config modifier 2016-06-07 11:06:53 +02:00
Samy Pessé dc2ab51a88 Update i18n-t and juice 2016-06-07 10:54:34 +02:00
Samy Pessé cedfa186f2 Remove merge-defaults dependency 2016-06-07 10:48:04 +02:00
Samy Pessé 80f0d1bfed Update gitbook-markdown and gitbook-asciidoc to use lodash@4 2016-06-07 10:36:01 +02:00
Samy Pessé 7178ab1f82 Improve Config.toText to return only the required fields 2016-06-07 10:26:28 +02:00
Samy Pesse bf895b707e Improve addPlugin to not append default plugins to the list 2016-06-07 00:56:24 +02:00
Samy Pesse 996170a4ad ConfigModifier.removePlugin should toggle off if default 2016-06-07 00:48:47 +02:00
Samy Pesse aa66a4f928 Expose DEFAULT_PLUGINS as a list of PluginDependency 2016-06-06 22:04:13 +02:00
Samy Pesse 23010fa54a Adapt tests for config modifiers 2016-06-06 21:22:44 +02:00
Samy Pesse 0baebc7979 Change config modifier to work on a config object 2016-06-06 21:20:00 +02:00
100 changed files with 1588 additions and 793 deletions
+19
View File
@@ -0,0 +1,19 @@
Thank you for giving us your feedback! We listed some guidelines to help you:
If possible, open separate issues for each bug report. Avoid grouping bugs that are unrelated.
### What is the current behavior?
If the current behavior is a bug, please provide the steps to reproduce. if possible, provide the GitBook version being used (`gitbook -V`) and the system infromations (OS and version).
### What is the expected behavior?
How do you think, or how would you like it should behave.
#### For GitBook.com
Provide, if possible, your username and the URL of the concerned book.
----
The GitBook Team
+3
View File
@@ -1,5 +1,8 @@
sudo: false sudo: false
language: node_js language: node_js
os:
- linux
- osx
node_js: node_js:
- "stable" - "stable"
- "4.1" - "4.1"
+23
View File
@@ -2,6 +2,29 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## 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
## 3.2.1
- Fix bug on Firefox when navigating to an url containing an hash
- Update nunjucks to fix JS error when page contains a lot of templating blocks
## 3.2.0
- Switch markdown parser from `kramed` to `markup-it`
- Fix support of `|` in tables
- Fix access to `output.name` in templating
- Fix i18n for website and PDF
- Fix minor scrolling issues on website
- Improve options of default search indexer (keywords, disabling per pages)
- Improve pertinence of search results
## 3.1.1
- Fix order of plugins during loading
- Fix error when using math and conrefs
- Fix target attribute for external links
- Fix serve command
## 3.1.0 ## 3.1.0
- Always load themes after plugins - Always load themes after plugins
- README/Introduction doesn't require to be the first entry in the summary - README/Introduction doesn't require to be the first entry in the summary
+2 -2
View File
@@ -6,10 +6,10 @@ module.exports = {
title: 'GitBook Toolchain Documentation', title: 'GitBook Toolchain Documentation',
// Enforce use of GitBook v3 // Enforce use of GitBook v3
gitbook: '>=3.0.0-pre.0', gitbook: '3.1.1',
// Use the "official" theme // Use the "official" theme
plugins: ['theme-official', 'sitemap'], plugins: ['theme-official@2.1.1', '-sharing', '-fontsettings', 'sitemap'],
variables: { variables: {
version: pkg.version version: pkg.version
+2 -8
View File
@@ -20,18 +20,12 @@ GitBook allows you to customize your book using a flexible configuration. These
Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins/README.md) for more details. Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins/README.md) for more details.
| Variable | Description |
| -------- | ----------- |
| `plugins` | List of plugins to load |
| `pluginsConfig` |Configuration for plugins |
### Theme
Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details. Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details.
| Variable | Description | | Variable | Description |
| -------- | ----------- | | -------- | ----------- |
| `theme` | The theme to use for the book | | `plugins` | List of plugins to load |
| `pluginsConfig` |Configuration for plugins |
### Structure ### Structure
+4 -3
View File
@@ -29,12 +29,13 @@ You can replace `/usr/bin` with any directory that is in your $PATH.
### Cover ### Cover
Covers are used for all the ebook formats. It's an important part of an ebook brandline. Covers are used for all the ebook formats. You can either provide one yourself, or generate one using the [autocover plugin](https://plugins.gitbook.com/plugin/autocover).
To provide a cover, place a **`cover.jpg`** file at the root directory of your book. Adding a **`cover_small.jpg`** will specify a smaller version of the cover. The cover should be a **JPEG** file.
A good cover should respect the following guidelines: A good cover should respect the following guidelines:
* Size of 1800x2360 (pixels) * Size of 1800x2360 pixels for `cover.jpg`, 200x262 for `cover_small.jpg`
* No border * No border
* Clearly visible book title * Clearly visible book title
* Any important text should be visible in the small version * Any important text should be visible in the small version
+2
View File
@@ -23,5 +23,7 @@ More than 50,000 books have been published on [GitBook.com](https://www.gitbook.
### Documentation ### Documentation
- [DuckDuckHack Documentation](http://docs.duckduckhack.com) by [DuckDuckGo](https://duckduckgo.com/about) - [DuckDuckHack Documentation](http://docs.duckduckhack.com) by [DuckDuckGo](https://duckduckgo.com/about)
- [Loomio Handbook](http://loomio.coop/) by [Loomio](https://www.loomio.org/)
- [Enspiral Handbook](http://handbook.enspiral.com/) by [Enspiral](http://enspiral.com/)
- This documentation - This documentation
+2 -2
View File
@@ -2,7 +2,7 @@
### Testing your plugin locally ### Testing your plugin locally
Testing your plugin on your book before plushing it is possible using [npm link](https://docs.npmjs.com/cli/link). Testing your plugin on your book before publishing it is possible using [npm link](https://docs.npmjs.com/cli/link).
In the plugin's folder, run: In the plugin's folder, run:
@@ -10,7 +10,7 @@ In the plugin's folder, run:
$ npm link $ npm link
``` ```
The nin your book's folder: Then in your book's folder:
``` ```
$ npm link gitbook-plugin-<plugin's name> $ npm link gitbook-plugin-<plugin's name>
+4
View File
@@ -0,0 +1,4 @@
/* Reduce size of logo to allow long versions like 3.0.0-pre.1 */
.gb-page-header .container .logo h1 {
font-size: 24px ;
}
+16 -1
View File
@@ -17,7 +17,22 @@ A theme is a plugin containing templates and assets. Overriding any individual t
### Extend/Customize theme in a book ### Extend/Customize theme in a book
Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in the installed plugins/themes.
### Extend instead of Forking
When you want to make your theme changes available to multiple books, instead of forking the default theme, you can extend it using the [templating syntax](../templating/README.md):
```html
{% extends template.self %}
{% block body %}
{{ super() }}
... This will be added to the "body" block
{% endblock %}
```
Take a look at the [API](https://github.com/GitbookIO/theme-api) theme for a more complete example.
### Publish a theme ### Publish a theme
+11 -4
View File
@@ -6,14 +6,21 @@ module.exports = {
// Models // Models
Book: require('./models/book'), Book: require('./models/book'),
FS: require('./models/fs'), FS: require('./models/fs'),
File: require('./models/file'),
Summary: require('./models/summary'), Summary: require('./models/summary'),
Glossary: require('./models/glossary'), Glossary: require('./models/glossary'),
Config: require('./models/config'),
Page: require('./models/page'),
PluginDependency: require('./models/pluginDependency'),
// Modifiers // Modifiers
SummaryModifier: Modifiers.Summary, SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config, ConfigModifier: Modifiers.Config,
// Constants // Constants
CONFIG_FILES: require('./constants/configFiles.js'), CONFIG_FILES: require('./constants/configFiles.js'),
IGNORE_FILES: require('./constants/ignoreFiles.js') IGNORE_FILES: require('./constants/ignoreFiles.js'),
DEFAULT_PLUGINS: require('./constants/defaultPlugins'),
EXTENSIONS_MARKDOWN: require('./constants/extsMarkdown'),
EXTENSIONS_ASCIIDOC: require('./constants/extsAsciidoc')
}; };
+20 -1
View File
@@ -1,6 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
var tinylr = require('tiny-lr'); var tinylr = require('tiny-lr');
var open = require('open');
var Parse = require('../parse'); var Parse = require('../parse');
var Output = require('../output'); var Output = require('../output');
@@ -32,9 +33,11 @@ function generateBook(args, kwargs) {
var outputFolder = getOutputFolder(args); var outputFolder = getOutputFolder(args);
var book = getBook(args, kwargs); var book = getBook(args, kwargs);
var Generator = Output.getGenerator(kwargs.format); var Generator = Output.getGenerator(kwargs.format);
var browser = kwargs['browser'];
var hasWatch = kwargs['watch']; var hasWatch = kwargs['watch'];
var hasLiveReloading = kwargs['live']; var hasLiveReloading = kwargs['live'];
var hasOpen = kwargs['open'];
// Stop server if running // Stop server if running
if (server.isRunning()) console.log('Stopping server'); if (server.isRunning()) console.log('Stopping server');
@@ -45,7 +48,9 @@ function generateBook(args, kwargs) {
.then(function(resultBook) { .then(function(resultBook) {
if (hasLiveReloading) { if (hasLiveReloading) {
// Enable livereload plugin // Enable livereload plugin
resultBook = ConfigModifier.addPlugin(resultBook, 'livereload'); var config = resultBook.getConfig();
config = ConfigModifier.addPlugin(config, 'livereload');
resultBook = resultBook.set('config', config);
} }
return Output.generate(Generator, resultBook, { return Output.generate(Generator, resultBook, {
@@ -69,6 +74,10 @@ function generateBook(args, kwargs) {
} }
}); });
} }
if (hasOpen) {
open('http://localhost:'+port, browser);
}
}) })
.then(function() { .then(function() {
if (!hasWatch) { if (!hasWatch) {
@@ -110,6 +119,16 @@ module.exports = {
description: 'Enable live reloading', description: 'Enable live reloading',
defaults: true defaults: true
}, },
{
name: 'open',
description: 'Enable opening book in browser',
defaults: false
},
{
name: 'browser',
description: 'Specify browser for opening book',
defaults: ''
},
options.log, options.log,
options.format options.format
], ],
+4
View File
@@ -18,6 +18,10 @@ module.exports = {
'type': 'string', 'type': 'string',
'title': 'ISBN for published book' 'title': 'ISBN for published book'
}, },
'language': {
'type': 'string',
'title': 'Language of the book'
},
'author': { 'author': {
'type': 'string', 'type': 'string',
'title': 'Name of the author' 'title': 'Name of the author'
+19 -4
View File
@@ -1,9 +1,24 @@
var Immutable = require('immutable'); var Immutable = require('immutable');
var PluginDependency = require('../models/pluginDependency');
var pkg = require('../../package.json');
/**
* Create a PluginDependency from a dependency of gitbook
* @param {String} pluginName
* @return {PluginDependency}
*/
function createFromDependency(pluginName) {
var npmID = PluginDependency.nameToNpmID(pluginName);
var version = pkg.dependencies[npmID];
return PluginDependency.create(pluginName, version);
}
/* /*
List of default plugins for all books, * List of default plugins for all books,
default plugins should be installed in node dependencies of GitBook * default plugins should be installed in node dependencies of GitBook
*/ */
module.exports = Immutable.List([ module.exports = Immutable.List([
'highlight', 'highlight',
'search', 'search',
@@ -11,4 +26,4 @@ module.exports = Immutable.List([
'sharing', 'sharing',
'fontsettings', 'fontsettings',
'theme-default' 'theme-default'
]); ]).map(createFromDependency);
+4
View File
@@ -0,0 +1,4 @@
module.exports = [
'.adoc',
'.asciidoc'
];
+5
View File
@@ -0,0 +1,5 @@
module.exports = [
'.md',
'.markdown',
'.mdown'
];
+6 -6
View File
@@ -3,12 +3,12 @@ var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile'); var encodeFile = require('./encodeFile');
/** /**
Return a JSON representation of a book with a specific file * Return a JSON representation of a book with a specific file
*
@param {Book} output * @param {Book} output
@param {Page} page * @param {Page} page
@return {Object} * @return {Object}
*/ */
function encodeBookWithPage(book, page) { function encodeBookWithPage(book, page) {
var file = page.getFile(); var file = page.getFile();
+5 -5
View File
@@ -1,11 +1,11 @@
var encodeBook = require('./encodeBook'); var encodeBook = require('./encodeBook');
/** /**
Encode an output to JSON * Encode an output to JSON
*
@param {Output} * @param {Output}
@return {Object} * @return {Object}
*/ */
function encodeOutputToJson(output) { function encodeOutputToJson(output) {
var book = output.getBook(); var book = output.getBook();
var generator = output.getGenerator(); var generator = output.getGenerator();
+23
View File
@@ -0,0 +1,23 @@
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;
+1
View File
@@ -2,6 +2,7 @@
module.exports = { module.exports = {
encodeOutput: require('./encodeOutput'), encodeOutput: require('./encodeOutput'),
encodeBookWithPage: require('./encodeBookWithPage'), encodeBookWithPage: require('./encodeBookWithPage'),
encodeOutputWithPage: require('./encodeOutputWithPage'),
encodeBook: require('./encodeBook'), encodeBook: require('./encodeBook'),
encodeFile: require('./encodeFile'), encodeFile: require('./encodeFile'),
encodePage: require('./encodePage'), encodePage: require('./encodePage'),
+30
View File
@@ -55,6 +55,36 @@ describe('Config', function() {
expect(world).toBe(2); expect(world).toBe(2);
}); });
}); });
describe('toReducedVersion', function() {
it('must only return diffs for simple values', function() {
var _config = Config.createWithValues({
gitbook: '3.0.0'
});
var reducedVersion = _config.toReducedVersion();
expect(reducedVersion.toJS()).toEqual({
gitbook: '3.0.0'
});
});
it('must only return diffs for deep values', function() {
var _config = Config.createWithValues({
structure: {
readme: 'intro.md'
}
});
var reducedVersion = _config.toReducedVersion();
expect(reducedVersion.toJS()).toEqual({
structure: {
readme: 'intro.md'
}
});
});
});
}); });
+28
View File
@@ -0,0 +1,28 @@
var Immutable = require('immutable');
var Page = require('../page');
describe('Page', function() {
describe('toText', function() {
it('must not prepend frontmatter if no attributes', function() {
var page = Page().merge({
content: 'Hello World'
});
expect(page.toText()).toBe('Hello World');
});
it('must prepend frontmatter if attributes', function() {
var page = Page().merge({
content: 'Hello World',
attributes: Immutable.fromJS({
hello: 'world'
})
});
expect(page.toText()).toBe('---\nhello: world\n---\n\nHello World');
});
});
});
+53
View File
@@ -0,0 +1,53 @@
var SummaryArticle = require('../summaryArticle');
var File = require('../file');
describe('SummaryArticle', function() {
describe('createChildLevel', function() {
it('must create the right level', function() {
var article = SummaryArticle.create({}, '1.1');
expect(article.createChildLevel()).toBe('1.1.1');
});
it('must create the right level when has articles', function() {
var article = SummaryArticle.create({
articles: [
{
title: 'Test'
}
]
}, '1.1');
expect(article.createChildLevel()).toBe('1.1.2');
});
});
describe('isFile', function() {
it('must return true when exactly the file', function() {
var article = SummaryArticle.create({
ref: 'hello.md'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(true);
});
it('must return true when path is not normalized', function() {
var article = SummaryArticle.create({
ref: '/hello.md'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(true);
});
it('must return false when has anchor', function() {
var article = SummaryArticle.create({
ref: 'hello.md#world'
}, '1.1');
var file = File.createWithFilepath('hello.md');
expect(article.isFile(file)).toBe(false);
});
});
});
+23
View File
@@ -0,0 +1,23 @@
var SummaryPart = require('../summaryPart');
describe('SummaryPart', function() {
describe('createChildLevel', function() {
it('must create the right level', function() {
var article = SummaryPart.create({}, '1');
expect(article.createChildLevel()).toBe('1.1');
});
it('must create the right level when has articles', function() {
var article = SummaryPart.create({
articles: [
{
title: 'Test'
}
]
}, '1');
expect(article.createChildLevel()).toBe('1.2');
});
});
});
+11 -1
View File
@@ -4,6 +4,7 @@ var Immutable = require('immutable');
var File = require('./file'); var File = require('./file');
var PluginDependency = require('./pluginDependency'); var PluginDependency = require('./pluginDependency');
var configDefault = require('../constants/configDefault'); var configDefault = require('../constants/configDefault');
var reducedObject = require('../utils/reducedObject');
var Config = Immutable.Record({ var Config = Immutable.Record({
file: File(), file: File(),
@@ -18,12 +19,21 @@ Config.prototype.getValues = function() {
return this.get('values'); return this.get('values');
}; };
/**
* Return minimum version of configuration,
* Basically it returns the current config minus the default one
* @return {Map}
*/
Config.prototype.toReducedVersion = function() {
return reducedObject(configDefault, this.getValues());
};
/** /**
* Render config as text * Render config as text
* @return {Promise<String>} * @return {Promise<String>}
*/ */
Config.prototype.toText = function() { Config.prototype.toText = function() {
return JSON.stringify(this.getValues().toJS(), null, 4); return JSON.stringify(this.toReducedVersion().toJS(), null, 4);
}; };
/** /**
+21 -6
View File
@@ -1,4 +1,5 @@
var Immutable = require('immutable'); var Immutable = require('immutable');
var yaml = require('js-yaml');
var File = require('./file'); var File = require('./file');
@@ -32,19 +33,33 @@ Page.prototype.getDir = function() {
}; };
/** /**
Return path of the page * Return page as text
* @return {String}
*/
Page.prototype.toText = function() {
var attrs = this.getAttributes();
var content = this.getContent();
@return {String} 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() { Page.prototype.getPath = function() {
return this.getFile().getPath(); return this.getFile().getPath();
}; };
/** /**
Create a page for a file * Create a page for a file
* @param {File} file
@param {File} file * @return {Page}
@return {Page}
*/ */
Page.createForFile = function(file) { Page.createForFile = function(file) {
return new Page({ return new Page({
+50 -39
View File
@@ -1,9 +1,10 @@
var Immutable = require('immutable'); var Immutable = require('immutable');
var TemplateBlock = require('./templateBlock'); var TemplateBlock = require('./templateBlock');
var DEFAULT_VERSION = '*';
var PluginDependency = require('./pluginDependency'); var PluginDependency = require('./pluginDependency');
var THEME_PREFIX = require('../constants/themePrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({ var Plugin = Immutable.Record({
name: String(), name: String(),
@@ -17,6 +18,9 @@ var Plugin = Immutable.Record({
// Depth of this plugin in the dependency tree // Depth of this plugin in the dependency tree
depth: Number(0), depth: Number(0),
// Parent depending on this plugin
parent: String(),
// Content of the "package.json" // Content of the "package.json"
package: Immutable.Map(), package: Immutable.Map(),
@@ -48,38 +52,48 @@ Plugin.prototype.getDepth = function() {
return this.get('depth'); return this.get('depth');
}; };
/** Plugin.prototype.getParent = function() {
Return the ID on NPM for this plugin return this.get('parent');
};
@return {String} /**
*/ * Return the ID on NPM for this plugin
* @return {String}
*/
Plugin.prototype.getNpmID = function() { Plugin.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName()); return PluginDependency.nameToNpmID(this.getName());
}; };
/** /**
Check if a plugin is loaded * Check if a plugin is loaded
* @return {Boolean}
@return {Boolean} */
*/
Plugin.prototype.isLoaded = function() { Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0); return Boolean(this.getPackage().size > 0);
}; };
/** /**
Return map of hooks * Check if a plugin is a theme given its name
@return {Map<String:Function>} * @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() { Plugin.prototype.getHooks = function() {
return this.getContent().get('hooks') || Immutable.Map(); return this.getContent().get('hooks') || Immutable.Map();
}; };
/** /**
Return infos about resources for a specific type * Return infos about resources for a specific type
* @param {String} type
@param {String} type * @return {Map<String:Mixed>}
@return {Map<String:Mixed>} */
*/
Plugin.prototype.getResources = function(type) { Plugin.prototype.getResources = function(type) {
if (type != 'website' && type != 'ebook') { if (type != 'website' && type != 'ebook') {
throw new Error('Invalid assets type ' + type); throw new Error('Invalid assets type ' + type);
@@ -92,17 +106,17 @@ Plugin.prototype.getResources = function(type) {
}; };
/** /**
Return map of filters * Return map of filters
@return {Map<String:Function>} * @return {Map<String:Function>}
*/ */
Plugin.prototype.getFilters = function() { Plugin.prototype.getFilters = function() {
return this.getContent().get('filters'); return this.getContent().get('filters');
}; };
/** /**
Return map of blocks * Return map of blocks
@return {Map<String:TemplateBlock>} * @return {Map<String:TemplateBlock>}
*/ */
Plugin.prototype.getBlocks = function() { Plugin.prototype.getBlocks = function() {
var blocks = this.getContent().get('blocks'); var blocks = this.getContent().get('blocks');
blocks = blocks || Immutable.Map(); blocks = blocks || Immutable.Map();
@@ -114,21 +128,19 @@ Plugin.prototype.getBlocks = function() {
}; };
/** /**
Return a specific hook * Return a specific hook
* @param {String} name
@param {String} name * @return {Function|undefined}
@return {Function|undefined} */
*/
Plugin.prototype.getHook = function(name) { Plugin.prototype.getHook = function(name) {
return this.getHooks().get(name); return this.getHooks().get(name);
}; };
/** /**
Create a plugin from a string * Create a plugin from a string
* @param {String}
@param {String} * @return {Plugin}
@return {Plugin} */
*/
Plugin.createFromString = function(s) { Plugin.createFromString = function(s) {
var parts = s.split('@'); var parts = s.split('@');
var name = parts[0]; var name = parts[0];
@@ -141,11 +153,10 @@ Plugin.createFromString = function(s) {
}; };
/** /**
Create a plugin from a dependency * Create a plugin from a dependency
* @param {PluginDependency}
@param {PluginDependency} * @return {Plugin}
@return {Plugin} */
*/
Plugin.createFromDep = function(dep) { Plugin.createFromDep = function(dep) {
return new Plugin({ return new Plugin({
name: dep.getName(), name: dep.getName(),
+76 -37
View File
@@ -30,20 +30,21 @@ SummaryArticle.prototype.getArticles = function() {
}; };
/** /**
Return how deep the article is. * Return how deep the article is.
The README has a depth of 1 * The README has a depth of 1
*
@return {Number} * @return {Number}
*/ */
SummaryArticle.prototype.getDepth = function() { SummaryArticle.prototype.getDepth = function() {
return (this.getLevel().split('.').length - 1); return (this.getLevel().split('.').length - 1);
}; };
/** /**
Get path (without anchor) to the pointing file * Get path (without anchor) to the pointing file.
* It also normalizes the file path.
@return {String} *
*/ * @return {String}
*/
SummaryArticle.prototype.getPath = function() { SummaryArticle.prototype.getPath = function() {
if (this.isExternal()) { if (this.isExternal()) {
return undefined; return undefined;
@@ -58,24 +59,24 @@ SummaryArticle.prototype.getPath = function() {
var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref); var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref);
// Normalize path to remove ('./', etc) // Normalize path to remove ('./', '/...', etc)
return location.normalize(pathname); return location.flatten(pathname);
}; };
/** /**
Return url if article is external * Return url if article is external
*
@return {String} * @return {String}
*/ */
SummaryArticle.prototype.getUrl = function() { SummaryArticle.prototype.getUrl = function() {
return this.isExternal()? this.getRef() : undefined; return this.isExternal()? this.getRef() : undefined;
}; };
/** /**
Get anchor for this article (or undefined) * Get anchor for this article (or undefined)
*
@return {String} * @return {String}
*/ */
SummaryArticle.prototype.getAnchor = function() { SummaryArticle.prototype.getAnchor = function() {
var ref = this.getRef(); var ref = this.getRef();
var parts = ref.split('#'); var parts = ref.split('#');
@@ -85,29 +86,68 @@ SummaryArticle.prototype.getAnchor = function() {
}; };
/** /**
Is article pointing to a page of an absolute url * Create a new level for a new child article
*
* @return {String}
*/
SummaryArticle.prototype.createChildLevel = function() {
var level = this.getLevel();
var subArticles = this.getArticles();
var childLevel = level + '.' + (subArticles.size + 1);
@return {Boolean} return childLevel;
*/ };
/**
* Is article pointing to a page of an absolute url
*
* @return {Boolean}
*/
SummaryArticle.prototype.isPage = function() { SummaryArticle.prototype.isPage = function() {
return !this.isExternal() && this.getRef(); return !this.isExternal() && this.getRef();
}; };
/** /**
Is article pointing to aan absolute url * Check if this article is a file (exatcly)
*
* @param {File} file
* @return {Boolean}
*/
SummaryArticle.prototype.isFile = function(file) {
return (
file.getPath() === this.getPath()
&& this.getAnchor() === undefined
);
};
@return {Boolean} /**
*/ * Check if this article is the introduction of the book
*
* @param {Book|Readme} book
* @return {Boolean}
*/
SummaryArticle.prototype.isReadme = function(book) {
var readme = book.getFile? book : book.getReadme();
var file = readme.getFile();
return this.isFile(file);
};
/**
* Is article pointing to aan absolute url
*
* @return {Boolean}
*/
SummaryArticle.prototype.isExternal = function() { SummaryArticle.prototype.isExternal = function() {
return location.isExternal(this.getRef()); return location.isExternal(this.getRef());
}; };
/** /**
Create a SummaryArticle * Create a SummaryArticle
*
@param {Object} def * @param {Object} def
@return {SummaryArticle} * @return {SummaryArticle}
*/ */
SummaryArticle.create = function(def, level) { SummaryArticle.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) { var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) { if (article instanceof SummaryArticle) {
@@ -124,14 +164,13 @@ SummaryArticle.create = function(def, level) {
}); });
}; };
/** /**
Find an article from a base one * Find an article from a base one
*
@param {Article|Part} base * @param {Article|Part} base
@param {Function(article)} iter * @param {Function(article)} iter
@return {Article} * @return {Article}
*/ */
SummaryArticle.findArticle = function(base, iter) { SummaryArticle.findArticle = function(base, iter) {
var articles = base.getArticles(); var articles = base.getArticles();
+17 -4
View File
@@ -25,11 +25,24 @@ SummaryPart.prototype.getArticles = function() {
}; };
/** /**
Create a SummaryPart * Create a new level for a new child article
*
* @return {String}
*/
SummaryPart.prototype.createChildLevel = function() {
var level = this.getLevel();
var subArticles = this.getArticles();
var childLevel = level + '.' + (subArticles.size + 1);
@param {Object} def return childLevel;
@return {SummaryPart} };
*/
/**
* Create a SummaryPart
*
* @param {Object} def
* @return {SummaryPart}
*/
SummaryPart.create = function(def, level) { SummaryPart.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) { var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) { if (article instanceof SummaryArticle) {
+1 -4
View File
@@ -1,12 +1,10 @@
var addPlugin = require('../addPlugin'); var addPlugin = require('../addPlugin');
var Config = require('../../../models/config'); var Config = require('../../../models/config');
var Book = require('../../../models/book');
describe('addPlugin', function() { describe('addPlugin', function() {
var config = Config.createWithValues({ var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled'] plugins: ['hello', 'world', '-disabled']
}); });
var book = Book().setConfig(config);
it('should have correct state of dependencies', function() { it('should have correct state of dependencies', function() {
var disabledDep = config.getPluginDependency('disabled'); var disabledDep = config.getPluginDependency('disabled');
@@ -17,8 +15,7 @@ describe('addPlugin', function() {
}); });
it('should add the plugin to the list', function() { it('should add the plugin to the list', function() {
var newBook = addPlugin(book, 'test'); var newConfig = addPlugin(config, 'test');
var newConfig = newBook.getConfig();
var testDep = newConfig.getPluginDependency('test'); var testDep = newConfig.getPluginDependency('test');
expect(testDep).toBeDefined(); expect(testDep).toBeDefined();
@@ -0,0 +1,33 @@
var removePlugin = require('../removePlugin');
var Config = require('../../../models/config');
describe('removePlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
it('should remove the plugin from the list', function() {
var newConfig = removePlugin(config, 'hello');
var testDep = newConfig.getPluginDependency('hello');
expect(testDep).toNotBeDefined();
});
it('should remove the disabled plugin from the list', function() {
var newConfig = removePlugin(config, 'disabled');
var testDep = newConfig.getPluginDependency('disabled');
expect(testDep).toNotBeDefined();
});
it('should disable default plugin', function() {
var newConfig = removePlugin(config, 'search');
var disabledDep = newConfig.getPluginDependency('search');
expect(disabledDep).toBeDefined();
expect(disabledDep.getVersion()).toEqual('*');
expect(disabledDep.isEnabled()).toBeFalsy();
});
});
@@ -1,16 +1,13 @@
var togglePlugin = require('../togglePlugin'); var togglePlugin = require('../togglePlugin');
var Config = require('../../../models/config'); var Config = require('../../../models/config');
var Book = require('../../../models/book');
describe('togglePlugin', function() { describe('togglePlugin', function() {
var config = Config.createWithValues({ var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled'] plugins: ['hello', 'world', '-disabled']
}); });
var book = Book().setConfig(config);
it('should enable plugin', function() { it('should enable plugin', function() {
var newBook = togglePlugin(book, 'disabled'); var newConfig = togglePlugin(config, 'disabled');
var newConfig = newBook.getConfig();
var testDep = newConfig.getPluginDependency('disabled'); var testDep = newConfig.getPluginDependency('disabled');
expect(testDep).toBeDefined(); expect(testDep).toBeDefined();
@@ -19,8 +16,7 @@ describe('togglePlugin', function() {
}); });
it('should disable plugin', function() { it('should disable plugin', function() {
var newBook = togglePlugin(book, 'world'); var newConfig = togglePlugin(config, 'world');
var newConfig = newBook.getConfig();
var testDep = newConfig.getPluginDependency('world'); var testDep = newConfig.getPluginDependency('world');
expect(testDep).toBeDefined(); expect(testDep).toBeDefined();
+13 -10
View File
@@ -1,22 +1,25 @@
var PluginDependency = require('../../models/pluginDependency'); var PluginDependency = require('../../models/pluginDependency');
var togglePlugin = require('./togglePlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/** /**
* Add a plugin to a book's configuration * Add a plugin to a book's configuration
* @param {Book} book * @param {Config} config
* @param {String} plugin * @param {String} pluginName
* @param {String} version (optional) * @param {String} version (optional)
* @return {Book} * @return {Config}
*/ */
function addPlugin(book, plugin, version) { function addPlugin(config, pluginName, version) {
var config = book.getConfig(); // For default plugin, we only ensure it is enabled
var deps = config.getPluginDependencies(); if (isDefaultPlugin(pluginName, version)) {
return togglePlugin(config, pluginName, true);
}
var dep = PluginDependency.create(plugin, version); var deps = config.getPluginDependencies();
var dep = PluginDependency.create(pluginName, version);
deps = deps.push(dep); deps = deps.push(dep);
config = config.setPluginDependencies(deps); return config.setPluginDependencies(deps);
return book.setConfig(config);
} }
module.exports = addPlugin; module.exports = addPlugin;
+5 -8
View File
@@ -1,16 +1,13 @@
/** /**
* Edit ocnfiguration of a plugin * Edit configuration of a plugin
* @param {Book} book * @param {Config} config
* @param {String} plugin * @param {String} plugin
* @param {Object} pluginConfig * @param {Object} pluginConfig
* @return {Book} * @return {Config}
*/ */
function editPlugin(book, pluginName, pluginConfig) { function editPlugin(config, pluginName, pluginConfig) {
var config = book.getConfig(); return config.setValue('pluginsConfig.'+pluginName, pluginConfig);
config = config.set('pluginsConfig.'+pluginName, pluginConfig);
return book.setConfig(config);
} }
module.exports = editPlugin; module.exports = editPlugin;
+20
View File
@@ -0,0 +1,20 @@
/**
* Return the configuration for a plugin
* @param {Config} config
* @param {String} pluginName
* @return {Object}
*/
function getPluginConfig(config, pluginName) {
var pluginsConfig = config.getValues().get('pluginsConfig');
if (pluginsConfig === undefined) {
return {};
}
var pluginConf = pluginsConfig.get(pluginName);
if (pluginConf === undefined) {
return {};
} else {
return pluginConf.toJS();
}
}
module.exports = getPluginConfig;
+15
View File
@@ -0,0 +1,15 @@
/**
* Test if a plugin is listed
* @param { {List<PluginDependency}} deps
* @param {String} plugin
* @param {String} version
* @return {Boolean}
*/
function hasPlugin(deps, pluginName, version) {
return !!deps.find(function(dep) {
return dep.getName() === pluginName && (!version || dep.getVersion() === version);
});
}
module.exports = hasPlugin;
+7 -4
View File
@@ -1,7 +1,10 @@
module.exports = { module.exports = {
addPlugin: require('./addPlugin'), addPlugin: require('./addPlugin'),
removePlugin: require('./removePlugin'), removePlugin: require('./removePlugin'),
togglePlugin: require('./togglePlugin'), togglePlugin: require('./togglePlugin'),
editPlugin: require('./editPlugin') editPlugin: require('./editPlugin'),
hasPlugin: require('./hasPlugin'),
getPluginConfig: require('./getPluginConfig'),
isDefaultPlugin: require('./isDefaultPlugin')
}; };
+14
View File
@@ -0,0 +1,14 @@
var DEFAULT_PLUGINS = require('../../constants/defaultPlugins');
var hasPlugin = require('./hasPlugin');
/**
* Test if a plugin is a default one
* @param {String} plugin
* @param {String} version
* @return {Boolean}
*/
function isDefaultPlugin(pluginName, version) {
return hasPlugin(DEFAULT_PLUGINS, pluginName, version);
}
module.exports = isDefaultPlugin;
+12 -8
View File
@@ -1,21 +1,25 @@
var togglePlugin = require('./togglePlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/** /**
* Remove a plugin from a book's configuration * Remove a plugin from a book's configuration
* @param {Book} book * @param {Config} config
* @param {String} plugin * @param {String} plugin
* @return {Book} * @return {Config}
*/ */
function removePlugin(book, pluginName) { function removePlugin(config, pluginName) {
var config = book.getConfig();
var deps = config.getPluginDependencies(); var deps = config.getPluginDependencies();
// For default plugin, we have to disable it instead of removing from the list
if (isDefaultPlugin(pluginName)) {
return togglePlugin(config, pluginName, false);
}
deps = deps.filter(function(dep) { // Remove the dependency from the list
deps = deps.filterNot(function(dep) {
return dep.getName() === pluginName; return dep.getName() === pluginName;
}); });
config = config.setPluginDependencies(deps); return config.setPluginDependencies(deps);
return book.setConfig(config);
} }
module.exports = removePlugin; module.exports = removePlugin;
+14 -8
View File
@@ -1,25 +1,31 @@
var PluginDependency = require('../../models/pluginDependency');
var hasPlugin = require('./hasPlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/** /**
* Enable/disable a plugin dependency * Enable/disable a plugin dependency
* @param {Book} book * @param {Config} config
* @param {String} plugin * @param {String} pluginName
* @param {Boolean} state (optional) * @param {Boolean} state (optional)
* @return {Book} * @return {Config}
*/ */
function togglePlugin(book, plugin, state) { function togglePlugin(config, pluginName, state) {
var config = book.getConfig();
var deps = config.getPluginDependencies(); var deps = config.getPluginDependencies();
// For default plugin, we should ensure it's listed first
if (isDefaultPlugin(pluginName) && !hasPlugin(deps, pluginName)) {
deps = deps.push(PluginDependency.create(pluginName));
}
deps = deps.map(function(dep) { deps = deps.map(function(dep) {
if (dep.getName() === plugin) { if (dep.getName() === pluginName) {
return dep.toggle(state); return dep.toggle(state);
} }
return dep; return dep;
}); });
config = config.setPluginDependencies(deps); return config.setPluginDependencies(deps);
return book.setConfig(config);
} }
module.exports = togglePlugin; module.exports = togglePlugin;
@@ -0,0 +1,60 @@
var Summary = require('../../../models/summary');
var SummaryPart = require('../../../models/summaryPart');
var File = require('../../../models/file');
describe('insertPart', function() {
var insertPart = require('../insertPart');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: []
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should insert an part at a given level', function() {
var part = SummaryPart.create({
title: 'Inserted'
}, 'meaningless.level');
var newSummary = insertPart(summary, part, 1);
var inserted = newSummary.getPart(1);
expect(inserted.getTitle()).toBe('Inserted');
expect(newSummary.getParts().count()).toBe(3);
var otherArticle = newSummary.getByLevel('3.1');
expect(otherArticle.getTitle()).toBe('2.1');
expect(otherArticle.getLevel()).toBe('3.1');
});
it('should insert an part in last position', function() {
var part = SummaryPart.create({
title: 'Inserted'
}, 'meaningless.level');
var newSummary = insertPart(summary, part, 2);
var inserted = newSummary.getPart(2);
expect(inserted.getTitle()).toBe('Inserted');
expect(newSummary.getParts().count()).toBe(3);
});
});
+38
View File
@@ -0,0 +1,38 @@
var Immutable = require('immutable');
var Output = require('../../models/output');
var Book = require('../../models/book');
var parseBook = require('../../parse/parseBook');
var createMockFS = require('../../fs/mock');
var preparePlugins = require('../preparePlugins');
/**
* Create an output using a generator
*
* FOR TESTING PURPOSE ONLY
*
* @param {Generator} generator
* @param {Map<String:String|Map>} files
* @return {Promise<Output>}
*/
function createMockOutput(generator, files, options) {
var fs = createMockFS(files);
var book = Book.createForFS(fs);
var state = generator.State? generator.State({}) : Immutable.Map();
book = book.setLogLevel('disabled');
options = generator.Options(options);
return parseBook(book)
.then(function(resultBook) {
return new Output({
book: resultBook,
options: options,
state: state,
generator: generator.name
});
})
.then(preparePlugins);
}
module.exports = createMockOutput;
+1 -1
View File
@@ -1,4 +1,4 @@
var generateMock = require('../generateMock'); var generateMock = require('./generateMock');
var EbookGenerator = require('../ebook'); var EbookGenerator = require('../ebook');
describe('EbookGenerator', function() { describe('EbookGenerator', function() {
@@ -1,21 +1,20 @@
var tmp = require('tmp'); var tmp = require('tmp');
var Book = require('../models/book'); var Book = require('../../models/book');
var createMockFS = require('../fs/mock'); var createMockFS = require('../../fs/mock');
var parseBook = require('../parse/parseBook'); var parseBook = require('../../parse/parseBook');
var generateBook = require('./generateBook'); var generateBook = require('../generateBook');
/** /**
Generate a book using JSON generator * Generate a book using a generator
And returns the path to the output dir. * And returns the path to the output dir.
*
FOR TESTING PURPOSE ONLY * FOR TESTING PURPOSE ONLY
*
@param {Generator} * @param {Generator}
@param {Map<String:String|Map>} files * @param {Map<String:String|Map>} files
@return {Promise<String>} * @return {Promise<String>}
*/ */
function generateMock(Generator, files) { function generateMock(Generator, files) {
var fs = createMockFS(files); var fs = createMockFS(files);
var book = Book.createForFS(fs); var book = Book.createForFS(fs);
+1 -1
View File
@@ -1,4 +1,4 @@
var generateMock = require('../generateMock'); var generateMock = require('./generateMock');
var JSONGenerator = require('../json'); var JSONGenerator = require('../json');
describe('JSONGenerator', function() { describe('JSONGenerator', function() {
+1 -1
View File
@@ -1,5 +1,5 @@
var fs = require('fs'); var fs = require('fs');
var generateMock = require('../generateMock'); var generateMock = require('./generateMock');
var WebsiteGenerator = require('../website'); var WebsiteGenerator = require('../website');
describe('WebsiteGenerator', function() { describe('WebsiteGenerator', function() {
+26 -28
View File
@@ -13,13 +13,12 @@ var generateAssets = require('./generateAssets');
var generatePages = require('./generatePages'); var generatePages = require('./generatePages');
/** /**
Process an output to generate the book * Process an output to generate the book
*
@param {Generator} generator * @param {Generator} generator
@param {Output} output * @param {Output} output
* @return {Promise<Output>}
@return {Promise<Output>} */
*/
function processOutput(generator, startOutput) { function processOutput(generator, startOutput) {
return Promise(startOutput) return Promise(startOutput)
.then(preparePlugins) .then(preparePlugins)
@@ -133,27 +132,26 @@ function processOutput(generator, startOutput) {
} }
/** /**
Generate a book using a generator. * Generate a book using a generator.
*
The overall process is: * The overall process is:
1. List and load plugins for this book * 1. List and load plugins for this book
2. Call hook "config" * 2. Call hook "config"
3. Call hook "init" * 3. Call hook "init"
4. Initialize generator * 4. Initialize generator
5. List all assets and pages * 5. List all assets and pages
6. Copy all assets to output * 6. Copy all assets to output
7. Generate all pages * 7. Generate all pages
8. Call hook "finish:before" * 8. Call hook "finish:before"
9. Finish generation * 9. Finish generation
10. Call hook "finish" * 10. Call hook "finish"
*
*
@param {Generator} generator * @param {Generator} generator
@param {Book} book * @param {Book} book
@param {Object} options * @param {Object} options
* @return {Promise<Output>}
@return {Promise<Output>} */
*/
function generateBook(generator, book, options) { function generateBook(generator, book, options) {
options = generator.Options(options); options = generator.Options(options);
var state = generator.State? generator.State({}) : Immutable.Map(); var state = generator.State? generator.State({}) : Immutable.Map();
+7 -7
View File
@@ -10,12 +10,12 @@ var createTemplateEngine = require('./createTemplateEngine');
var callPageHook = require('./callPageHook'); var callPageHook = require('./callPageHook');
/** /**
Prepare and generate HTML for a page * Prepare and generate HTML for a page
*
@param {Output} output * @param {Output} output
@param {Page} page * @param {Page} page
@return {Promise<Page>} * @return {Promise<Page>}
*/ */
function generatePage(output, page) { function generatePage(output, page) {
var book = output.getBook(); var book = output.getBook();
var engine = createTemplateEngine(output); var engine = createTemplateEngine(output);
@@ -27,7 +27,7 @@ function generatePage(output, page) {
var file = resultPage.getFile(); var file = resultPage.getFile();
var filePath = file.getPath(); var filePath = file.getPath();
var parser = file.getParser(); var parser = file.getParser();
var context = JSONUtils.encodeBookWithPage(book, resultPage); var context = JSONUtils.encodeOutputWithPage(output, resultPage);
if (!parser) { if (!parser) {
return Promise.reject(error.FileNotParsableError({ return Promise.reject(error.FileNotParsableError({
+5 -5
View File
@@ -9,11 +9,11 @@ var fileToOutput = require('./helper/fileToOutput');
var CODEBLOCK = 'code'; var CODEBLOCK = 'code';
/** /**
Return default modifier to prepare a page for * Return default modifier to prepare a page for
rendering. * rendering.
*
@return {Array<Modifier>} * @return {Array<Modifier>}
*/ */
function getModifiers(output, page) { function getModifiers(output, page) {
var book = output.getBook(); var book = output.getBook();
var plugins = output.getPlugins(); var plugins = output.getPlugins();
+6 -6
View File
@@ -6,12 +6,12 @@ var LocationUtils = require('../../utils/location');
var OUTPUT_EXTENSION = '.html'; var OUTPUT_EXTENSION = '.html';
/** /**
Convert a filePath (absolute) to a filename for output * Convert a filePath (absolute) to a filename for output
*
@param {Output} output * @param {Output} output
@param {String} filePath * @param {String} filePath
@return {String} * @return {String}
*/ */
function fileToOutput(output, filePath) { function fileToOutput(output, filePath) {
var book = output.getBook(); var book = output.getBook();
var readme = book.getReadme(); var readme = book.getReadme();
+6 -6
View File
@@ -3,12 +3,12 @@ var LocationUtils = require('../../utils/location');
var fileToURL = require('./fileToURL'); var fileToURL = require('./fileToURL');
/** /**
Resolve an absolute path (extracted from a link) * Resolve an absolute path (extracted from a link)
*
@param {Output} output * @param {Output} output
@param {String} filePath * @param {String} filePath
@return {String} * @return {String}
*/ */
function resolveFileToURL(output, filePath) { function resolveFileToURL(output, filePath) {
// Convert /test.png -> test.png // Convert /test.png -> test.png
filePath = LocationUtils.toAbsolute(filePath, '', ''); filePath = LocationUtils.toAbsolute(filePath, '', '');
+5 -5
View File
@@ -7,11 +7,11 @@ var getModifiers = require('../getModifiers');
var JSON_VERSION = '3'; var JSON_VERSION = '3';
/** /**
Write a page as a json file * Write a page as a json file
*
@param {Output} output * @param {Output} output
@param {Page} page * @param {Page} page
*/ */
function onPage(output, page) { function onPage(output, page) {
var file = page.getFile(); var file = page.getFile();
var readme = output.getBook().getReadme().getFile(); var readme = output.getBook().getReadme().getFile();
+14 -2
View File
@@ -87,6 +87,18 @@ describe('resolveLinks', function() {
}); });
}); });
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');
});
});
});
}); });
+9 -10
View File
@@ -59,19 +59,18 @@ function replaceText($, el, search, replace, text_only ) {
} }
/** /**
Annotate text using a list of GlossaryEntry * Annotate text using a list of GlossaryEntry
*
@param {List<GlossaryEntry>} * @param {List<GlossaryEntry>}
@param {String} glossaryFilePath * @param {String} glossaryFilePath
@param {HTMLDom} $ * @param {HTMLDom} $
*/ */
function annotateText(entries, glossaryFilePath, $) { function annotateText(entries, glossaryFilePath, $) {
entries.forEach(function(entry) { entries.forEach(function(entry) {
var entryId = entry.getID(); var entryId = entry.getID();
var name = entry.getName(); var name = entry.getName();
var description = entry.getDescription(); var description = entry.getDescription();
var searchRegex = new RegExp( '\\b(' + pregQuote(name.toLowerCase()) + ')\\b' , 'gi' );
var searchRegex = new RegExp( '\\b(' + pregQuote(name.toLowerCase()) + ')\\b' , 'gi' );
$('*').each(function() { $('*').each(function() {
var $this = $(this); var $this = $(this);
+1 -1
View File
@@ -24,7 +24,7 @@ function resolveLinks(currentFile, resolveFile, $) {
} }
if (LocationUtils.isExternal(href)) { if (LocationUtils.isExternal(href)) {
$a.attr('_target', 'blank'); $a.attr('target', '_blank');
return; return;
} }
+5 -5
View File
@@ -2,11 +2,11 @@ var Plugins = require('../plugins');
var Promise = require('../utils/promise'); var Promise = require('../utils/promise');
/** /**
Load and setup plugins * Load and setup plugins
*
@param {Output} * @param {Output}
@return {Promise<Output>} * @return {Promise<Output>}
*/ */
function preparePlugins(output) { function preparePlugins(output) {
var book = output.getBook(); var book = output.getBook();
+38
View File
@@ -0,0 +1,38 @@
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');
});
});
});
+27 -31
View File
@@ -18,19 +18,19 @@ var fileToURL = require('../helper/fileToURL');
var resolveFileToURL = require('../helper/resolveFileToURL'); var resolveFileToURL = require('../helper/resolveFileToURL');
/** /**
Directory for a theme with the templates * Directory for a theme with the templates
*/ */
function templateFolder(dir) { function templateFolder(dir) {
return path.join(dir, templatesFolder); return path.join(dir, templatesFolder);
} }
/** /**
Create templating engine to render themes * Create templating engine to render themes
*
@param {Output} output * @param {Output} output
@param {String} currentFile * @param {String} currentFile
@return {TemplateEngine} * @return {TemplateEngine}
*/ */
function createTemplateEngine(output, currentFile) { function createTemplateEngine(output, currentFile) {
var book = output.getBook(); var book = output.getBook();
var state = output.getState(); var state = output.getState();
@@ -47,18 +47,17 @@ function createTemplateEngine(output, currentFile) {
var loader = new Templating.ThemesLoader(tplSearchPaths); var loader = new Templating.ThemesLoader(tplSearchPaths);
// Get languages // Get languages
var language = config.get('language'); var language = config.getValue('language');
// Create API context // Create API context
var context = Api.encodeGlobal(output); var context = Api.encodeGlobal(output);
/** /**
Check if a file exists * Check if a file exists
* @param {String} fileName
@param {String} fileName * @return {Boolean}
@return {Boolean} */
*/
function fileExists(fileName) { function fileExists(fileName) {
if (!fileName) { if (!fileName) {
return false; return false;
@@ -69,11 +68,10 @@ function createTemplateEngine(output, currentFile) {
} }
/** /**
Return an article by its path * Return an article by its path
* @param {String} filePath
@param {String} filePath * @return {Object|undefined}
@return {Object|undefined} */
*/
function getArticleByPath(filePath) { function getArticleByPath(filePath) {
var article = summary.getByPath(filePath); var article = summary.getByPath(filePath);
if (!article) return undefined; if (!article) return undefined;
@@ -82,11 +80,10 @@ function createTemplateEngine(output, currentFile) {
} }
/** /**
Return a page by its path * Return a page by its path
* @param {String} filePath
@param {String} filePath * @return {Object|undefined}
@return {Object|undefined} */
*/
function getPageByPath(filePath) { function getPageByPath(filePath) {
var page = output.getPage(filePath); var page = output.getPage(filePath);
if (!page) return undefined; if (!page) return undefined;
@@ -94,7 +91,6 @@ function createTemplateEngine(output, currentFile) {
return JSONUtils.encodePage(page, summary); return JSONUtils.encodePage(page, summary);
} }
return TemplateEngine.create({ return TemplateEngine.create({
loader: loader, loader: loader,
@@ -108,17 +104,17 @@ function createTemplateEngine(output, currentFile) {
filters: defaultFilters.merge({ filters: defaultFilters.merge({
/** /**
Translate a sentence * Translate a sentence
*/ */
t: function t(s) { t: function t(s) {
return i18n.t(language, s); return i18n.t(language, s);
}, },
/** /**
Resolve an absolute file path into a * Resolve an absolute file path into a
relative path. * relative path.
it also resolve pages * it also resolve pages
*/ */
resolveFile: function(filePath) { resolveFile: function(filePath) {
filePath = resolveFileToURL(output, filePath); filePath = resolveFileToURL(output, filePath);
return LocationUtils.relativeForFile(currentFile, filePath); return LocationUtils.relativeForFile(currentFile, filePath);
+6 -6
View File
@@ -12,11 +12,11 @@ var createTemplateEngine = require('./createTemplateEngine');
var fileToOutput = require('../helper/fileToOutput'); var fileToOutput = require('../helper/fileToOutput');
/** /**
Write a page as a json file * Write a page as a json file
*
@param {Output} output * @param {Output} output
@param {Page} page * @param {Page} page
*/ */
function onPage(output, page) { function onPage(output, page) {
var options = output.getOptions(); var options = output.getOptions();
var prefix = options.get('prefix'); var prefix = options.get('prefix');
@@ -40,7 +40,7 @@ function onPage(output, page) {
return Modifiers.modifyHTML(page, getModifiers(output, page)) return Modifiers.modifyHTML(page, getModifiers(output, page))
.then(function(resultPage) { .then(function(resultPage) {
// Generate the context // Generate the context
var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); var context = JSONUtils.encodeOutputWithPage(output, resultPage);
context.plugins = { context.plugins = {
resources: Plugins.listResources(plugins, resources).toJS() resources: Plugins.listResources(plugins, resources).toJS()
}; };
+5 -5
View File
@@ -5,11 +5,11 @@ var Promise = require('../../utils/promise');
var listSearchPaths = require('./listSearchPaths'); var listSearchPaths = require('./listSearchPaths');
/** /**
Prepare i18n, load translations from plugins and book * Prepare i18n, load translations from plugins and book
*
@param {Output} * @param {Output}
@return {Promise<Output>} * @return {Promise<Output>}
*/ */
function prepareI18n(output) { function prepareI18n(output) {
var state = output.getState(); var state = output.getState();
var i18n = state.getI18n(); var i18n = state.getI18n();
@@ -0,0 +1,37 @@
var parsePageFromString = require('../parsePageFromString');
var Page = require('../../models/page');
describe('parsePageFromString', function() {
var page = new Page();
it('should parse YAML frontmatter', function() {
var CONTENT = '---\nhello: true\nworld: "cool"\n---\n# Hello World\n';
var newPage = parsePageFromString(page, CONTENT);
expect(newPage.getDir()).toBe('ltr');
expect(newPage.getContent()).toBe('# Hello World\n');
var attrs = newPage.getAttributes();
expect(attrs.size).toBe(2);
expect(attrs.get('hello')).toBe(true);
expect(attrs.get('world')).toBe('cool');
});
it('should parse text direction (english)', function() {
var CONTENT = 'Hello World';
var newPage = parsePageFromString(page, CONTENT);
expect(newPage.getDir()).toBe('ltr');
expect(newPage.getContent()).toBe('Hello World');
expect(newPage.getAttributes().size).toBe(0);
});
it('should parse text direction (arab)', function() {
var CONTENT = 'مرحبا بالعالم';
var newPage = parsePageFromString(page, CONTENT);
expect(newPage.getDir()).toBe('rtl');
expect(newPage.getContent()).toBe('مرحبا بالعالم');
expect(newPage.getAttributes().size).toBe(0);
});
});
+12 -11
View File
@@ -1,14 +1,15 @@
module.exports = { module.exports = {
parseBook: require('./parseBook'), parseBook: require('./parseBook'),
parseSummary: require('./parseSummary'), parseSummary: require('./parseSummary'),
parseGlossary: require('./parseGlossary'), parseGlossary: require('./parseGlossary'),
parseReadme: require('./parseReadme'), parseReadme: require('./parseReadme'),
parseConfig: require('./parseConfig'), parseConfig: require('./parseConfig'),
parsePagesList: require('./parsePagesList'), parsePagesList: require('./parsePagesList'),
parseIgnore: require('./parseIgnore'), parseIgnore: require('./parseIgnore'),
listAssets: require('./listAssets'), listAssets: require('./listAssets'),
parseLanguages: require('./parseLanguages'), parseLanguages: require('./parseLanguages'),
parsePage: require('./parsePage'), parsePage: require('./parsePage'),
lookupStructureFile: require('./lookupStructureFile') parsePageFromString: require('./parsePageFromString'),
lookupStructureFile: require('./lookupStructureFile')
}; };
+1 -1
View File
@@ -6,7 +6,7 @@ var findParsableFile = require('./findParsableFile');
@param {Book} book @param {Book} book
@param {String} type: one of ["glossary", "readme", "summary", "langs"] @param {String} type: one of ["glossary", "readme", "summary", "langs"]
@return {Promise<File>} The path of the file found, relative @return {Promise<File | Undefined>} The path of the file found, relative
to the book content root. to the book content root.
*/ */
function lookupStructureFile(book, type) { function lookupStructureFile(book, type) {
+8 -16
View File
@@ -1,27 +1,19 @@
var Immutable = require('immutable'); var parsePageFromString = require('./parsePageFromString');
var fm = require('front-matter');
var direction = require('direction');
/** /**
Parse a page, read its content and parse the YAMl header * Parse a page, read its content and parse the YAMl header
*
@param {Book} book * @param {Book} book
@param {Page} page * @param {Page} page
@return {Promise<Page>} * @return {Promise<Page>}
*/ */
function parsePage(book, page) { function parsePage(book, page) {
var fs = book.getContentFS(); var fs = book.getContentFS();
var file = page.getFile(); var file = page.getFile();
return fs.readAsString(file.getPath()) return fs.readAsString(file.getPath())
.then(function(content) { .then(function(content) {
var parsed = fm(content); return parsePageFromString(page, content);
return page.merge({
content: parsed.body,
attributes: Immutable.fromJS(parsed.attributes),
dir: direction(parsed.body)
});
}); });
} }
+23
View File
@@ -0,0 +1,23 @@
var Immutable = require('immutable');
var fm = require('front-matter');
var direction = require('direction');
/**
* Parse a page, its content and the YAMl header
*
* @param {Page} page
* @return {Page}
*/
function parsePageFromString(page, content) {
// Parse page YAML
var parsed = fm(content);
return page.merge({
content: parsed.body,
attributes: Immutable.fromJS(parsed.attributes),
dir: direction(parsed.body)
});
}
module.exports = parsePageFromString;
+25 -6
View File
@@ -11,15 +11,26 @@ var parsePage = require('./parsePage');
@param {Book} book @param {Book} book
@param {String} filePath @param {String} filePath
@return {Page} @return {Page?}
*/ */
function parseFilePage(book, filePath) { function parseFilePage(book, filePath) {
var fs = book.getContentFS(); var fs = book.getContentFS();
return fs.statFile(filePath) return fs.statFile(filePath)
.then(function(file) { .then(
var page = Page.createForFile(file); function(file) {
return parsePage(book, page); var page = Page.createForFile(file);
return parsePage(book, page);
},
function(err) {
// file doesn't exist
return null;
}
)
.fail(function(err) {
var logger = book.getLogger();
logger.error.ln('error while parsing page "' + filePath + '":');
throw err;
}); });
} }
@@ -48,9 +59,12 @@ function parsePagesList(book) {
return parseFilePage(book, filepath) return parseFilePage(book, filepath)
.then(function(page) { .then(function(page) {
map = map.set(filepath, page);
}, function() {
// file doesn't exist // file doesn't exist
if (!page) {
return;
}
map = map.set(filepath, page);
}); });
}) })
) )
@@ -65,6 +79,11 @@ function parsePagesList(book) {
return parseFilePage(book, file.getPath()) return parseFilePage(book, file.getPath())
.then(function(page) { .then(function(page) {
// file doesn't exist
if (!page) {
return;
}
map = map.set(file.getPath(), page); map = map.set(file.getPath(), page);
}); });
}) })
+1 -1
View File
@@ -1,9 +1,9 @@
var jsonschema = require('jsonschema'); var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults'); var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var schema = require('../constants/configSchema'); var schema = require('../constants/configSchema');
var error = require('../utils/error'); var error = require('../utils/error');
var mergeDefaults = require('../utils/mergeDefaults');
/** /**
Validate a book.json content Validate a book.json content
+19 -17
View File
@@ -4,20 +4,22 @@ var Immutable = require('immutable');
var markdownParser = require('gitbook-markdown'); var markdownParser = require('gitbook-markdown');
var asciidocParser = require('gitbook-asciidoc'); var asciidocParser = require('gitbook-asciidoc');
var EXTENSIONS_MARKDOWN = require('./constants/extsMarkdown');
var EXTENSIONS_ASCIIDOC = require('./constants/extsAsciidoc');
var Parser = require('./models/parser'); var Parser = require('./models/parser');
// This list is ordered by priority of parsers to use // This list is ordered by priority of parsers to use
var parsers = Immutable.List([ var parsers = Immutable.List([
Parser.create('markdown', ['.md', '.markdown', '.mdown'], markdownParser), Parser.create('markdown', EXTENSIONS_MARKDOWN, markdownParser),
Parser.create('asciidoc', ['.adoc', '.asciidoc'], asciidocParser) Parser.create('asciidoc', EXTENSIONS_ASCIIDOC, asciidocParser)
]); ]);
/** /**
Return a specific parser by its name * Return a specific parser by its name
*
@param {String} name * @param {String} name
@return {Parser|undefined} * @return {Parser|undefined}
*/ */
function getParser(name) { function getParser(name) {
return parsers.find(function(parser) { return parsers.find(function(parser) {
return parser.getName() === name; return parser.getName() === name;
@@ -25,11 +27,11 @@ function getParser(name) {
} }
/** /**
Return a specific parser according to an extension * Return a specific parser according to an extension
*
@param {String} ext * @param {String} ext
@return {Parser|undefined} * @return {Parser|undefined}
*/ */
function getParserByExt(ext) { function getParserByExt(ext) {
return parsers.find(function(parser) { return parsers.find(function(parser) {
return parser.matchExtension(ext); return parser.matchExtension(ext);
@@ -37,11 +39,11 @@ function getParserByExt(ext) {
} }
/** /**
Return parser for a file * Return parser for a file
*
@param {String} ext * @param {String} ext
@return {Parser|undefined} * @return {Parser|undefined}
*/ */
function getParserForFile(filename) { function getParserForFile(filename) {
return getParserByExt(path.extname(filename)); return getParserByExt(path.extname(filename));
} }
+19
View File
@@ -0,0 +1,19 @@
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();
});
});
});
-36
View File
@@ -1,36 +0,0 @@
var PluginDependency = require('../../models/pluginDependency');
var listAll = require('../listAll');
describe('listAll', function() {
it('must list default', function() {
var deps = PluginDependency.listFromString('ga,great');
var plugins = listAll(deps);
expect(plugins.size).toBe(8);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
expect(plugins.has('search')).toBe(true);
});
it('must list from array with -', function() {
var deps = PluginDependency.listFromString('ga,-great');
var plugins = listAll(deps);
expect(plugins.size).toBe(7);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(false);
});
it('must remove default plugins using -', function() {
var deps = PluginDependency.listFromString('ga,-search');
var plugins = listAll(deps);
expect(plugins.size).toBe(6);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('search')).toBe(false);
});
});
+38
View File
@@ -0,0 +1,38 @@
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' ]);
});
});
+10
View File
@@ -0,0 +1,10 @@
var path = require('path');
var locateRootFolder = require('../locateRootFolder');
describe('locateRootFolder', function() {
it('should correctly resolve the node_modules for gitbook', function() {
expect(locateRootFolder()).toBe(
path.resolve(__dirname, '../../../')
);
});
});
+42
View File
@@ -0,0 +1,42 @@
var PluginDependency = require('../../models/pluginDependency');
var sortDependencies = require('../sortDependencies');
var toNames = require('../toNames');
describe('sortDependencies', function() {
it('must load themes after plugins', function() {
var allPlugins = PluginDependency.listFromArray([
'hello',
'theme-test',
'world'
]);
var sorted = sortDependencies(allPlugins);
var names = toNames(sorted);
expect(names).toEqual([
'hello',
'world',
'theme-test'
]);
});
it('must keep order of themes', function() {
var allPlugins = PluginDependency.listFromArray([
'theme-test',
'theme-test1',
'hello',
'theme-test2',
'world'
]);
var sorted = sortDependencies(allPlugins);
var names = toNames(sorted);
expect(names).toEqual([
'hello',
'world',
'theme-test',
'theme-test1',
'theme-test2'
]);
});
});
-70
View File
@@ -1,70 +0,0 @@
var PluginDependency = require('../../models/pluginDependency');
var sortPlugins = require('../sortPlugins');
var listAll = require('../listAll');
var THEME_PREFIX = require('../../constants/themePrefix');
/**
Check if a plugin is a theme given its name
@return {Boolean}
*/
function isTheme(name) {
return (name && name.indexOf(THEME_PREFIX) === 0);
}
describe('sortPlugins', function() {
it('must load themes after plugins', function() {
var deps = PluginDependency.listFromString('theme-faq'),
allPlugins = listAll(deps);
return sortPlugins(allPlugins, [])
.then(function(sorted) {
var plugins = sorted.slice(0, -2),
themes = sorted.slice(-2);
var pluginsOk = plugins.every(function(plugin) {
return !isTheme(plugin.getName());
});
var themesOk = themes.every(function(theme) {
return isTheme(theme.getName());
});
expect(pluginsOk).toBe(true);
expect(plugins.has('search')).toBe(true);
expect(themesOk).toBe(true);
expect(themes.size).toBe(2);
expect(themes.has('theme-faq')).toBe(true);
expect(themes.has('theme-default')).toBe(true);
});
});
it('must load themes after plugins with a complex dependencies list', function() {
var deps = PluginDependency.listFromString('comment,theme-faq,-search,ga'),
allPlugins = listAll(deps);
return sortPlugins(allPlugins, [])
.then(function(sorted) {
var plugins = sorted.slice(0, -2),
themes = sorted.slice(-2);
var pluginsOk = plugins.every(function(plugin) {
return !isTheme(plugin.getName());
});
var themesOk = themes.every(function(theme) {
return isTheme(theme.getName());
});
expect(pluginsOk).toBe(true);
expect(plugins.has('search')).toBe(false);
expect(themesOk).toBe(true);
expect(themes.size).toBe(2);
expect(themes.has('theme-faq')).toBe(true);
expect(themes.has('theme-default')).toBe(true);
});
});
});
+7 -7
View File
@@ -1,22 +1,22 @@
var path = require('path');
var Immutable = require('immutable'); var Immutable = require('immutable');
var Promise = require('../utils/promise'); var Promise = require('../utils/promise');
var timing = require('../utils/timing'); var timing = require('../utils/timing');
var findInstalled = require('./findInstalled'); var findInstalled = require('./findInstalled');
var locateRootFolder = require('./locateRootFolder');
/** /**
List all plugins installed in a book * List all plugins installed in a book
*
@param {Book} * @param {Book}
@return {Promise<OrderedMap<String:Plugin>>} * @return {Promise<OrderedMap<String:Plugin>>}
*/ */
function findForBook(book) { function findForBook(book) {
return timing.measure( return timing.measure(
'plugins.findForBook', 'plugins.findForBook',
Promise.all([ Promise.all([
findInstalled(path.resolve(__dirname, '../..')), findInstalled(locateRootFolder()),
findInstalled(book.getRoot()) findInstalled(book.getRoot())
]) ])
+15 -14
View File
@@ -8,21 +8,21 @@ var Plugin = require('../models/plugin');
var PREFIX = require('../constants/pluginPrefix'); var PREFIX = require('../constants/pluginPrefix');
/** /**
Validate if a package name is a GitBook plugin * Validate if a package name is a GitBook plugin
*
@return {Boolean} * @return {Boolean}
*/ */
function validateId(name) { function validateId(name) {
return name && name.indexOf(PREFIX) === 0; return name && name.indexOf(PREFIX) === 0;
} }
/** /**
List all packages installed inside a folder * List all packages installed inside a folder
*
@param {String} folder * @param {String} folder
@return {OrderedMap<String:Plugin>} * @return {OrderedMap<String:Plugin>}
*/ */
function findInstalled(folder) { function findInstalled(folder) {
var options = { var options = {
dev: false, dev: false,
@@ -31,7 +31,7 @@ function findInstalled(folder) {
}; };
var results = Immutable.OrderedMap(); var results = Immutable.OrderedMap();
function onPackage(pkg, isRoot) { function onPackage(pkg, parent) {
if (!pkg.name) return; if (!pkg.name) return;
var name = pkg.name; var name = pkg.name;
@@ -43,18 +43,19 @@ function findInstalled(folder) {
var pluginName = name.slice(PREFIX.length); var pluginName = name.slice(PREFIX.length);
if (!validateId(name)){ if (!validateId(name)){
if (!isRoot) return; if (parent) return;
} else { } else {
results = results.set(pluginName, Plugin({ results = results.set(pluginName, Plugin({
name: pluginName, name: pluginName,
version: version, version: version,
path: pkgPath, path: pkgPath,
depth: depth depth: depth,
parent: parent
})); }));
} }
Immutable.Map(dependencies).forEach(function(dep) { Immutable.Map(dependencies).forEach(function(dep) {
onPackage(dep); onPackage(dep, pluginName);
}); });
} }
@@ -77,7 +78,7 @@ function findInstalled(folder) {
var module_folder = path.join(node_modules, module); var module_folder = path.join(node_modules, module);
return Promise.nfcall(readInstalled, module_folder, options) return Promise.nfcall(readInstalled, module_folder, options)
.then(function(data) { .then(function(data) {
onPackage(data, true); onPackage(data);
}); });
}); });
}) })
+6 -4
View File
@@ -1,11 +1,9 @@
var npmi = require('npmi'); var npmi = require('npmi');
var pkg = require('../../package.json');
var DEFAULT_PLUGINS = require('../constants/defaultPlugins'); var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
var Promise = require('../utils/promise'); var Promise = require('../utils/promise');
var installPlugin = require('./installPlugin'); var installPlugin = require('./installPlugin');
/** /**
Install plugin requirements for a book Install plugin requirements for a book
@@ -20,13 +18,17 @@ function installPlugins(book) {
// Remove default plugins // Remove default plugins
// (only if version is same as installed) // (only if version is same as installed)
plugins = plugins.filterNot(function(plugin) { plugins = plugins.filterNot(function(plugin) {
var dependency = DEFAULT_PLUGINS.find(function(dep) {
return dep.getName() === plugin.getName();
});
return ( return (
// Disabled plugin // Disabled plugin
!plugin.isEnabled() || !plugin.isEnabled() ||
// Or default one installed in GitBook itself // Or default one installed in GitBook itself
(DEFAULT_PLUGINS.includes(plugin.getName()) && (dependency &&
plugin.getVersion() === pkg.dependencies[plugin.getNpmID()]) plugin.getVersion() === dependency.getVersion())
); );
}); });
-50
View File
@@ -1,50 +0,0 @@
var Immutable = require('immutable');
var Plugin = require('../models/plugin');
var pkg = require('../../package.json');
var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
/**
List all plugins for a book
@param {List<PluginDependency>} deps
@return {OrderedMap<Plugin>}
*/
function listAll(deps) {
// Extract list of plugins to disable (starting with -)
var toRemove = deps
.filter(function(plugin) {
return !plugin.isEnabled();
})
.map(function(plugin) {
return plugin.getName();
});
// Convert to an ordered map of Plugin
var plugins = deps
.map(function(dep) {
var plugin = Plugin.createFromDep(dep);
return [dep.getName(), plugin];
});
plugins = Immutable.OrderedMap(plugins);
// Append default plugins
DEFAULT_PLUGINS.forEach(function(pluginName) {
if (plugins.has(pluginName)) return;
plugins = plugins.set(pluginName, new Plugin({
name: pluginName,
version: pkg.dependencies[Plugin.nameToNpmID(pluginName)]
}));
});
// Remove plugins
plugins = plugins.filterNot(function(plugin, name) {
return toRemove.includes(name);
});
return plugins;
}
module.exports = listAll;
+33
View File
@@ -0,0 +1,33 @@
var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
var sortDependencies = require('./sortDependencies');
/**
* List all dependencies for a book, including default plugins.
* It returns a concat with default plugins and remove disabled ones.
*
* @param {List<PluginDependency>} deps
* @return {List<PluginDependency>}
*/
function listDependencies(deps) {
// Extract list of plugins to disable (starting with -)
var toRemove = deps
.filter(function(plugin) {
return !plugin.isEnabled();
})
.map(function(plugin) {
return plugin.getName();
});
// Concat with default plugins
deps = deps.concat(DEFAULT_PLUGINS);
// Remove plugins
deps = deps.filterNot(function(plugin) {
return toRemove.includes(plugin.getName());
});
// Sort
return sortDependencies(deps);
}
module.exports = listDependencies;
+18
View File
@@ -0,0 +1,18 @@
var listDependencies = require('./listDependencies');
/**
* List all plugin requirements for a book.
* It can be different from the final list of plugins,
* since plugins can have their own dependencies
*
* @param {Book}
* @return {List<PluginDependency>}
*/
function listDepsForBook(book) {
var config = book.getConfig();
var plugins = config.getPluginDependencies();
return listDependencies(plugins);
}
module.exports = listDepsForBook;
-18
View File
@@ -1,18 +0,0 @@
var listAll = require('./listAll');
/**
List all plugin requirements for a book.
It can be different from the final list of plugins,
since plugins can have their own dependencies
@param {Book}
@return {OrderedMap<Plugin>}
*/
function listForBook(book) {
var config = book.getConfig();
var plugins = config.getPluginDependencies();
return listAll(plugins);
}
module.exports = listForBook;
+48 -31
View File
@@ -1,52 +1,69 @@
var Promise = require('../utils/promise'); var Immutable = require('immutable');
var listForBook = require('./listForBook'); var Promise = require('../utils/promise');
var listDepsForBook = require('./listDepsForBook');
var findForBook = require('./findForBook'); var findForBook = require('./findForBook');
var loadPlugin = require('./loadPlugin'); var loadPlugin = require('./loadPlugin');
var sortPlugins = require('./sortPlugins');
/** /**
Load a list of plugins in a book * Load all plugins in a book
*
@param {Book} * @param {Book}
@return {Promise<Map<String:Plugin>} * @return {Promise<Map<String:Plugin>}
*/ */
function loadForBook(book) { function loadForBook(book) {
var logger = book.getLogger(); var logger = book.getLogger();
var requirements = listForBook(book);
var requirementsKeys = requirements.keySeq().toList();
// List the dependencies
var requirements = listDepsForBook(book);
// List all plugins installed in the book
return findForBook(book) return findForBook(book)
.then(function(installed) { .then(function(installedMap) {
// Filter out plugins not listed of first level var missing = [];
// (aka pre-installed plugins) var plugins = requirements.reduce(function(result, dep) {
installed = installed.filter(function(plugin) { var name = dep.getName();
return ( var installed = installedMap.get(name);
plugin.getDepth() > 0 ||
requirements.has(plugin.getName()) if (installed) {
); var deps = installedMap
}); .filter(function(plugin) {
return plugin.getParent() === name;
})
.toArray();
result = result.concat(deps);
result.push(installed);
} else {
missing.push(name);
}
return result;
}, []);
// Convert plugins list to a map
plugins = Immutable.List(plugins)
.map(function(plugin) {
return [
plugin.getName(),
plugin
];
});
plugins = Immutable.OrderedMap(plugins);
return sortPlugins(installed, requirementsKeys);
})
.then(function(installed) {
// Log state // Log state
logger.info.ln(installed.size + ' plugins are installed'); logger.info.ln(installedMap.size + ' plugins are installed');
if (requirements.size != installed.size) { if (requirements.size != installedMap.size) {
logger.info.ln(requirements.size + ' explicitly listed'); logger.info.ln(requirements.size + ' explicitly listed');
} }
// Verify that all plugins are present // Verify that all plugins are present
var notInstalled = requirementsKeys.filter(function(name) { if (missing.length > 0) {
return !installed.has(name); throw new Error('Couldn\'t locate plugins "' + missing.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
});
if (notInstalled.size > 0) {
throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
} }
return Promise.map(installed, function(plugin) { return Promise.map(plugins, function(plugin) {
return loadPlugin(book, plugin); return loadPlugin(book, plugin);
}); });
}); });
+22
View File
@@ -0,0 +1,22 @@
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;
+34
View File
@@ -0,0 +1,34 @@
var Immutable = require('immutable');
var THEME_PREFIX = require('../constants/themePrefix');
var TYPE_PLUGIN = 'plugin';
var TYPE_THEME = 'theme';
/**
* Returns the type of a plugin given its name
* @param {Plugin} plugin
* @return {String}
*/
function pluginType(plugin) {
var name = plugin.getName();
return (name && name.indexOf(THEME_PREFIX) === 0) ? TYPE_THEME : TYPE_PLUGIN;
}
/**
* Sort the list of dependencies to match list in book.json
* The themes should always be loaded after the plugins
*
* @param {List<PluginDependency>} deps
* @return {List<PluginDependency>}
*/
function sortDependencies(plugins) {
var byTypes = plugins.groupBy(pluginType);
return byTypes.get(TYPE_PLUGIN, Immutable.List())
.concat(byTypes.get(TYPE_THEME, Immutable.List()));
}
module.exports = sortDependencies;
-45
View File
@@ -1,45 +0,0 @@
var Promise = require('../utils/promise');
var THEME_PREFIX = require('../constants/themePrefix');
var LOADING_ORDER = ['plugin', 'theme'];
/**
Returns the type of a plugin given its name
@return {String}
*/
function pluginType(name) {
return (name && name.indexOf(THEME_PREFIX) === 0) ? 'theme' : 'plugin';
}
/**
Sort the list of installed plugins to match list in book.json
The themes should always be loaded after the plugins
@param {<OrderedMap<String:Plugin>>} plugins
@param {<List<String>>} requirementsKeys
@return {Promise<OrderedMap<String:Plugin>>}
*/
function sortPlugins(plugins, requirementsKeys) {
return Promise()
.then(function() {
// Sort plugins to match list in book.json
plugins = plugins.sort(function(a, b) {
// Get order from book.json
var definitionOrder = requirementsKeys.indexOf(a.getName()) < requirementsKeys.indexOf(b.getName());
// Get order from plugins a and b type
var aType = pluginType(a.getName()),
bType = pluginType(b.getName()),
loadingOrder = LOADING_ORDER.indexOf(aType) < LOADING_ORDER.indexOf(bType);
return loadingOrder || definitionOrder ? -1 : 1;
});
return plugins;
});
}
module.exports = sortPlugins;
+16
View File
@@ -0,0 +1,16 @@
/**
* Return list of plugin names. This method is nly used in unit tests.
*
* @param {OrderedMap<String:Plugin} plugins
* @return {Array<String>}
*/
function toNames(plugins) {
return plugins
.map(function(plugin) {
return plugin.getName();
})
.toArray();
}
module.exports = toNames;
+1 -1
View File
@@ -1,10 +1,10 @@
var Immutable = require('immutable'); var Immutable = require('immutable');
var jsonschema = require('jsonschema'); var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults'); var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var Promise = require('../utils/promise'); var Promise = require('../utils/promise');
var error = require('../utils/error'); var error = require('../utils/error');
var mergeDefaults = require('../utils/mergeDefaults');
/** /**
Validate one plugin for a book and update book's confiration Validate one plugin for a book and update book's confiration
@@ -75,6 +75,12 @@ describe('ConrefsLoader', function() {
describe('transform', function() { describe('transform', function() {
function transform(filePath, source) { 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'; return 'test-' + source + '-endtest';
} }
var engine = TemplateEngine({ var engine = TemplateEngine({
+12 -11
View File
@@ -8,16 +8,15 @@ var PathUtils = require('../utils/path');
/** /**
Template loader resolving both: * Template loader resolving both:
- relative url ("./test.md") * - relative url ("./test.md")
- absolute url ("/test.md") * - absolute url ("/test.md")
- git url ("") * - git url ("")
*
* @param {String} rootFolder
@param {String} rootFolder * @param {Function(filePath, source)} transformFn (optional)
@param {Function(filePath, source)} transformFn (optional) * @param {Logger} logger (optional)
@param {Logger} logger (optional) */
*/
var ConrefsLoader = nunjucks.Loader.extend({ var ConrefsLoader = nunjucks.Loader.extend({
async: true, async: true,
@@ -43,6 +42,8 @@ var ConrefsLoader = nunjucks.Loader.extend({
// Read file from absolute path // Read file from absolute path
return fs.readFile(filepath) return fs.readFile(filepath)
.then(function(source) { .then(function(source) {
source = source.toString('utf8');
if (that.transformFn) { if (that.transformFn) {
return that.transformFn(filepath, source); return that.transformFn(filepath, source);
} }
@@ -51,7 +52,7 @@ var ConrefsLoader = nunjucks.Loader.extend({
}) })
.then(function(source) { .then(function(source) {
return { return {
src: source.toString('utf8'), src: source,
path: filepath path: filepath
}; };
}); });
+7 -7
View File
@@ -2,13 +2,13 @@ var Immutable = require('immutable');
var parsers = require('../parsers'); var parsers = require('../parsers');
/** /**
Return a list of all shortcuts that can apply * Return a list of all shortcuts that can apply
to a file for a TemplatEngine * to a file for a TemplatEngine
*
@param {List<TemplateBlock>} engine * @param {List<TemplateBlock>} engine
@param {String} filePath * @param {String} filePath
@return {List<TemplateShortcut>} * @return {List<TemplateShortcut>}
*/ */
function listShortcuts(blocks, filePath) { function listShortcuts(blocks, filePath) {
var parser = parsers.getForFile(filePath); var parser = parsers.getForFile(filePath);
+7 -7
View File
@@ -3,13 +3,13 @@ var error = require('../utils/error');
var render = require('./render'); var render = require('./render');
/** /**
Render a template * Render a template
*
@param {TemplateEngine} engine * @param {TemplateEngine} engine
@param {String} filePath * @param {String} filePath
@param {Object} context * @param {Object} context
@return {Promise<TemplateOutput>} * @return {Promise<TemplateOutput>}
*/ */
function renderTemplateFile(engine, filePath, context) { function renderTemplateFile(engine, filePath, context) {
var loader = engine.getLoader(); var loader = engine.getLoader();
+13 -13
View File
@@ -1,12 +1,12 @@
var escapeStringRegexp = require('escape-string-regexp'); var escapeStringRegexp = require('escape-string-regexp');
var listShortcuts = require('./listShortcuts'); var listShortcuts = require('./listShortcuts');
/* /**
Apply a shortcut of block to a template * Apply a shortcut of block to a template
@param {String} content * @param {String} content
@param {Shortcut} shortcut * @param {Shortcut} shortcut
@return {String} * @return {String}
*/ */
function applyShortcut(content, shortcut) { function applyShortcut(content, shortcut) {
var start = shortcut.getStart(); var start = shortcut.getStart();
var end = shortcut.getEnd(); var end = shortcut.getEnd();
@@ -24,13 +24,13 @@ function applyShortcut(content, shortcut) {
} }
/** /**
Replace shortcuts from blocks in a string * Replace shortcuts from blocks in a string
*
@param {List<TemplateBlock>} engine * @param {List<TemplateBlock>} engine
@param {String} filePath * @param {String} filePath
@param {String} content * @param {String} content
@return {String} * @return {String}
*/ */
function replaceShortcuts(blocks, filePath, content) { function replaceShortcuts(blocks, filePath, content) {
var shortcuts = listShortcuts(blocks, filePath); var shortcuts = listShortcuts(blocks, filePath);
return shortcuts.reduce(applyShortcut, content); return shortcuts.reduce(applyShortcut, content);
+25 -25
View File
@@ -12,11 +12,11 @@ var ThemesLoader = nunjucks.Loader.extend({
.map(path.normalize); .map(path.normalize);
}, },
/* /**
Read source of a resolved filepath * Read source of a resolved filepath
@param {String} * @param {String}
@return {Object} * @return {Object}
*/ */
getSource: function(fullpath) { getSource: function(fullpath) {
if (!fullpath) return null; if (!fullpath) return null;
@@ -40,19 +40,19 @@ var ThemesLoader = nunjucks.Loader.extend({
}; };
}, },
/* /**
Nunjucks calls "isRelative" to determine when to call "resolve". * Nunjucks calls "isRelative" to determine when to call "resolve".
We handle absolute paths ourselves in ".resolve" so we always return true * We handle absolute paths ourselves in ".resolve" so we always return true
*/ */
isRelative: function() { isRelative: function() {
return true; return true;
}, },
/* /**
Get original search path containing a template * Get original search path containing a template
@param {String} filepath * @param {String} filepath
@return {String} searchPath * @return {String} searchPath
*/ */
getSearchPath: function(filepath) { getSearchPath: function(filepath) {
return this.searchPaths return this.searchPaths
.sortBy(function(s) { .sortBy(function(s) {
@@ -63,22 +63,22 @@ var ThemesLoader = nunjucks.Loader.extend({
}); });
}, },
/* /**
Get template name from a filepath * Get template name from a filepath
@param {String} filepath * @param {String} filepath
@return {String} name * @return {String} name
*/ */
getTemplateName: function(filepath) { getTemplateName: function(filepath) {
var originalSearchPath = this.getSearchPath(filepath); var originalSearchPath = this.getSearchPath(filepath);
return originalSearchPath? path.relative(originalSearchPath, filepath) : null; return originalSearchPath? path.relative(originalSearchPath, filepath) : null;
}, },
/* /**
Resolve a template from a current template * Resolve a template from a current template
@param {String|null} from * @param {String|null} from
@param {String} to * @param {String} to
@return {String|null} * @return {String|null}
*/ */
resolve: function(from, to) { resolve: function(from, to) {
var searchPaths = this.searchPaths; var searchPaths = this.searchPaths;
+15
View File
@@ -39,6 +39,21 @@ describe('LocationUtils', function() {
}); });
}); });
describe('.flatten', function() {
it('should remove leading slash', function() {
expect(LocationUtils.flatten('/test.md')).toBe('test.md');
expect(LocationUtils.flatten('/hello/cool.md')).toBe('hello/cool.md');
});
it('should remove leading slashes', function() {
expect(LocationUtils.flatten('///test.md')).toBe('test.md');
});
it('should not break paths', function() {
expect(LocationUtils.flatten('hello/cool.md')).toBe('hello/cool.md');
});
});
describe('.toAbsolute', function() { describe('.toAbsolute', function() {
it('should correctly transform as absolute', function() { it('should correctly transform as absolute', function() {
expect(LocationUtils.toAbsolute('http://google.fr')).toBe('http://google.fr'); expect(LocationUtils.toAbsolute('http://google.fr')).toBe('http://google.fr');
+51 -35
View File
@@ -40,13 +40,28 @@ function normalize(s) {
} }
/** /**
Convert a relative path to absolute * Flatten a path, it removes the leading "/"
*
* @param {String} href
* @return {String}
*/
function flatten(href) {
href = normalize(href);
if (href[0] == '/') {
href = normalize(href.slice(1));
}
@param {String} href return href;
@param {String} dir: directory parent of the file currently in rendering process }
@param {String} outdir: directory parent from the html output
@return {String} /**
*/ * Convert a relative path to absolute
*
* @param {String} href
* @param {String} dir: directory parent of the file currently in rendering process
* @param {String} outdir: directory parent from the html output
* @return {String}
*/
function toAbsolute(_href, dir, outdir) { function toAbsolute(_href, dir, outdir) {
if (isExternal(_href) || isDataURI(_href)) { if (isExternal(_href) || isDataURI(_href)) {
return _href; return _href;
@@ -74,50 +89,51 @@ function toAbsolute(_href, dir, outdir) {
} }
/** /**
Convert an absolute path to a relative path for a specific folder (dir) * Convert an absolute path to a relative path for a specific folder (dir)
('test/', 'hello.md') -> '../hello.md' * ('test/', 'hello.md') -> '../hello.md'
*
@param {String} dir: current directory * @param {String} dir: current directory
@param {String} file: absolute path of file * @param {String} file: absolute path of file
@return {String} * @return {String}
*/ */
function relative(dir, file) { function relative(dir, file) {
var isDirectory = file.slice(-1) === '/'; var isDirectory = file.slice(-1) === '/';
return normalize(path.relative(dir, file)) + (isDirectory? '/': ''); return normalize(path.relative(dir, file)) + (isDirectory? '/': '');
} }
/** /**
Convert an absolute path to a relative path for a specific folder (dir) * Convert an absolute path to a relative path for a specific folder (dir)
('test/test.md', 'hello.md') -> '../hello.md' * ('test/test.md', 'hello.md') -> '../hello.md'
*
@param {String} baseFile: current file * @param {String} baseFile: current file
@param {String} file: absolute path of file * @param {String} file: absolute path of file
@return {String} * @return {String}
*/ */
function relativeForFile(baseFile, file) { function relativeForFile(baseFile, file) {
return relative(path.dirname(baseFile), file); return relative(path.dirname(baseFile), file);
} }
/** /**
Compare two paths, return true if they are identical * Compare two paths, return true if they are identical
('README.md', './README.md') -> true * ('README.md', './README.md') -> true
*
@param {String} p1: first path * @param {String} p1: first path
@param {String} p2: second path * @param {String} p2: second path
@return {Boolean} * @return {Boolean}
*/ */
function areIdenticalPaths(p1, p2) { function areIdenticalPaths(p1, p2) {
return normalize(p1) === normalize(p2); return normalize(p1) === normalize(p2);
} }
module.exports = { module.exports = {
areIdenticalPaths: areIdenticalPaths, areIdenticalPaths: areIdenticalPaths,
isDataURI: isDataURI, isDataURI: isDataURI,
isExternal: isExternal, isExternal: isExternal,
isRelative: isRelative, isRelative: isRelative,
isAnchor: isAnchor, isAnchor: isAnchor,
normalize: normalize, normalize: normalize,
toAbsolute: toAbsolute, toAbsolute: toAbsolute,
relative: relative, relative: relative,
relativeForFile: relativeForFile relativeForFile: relativeForFile,
flatten: flatten
}; };
+16
View File
@@ -0,0 +1,16 @@
var Immutable = require('immutable');
/**
* Merge
* @param {Object|Map} obj
* @param {Object|Map} src
* @return {Object}
*/
function mergeDefaults(obj, src) {
var objValue = Immutable.fromJS(obj);
var srcValue = Immutable.fromJS(src);
return srcValue.mergeDeep(objValue).toJS();
}
module.exports = mergeDefaults;
+44 -43
View File
@@ -2,34 +2,35 @@ var Q = require('q');
var Immutable = require('immutable'); var Immutable = require('immutable');
// Debugging for long stack traces // Debugging for long stack traces
if (global.__DEV__ || process.env.DEBUG) { if (process.env.DEBUG || process.env.CI) {
Q.longStackSupport = true; Q.longStackSupport = true;
} }
/** /**
Reduce an array to a promise * Reduce an array to a promise
*
@param {Array|List} arr * @param {Array|List} arr
@param {Function(value, element, index)} * @param {Function(value, element, index)}
@return {Promise<Mixed>} * @return {Promise<Mixed>}
*/ */
function reduce(arr, iter, base) { function reduce(arr, iter, base) {
arr = Immutable.Iterable.isIterable(arr)? arr : Immutable.List(arr); arr = Immutable.Iterable.isIterable(arr)? arr : Immutable.List(arr);
return arr.reduce(function(prev, elem, key) { return arr.reduce(function(prev, elem, key) {
return prev.then(function(val) { return prev
.then(function(val) {
return iter(val, elem, key); return iter(val, elem, key);
}); });
}, Q(base)); }, Q(base));
} }
/** /**
Iterate over an array using an async iter * Iterate over an array using an async iter
*
@param {Array|List} arr * @param {Array|List} arr
@param {Function(value, element, index)} * @param {Function(value, element, index)}
@return {Promise} * @return {Promise}
*/ */
function forEach(arr, iter) { function forEach(arr, iter) {
return reduce(arr, function(val, el, key) { return reduce(arr, function(val, el, key) {
return iter(el, key); return iter(el, key);
@@ -37,12 +38,12 @@ function forEach(arr, iter) {
} }
/** /**
Transform an array * Transform an array
*
@param {Array|List} arr * @param {Array|List} arr
@param {Function(value, element, index)} * @param {Function(value, element, index)}
@return {Promise} * @return {Promise}
*/ */
function serie(arr, iter, base) { function serie(arr, iter, base) {
return reduce(arr, function(before, item, key) { return reduce(arr, function(before, item, key) {
return Q(iter(item, key)) return Q(iter(item, key))
@@ -54,12 +55,12 @@ function serie(arr, iter, base) {
} }
/** /**
Iter over an array and return first result (not null) * Iter over an array and return first result (not null)
*
@param {Array|List} arr * @param {Array|List} arr
@param {Function(element, index)} * @param {Function(element, index)}
@return {Promise<Mixed>} * @return {Promise<Mixed>}
*/ */
function some(arr, iter) { function some(arr, iter) {
arr = Immutable.List(arr); arr = Immutable.List(arr);
@@ -73,12 +74,12 @@ function some(arr, iter) {
} }
/** /**
Map an array using an async (promised) iterator * Map an array using an async (promised) iterator
*
@param {Array|List} arr * @param {Array|List} arr
@param {Function(element, index)} * @param {Function(element, index)}
@return {Promise<List>} * @return {Promise<List>}
*/ */
function mapAsList(arr, iter) { function mapAsList(arr, iter) {
return reduce(arr, function(prev, entry, i) { return reduce(arr, function(prev, entry, i) {
return Q(iter(entry, i)) return Q(iter(entry, i))
@@ -90,12 +91,12 @@ function mapAsList(arr, iter) {
} }
/** /**
Map an array or map * Map an array or map
*
@param {Array|List|Map|OrderedMap} arr * @param {Array|List|Map|OrderedMap} arr
@param {Function(element, key)} * @param {Function(element, key)}
@return {Promise<List|Map|OrderedMap>} * @return {Promise<List|Map|OrderedMap>}
*/ */
function map(arr, iter) { function map(arr, iter) {
if (Immutable.Map.isMap(arr)) { if (Immutable.Map.isMap(arr)) {
var type = 'Map'; var type = 'Map';
@@ -122,11 +123,11 @@ function map(arr, iter) {
/** /**
Wrap a function in a promise * Wrap a function in a promise
*
@param {Function} func * @param {Function} func
@return {Funciton} * @return {Funciton}
*/ */
function wrap(func) { function wrap(func) {
return function() { return function() {
var args = Array.prototype.slice.call(arguments, 0); var args = Array.prototype.slice.call(arguments, 0);
+33
View File
@@ -0,0 +1,33 @@
var Immutable = require('immutable');
/**
* Reduce the difference between a map and its default version
* @param {Map} defaultVersion
* @param {Map} currentVersion
* @return {Map} The properties of currentVersion that differs from defaultVersion
*/
function reducedObject(defaultVersion, currentVersion) {
if(defaultVersion === undefined) {
return currentVersion;
}
return currentVersion.reduce(function(result, value, key) {
var defaultValue = defaultVersion.get(key);
if (Immutable.Map.isMap(value)) {
var diffs = reducedObject(defaultValue, value);
if (diffs.size > 0) {
return result.set(key, diffs);
}
}
if (Immutable.is(defaultValue, value)) {
return result;
}
return result.set(key, value);
}, Immutable.Map());
}
module.exports = reducedObject;
+11 -10
View File
@@ -1,6 +1,6 @@
{ {
"name": "gitbook", "name": "gitbook",
"version": "3.1.0", "version": "3.2.2",
"homepage": "https://www.gitbook.com", "homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks", "description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js", "main": "lib/index.js",
@@ -20,34 +20,35 @@
"escape-string-regexp": "1.0.5", "escape-string-regexp": "1.0.5",
"extend": "^3.0.0", "extend": "^3.0.0",
"fresh-require": "1.0.3", "fresh-require": "1.0.3",
"front-matter": "2.0.8", "front-matter": "^2.1.0",
"gitbook-asciidoc": "1.2.0", "gitbook-asciidoc": "1.2.2",
"gitbook-markdown": "1.3.0", "gitbook-markdown": "1.3.2",
"gitbook-plugin-fontsettings": "2.0.0", "gitbook-plugin-fontsettings": "2.0.0",
"gitbook-plugin-highlight": "2.0.2", "gitbook-plugin-highlight": "2.0.2",
"gitbook-plugin-livereload": "0.0.1", "gitbook-plugin-livereload": "0.0.1",
"gitbook-plugin-lunr": "1.1.0", "gitbook-plugin-lunr": "1.2.0",
"gitbook-plugin-search": "2.2.1", "gitbook-plugin-search": "2.2.1",
"gitbook-plugin-sharing": "1.0.2", "gitbook-plugin-sharing": "1.0.2",
"gitbook-plugin-theme-default": "1.0.4", "gitbook-plugin-theme-default": "1.0.6",
"github-slugid": "1.0.1", "github-slugid": "1.0.1",
"graceful-fs": "4.1.4", "graceful-fs": "4.1.4",
"i18n-t": "1.0.0", "i18n-t": "1.0.1",
"ignore": "3.1.2", "ignore": "3.1.2",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"is": "^3.1.0", "is": "^3.1.0",
"js-yaml": "^3.6.1",
"json-schema-defaults": "0.1.1", "json-schema-defaults": "0.1.1",
"jsonschema": "1.1.0", "jsonschema": "1.1.0",
"juice": "1.11.0", "juice": "2.0.0",
"merge-defaults": "0.2.1",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"moment": "2.13.0", "moment": "2.13.0",
"npm": "3.9.2", "npm": "3.9.2",
"npmi": "2.0.1", "npmi": "2.0.1",
"nunjucks": "2.4.2", "nunjucks": "2.5.2",
"nunjucks-do": "1.0.0", "nunjucks-do": "1.0.0",
"object-path": "^0.9.2", "object-path": "^0.9.2",
"omit-keys": "^0.1.0", "omit-keys": "^0.1.0",
"open": "0.0.5",
"q": "1.4.1", "q": "1.4.1",
"read-installed": "^4.0.3", "read-installed": "^4.0.3",
"request": "2.72.0", "request": "2.72.0",
+19 -9
View File
@@ -6,10 +6,9 @@ var cheerio = require('cheerio');
expect.extend({ expect.extend({
/** /**
Check that a file is created in a directory: * Check that a file is created in a directory:
* expect('myFolder').toHaveFile('hello.md');
expect('myFolder').toHaveFile('hello.md'); */
*/
toHaveFile: function(fileName) { toHaveFile: function(fileName) {
var filePath = path.join(this.actual, fileName); var filePath = path.join(this.actual, fileName);
var exists = fs.existsSync(filePath); var exists = fs.existsSync(filePath);
@@ -36,8 +35,8 @@ expect.extend({
}, },
/** /**
Check that a value is defined (not null nor undefined) * Check that a value is defined (not null nor undefined)
*/ */
toBeDefined: function() { toBeDefined: function() {
expect.assert( expect.assert(
!(is.undefined(this.actual) || is.null(this.actual)), !(is.undefined(this.actual) || is.null(this.actual)),
@@ -47,10 +46,21 @@ expect.extend({
}, },
/** /**
Check that a dom element exists in HTML * Check that a value is defined (not null nor undefined)
*/
toNotBeDefined: function() {
expect.assert(
(is.undefined(this.actual) || is.null(this.actual)),
'expected %s to be not defined',
this.actual
);
return this;
},
@param {String} selector /**
*/ * Check that a dom element exists in HTML
* @param {String} selector
*/
toHaveDOMElement: function(selector) { toHaveDOMElement: function(selector) {
var $ = cheerio.load(this.actual); var $ = cheerio.load(this.actual);
var $el = $(selector); var $el = $(selector);