Compare commits

...

82 Commits

Author SHA1 Message Date
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
Samy Pesse 257158321c Bump version to 3.1.0 2016-06-06 19:51:44 +02:00
Samy Pesse ddf0a41e29 Add method editPlugin to config modifiers 2016-06-06 19:42:44 +02:00
Samy Pesse 357e1e8e96 Add unit tests for postRender 2016-06-06 19:24:12 +02:00
Samy Pesse 7a89f9869a Update gitbook-markdown@1.3.0 and gitbook-asciidoc@1.2.0 2016-06-06 18:07:31 +02:00
Samy Pesse 30fbd22efe Add modifier togglePlugin 2016-06-06 17:27:47 +02:00
Samy Pesse 29df73ba72 Add method "toText" to config 2016-06-06 17:16:53 +02:00
Soreine f8676483c1 Default paths are relative to the current book 2016-06-06 16:55:00 +02:00
Johan Preynat c79a6f2fba Use theme-default@1.0.4 2016-06-06 15:46:44 +02:00
Samy Pessé c38e67ea61 Merge pull request #1344 from GitbookIO/fix/readme-in-summary
Search for README file in whole summary before inserting as first article
2016-06-06 12:18:06 +02:00
Johan Preynat 97a5f8805e Use plugin-fontsettings@2.0.0 2016-06-02 10:24:12 +02:00
Johan Preynat 421fef6ebf Search for README file in whole summary before inserting as first article 2016-05-30 15:07:39 +02:00
Johan Preynat 33406c3082 Merge pull request #1336 from GitbookIO/loading/themes
Load plugins before themes
2016-05-28 11:56:34 +02:00
Johan Preynat c3851889b0 Throw an error if generateMock() cannot create the temp directory 2016-05-27 17:15:23 +02:00
Johan Preynat 4a5de2da6b Use sortPlugins() in loadForBook() 2016-05-27 15:06:20 +02:00
Johan Preynat 1afb23cada Add test file for sortPlugins() 2016-05-27 15:06:08 +02:00
Johan Preynat a3b0f3f868 Export plugins sorting method to load plugins before themes 2016-05-27 15:05:55 +02:00
Johan Preynat 0aa32916d6 Add prefix for theme plugins as constant 2016-05-27 15:05:33 +02:00
Samy Pessé ebe845d46c Bump version to 3.0.3 2016-05-27 12:16:56 +02:00
Samy Pessé edee8d2fc5 Update gitbook-theme-default@1.0.3 2016-05-27 12:07:35 +02:00
Samy Pessé 5f3de0876f Add header 'X-Current-Location' to serve and fix handling of directories 2016-05-27 12:01:28 +02:00
Samy Pessé 2ceab04d01 Increase timeout for tests of plugin installation 2016-05-27 11:40:05 +02:00
Samy Pessé de37de44e8 Add tests for installation of all plugins 2016-05-27 11:18:12 +02:00
Samy Pessé ccb87d5a13 Add unit tests for installPlugin 2016-05-27 11:10:39 +02:00
Samy Pessé e2e0ca3f05 Add test for plugin version resolution 2016-05-27 10:54:56 +02:00
Samy Pessé b33a79704b Switch plugin installation and resolveVersion to use PluginDependency instead of Plugin 2016-05-27 09:38:23 +02:00
Samy Pessé 6def174b84 Merge pull request #1334 from ryanswanson/master
Fixed two issues for handling git URLs for plugins
2016-05-27 09:27:11 +02:00
Samy Pessé 346a18b444 Merge pull request #1335 from GitbookIO/fix/1333
Fix #1333: fix invalid URLs for glossary annotations
2016-05-26 22:30:06 +02:00
Samy Pesse b32685c698 Add test for custom glossary file 2016-05-26 22:01:40 +02:00
Samy Pesse 09b3618da8 Annotate glossary terms before resolving links 2016-05-26 21:38:27 +02:00
Samy Pesse 901328f247 Complete basic tests for glossary 2016-05-26 21:37:10 +02:00
Samy Pesse b47ebaa9d9 Use real glossary name when annotating 2016-05-26 21:10:06 +02:00
Ryan Swanson 465e8d6c0a Fixed two issues for handling git URLs for plugins that are published to git repositories rather than on npmjs.org.
Fixed issue in pluginDependency to dereference 'name' and 'version' using Immutable Map.get(...) accessors since config values are now wrapped by Immutable.fromJS(...) in config.js > setValue(...).  Added associated unit test.

Fixed issue in resolveVersion where a plugin may be using a git URL rather than a semver for the version portion of the plugin config definition.  In addition, refactored the resolveVersion function into a new module to allow for unit testing.  Added associated unit test.
2016-05-26 14:30:29 -04:00
Samy Pessé ecd86ec877 Method for parsing glossary when not listed in summary 2016-05-26 17:26:11 +02:00
Samy Pessé 5d7864154c Start test for glossary generation 2016-05-26 17:25:49 +02:00
Samy Pessé 7b915428f7 Fix #1316: fix regression for supporting uppercase filenames in structure 2016-05-26 11:07:13 +02:00
Samy Pesse 0bce3517fd Bump version to 3.0.2 2016-05-25 21:50:38 +02:00
Samy Pesse 089815a8fd Fix #1328: fix crash when anchor links 2016-05-25 21:49:58 +02:00
89 changed files with 1819 additions and 764 deletions
+21
View File
@@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## 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
- Always load themes after plugins
- README/Introduction doesn't require to be the first entry in the summary
- Highlight active entry in summary when scrolling a page with anchors
- Support empty parts in summary
## 3.0.3
- Fix redirection in `gitbook serve` when accessing a folder without trailing slash
- Fix links/annotations for glossary not being correctly resolved
- Fix regression for supporting uppercase filenames in structure files
- Fix `gitbook install` when using Git URLs for plugins
## 3.0.2
- Fix crash for anchor links
## 3.0.1
- Fix regression in link resolution when contain anchor
- Fix `structure.<name>` configuration not supporting filenames with dots
+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.
| 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.
| Variable | Description |
| -------- | ----------- |
| `theme` | The theme to use for the book |
| `plugins` | List of plugins to load |
| `pluginsConfig` |Configuration for plugins |
### Structure
+4 -3
View File
@@ -29,12 +29,13 @@ You can replace `/usr/bin` with any directory that is in your $PATH.
### Cover
Covers are used for all the ebook formats. It's an important part of an ebook brandline.
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:
* Size of 1800x2360 (pixels)
* Size of 1800x2360 pixels for `cover.jpg`, 200x262 for `cover_small.jpg`
* No border
* Clearly visible book title
* Any important text should be visible in the small version
+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
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
+10 -4
View File
@@ -8,12 +8,18 @@ module.exports = {
FS: require('./models/fs'),
Summary: require('./models/summary'),
Glossary: require('./models/glossary'),
Config: require('./models/config'),
Page: require('./models/page'),
PluginDependency: require('./models/pluginDependency'),
// Modifiers
SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config,
SummaryModifier: Modifiers.Summary,
ConfigModifier: Modifiers.Config,
// Constants
CONFIG_FILES: require('./constants/configFiles.js'),
IGNORE_FILES: require('./constants/ignoreFiles.js')
CONFIG_FILES: require('./constants/configFiles.js'),
IGNORE_FILES: require('./constants/ignoreFiles.js'),
DEFAULT_PLUGINS: require('./constants/defaultPlugins'),
EXTENSIONS_MARKDOWN: require('./constants/extsMarkdown'),
EXTENSIONS_ASCIIDOC: require('./constants/extsAsciidoc')
};
+3 -1
View File
@@ -45,7 +45,9 @@ function generateBook(args, kwargs) {
.then(function(resultBook) {
if (hasLiveReloading) {
// 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, {
+21 -2
View File
@@ -71,11 +71,18 @@ Server.prototype.start = function(dir, port) {
// Redirect to directory's index.html
function redirect() {
var resultURL = urlTransform(req.url, function(parsed) {
parsed.pathname += '/';
return parsed;
});
res.statusCode = 301;
res.setHeader('Location', req.url + '/');
res.end('Redirecting to ' + req.url + '/');
res.setHeader('Location', resultURL);
res.end('Redirecting to ' + resultURL);
}
res.setHeader('X-Current-Location', req.url);
// Send file
send(req, url.parse(req.url).pathname, {
root: dir
@@ -106,4 +113,16 @@ Server.prototype.start = function(dir, port) {
});
};
/**
urlTransform is a helper function that allows a function to transform
a url string in it's parsed form and returns the new url as a string
@param {String} uri
@param {Function} fn
@return {String}
*/
function urlTransform(uri, fn) {
return url.format(fn(url.parse(uri)));
}
module.exports = Server;
+10
View File
@@ -22,6 +22,16 @@ describe('configSchema', function() {
expect(result.errors.length).toBe(0);
});
it('should accept uppercase in filename', function() {
var result = validate({
structure: {
readme: 'BOOK.adoc'
}
});
expect(result.errors.length).toBe(0);
});
it('should not accept filepath', function() {
var result = validate({
structure: {
+1 -1
View File
@@ -1,4 +1,4 @@
var FILENAME_REGEX = '^[a-z-._\d,\s]+$';
var FILENAME_REGEX = '^[a-zA-Z-._\d,\s]+$';
module.exports = {
'$schema': 'http://json-schema.org/schema#',
+19 -4
View File
@@ -1,9 +1,24 @@
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,
default plugins should be installed in node dependencies of GitBook
*/
* List of default plugins for all books,
* default plugins should be installed in node dependencies of GitBook
*/
module.exports = Immutable.List([
'highlight',
'search',
@@ -11,4 +26,4 @@ module.exports = Immutable.List([
'sharing',
'fontsettings',
'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'
];
+4
View File
@@ -0,0 +1,4 @@
/*
All GitBook themes plugins name start with this prefix once shorted.
*/
module.exports = 'theme-';
+30
View File
@@ -55,6 +55,36 @@ describe('Config', function() {
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');
});
});
});
+32 -14
View File
@@ -42,21 +42,39 @@ describe('PluginDependency', function() {
expect(plugin.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
});
describe('listToArray', function() {
var list = PluginDependency.listToArray(Immutable.List([
PluginDependency.createFromString('hello@1.0.0'),
PluginDependency.createFromString('noversion'),
PluginDependency.createFromString('-disabled')
]));
describe('listToArray', function() {
it('must create an array from a list of plugin dependencies', function() {
var list = PluginDependency.listToArray(Immutable.List([
PluginDependency.createFromString('hello@1.0.0'),
PluginDependency.createFromString('noversion'),
PluginDependency.createFromString('-disabled')
]));
expect(list).toEqual([
'hello@1.0.0',
'noversion',
'-disabled'
]);
expect(list).toEqual([
'hello@1.0.0',
'noversion',
'-disabled'
]);
});
});
describe('listFromArray', function() {
it('must create an array from a list of plugin dependencies', function() {
var arr = Immutable.fromJS([
'hello@1.0.0',
{
'name': 'plugin-ga',
'version': 'git+ssh://samy@github.com/GitbookIO/plugin-ga.git'
}
]);
var list = PluginDependency.listFromArray(arr);
expect(list.first().getName()).toBe('hello');
expect(list.first().getVersion()).toBe('1.0.0');
expect(list.last().getName()).toBe('plugin-ga');
expect(list.last().getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
});
});
+23
View File
@@ -0,0 +1,23 @@
var SummaryArticle = require('../summaryArticle');
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');
});
});
});
+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');
});
});
});
+27 -6
View File
@@ -293,26 +293,47 @@ Book.prototype.getDefaultExt = function() {
/**
Infer the default path for a Readme.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultReadmePath = function() {
return this.getContentRoot()+'README'+this.getDefaultExt();
Book.prototype.getDefaultReadmePath = function(absolute) {
var defaultPath = 'README'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Summary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultSummaryPath = function() {
return this.getContentRoot()+'SUMMARY'+this.getDefaultExt();
Book.prototype.getDefaultSummaryPath = function(absolute) {
var defaultPath = 'SUMMARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
Infer the default path for a Glossary.
@param {Boolean} [absolute=false] False for a path relative to
this book's content root
@return {String}
*/
Book.prototype.getDefaultGlossaryPath = function() {
return this.getContentRoot()+'GLOSSARY'+this.getDefaultExt();
Book.prototype.getDefaultGlossaryPath = function(absolute) {
var defaultPath = 'GLOSSARY'+this.getDefaultExt();
if (absolute) {
return path.join(this.getContentRoot(), defaultPath);
} else {
return defaultPath;
}
};
/**
+63 -56
View File
@@ -4,6 +4,7 @@ var Immutable = require('immutable');
var File = require('./file');
var PluginDependency = require('./pluginDependency');
var configDefault = require('../constants/configDefault');
var reducedObject = require('../utils/reducedObject');
var Config = Immutable.Record({
file: File(),
@@ -19,21 +20,36 @@ Config.prototype.getValues = function() {
};
/**
Change the file for the configuration
* 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());
};
@param {File} file
@return {Config}
*/
/**
* Render config as text
* @return {Promise<String>}
*/
Config.prototype.toText = function() {
return JSON.stringify(this.toReducedVersion().toJS(), null, 4);
};
/**
* Change the file for the configuration
* @param {File} file
* @return {Config}
*/
Config.prototype.setFile = function(file) {
return this.set('file', file);
};
/**
Return a configuration value by its key path
@param {String} key
@return {Mixed}
*/
* Return a configuration value by its key path
* @param {String} key
* @return {Mixed}
*/
Config.prototype.getValue = function(keyPath, def) {
var values = this.getValues();
keyPath = Config.keyToKeyPath(keyPath);
@@ -46,12 +62,11 @@ Config.prototype.getValue = function(keyPath, def) {
};
/**
Update a configuration value
@param {String} key
@param {Mixed} value
@return {Config}
*/
* Update a configuration value
* @param {String} key
* @param {Mixed} value
* @return {Config}
*/
Config.prototype.setValue = function(keyPath, value) {
keyPath = Config.keyToKeyPath(keyPath);
@@ -64,10 +79,9 @@ Config.prototype.setValue = function(keyPath, value) {
};
/**
Return a list of plugin dependencies
@return {List<PluginDependency>}
*/
* Return a list of plugin dependencies
* @return {List<PluginDependency>}
*/
Config.prototype.getPluginDependencies = function() {
var plugins = this.getValue('plugins');
@@ -79,11 +93,10 @@ Config.prototype.getPluginDependencies = function() {
};
/**
Return a plugin dependency by its name
@param {String} name
@return {PluginDependency}
*/
* Return a plugin dependency by its name
* @param {String} name
* @return {PluginDependency}
*/
Config.prototype.getPluginDependency = function(name) {
var plugins = this.getPluginDependencies();
@@ -93,11 +106,10 @@ Config.prototype.getPluginDependency = function(name) {
};
/**
Update the list of plugins dependencies
@param {List<PluginDependency>}
@return {Config}
*/
* Update the list of plugins dependencies
* @param {List<PluginDependency>}
* @return {Config}
*/
Config.prototype.setPluginDependencies = function(deps) {
var plugins = PluginDependency.listToArray(deps);
@@ -106,11 +118,10 @@ Config.prototype.setPluginDependencies = function(deps) {
/**
Update values for an existing configuration
@param {Object} values
@returns {Config}
*/
* Update values for an existing configuration
* @param {Object} values
* @returns {Config}
*/
Config.prototype.updateValues = function(values) {
values = Immutable.fromJS(values);
@@ -118,12 +129,11 @@ Config.prototype.updateValues = function(values) {
};
/**
Update values for an existing configuration
@param {Config} config
@param {Object} values
@returns {Config}
*/
* Update values for an existing configuration
* @param {Config} config
* @param {Object} values
* @returns {Config}
*/
Config.prototype.mergeValues = function(values) {
var currentValues = this.getValues();
values = Immutable.fromJS(values);
@@ -134,12 +144,11 @@ Config.prototype.mergeValues = function(values) {
};
/**
Create a new config for a file
@param {File} file
@param {Object} values
@returns {Config}
*/
* Create a new config for a file
* @param {File} file
* @param {Object} values
* @returns {Config}
*/
Config.create = function(file, values) {
return new Config({
file: file,
@@ -148,11 +157,10 @@ Config.create = function(file, values) {
};
/**
Create a new config
@param {Object} values
@returns {Config}
*/
* Create a new config
* @param {Object} values
* @returns {Config}
*/
Config.createWithValues = function(values) {
return new Config({
values: Immutable.fromJS(values)
@@ -161,11 +169,10 @@ Config.createWithValues = function(values) {
/**
Convert a keyPath to an array of keys
@param {String|Array}
@return {Array}
*/
* Convert a keyPath to an array of keys
* @param {String|Array}
* @return {Array}
*/
Config.keyToKeyPath = function(keyPath) {
if (is.string(keyPath)) keyPath = keyPath.split('.');
return keyPath;
+21 -6
View File
@@ -1,4 +1,5 @@
var Immutable = require('immutable');
var yaml = require('js-yaml');
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() {
return this.getFile().getPath();
};
/**
Create a page for a file
@param {File} file
@return {Page}
* Create a page for a file
* @param {File} file
* @return {Page}
*/
Page.createForFile = function(file) {
return new Page({
+53 -49
View File
@@ -1,7 +1,9 @@
var Immutable = require('immutable');
var TemplateBlock = require('./templateBlock');
var PREFIX = require('../constants/pluginPrefix');
var PluginDependency = require('./pluginDependency');
var THEME_PREFIX = require('../constants/themePrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({
@@ -16,6 +18,9 @@ var Plugin = Immutable.Record({
// Depth of this plugin in the dependency tree
depth: Number(0),
// Parent depending on this plugin
parent: String(),
// Content of the "package.json"
package: Immutable.Map(),
@@ -47,38 +52,48 @@ Plugin.prototype.getDepth = function() {
return this.get('depth');
};
/**
Return the ID on NPM for this plugin
@return {String}
*/
Plugin.prototype.getNpmID = function() {
return Plugin.nameToNpmID(this.getName());
Plugin.prototype.getParent = function() {
return this.get('parent');
};
/**
Check if a plugin is loaded
* Return the ID on NPM for this plugin
* @return {String}
*/
Plugin.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName());
};
@return {Boolean}
*/
/**
* Check if a plugin is loaded
* @return {Boolean}
*/
Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0);
};
/**
Return map of hooks
@return {Map<String:Function>}
*/
* Check if a plugin is a theme given its name
* @return {Boolean}
*/
Plugin.prototype.isTheme = function() {
var name = this.getName();
return (name && name.indexOf(THEME_PREFIX) === 0);
};
/**
* Return map of hooks
* @return {Map<String:Function>}
*/
Plugin.prototype.getHooks = function() {
return this.getContent().get('hooks') || Immutable.Map();
};
/**
Return infos about resources for a specific type
@param {String} type
@return {Map<String:Mixed>}
*/
* Return infos about resources for a specific type
* @param {String} type
* @return {Map<String:Mixed>}
*/
Plugin.prototype.getResources = function(type) {
if (type != 'website' && type != 'ebook') {
throw new Error('Invalid assets type ' + type);
@@ -91,17 +106,17 @@ Plugin.prototype.getResources = function(type) {
};
/**
Return map of filters
@return {Map<String:Function>}
*/
* Return map of filters
* @return {Map<String:Function>}
*/
Plugin.prototype.getFilters = function() {
return this.getContent().get('filters');
};
/**
Return map of blocks
@return {Map<String:TemplateBlock>}
*/
* Return map of blocks
* @return {Map<String:TemplateBlock>}
*/
Plugin.prototype.getBlocks = function() {
var blocks = this.getContent().get('blocks');
blocks = blocks || Immutable.Map();
@@ -113,21 +128,19 @@ Plugin.prototype.getBlocks = function() {
};
/**
Return a specific hook
@param {String} name
@return {Function|undefined}
*/
* Return a specific hook
* @param {String} name
* @return {Function|undefined}
*/
Plugin.prototype.getHook = function(name) {
return this.getHooks().get(name);
};
/**
Create a plugin from a string
@param {String}
@return {Plugin}
*/
* Create a plugin from a string
* @param {String}
* @return {Plugin}
*/
Plugin.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
@@ -140,11 +153,10 @@ Plugin.createFromString = function(s) {
};
/**
Create a plugin from a dependency
@param {PluginDependency}
@return {Plugin}
*/
* Create a plugin from a dependency
* @param {PluginDependency}
* @return {Plugin}
*/
Plugin.createFromDep = function(dep) {
return new Plugin({
name: dep.getName(),
@@ -152,14 +164,6 @@ Plugin.createFromDep = function(dep) {
});
};
/**
Return NPM id for a plugin name
@param {String}
@return {String}
*/
Plugin.nameToNpmID = function(s) {
return PREFIX + s;
};
Plugin.nameToNpmID = PluginDependency.nameToNpmID;
module.exports = Plugin;
+64 -29
View File
@@ -1,12 +1,14 @@
var is = require('is');
var semver = require('semver');
var Immutable = require('immutable');
var PREFIX = require('../constants/pluginPrefix');
var DEFAULT_VERSION = '*';
/*
PluginDependency represents the informations about a plugin
stored in config.plugins
*/
* PluginDependency represents the informations about a plugin
* stored in config.plugins
*/
var PluginDependency = Immutable.Record({
name: String(),
@@ -30,11 +32,39 @@ PluginDependency.prototype.isEnabled = function() {
};
/**
Create a plugin with a name and a plugin
* Toggle this plugin state
* @param {Boolean}
* @return {PluginDependency}
*/
PluginDependency.prototype.toggle = function(state) {
if (is.undef(state)) {
state = !this.isEnabled();
}
@param {String}
@return {Plugin|undefined}
*/
return this.set('enabled', state);
};
/**
* Return NPM ID for the dependency
* @return {String}
*/
PluginDependency.prototype.getNpmID = function() {
return PluginDependency.nameToNpmID(this.getName());
};
/**
* Is the plugin using a git dependency
* @return {Boolean}
*/
PluginDependency.prototype.isGitDependency = function() {
return !semver.validRange(this.getVersion());
};
/**
* Create a plugin with a name and a plugin
* @param {String}
* @return {Plugin|undefined}
*/
PluginDependency.create = function(name, version, enabled) {
if (is.undefined(enabled)) {
enabled = true;
@@ -48,11 +78,10 @@ PluginDependency.create = function(name, version, enabled) {
};
/**
Create a plugin from a string
@param {String}
@return {Plugin|undefined}
*/
* Create a plugin from a string
* @param {String}
* @return {Plugin|undefined}
*/
PluginDependency.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
@@ -72,22 +101,20 @@ PluginDependency.createFromString = function(s) {
};
/**
Create a PluginDependency from a string
@param {String}
@return {List<PluginDependency>}
*/
* Create a PluginDependency from a string
* @param {String}
* @return {List<PluginDependency>}
*/
PluginDependency.listFromString = function(s) {
var parts = s.split(',');
return PluginDependency.listFromArray(parts);
};
/**
Create a PluginDependency from an array
@param {Array}
@return {List<PluginDependency>}
*/
* Create a PluginDependency from an array
* @param {Array}
* @return {List<PluginDependency>}
*/
PluginDependency.listFromArray = function(arr) {
return Immutable.List(arr)
.map(function(entry) {
@@ -95,8 +122,8 @@ PluginDependency.listFromArray = function(arr) {
return PluginDependency.createFromString(entry);
} else {
return PluginDependency({
name: entry.name,
version: entry.version
name: entry.get('name'),
version: entry.get('version')
});
}
})
@@ -106,11 +133,10 @@ PluginDependency.listFromArray = function(arr) {
};
/**
Export plugin dependencies as an array
@param {List<PluginDependency>} list
@return {Array<String>}
*/
* Export plugin dependencies as an array
* @param {List<PluginDependency>} list
* @return {Array<String>}
*/
PluginDependency.listToArray = function(list) {
return list
.map(function(dep) {
@@ -130,4 +156,13 @@ PluginDependency.listToArray = function(list) {
.toJS();
};
/**
* Return NPM id for a plugin name
* @param {String}
* @return {String}
*/
PluginDependency.nameToNpmID = function(s) {
return PREFIX + s;
};
module.exports = PluginDependency;
+48 -36
View File
@@ -30,20 +30,20 @@ SummaryArticle.prototype.getArticles = function() {
};
/**
Return how deep the article is.
The README has a depth of 1
@return {Number}
*/
* Return how deep the article is.
* The README has a depth of 1
*
* @return {Number}
*/
SummaryArticle.prototype.getDepth = function() {
return (this.getLevel().split('.').length - 1);
};
/**
Get path (without anchor) to the pointing file
@return {String}
*/
* Get path (without anchor) to the pointing file
*
* @return {String}
*/
SummaryArticle.prototype.getPath = function() {
if (this.isExternal()) {
return undefined;
@@ -63,19 +63,19 @@ SummaryArticle.prototype.getPath = function() {
};
/**
Return url if article is external
@return {String}
*/
* Return url if article is external
*
* @return {String}
*/
SummaryArticle.prototype.getUrl = function() {
return this.isExternal()? this.getRef() : undefined;
};
/**
Get anchor for this article (or undefined)
@return {String}
*/
* Get anchor for this article (or undefined)
*
* @return {String}
*/
SummaryArticle.prototype.getAnchor = function() {
var ref = this.getRef();
var parts = ref.split('#');
@@ -85,29 +85,42 @@ 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() {
return !this.isExternal() && this.getRef();
};
/**
Is article pointing to aan absolute url
@return {Boolean}
*/
* Is article pointing to aan absolute url
*
* @return {Boolean}
*/
SummaryArticle.prototype.isExternal = function() {
return location.isExternal(this.getRef());
};
/**
Create a SummaryArticle
@param {Object} def
@return {SummaryArticle}
*/
* Create a SummaryArticle
*
* @param {Object} def
* @return {SummaryArticle}
*/
SummaryArticle.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
@@ -124,14 +137,13 @@ SummaryArticle.create = function(def, level) {
});
};
/**
Find an article from a base one
@param {Article|Part} base
@param {Function(article)} iter
@return {Article}
*/
* Find an article from a base one
*
* @param {Article|Part} base
* @param {Function(article)} iter
* @return {Article}
*/
SummaryArticle.findArticle = function(base, iter) {
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 {SummaryPart}
*/
return childLevel;
};
/**
* Create a SummaryPart
*
* @param {Object} def
* @return {SummaryPart}
*/
SummaryPart.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
+32 -39
View File
@@ -43,10 +43,9 @@ TemplateBlock.prototype.getBlocks = function() {
/**
Return shortcuts associated with this block or undefined
@return {TemplateShortcut|undefined}
*/
* Return shortcuts associated with this block or undefined
* @return {TemplateShortcut|undefined}
*/
TemplateBlock.prototype.getShortcuts = function() {
var shortcuts = this.get('shortcuts');
if (shortcuts.size === 0) {
@@ -57,19 +56,17 @@ TemplateBlock.prototype.getShortcuts = function() {
};
/**
Return name for the nunjucks extension
@return {String}
*/
* Return name for the nunjucks extension
* @return {String}
*/
TemplateBlock.prototype.getExtensionName = function() {
return 'Block' + this.getName() + 'Extension';
};
/**
Return a nunjucks extension to represents this block
@return {Nunjucks.Extension}
*/
* Return a nunjucks extension to represents this block
* @return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
blocksOutput = blocksOutput || {};
@@ -192,11 +189,11 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
};
/**
Apply a block to a content
@param {Object} inner
@param {Object} context
@return {Promise<String>|String}
*/
* Apply a block to a content
* @param {Object} inner
* @param {Object} context
* @return {Promise<String>|String}
*/
TemplateBlock.prototype.applyBlock = function(inner, context) {
var processFn = this.getProcess();
@@ -215,11 +212,10 @@ TemplateBlock.prototype.applyBlock = function(inner, context) {
};
/**
Normalize result from a block process function
@param {Object|String} result
@return {Object}
*/
* Normalize result from a block process function
* @param {Object|String} result
* @return {Object}
*/
TemplateBlock.prototype.normalizeBlockResult = function(result) {
if (is.string(result)) {
result = { body: result };
@@ -230,12 +226,11 @@ TemplateBlock.prototype.normalizeBlockResult = function(result) {
};
/**
Convert a block result to HTML
@param {Object} result
@param {Object} blocksOutput: stored post processing blocks in this object
@return {String}
*/
* Convert a block result to HTML
* @param {Object} result
* @param {Object} blocksOutput: stored post processing blocks in this object
* @return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
var indexedKey;
var toIndex = (!result.parse) || (result.post !== undefined);
@@ -256,12 +251,11 @@ TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
};
/**
Create a template block from a function or an object
@param {String} blockName
@param {Object} block
@return {TemplateBlock}
*/
* Create a template block from a function or an object
* @param {String} blockName
* @param {Object} block
* @return {TemplateBlock}
*/
TemplateBlock.create = function(blockName, block) {
if (is.fn(block)) {
block = new Immutable.Map({
@@ -275,11 +269,10 @@ TemplateBlock.create = function(blockName, block) {
};
/**
Extract kwargs from an arguments array
@param {Array} args
@return {Object}
*/
* Extract kwargs from an arguments array
* @param {Array} args
* @return {Object}
*/
function extractKwargs(args) {
var last = args[args.length - 1];
return (is.object(last) && last.__keywords)? args.pop() : {};
+10 -12
View File
@@ -17,23 +17,21 @@ TemplateOutput.prototype.getBlocks = function() {
};
/**
Update content of this output
@param {String} content
@return {TemplateContent}
*/
* Update content of this output
* @param {String} content
* @return {TemplateContent}
*/
TemplateOutput.prototype.setContent = function(content) {
return this.set('content', content);
};
/**
Create a TemplateOutput from a text content
and an object containing block definition
@param {String} content
@param {Object} blocks
@return {TemplateOutput}
*/
* Create a TemplateOutput from a text content
* and an object containing block definition
* @param {String} content
* @param {Object} blocks
* @return {TemplateOutput}
*/
TemplateOutput.create = function(content, blocks) {
return new TemplateOutput({
content: content,
+1 -4
View File
@@ -1,12 +1,10 @@
var addPlugin = require('../addPlugin');
var Config = require('../../../models/config');
var Book = require('../../../models/book');
describe('addPlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
var book = Book().setConfig(config);
it('should have correct state of dependencies', function() {
var disabledDep = config.getPluginDependency('disabled');
@@ -17,8 +15,7 @@ describe('addPlugin', function() {
});
it('should add the plugin to the list', function() {
var newBook = addPlugin(book, 'test');
var newConfig = newBook.getConfig();
var newConfig = addPlugin(config, 'test');
var testDep = newConfig.getPluginDependency('test');
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();
});
});
@@ -0,0 +1,28 @@
var togglePlugin = require('../togglePlugin');
var Config = require('../../../models/config');
describe('togglePlugin', function() {
var config = Config.createWithValues({
plugins: ['hello', 'world', '-disabled']
});
it('should enable plugin', function() {
var newConfig = togglePlugin(config, 'disabled');
var testDep = newConfig.getPluginDependency('disabled');
expect(testDep).toBeDefined();
expect(testDep.getVersion()).toEqual('*');
expect(testDep.isEnabled()).toBeTruthy();
});
it('should disable plugin', function() {
var newConfig = togglePlugin(config, 'world');
var testDep = newConfig.getPluginDependency('world');
expect(testDep).toBeDefined();
expect(testDep.getVersion()).toEqual('*');
expect(testDep.isEnabled()).toBeFalsy();
});
});
+16 -13
View File
@@ -1,22 +1,25 @@
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 {Config} config
* @param {String} pluginName
* @param {String} version (optional)
* @return {Config}
*/
function addPlugin(config, pluginName, version) {
// For default plugin, we only ensure it is enabled
if (isDefaultPlugin(pluginName, version)) {
return togglePlugin(config, pluginName, true);
}
@param {Book} book
@param {String} plugin
@param {String} version (optional)
@return {Book}
*/
function addPlugin(book, plugin, version) {
var config = book.getConfig();
var deps = config.getPluginDependencies();
var dep = PluginDependency.create(plugin, version);
var dep = PluginDependency.create(pluginName, version);
deps = deps.push(dep);
config = config.setPluginDependencies(deps);
return book.setConfig(config);
return config.setPluginDependencies(deps);
}
module.exports = addPlugin;
+13
View File
@@ -0,0 +1,13 @@
/**
* Edit configuration of a plugin
* @param {Config} config
* @param {String} plugin
* @param {Object} pluginConfig
* @return {Config}
*/
function editPlugin(config, pluginName, pluginConfig) {
return config.set('pluginsConfig.'+pluginName, pluginConfig);
}
module.exports = editPlugin;
+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;
+6 -2
View File
@@ -1,5 +1,9 @@
module.exports = {
addPlugin: require('./addPlugin'),
removePlugin: require('./removePlugin')
addPlugin: require('./addPlugin'),
removePlugin: require('./removePlugin'),
togglePlugin: require('./togglePlugin'),
editPlugin: require('./editPlugin'),
hasPlugin: require('./hasPlugin'),
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;
+15 -12
View File
@@ -1,22 +1,25 @@
var togglePlugin = require('./togglePlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/**
Remove a plugin from a book's configuration
@param {Book} book
@param {String} plugin
@return {Book}
*/
function removePlugin(book, pluginName) {
var config = book.getConfig();
* Remove a plugin from a book's configuration
* @param {Config} config
* @param {String} plugin
* @return {Config}
*/
function removePlugin(config, pluginName) {
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;
});
config = config.setPluginDependencies(deps);
return book.setConfig(config);
return config.setPluginDependencies(deps);
}
module.exports = removePlugin;
+31
View File
@@ -0,0 +1,31 @@
var PluginDependency = require('../../models/pluginDependency');
var hasPlugin = require('./hasPlugin');
var isDefaultPlugin = require('./isDefaultPlugin');
/**
* Enable/disable a plugin dependency
* @param {Config} config
* @param {String} pluginName
* @param {Boolean} state (optional)
* @return {Config}
*/
function togglePlugin(config, pluginName, state) {
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) {
if (dep.getName() === pluginName) {
return dep.toggle(state);
}
return dep;
});
return config.setPluginDependencies(deps);
}
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);
});
});
+49
View File
@@ -1,3 +1,4 @@
var fs = require('fs');
var generateMock = require('../generateMock');
var WebsiteGenerator = require('../website');
@@ -12,6 +13,54 @@ describe('WebsiteGenerator', function() {
});
});
describe('Glossary', function() {
var folder;
before(function() {
return generateMock(WebsiteGenerator, {
'README.md': 'Hello World',
'SUMMARY.md': '* [Deep](folder/page.md)',
'folder': {
'page.md': 'Hello World'
},
'GLOSSARY.md': '# Glossary\n\n## Hello\n\nHello World'
})
.then(function(_folder) {
folder = _folder;
});
});
it('should generate a GLOSSARY.html', function() {
expect(folder).toHaveFile('GLOSSARY.html');
});
it('should correctly resolve glossary links in README', function() {
var html = fs.readFileSync(folder + '/index.html', 'utf8');
expect(html).toHaveDOMElement('.page-inner a[href="GLOSSARY.html#hello"]');
});
it('should correctly resolve glossary links in directory', function() {
var html = fs.readFileSync(folder + '/folder/page.html', 'utf8');
expect(html).toHaveDOMElement('.page-inner a[href="../GLOSSARY.html#hello"]');
});
it('should accept a custom glossary file', function() {
return generateMock(WebsiteGenerator, {
'README.md': 'Hello World',
'book.json': '{ "structure": { "glossary": "custom.md" } }',
'custom.md': '# Glossary\n\n## Hello\n\nHello World'
})
.then(function(folder) {
expect(folder).toHaveFile('custom.html');
expect(folder).toNotHaveFile('GLOSSARY.html');
var html = fs.readFileSync(folder + '/index.html', 'utf8');
expect(html).toHaveDOMElement('.page-inner a[href="custom.html#hello"]');
});
});
});
it('should copy asset files', function() {
return generateMock(WebsiteGenerator, {
'README.md': 'Hello World',
+7 -1
View File
@@ -19,7 +19,13 @@ var generateBook = require('./generateBook');
function generateMock(Generator, files) {
var fs = createMockFS(files);
var book = Book.createForFS(fs);
var dir = tmp.dirSync();
var dir;
try {
dir = tmp.dirSync();
} catch(err) {
throw err;
}
book = book.setLogLevel('disabled');
+12 -7
View File
@@ -4,6 +4,7 @@ var Api = require('../api');
var Plugins = require('../plugins');
var Promise = require('../utils/promise');
var defaultBlocks = require('../constants/defaultBlocks');
var fileToOutput = require('./helper/fileToOutput');
var CODEBLOCK = 'code';
@@ -17,9 +18,13 @@ function getModifiers(output, page) {
var book = output.getBook();
var plugins = output.getPlugins();
var glossary = book.getGlossary();
var entries = glossary.getEntries();
var file = page.getFile();
// Glossary entries
var entries = glossary.getEntries();
var glossaryFile = glossary.getFile();
var glossaryFilename = fileToOutput(output, glossaryFile.getPath());
// Current file path
var currentFilePath = file.getPath();
@@ -34,18 +39,18 @@ function getModifiers(output, page) {
// Normalize IDs on headings
Modifiers.addHeadingId,
// Annotate text with glossary entries
Modifiers.annotateText.bind(null, entries, glossaryFilename),
// Resolve images
Modifiers.resolveImages.bind(null, currentFilePath),
// Resolve links (.md -> .html)
Modifiers.resolveLinks.bind(null,
currentFilePath,
resolveFileToURL.bind(null, output)
),
// Resolve images
Modifiers.resolveImages.bind(null, currentFilePath),
// Annotate text with glossary entries
Modifiers.annotateText.bind(null, entries),
// Highlight code blocks using "code" block
Modifiers.highlightCode.bind(null, function(lang, source) {
return Promise(code.applyBlock({
@@ -12,7 +12,7 @@ describe('annotateText', function() {
it('should annotate text', function() {
var $ = cheerio.load('<p>This is a word, and multiple words</p>');
annotateText(entries, $);
annotateText(entries, 'GLOSSARY.md', $);
var links = $('a');
expect(links.length).toBe(2);
@@ -31,14 +31,14 @@ describe('annotateText', function() {
it('should not annotate scripts', function() {
var $ = cheerio.load('<script>This is a word, and multiple words</script>');
annotateText(entries, $);
annotateText(entries, 'GLOSSARY.md', $);
expect($('a').length).toBe(0);
});
it('should not annotate when has class "no-glossary"', function() {
var $ = cheerio.load('<p class="no-glossary">This is a word, and multiple words</p>');
annotateText(entries, $);
annotateText(entries, 'GLOSSARY.md', $);
expect($('a').length).toBe(0);
});
});
+26 -4
View File
@@ -40,9 +40,8 @@ describe('resolveLinks', function() {
});
describe('Anchor', function() {
var TEST = '<p>This is a <a href="test/cool.md#an-anchor"></a></p>';
it('should prevent anchors in resolution', function() {
var TEST = '<p>This is a <a href="test/cool.md#an-anchor"></a></p>';
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileCustom, $)
@@ -51,6 +50,17 @@ describe('resolveLinks', function() {
expect(link.attr('href')).toBe('test/cool.html#an-anchor');
});
});
it('should ignore pure anchor links', function() {
var TEST = '<p>This is a <a href="#an-anchor"></a></p>';
var $ = cheerio.load(TEST);
return resolveLinks('hello.md', resolveFileCustom, $)
.then(function() {
var link = $('a');
expect(link.attr('href')).toBe('#an-anchor');
});
});
});
describe('Custom Resolver', function() {
@@ -77,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');
});
});
});
});
+3 -2
View File
@@ -62,9 +62,10 @@ function replaceText($, el, search, replace, text_only ) {
Annotate text using a list of GlossaryEntry
@param {List<GlossaryEntry>}
@param {String} glossaryFilePath
@param {HTMLDom} $
*/
function annotateText(entries, $) {
function annotateText(entries, glossaryFilePath, $) {
entries.forEach(function(entry) {
var entryId = entry.getID();
var name = entry.getName();
@@ -81,7 +82,7 @@ function annotateText(entries, $) {
) return;
replaceText($, this, searchRegex, function(match) {
return '<a href="/GLOSSARY.md#' + entryId + '" '
return '<a href="/' + glossaryFilePath + '#' + entryId + '" '
+ 'class="glossary-term" title="' + escape(description) + '">'
+ match
+ '</a>';
+10 -8
View File
@@ -24,22 +24,24 @@ function resolveLinks(currentFile, resolveFile, $) {
}
if (LocationUtils.isExternal(href)) {
$a.attr('_target', 'blank');
$a.attr('target', '_blank');
return;
}
// Split anchor
var parsed = url.parse(href);
href = parsed.pathname;
href = parsed.pathname || '';
// Calcul absolute path for this
href = LocationUtils.toAbsolute(href, currentDirectory, '.');
if (href) {
// Calcul absolute path for this
href = LocationUtils.toAbsolute(href, currentDirectory, '.');
// Resolve file
href = resolveFile(href);
// Resolve file
href = resolveFile(href);
// Convert back to relative
href = LocationUtils.relative(currentDirectory, href);
// Convert back to relative
href = LocationUtils.relative(currentDirectory, href);
}
// Add back anchor
href = href + (parsed.hash || '');
@@ -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 = {
parseBook: require('./parseBook'),
parseSummary: require('./parseSummary'),
parseGlossary: require('./parseGlossary'),
parseReadme: require('./parseReadme'),
parseConfig: require('./parseConfig'),
parsePagesList: require('./parsePagesList'),
parseIgnore: require('./parseIgnore'),
listAssets: require('./listAssets'),
parseLanguages: require('./parseLanguages'),
parsePage: require('./parsePage'),
lookupStructureFile: require('./lookupStructureFile')
parseBook: require('./parseBook'),
parseSummary: require('./parseSummary'),
parseGlossary: require('./parseGlossary'),
parseReadme: require('./parseReadme'),
parseConfig: require('./parseConfig'),
parsePagesList: require('./parsePagesList'),
parseIgnore: require('./parseIgnore'),
listAssets: require('./listAssets'),
parseLanguages: require('./parseLanguages'),
parsePage: require('./parsePage'),
parsePageFromString: require('./parsePageFromString'),
lookupStructureFile: require('./lookupStructureFile')
};
+1 -1
View File
@@ -6,7 +6,7 @@ var findParsableFile = require('./findParsableFile');
@param {Book} book
@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.
*/
function lookupStructureFile(book, type) {
+8 -16
View File
@@ -1,27 +1,19 @@
var Immutable = require('immutable');
var fm = require('front-matter');
var direction = require('direction');
var parsePageFromString = require('./parsePageFromString');
/**
Parse a page, read its content and parse the YAMl header
@param {Book} book
@param {Page} page
@return {Promise<Page>}
*/
* Parse a page, read its content and parse the YAMl header
*
* @param {Book} book
* @param {Page} page
* @return {Promise<Page>}
*/
function parsePage(book, page) {
var fs = book.getContentFS();
var file = page.getFile();
return fs.readAsString(file.getPath())
.then(function(content) {
var parsed = fm(content);
return page.merge({
content: parsed.body,
attributes: Immutable.fromJS(parsed.attributes),
dir: direction(parsed.body)
});
return parsePageFromString(page, content);
});
}
+22
View File
@@ -0,0 +1,22 @@
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) {
var parsed = fm(content);
return page.merge({
content: parsed.body,
attributes: Immutable.fromJS(parsed.attributes),
dir: direction(parsed.body)
});
}
module.exports = parsePageFromString;
+41 -10
View File
@@ -5,6 +5,25 @@ var Page = require('../models/page');
var walkSummary = require('./walkSummary');
var parsePage = require('./parsePage');
/**
Parse a page from a path
@param {Book} book
@param {String} filePath
@return {Page}
*/
function parseFilePage(book, filePath) {
var fs = book.getContentFS();
return fs.statFile(filePath)
.then(function(file) {
var page = Page.createForFile(file);
return parsePage(book, page);
});
}
/**
Parse all pages from a book as an OrderedMap
@@ -12,10 +31,11 @@ var parsePage = require('./parsePage');
@return {Promise<OrderedMap<Page>>}
*/
function parsePagesList(book) {
var fs = book.getContentFS();
var summary = book.getSummary();
var glossary = book.getGlossary();
var map = Immutable.OrderedMap();
// Parse pages from summary
return timing.measure(
'parse.listPages',
walkSummary(summary, function(article) {
@@ -26,21 +46,32 @@ function parsePagesList(book) {
// Is the page ignored?
if (book.isContentFileIgnored(filepath)) return;
return fs.statFile(filepath)
.then(function(file) {
var page = Page.createForFile(file);
return parsePage(book, page);
})
return parseFilePage(book, filepath)
.then(function(page) {
map = map.set(filepath, page);
}, function() {
// file doesn't exist
});
})
.then(function() {
return map;
})
);
)
// Parse glossary
.then(function() {
var file = glossary.getFile();
if (!file.exists()) {
return;
}
return parseFilePage(book, file.getPath())
.then(function(page) {
map = map.set(file.getPath(), page);
});
})
.then(function() {
return map;
});
}
+3 -5
View File
@@ -1,7 +1,6 @@
var parseStructureFile = require('./parseStructureFile');
var Summary = require('../models/summary');
var SummaryModifier = require('../modifiers').Summary;
var location = require('../utils/location');
/**
Parse summary in a book, the summary can only be parsed
@@ -27,11 +26,10 @@ function parseSummary(book) {
summary = Summary.createFromParts(file, result.parts);
}
// Insert readme as first entry
var firstArticle = summary.getFirstArticle();
// Insert readme as first entry if not in SUMMARY.md
var readmeArticle = summary.getByPath(readmeFile.getPath());
if (readmeFile.exists() &&
(!firstArticle || !location.areIdenticalPaths(firstArticle.getRef(), readmeFile.getPath()))) {
if (readmeFile.exists() && !readmeArticle) {
summary = SummaryModifier.unshiftArticle(summary, {
title: 'Introduction',
ref: readmeFile.getPath()
+1 -1
View File
@@ -1,9 +1,9 @@
var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var schema = require('../constants/configSchema');
var error = require('../utils/error');
var mergeDefaults = require('../utils/mergeDefaults');
/**
Validate a book.json content
+19 -17
View File
@@ -4,20 +4,22 @@ var Immutable = require('immutable');
var markdownParser = require('gitbook-markdown');
var asciidocParser = require('gitbook-asciidoc');
var EXTENSIONS_MARKDOWN = require('./constants/extsMarkdown');
var EXTENSIONS_ASCIIDOC = require('./constants/extsAsciidoc');
var Parser = require('./models/parser');
// This list is ordered by priority of parsers to use
var parsers = Immutable.List([
Parser.create('markdown', ['.md', '.markdown', '.mdown'], markdownParser),
Parser.create('asciidoc', ['.adoc', '.asciidoc'], asciidocParser)
Parser.create('markdown', EXTENSIONS_MARKDOWN, markdownParser),
Parser.create('asciidoc', EXTENSIONS_ASCIIDOC, asciidocParser)
]);
/**
Return a specific parser by its name
@param {String} name
@return {Parser|undefined}
*/
* Return a specific parser by its name
*
* @param {String} name
* @return {Parser|undefined}
*/
function getParser(name) {
return parsers.find(function(parser) {
return parser.getName() === name;
@@ -25,11 +27,11 @@ function getParser(name) {
}
/**
Return a specific parser according to an extension
@param {String} ext
@return {Parser|undefined}
*/
* Return a specific parser according to an extension
*
* @param {String} ext
* @return {Parser|undefined}
*/
function getParserByExt(ext) {
return parsers.find(function(parser) {
return parser.matchExtension(ext);
@@ -37,11 +39,11 @@ function getParserByExt(ext) {
}
/**
Return parser for a file
@param {String} ext
@return {Parser|undefined}
*/
* Return parser for a file
*
* @param {String} ext
* @return {Parser|undefined}
*/
function getParserForFile(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();
});
});
});
+29
View File
@@ -0,0 +1,29 @@
var path = require('path');
var PluginDependency = require('../../models/pluginDependency');
var Book = require('../../models/book');
var NodeFS = require('../../fs/node');
var installPlugin = require('../installPlugin');
var Parse = require('../../parse');
describe('installPlugin', function() {
var book;
this.timeout(30000);
before(function() {
var fs = NodeFS(path.resolve(__dirname, '../../../'));
var baseBook = Book.createForFS(fs);
return Parse.parseConfig(baseBook)
.then(function(_book) {
book = _book;
});
});
it('must install a plugin from NPM', function() {
var dep = PluginDependency.createFromString('ga');
return installPlugin(book, dep);
});
});
+30
View File
@@ -0,0 +1,30 @@
var path = require('path');
var Book = require('../../models/book');
var NodeFS = require('../../fs/node');
var installPlugins = require('../installPlugins');
var Parse = require('../../parse');
describe('installPlugins', function() {
var book;
this.timeout(30000);
before(function() {
var fs = NodeFS(path.resolve(__dirname, '../../../'));
var baseBook = Book.createForFS(fs);
return Parse.parseConfig(baseBook)
.then(function(_book) {
book = _book;
});
});
it('must install all plugins from NPM', function() {
return installPlugins(book)
.then(function(n) {
expect(n).toBe(2);
});
});
});
-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, '../../../')
);
});
});
+22
View File
@@ -0,0 +1,22 @@
var PluginDependency = require('../../models/pluginDependency');
var resolveVersion = require('../resolveVersion');
describe('resolveVersion', function() {
it('must skip resolving and return non-semver versions', function() {
var plugin = PluginDependency.createFromString('ga@git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
return resolveVersion(plugin)
.then(function(version) {
expect(version).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
it('must resolve a normal plugin dependency', function() {
var plugin = PluginDependency.createFromString('ga@>0.9.0 < 1.0.1');
return resolveVersion(plugin)
.then(function(version) {
expect(version).toBe('1.0.0');
});
});
});
+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'
]);
});
});
+7 -7
View File
@@ -1,22 +1,22 @@
var path = require('path');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var timing = require('../utils/timing');
var findInstalled = require('./findInstalled');
var locateRootFolder = require('./locateRootFolder');
/**
List all plugins installed in a book
@param {Book}
@return {Promise<OrderedMap<String:Plugin>>}
*/
* List all plugins installed in a book
*
* @param {Book}
* @return {Promise<OrderedMap<String:Plugin>>}
*/
function findForBook(book) {
return timing.measure(
'plugins.findForBook',
Promise.all([
findInstalled(path.resolve(__dirname, '../..')),
findInstalled(locateRootFolder()),
findInstalled(book.getRoot())
])
+15 -14
View File
@@ -8,21 +8,21 @@ var Plugin = require('../models/plugin');
var PREFIX = require('../constants/pluginPrefix');
/**
Validate if a package name is a GitBook plugin
@return {Boolean}
*/
* Validate if a package name is a GitBook plugin
*
* @return {Boolean}
*/
function validateId(name) {
return name && name.indexOf(PREFIX) === 0;
}
/**
List all packages installed inside a folder
@param {String} folder
@return {OrderedMap<String:Plugin>}
*/
* List all packages installed inside a folder
*
* @param {String} folder
* @return {OrderedMap<String:Plugin>}
*/
function findInstalled(folder) {
var options = {
dev: false,
@@ -31,7 +31,7 @@ function findInstalled(folder) {
};
var results = Immutable.OrderedMap();
function onPackage(pkg, isRoot) {
function onPackage(pkg, parent) {
if (!pkg.name) return;
var name = pkg.name;
@@ -43,18 +43,19 @@ function findInstalled(folder) {
var pluginName = name.slice(PREFIX.length);
if (!validateId(name)){
if (!isRoot) return;
if (parent) return;
} else {
results = results.set(pluginName, Plugin({
name: pluginName,
version: version,
path: pkgPath,
depth: depth
depth: depth,
parent: parent
}));
}
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);
return Promise.nfcall(readInstalled, module_folder, options)
.then(function(data) {
onPackage(data, true);
onPackage(data);
});
});
})
+47
View File
@@ -0,0 +1,47 @@
var npmi = require('npmi');
var Promise = require('../utils/promise');
var resolveVersion = require('./resolveVersion');
/**
Install a plugin for a book
@param {Book}
@param {PluginDependency}
@return {Promise}
*/
function installPlugin(book, plugin) {
var logger = book.getLogger();
var installFolder = book.getRoot();
var name = plugin.getName();
var requirement = plugin.getVersion();
logger.info.ln('');
logger.info.ln('installing plugin "' + name + '"');
// Find a version to install
return resolveVersion(plugin)
.then(function(version) {
if (!version) {
throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"');
}
logger.info.ln('install plugin "' + name +'" (' + requirement + ') from NPM with version', version);
return Promise.nfcall(npmi, {
'name': plugin.getNpmID(),
'version': version,
'path': installFolder,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': installFolder
}
});
})
.then(function() {
logger.info.ok('plugin "' + name + '" installed with success');
});
}
module.exports = installPlugin;
+16 -115
View File
@@ -1,134 +1,34 @@
var npm = require('npm');
var npmi = require('npmi');
var semver = require('semver');
var Immutable = require('immutable');
var pkg = require('../../package.json');
var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
var Promise = require('../utils/promise');
var Plugin = require('../models/plugin');
var gitbook = require('../gitbook');
var listForBook = require('./listForBook');
var npmIsReady;
/**
Initialize and prepare NPM
@return {Promise}
*/
function initNPM() {
if (npmIsReady) return npmIsReady;
npmIsReady = Promise.nfcall(npm.load, {
silent: true,
loglevel: 'silent'
});
return npmIsReady;
}
/**
Resolve a plugin to a version
@param {Plugin}
@return {Promise<String>}
*/
function resolveVersion(plugin) {
var npmId = Plugin.nameToNpmID(plugin.getName());
var requiredVersion = plugin.getVersion();
return initNPM()
.then(function() {
return Promise.nfcall(npm.commands.view, [npmId + '@' + requiredVersion, 'engines'], true);
})
.then(function(versions) {
versions = Immutable.Map(versions).entrySeq();
var result = versions
.map(function(entry) {
return {
version: entry[0],
gitbook: (entry[1].engines || {}).gitbook
};
})
.filter(function(v) {
return v.gitbook && gitbook.satisfies(v.gitbook);
})
.sort(function(v1, v2) {
return semver.lt(v1.version, v2.version)? 1 : -1;
})
.get(0);
if (!result) {
return undefined;
} else {
return result.version;
}
});
}
/**
Install a plugin for a book
@param {Book}
@param {Plugin}
@return {Promise}
*/
function installPlugin(book, plugin) {
var logger = book.getLogger();
var installFolder = book.getRoot();
var name = plugin.getName();
var requirement = plugin.getVersion();
logger.info.ln('');
logger.info.ln('installing plugin "' + name + '"');
// Find a version to install
return resolveVersion(plugin)
.then(function(version) {
if (!version) {
throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"');
}
logger.info.ln('install plugin "' + name +'" (' + requirement + ') from NPM with version', version);
return Promise.nfcall(npmi, {
'name': plugin.getNpmID(),
'version': version,
'path': installFolder,
'npmLoad': {
'loglevel': 'silent',
'loaded': true,
'prefix': installFolder
}
});
})
.then(function() {
logger.info.ok('plugin "' + name + '" installed with success');
});
}
var installPlugin = require('./installPlugin');
/**
Install plugin requirements for a book
@param {Book}
@return {Promise}
@return {Promise<Number>}
*/
function installPlugins(book) {
var logger = book.getLogger();
var plugins = listForBook(book);
var config = book.getConfig();
var plugins = config.getPluginDependencies();
// Remove default plugins
// (only if version is same as installed)
plugins = plugins.filterNot(function(plugin) {
var dependency = DEFAULT_PLUGINS.find(function(dep) {
return dep.getName() === plugin.getName();
});
return (
DEFAULT_PLUGINS.includes(plugin.getName()) &&
plugin.getVersion() === pkg.dependencies[plugin.getNpmID()]
// Disabled plugin
!plugin.isEnabled() ||
// Or default one installed in GitBook itself
(dependency &&
plugin.getVersion() === dependency.getVersion())
);
});
@@ -141,7 +41,8 @@ function installPlugins(book) {
return Promise.forEach(plugins, function(plugin) {
return installPlugin(book, plugin);
});
})
.thenResolve(plugins.size);
}
module.exports = installPlugins;
-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;
+47 -31
View File
@@ -1,53 +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 loadPlugin = require('./loadPlugin');
/**
Load a list of plugins in a book
@param {Book}
@return {Promise<Map<String:Plugin>}
*/
* Load all plugins in a book
*
* @param {Book}
* @return {Promise<Map<String:Plugin>}
*/
function loadForBook(book) {
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)
.then(function(installed) {
// Filter out plugins not listed of first level
// (aka pre-installed plugins)
installed = installed.filter(function(plugin) {
return (
plugin.getDepth() > 0 ||
requirements.has(plugin.getName())
);
});
.then(function(installedMap) {
var missing = [];
var plugins = requirements.reduce(function(result, dep) {
var name = dep.getName();
var installed = installedMap.get(name);
// Sort plugins to match list in book.json
installed = installed.sort(function(a, b){
return requirementsKeys.indexOf(a.getName()) < requirementsKeys.indexOf(b.getName()) ? -1 : 1;
});
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);
// Log state
logger.info.ln(installed.size + ' plugins are installed');
if (requirements.size != installed.size) {
logger.info.ln(installedMap.size + ' plugins are installed');
if (requirements.size != installedMap.size) {
logger.info.ln(requirements.size + ' explicitly listed');
}
// Verify that all plugins are present
var notInstalled = requirementsKeys.filter(function(name) {
return !installed.has(name);
});
if (notInstalled.size > 0) {
throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
if (missing.length > 0) {
throw new Error('Couldn\'t locate plugins "' + missing.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);
});
});
+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;
+71
View File
@@ -0,0 +1,71 @@
var npm = require('npm');
var semver = require('semver');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var Plugin = require('../models/plugin');
var gitbook = require('../gitbook');
var npmIsReady;
/**
Initialize and prepare NPM
@return {Promise}
*/
function initNPM() {
if (npmIsReady) return npmIsReady;
npmIsReady = Promise.nfcall(npm.load, {
silent: true,
loglevel: 'silent'
});
return npmIsReady;
}
/**
Resolve a plugin dependency to a version
@param {PluginDependency} plugin
@return {Promise<String>}
*/
function resolveVersion(plugin) {
var npmId = Plugin.nameToNpmID(plugin.getName());
var requiredVersion = plugin.getVersion();
if (plugin.isGitDependency()) {
return Promise.resolve(requiredVersion);
}
return initNPM()
.then(function() {
return Promise.nfcall(npm.commands.view, [npmId + '@' + requiredVersion, 'engines'], true);
})
.then(function(versions) {
versions = Immutable.Map(versions).entrySeq();
var result = versions
.map(function(entry) {
return {
version: entry[0],
gitbook: (entry[1].engines || {}).gitbook
};
})
.filter(function(v) {
return v.gitbook && gitbook.satisfies(v.gitbook);
})
.sort(function(v1, v2) {
return semver.lt(v1.version, v2.version)? 1 : -1;
})
.get(0);
if (!result) {
return undefined;
} else {
return result.version;
}
});
}
module.exports = resolveVersion;
+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;
+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 jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var mergeDefaults = require('../utils/mergeDefaults');
/**
Validate one plugin for a book and update book's confiration
@@ -75,6 +75,12 @@ describe('ConrefsLoader', function() {
describe('transform', function() {
function transform(filePath, source) {
expect(filePath).toBeA('string');
expect(source).toBeA('string');
expect(filePath).toBe(path.resolve(__dirname, 'include.md'));
expect(source).toBe('Hello World');
return 'test-' + source + '-endtest';
}
var engine = TemplateEngine({
+51
View File
@@ -0,0 +1,51 @@
var TemplateEngine = require('../../models/templateEngine');
var TemplateBlock = require('../../models/templateBlock');
var renderTemplate = require('../render');
var postRender = require('../postRender');
describe('postRender', function() {
var testPost;
var engine = TemplateEngine.create({
blocks: [
TemplateBlock.create('lower', function(blk) {
return blk.body.toLowerCase();
}),
TemplateBlock.create('prefix', function(blk) {
return {
body: '_' + blk.body + '_',
post: function() {
testPost = true;
}
};
})
]
});
it('should correctly replace block', function() {
return renderTemplate(engine, 'README.md', 'Hello {% lower %}Samy{% endlower %}')
.then(function(output) {
expect(output.getContent()).toMatch(/Hello \{\{\-([\S]+)\-\}\}/);
expect(output.getBlocks().size).toBe(1);
return postRender(engine, output);
})
.then(function(result) {
expect(result).toBe('Hello samy');
});
});
it('should correctly replace blocks', function() {
return renderTemplate(engine, 'README.md', 'Hello {% lower %}Samy{% endlower %}{% prefix %}Pesse{% endprefix %}')
.then(function(output) {
expect(output.getContent()).toMatch(/Hello \{\{\-([\S]+)\-\}\}\{\{\-([\S]+)\-\}\}/);
expect(output.getBlocks().size).toBe(2);
return postRender(engine, output);
})
.then(function(result) {
expect(result).toBe('Hello samy_Pesse_');
expect(testPost).toBe(true);
});
});
});
+12 -11
View File
@@ -8,16 +8,15 @@ var PathUtils = require('../utils/path');
/**
Template loader resolving both:
- relative url ("./test.md")
- absolute url ("/test.md")
- git url ("")
@param {String} rootFolder
@param {Function(filePath, source)} transformFn (optional)
@param {Logger} logger (optional)
*/
* Template loader resolving both:
* - relative url ("./test.md")
* - absolute url ("/test.md")
* - git url ("")
*
* @param {String} rootFolder
* @param {Function(filePath, source)} transformFn (optional)
* @param {Logger} logger (optional)
*/
var ConrefsLoader = nunjucks.Loader.extend({
async: true,
@@ -43,6 +42,8 @@ var ConrefsLoader = nunjucks.Loader.extend({
// Read file from absolute path
return fs.readFile(filepath)
.then(function(source) {
source = source.toString('utf8');
if (that.transformFn) {
return that.transformFn(filepath, source);
}
@@ -51,7 +52,7 @@ var ConrefsLoader = nunjucks.Loader.extend({
})
.then(function(source) {
return {
src: source.toString('utf8'),
src: source,
path: filepath
};
});
+7 -7
View File
@@ -2,13 +2,13 @@ var Immutable = require('immutable');
var parsers = require('../parsers');
/**
Return a list of all shortcuts that can apply
to a file for a TemplatEngine
@param {List<TemplateBlock>} engine
@param {String} filePath
@return {List<TemplateShortcut>}
*/
* Return a list of all shortcuts that can apply
* to a file for a TemplatEngine
*
* @param {List<TemplateBlock>} engine
* @param {String} filePath
* @return {List<TemplateShortcut>}
*/
function listShortcuts(blocks, filePath) {
var parser = parsers.getForFile(filePath);
+14 -14
View File
@@ -2,12 +2,12 @@ var Promise = require('../utils/promise');
/**
Replace position markers of blocks by body after processing
This is done to avoid that markdown/asciidoc processer parse the block content
@param {String} content
@return {Object} {blocks: Set, content: String}
*/
* Replace position markers of blocks by body after processing
* This is done to avoid that markdown/asciidoc processer parse the block content
*
* @param {String} content
* @return {Object} {blocks: Set, content: String}
*/
function replaceBlocks(content, blocks) {
var newContent = content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) {
var replacedWith = match;
@@ -24,14 +24,14 @@ function replaceBlocks(content, blocks) {
}
/**
Post render a template:
- Execute "post" for blocks
- Replace block content
@param {TemplateEngine} engine
@param {TemplateOutput} content
@return {Promise<String>}
*/
* Post render a template:
* - Execute "post" for blocks
* - Replace block content
*
* @param {TemplateEngine} engine
* @param {TemplateOutput} content
* @return {Promise<String>}
*/
function postRender(engine, output) {
var content = output.getContent();
var blocks = output.getBlocks();
+8 -8
View File
@@ -4,14 +4,14 @@ var TemplateOutput = require('../models/templateOutput');
var replaceShortcuts = require('./replaceShortcuts');
/**
Render a template
@param {TemplateEngine} engine
@param {String} filePath: absolute path for the loader
@param {String} content
@param {Object} context
@return {Promise<TemplateOutput>}
*/
* Render a template
*
* @param {TemplateEngine} engine
* @param {String} filePath: absolute path for the loader
* @param {String} content
* @param {Object} context (optional)
* @return {Promise<TemplateOutput>}
*/
function renderTemplate(engine, filePath, content, context) {
context = context || {};
+7 -7
View File
@@ -3,13 +3,13 @@ var error = require('../utils/error');
var render = require('./render');
/**
Render a template
@param {TemplateEngine} engine
@param {String} filePath
@param {Object} context
@return {Promise<TemplateOutput>}
*/
* Render a template
*
* @param {TemplateEngine} engine
* @param {String} filePath
* @param {Object} context
* @return {Promise<TemplateOutput>}
*/
function renderTemplateFile(engine, filePath, context) {
var loader = engine.getLoader();
+13 -13
View File
@@ -1,12 +1,12 @@
var escapeStringRegexp = require('escape-string-regexp');
var listShortcuts = require('./listShortcuts');
/*
Apply a shortcut of block to a template
@param {String} content
@param {Shortcut} shortcut
@return {String}
*/
/**
* Apply a shortcut of block to a template
* @param {String} content
* @param {Shortcut} shortcut
* @return {String}
*/
function applyShortcut(content, shortcut) {
var start = shortcut.getStart();
var end = shortcut.getEnd();
@@ -24,13 +24,13 @@ function applyShortcut(content, shortcut) {
}
/**
Replace shortcuts from blocks in a string
@param {List<TemplateBlock>} engine
@param {String} filePath
@param {String} content
@return {String}
*/
* Replace shortcuts from blocks in a string
*
* @param {List<TemplateBlock>} engine
* @param {String} filePath
* @param {String} content
* @return {String}
*/
function replaceShortcuts(blocks, filePath, content) {
var shortcuts = listShortcuts(blocks, filePath);
return shortcuts.reduce(applyShortcut, content);
+25 -25
View File
@@ -12,11 +12,11 @@ var ThemesLoader = nunjucks.Loader.extend({
.map(path.normalize);
},
/*
Read source of a resolved filepath
@param {String}
@return {Object}
*/
/**
* Read source of a resolved filepath
* @param {String}
* @return {Object}
*/
getSource: function(fullpath) {
if (!fullpath) return null;
@@ -40,19 +40,19 @@ var ThemesLoader = nunjucks.Loader.extend({
};
},
/*
Nunjucks calls "isRelative" to determine when to call "resolve".
We handle absolute paths ourselves in ".resolve" so we always return true
*/
/**
* Nunjucks calls "isRelative" to determine when to call "resolve".
* We handle absolute paths ourselves in ".resolve" so we always return true
*/
isRelative: function() {
return true;
},
/*
Get original search path containing a template
@param {String} filepath
@return {String} searchPath
*/
/**
* Get original search path containing a template
* @param {String} filepath
* @return {String} searchPath
*/
getSearchPath: function(filepath) {
return this.searchPaths
.sortBy(function(s) {
@@ -63,22 +63,22 @@ var ThemesLoader = nunjucks.Loader.extend({
});
},
/*
Get template name from a filepath
@param {String} filepath
@return {String} name
*/
/**
* Get template name from a filepath
* @param {String} filepath
* @return {String} name
*/
getTemplateName: function(filepath) {
var originalSearchPath = this.getSearchPath(filepath);
return originalSearchPath? path.relative(originalSearchPath, filepath) : null;
},
/*
Resolve a template from a current template
@param {String|null} from
@param {String} to
@return {String|null}
*/
/**
* Resolve a template from a current template
* @param {String|null} from
* @param {String} to
* @return {String|null}
*/
resolve: function(from, to) {
var searchPaths = this.searchPaths;
+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;
+28
View File
@@ -0,0 +1,28 @@
var Immutable = require('immutable');
/**
* Reduce the difference between a map and its default version
* @param {Map} defaultVersion
* @param {Map} currentVersion
*/
function reducedObject(defaultVersion, 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;
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "3.0.1",
"version": "3.1.1",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
@@ -20,26 +20,26 @@
"escape-string-regexp": "1.0.5",
"extend": "^3.0.0",
"fresh-require": "1.0.3",
"front-matter": "2.0.8",
"gitbook-asciidoc": "1.1.4",
"gitbook-markdown": "1.2.3",
"gitbook-plugin-fontsettings": "1.0.3",
"front-matter": "^2.1.0",
"gitbook-asciidoc": "1.2.2",
"gitbook-markdown": "1.3.2",
"gitbook-plugin-fontsettings": "2.0.0",
"gitbook-plugin-highlight": "2.0.2",
"gitbook-plugin-livereload": "0.0.1",
"gitbook-plugin-lunr": "1.1.0",
"gitbook-plugin-search": "2.2.1",
"gitbook-plugin-sharing": "1.0.2",
"gitbook-plugin-theme-default": "1.0.2",
"gitbook-plugin-theme-default": "1.0.4",
"github-slugid": "1.0.1",
"graceful-fs": "4.1.4",
"i18n-t": "1.0.0",
"i18n-t": "1.0.1",
"ignore": "3.1.2",
"immutable": "^3.8.1",
"is": "^3.1.0",
"js-yaml": "^3.6.1",
"json-schema-defaults": "0.1.1",
"jsonschema": "1.1.0",
"juice": "1.11.0",
"merge-defaults": "0.2.1",
"juice": "2.0.0",
"mkdirp": "0.5.1",
"moment": "2.13.0",
"npm": "3.9.2",
+29 -6
View File
@@ -2,13 +2,13 @@ var is = require('is');
var path = require('path');
var fs = require('fs');
var expect = require('expect');
var cheerio = require('cheerio');
expect.extend({
/**
Check that a file is created in a directory:
expect('myFolder').toHaveFile('hello.md');
*/
* Check that a file is created in a directory:
* expect('myFolder').toHaveFile('hello.md');
*/
toHaveFile: function(fileName) {
var filePath = path.join(this.actual, fileName);
var exists = fs.existsSync(filePath);
@@ -35,14 +35,37 @@ expect.extend({
},
/**
Check that a value is defined (not null nor undefined)
*/
* Check that a value is defined (not null nor undefined)
*/
toBeDefined: function() {
expect.assert(
!(is.undefined(this.actual) || is.null(this.actual)),
'expected to be defined'
);
return this;
},
/**
* 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;
},
/**
* Check that a dom element exists in HTML
* @param {String} selector
*/
toHaveDOMElement: function(selector) {
var $ = cheerio.load(this.actual);
var $el = $(selector);
expect.assert($el.length > 0, 'expected HTML to contains %s', selector);
}
});