summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamy Pessé <samypesse@gmail.com>2016-04-22 11:00:21 +0200
committerSamy Pessé <samypesse@gmail.com>2016-04-22 11:00:21 +0200
commit4336fdb2414d460ffee68a0cc87c0cb0c85cf56e (patch)
tree279f711ab98666c892c19a7b9e4073a094f03f98
parent87db7cf1d412fa6fbd18e9a7e4f4755f2c0c5547 (diff)
downloadgitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.zip
gitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.tar.gz
gitbook-4336fdb2414d460ffee68a0cc87c0cb0c85cf56e.tar.bz2
Base
-rw-r--r--.eslintrc3
-rw-r--r--lib/__tests__/gitbook.js11
-rw-r--r--lib/api/index.js3
-rw-r--r--lib/backbone/file.js69
-rw-r--r--lib/backbone/glossary.js99
-rw-r--r--lib/backbone/index.js8
-rw-r--r--lib/backbone/langs.js81
-rw-r--r--lib/backbone/readme.js44
-rw-r--r--lib/backbone/summary.js349
-rw-r--r--lib/config/index.js137
-rw-r--r--lib/config/plugins.js67
-rw-r--r--lib/constants/configFiles.js5
-rw-r--r--lib/constants/configSchema.js (renamed from lib/config/schema.js)0
-rw-r--r--lib/constants/defaultPlugins.js14
-rw-r--r--lib/constants/ignoreFiles.js6
-rw-r--r--lib/constants/pluginPrefix.js5
-rw-r--r--lib/fs/index.js106
-rw-r--r--lib/fs/node.js68
-rw-r--r--lib/gitbook.js9
-rw-r--r--lib/index.js6
-rw-r--r--lib/models/__tests__/plugin.js29
-rw-r--r--lib/models/book.js146
-rw-r--r--lib/models/config.js47
-rw-r--r--lib/models/file.js55
-rw-r--r--lib/models/fs.js277
-rw-r--r--lib/models/generator.js13
-rw-r--r--lib/models/glossary.js86
-rw-r--r--lib/models/glossaryEntry.js45
-rw-r--r--lib/models/language.js21
-rw-r--r--lib/models/languages.js37
-rw-r--r--lib/models/page.js39
-rw-r--r--lib/models/plugin.js84
-rw-r--r--lib/models/readme.js30
-rw-r--r--lib/models/summary.js41
-rw-r--r--lib/models/summaryArticle.js100
-rw-r--r--lib/models/summaryPart.js42
-rw-r--r--lib/output/generateBook.js42
-rw-r--r--lib/output/generatePage.js9
-rw-r--r--lib/output/generators/json.js26
-rw-r--r--lib/output/index.js11
-rw-r--r--lib/output/modifiers/addHeadingId.js9
-rw-r--r--lib/output/modifiers/htmlTransform.js16
-rw-r--r--lib/output/modifiers/index.js9
-rw-r--r--lib/output/modifiers/inlineAssets.js11
-rw-r--r--lib/output/modifiers/svgToImg.js28
-rw-r--r--lib/output2/assets-inliner.js (renamed from lib/output/assets-inliner.js)0
-rw-r--r--lib/output2/base.js (renamed from lib/output/base.js)0
-rw-r--r--lib/output2/conrefs.js (renamed from lib/output/conrefs.js)0
-rw-r--r--lib/output2/ebook.js (renamed from lib/output/ebook.js)0
-rw-r--r--lib/output2/folder.js (renamed from lib/output/folder.js)0
-rw-r--r--lib/output2/json.js (renamed from lib/output/json.js)0
-rw-r--r--lib/output2/website/index.js (renamed from lib/output/website/index.js)0
-rw-r--r--lib/output2/website/templateEnv.js (renamed from lib/output/website/templateEnv.js)0
-rw-r--r--lib/output2/website/themeLoader.js (renamed from lib/output/website/themeLoader.js)0
-rw-r--r--lib/parse/findParsableFile.js36
-rw-r--r--lib/parse/index.js10
-rw-r--r--lib/parse/parseBook.js26
-rw-r--r--lib/parse/parseConfig.js51
-rw-r--r--lib/parse/parseGlossary.js22
-rw-r--r--lib/parse/parseIgnore.js43
-rw-r--r--lib/parse/parsePage.js26
-rw-r--r--lib/parse/parsePagesList.js39
-rw-r--r--lib/parse/parseReadme.js24
-rw-r--r--lib/parse/parseStructureFile.js57
-rw-r--r--lib/parse/parseSummary.js22
-rw-r--r--lib/parse/validateConfig.js (renamed from lib/config/validator.js)17
-rw-r--r--lib/parse/walkSummary.js34
-rw-r--r--lib/plugins/__tests__/findInstalled.js18
-rw-r--r--lib/plugins/__tests__/listAll.js54
-rw-r--r--lib/plugins/__tests__/validatePlugin.js21
-rw-r--r--lib/plugins/findForBook.js27
-rw-r--r--lib/plugins/findInstalled.js66
-rw-r--r--lib/plugins/index.js184
-rw-r--r--lib/plugins/listAll.js56
-rw-r--r--lib/plugins/listForBook.js18
-rw-r--r--lib/plugins/loadForBook.js57
-rw-r--r--lib/plugins/loadPlugin.js89
-rw-r--r--lib/plugins/validatePlugin.js33
-rw-r--r--lib/plugins2/compatibility.js (renamed from lib/plugins/compatibility.js)0
-rw-r--r--lib/plugins2/index.js188
-rw-r--r--lib/plugins2/plugin.js (renamed from lib/plugins/plugin.js)0
-rw-r--r--lib/plugins2/registry.js (renamed from lib/plugins/registry.js)0
-rw-r--r--lib/utils/error.js8
-rw-r--r--lib/utils/promise.js72
-rw-r--r--package.json7
85 files changed, 2431 insertions, 1217 deletions
diff --git a/.eslintrc b/.eslintrc
index 35b58c7..2dc8891 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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"
},