This commit is contained in:
Samy Pessé
2016-04-22 11:00:21 +02:00
parent 87db7cf1d4
commit 4336fdb241
85 changed files with 2435 additions and 1221 deletions
+2 -1
View File
@@ -13,7 +13,8 @@
"env": {
"node": true,
"mocha": true,
"browser": true
"browser": true,
"jest": true
},
"extends": "eslint:recommended"
}
+11
View File
@@ -0,0 +1,11 @@
jest.autoMockOff();
var gitbook = require('../gitbook');
describe('satisfies', function() {
it('should return true for *', function() {
expect(gitbook.satisfies('*')).toBe(true);
});
});
+3
View File
@@ -0,0 +1,3 @@
module.exports = {};
-69
View File
@@ -1,69 +0,0 @@
var _ = require('lodash');
function BackboneFile(book) {
if (!(this instanceof BackboneFile)) return new BackboneFile(book);
this.book = book;
this.log = this.book.log;
// Filename in the book
this.path = '';
this.parser;
_.bindAll(this);
}
// Type of the backbone file
BackboneFile.prototype.type = '';
// Parse a backbone file
BackboneFile.prototype.parse = function() {
// To be implemented by each child
};
// Handle case where file doesn't exists
BackboneFile.prototype.parseNotFound = function() {
};
// Return true if backbone file exists
BackboneFile.prototype.exists = function() {
return Boolean(this.path);
};
// Locate a backbone file, could be .md, .asciidoc, etc
BackboneFile.prototype.locate = function() {
var that = this;
var filename = this.book.config.getStructure(this.type, true);
this.log.debug.ln('locating', this.type, ':', filename);
return this.book.findParsableFile(filename)
.then(function(result) {
if (!result) return;
that.path = result.path;
that.parser = result.parser;
});
};
// Read and parse the file
BackboneFile.prototype.load = function() {
var that = this;
this.log.debug.ln('loading', this.type, ':', that.path);
return this.locate()
.then(function() {
if (!that.path) return that.parseNotFound();
that.log.debug.ln(that.type, 'located at', that.path);
return that.book.readFile(that.path)
// Parse it
.then(function(content) {
return that.parse(content);
});
});
};
module.exports = BackboneFile;
-99
View File
@@ -1,99 +0,0 @@
var _ = require('lodash');
var util = require('util');
var BackboneFile = require('./file');
// Normalize a glossary entry name into a unique id
function nameToId(name) {
return name.toLowerCase()
.replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
.replace(/ /g, '_')
.trim();
}
/*
A glossary entry is represented by a name and a short description
An unique id for the entry is generated using its name
*/
function GlossaryEntry(name, description) {
if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
this.name = name;
this.description = description;
Object.defineProperty(this, 'id', {
get: _.bind(this.getId, this)
});
}
// Normalizes a glossary entry's name to create an ID
GlossaryEntry.prototype.getId = function() {
return nameToId(this.name);
};
/*
A glossary is a list of entries stored in a GLOSSARY.md file
*/
function Glossary() {
BackboneFile.apply(this, arguments);
this.entries = [];
}
util.inherits(Glossary, BackboneFile);
Glossary.prototype.type = 'glossary';
// Get templating context
Glossary.prototype.getContext = function() {
if (!this.path) return {};
return {
glossary: {
path: this.path
}
};
};
// Parse the readme content
Glossary.prototype.parse = function(content) {
var that = this;
return this.parser.glossary(content)
.then(function(entries) {
that.entries = _.map(entries, function(entry) {
return new GlossaryEntry(entry.name, entry.description);
});
});
};
// Return an entry by its id
Glossary.prototype.get = function(id) {
return _.find(this.entries, {
id: id
});
};
// Find an entry by its name
Glossary.prototype.find = function(name) {
return this.get(nameToId(name));
};
// Return false if glossary has entries (and exists)
Glossary.prototype.isEmpty = function(id) {
return _.size(this.entries) === 0;
};
// Convert the glossary to a list of annotations
Glossary.prototype.annotations = function() {
return _.map(this.entries, function(entry) {
return {
id: entry.id,
name: entry.name,
description: entry.description,
href: '/' + this.path + '#' + entry.id
};
}, this);
};
module.exports = Glossary;
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
Readme: require('./readme'),
Summary: require('./summary'),
Glossary: require('./glossary'),
Langs: require('./langs')
};
-81
View File
@@ -1,81 +0,0 @@
var _ = require('lodash');
var path = require('path');
var util = require('util');
var BackboneFile = require('./file');
function Language(title, folder) {
var that = this;
this.title = title;
this.folder = folder;
Object.defineProperty(this, 'id', {
get: function() {
return path.basename(that.folder);
}
});
}
/*
A Langs is a list of languages stored in a LANGS.md file
*/
function Langs() {
BackboneFile.apply(this, arguments);
this.languages = [];
}
util.inherits(Langs, BackboneFile);
Langs.prototype.type = 'langs';
// Parse the readme content
Langs.prototype.parse = function(content) {
var that = this;
return this.parser.langs(content)
.then(function(langs) {
that.languages = _.map(langs, function(entry) {
return new Language(entry.title, entry.path);
});
});
};
// Return the list of languages
Langs.prototype.list = function() {
return this.languages;
};
// Return default/main language for the book
Langs.prototype.getDefault = function() {
return _.first(this.languages);
};
// Return true if a language is the default one
// "lang" cam be a string (id) or a Language entry
Langs.prototype.isDefault = function(lang) {
lang = lang.id || lang;
return (this.cound() > 0 && this.getDefault().id == lang);
};
// Return the count of languages
Langs.prototype.count = function() {
return _.size(this.languages);
};
// Return templating context for the languages list
Langs.prototype.getContext = function() {
if (this.count() == 0) return {};
return {
languages: {
list: _.map(this.languages, function(lang) {
return {
id: lang.id,
title: lang.title
};
})
}
};
};
module.exports = Langs;
-44
View File
@@ -1,44 +0,0 @@
var util = require('util');
var BackboneFile = require('./file');
function Readme() {
BackboneFile.apply(this, arguments);
this.title;
this.description;
}
util.inherits(Readme, BackboneFile);
Readme.prototype.type = 'readme';
/*
Return and extension of context to define the readme
@retrun {Object}
*/
Readme.prototype.getContext = function() {
return {
readme: {
path: this.path
}
};
};
/*
Parse the readme content
@param {String} content
@retrun {Promise}
*/
Readme.prototype.parse = function(content) {
var that = this;
return this.parser.readme(content)
.then(function(out) {
that.title = out.title;
that.description = out.description;
});
};
module.exports = Readme;
-349
View File
@@ -1,349 +0,0 @@
var _ = require('lodash');
var util = require('util');
var location = require('../utils/location');
var error = require('../utils/error');
var BackboneFile = require('./file');
/*
An article represent an entry in the Summary.
It's defined by a title, a reference, and children articles,
the reference (ref) can be a filename + anchor or an external file (optional)
*/
function TOCArticle(def, parent) {
// Title
this.title = def.title;
// Parent TOCPart or TOCArticle
this.parent = parent;
// As string indicating the overall position
// ex: '1.0.0'
this.level;
this._next;
this._prev;
// When README has been automatically added
this.isAutoIntro = def.isAutoIntro;
this.isIntroduction = def.isIntroduction;
this.validate();
// Path can be a relative path or an url, or nothing
this.ref = def.path;
if (this.ref && !this.isExternal()) {
var parts = this.ref.split('#');
this.path = (parts.length > 1? parts.slice(0, -1).join('#') : this.ref);
this.anchor = (parts.length > 1? '#' + _.last(parts) : null);
// Normalize path to remove ('./', etc)
this.path = location.normalize(this.path);
}
this.articles = _.map(def.articles || [], function(article) {
if (article instanceof TOCArticle) return article;
return new TOCArticle(article, this);
}, this);
}
// Validate the article
TOCArticle.prototype.validate = function() {
if (!this.title) {
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
}
};
// Iterate over all articles in this articles
TOCArticle.prototype.walk = function(iter, base) {
base = base || this.level;
_.each(this.articles, function(article, i) {
var level = levelId(base, i);
if (iter(article, level) === false) {
return false;
}
article.walk(iter, level);
});
};
// Return templating context for an article
TOCArticle.prototype.getContext = function() {
return {
level: this.level,
title: this.title,
depth: this.depth(),
path: this.isExternal()? undefined : this.path,
anchor: this.isExternal()? undefined : this.anchor,
url: this.isExternal()? this.ref : undefined
};
};
// Return true if is pointing to a file
TOCArticle.prototype.hasLocation = function() {
return Boolean(this.path);
};
// Return true if is pointing to an external location
TOCArticle.prototype.isExternal = function() {
return location.isExternal(this.ref);
};
// Return true if this article is the introduction
TOCArticle.prototype.isIntro = function() {
return Boolean(this.isIntroduction);
};
// Return true if has children
TOCArticle.prototype.hasChildren = function() {
return this.articles.length > 0;
};
// Return true if has an article as parent
TOCArticle.prototype.hasParent = function() {
return !(this.parent instanceof TOCPart);
};
// Return depth of this article
TOCArticle.prototype.depth = function() {
return this.level.split('.').length;
};
// Return next article in the TOC
TOCArticle.prototype.next = function() {
return this._next;
};
// Return previous article in the TOC
TOCArticle.prototype.prev = function() {
return this._prev;
};
// Map over all articles
TOCArticle.prototype.map = function(iter) {
return _.map(this.articles, iter);
};
/*
A part of a ToC is a composed of a tree of articles and an optiona title
*/
function TOCPart(part, parent) {
if (!(this instanceof TOCPart)) return new TOCPart(part, parent);
TOCArticle.apply(this, arguments);
}
util.inherits(TOCPart, TOCArticle);
// Validate the part
TOCPart.prototype.validate = function() { };
// Return a sibling (next or prev) of this part
TOCPart.prototype.sibling = function(direction) {
var parts = this.parent.parts;
var pos = _.findIndex(parts, this);
if (parts[pos + direction]) {
return parts[pos + direction];
}
return null;
};
// Iterate over all entries of the part
TOCPart.prototype.walk = function(iter, base) {
var articles = this.articles;
if (articles.length == 0) return;
// Has introduction?
if (articles[0].isIntro()) {
if (iter(articles[0], '0') === false) {
return;
}
articles = articles.slice(1);
}
_.each(articles, function(article, i) {
var level = levelId(base, i);
if (iter(article, level) === false) {
return false;
}
article.walk(iter, level);
});
};
// Return templating context for a part
TOCPart.prototype.getContext = function(onArticle) {
onArticle = onArticle || function(article) {
return article.getContext();
};
return {
title: this.title,
articles: this.map(onArticle)
};
};
/*
A summary is composed of a list of parts, each composed wit a tree of articles.
*/
function Summary() {
BackboneFile.apply(this, arguments);
this.parts = [];
this._length = 0;
}
util.inherits(Summary, BackboneFile);
Summary.prototype.type = 'summary';
// Prepare summary when non existant
Summary.prototype.parseNotFound = function() {
this.update([]);
};
// Parse the summary content
Summary.prototype.parse = function(content) {
var that = this;
return this.parser.summary(content)
.then(function(summary) {
that.update(summary.parts);
});
};
// Return templating context for the summary
Summary.prototype.getContext = function() {
function onArticle(article) {
var result = article.getContext();
if (article.hasChildren()) {
result.articles = article.map(onArticle);
}
return result;
}
return {
summary: {
path: this.path,
parts: _.map(this.parts, function(part) {
return part.getContext(onArticle);
})
}
};
};
// Iterate over all entries of the summary
// iter is called with an TOCArticle
Summary.prototype.walk = function(iter) {
var hasMultipleParts = this.parts.length > 1;
_.each(this.parts, function(part, i) {
part.walk(iter, hasMultipleParts? levelId('', i) : null);
});
};
// Find a specific article using a filter
Summary.prototype.find = function(filter) {
var result;
this.walk(function(article) {
if (filter(article)) {
result = article;
return false;
}
});
return result;
};
// Flatten the list of articles
Summary.prototype.flatten = function() {
var result = [];
this.walk(function(article) {
result.push(article);
});
return result;
};
// Return the first TOCArticle for a specific page (or path)
Summary.prototype.getArticle = function(page) {
if (!_.isString(page)) page = page.path;
return this.find(function(article) {
return article.path == page;
});
};
// Return the first TOCArticle for a specific level
Summary.prototype.getArticleByLevel = function(lvl) {
return this.find(function(article) {
return article.level == lvl;
});
};
// Return the count of articles in the summary
Summary.prototype.count = function() {
return this._length;
};
// Prepare the summary
Summary.prototype.update = function(parts) {
var that = this;
that.parts = _.map(parts, function(part) {
return new TOCPart(part, that);
});
// Create first part if none
if (that.parts.length == 0) {
that.parts.push(new TOCPart({}, that));
}
// Add README as first entry
var firstArticle = that.parts[0].articles[0];
if (!firstArticle || firstArticle.path != that.book.readme.path) {
that.parts[0].articles.unshift(new TOCArticle({
title: 'Introduction',
path: that.book.readme.path,
isAutoIntro: true
}, that.parts[0]));
}
that.parts[0].articles[0].isIntroduction = true;
// Update the count and indexing of "level"
var prev = undefined;
that._length = 0;
that.walk(function(article, level) {
// Index level
article.level = level;
// Chain articles
article._prev = prev;
if (prev) prev._next = article;
prev = article;
that._length += 1;
});
};
// Return a level string from a base level and an index
function levelId(base, i) {
i = i + 1;
return (base? [base || '', i] : [i]).join('.');
}
module.exports = Summary;
-137
View File
@@ -1,137 +0,0 @@
var _ = require('lodash');
var semver = require('semver');
var gitbook = require('../gitbook');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var validator = require('./validator');
var plugins = require('./plugins');
// Config files to tested (sorted)
var CONFIG_FILES = [
'book.js',
'book.json'
];
/*
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
*/
function Config(book, baseConfig) {
this.book = book;
this.fs = book.fs;
this.log = book.log;
this.path = '';
this.baseConfig = baseConfig || {};
this.replace({});
}
// Load configuration of the book
// and verify that the configuration is satisfying
Config.prototype.load = function() {
var that = this;
var isLanguageBook = this.book.isLanguageBook();
// Try all potential configuration file
return Promise.some(CONFIG_FILES, function(filename) {
that.log.debug.ln('try loading configuration from', filename);
return that.fs.loadAsObject(that.book.resolve(filename))
.then(function(_config) {
that.log.debug.ln('configuration loaded from', filename);
that.path = filename;
return that.replace(_config);
})
.fail(function(err) {
if (err.code != 'MODULE_NOT_FOUND') throw(err);
else return Promise(false);
});
})
.then(function() {
if (!isLanguageBook) {
if (!gitbook.satisfies(that.options.gitbook)) {
throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
}
if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(gitbook.version, 'patch'), that.options.gitbook)) {
that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \'>='+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate');
}
that.options.plugins = plugins.toList(that.options.plugins);
} else {
// Multilingual book should inherits the plugins list from parent
that.options.plugins = that.book.parent.config.get('plugins');
}
that.options.gitbook = gitbook.version;
});
};
// Replace the whole configuration
Config.prototype.replace = function(options) {
var that = this;
// Extend base config
options = _.defaults(_.cloneDeep(options), this.baseConfig);
// Validate the config
this.options = validator.validate(options);
// options.input == book.root
Object.defineProperty(this.options, 'input', {
get: function () {
return that.book.root;
}
});
// options.originalInput == book.parent.root
Object.defineProperty(this.options, 'originalInput', {
get: function () {
return that.book.parent? that.book.parent.root : undefined;
}
});
error.deprecateField(this.options, 'generator', (this.book.output? this.book.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
error.deprecateField(this.options, 'output', (this.book.output && this.book.output.root? this.book.output.root() : null), '"options.output" property is deprecated, use "output.root()" instead');
};
// Return true if book has a configuration file
Config.prototype.exists = function() {
return Boolean(this.path);
};
// Return path to a structure file
// Strip the extension by default
Config.prototype.getStructure = function(name, dontStripExt) {
var filename = this.options.structure[name];
if (dontStripExt) return filename;
filename = filename.split('.').slice(0, -1).join('.');
return filename;
};
// Return a configuration using a key and a default value
Config.prototype.get = function(key, def) {
return _.get(this.options, key, def);
};
// Update a configuration
Config.prototype.set = function(key, value) {
return _.set(this.options, key, value);
};
// Return a dump of the configuration
Config.prototype.dump = function() {
var opts = _.omit(this.options, 'generator', 'output');
return _.cloneDeep(opts);
};
// Return templating context
Config.prototype.getContext = function() {
return {
config: this.book.config.dump()
};
};
module.exports = Config;
-67
View File
@@ -1,67 +0,0 @@
var _ = require('lodash');
// Default plugins added to each books
var DEFAULT_PLUGINS = ['highlight', 'search', 'lunr', 'sharing', 'fontsettings', 'theme-default'];
// Return true if a plugin is a default plugin
function isDefaultPlugin(name, version) {
return _.contains(DEFAULT_PLUGINS, name);
}
// Normalize a list of plugins to use
function normalizePluginsList(plugins) {
// Normalize list to an array
plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
// Remove empty parts
plugins = _.compact(plugins);
// Divide as {name, version} to handle format like 'myplugin@1.0.0'
plugins = _.map(plugins, function(plugin) {
if (plugin.name) return plugin;
var parts = plugin.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
return {
'name': name,
'version': version // optional
};
});
// List plugins to remove
var toremove = _.chain(plugins)
.filter(function(plugin) {
return plugin.name.length > 0 && plugin.name[0] == '-';
})
.map(function(plugin) {
return plugin.name.slice(1);
})
.value();
// Merge with defaults
_.each(DEFAULT_PLUGINS, function(plugin) {
if (_.find(plugins, { name: plugin })) {
return;
}
plugins.push({
'name': plugin
});
});
// Remove plugin that start with '-'
plugins = _.filter(plugins, function(plugin) {
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
});
// Remove duplicates
plugins = _.uniq(plugins, 'name');
return plugins;
}
module.exports = {
isDefaultPlugin: isDefaultPlugin,
toList: normalizePluginsList
};
+5
View File
@@ -0,0 +1,5 @@
// Configuration files to test (sorted)
module.exports = [
'book.js',
'book.json'
];
+14
View File
@@ -0,0 +1,14 @@
var Immutable = require('immutable');
/*
List of default plugins for all books,
default plugins should be installed in node dependencies of GitBook
*/
module.exports = Immutable.List([
'highlight',
'search',
'lunr',
'sharing',
'fontsettings',
'theme-default'
]);
+6
View File
@@ -0,0 +1,6 @@
// Files containing ignore pattner (sorted by priority)
module.exports = [
'.ignore',
'.gitignore',
'.bookignore'
];
+5
View File
@@ -0,0 +1,5 @@
/*
All GitBook plugins are NPM packages starting with this prefix.
*/
module.exports = 'gitbook-plugin-';
-106
View File
@@ -1,106 +0,0 @@
var _ = require('lodash');
var path = require('path');
var Promise = require('../utils/promise');
/*
A filesystem is an interface to read files
GitBook can works with a virtual filesystem, for example in the browser.
*/
// .readdir return files/folder as a list of string, folder ending with '/'
function pathIsFolder(filename) {
return _.last(filename) == '/' || _.last(filename) == '\\';
}
function FS() {
}
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
FS.prototype.exists = function(filename) {
// To implement for each fs
};
// Read a file and returns a promise with the content as a buffer
FS.prototype.read = function(filename) {
// To implement for each fs
};
// Read stat infos about a file
FS.prototype.stat = function(filename) {
// To implement for each fs
};
// List files/directories in a directory
FS.prototype.readdir = function(folder) {
// To implement for each fs
};
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
// For optmization, it can be redefined:
// List files in a directory
FS.prototype.listFiles = function(folder) {
return this.readdir(folder)
.then(function(files) {
return _.reject(files, pathIsFolder);
});
};
// List all files in the fs
FS.prototype.listAllFiles = function(folder) {
var that = this;
return this.readdir(folder)
.then(function(files) {
return _.reduce(files, function(prev, file) {
return prev.then(function(output) {
var isDirectory = pathIsFolder(file);
if (!isDirectory) {
output.push(file);
return output;
} else {
return that.listAllFiles(path.join(folder, file))
.then(function(files) {
return output.concat(_.map(files, function(_file) {
return path.join(file, _file);
}));
});
}
});
}, Promise([]));
});
};
// Read a file as a string (utf-8)
FS.prototype.readAsString = function(filename) {
return this.read(filename)
.then(function(buf) {
return buf.toString('utf-8');
});
};
// Find a file in a folder (case incensitive)
// Return the real filename
FS.prototype.findFile = function findFile(root, filename) {
return this.listFiles(root)
.then(function(files) {
return _.find(files, function(file) {
return (file.toLowerCase() == filename.toLowerCase());
});
});
};
// Load a JSON file
// By default, fs only supports JSON
FS.prototype.loadAsObject = function(filename) {
return this.readAsString(filename)
.then(function(str) {
return JSON.parse(str);
});
};
module.exports = FS;
+25 -51
View File
@@ -1,36 +1,15 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
var Immutable = require('immutable');
var fs = require('../utils/fs');
var Promise = require('../utils/promise');
var BaseFS = require('./');
var FS = require('../models/fs');
function NodeFS() {
BaseFS.call(this);
}
util.inherits(NodeFS, BaseFS);
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
NodeFS.prototype.exists = function(filename) {
return fs.exists(filename);
};
// Read a file and returns a promise with the content as a buffer
NodeFS.prototype.read = function(filename) {
return fs.readFile(filename);
};
// Read stat infos about a file
NodeFS.prototype.stat = function(filename) {
return fs.stat(filename);
};
// List files in a directory
NodeFS.prototype.readdir = function(folder) {
function fsReadDir(folder) {
return fs.readdir(folder)
.then(function(files) {
return _.chain(files)
files = Immutable.List(files);
return files
.map(function(file) {
if (file == '.' || file == '..') return;
@@ -38,29 +17,24 @@ NodeFS.prototype.readdir = function(folder) {
if (stat.isDirectory()) file = file + path.sep;
return file;
})
.compact()
.value();
.filter(function(file) {
return Boolean(file);
});
});
}
function fsLoadObject(filename) {
return require(filename);
}
module.exports = function createNodeFS(root) {
return FS.create({
root: root,
fsExists: fs.exists,
fsReadFile: fs.readFile,
fsStatFile: fs.stat,
fsReadDir: fsReadDir,
fsLoadObject: fsLoadObject
});
};
// Load a JSON/JS file
NodeFS.prototype.loadAsObject = function(filename) {
return Promise()
.then(function() {
var jsFile;
try {
jsFile = require.resolve(filename);
// Invalidate node.js cache for livreloading
delete require.cache[jsFile];
return require(jsFile);
}
catch(err) {
return Promise.reject(err);
}
});
};
module.exports = NodeFS;
+7 -2
View File
@@ -6,8 +6,13 @@ var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
var START_TIME = new Date();
// Verify that this gitbook version satisfies a requirement
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
/**
Verify that this gitbook version satisfies a requirement
We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
@param {String} condition
@return {Boolean}
*/
function satisfies(condition) {
// Test with real version
if (semver.satisfies(VERSION, condition)) return true;
+3 -3
View File
@@ -1,7 +1,7 @@
var Book = require('./book');
var cli = require('./cli');
var Book = require('./models/book');
var Parse = require('./parse');
module.exports = {
Book: Book,
commands: cli.commands
Parse: Parse
};
+29
View File
@@ -0,0 +1,29 @@
jest.autoMockOff();
describe('Plugin', function() {
var Plugin = require('../plugin');
describe('createFromString', function() {
it('must parse name', function() {
var plugin = Plugin.createFromString('hello');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('*');
});
it('must parse version', function() {
var plugin = Plugin.createFromString('hello@1.0.0');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('1.0.0');
});
});
describe('isLoaded', function() {
it('must return false for empty plugin', function() {
var plugin = Plugin.createFromString('hello');
expect(plugin.isLoaded()).toBe(false);
});
});
});
+146
View File
@@ -0,0 +1,146 @@
var path = require('path');
var Immutable = require('immutable');
var Ignore = require('ignore');
var Logger = require('../utils/logger');
var FS = require('./fs');
var Config = require('./config');
var Readme = require('./readme');
var Summary = require('./summary');
var Glossary = require('./glossary');
var Languages = require('./languages');
var Book = Immutable.Record({
// Logger for outptu message
logger: Logger(),
// Filesystem binded to the book scope to read files/directories
fs: FS(),
// Ignore files parser
ignore: Ignore(),
// Structure files
config: Config(),
readme: Readme(),
summary: Summary(),
glossary: Glossary(),
languages: Languages()
});
Book.prototype.getLogger = function() {
return this.get('logger');
};
Book.prototype.getFS = function() {
return this.get('fs');
};
Book.prototype.getIgnore = function() {
return this.get('ignore');
};
Book.prototype.getConfig = function() {
return this.get('config');
};
Book.prototype.getReadme = function() {
return this.get('readme');
};
Book.prototype.getSummary = function() {
return this.get('summary');
};
Book.prototype.getGlossary = function() {
return this.get('glossary');
};
Book.prototype.getLanguages = function() {
return this.get('languages');
};
Book.prototype.getPages = function() {
return this.get('pages');
};
/**
Return FS instance to access the content
@return {FS}
*/
Book.prototype.getContentFS = function() {
var fs = this.getFS();
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
return FS.reduceScope(fs, rootFolder);
}
return fs;
};
/**
Return root of the book
@return {String}
*/
Book.prototype.getRoot = function() {
var fs = this.getFS();
return fs.getRoot();
};
/**
Check if a file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isFileIgnored = function(filename) {
var ignore = this.getIgnore();
return ignore.filter([filename]).length == 0;
};
/**
Check if a content file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isContentFileIgnored = function(filename) {
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
filename = path.join(rootFolder, filename);
}
return this.isFileIgnored(filename);
};
/**
Return a page from a book by its path
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.getPage = function(ref) {
return this.getPages().get(ref);
};
/**
Create a book using a filesystem
@param {FS} fs
@return {Book}
*/
Book.createForFS = function createForFS(fs) {
return new Book({
fs: fs
});
};
module.exports = Book;
+47
View File
@@ -0,0 +1,47 @@
var is = require('is');
var Immutable = require('immutable');
var File = require('./file');
var Config = Immutable.Record({
file: File(),
values: Immutable.Map()
});
Config.prototype.getPath = function() {
return this.get('path');
};
Config.prototype.getValues = function() {
return this.get('values');
};
/**
Return a configuration value by its key path
@param {String} key
@return {Mixed}
*/
Config.prototype.getValue = function(keyPath, def) {
var values = this.getValues();
if (is.string(keyPath)) keyPath = keyPath.split('.');
return values.getIn(keyPath) || def;
};
/**
Create a new config, throw error if invalid
@param {File} file
@param {Object} values
@returns {Config}
*/
Config.create = function(file, values) {
return new Config({
file: file,
values: Immutable.fromJS(values)
});
};
module.exports = Config;
+55
View File
@@ -0,0 +1,55 @@
var path = require('path');
var Immutable = require('immutable');
var parsers = require('../parsers');
var File = Immutable.Record({
// Path of the file, relative to the FS
path: String(),
// Time when file data last modified
mtime: Date()
});
File.prototype.getPath = function() {
return this.get('path');
};
File.prototype.getMTime = function() {
return this.get('mtime');
};
/**
Return extension of this file (lowercased)
@return {String}
*/
File.prototype.getExtension = function() {
return path.extname(this.getPath()).toLowerCase();
};
/**
Return parser for this file
@return {Parser}
*/
File.prototype.getParser = function() {
return parsers.getByExt(this.getExtension());
};
/**
Create a file from stats informations
@param {String} filepath
@param {Object|fs.Stats} stat
@return {File}
*/
File.createFromStat = function createFromStat(filepath, stat) {
return new File({
path: filepath,
mtime: stat.mtime
});
};
module.exports = File;
+277
View File
@@ -0,0 +1,277 @@
var path = require('path');
var Immutable = require('immutable');
var File = require('./file');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var PathUtil = require('../utils/path');
var FS = Immutable.Record({
root: String(),
fsExists: Function(),
fsReadFile: Function(),
fsStatFile: Function(),
fsReadDir: Function(),
fsLoadObject: null
});
/**
Return path to the root
@return {String}
*/
FS.prototype.getRoot = function() {
return this.get('root');
};
/**
Verify that a file is in the fs scope
@param {String} filename
@return {Boolean}
*/
FS.prototype.isInScope = function(filename) {
var rootPath = this.getRoot();
filename = path.resolve(rootPath, filename);
return PathUtil.isInRoot(rootPath, filename);
};
/**
Resolve a file in this FS
@param {String}
@return {String}
*/
FS.prototype.resolve = function() {
var rootPath = this.getRoot();
var args = Array.prototype.slice.call(arguments);
var filename = path.resolve.apply(path, [rootPath].concat(args));
if (!this.isInScope(filename)) {
throw error.FileOutOfScopeError({
filename: filename,
root: this.root
});
}
return filename;
};
/**
Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
@param {String} filename
@return {Promise<Boolean>}
*/
FS.prototype.exists = function(filename) {
var that = this;
return Promise()
.then(function() {
filename = that.resolve(filename);
var exists = that.get('fsExists');
return exists(filename);
});
};
/**
Read a file and returns a promise with the content as a buffer
@param {String} filename
@return {Promise<Buffer>}
*/
FS.prototype.read = function(filename) {
var that = this;
return Promise()
.then(function() {
filename = that.resolve(filename);
var read = that.get('fsReadFile');
return read(filename);
});
};
/**
Read a file as a string (utf-8)
@param {String} filename
@return {Promise<String>}
*/
FS.prototype.readAsString = function(filename, encoding) {
encoding = encoding || 'utf8';
return this.read(filename)
.then(function(buf) {
return buf.toString(encoding);
});
};
/**
Read stat infos about a file
@param {String} filename
@return {Promise<File>}
*/
FS.prototype.statFile = function(filename) {
var that = this;
return Promise()
.then(function() {
var filepath = that.resolve(filename);
var stat = that.get('fsStatFile');
return stat(filepath);
})
.then(function(stat) {
return File.createFromStat(filename, stat);
});
};
/**
List files/directories in a directory.
Directories ends with '/'
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.readDir = function(dirname) {
var that = this;
return Promise()
.then(function() {
var dirpath = that.resolve(dirname);
var readDir = that.get('fsReadDir');
return readDir(dirpath);
})
.then(function(files) {
return Immutable.List(files);
});
};
/**
List only files in a diretcory
Directories ends with '/'
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.listFiles = function(dirname) {
return this.readDir(dirname)
.then(function(files) {
return files.filterNot(pathIsFolder);
});
};
/**
List all files in a directory
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.listAllFiles = function(folder) {
var that = this;
folder = folder || '.';
return this.readDir(folder)
.then(function(files) {
return Promise.reduce(files, function(out, file) {
var isDirectory = pathIsFolder(file);
if (!isDirectory) {
return out.push(file);
}
return that.listAllFiles(path.join(folder, file))
.then(function(inner) {
return out.concat(inner);
});
}, Immutable.List());
})
.then(function(files) {
return files.map(function(file) {
return path.join(folder, file);
});
});
};
/**
Find a file in a folder (case incensitive)
Return the found filename
@param {String} dirname
@param {String} filename
@return {Promise<String>}
*/
FS.prototype.findFile = function(dirname, filename) {
return this.listFiles(dirname)
.then(function(files) {
return files.find(function(file) {
return (file.toLowerCase() == filename.toLowerCase());
});
});
};
/**
Load a JSON file
By default, fs only supports JSON
@param {String} filename
@return {Promise<Object>}
*/
FS.prototype.loadAsObject = function(filename) {
var that = this;
var fsLoadObject = this.get('fsLoadObject');
return this.exists(filename)
.then(function(exists) {
if (!exists) {
var err = new Error('Module doesn\'t exist');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
if (fsLoadObject) {
return fsLoadObject(that.resolve(filename));
} else {
return that.readAsString(filename)
.then(function(str) {
return JSON.parse(str);
});
}
});
};
/**
Create a FS instance
@param {Object} def
@return {FS}
*/
FS.create = function create(def) {
return new FS(def);
};
/**
Create a new FS instance with a reduced scope
@param {FS} fs
@param {String} scope
@return {FS}
*/
FS.reduceScope = function reduceScope(fs, scope) {
return fs.set('root', path.join(fs.getRoot(), scope));
};
// .readdir return files/folder as a list of string, folder ending with '/'
function pathIsFolder(filename) {
var lastChar = filename[filename.length - 1];
return lastChar == '/' || lastChar == '\\';
}
module.exports = FS;
+13
View File
@@ -0,0 +1,13 @@
var Immutable = require('immutable');
var Generator = Immutable.Record({
name: String()
});
Generator.create = function(def) {
return new Generator(def);
};
module.exports = Generator;
+86
View File
@@ -0,0 +1,86 @@
var Immutable = require('immutable');
var File = require('./file');
var GlossaryEntry = require('./glossaryEntry');
var Glossary = Immutable.Record({
file: File(),
entries: Immutable.OrderedMap()
});
Glossary.prototype.getFile = function() {
return this.get('file');
};
Glossary.prototype.getEntries = function() {
return this.get('entries');
};
/**
Return an entry by its name
@param {String} name
@return {GlossaryEntry}
*/
Glossary.prototype.getEntry = function(name) {
var entries = this.getEntries();
var id = GlossaryEntry.nameToID(name);
return entries.get(id);
};
/**
Add/Replace an entry to a glossary
@param {Glossary} glossary
@param {GlossaryEntry} entry
@return {Glossary}
*/
Glossary.addEntry = function addEntry(glossary, entry) {
var id = entry.getID();
var entries = glossary.getEntries();
entries = entries.set(id, entry);
return glossary.set('entries', entries);
};
/**
Add/Replace an entry to a glossary by name/description
@param {Glossary} glossary
@param {GlossaryEntry} entry
@return {Glossary}
*/
Glossary.addEntryByName = function addEntryByName(glossary, name, description) {
var entry = new GlossaryEntry({
name: name,
description: description
});
return Glossary.addEntry(glossary, entry);
};
/**
Create a glossary from a list of entries
@param {String} filename
@param {Array|List} entries
@return {Glossary}
*/
Glossary.createFromEntries = function createFromEntries(file, entries) {
entries = entries.map(function(entry) {
if (!(entry instanceof GlossaryEntry)) {
entry = new GlossaryEntry(entry);
}
return [entry.getID(), entry];
});
return new Glossary({
file: file,
entries: Immutable.OrderedMap(entries)
});
};
module.exports = Glossary;
+45
View File
@@ -0,0 +1,45 @@
var Immutable = require('immutable');
/*
A definition represents an entry in the glossary
*/
var GlossaryEntry = Immutable.Record({
name: String(),
description: String()
});
GlossaryEntry.prototype.getName = function() {
return this.get('name');
};
GlossaryEntry.prototype.getDescription = function() {
return this.get('description');
};
/**
Get identifier for this entry
@retrun {Boolean}
*/
GlossaryEntry.prototype.getID = function() {
return GlossaryEntry.nameToID(this.getName());
};
/**
Normalize a glossary entry name into a unique id
@param {String}
@return {String}
*/
GlossaryEntry.nameToID = function nameToID(name) {
return name.toLowerCase()
.replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
.replace(/ /g, '_')
.trim();
};
module.exports = GlossaryEntry;
+21
View File
@@ -0,0 +1,21 @@
var path = require('path');
var Immutable = require('immutable');
var Language = Immutable.Record({
title: String(),
path: String()
});
Language.prototype.getTitle = function() {
return this.get('title');
};
Language.prototype.getPath = function() {
return this.get('path');
};
Language.prototype.getID = function() {
return path.basename(this.getPath());
};
module.exports = Language;
+37
View File
@@ -0,0 +1,37 @@
var Immutable = require('immutable');
var File = require('./file');
var Languages = Immutable.Record({
file: File(),
list: Immutable.OrderedMap()
});
Languages.prototype.getFile = function() {
return this.get('file');
};
Languages.prototype.getList = function() {
return this.get('list');
};
/**
Get default languages
@return {Language}
*/
Languages.prototype.getDefaultLanguage = function() {
return this.getList().first();
};
/**
Get a language by its ID
@param {String} lang
@return {Language}
*/
Languages.prototype.getLanguage = function(lang) {
return this.getList().get(lang);
};
module.exports = Languages;
+39
View File
@@ -0,0 +1,39 @@
var Immutable = require('immutable');
var File = require('./file');
var Page = Immutable.Record({
file: File(),
// Attributes extracted from the YAML header
attributes: Immutable.Map(),
// Content of the page
content: String()
});
Page.prototype.getFile = function() {
return this.get('file');
};
Page.prototype.getAttributes = function() {
return this.get('attributes');
};
Page.prototype.getContent = function() {
return this.get('content');
};
/**
Create a page for a file
@param {File} file
@return {Page}
*/
Page.createForFile = function(file) {
return new Page({
file: file
});
};
module.exports = Page;
+84
View File
@@ -0,0 +1,84 @@
var Immutable = require('immutable');
var PREFIX = require('../constants/pluginPrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({
name: String(),
// Requirement version (ex: ">1.0.0")
version: String(DEFAULT_VERSION),
// Path to load this plugin
path: String(),
// Depth of this plugin in the dependency tree
depth: Number(0),
// Content of the "package.json"
package: Immutable.Map(),
// Content of the package itself
content: Immutable.Map()
}, 'Plugin');
Plugin.prototype.getName = function() {
return this.get('name');
};
Plugin.prototype.getPath = function() {
return this.get('path');
};
Plugin.prototype.getVersion = function() {
return this.get('version');
};
Plugin.prototype.getPackage = function() {
return this.get('package');
};
Plugin.prototype.getContent = function() {
return this.get('content');
};
Plugin.prototype.getDepth = function() {
return this.get('depth');
};
/**
Return the ID on NPM for this plugin
@return {String}
*/
Plugin.prototype.getNpmID = function() {
return PREFIX + this.getName();
};
/**
Check if a plugin is loaded
@return {Boolean}
*/
Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0 && this.getContent().size > 0);
};
/**
Create a plugin from a string
@param {String}
@return {Plugin}
*/
Plugin.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
return new Plugin({
name: name,
version: version || DEFAULT_VERSION
});
};
module.exports = Plugin;
+30
View File
@@ -0,0 +1,30 @@
var Immutable = require('immutable');
var File = require('./file');
var Readme = Immutable.Record({
file: File(),
title: String(),
description: String()
});
Readme.prototype.getFile = function() {
return this.get('file');
};
/**
Create a new readme
@param {File} file
@param {Object} def
@return {Readme}
*/
Readme.create = function(file, def) {
return new Readme({
file: file,
title: def.title,
description: def.description
});
};
module.exports = Readme;
+41
View File
@@ -0,0 +1,41 @@
var Immutable = require('immutable');
var File = require('./file');
var SummaryPart = require('./summaryPart');
var Summary = Immutable.Record({
file: File(),
parts: Immutable.List()
});
Summary.prototype.getFile = function() {
return this.get('file');
};
Summary.prototype.getParts = function() {
return this.get('parts');
};
/**
Create a new summary for a list of parts
@param {Lust|Array} parts
@return {Summary}
*/
Summary.createFromParts = function createFromParts(file, parts) {
parts = parts.map(function(part) {
if (part instanceof SummaryPart) {
return part;
}
return SummaryPart.create(part);
});
return new Summary({
file: file,
parts: new Immutable.List(parts)
});
};
module.exports = Summary;
+100
View File
@@ -0,0 +1,100 @@
var Immutable = require('immutable');
var location = require('../utils/location');
/*
An article represents an entry in the Summary / table of Contents
*/
var SummaryArticle = Immutable.Record({
level: String(),
title: String(),
ref: String(),
articles: Immutable.List()
});
SummaryArticle.prototype.getLevel = function() {
return this.get('level');
};
SummaryArticle.prototype.getTitle = function() {
return this.get('title');
};
SummaryArticle.prototype.getRef = function() {
return this.get('ref');
};
SummaryArticle.prototype.getArticles = function() {
return this.get('articles');
};
/**
Get path (without anchor) to the pointing file
@return {String}
*/
SummaryArticle.prototype.getPath = function() {
var ref = this.getRef();
var parts = ref.split('#');
var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref);
// Normalize path to remove ('./', etc)
return location.normalize(pathname);
};
/**
Get anchor for this article (or undefined)
@return {String}
*/
SummaryArticle.prototype.getAnchor = function() {
var ref = this.getRef();
var parts = ref.split('#');
var anchor = (parts.length > 1? '#' + parts[parts.length - 1] : null);
return anchor;
};
/**
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}
*/
SummaryArticle.prototype.isExternal = function() {
return location.isExternal(this.getRef());
};
/**
Create a SummaryArticle
@param {Object} def
@return {SummaryArticle}
*/
SummaryArticle.create = function(def) {
var articles = (def.articles || []).map(function(article) {
if (article instanceof SummaryArticle) {
return article;
}
return SummaryArticle.create(article);
});
return new SummaryArticle({
title: def.title,
ref: def.ref || def.path,
articles: Immutable.List(articles)
});
};
module.exports = SummaryArticle;
+42
View File
@@ -0,0 +1,42 @@
var Immutable = require('immutable');
var SummaryArticle = require('./summaryArticle');
/*
A part represents a section in the Summary / table of Contents
*/
var SummaryPart = Immutable.Record({
title: String(),
articles: Immutable.List()
});
SummaryPart.prototype.getTitle = function() {
return this.get('title');
};
SummaryPart.prototype.getArticles = function() {
return this.get('articles');
};
/**
Create a SummaryPart
@param {Object} def
@return {SummaryPart}
*/
SummaryPart.create = function(def) {
var articles = (def.articles || []).map(function(article) {
if (article instanceof SummaryArticle) {
return article;
}
return SummaryArticle.create(article);
});
return new SummaryPart({
title: def.title,
articles: Immutable.List(articles)
});
};
module.exports = SummaryPart;
+42
View File
@@ -0,0 +1,42 @@
var Parse = require('../parse');
/**
List all assets for a book
@param {Book} book
@param {Map<String:Page>} pages
@param
*/
function listAssets(book, pages) {
var fs = book.getContentFS();
return fs.listAllFiles()
.then(function(files) {
return files.filterNot(function(file) {
return (
book.isContentFileIgnored(file) ||
pages.has(file)
);
});
});
}
/**
Generate a book using a generator
@param {Generator} generator
@param {Book} book
@return {Promise}
*/
function generateBook(generator, book) {
// List all parsable pages
return Parse.parsePagesList(book)
.then(function(pages) {
return listAssets(book, pages);
});
}
module.exports = generateBook;
+9
View File
@@ -0,0 +1,9 @@
/**
Generate a page using a generator
*/
function generatePage(generator, book, page) {
}
module.exports = generatePage;
+26
View File
@@ -0,0 +1,26 @@
var Promise = require('../../utils/promise');
var Modifier = require('../');
function JSONGenerator(book) {
this.book = book;
}
JSONGenerator.prototype.onPage = function(page) {
return Modifier.HTMLTransformations(page, [
Modifier.svgToImg(),
Modifier.svgToPng()
])
.then(function() {
});
};
JSONGenerator.prototype.onAsset = function(file) {
};
module.exports = JSONGenerator;
+11
View File
@@ -0,0 +1,11 @@
function generate(book, output) {
}
module.exports = {
generate: generate
};
+9
View File
@@ -0,0 +1,9 @@
var slug = require('github-slugid');
var HTMLModifier = require('./html');
var addHeadingID = HTMLModifier('h1,h2,h3,h4,h5,h6', function(heading) {
if (heading.attr('id')) return;
heading.attr('id', slug(heading.text()));
});
module.exports = addHeadingID;
+16
View File
@@ -0,0 +1,16 @@
var Promise = require('../../utils/promise');
/**
*/
function transformTags() {
var $elements = $(query);
return Promise.serie($elements, function(el) {
var $el = that.$(el);
return fn.call(that, $el);
});
}
module.exports = transformTags;
+9
View File
@@ -0,0 +1,9 @@
function modifyPage() {
}
module.exports = modifyPage;
+11
View File
@@ -0,0 +1,11 @@
/**
*/
function inlineAssets() {
}
module.exports = inlineAssets;
+28
View File
@@ -0,0 +1,28 @@
var cheerio = require('cheerio');
var domSerializer = require('dom-serializer');
// Render a cheerio DOM as html
function renderDOM($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
}
/**
*/
var svgToImg = HTMLModifier('svg', function($svg, $) {
var content = '<?xml version="1.0" encoding="UTF-8"?>' + renderDOM($, $svg);
});
function svgToImg(page) {
var $ = cheerio.load(page.content);
}
module.exports = svgToImg;
+36
View File
@@ -0,0 +1,36 @@
var path = require('path');
var Promise = require('../utils/promise');
var parsers = require('../parsers');
/**
Find a file parsable (Markdown or AsciiDoc) in a book
@param {Book} book
@param {String} filename
@return {Promise<>}
*/
function findParsableFile(book, filename) {
var fs = book.getContentFS();
var ext = path.extname(filename);
var basename = path.basename(filename, ext);
var basedir = path.dirname(filename);
// Ordered list of extensions to test
var exts = parsers.extensions;
return Promise.some(exts, function(ext) {
var filepath = basename + ext;
return fs.findFile(basedir, filepath)
.then(function(found) {
if (!found || book.isContentFileIgnored(found)) {
return undefined;
}
return fs.statFile(found);
});
});
}
module.exports = findParsableFile;
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
parseBook: require('./parseBook'),
parseSummary: require('./parseSummary'),
parseGlossary: require('./parseGlossary'),
parseReadme: require('./parseReadme'),
parseConfig: require('./parseConfig'),
parsePagesList: require('./parsePagesList'),
parseIgnore: require('./parseIgnore')
};
+26
View File
@@ -0,0 +1,26 @@
var Promise = require('../utils/promise');
var parseIgnore = require('./parseIgnore');
var parseConfig = require('./parseConfig');
var parseGlossary = require('./parseGlossary');
var parseSummary = require('./parseSummary');
var parseReadme = require('./parseReadme');
//var parseLanguages = require('./parseLanguages');
/**
Parse a whole book from a filesystem
@param {Book} book
@return {Promise<Book>}
*/
function parseBook(book) {
return Promise(book)
.then(parseIgnore)
.then(parseConfig)
//.then(parseLanguages)
.then(parseReadme)
.then(parseSummary)
.then(parseGlossary);
}
module.exports = parseBook;
+51
View File
@@ -0,0 +1,51 @@
var Promise = require('../utils/promise');
var Config = require('../models/config');
var File = require('../models/file');
var validateConfig = require('./validateConfig');
var CONFIG_FILES = require('../constants/configFiles');
/**
Parse configuration from "book.json" or "book.js"
@param {Book} book
@return {Promise<Book>}
*/
function parseConfig(book) {
var fs = book.getFS();
return Promise.some(CONFIG_FILES, function(filename) {
// Is this file ignored?
if (book.isFileIgnored(filename)) {
return;
}
// Try loading it
return Promise.all([
fs.loadAsObject(filename),
fs.statFile(filename)
])
.spread(function(cfg, file) {
return {
file: file,
values: cfg
};
})
.fail(function(err) {
if (err.code != 'MODULE_NOT_FOUND') throw(err);
else return Promise(false);
});
})
.then(function(result) {
var file = result? result.file : File();
var values = result? result.values : {};
values = validateConfig(values);
var config = Config.create(file, values);
return book.set('config', config);
});
}
module.exports = parseConfig;
+22
View File
@@ -0,0 +1,22 @@
var parseStructureFile = require('./parseStructureFile');
var Glossary = require('../models/glossary');
/**
Parse glossary
@param {Book} book
@return {Promise<Book>}
*/
function parseGlossary(book) {
return parseStructureFile(book, 'glossary')
.spread(function(file, entries) {
if (!file) {
return book;
}
var glossary = Glossary.createFromEntries(file, entries);
return book.set('glossary', glossary);
});
}
module.exports = parseGlossary;
+43
View File
@@ -0,0 +1,43 @@
var Promise = require('../utils/promise');
var IGNORE_FILES = require('../constants/ignoreFiles');
/**
Parse ignore files
@param {Book}
@return {Book}
*/
function parseIgnore(book) {
var fs = book.getFS();
var ignore = book.getIgnore();
ignore.addPattern([
// Skip Git stuff
'.git/',
// Skip OS X meta data
'.DS_Store',
// Skip stuff installed by plugins
'node_modules',
// Skip book outputs
'_book',
'*.pdf',
'*.epub',
'*.mobi'
]);
return Promise.serie(IGNORE_FILES, function(filename) {
return fs.readAsString(filename)
.then(function(content) {
ignore.addPattern(content.toString().split(/\r?\n/));
}, function() {
return Promise();
});
})
.thenResolve(book);
}
module.exports = parseIgnore;
+26
View File
@@ -0,0 +1,26 @@
var fm = require('front-matter');
/**
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);
page = page.set('content', parsed.body);
page = page.set('attributes', parsed.attributes);
return page;
});
}
module.exports = parsePage;
+39
View File
@@ -0,0 +1,39 @@
var Immutable = require('immutable');
var Page = require('../models/page');
var walkSummary = require('./walkSummary');
/**
Parse all pages from a book as an OrderedMap
@param {Book} book
@return {Promise<OrderedMap<Page>>}
*/
function parsePagesList(book) {
var fs = book.getContentFS();
var summary = book.getSummary();
var map = Immutable.OrderedMap();
return walkSummary(summary, function(article) {
if (!article.isPage()) return;
var filepath = article.getPath();
// Is the page ignored?
if (book.isContentFileIgnored(filepath)) return;
return fs.statFile(filepath)
.then(function(file) {
map = map.set(
filepath,
Page.createForFile(file)
);
});
})
.then(function() {
return map;
});
}
module.exports = parsePagesList;
+24
View File
@@ -0,0 +1,24 @@
var parseStructureFile = require('./parseStructureFile');
var Readme = require('../models/readme');
var error = require('../utils/error');
/**
Parse readme from book
@param {Book} book
@return {Promise<Book>}
*/
function parseReadme(book) {
return parseStructureFile(book, 'readme')
.spread(function(file, result) {
if (!file) {
throw new error.FileNotFoundError({ filename: 'README' });
}
var readme = Readme.create(file, result);
return book.set('readme', readme);
});
}
module.exports = parseReadme;
+57
View File
@@ -0,0 +1,57 @@
var findParsableFile = require('./findParsableFile');
var Promise = require('../utils/promise');
var error = require('../utils/error');
/**
Parse a ParsableFile using a specific method
@param {FS} fs
@param {ParsableFile} file
@param {String} type
@return {Promise<Array<String, List|Map>>}
*/
function parseFile(fs, file, type) {
var filepath = file.getPath();
var parser = file.getParser();
if (!parser) {
return Promise.reject(
error.FileNotParsableError({
filename: filepath
})
);
}
return fs.readAsString(filepath)
.then(function(content) {
return [
file,
parser[type](content)
];
});
}
/**
Parse a structure file (ex: SUMMARY.md, GLOSSARY.md).
It uses the configuration to find the specified file.
@param {Book} book
@param {String} type: one of ["glossary", "readme", "summary"]
@return {Promise<List|Map>}
*/
function parseStructureFile(book, type) {
var fs = book.getContentFS();
var config = book.getConfig();
var fileToSearch = config.getValue(['structure', type]);
return findParsableFile(book, fileToSearch)
.then(function(file) {
if (!file) return [undefined, undefined];
return parseFile(fs, file, type);
});
}
module.exports = parseStructureFile;
+22
View File
@@ -0,0 +1,22 @@
var parseStructureFile = require('./parseStructureFile');
var Summary = require('../models/summary');
/**
Parse summary in a book
@param {Book} book
@return {Promise<Book>}
*/
function parseSummary(book) {
return parseStructureFile(book, 'summary')
.spread(function(file, result) {
if (!file) {
return book;
}
var summary = Summary.createFromParts(file, result.parts);
return book.set('summary', summary);
});
}
module.exports = parseSummary;
@@ -2,12 +2,17 @@ var jsonschema = require('jsonschema');
var jsonSchemaDefaults = require('json-schema-defaults');
var mergeDefaults = require('merge-defaults');
var schema = require('./schema');
var schema = require('../constants/configSchema');
var error = require('../utils/error');
// Validate a book.json content
// And return a mix with the default value
function validate(bookJson) {
/**
Validate a book.json content
And return a mix with the default value
@param {Object} bookJson
@return {Object}
*/
function validateConfig(bookJson) {
var v = new jsonschema.Validator();
var result = v.validate(bookJson, schema, {
propertyName: 'config'
@@ -23,6 +28,4 @@ function validate(bookJson) {
return mergeDefaults(bookJson, defaults);
}
module.exports = {
validate: validate
};
module.exports = validateConfig;
+34
View File
@@ -0,0 +1,34 @@
var Promise = require('../utils/promise');
/**
Walk over a list of articles
@param {List<Article>} articles
@param {Function(article)}
@return {Promise}
*/
function walkArticles(articles, fn) {
return Promise.forEach(articles, function(article) {
return Promise(fn(article))
.then(function() {
return walkArticles(article.getArticles(), fn);
});
});
}
/**
Walk over summary and execute "fn" on each article
@param {Summary} summary
@param {Function(article)}
@return {Promise}
*/
function walkSummary(summary, fn) {
var parts = summary.getParts();
return Promise.forEach(parts, function(part) {
return walkArticles(part.getArticles(), fn);
});
}
module.exports = walkSummary;
+18
View File
@@ -0,0 +1,18 @@
jest.autoMockOff();
var path = require('path');
describe('findInstalled', function() {
var findInstalled = require('../findInstalled');
pit('must list default plugins for gitbook directory', function() {
return findInstalled(path.resolve(__dirname, '../../../'))
.then(function(plugins) {
expect(plugins.size).toBe(7);
expect(plugins.has('fontsettings')).toBe(true);
expect(plugins.has('search')).toBe(true);
});
});
});
+54
View File
@@ -0,0 +1,54 @@
jest.autoMockOff();
describe('listAll', function() {
var listAll = require('../listAll');
it('must list from string', function() {
var plugins = listAll('ga,great');
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', function() {
var plugins = listAll(['ga', 'great']);
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 parse version (semver)', function() {
var plugins = listAll(['ga@1.0.0', 'great@>=4.0.0']);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
var ga = plugins.get('ga');
expect(ga.getVersion()).toBe('1.0.0');
var great = plugins.get('great');
expect(great.getVersion()).toBe('>=4.0.0');
});
it('must parse version (git)', function() {
var plugins = listAll(['ga@git+https://github.com/GitbookIO/plugin-ga.git', 'great@git+ssh://samy@github.com/GitbookIO/plugin-ga.git']);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
var ga = plugins.get('ga');
expect(ga.getVersion()).toBe('git+https://github.com/GitbookIO/plugin-ga.git');
var great = plugins.get('great');
expect(great.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
+21
View File
@@ -0,0 +1,21 @@
jest.autoMockOff();
var Promise = require('../../utils/promise');
var Plugin = require('../../models/plugin');
describe('validatePlugin', function() {
var validatePlugin = require('../validatePlugin');
pit('must not validate a not loaded plugin', function() {
var plugin = Plugin.createFromString('test');
return validatePlugin(plugin)
.then(function() {
throw new Error('Should not be validate');
}, function(err) {
return Promise();
});
});
});
+27
View File
@@ -0,0 +1,27 @@
var path = require('path');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var findInstalled = require('./findInstalled');
/**
List all plugins installed in a book
@param {Book}
@return {Promise<OrderedMap<String:Plugin>>}
*/
function findForBook(book) {
return Promise.all([
findInstalled(path.resolve(__dirname, '../..')),
findInstalled(book.getRoot())
])
.then(function(results) {
return Immutable.List(results)
.reduce(function(out, result) {
return out.merge(result);
}, Immutable.OrderedMap());
});
}
module.exports = findForBook;
+66
View File
@@ -0,0 +1,66 @@
var readInstalled = require('read-installed');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var Plugin = require('../models/plugin');
var PREFIX = require('../constants/pluginPrefix');
/**
Validate if a package name is a GitBook plugin
@return {Boolean}
*/
function validateId(name) {
return name && name.indexOf(PREFIX) === 0;
}
/**
List all packages installed inside a folder
@param {String} folder
@return {OrderedMap<String:Plugin>}
*/
function findInstalled(folder) {
var options = {
dev: false,
log: function() {},
depth: 4
};
var results = Immutable.OrderedMap();
function onPackage(pkg, isRoot) {
if (!pkg.name) return;
var name = pkg.name;
var version = pkg.version;
var pkgPath = pkg.realPath;
var depth = pkg.depth;
var dependencies = pkg.dependencies;
var pluginName = name.slice(PREFIX.length);
if (!validateId(name)){
if (!isRoot) return;
} else {
results = results.set(pluginName, Plugin({
name: pluginName,
version: version,
path: pkgPath,
depth: depth
}));
}
Immutable.Map(dependencies).forEach(function(dep) {
onPackage(dep);
});
}
return Promise.nfcall(readInstalled, folder, options)
.then(function(data) {
onPackage(data, true);
return results;
});
}
module.exports = findInstalled;
+1 -183
View File
@@ -1,188 +1,6 @@
var _ = require('lodash');
var path = require('path');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var BookPlugin = require('./plugin');
var registry = require('./registry');
var pluginsConfig = require('../config/plugins');
module.exports = {
/*
PluginsManager is an interface to work with multiple plugins at once:
- Extract assets from plugins
- Call hooks for all plugins, etc
*/
function PluginsManager(book) {
this.book = book;
this.log = this.book.log;
this.plugins = [];
_.bindAll(this);
}
// Returns the list of plugins
PluginsManager.prototype.list = function() {
return this.plugins;
};
// Return count of plugins loaded
PluginsManager.prototype.count = function() {
return _.size(this.plugins);
};
// Returns a plugin by its name
PluginsManager.prototype.get = function(name) {
return _.find(this.plugins, {
id: name
});
};
// Load a plugin (could be a BookPlugin or {name,path})
PluginsManager.prototype.load = function(plugin) {
var that = this;
if (_.isArray(plugin)) {
return Promise.serie(plugin, that.load);
}
return Promise()
// Initiate and load the plugin
.then(function() {
if (!(plugin instanceof BookPlugin)) {
plugin = new BookPlugin(that.book, plugin.name, plugin.path);
}
if (that.get(plugin.id)) {
throw new Error('Plugin "'+plugin.id+'" is already loaded');
}
if (plugin.isLoaded()) return plugin;
else return plugin.load()
.thenResolve(plugin);
})
// Setup the plugin
.then(this._setup);
};
// Load all plugins from the book's configuration
PluginsManager.prototype.loadAll = function() {
var that = this;
var pluginNames = _.pluck(this.book.config.get('plugins'), 'name');
return registry.list(this.book)
.then(function(plugins) {
// Filter out plugins not listed of first level
// (aka pre-installed plugins)
plugins = _.filter(plugins, function(plugin) {
return (
plugin.depth > 1 ||
_.contains(pluginNames, plugin.name)
);
});
// Sort plugins to match list in book.json
plugins.sort(function(a, b){
return pluginNames.indexOf(a.name) < pluginNames.indexOf(b.name) ? -1 : 1;
});
// Log state
that.log.info.ln(_.size(plugins) + ' are installed');
if (_.size(pluginNames) != _.size(plugins)) that.log.info.ln(_.size(pluginNames) + ' explicitly listed');
// Verify that all plugins are present
var notInstalled = _.filter(pluginNames, function(name) {
return !_.find(plugins, { name: name });
});
if (_.size(notInstalled) > 0) {
throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
}
// Load plugins
return that.load(plugins);
});
};
// Setup a plugin
// Register its filter, blocks, etc
PluginsManager.prototype._setup = function(plugin) {
this.plugins.push(plugin);
};
// Install all plugins for the book
PluginsManager.prototype.install = function() {
var that = this;
var plugins = _.filter(this.book.config.get('plugins'), function(plugin) {
return !pluginsConfig.isDefaultPlugin(plugin.name);
});
if (plugins.length == 0) {
this.log.info.ln('nothing to install!');
return Promise(0);
}
this.log.info.ln('installing', plugins.length, 'plugins');
return Promise.serie(plugins, function(plugin) {
return registry.install(that.book, plugin.name, plugin.version);
})
.thenResolve(plugins.length);
};
// Call a hook on all plugins to transform an input
PluginsManager.prototype.hook = function(name, input) {
return Promise.reduce(this.plugins, function(current, plugin) {
return plugin.hook(name, current);
}, input);
};
// Extract all resources for a namespace
PluginsManager.prototype.getResources = function(namespace) {
return Promise.reduce(this.plugins, function(out, plugin) {
return plugin.getResources(namespace)
.then(function(pluginResources) {
_.each(BookPlugin.RESOURCES, function(resourceType) {
out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []);
});
return out;
});
}, {});
};
// Copy all resources for a plugin
PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
return Promise.serie(this.plugins, function(plugin) {
return plugin.getResources(namespace)
.then(function(resources) {
if (!resources.assets) return;
var input = path.resolve(plugin.root, resources.assets);
var output = path.resolve(outputRoot, plugin.npmId);
return fs.copyDir(input, output);
});
});
};
// Get all filters and blocks
PluginsManager.prototype.getFilters = function() {
return _.reduce(this.plugins, function(out, plugin) {
var filters = plugin.getFilters();
return _.extend(out, filters);
}, {});
};
PluginsManager.prototype.getBlocks = function() {
return _.reduce(this.plugins, function(out, plugin) {
var blocks = plugin.getBlocks();
return _.extend(out, blocks);
}, {});
};
module.exports = PluginsManager;
+56
View File
@@ -0,0 +1,56 @@
var is = require('is');
var Immutable = require('immutable');
var Plugin = require('../models/plugin');
var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
/**
List all plugins for a book
@param {List<Plugin|String>}
@return {OrderedMap<Plugin>}
*/
function listAll(plugins) {
if (is.string(plugins)) {
plugins = new Immutable.List(plugins.split(','));
}
// Convert to an ordered map
plugins = plugins.map(function(plugin) {
if (is.string(plugin)) {
plugin = Plugin.createFromString(plugin);
} else {
plugin = new Plugin(plugin);
}
return [plugin.getName(), plugin];
});
plugins = Immutable.OrderedMap(plugins);
// Extract list of plugins to disable (starting with -)
var toRemove = plugins.toList()
.filter(function(plugin) {
return plugin.getName()[0] == '-';
})
.map(function(plugin) {
return plugin.slice(1);
});
// Append default plugins
DEFAULT_PLUGINS.forEach(function(pluginName) {
if (plugins.has(pluginName)) return;
plugins = plugins.set(pluginName, new Plugin({
name: pluginName
}));
});
// Remove plugins
plugins = plugins.filterNot(function(plugin) {
return toRemove.includes(plugin.getName());
});
return plugins;
}
module.exports = listAll;
+18
View File
@@ -0,0 +1,18 @@
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.getValue('plugins');
return listAll(plugins);
}
module.exports = listForBook;
+57
View File
@@ -0,0 +1,57 @@
var Promise = require('../utils/promise');
var listForBook = require('./listForBook');
var listInstalledForBook = require('./listInstalledForBook');
var loadPlugin = require('./loadPlugin');
/**
Load a list of 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.keys().toList();
return listInstalledForBook(book)
.then(function(installed) {
// Filter out plugins not listed of first level
// (aka pre-installed plugins)
installed = installed.filter(function(plugin) {
return (
plugin.getDepth() > 1 ||
requirements.has(plugin.getName())
);
});
// 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;
});
// Log state
logger.info.ln(installed.size + ' are installed');
if (requirements.size != installed.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.');
}
return Promise.map(installed, function(plugin) {
return loadPlugin(plugin);
});
});
}
module.exports = loadForBook;
+89
View File
@@ -0,0 +1,89 @@
var path = require('path');
var resolve = require('resolve');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var validatePlugin = require('./validatePlugin');
// Return true if an error is a "module not found"
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
return err.message.indexOf('Cannot find module') >= 0;
}
/**
Load a plugin in a book
@param {Book} book
@param {Plugin} plugin
@param {String} pkgPath (optional)
@return {Promise<Plugin>}
*/
function loadPlugin(book, plugin) {
var logger = book.getLogger();
var name = plugin.getName();
var pkgPath = plugin.getPath();
// Try loading plugins from different location
var p = Promise()
.then(function() {
var packageContent;
var content;
// Locate plugin and load pacjage.json
try {
var res = resolve.sync('./package.json', { basedir: pkgPath });
pkgPath = path.dirname(res);
packageContent = require(res);
} catch (err) {
if (!isModuleNotFound(err)) throw err;
packageContent = undefined;
content = undefined;
return;
}
// Load plugin JS content
try {
content = require(pkgPath);
} catch(err) {
// It's no big deal if the plugin doesn't have an "index.js"
// (For example: themes)
if (isModuleNotFound(err)) {
content = {};
} else {
throw new error.PluginError(err, {
plugin: name
});
}
}
// Update plugin
return plugin.merge({
'package': packageContent,
'content': content
});
})
.then(validatePlugin)
// Validate the configuration and update it
.then(function() {
var config = that.book.config.get(that.getConfigKey(), {});
return that.validateConfig(config);
})
.then(function(config) {
that.book.config.set(that.getConfigKey(), config);
});
logger.info('loading plugin "' + name + '"... ');
return logger.info.promise(p);
}
module.exports = loadPlugin;
+33
View File
@@ -0,0 +1,33 @@
var gitbook = require('../gitbook');
var Promise = require('../utils/promise');
/**
Validate a plugin
@param {Plugin}
@return {Promise<Plugin>}
*/
function validatePlugin(plugin) {
var packageInfos = plugin.getPackage();
var isValid = (
plugin.isLoaded() &&
packageInfos &&
packageInfos.name &&
packageInfos.engines &&
packageInfos.engines.gitbook
);
if (!isValid) {
return Promise.reject(new Error('Error loading plugin "' + plugin.getName() + '" at "' + plugin.getPath() + '"'));
}
if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
return Promise.reject(new Error('GitBook doesn\'t satisfy the requirements of this plugin: ' + packageInfos.engines.gitbook));
}
return Promise();
}
module.exports = validatePlugin;
+188
View File
@@ -0,0 +1,188 @@
var _ = require('lodash');
var path = require('path');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var BookPlugin = require('./plugin');
var registry = require('./registry');
var pluginsConfig = require('../config/plugins');
/*
PluginsManager is an interface to work with multiple plugins at once:
- Extract assets from plugins
- Call hooks for all plugins, etc
*/
function PluginsManager(book) {
this.book = book;
this.log = this.book.log;
this.plugins = [];
_.bindAll(this);
}
// Returns the list of plugins
PluginsManager.prototype.list = function() {
return this.plugins;
};
// Return count of plugins loaded
PluginsManager.prototype.count = function() {
return _.size(this.plugins);
};
// Returns a plugin by its name
PluginsManager.prototype.get = function(name) {
return _.find(this.plugins, {
id: name
});
};
// Load a plugin (could be a BookPlugin or {name,path})
PluginsManager.prototype.load = function(plugin) {
var that = this;
if (_.isArray(plugin)) {
return Promise.serie(plugin, that.load);
}
return Promise()
// Initiate and load the plugin
.then(function() {
if (!(plugin instanceof BookPlugin)) {
plugin = new BookPlugin(that.book, plugin.name, plugin.path);
}
if (that.get(plugin.id)) {
throw new Error('Plugin "'+plugin.id+'" is already loaded');
}
if (plugin.isLoaded()) return plugin;
else return plugin.load()
.thenResolve(plugin);
})
// Setup the plugin
.then(this._setup);
};
// Load all plugins from the book's configuration
PluginsManager.prototype.loadAll = function() {
var that = this;
var pluginNames = _.pluck(this.book.config.get('plugins'), 'name');
return registry.list(this.book)
.then(function(plugins) {
// Filter out plugins not listed of first level
// (aka pre-installed plugins)
plugins = _.filter(plugins, function(plugin) {
return (
plugin.depth > 1 ||
_.contains(pluginNames, plugin.name)
);
});
// Sort plugins to match list in book.json
plugins.sort(function(a, b){
return pluginNames.indexOf(a.name) < pluginNames.indexOf(b.name) ? -1 : 1;
});
// Log state
that.log.info.ln(_.size(plugins) + ' are installed');
if (_.size(pluginNames) != _.size(plugins)) that.log.info.ln(_.size(pluginNames) + ' explicitly listed');
// Verify that all plugins are present
var notInstalled = _.filter(pluginNames, function(name) {
return !_.find(plugins, { name: name });
});
if (_.size(notInstalled) > 0) {
throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
}
// Load plugins
return that.load(plugins);
});
};
// Setup a plugin
// Register its filter, blocks, etc
PluginsManager.prototype._setup = function(plugin) {
this.plugins.push(plugin);
};
// Install all plugins for the book
PluginsManager.prototype.install = function() {
var that = this;
var plugins = _.filter(this.book.config.get('plugins'), function(plugin) {
return !pluginsConfig.isDefaultPlugin(plugin.name);
});
if (plugins.length == 0) {
this.log.info.ln('nothing to install!');
return Promise(0);
}
this.log.info.ln('installing', plugins.length, 'plugins');
return Promise.serie(plugins, function(plugin) {
return registry.install(that.book, plugin.name, plugin.version);
})
.thenResolve(plugins.length);
};
// Call a hook on all plugins to transform an input
PluginsManager.prototype.hook = function(name, input) {
return Promise.reduce(this.plugins, function(current, plugin) {
return plugin.hook(name, current);
}, input);
};
// Extract all resources for a namespace
PluginsManager.prototype.getResources = function(namespace) {
return Promise.reduce(this.plugins, function(out, plugin) {
return plugin.getResources(namespace)
.then(function(pluginResources) {
_.each(BookPlugin.RESOURCES, function(resourceType) {
out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []);
});
return out;
});
}, {});
};
// Copy all resources for a plugin
PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
return Promise.serie(this.plugins, function(plugin) {
return plugin.getResources(namespace)
.then(function(resources) {
if (!resources.assets) return;
var input = path.resolve(plugin.root, resources.assets);
var output = path.resolve(outputRoot, plugin.npmId);
return fs.copyDir(input, output);
});
});
};
// Get all filters and blocks
PluginsManager.prototype.getFilters = function() {
return _.reduce(this.plugins, function(out, plugin) {
var filters = plugin.getFilters();
return _.extend(out, filters);
}, {});
};
PluginsManager.prototype.getBlocks = function() {
return _.reduce(this.plugins, function(out, plugin) {
var blocks = plugin.getBlocks();
return _.extend(out, blocks);
}, {});
};
module.exports = PluginsManager;
+8
View File
@@ -32,6 +32,13 @@ var FileNotFoundError = TypedError({
filename: null
});
// A file cannot be parsed
var FileNotParsableError = TypedError({
type: 'file.not-parsable',
message: '"{filename}" file cannot be parsed',
filename: null
});
// A file is outside the scope
var FileOutOfScopeError = TypedError({
type: 'file.out-of-scope',
@@ -92,6 +99,7 @@ module.exports = {
OutputError: OutputError,
RequireInstallError: RequireInstallError,
FileNotParsableError: FileNotParsableError,
FileNotFoundError: FileNotFoundError,
FileOutOfScopeError: FileOutOfScopeError,
+60 -12
View File
@@ -1,16 +1,43 @@
var Q = require('q');
var _ = require('lodash');
var Immutable = require('immutable');
// Reduce an array to a promise
/**
Reduce an array to a promise
@param {Array|List} arr
@param {Function(value, element, index)}
@return {Promise<Mixed>}
*/
function reduce(arr, iter, base) {
return _.reduce(arr, function(prev, elem, i) {
arr = Immutable.List(arr);
return arr.reduce(function(prev, elem, i) {
return prev.then(function(val) {
return iter(val, elem, i);
});
}, Q(base));
}
// Transform an array
/**
Iterate over an array using an async iter
@param {Array|List} arr
@param {Function(value, element, index)}
@return {Promise}
*/
function forEach(arr, iter) {
return reduce(arr, function(val, el) {
return iter(el);
});
}
/**
Transform an array
@param {Array|List} arr
@param {Function(value, element, index)}
@return {Promise}
*/
function serie(arr, iter, base) {
return reduce(arr, function(before, item, i) {
return Q(iter(item, i))
@@ -21,9 +48,17 @@ function serie(arr, iter, base) {
}, []);
}
// Iter over an array and return first result (not null)
/**
Iter over an array and return first result (not null)
@param {Array|List} arr
@param {Function(element, index)}
@return {Promise<Mixed>}
*/
function some(arr, iter) {
return _.reduce(arr, function(prev, elem, i) {
arr = Immutable.List(arr);
return arr.reduce(function(prev, elem, i) {
return prev.then(function(val) {
if (val) return val;
@@ -32,7 +67,13 @@ function some(arr, iter) {
}, Q());
}
// Map an array using an async (promised) iterator
/**
Map an array using an async (promised) iterator
@param {Array|List} arr
@param {Function(element, index)}
@return {Promise<List>}
*/
function map(arr, iter) {
return reduce(arr, function(prev, entry, i) {
return Q(iter(entry, i))
@@ -43,18 +84,25 @@ function map(arr, iter) {
}, []);
}
// Wrap a fucntion in a promise
/**
Wrap a fucntion in a promise
@param {Function} func
@return {Funciton}
*/
function wrap(func) {
return _.wrap(func, function(_func) {
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var args = Array.prototype.slice.call(arguments, 0);
return Q()
.then(function() {
return _func.apply(null, args);
return func.apply(null, args);
});
});
};
}
module.exports = Q;
module.exports.forEach = forEach;
module.exports.reduce = reduce;
module.exports.map = map;
module.exports.serie = serie;
+5 -2
View File
@@ -24,14 +24,16 @@
"gitbook-plugin-fontsettings": "1.0.3",
"gitbook-plugin-highlight": "2.0.2",
"gitbook-plugin-livereload": "0.0.1",
"gitbook-plugin-search": "2.2.0",
"gitbook-plugin-lunr": "1.0.0",
"gitbook-plugin-theme-default": "1.0.0-pre.8",
"gitbook-plugin-search": "2.2.0",
"gitbook-plugin-sharing": "1.0.2",
"gitbook-plugin-theme-default": "1.0.0-pre.8",
"github-slugid": "1.0.1",
"graceful-fs": "4.1.3",
"i18n-t": "1.0.0",
"ignore": "3.1.1",
"immutable": "^3.8.1",
"is": "^3.1.0",
"json-schema-defaults": "0.1.1",
"jsonschema": "1.1.0",
"juice": "1.10.0",
@@ -57,6 +59,7 @@
},
"devDependencies": {
"eslint": "2.7.0",
"jest-cli": "^11.0.2",
"mocha": "2.4.5",
"should": "8.3.0"
},