diff options
author | Samy Pessé <samypesse@gmail.com> | 2016-04-22 11:00:21 +0200 |
---|---|---|
committer | Samy Pessé <samypesse@gmail.com> | 2016-04-22 11:00:21 +0200 |
commit | 4336fdb2414d460ffee68a0cc87c0cb0c85cf56e (patch) | |
tree | 279f711ab98666c892c19a7b9e4073a094f03f98 | |
parent | 87db7cf1d412fa6fbd18e9a7e4f4755f2c0c5547 (diff) | |
download | gitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.zip gitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.tar.gz gitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.tar.bz2 |
Base
85 files changed, 2431 insertions, 1217 deletions
@@ -13,7 +13,8 @@ "env": { "node": true, "mocha": true, - "browser": true + "browser": true, + "jest": true }, "extends": "eslint:recommended" }
\ No newline at end of file diff --git a/lib/__tests__/gitbook.js b/lib/__tests__/gitbook.js new file mode 100644 index 0000000..b45fe70 --- /dev/null +++ b/lib/__tests__/gitbook.js @@ -0,0 +1,11 @@ +jest.autoMockOff(); + +var gitbook = require('../gitbook'); + +describe('satisfies', function() { + + it('should return true for *', function() { + expect(gitbook.satisfies('*')).toBe(true); + }); + +}); diff --git a/lib/api/index.js b/lib/api/index.js new file mode 100644 index 0000000..8b4f0ff --- /dev/null +++ b/lib/api/index.js @@ -0,0 +1,3 @@ + + +module.exports = {}; diff --git a/lib/backbone/file.js b/lib/backbone/file.js deleted file mode 100644 index 209e261..0000000 --- a/lib/backbone/file.js +++ /dev/null @@ -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; diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js deleted file mode 100644 index cc0fdce..0000000 --- a/lib/backbone/glossary.js +++ /dev/null @@ -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; diff --git a/lib/backbone/index.js b/lib/backbone/index.js deleted file mode 100644 index 4c3c3f3..0000000 --- a/lib/backbone/index.js +++ /dev/null @@ -1,8 +0,0 @@ - -module.exports = { - Readme: require('./readme'), - Summary: require('./summary'), - Glossary: require('./glossary'), - Langs: require('./langs') -}; - diff --git a/lib/backbone/langs.js b/lib/backbone/langs.js deleted file mode 100644 index e339fa9..0000000 --- a/lib/backbone/langs.js +++ /dev/null @@ -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; diff --git a/lib/backbone/readme.js b/lib/backbone/readme.js deleted file mode 100644 index 088a942..0000000 --- a/lib/backbone/readme.js +++ /dev/null @@ -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; diff --git a/lib/backbone/summary.js b/lib/backbone/summary.js deleted file mode 100644 index 2dbcecb..0000000 --- a/lib/backbone/summary.js +++ /dev/null @@ -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; diff --git a/lib/config/index.js b/lib/config/index.js deleted file mode 100644 index 6887cc2..0000000 --- a/lib/config/index.js +++ /dev/null @@ -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; diff --git a/lib/config/plugins.js b/lib/config/plugins.js deleted file mode 100644 index 24f0041..0000000 --- a/lib/config/plugins.js +++ /dev/null @@ -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 -}; - diff --git a/lib/constants/configFiles.js b/lib/constants/configFiles.js new file mode 100644 index 0000000..a67fd74 --- /dev/null +++ b/lib/constants/configFiles.js @@ -0,0 +1,5 @@ +// Configuration files to test (sorted) +module.exports = [ + 'book.js', + 'book.json' +]; diff --git a/lib/config/schema.js b/lib/constants/configSchema.js index 3fb2050..3fb2050 100644 --- a/lib/config/schema.js +++ b/lib/constants/configSchema.js diff --git a/lib/constants/defaultPlugins.js b/lib/constants/defaultPlugins.js new file mode 100644 index 0000000..e6ea2bb --- /dev/null +++ b/lib/constants/defaultPlugins.js @@ -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' +]); diff --git a/lib/constants/ignoreFiles.js b/lib/constants/ignoreFiles.js new file mode 100644 index 0000000..aac225e --- /dev/null +++ b/lib/constants/ignoreFiles.js @@ -0,0 +1,6 @@ +// Files containing ignore pattner (sorted by priority) +module.exports = [ + '.ignore', + '.gitignore', + '.bookignore' +]; diff --git a/lib/constants/pluginPrefix.js b/lib/constants/pluginPrefix.js new file mode 100644 index 0000000..c7f2dd0 --- /dev/null +++ b/lib/constants/pluginPrefix.js @@ -0,0 +1,5 @@ + +/* + All GitBook plugins are NPM packages starting with this prefix. +*/ +module.exports = 'gitbook-plugin-'; diff --git a/lib/fs/index.js b/lib/fs/index.js deleted file mode 100644 index 8a3ca1e..0000000 --- a/lib/fs/index.js +++ /dev/null @@ -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; diff --git a/lib/fs/node.js b/lib/fs/node.js index fc2517e..e05cb65 100644 --- a/lib/fs/node.js +++ b/lib/fs/node.js @@ -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); + }); }); -}; - -// Load a JSON/JS file -NodeFS.prototype.loadAsObject = function(filename) { - return Promise() - .then(function() { - var jsFile; +} - try { - jsFile = require.resolve(filename); +function fsLoadObject(filename) { + return require(filename); +} - // Invalidate node.js cache for livreloading - delete require.cache[jsFile]; +module.exports = function createNodeFS(root) { + return FS.create({ + root: root, - return require(jsFile); - } - catch(err) { - return Promise.reject(err); - } + fsExists: fs.exists, + fsReadFile: fs.readFile, + fsStatFile: fs.stat, + fsReadDir: fsReadDir, + fsLoadObject: fsLoadObject }); }; - -module.exports = NodeFS; diff --git a/lib/gitbook.js b/lib/gitbook.js index 54513c1..f847899 100644 --- a/lib/gitbook.js +++ b/lib/gitbook.js @@ -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; diff --git a/lib/index.js b/lib/index.js index fdad6ee..5a3c00e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 }; diff --git a/lib/models/__tests__/plugin.js b/lib/models/__tests__/plugin.js new file mode 100644 index 0000000..81d9d51 --- /dev/null +++ b/lib/models/__tests__/plugin.js @@ -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); + }); + + }); +}); + + diff --git a/lib/models/book.js b/lib/models/book.js new file mode 100644 index 0000000..da0deee --- /dev/null +++ b/lib/models/book.js @@ -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; diff --git a/lib/models/config.js b/lib/models/config.js new file mode 100644 index 0000000..fd4201d --- /dev/null +++ b/lib/models/config.js @@ -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; diff --git a/lib/models/file.js b/lib/models/file.js new file mode 100644 index 0000000..ebfe629 --- /dev/null +++ b/lib/models/file.js @@ -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; diff --git a/lib/models/fs.js b/lib/models/fs.js new file mode 100644 index 0000000..2400ff2 --- /dev/null +++ b/lib/models/fs.js @@ -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;
\ No newline at end of file diff --git a/lib/models/generator.js b/lib/models/generator.js new file mode 100644 index 0000000..afc65b1 --- /dev/null +++ b/lib/models/generator.js @@ -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; diff --git a/lib/models/glossary.js b/lib/models/glossary.js new file mode 100644 index 0000000..51aa370 --- /dev/null +++ b/lib/models/glossary.js @@ -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; diff --git a/lib/models/glossaryEntry.js b/lib/models/glossaryEntry.js new file mode 100644 index 0000000..9c390c5 --- /dev/null +++ b/lib/models/glossaryEntry.js @@ -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; diff --git a/lib/models/language.js b/lib/models/language.js new file mode 100644 index 0000000..dcefbf6 --- /dev/null +++ b/lib/models/language.js @@ -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; diff --git a/lib/models/languages.js b/lib/models/languages.js new file mode 100644 index 0000000..c64857f --- /dev/null +++ b/lib/models/languages.js @@ -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; diff --git a/lib/models/page.js b/lib/models/page.js new file mode 100644 index 0000000..1ac0d50 --- /dev/null +++ b/lib/models/page.js @@ -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; diff --git a/lib/models/plugin.js b/lib/models/plugin.js new file mode 100644 index 0000000..b6affd4 --- /dev/null +++ b/lib/models/plugin.js @@ -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; diff --git a/lib/models/readme.js b/lib/models/readme.js new file mode 100644 index 0000000..7a184c4 --- /dev/null +++ b/lib/models/readme.js @@ -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; diff --git a/lib/models/summary.js b/lib/models/summary.js new file mode 100644 index 0000000..3918df7 --- /dev/null +++ b/lib/models/summary.js @@ -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; diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js new file mode 100644 index 0000000..3d642fc --- /dev/null +++ b/lib/models/summaryArticle.js @@ -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; diff --git a/lib/models/summaryPart.js b/lib/models/summaryPart.js new file mode 100644 index 0000000..4b41621 --- /dev/null +++ b/lib/models/summaryPart.js @@ -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; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js new file mode 100644 index 0000000..3f04875 --- /dev/null +++ b/lib/output/generateBook.js @@ -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; diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js new file mode 100644 index 0000000..9afb50a --- /dev/null +++ b/lib/output/generatePage.js @@ -0,0 +1,9 @@ +/** + Generate a page using a generator +*/ +function generatePage(generator, book, page) { + +} + + +module.exports = generatePage; diff --git a/lib/output/generators/json.js b/lib/output/generators/json.js new file mode 100644 index 0000000..e75ae7c --- /dev/null +++ b/lib/output/generators/json.js @@ -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;
\ No newline at end of file diff --git a/lib/output/index.js b/lib/output/index.js new file mode 100644 index 0000000..67ca5ee --- /dev/null +++ b/lib/output/index.js @@ -0,0 +1,11 @@ + + +function generate(book, output) { + +} + + + +module.exports = { + generate: generate +}; diff --git a/lib/output/modifiers/addHeadingId.js b/lib/output/modifiers/addHeadingId.js new file mode 100644 index 0000000..751f4b8 --- /dev/null +++ b/lib/output/modifiers/addHeadingId.js @@ -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; diff --git a/lib/output/modifiers/htmlTransform.js b/lib/output/modifiers/htmlTransform.js new file mode 100644 index 0000000..528b08d --- /dev/null +++ b/lib/output/modifiers/htmlTransform.js @@ -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; diff --git a/lib/output/modifiers/index.js b/lib/output/modifiers/index.js new file mode 100644 index 0000000..76ce3c2 --- /dev/null +++ b/lib/output/modifiers/index.js @@ -0,0 +1,9 @@ + + +function modifyPage() { + + +} + + +module.exports = modifyPage; diff --git a/lib/output/modifiers/inlineAssets.js b/lib/output/modifiers/inlineAssets.js new file mode 100644 index 0000000..190a945 --- /dev/null +++ b/lib/output/modifiers/inlineAssets.js @@ -0,0 +1,11 @@ + + +/** + +*/ +function inlineAssets() { + + +} + +module.exports = inlineAssets; diff --git a/lib/output/modifiers/svgToImg.js b/lib/output/modifiers/svgToImg.js new file mode 100644 index 0000000..b36770a --- /dev/null +++ b/lib/output/modifiers/svgToImg.js @@ -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; diff --git a/lib/output/assets-inliner.js b/lib/output2/assets-inliner.js index 6f1f02d..6f1f02d 100644 --- a/lib/output/assets-inliner.js +++ b/lib/output2/assets-inliner.js diff --git a/lib/output/base.js b/lib/output2/base.js index 868b85b..868b85b 100644 --- a/lib/output/base.js +++ b/lib/output2/base.js diff --git a/lib/output/conrefs.js b/lib/output2/conrefs.js index e58f836..e58f836 100644 --- a/lib/output/conrefs.js +++ b/lib/output2/conrefs.js diff --git a/lib/output/ebook.js b/lib/output2/ebook.js index 2b8fac9..2b8fac9 100644 --- a/lib/output/ebook.js +++ b/lib/output2/ebook.js diff --git a/lib/output/folder.js b/lib/output2/folder.js index 8303ed2..8303ed2 100644 --- a/lib/output/folder.js +++ b/lib/output2/folder.js diff --git a/lib/output/json.js b/lib/output2/json.js index 7061141..7061141 100644 --- a/lib/output/json.js +++ b/lib/output2/json.js diff --git a/lib/output/website/index.js b/lib/output2/website/index.js index 0a8618c..0a8618c 100644 --- a/lib/output/website/index.js +++ b/lib/output2/website/index.js diff --git a/lib/output/website/templateEnv.js b/lib/output2/website/templateEnv.js index d385108..d385108 100644 --- a/lib/output/website/templateEnv.js +++ b/lib/output2/website/templateEnv.js diff --git a/lib/output/website/themeLoader.js b/lib/output2/website/themeLoader.js index 774a39e..774a39e 100644 --- a/lib/output/website/themeLoader.js +++ b/lib/output2/website/themeLoader.js diff --git a/lib/parse/findParsableFile.js b/lib/parse/findParsableFile.js new file mode 100644 index 0000000..4434d64 --- /dev/null +++ b/lib/parse/findParsableFile.js @@ -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; diff --git a/lib/parse/index.js b/lib/parse/index.js new file mode 100644 index 0000000..042024b --- /dev/null +++ b/lib/parse/index.js @@ -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') +}; diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js new file mode 100644 index 0000000..4af1768 --- /dev/null +++ b/lib/parse/parseBook.js @@ -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; diff --git a/lib/parse/parseConfig.js b/lib/parse/parseConfig.js new file mode 100644 index 0000000..5200de2 --- /dev/null +++ b/lib/parse/parseConfig.js @@ -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; diff --git a/lib/parse/parseGlossary.js b/lib/parse/parseGlossary.js new file mode 100644 index 0000000..f56c751 --- /dev/null +++ b/lib/parse/parseGlossary.js @@ -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; diff --git a/lib/parse/parseIgnore.js b/lib/parse/parseIgnore.js new file mode 100644 index 0000000..3ffe89e --- /dev/null +++ b/lib/parse/parseIgnore.js @@ -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; diff --git a/lib/parse/parsePage.js b/lib/parse/parsePage.js new file mode 100644 index 0000000..75bcf61 --- /dev/null +++ b/lib/parse/parsePage.js @@ -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; diff --git a/lib/parse/parsePagesList.js b/lib/parse/parsePagesList.js new file mode 100644 index 0000000..36fcdec --- /dev/null +++ b/lib/parse/parsePagesList.js @@ -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; diff --git a/lib/parse/parseReadme.js b/lib/parse/parseReadme.js new file mode 100644 index 0000000..ea6ef59 --- /dev/null +++ b/lib/parse/parseReadme.js @@ -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; diff --git a/lib/parse/parseStructureFile.js b/lib/parse/parseStructureFile.js new file mode 100644 index 0000000..bdb97db --- /dev/null +++ b/lib/parse/parseStructureFile.js @@ -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; diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js new file mode 100644 index 0000000..3fb471e --- /dev/null +++ b/lib/parse/parseSummary.js @@ -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; diff --git a/lib/config/validator.js b/lib/parse/validateConfig.js index 764b19a..855edc3 100644 --- a/lib/config/validator.js +++ b/lib/parse/validateConfig.js @@ -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; diff --git a/lib/parse/walkSummary.js b/lib/parse/walkSummary.js new file mode 100644 index 0000000..0117752 --- /dev/null +++ b/lib/parse/walkSummary.js @@ -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; diff --git a/lib/plugins/__tests__/findInstalled.js b/lib/plugins/__tests__/findInstalled.js new file mode 100644 index 0000000..956e73f --- /dev/null +++ b/lib/plugins/__tests__/findInstalled.js @@ -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); + }); + }); + +}); diff --git a/lib/plugins/__tests__/listAll.js b/lib/plugins/__tests__/listAll.js new file mode 100644 index 0000000..6da5b8d --- /dev/null +++ b/lib/plugins/__tests__/listAll.js @@ -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'); + }); + +}); diff --git a/lib/plugins/__tests__/validatePlugin.js b/lib/plugins/__tests__/validatePlugin.js new file mode 100644 index 0000000..3d50839 --- /dev/null +++ b/lib/plugins/__tests__/validatePlugin.js @@ -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(); + }); + }); + +}); diff --git a/lib/plugins/findForBook.js b/lib/plugins/findForBook.js new file mode 100644 index 0000000..75a4988 --- /dev/null +++ b/lib/plugins/findForBook.js @@ -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; diff --git a/lib/plugins/findInstalled.js b/lib/plugins/findInstalled.js new file mode 100644 index 0000000..5e13c79 --- /dev/null +++ b/lib/plugins/findInstalled.js @@ -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; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index c6f1686..bee8ac6 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -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; diff --git a/lib/plugins/listAll.js b/lib/plugins/listAll.js new file mode 100644 index 0000000..46eaea0 --- /dev/null +++ b/lib/plugins/listAll.js @@ -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; diff --git a/lib/plugins/listForBook.js b/lib/plugins/listForBook.js new file mode 100644 index 0000000..ce94678 --- /dev/null +++ b/lib/plugins/listForBook.js @@ -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; diff --git a/lib/plugins/loadForBook.js b/lib/plugins/loadForBook.js new file mode 100644 index 0000000..fcfac08 --- /dev/null +++ b/lib/plugins/loadForBook.js @@ -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; diff --git a/lib/plugins/loadPlugin.js b/lib/plugins/loadPlugin.js new file mode 100644 index 0000000..a0dac5f --- /dev/null +++ b/lib/plugins/loadPlugin.js @@ -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; diff --git a/lib/plugins/validatePlugin.js b/lib/plugins/validatePlugin.js new file mode 100644 index 0000000..37f6900 --- /dev/null +++ b/lib/plugins/validatePlugin.js @@ -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; diff --git a/lib/plugins/compatibility.js b/lib/plugins2/compatibility.js index 77f4be2..77f4be2 100644 --- a/lib/plugins/compatibility.js +++ b/lib/plugins2/compatibility.js diff --git a/lib/plugins2/index.js b/lib/plugins2/index.js new file mode 100644 index 0000000..c6f1686 --- /dev/null +++ b/lib/plugins2/index.js @@ -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; diff --git a/lib/plugins/plugin.js b/lib/plugins2/plugin.js index d1c00d8..d1c00d8 100644 --- a/lib/plugins/plugin.js +++ b/lib/plugins2/plugin.js diff --git a/lib/plugins/registry.js b/lib/plugins2/registry.js index fe9406d..fe9406d 100644 --- a/lib/plugins/registry.js +++ b/lib/plugins2/registry.js diff --git a/lib/utils/error.js b/lib/utils/error.js index 27fa59d..d34c17e 100644 --- a/lib/utils/error.js +++ b/lib/utils/error.js @@ -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, diff --git a/lib/utils/promise.js b/lib/utils/promise.js index d49cf27..b0cfb34 100644 --- a/lib/utils/promise.js +++ b/lib/utils/promise.js @@ -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; diff --git a/package.json b/package.json index db41a42..70bb58a 100644 --- a/package.json +++ b/package.json @@ -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" }, |