From 4336fdb2414d460ffee68a0cc87c0cb0c85cf56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 22 Apr 2016 11:00:21 +0200 Subject: [PATCH 001/118] Base --- .eslintrc | 3 +- lib/__tests__/gitbook.js | 11 + lib/api/index.js | 3 + lib/backbone/file.js | 69 ---- lib/backbone/glossary.js | 99 ----- lib/backbone/index.js | 8 - lib/backbone/langs.js | 81 ---- lib/backbone/readme.js | 44 --- lib/backbone/summary.js | 349 ------------------ lib/config/index.js | 137 ------- lib/config/plugins.js | 67 ---- lib/constants/configFiles.js | 5 + .../schema.js => constants/configSchema.js} | 0 lib/constants/defaultPlugins.js | 14 + lib/constants/ignoreFiles.js | 6 + lib/constants/pluginPrefix.js | 5 + lib/fs/index.js | 106 ------ lib/fs/node.js | 76 ++-- lib/gitbook.js | 9 +- lib/index.js | 6 +- lib/models/__tests__/plugin.js | 29 ++ lib/models/book.js | 146 ++++++++ lib/models/config.js | 47 +++ lib/models/file.js | 55 +++ lib/models/fs.js | 277 ++++++++++++++ lib/models/generator.js | 13 + lib/models/glossary.js | 86 +++++ lib/models/glossaryEntry.js | 45 +++ lib/models/language.js | 21 ++ lib/models/languages.js | 37 ++ lib/models/page.js | 39 ++ lib/models/plugin.js | 84 +++++ lib/models/readme.js | 30 ++ lib/models/summary.js | 41 ++ lib/models/summaryArticle.js | 100 +++++ lib/models/summaryPart.js | 42 +++ lib/output/generateBook.js | 42 +++ lib/output/generatePage.js | 9 + lib/output/generators/json.js | 26 ++ lib/output/index.js | 11 + lib/output/modifiers/addHeadingId.js | 9 + lib/output/modifiers/htmlTransform.js | 16 + lib/output/modifiers/index.js | 9 + lib/output/modifiers/inlineAssets.js | 11 + lib/output/modifiers/svgToImg.js | 28 ++ lib/{output => output2}/assets-inliner.js | 0 lib/{output => output2}/base.js | 0 lib/{output => output2}/conrefs.js | 0 lib/{output => output2}/ebook.js | 0 lib/{output => output2}/folder.js | 0 lib/{output => output2}/json.js | 0 lib/{output => output2}/website/index.js | 0 .../website/templateEnv.js | 0 .../website/themeLoader.js | 0 lib/parse/findParsableFile.js | 36 ++ lib/parse/index.js | 10 + lib/parse/parseBook.js | 26 ++ lib/parse/parseConfig.js | 51 +++ lib/parse/parseGlossary.js | 22 ++ lib/parse/parseIgnore.js | 43 +++ lib/parse/parsePage.js | 26 ++ lib/parse/parsePagesList.js | 39 ++ lib/parse/parseReadme.js | 24 ++ lib/parse/parseStructureFile.js | 57 +++ lib/parse/parseSummary.js | 22 ++ .../validator.js => parse/validateConfig.js} | 17 +- lib/parse/walkSummary.js | 34 ++ lib/plugins/__tests__/findInstalled.js | 18 + lib/plugins/__tests__/listAll.js | 54 +++ lib/plugins/__tests__/validatePlugin.js | 21 ++ lib/plugins/findForBook.js | 27 ++ lib/plugins/findInstalled.js | 66 ++++ lib/plugins/index.js | 184 +-------- lib/plugins/listAll.js | 56 +++ lib/plugins/listForBook.js | 18 + lib/plugins/loadForBook.js | 57 +++ lib/plugins/loadPlugin.js | 89 +++++ lib/plugins/validatePlugin.js | 33 ++ lib/{plugins => plugins2}/compatibility.js | 0 lib/plugins2/index.js | 188 ++++++++++ lib/{plugins => plugins2}/plugin.js | 0 lib/{plugins => plugins2}/registry.js | 0 lib/utils/error.js | 8 + lib/utils/promise.js | 72 +++- package.json | 7 +- 85 files changed, 2435 insertions(+), 1221 deletions(-) create mode 100644 lib/__tests__/gitbook.js create mode 100644 lib/api/index.js delete mode 100644 lib/backbone/file.js delete mode 100644 lib/backbone/glossary.js delete mode 100644 lib/backbone/index.js delete mode 100644 lib/backbone/langs.js delete mode 100644 lib/backbone/readme.js delete mode 100644 lib/backbone/summary.js delete mode 100644 lib/config/index.js delete mode 100644 lib/config/plugins.js create mode 100644 lib/constants/configFiles.js rename lib/{config/schema.js => constants/configSchema.js} (100%) create mode 100644 lib/constants/defaultPlugins.js create mode 100644 lib/constants/ignoreFiles.js create mode 100644 lib/constants/pluginPrefix.js delete mode 100644 lib/fs/index.js create mode 100644 lib/models/__tests__/plugin.js create mode 100644 lib/models/book.js create mode 100644 lib/models/config.js create mode 100644 lib/models/file.js create mode 100644 lib/models/fs.js create mode 100644 lib/models/generator.js create mode 100644 lib/models/glossary.js create mode 100644 lib/models/glossaryEntry.js create mode 100644 lib/models/language.js create mode 100644 lib/models/languages.js create mode 100644 lib/models/page.js create mode 100644 lib/models/plugin.js create mode 100644 lib/models/readme.js create mode 100644 lib/models/summary.js create mode 100644 lib/models/summaryArticle.js create mode 100644 lib/models/summaryPart.js create mode 100644 lib/output/generateBook.js create mode 100644 lib/output/generatePage.js create mode 100644 lib/output/generators/json.js create mode 100644 lib/output/index.js create mode 100644 lib/output/modifiers/addHeadingId.js create mode 100644 lib/output/modifiers/htmlTransform.js create mode 100644 lib/output/modifiers/index.js create mode 100644 lib/output/modifiers/inlineAssets.js create mode 100644 lib/output/modifiers/svgToImg.js rename lib/{output => output2}/assets-inliner.js (100%) rename lib/{output => output2}/base.js (100%) rename lib/{output => output2}/conrefs.js (100%) rename lib/{output => output2}/ebook.js (100%) rename lib/{output => output2}/folder.js (100%) rename lib/{output => output2}/json.js (100%) rename lib/{output => output2}/website/index.js (100%) rename lib/{output => output2}/website/templateEnv.js (100%) rename lib/{output => output2}/website/themeLoader.js (100%) create mode 100644 lib/parse/findParsableFile.js create mode 100644 lib/parse/index.js create mode 100644 lib/parse/parseBook.js create mode 100644 lib/parse/parseConfig.js create mode 100644 lib/parse/parseGlossary.js create mode 100644 lib/parse/parseIgnore.js create mode 100644 lib/parse/parsePage.js create mode 100644 lib/parse/parsePagesList.js create mode 100644 lib/parse/parseReadme.js create mode 100644 lib/parse/parseStructureFile.js create mode 100644 lib/parse/parseSummary.js rename lib/{config/validator.js => parse/validateConfig.js} (69%) create mode 100644 lib/parse/walkSummary.js create mode 100644 lib/plugins/__tests__/findInstalled.js create mode 100644 lib/plugins/__tests__/listAll.js create mode 100644 lib/plugins/__tests__/validatePlugin.js create mode 100644 lib/plugins/findForBook.js create mode 100644 lib/plugins/findInstalled.js create mode 100644 lib/plugins/listAll.js create mode 100644 lib/plugins/listForBook.js create mode 100644 lib/plugins/loadForBook.js create mode 100644 lib/plugins/loadPlugin.js create mode 100644 lib/plugins/validatePlugin.js rename lib/{plugins => plugins2}/compatibility.js (100%) create mode 100644 lib/plugins2/index.js rename lib/{plugins => plugins2}/plugin.js (100%) rename lib/{plugins => plugins2}/registry.js (100%) diff --git a/.eslintrc b/.eslintrc index 35b58c76d..2dc8891aa 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 000000000..b45fe705c --- /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 000000000..8b4f0ff70 --- /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 209e261ca..000000000 --- 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 cc0fdce0b..000000000 --- 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 4c3c3f342..000000000 --- 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 e339fa984..000000000 --- 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 088a94292..000000000 --- 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 2dbcecbcf..000000000 --- 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 6887cc289..000000000 --- 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 24f00411f..000000000 --- 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 000000000..a67fd74f4 --- /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 similarity index 100% rename from lib/config/schema.js rename to lib/constants/configSchema.js diff --git a/lib/constants/defaultPlugins.js b/lib/constants/defaultPlugins.js new file mode 100644 index 000000000..e6ea2bb9d --- /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 000000000..aac225eed --- /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 000000000..c7f2dd041 --- /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 8a3ca1e3f..000000000 --- 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 fc2517e76..e05cb657c 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); + }); + }); +} + +function fsLoadObject(filename) { + return require(filename); +} + +module.exports = function createNodeFS(root) { + return FS.create({ + root: root, + + fsExists: fs.exists, + fsReadFile: fs.readFile, + fsStatFile: fs.stat, + fsReadDir: fsReadDir, + fsLoadObject: fsLoadObject }); }; - -// Load a JSON/JS file -NodeFS.prototype.loadAsObject = function(filename) { - return Promise() - .then(function() { - var jsFile; - - try { - jsFile = require.resolve(filename); - - // Invalidate node.js cache for livreloading - delete require.cache[jsFile]; - - return require(jsFile); - } - catch(err) { - return Promise.reject(err); - } - }); -}; - -module.exports = NodeFS; diff --git a/lib/gitbook.js b/lib/gitbook.js index 54513c165..f847899bd 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 fdad6ee2c..5a3c00e8f 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 000000000..81d9d51fd --- /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 000000000..da0deee62 --- /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 000000000..fd4201d7a --- /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 000000000..ebfe629bf --- /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 000000000..2400ff271 --- /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} +*/ +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} +*/ +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} +*/ +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} +*/ +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>} +*/ +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>} +*/ +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>} +*/ +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} +*/ +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} +*/ +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 000000000..afc65b111 --- /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 000000000..51aa37053 --- /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 000000000..9c390c5cb --- /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 000000000..dcefbf613 --- /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 000000000..c64857fb1 --- /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 000000000..1ac0d50d6 --- /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 000000000..b6affd4d0 --- /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 000000000..7a184c44f --- /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 000000000..3918df709 --- /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 000000000..3d642fc2a --- /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 000000000..4b41621ce --- /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 000000000..3f0487543 --- /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} 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 000000000..9afb50a71 --- /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 000000000..e75ae7cf2 --- /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 000000000..67ca5eeb2 --- /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 000000000..751f4b82d --- /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 000000000..528b08d63 --- /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 000000000..76ce3c2a2 --- /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 000000000..190a94509 --- /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 000000000..b36770a85 --- /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 = '' + 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 similarity index 100% rename from lib/output/assets-inliner.js rename to lib/output2/assets-inliner.js diff --git a/lib/output/base.js b/lib/output2/base.js similarity index 100% rename from lib/output/base.js rename to lib/output2/base.js diff --git a/lib/output/conrefs.js b/lib/output2/conrefs.js similarity index 100% rename from lib/output/conrefs.js rename to lib/output2/conrefs.js diff --git a/lib/output/ebook.js b/lib/output2/ebook.js similarity index 100% rename from lib/output/ebook.js rename to lib/output2/ebook.js diff --git a/lib/output/folder.js b/lib/output2/folder.js similarity index 100% rename from lib/output/folder.js rename to lib/output2/folder.js diff --git a/lib/output/json.js b/lib/output2/json.js similarity index 100% rename from lib/output/json.js rename to lib/output2/json.js diff --git a/lib/output/website/index.js b/lib/output2/website/index.js similarity index 100% rename from lib/output/website/index.js rename to lib/output2/website/index.js diff --git a/lib/output/website/templateEnv.js b/lib/output2/website/templateEnv.js similarity index 100% rename from lib/output/website/templateEnv.js rename to lib/output2/website/templateEnv.js diff --git a/lib/output/website/themeLoader.js b/lib/output2/website/themeLoader.js similarity index 100% rename from lib/output/website/themeLoader.js rename to lib/output2/website/themeLoader.js diff --git a/lib/parse/findParsableFile.js b/lib/parse/findParsableFile.js new file mode 100644 index 000000000..4434d64ed --- /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 000000000..042024bea --- /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 000000000..4af176841 --- /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} +*/ +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 000000000..5200de2f9 --- /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} +*/ +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 000000000..f56c751cf --- /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} +*/ +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 000000000..3ffe89ed7 --- /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 000000000..75bcf6154 --- /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} +*/ +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 000000000..36fcdec64 --- /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>} +*/ +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 000000000..ea6ef59fd --- /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} +*/ +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 000000000..bdb97dbc1 --- /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>} +*/ +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} +*/ +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 000000000..3fb471ed1 --- /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} +*/ +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 similarity index 69% rename from lib/config/validator.js rename to lib/parse/validateConfig.js index 764b19a33..855edc332 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 000000000..011775237 --- /dev/null +++ b/lib/parse/walkSummary.js @@ -0,0 +1,34 @@ +var Promise = require('../utils/promise'); + +/** + Walk over a list of articles + + @param {List
} 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 000000000..956e73faa --- /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 000000000..6da5b8d89 --- /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 000000000..3d508399b --- /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 000000000..75a498836 --- /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>} +*/ +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 000000000..5e13c7984 --- /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} +*/ +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 c6f1686a3..bee8ac620 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 000000000..46eaea0bd --- /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} + @return {OrderedMap} +*/ +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 000000000..ce946788b --- /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} +*/ +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 000000000..fcfac08c9 --- /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} +*/ +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 000000000..a0dac5f71 --- /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} +*/ +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 000000000..37f69007a --- /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} +*/ +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 similarity index 100% rename from lib/plugins/compatibility.js rename to lib/plugins2/compatibility.js diff --git a/lib/plugins2/index.js b/lib/plugins2/index.js new file mode 100644 index 000000000..c6f1686a3 --- /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 similarity index 100% rename from lib/plugins/plugin.js rename to lib/plugins2/plugin.js diff --git a/lib/plugins/registry.js b/lib/plugins2/registry.js similarity index 100% rename from lib/plugins/registry.js rename to lib/plugins2/registry.js diff --git a/lib/utils/error.js b/lib/utils/error.js index 27fa59d59..d34c17ef4 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 d49cf27a2..b0cfb34a9 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} +*/ 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} +*/ 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} +*/ 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 db41a42ea..70bb58adb 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" }, From cc78b880cef726201f1670b5c9f7f6f3512c2dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 22 Apr 2016 11:33:25 +0200 Subject: [PATCH 002/118] Add utils to install plugins --- lib/models/plugin.js | 12 +++- lib/plugins/index.js | 5 +- lib/plugins/installPlugins.js | 125 ++++++++++++++++++++++++++++++++++ lib/plugins/loadPlugin.js | 13 +--- lib/plugins/validateConfig.js | 70 +++++++++++++++++++ 5 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 lib/plugins/installPlugins.js create mode 100644 lib/plugins/validateConfig.js diff --git a/lib/models/plugin.js b/lib/models/plugin.js index b6affd4d0..2e8fb03c9 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -52,7 +52,7 @@ Plugin.prototype.getDepth = function() { @return {String} */ Plugin.prototype.getNpmID = function() { - return PREFIX + this.getName(); + return Plugin.nameToNpmID(this.getName()); }; /** @@ -81,4 +81,14 @@ Plugin.createFromString = function(s) { }); }; +/** + Return NPM id for a plugin name + + @param {String} + @return {String} +*/ +Plugin.nameToNpmID = function(s) { + return PREFIX + s; +}; + module.exports = Plugin; diff --git a/lib/plugins/index.js b/lib/plugins/index.js index bee8ac620..622f00c9b 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -1,6 +1,7 @@ module.exports = { - - + loadForBook: require('./loadForBook'), + validateConfig: require('./validateConfig'), + installPlugins: require('./installPlugins') }; diff --git a/lib/plugins/installPlugins.js b/lib/plugins/installPlugins.js new file mode 100644 index 000000000..77ecfced9 --- /dev/null +++ b/lib/plugins/installPlugins.js @@ -0,0 +1,125 @@ +var npm = require('npm'); +var npmi = require('npmi'); +var semver = require('semver'); +var Immutable = require('immutable'); + +var Promise = require('../utils/promise'); +var Plugin = require('../models/plugin'); +var gitbook = require('../gitbook'); + +var npmIsReady; + +/** + Initialize and prepare NPM + + @return {Promise} +*/ +function initNPM() { + if (npmIsReady) return npmIsReady; + + npmIsReady = Promise.nfcall(npm.load, { + silent: true, + loglevel: 'silent' + }); + + return npmIsReady; +} + + + +/** + Resolve a plugin to a version + + @param {Plugin} + @return {Promise} +*/ +function resolveVersion(plugin) { + var npmId = Plugin.nameToNpmID(plugin.getName()); + var requiredVersion = plugin.getVersion(); + + return initNPM() + .then(function() { + return Promise.nfcall(npm.commands.view, [npmId + '@' + requiredVersion, 'engines'], true); + }) + .then(function(versions) { + versions = Immutable.Map(versions).entries().toList(); + + var result = versions + .map(function(entry) { + return { + version: entry[0], + gitbook: (entry[1].engines || {}).gitbook + }; + }) + .filter(function(v) { + return v.gitbook && gitbook.satisfies(v.gitbook); + }) + .sort(function(v1, v2) { + return semver.lt(v1.version, v2.version)? 1 : -1; + }) + .get(0); + + if (!result) { + return undefined; + } else { + return result.get('version'); + } + }); +} + + +/** + Install a plugin for a book + + @param {Book} + @param {Plugin} + @return {Promise} +*/ +function installPlugin(book, plugin) { + var logger = book.getLogger(); + + var installFolder = book.getRoot(); + var name = plugin.getName(); + var requirement = plugin.getVersion(); + + logger.info.ln('installing plugin', name); + + // Find a version to install + return resolveVersion(plugin) + .then(function(version) { + if (!version) { + throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"'); + } + + logger.info.ln('install plugin "' + plugin +'" from NPM with version', requirement); + return Promise.nfcall(npmi, { + 'name': plugin.getNpmID(), + 'version': version, + 'path': installFolder, + 'npmLoad': { + 'loglevel': 'silent', + 'loaded': true, + 'prefix': installFolder + } + }); + }) + .then(function() { + logger.info.ok('plugin "' + plugin + '" installed with success'); + }); +} + + +/** + Install plugin requirements for a book + + @param {Book} + @param {OrderedMap} + @return {Promise} +*/ +function installPlugins(book, plugins) { + return Promise.forEach(plugins, function(plugin) { + return installPlugin(book, plugin); + }); +} + +module.exports = installPlugins; diff --git a/lib/plugins/loadPlugin.js b/lib/plugins/loadPlugin.js index a0dac5f71..375329e61 100644 --- a/lib/plugins/loadPlugin.js +++ b/lib/plugins/loadPlugin.js @@ -33,7 +33,7 @@ function loadPlugin(book, plugin) { var packageContent; var content; - // Locate plugin and load pacjage.json + // Locate plugin and load package.json try { var res = resolve.sync('./package.json', { basedir: pkgPath }); @@ -70,16 +70,7 @@ function loadPlugin(book, plugin) { }); }) - .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); - }); + .then(validatePlugin); logger.info('loading plugin "' + name + '"... '); return logger.info.promise(p); diff --git a/lib/plugins/validateConfig.js b/lib/plugins/validateConfig.js new file mode 100644 index 000000000..fc32344fb --- /dev/null +++ b/lib/plugins/validateConfig.js @@ -0,0 +1,70 @@ +var jsonschema = require('jsonschema'); +var jsonSchemaDefaults = require('json-schema-defaults'); +var mergeDefaults = require('merge-defaults'); + +var Promise = require('../utils/promise'); +var error = require('../utils/error'); + +/** + Validate one plugin for a book and update book's confiration + + @param {Book} + @param {Plugin} + @return {Book} +*/ +function validatePluginConfig(book, plugin) { + var config = book.getConfig(); + var packageInfos = plugin.getPackage(); + + var configKey = [ + 'pluginsConfig', + plugin.getName() + ].join('.'); + + var pluginConfig = config.getValue(configKey, {}); + + var schema = packageInfos.gitbook || {}; + if (!schema) return book; + + // Normalize schema + schema.id = '/' + configKey; + schema.type = 'object'; + + // Validate and throw if invalid + var v = new jsonschema.Validator(); + var result = v.validate(config, schema, { + propertyName: configKey + }); + + // Throw error + if (result.errors.length > 0) { + throw new error.ConfigurationError(new Error(result.errors[0].stack)); + } + + // Insert default values + var defaults = jsonSchemaDefaults(schema); + pluginConfig = mergeDefaults(config, defaults); + + + // Update configuration + config = config.setValue(configKey, pluginConfig); + + // Return new book + return book.set('config', config); +} + +/** + Validate a book configuration for plugins and + returns an update configuration with default values. + + @param {Book} + @param {OrderedMap} + @return {Promise} +*/ +function validateConfig(book, plugins) { + return Promise.reduce(plugins, function(newBook, plugin) { + return validatePluginConfig(newBook, plugin); + }, book); +} + +module.exports = validateConfig; From 4f86a9978b4b27b12e40ab0b5b9e18b8e0615266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 22 Apr 2016 11:56:34 +0200 Subject: [PATCH 003/118] Add test for config --- lib/constants/pluginHooks.js | 8 ++++ lib/constants/pluginResources.js | 4 ++ lib/models/__tests__/config.js | 63 ++++++++++++++++++++++++++++++++ lib/models/config.js | 53 +++++++++++++++++++++++++-- package.json | 2 +- 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 lib/constants/pluginHooks.js create mode 100644 lib/constants/pluginResources.js create mode 100644 lib/models/__tests__/config.js diff --git a/lib/constants/pluginHooks.js b/lib/constants/pluginHooks.js new file mode 100644 index 000000000..2d5dcaa8e --- /dev/null +++ b/lib/constants/pluginHooks.js @@ -0,0 +1,8 @@ +module.exports = [ + 'init', + 'finish', + 'finish:before', + 'config', + 'page', + 'page:before' +]; diff --git a/lib/constants/pluginResources.js b/lib/constants/pluginResources.js new file mode 100644 index 000000000..de5eb18ee --- /dev/null +++ b/lib/constants/pluginResources.js @@ -0,0 +1,4 @@ +module.exports = [ + 'js', + 'css' +]; diff --git a/lib/models/__tests__/config.js b/lib/models/__tests__/config.js new file mode 100644 index 000000000..8445cef43 --- /dev/null +++ b/lib/models/__tests__/config.js @@ -0,0 +1,63 @@ +jest.autoMockOff(); + +var Immutable = require('immutable'); + +describe('Config', function() { + var Config = require('../config'); + + var config = Config.createWithValues({ + hello: { + world: 1, + test: 'Hello', + isFalse: false + } + }); + + describe('getValue', function() { + it('must return value as immutable', function() { + var value = config.getValue('hello'); + expect(Immutable.Map.isMap(value)).toBeTruthy(); + }); + + it('must return deep value', function() { + var value = config.getValue('hello.world'); + expect(value).toBe(1); + }); + + it('must return default value if non existant', function() { + var value = config.getValue('hello.nonExistant', 'defaultValue'); + expect(value).toBe('defaultValue'); + }); + + it('must not return default value for falsy values', function() { + var value = config.getValue('hello.isFalse', 'defaultValue'); + expect(value).toBe(false); + }); + }); + + describe('setValue', function() { + it('must set value as immutable', function() { + var testConfig = config.setValue('hello', { + 'cool': 1 + }); + var value = testConfig.getValue('hello'); + + expect(Immutable.Map.isMap(value)).toBeTruthy(); + expect(value.size).toBe(1); + expect(value.has('cool')).toBeTruthy(); + }); + + it('must set deep value', function() { + var testConfig = config.setValue('hello.world', 2); + var hello = testConfig.getValue('hello'); + var world = testConfig.getValue('hello.world'); + + expect(Immutable.Map.isMap(hello)).toBeTruthy(); + expect(hello.size).toBe(3); + + expect(world).toBe(2); + }); + }); +}); + + diff --git a/lib/models/config.js b/lib/models/config.js index fd4201d7a..e32ccdced 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -6,7 +6,7 @@ var File = require('./file'); var Config = Immutable.Record({ file: File(), values: Immutable.Map() -}); +}, 'Config'); Config.prototype.getPath = function() { return this.get('path'); @@ -24,13 +24,35 @@ Config.prototype.getValues = function() { */ Config.prototype.getValue = function(keyPath, def) { var values = this.getValues(); - if (is.string(keyPath)) keyPath = keyPath.split('.'); + keyPath = Config.keyToKeyPath(keyPath); - return values.getIn(keyPath) || def; + if (!values.hasIn(keyPath)) { + return def; + } + + return values.getIn(keyPath); }; /** - Create a new config, throw error if invalid + Update a configuration value + + @param {String} key + @param {Mixed} value + @return {Mixed} +*/ +Config.prototype.setValue = function(keyPath, value) { + keyPath = Config.keyToKeyPath(keyPath); + + value = Immutable.fromJS(value); + + var values = this.getValues(); + values = values.setIn(keyPath, value); + + return this.set('values', values); +}; + +/** + Create a new config for a file @param {File} file @param {Object} values @@ -43,5 +65,28 @@ Config.create = function(file, values) { }); }; +/** + Create a new config + + @param {Object} values + @returns {Config} +*/ +Config.createWithValues = function(values) { + return new Config({ + values: Immutable.fromJS(values) + }); +}; + + +/** + Convert a keyPath to an array of keys + + @param {String|Array} + @return {Array} +*/ +Config.keyToKeyPath = function(keyPath) { + if (is.string(keyPath)) keyPath = keyPath.split('.'); + return keyPath; +} module.exports = Config; diff --git a/package.json b/package.json index 70bb58adb..5f35410a6 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "should": "8.3.0" }, "scripts": { - "test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js", + "test": "node_modules/.bin/jest --bail", "lint": "eslint ." }, "repository": { From fb0ea4610d7835a14c91fd57268ae3d809062f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 22 Apr 2016 22:21:34 +0200 Subject: [PATCH 004/118] Add base utils for generation --- lib/models/__tests__/glossary.js | 33 ++++++++++++++ lib/models/__tests__/glossaryEntry.js | 17 ++++++++ lib/models/generator.js | 13 ------ lib/models/glossaryEntry.js | 6 +-- lib/models/output.js | 47 ++++++++++++++++++++ lib/output/callHook.js | 15 +++++++ lib/output/generateAssets.js | 23 ++++++++++ lib/output/generateBook.js | 62 ++++++++++++++------------- lib/output/generatePage.js | 9 ---- lib/output/generatePages.js | 23 ++++++++++ lib/output/generators/json.js | 26 ----------- lib/output/index.js | 11 +---- lib/output/json/index.js | 6 +++ lib/output/json/onPage.js | 20 +++++++++ lib/output/json/options.js | 8 ++++ lib/output/prepareAssets.js | 19 ++++++++ lib/output/preparePages.js | 18 ++++++++ lib/output/preparePlugins.js | 24 +++++++++++ lib/parse/index.js | 3 +- lib/parse/listAssets.js | 24 +++++++++++ test.js | 25 +++++++++++ 21 files changed, 341 insertions(+), 91 deletions(-) create mode 100644 lib/models/__tests__/glossary.js create mode 100644 lib/models/__tests__/glossaryEntry.js delete mode 100644 lib/models/generator.js create mode 100644 lib/models/output.js create mode 100644 lib/output/callHook.js create mode 100644 lib/output/generateAssets.js delete mode 100644 lib/output/generatePage.js create mode 100644 lib/output/generatePages.js delete mode 100644 lib/output/generators/json.js create mode 100644 lib/output/json/index.js create mode 100644 lib/output/json/onPage.js create mode 100644 lib/output/json/options.js create mode 100644 lib/output/prepareAssets.js create mode 100644 lib/output/preparePages.js create mode 100644 lib/output/preparePlugins.js create mode 100644 lib/parse/listAssets.js create mode 100644 test.js diff --git a/lib/models/__tests__/glossary.js b/lib/models/__tests__/glossary.js new file mode 100644 index 000000000..03456275a --- /dev/null +++ b/lib/models/__tests__/glossary.js @@ -0,0 +1,33 @@ +jest.autoMockOff(); + +describe('Glossary', function() { + var File = require('../file'); + var Glossary = require('../glossary'); + var GlossaryEntry = require('../glossaryEntry'); + + describe('createFromEntries', function() { + var glossary = Glossary.createFromEntries(File(), [ + { + name: 'Hello World', + description: 'Awesome!' + }, + { + name: 'JavaScript', + description: 'This is a cool language' + } + ]); + + it('must add all entries', function() { + var entries = glossary.getEntries(); + expect(entries.size).toBe(2); + }); + + it('must add entries as GlossaryEntries', function() { + var entries = glossary.getEntries(); + var entry = entries.get('hello-world'); + expect(entry instanceof GlossaryEntry).toBeTruthy(); + }); + }); +}); + + diff --git a/lib/models/__tests__/glossaryEntry.js b/lib/models/__tests__/glossaryEntry.js new file mode 100644 index 000000000..9eabc684c --- /dev/null +++ b/lib/models/__tests__/glossaryEntry.js @@ -0,0 +1,17 @@ +jest.autoMockOff(); + +describe('GlossaryEntry', function() { + var GlossaryEntry = require('../glossaryEntry'); + + describe('getID', function() { + it('must return a normalized ID', function() { + var entry = new GlossaryEntry({ + name: 'Hello World' + }); + + expect(entry.getID()).toBe('hello-world'); + }); + }); +}); + + diff --git a/lib/models/generator.js b/lib/models/generator.js deleted file mode 100644 index afc65b111..000000000 --- a/lib/models/generator.js +++ /dev/null @@ -1,13 +0,0 @@ -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/glossaryEntry.js b/lib/models/glossaryEntry.js index 9c390c5cb..10791db1d 100644 --- a/lib/models/glossaryEntry.js +++ b/lib/models/glossaryEntry.js @@ -1,4 +1,5 @@ var Immutable = require('immutable'); +var slug = require('github-slugid'); /* A definition represents an entry in the glossary @@ -35,10 +36,7 @@ GlossaryEntry.prototype.getID = function() { @return {String} */ GlossaryEntry.nameToID = function nameToID(name) { - return name.toLowerCase() - .replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '') - .replace(/ /g, '_') - .trim(); + return slug(name); }; diff --git a/lib/models/output.js b/lib/models/output.js new file mode 100644 index 000000000..957d9d389 --- /dev/null +++ b/lib/models/output.js @@ -0,0 +1,47 @@ +var Immutable = require('immutable'); + +var Book = require('./book'); + +var Output = Immutable.Record({ + book: Book(), + plugins: Immutable.OrderedMap(), + pages: Immutable.OrderedMap(), + assets: Immutable.List(), + options: Immutable.Map() +}); + +Output.prototype.getBook = function() { + return this.get('book'); +}; + +Output.prototype.getPlugins = function() { + return this.get('plugins'); +}; + +Output.prototype.getPages = function() { + return this.get('pages'); +}; + +Output.prototype.getOptions = function() { + return this.get('options'); +}; + +Output.prototype.getAssets = function() { + return this.get('assets'); +}; + +/** + Create an Output instance from a book and a set of options + + @param {Book} book + @param {Object} options + @return {Output} +*/ +Output.createForBook = function(book, options) { + return new Output({ + book: book, + options: options + }); +}; + +module.exports = Output; diff --git a/lib/output/callHook.js b/lib/output/callHook.js new file mode 100644 index 000000000..0db1ff12a --- /dev/null +++ b/lib/output/callHook.js @@ -0,0 +1,15 @@ + +/** + Call a "global" hook for an output + + @param {String} name + @param {Output} output + @return {Promise} +*/ +function callHook(name, output) { + +} + + + +module.exports = callHook; diff --git a/lib/output/generateAssets.js b/lib/output/generateAssets.js new file mode 100644 index 000000000..04ca05d17 --- /dev/null +++ b/lib/output/generateAssets.js @@ -0,0 +1,23 @@ +var Promise = require('../utils/promise'); + +/** + Output all assets using a generator + + @param {Generator} generator + @param {Output} output + @return {Promise} +*/ +function generateAssets(generator, output) { + var assets = output.getAssets(); + + // Is generator ignoring assets? + if (!generator.onAsset) { + return Promise(output); + } + + return Promise.reduce(assets, function(out, assetFile) { + return generator.onAsset(out, assetFile); + }, output); +} + +module.exports = generateAssets; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index 3f0487543..fb0341893 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -1,42 +1,46 @@ -var Parse = require('../parse'); +var Output = require('../models/output'); +var Promise = require('../utils/promise'); + +var callHook = require('./callHook'); +var preparePlugins = require('./preparePlugins'); +var preparePages = require('./preparePages'); +var prepareAssets = require('./prepareAssets'); +var generateAssets = require('./generateAssets'); +var generatePages = require('./generatePages'); /** - List all assets for a book + Generate a book using a generator. - @param {Book} book - @param {Map} pages - @param -*/ -function listAssets(book, pages) { - var fs = book.getContentFS(); + The overall process is: + 1. List and load plugins for this book + 2. Call hook "config" + 3. Call hook "init" + 4. Initialize generator + 5. List all assets and pages + 6. Copy all assets to output + 7. Generate all pages + 8. Call hook "finish:before" + 9. Finish generation + 10. Call hook "finish" - 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 + @param {Object} options @return {Promise} */ -function generateBook(generator, book) { - // List all parsable pages - return Parse.parsePagesList(book) - .then(function(pages) { - return listAssets(book, pages); - }); +function generateBook(generator, book, options) { + options = generator.Options(options); + + return Promise( + Output.createForBook(book, options) + ) + .then(preparePlugins) + .then(preparePages) + .then(prepareAssets) + .then(generateAssets.bind(null, generator)) + .then(generatePages.bind(null, generator)); } - module.exports = generateBook; diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js deleted file mode 100644 index 9afb50a71..000000000 --- a/lib/output/generatePage.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - Generate a page using a generator -*/ -function generatePage(generator, book, page) { - -} - - -module.exports = generatePage; diff --git a/lib/output/generatePages.js b/lib/output/generatePages.js new file mode 100644 index 000000000..406f24204 --- /dev/null +++ b/lib/output/generatePages.js @@ -0,0 +1,23 @@ +var Promise = require('../utils/promise'); + +/** + Output all pages using a generator + + @param {Generator} generator + @param {Output} output + @return {Promise} +*/ +function generatePages(generator, output) { + var pages = output.getPages(); + + // Is generator ignoring assets? + if (!generator.onPage) { + return Promise(output); + } + + return Promise.reduce(pages, function(out, assetFile) { + return generator.onPage(out, assetFile); + }, output); +} + +module.exports = generatePages; diff --git a/lib/output/generators/json.js b/lib/output/generators/json.js deleted file mode 100644 index e75ae7cf2..000000000 --- a/lib/output/generators/json.js +++ /dev/null @@ -1,26 +0,0 @@ -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 index 67ca5eeb2..45bdf6579 100644 --- a/lib/output/index.js +++ b/lib/output/index.js @@ -1,11 +1,4 @@ - - -function generate(book, output) { - -} - - - module.exports = { - generate: generate + generate: require('./generateBook'), + JSONGenerator: require('./json') }; diff --git a/lib/output/json/index.js b/lib/output/json/index.js new file mode 100644 index 000000000..f721d224b --- /dev/null +++ b/lib/output/json/index.js @@ -0,0 +1,6 @@ + + +module.exports = { + Options: require('./options'), + onPage: require('./onPage') +}; diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js new file mode 100644 index 000000000..3ef5c7410 --- /dev/null +++ b/lib/output/json/onPage.js @@ -0,0 +1,20 @@ +var Modifier = require('../modifier'); + +/** + Write a page as a json file + + @param {Output} output + @param {Page} page +*/ +function onPage(output, page) { + var options = output. + + return Modifier.applyHTMLTransformations(page, [ + Modifier.HTML.resolveLinks() + ]) + .then(function(newPage) { + + }); +} + +module.exports = onPage; diff --git a/lib/output/json/options.js b/lib/output/json/options.js new file mode 100644 index 000000000..79167b180 --- /dev/null +++ b/lib/output/json/options.js @@ -0,0 +1,8 @@ +var Immutable = require('immutable'); + +var Options = Immutable.Record({ + // Root folder for the output + root: String() +}); + +module.exports = Options; diff --git a/lib/output/prepareAssets.js b/lib/output/prepareAssets.js new file mode 100644 index 000000000..19aabef1d --- /dev/null +++ b/lib/output/prepareAssets.js @@ -0,0 +1,19 @@ +var Parse = require('../parse'); + +/** + List all assets in the book + + @param {Output} + @return {Promise} +*/ +function prepareAssets(output) { + var book = output.getBook(); + var pages = output.getPages(); + + return Parse.listAssets(book, pages) + .then(function(assets) { + return output.set('assets', assets); + }); +} + +module.exports = prepareAssets; diff --git a/lib/output/preparePages.js b/lib/output/preparePages.js new file mode 100644 index 000000000..396edde31 --- /dev/null +++ b/lib/output/preparePages.js @@ -0,0 +1,18 @@ +var Parse = require('../parse'); + +/** + List and prepare all pages + + @param {Output} + @return {Promise} +*/ +function preparePages(output) { + var book = output.getBook(); + + return Parse.parsePagesList(book) + .then(function(pages) { + return output.set('pages', pages); + }); +} + +module.exports = preparePages; diff --git a/lib/output/preparePlugins.js b/lib/output/preparePlugins.js new file mode 100644 index 000000000..5768e685a --- /dev/null +++ b/lib/output/preparePlugins.js @@ -0,0 +1,24 @@ +var Plugins = require('../plugins'); + +/** + Load and setup plugins + + @param {Output} + @return {Promise} +*/ +function preparePlugins(output) { + var book = output.getBook(); + + return Plugins.loadForBook(book) + .then(function(plugins) { + return Plugins.validateConfig(book, plugins) + .then(function(newBook) { + return output.merge({ + book: newBook, + plugins: plugins + }); + }); + }); +} + +module.exports = preparePlugins; diff --git a/lib/parse/index.js b/lib/parse/index.js index 042024bea..58561fbe5 100644 --- a/lib/parse/index.js +++ b/lib/parse/index.js @@ -6,5 +6,6 @@ module.exports = { parseReadme: require('./parseReadme'), parseConfig: require('./parseConfig'), parsePagesList: require('./parsePagesList'), - parseIgnore: require('./parseIgnore') + parseIgnore: require('./parseIgnore'), + listAssets: require('./listAssets') }; diff --git a/lib/parse/listAssets.js b/lib/parse/listAssets.js new file mode 100644 index 000000000..6159eda0c --- /dev/null +++ b/lib/parse/listAssets.js @@ -0,0 +1,24 @@ + +/** + List all assets in a book + Assets are file not ignored and not a page + + @param {Book} book + @param {List} 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) + ); + }); + }); +} + +module.exports = listAssets; diff --git a/test.js b/test.js new file mode 100644 index 000000000..82656834a --- /dev/null +++ b/test.js @@ -0,0 +1,25 @@ +var path = require('path'); + +var gitbook = require('./lib'); +var NodeFS = require('./lib/fs/node'); + + +var BASE_PATH = path.join(__dirname); //, 'docs'); + +// Create a filesystem to read the book +var fs = NodeFS(BASE_PATH); + +// Create a book instance +var book = gitbook.Book.createForFS(fs); + +// Parse the book +gitbook.Parse.parseBook(book) +.then(function(_book) { + return gitbook.Parse.parsePagesList(_book); +}) +.then(function(pages) { + //console.log('parsed', pages); +}, function(err) { + console.log('error:', err.stack); +}); + From a162af5a75453a6ecb818447540cbffdc774715f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 22 Apr 2016 22:58:37 +0200 Subject: [PATCH 005/118] Add base for calling hook on page/global --- lib/models/config.js | 15 +++++++++++- lib/output/callHook.js | 30 +++++++++++++++++++++--- lib/output/callPageHook.js | 29 +++++++++++++++++++++++ lib/output/generateBook.js | 47 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 lib/output/callPageHook.js diff --git a/lib/models/config.js b/lib/models/config.js index e32ccdced..8684a03e5 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -77,6 +77,19 @@ Config.createWithValues = function(values) { }); }; +/** + Update values for an existing configuration + + @param {Config} config + @param {Object} values + @returns {Config} +*/ +Config.updateValues = function(config, values) { + values = Immutable.fromJS(values); + + return config.set('values', values); +}; + /** Convert a keyPath to an array of keys @@ -87,6 +100,6 @@ Config.createWithValues = function(values) { Config.keyToKeyPath = function(keyPath) { if (is.string(keyPath)) keyPath = keyPath.split('.'); return keyPath; -} +}; module.exports = Config; diff --git a/lib/output/callHook.js b/lib/output/callHook.js index 0db1ff12a..f6119fda0 100644 --- a/lib/output/callHook.js +++ b/lib/output/callHook.js @@ -1,15 +1,39 @@ +var Promise = require('../utils/promise'); + +function defaultGetArgument() { + return undefined; +} + +function defaultHandleResult(output, result) { + return output; +} /** Call a "global" hook for an output @param {String} name + @param {Function(Output) -> Mixed} getArgument + @param {Function(Output, result) -> Output} handleResult @param {Output} output @return {Promise} */ -function callHook(name, output) { +function callHook(name, getArgument, handleResult, output) { + getArgument = getArgument || defaultGetArgument; + handleResult = handleResult || defaultHandleResult; + var plugins = output.getPlugins(); + + return Promise(getArgument(output)) + .then(function(arg) { + return Promise.reduce(plugins, function(prev, plugin) { + var hook = plugin.getHook(name); + + return hook(prev); + }, arg); + }) + .then(function(result) { + return handleResult(output, result); + }); } - - module.exports = callHook; diff --git a/lib/output/callPageHook.js b/lib/output/callPageHook.js new file mode 100644 index 000000000..f7d5bfaa5 --- /dev/null +++ b/lib/output/callPageHook.js @@ -0,0 +1,29 @@ +var callHook = require('./callHook'); + +/** + Call a hook for a specific page + + @param {String} name + @param {Output} output + @param {Page} page + @return {Page} +*/ +function callPageHook(name, output, page) { + return callHook( + name, + + function(out) { + + + }, + + function(result) { + + + }, + + output + ); +} + +module.exports = callPageHook; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index fb0341893..c35719b8a 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -1,4 +1,5 @@ var Output = require('../models/output'); +var Config = require('../models/config'); var Promise = require('../utils/promise'); var callHook = require('./callHook'); @@ -28,7 +29,7 @@ var generatePages = require('./generatePages'); @param {Book} book @param {Object} options - @return {Promise} + @return {Promise} */ function generateBook(generator, book, options) { options = generator.Options(options); @@ -39,8 +40,50 @@ function generateBook(generator, book, options) { .then(preparePlugins) .then(preparePages) .then(prepareAssets) + + .then( + callHook.bind(null, + 'config', + function(output) { + var book = output.getBook(); + var config = book.getConfig(); + var values = config.getValues(); + + return values.toJS(); + }, + function(output, result) { + var book = output.getBook(); + var config = book.getConfig(); + + config = Config.updateValues(config, result); + book = book.set('config', config); + return output.set('book', book); + } + ) + ) + + .then(callHook.bind(null, 'init')) + + .then(function(output) { + if (!generator.onInit) { + return output; + } + + return generator.onInit(output); + }) + .then(generateAssets.bind(null, generator)) - .then(generatePages.bind(null, generator)); + .then(generatePages.bind(null, generator)) + + .then(callHook.bind(null, 'finish:before')) + + .then(function(output) { + if (!generator.onFinish) { + return output; + } + + return generator.onFinish(output); + }); } module.exports = generateBook; From ce95f316b9ce1eac1e615db3540c4d0f30408d63 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 23 Apr 2016 11:22:20 +0200 Subject: [PATCH 006/118] Add method to be article by level --- lib/api/decodePage.js | 15 +++++++++++ lib/api/encodePage.js | 14 ++++++++++ lib/api/index.js | 6 +++-- lib/models/__tests__/summary.js | 40 +++++++++++++++++++++++++++++ lib/models/plugin.js | 18 +++++++++++++ lib/models/summary.js | 15 +++++++++-- lib/models/summaryArticle.js | 45 ++++++++++++++++++++++++++++++--- lib/models/summaryPart.js | 22 +++++++++++++--- lib/output/callPageHook.js | 9 +++---- 9 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 lib/api/decodePage.js create mode 100644 lib/api/encodePage.js create mode 100644 lib/models/__tests__/summary.js diff --git a/lib/api/decodePage.js b/lib/api/decodePage.js new file mode 100644 index 000000000..b9941c15d --- /dev/null +++ b/lib/api/decodePage.js @@ -0,0 +1,15 @@ + +/** + Decode changes from a JS API to a page boject + + @param {Output} output + @param {Page} page + @param {Object} result + @return {Page} +*/ +function decodePage(output, page, result) { + + +} + +module.exports = decodePage; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js new file mode 100644 index 000000000..9ec55bc65 --- /dev/null +++ b/lib/api/encodePage.js @@ -0,0 +1,14 @@ + +/** + Encode a page in a context to a JS API + + @param {Output} output + @param {Page} page + @return {Object} +*/ +function encodePage(output, page) { + + +} + +module.exports = encodePage; diff --git a/lib/api/index.js b/lib/api/index.js index 8b4f0ff70..213ef1ad9 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,3 +1,5 @@ - -module.exports = {}; +module.exports = { + encodePage: require('./encodePage'), + decodePage: require('./decodePage') +}; diff --git a/lib/models/__tests__/summary.js b/lib/models/__tests__/summary.js new file mode 100644 index 000000000..e641efefa --- /dev/null +++ b/lib/models/__tests__/summary.js @@ -0,0 +1,40 @@ +jest.autoMockOff(); + +describe('Summary', function() { + var File = require('../file'); + var Summary = require('../summary'); + + describe('createFromEntries', function() { + var summary = Summary.createFromParts(File(), [ + { + articles: [ + { + title: 'My First Article', + path: 'README.md' + }, + { + title: 'My Second Article', + path: 'article.md' + } + ] + }, + { + title: 'Test' + } + ]); + + it('must add all parts', function() { + var parts = summary.getParts(); + expect(parts.size).toBe(2); + }); + + it('must index by level', function() { + var part1 = summary.getByLevel('0'); + + expect(part1).toBeDefined(); + expect(part1.getArticles().size).toBe(2); + }); + }); +}); + + diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 2e8fb03c9..6d322f435 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -64,6 +64,24 @@ Plugin.prototype.isLoaded = function() { return Boolean(this.getPackage().size > 0 && this.getContent().size > 0); }; +/** + Return map of hooks + @return {Map} +*/ +Plugin.prototype.getHooks = function() { + return this.getContent().get('hooks'); +}; + +/** + Return a specific hook + + @param {String} name + @return {Function|undefined} +*/ +Plugin.prototype.getHook = function(name) { + return this.getHooks().get(name); +}; + /** Create a plugin from a string diff --git a/lib/models/summary.js b/lib/models/summary.js index 3918df709..f295a16cb 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -2,6 +2,7 @@ var Immutable = require('immutable'); var File = require('./file'); var SummaryPart = require('./summaryPart'); +var SummaryArticle = require('./summaryArticle'); var Summary = Immutable.Record({ file: File(), @@ -16,6 +17,16 @@ Summary.prototype.getParts = function() { return this.get('parts'); }; +/** + Return a part/article by its level + + @param {String} level + @return {Part|Article} +*/ +Summary.prototype.getByLevel = function(level) { + return SummaryArticle.getByLevel(this, level, 'getParts'); +}; + /** Create a new summary for a list of parts @@ -24,12 +35,12 @@ Summary.prototype.getParts = function() { @return {Summary} */ Summary.createFromParts = function createFromParts(file, parts) { - parts = parts.map(function(part) { + parts = parts.map(function(part, i) { if (part instanceof SummaryPart) { return part; } - return SummaryPart.create(part); + return SummaryPart.create(part, i); }); return new Summary({ diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index 3d642fc2a..4a448c8b7 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -29,6 +29,16 @@ SummaryArticle.prototype.getArticles = function() { return this.get('articles'); }; +/** + Return an article by its level + + @param {String} level + @return {Article} +*/ +SummaryArticle.prototype.getByLevel = function(level) { + return SummaryArticle.getByLevel(this, level); +}; + /** Get path (without anchor) to the pointing file @@ -81,15 +91,16 @@ SummaryArticle.prototype.isExternal = function() { @param {Object} def @return {SummaryArticle} */ -SummaryArticle.create = function(def) { - var articles = (def.articles || []).map(function(article) { +SummaryArticle.create = function(def, level) { + var articles = (def.articles || []).map(function(article, i) { if (article instanceof SummaryArticle) { return article; } - return SummaryArticle.create(article); + return SummaryArticle.create(article, [level, i].join('.')); }); return new SummaryArticle({ + level: level, title: def.title, ref: def.ref || def.path, articles: Immutable.List(articles) @@ -97,4 +108,32 @@ SummaryArticle.create = function(def) { }; +/** + Return an article by its level + + @param {Article|Part} base + @param {String} level + @param {String} method + @return {Article} +*/ +SummaryArticle.getByLevel = function(base, level, method) { + method = method || 'getArticles'; + var articles = base[method](); + var levelParts = level.split('.'); + var baseLevel = levelParts.shift(); + + var result = articles.find(function(a) { + return a.getLevel() === baseLevel; + }); + + if (!result) { + return undefined; + } + if (levelParts.length === 0) { + return result; + } + + return SummaryArticle.getByLevel(result, levelParts.join('.')); +}; + module.exports = SummaryArticle; diff --git a/lib/models/summaryPart.js b/lib/models/summaryPart.js index 4b41621ce..345cf83f2 100644 --- a/lib/models/summaryPart.js +++ b/lib/models/summaryPart.js @@ -7,10 +7,15 @@ var SummaryArticle = require('./summaryArticle'); */ var SummaryPart = Immutable.Record({ + level: String(), title: String(), articles: Immutable.List() }); +SummaryPart.prototype.getLevel = function() { + return this.get('level'); +}; + SummaryPart.prototype.getTitle = function() { return this.get('title'); }; @@ -19,21 +24,32 @@ SummaryPart.prototype.getArticles = function() { return this.get('articles'); }; +/** + Return an article by its level + + @param {String} level + @return {Article} +*/ +SummaryPart.prototype.getByLevel = function(level) { + return SummaryArticle.getByLevel(this, level); +}; + /** Create a SummaryPart @param {Object} def @return {SummaryPart} */ -SummaryPart.create = function(def) { - var articles = (def.articles || []).map(function(article) { +SummaryPart.create = function(def, level) { + var articles = (def.articles || []).map(function(article, i) { if (article instanceof SummaryArticle) { return article; } - return SummaryArticle.create(article); + return SummaryArticle.create(article, [level, i].join('.')); }); return new SummaryPart({ + level: String(level), title: def.title, articles: Immutable.List(articles) }); diff --git a/lib/output/callPageHook.js b/lib/output/callPageHook.js index f7d5bfaa5..ed80823c1 100644 --- a/lib/output/callPageHook.js +++ b/lib/output/callPageHook.js @@ -1,3 +1,4 @@ +var Api = require('../api'); var callHook = require('./callHook'); /** @@ -6,20 +7,18 @@ var callHook = require('./callHook'); @param {String} name @param {Output} output @param {Page} page - @return {Page} + @return {Promise} */ function callPageHook(name, output, page) { return callHook( name, function(out) { - - + return Api.encodePage(output, page); }, function(result) { - - + return Api.decodePage(output, page, result); }, output From e1fa977b5b1b3c03790de6e2c21ee39ba55d9555 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 23 Apr 2016 17:10:16 +0200 Subject: [PATCH 007/118] Add json encoding utils --- lib/api/index.js | 5 - lib/json/encodeBook.js | 21 ++ lib/json/encodeBookWithPage.js | 22 ++ lib/json/encodeConfig.js | 12 + lib/json/encodeFile.js | 16 + lib/json/encodeGlossary.js | 21 ++ lib/json/encodeGlossaryEntry.js | 16 + lib/json/encodePage.js | 30 ++ lib/json/encodeReadme.js | 17 + lib/json/encodeSummary.js | 20 ++ lib/json/encodeSummaryArticle.js | 18 + lib/json/encodeSummaryPart.js | 17 + lib/json/index.js | 10 + lib/models/file.js | 14 + lib/models/page.js | 9 +- lib/models/summaryArticle.js | 9 + lib/output/json/onPage.js | 19 +- lib/output/modifiers/index.js | 11 +- lib/output/modifiers/modifyHTML.js | 15 + lib/output/writer/index.js | 4 + lib/output/writer/writePage.js | 33 ++ lib/output2/assets-inliner.js | 140 -------- lib/output2/base.js | 309 ---------------- lib/output2/conrefs.js | 67 ---- lib/output2/ebook.js | 193 ---------- lib/output2/folder.js | 152 -------- lib/output2/json.js | 47 --- lib/output2/website/index.js | 225 ------------ lib/output2/website/templateEnv.js | 95 ----- lib/output2/website/themeLoader.js | 127 ------- lib/page/html.js | 290 --------------- lib/page/index.js | 246 ------------- lib/parse/parsePage.js | 11 +- lib/plugins2/compatibility.js | 61 ---- lib/plugins2/index.js | 188 ---------- lib/plugins2/plugin.js | 288 --------------- lib/plugins2/registry.js | 172 --------- lib/template/blocks.js | 36 -- lib/template/filters.js | 15 - lib/template/index.js | 552 ----------------------------- lib/template/loader.js | 42 --- 41 files changed, 328 insertions(+), 3267 deletions(-) delete mode 100644 lib/api/index.js create mode 100644 lib/json/encodeBook.js create mode 100644 lib/json/encodeBookWithPage.js create mode 100644 lib/json/encodeConfig.js create mode 100644 lib/json/encodeFile.js create mode 100644 lib/json/encodeGlossary.js create mode 100644 lib/json/encodeGlossaryEntry.js create mode 100644 lib/json/encodePage.js create mode 100644 lib/json/encodeReadme.js create mode 100644 lib/json/encodeSummary.js create mode 100644 lib/json/encodeSummaryArticle.js create mode 100644 lib/json/encodeSummaryPart.js create mode 100644 lib/json/index.js create mode 100644 lib/output/modifiers/modifyHTML.js create mode 100644 lib/output/writer/index.js create mode 100644 lib/output/writer/writePage.js delete mode 100644 lib/output2/assets-inliner.js delete mode 100644 lib/output2/base.js delete mode 100644 lib/output2/conrefs.js delete mode 100644 lib/output2/ebook.js delete mode 100644 lib/output2/folder.js delete mode 100644 lib/output2/json.js delete mode 100644 lib/output2/website/index.js delete mode 100644 lib/output2/website/templateEnv.js delete mode 100644 lib/output2/website/themeLoader.js delete mode 100644 lib/page/html.js delete mode 100644 lib/page/index.js delete mode 100644 lib/plugins2/compatibility.js delete mode 100644 lib/plugins2/index.js delete mode 100644 lib/plugins2/plugin.js delete mode 100644 lib/plugins2/registry.js delete mode 100644 lib/template/blocks.js delete mode 100644 lib/template/filters.js delete mode 100644 lib/template/index.js delete mode 100644 lib/template/loader.js diff --git a/lib/api/index.js b/lib/api/index.js deleted file mode 100644 index 213ef1ad9..000000000 --- a/lib/api/index.js +++ /dev/null @@ -1,5 +0,0 @@ - -module.exports = { - encodePage: require('./encodePage'), - decodePage: require('./decodePage') -}; diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js new file mode 100644 index 000000000..eed5e5f3b --- /dev/null +++ b/lib/json/encodeBook.js @@ -0,0 +1,21 @@ +var encodeSummary = require('./encodeSummary'); +var encodeGlossary = require('./encodeGlossary'); +var encodeReadme = require('./encodeReadme'); +var encodeConfig = require('./encodeConfig'); + +/** + Encode a book to JSON + + @param {Book} + @return {Object} +*/ +function encodeBookToJson(book) { + return { + summary: encodeSummary(book.getSummary()), + glossary: encodeGlossary(book.getGlossary()), + readme: encodeReadme(book.getReadme()), + config: encodeConfig(book.getConfig()) + }; +} + +module.exports = encodeBookToJson; diff --git a/lib/json/encodeBookWithPage.js b/lib/json/encodeBookWithPage.js new file mode 100644 index 000000000..26ab174af --- /dev/null +++ b/lib/json/encodeBookWithPage.js @@ -0,0 +1,22 @@ +var encodeBook = require('./encodeBook'); +var encodePage = require('./encodePage'); +var encodeFile = require('./encodeFile'); + +/** + Return a JSON representation of a book with a specific file + + @param {Book} output + @param {Page} page + @return {Object} +*/ +function encodeBookWithPage(book, page) { + var file = page.getFile(); + + var result = encodeBook(book); + result.page = encodePage(page); + result.file = encodeFile(file); + + return result; +} + +module.exports = encodeBookWithPage; diff --git a/lib/json/encodeConfig.js b/lib/json/encodeConfig.js new file mode 100644 index 000000000..195d43bac --- /dev/null +++ b/lib/json/encodeConfig.js @@ -0,0 +1,12 @@ +/** + Encode configuration to JSON + + @param {Config} + @return {Object} +*/ +function encodeConfig(config) { + var values = config.getValues(); + return values.toJS(); +} + +module.exports = encodeConfig; diff --git a/lib/json/encodeFile.js b/lib/json/encodeFile.js new file mode 100644 index 000000000..03b34505c --- /dev/null +++ b/lib/json/encodeFile.js @@ -0,0 +1,16 @@ + +/** + Return a JSON representation of a file + + @param {File} file + @return {Object} +*/ +function encodeFileToJson(file) { + return { + path: file.getPath(), + mtime: file.getMTime(), + type: file.getType() + }; +} + +module.exports = encodeFileToJson; diff --git a/lib/json/encodeGlossary.js b/lib/json/encodeGlossary.js new file mode 100644 index 000000000..fcaf4d2fd --- /dev/null +++ b/lib/json/encodeGlossary.js @@ -0,0 +1,21 @@ +var encodeFile = require('./encodeFileToJson'); +var encodeGlossaryEntry = require('./encodeGlossaryEntry'); + +/** + Encode a glossary to JSON + + @param {Glossary} + @return {Object} +*/ +function encodeGlossary(glossary) { + var file = glossary.getFile(); + var entries = glossary.getEntries(); + + return { + file: encodeFile(file), + entries: entries + .map(encodeGlossaryEntry).toJS() + }; +} + +module.exports = encodeGlossary; diff --git a/lib/json/encodeGlossaryEntry.js b/lib/json/encodeGlossaryEntry.js new file mode 100644 index 000000000..d163f45c0 --- /dev/null +++ b/lib/json/encodeGlossaryEntry.js @@ -0,0 +1,16 @@ + +/** + Encode a SummaryArticle to JSON + + @param {GlossaryEntry} + @return {Object} +*/ +function encodeGlossaryEntry(entry) { + return { + id: entry.getID(), + name: entry.getName(), + description: entry.getDescription() + }; +} + +module.exports = encodeGlossaryEntry; diff --git a/lib/json/encodePage.js b/lib/json/encodePage.js new file mode 100644 index 000000000..d876c784e --- /dev/null +++ b/lib/json/encodePage.js @@ -0,0 +1,30 @@ +/** + Return a JSON representation of a page + + @param {Page} page + @param {Summary} summary + @return {Object} +*/ +function encodePage(page, summary) { + var file = page.getFile(); + var attributes = page.getAttributes(); + var article = summary.getByPath(file.getPath()); + + + var result = attributes.toJS(); + + if (article) { + result.title = article.getTitle(); + result.level = article.getLevel(); + result.depth = article.getDepth(); + + // todo: next and prev + } + + result.content = page.getContent(); + result.dir = page.getDir(); + + return result; +} + +module.exports = encodePage; diff --git a/lib/json/encodeReadme.js b/lib/json/encodeReadme.js new file mode 100644 index 000000000..01277880f --- /dev/null +++ b/lib/json/encodeReadme.js @@ -0,0 +1,17 @@ +var encodeFile = require('./encodeFileToJson'); + +/** + Encode a readme to JSON + + @param {Readme} + @return {Object} +*/ +function encodeReadme(readme) { + var file = readme.getFile(); + + return { + file: encodeFile(file) + }; +} + +module.exports = encodeReadme; diff --git a/lib/json/encodeSummary.js b/lib/json/encodeSummary.js new file mode 100644 index 000000000..97db91058 --- /dev/null +++ b/lib/json/encodeSummary.js @@ -0,0 +1,20 @@ +var encodeFile = require('./encodeFile'); +var encodeSummaryPart = require('./encodeSummaryPart'); + +/** + Encode a summary to JSON + + @param {Summary} + @return {Object} +*/ +function encodeSummary(summary) { + var file = summary.getFile(); + var parts = summary.getParts(); + + return { + file: encodeFile(file), + parts: parts.map(encodeSummaryPart).toJS() + }; +} + +module.exports = encodeSummary; diff --git a/lib/json/encodeSummaryArticle.js b/lib/json/encodeSummaryArticle.js new file mode 100644 index 000000000..485d20990 --- /dev/null +++ b/lib/json/encodeSummaryArticle.js @@ -0,0 +1,18 @@ + +/** + Encode a SummaryArticle to JSON + + @param {SummaryArticle} + @return {Object} +*/ +function encodeSummaryArticle(article) { + return { + title: article.getTitle(), + level: article.getLevel(), + depth: article.getDepth(), + articles: article.getArticles() + .map(encodeSummaryArticle).toJS() + }; +} + +module.exports = encodeSummaryArticle; diff --git a/lib/json/encodeSummaryPart.js b/lib/json/encodeSummaryPart.js new file mode 100644 index 000000000..a5e72188c --- /dev/null +++ b/lib/json/encodeSummaryPart.js @@ -0,0 +1,17 @@ +var encodeSummaryArticle = require('./encodeSummaryArticle'); + +/** + Encode a SummaryPart to JSON + + @param {SummaryPart} + @return {Object} +*/ +function encodeSummaryPart(part) { + return { + title: part.getTitle(), + articles: part.getArticles() + .map(encodeSummaryArticle).toJS() + }; +} + +module.exports = encodeSummaryPart; diff --git a/lib/json/index.js b/lib/json/index.js new file mode 100644 index 000000000..a026a7fc6 --- /dev/null +++ b/lib/json/index.js @@ -0,0 +1,10 @@ + +module.exports = { + encodeBookWithPage: require('./encodeBookWithPage'), + encodeBook: require('./encodeBook'), + encodeFile: require('./encodeFile'), + encodePage: require('./encodePage'), + encodeSummary: require('./encodeSummary'), + encodeReadme: require('./encodeReadme'), + encodeConfig: require('./encodeConfig') +}; diff --git a/lib/models/file.js b/lib/models/file.js index ebfe629bf..b6b06eee0 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -19,6 +19,20 @@ File.prototype.getMTime = function() { return this.get('mtime'); }; +/** + Return type of file ('markdown' or 'asciidoc') + + @return {String} +*/ +File.prototype.getType = function() { + var parser = this.getParser(); + if (parser) { + return parser.name; + } else { + return undefined; + } +}; + /** Return extension of this file (lowercased) diff --git a/lib/models/page.js b/lib/models/page.js index 1ac0d50d6..0a9cf3853 100644 --- a/lib/models/page.js +++ b/lib/models/page.js @@ -9,7 +9,10 @@ var Page = Immutable.Record({ attributes: Immutable.Map(), // Content of the page - content: String() + content: String(), + + // Direction of the text + dir: String('ltr') }); Page.prototype.getFile = function() { @@ -24,6 +27,10 @@ Page.prototype.getContent = function() { return this.get('content'); }; +Page.prototype.getDir = function() { + return this.get('dir'); +}; + /** Create a page for a file diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index 4a448c8b7..e3d85ef50 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -29,6 +29,15 @@ SummaryArticle.prototype.getArticles = function() { return this.get('articles'); }; +/** + Return how deep the article is + + @return {Number} +*/ +SummaryArticle.prototype.getDepth = function() { + return this.getLevel().split('.').length; +}; + /** Return an article by its level diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 3ef5c7410..a84e297c5 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -1,4 +1,6 @@ +var JSONUtils = require('../../json'); var Modifier = require('../modifier'); +var Writer = require('../writer'); /** Write a page as a json file @@ -7,13 +9,22 @@ var Modifier = require('../modifier'); @param {Page} page */ function onPage(output, page) { - var options = output. - - return Modifier.applyHTMLTransformations(page, [ + return Modifier.modifyHTML(page, [ Modifier.HTML.resolveLinks() ]) - .then(function(newPage) { + .then(function(resultPage) { + // Generate the JSON + var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + // Write it to the disk + return Writer.writePage( + output, + page, + JSON.stringify(json, null, 4), + { + extension: '.json' + } + ); }); } diff --git a/lib/output/modifiers/index.js b/lib/output/modifiers/index.js index 76ce3c2a2..ced87164d 100644 --- a/lib/output/modifiers/index.js +++ b/lib/output/modifiers/index.js @@ -1,9 +1,4 @@ - -function modifyPage() { - - -} - - -module.exports = modifyPage; +module.exports = { + modifyHTML: require('./modifyHTML') +}; diff --git a/lib/output/modifiers/modifyHTML.js b/lib/output/modifiers/modifyHTML.js new file mode 100644 index 000000000..c1dad7480 --- /dev/null +++ b/lib/output/modifiers/modifyHTML.js @@ -0,0 +1,15 @@ + + +/** + Apply a list of operations to a page and + output the new page. + + @param {Page} + @param {List} +*/ +function modifyHTML(page, operations) { + +} + + +module.exports = modifyHTML; diff --git a/lib/output/writer/index.js b/lib/output/writer/index.js new file mode 100644 index 000000000..2ef336442 --- /dev/null +++ b/lib/output/writer/index.js @@ -0,0 +1,4 @@ + +module.exports = { + writePage: require('./writePage') +}; diff --git a/lib/output/writer/writePage.js b/lib/output/writer/writePage.js new file mode 100644 index 000000000..23e37d008 --- /dev/null +++ b/lib/output/writer/writePage.js @@ -0,0 +1,33 @@ +var path = require('path'); +var Immutable = require('immutable'); + +var fs = require('../../utils/fs'); +var PathUtils = require('../../utils/path'); + +var WriteOptions = Immutable.Record({ + extension: '.html' +}); + +/** + Write a file to the output folder + + @param {Output} output + @param {Page} page + @param {Buffer|String} content + @return {Promise} +*/ +function writePage(output, page, content, options) { + var file = page.getFile(); + var outputOpts = output.getOptions(); + var rootFolder = outputOpts.get('root'); + + options = WriteOptions(options); + + // Get filename for file to write + var fileName = PathUtils.setExtension(file.getPath(), options.get('extension')); + var filePath = path.join(rootFolder, fileName); + + return fs.writeFile(filePath, content); +} + +module.exports = writePage; diff --git a/lib/output2/assets-inliner.js b/lib/output2/assets-inliner.js deleted file mode 100644 index 6f1f02d4a..000000000 --- a/lib/output2/assets-inliner.js +++ /dev/null @@ -1,140 +0,0 @@ -var util = require('util'); -var path = require('path'); -var crc = require('crc'); - -var FolderOutput = require('./folder')(); -var Promise = require('../utils/promise'); -var fs = require('../utils/fs'); -var imagesUtil = require('../utils/images'); -var location = require('../utils/location'); - -var DEFAULT_ASSETS_FOLDER = 'assets'; - -/* -Mixin to inline all the assets in a book: - - Outline tags - - Download remote images - - Convert .svg images as png -*/ - -module.exports = function assetsInliner(Base) { - Base = Base || FolderOutput; - - function AssetsInliner() { - Base.apply(this, arguments); - - // Map of svg already converted - this.svgs = {}; - this.inlineSvgs = {}; - - // Map of images already downloaded - this.downloaded = {}; - } - util.inherits(AssetsInliner, Base); - - // Output a SVG buffer as a file - AssetsInliner.prototype.onOutputSVG = function(page, svg) { - this.log.debug.ln('output svg from', page.path); - - // Convert svg buffer to a png file - return this.convertSVGBuffer(svg) - - // Return relative path from the page - .then(function(filename) { - return page.relative('/' + filename); - }); - }; - - - // Output an image as a file - AssetsInliner.prototype.onOutputImage = function(page, src) { - var that = this; - - return Promise() - - // Download file if external - .then(function() { - if (!location.isExternal(src)) return; - - return that.downloadAsset(src) - .then(function(_asset) { - src = '/' + _asset; - }); - - }) - .then(function() { - // Resolve src to a relative filepath to the book's root - src = page.resolveLocal(src); - - // Already a PNG/JPG/.. ? - if (path.extname(src).toLowerCase() != '.svg') { - return src; - } - - // Convert SVG to PNG - return that.convertSVGFile(that.resolve(src)); - }) - - // Return relative path from the page - .then(function(filename) { - return page.relative(filename); - }); - }; - - // Download an asset if not already download; returns the output file - AssetsInliner.prototype.downloadAsset = function(src) { - if (this.downloaded[src]) return Promise(this.downloaded[src]); - - var that = this; - var ext = path.extname(src); - var hash = crc.crc32(src).toString(16); - - // Create new file - return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext) - .then(function(filename) { - that.downloaded[src] = filename; - - that.log.debug.ln('downloading asset', src); - return fs.download(src, that.resolve(filename)) - .thenResolve(filename); - }); - }; - - // Convert a .svg into an .png - // Return the output filename for the .png - AssetsInliner.prototype.convertSVGFile = function(src) { - if (this.svgs[src]) return Promise(this.svgs[src]); - - var that = this; - var hash = crc.crc32(src).toString(16); - - // Create new file - return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png') - .then(function(filename) { - that.svgs[src] = filename; - - return imagesUtil.convertSVGToPNG(src, that.resolve(filename)) - .thenResolve(filename); - }); - }; - - // Convert an inline svg into an .png - // Return the output filename for the .png - AssetsInliner.prototype.convertSVGBuffer = function(buf) { - var that = this; - var hash = crc.crc32(buf).toString(16); - - // Already converted? - if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]); - - return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png') - .then(function(filename) { - that.inlineSvgs[hash] = filename; - - return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename)) - .thenResolve(filename); - }); - }; - - return AssetsInliner; -}; diff --git a/lib/output2/base.js b/lib/output2/base.js deleted file mode 100644 index 868b85b78..000000000 --- a/lib/output2/base.js +++ /dev/null @@ -1,309 +0,0 @@ -var _ = require('lodash'); -var Ignore = require('ignore'); -var path = require('path'); - -var Promise = require('../utils/promise'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); -var error = require('../utils/error'); -var PluginsManager = require('../plugins'); -var TemplateEngine = require('../template'); -var gitbook = require('../gitbook'); - -/* -Output is like a stream interface for a parsed book -to output "something". - -The process is mostly on the behavior of "onPage" and "onAsset" -*/ - -function Output(book, opts, parent) { - _.bindAll(this); - this.parent = parent; - - this.opts = _.defaults({}, opts || {}, { - directoryIndex: true - }); - - this.book = book; - book.output = this; - this.log = this.book.log; - - // Create plugins manager - this.plugins = new PluginsManager(this.book); - - // Create template engine - this.template = new TemplateEngine(this); - - // Files to ignore in output - this.ignore = Ignore(); - - // Hack to inherits from rules of the book - this.ignore.add(this.book.ignore); -} - -// Default name for generator -Output.prototype.name = 'base'; - -// Default extension for output -Output.prototype.defaultExtension = '.html'; - -// Start the generation, for a parsed book -Output.prototype.generate = function() { - var that = this; - var isMultilingual = this.book.isMultilingual(); - - return Promise() - - // Load all plugins - .then(function() { - return that.plugins.loadAll() - .then(function() { - that.template.addFilters(that.plugins.getFilters()); - that.template.addBlocks(that.plugins.getBlocks()); - }); - }) - - // Transform the configuration - .then(function() { - return that.plugins.hook('config', that.book.config.dump()) - .then(function(cfg) { - that.book.config.replace(cfg); - }); - }) - - // Initialize the generation - .then(function() { - return that.plugins.hook('init'); - }) - .then(function() { - that.log.info.ln('preparing the generation'); - return that.prepare(); - }) - - // Process all files - .then(function() { - that.log.debug.ln('listing files'); - return that.book.fs.listAllFiles(that.book.root); - }) - - // We want to process assets first, then pages - // Since pages can have logic based on existance of assets - .then(function(files) { - // Split into pages/assets - var byTypes = _.chain(files) - .filter(that.ignore.createFilter()) - - // Ignore file present in a language book - .filter(function(filename) { - return !(isMultilingual && that.book.isInLanguageBook(filename)); - }) - - .groupBy(function(filename) { - return (that.book.hasPage(filename)? 'page' : 'asset'); - }) - - .value(); - - return Promise.serie(byTypes.asset, function(filename) { - that.log.debug.ln('copy asset', filename); - return that.onAsset(filename); - }) - .then(function() { - return Promise.serie(byTypes.page, function(filename) { - that.log.debug.ln('process page', filename); - return that.onPage(that.book.getPage(filename)); - }); - }); - }) - - // Generate sub-books - .then(function() { - if (!that.book.isMultilingual()) return; - - return Promise.serie(that.book.books, function(subbook) { - that.log.info.ln(''); - that.log.info.ln('start generation of language "' + path.relative(that.book.root, subbook.root) + '"'); - - var out = that.onLanguageBook(subbook); - return out.generate(); - }); - }) - - // Finish the generation - .then(function() { - return that.plugins.hook('finish:before'); - }) - .then(function() { - that.log.debug.ln('finishing the generation'); - return that.finish(); - }) - .then(function() { - return that.plugins.hook('finish'); - }) - - .then(function() { - if (!that.book.isLanguageBook()) that.log.info.ln(''); - that.log.info.ok('generation finished with success!'); - }); -}; - -// Prepare the generation -Output.prototype.prepare = function() { - this.ignore.addPattern(_.compact([ - '.gitignore', - '.ignore', - '.bookignore', - 'node_modules', - '_layouts', - - // The configuration file should not be copied in the output - '/' + this.book.config.path, - - // Structure file to ignore - '/' + this.book.summary.path, - '/' + this.book.langs.path - ])); -}; - -// Write a page (parsable file), ex: markdown, etc -Output.prototype.onPage = function(page) { - return page.toHTML(this); -}; - -// Copy an asset file (non-parsable), ex: images, etc -Output.prototype.onAsset = function(filename) { - -}; - -// Finish the generation -Output.prototype.finish = function() { - -}; - -// Resolve an HTML link -Output.prototype.onRelativeLink = function(currentPage, href) { - var to = currentPage.followPage(href); - - // Replace by an .html link - if (to) { - href = to.path; - - // Change README path to be "index.html" - if (href == this.book.readme.path) { - href = 'index.html'; - } - - // Recalcul as relative link - href = currentPage.relative(href); - - // Replace .md by .html - href = this.toURL(href); - } - - return href; -}; - -// Output a SVG buffer as a file -Output.prototype.onOutputSVG = function(page, svg) { - return null; -}; - -// Output an image as a file -// Normalize the relative link -Output.prototype.onOutputImage = function(page, imgFile) { - if (location.isExternal(imgFile)) { - return imgFile; - } - - imgFile = page.resolveLocal(imgFile); - return page.relative(imgFile); -}; - -// Read a template by its source URL -Output.prototype.onGetTemplate = function(sourceUrl) { - throw new Error('template not found '+sourceUrl); -}; - -// Generate a source URL for a template -Output.prototype.onResolveTemplate = function(from, to) { - return path.resolve(path.dirname(from), to); -}; - -// Prepare output for a language book -Output.prototype.onLanguageBook = function(book) { - return new this.constructor(book, this.opts, this); -}; - - -// ---- Utilities ---- - -// Return conetxt for the output itself -Output.prototype.getSelfContext = function() { - return { - name: this.name - }; -}; - -// Return a default context for templates -Output.prototype.getContext = function() { - var ctx = _.extend( - { - output: this.getSelfContext() - }, - this.book.getContext(), - (this.book.isLanguageBook()? this.book.parent: this.book).langs.getContext(), - this.book.readme.getContext(), - this.book.summary.getContext(), - this.book.glossary.getContext(), - this.book.config.getContext(), - gitbook.getContext() - ); - - // Deprecated fields - error.deprecateField(ctx.gitbook, 'generator', this.name, '"gitbook.generator" property is deprecated, use "output.name" instead'); - - return ctx; -}; - -// Resolve a file path in the context of a specific page -// Result is an "absolute path relative to the output folder" -Output.prototype.resolveForPage = function(page, href) { - if (_.isString(page)) page = this.book.getPage(page); - - href = page.relative(href); - return this.onRelativeLink(page, href); -}; - -// Filename for output -// READMEs are replaced by index.html -// /test/README.md -> /test/index.html -Output.prototype.outputPath = function(filename, ext) { - ext = ext || this.defaultExtension; - var output = filename; - - if ( - path.basename(filename, path.extname(filename)) == 'README' || - output == this.book.readme.path - ) { - output = path.join(path.dirname(output), 'index'+ext); - } else { - output = pathUtil.setExtension(output, ext); - } - - return output; -}; - -// Filename for output -// /test/index.html -> /test/ -Output.prototype.toURL = function(filename, ext) { - var href = this.outputPath(filename, ext); - - if (path.basename(href) == 'index.html' && this.opts.directoryIndex) { - href = path.dirname(href) + '/'; - } - - return location.normalize(href); -}; - -module.exports = Output; diff --git a/lib/output2/conrefs.js b/lib/output2/conrefs.js deleted file mode 100644 index e58f836fe..000000000 --- a/lib/output2/conrefs.js +++ /dev/null @@ -1,67 +0,0 @@ -var path = require('path'); -var util = require('util'); - -var folderOutput = require('./folder'); -var Git = require('../utils/git'); -var fs = require('../utils/fs'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); - -/* -Mixin for output to resolve git conrefs -*/ - -module.exports = function conrefsLoader(Base) { - Base = folderOutput(Base); - - function ConrefsLoader() { - Base.apply(this, arguments); - - this.git = new Git(); - } - util.inherits(ConrefsLoader, Base); - - // Read a template by its source URL - ConrefsLoader.prototype.onGetTemplate = function(sourceURL) { - var that = this; - - return this.git.resolve(sourceURL) - .then(function(filepath) { - // Is local file - if (!filepath) { - filepath = that.book.resolve(sourceURL); - } else { - that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath); - } - - // Read file from absolute path - return fs.readFile(filepath) - .then(function(source) { - return { - src: source.toString('utf8'), - path: filepath - }; - }); - }); - }; - - // Generate a source URL for a template - ConrefsLoader.prototype.onResolveTemplate = function(from, to) { - // If origin is in the book, we enforce result file to be in the book - if (this.book.isInBook(from)) { - var href = location.toAbsolute(to, path.dirname(from), ''); - return this.book.resolve(href); - } - - // If origin is in a git repository, we resolve file in the git repository - var gitRoot = this.git.resolveRoot(from); - if (gitRoot) { - return pathUtil.resolveInRoot(gitRoot, to); - } - - // If origin is not in the book (include from a git content ref) - return path.resolve(path.dirname(from), to); - }; - - return ConrefsLoader; -}; diff --git a/lib/output2/ebook.js b/lib/output2/ebook.js deleted file mode 100644 index 2b8fac938..000000000 --- a/lib/output2/ebook.js +++ /dev/null @@ -1,193 +0,0 @@ -var _ = require('lodash'); -var util = require('util'); -var juice = require('juice'); - -var command = require('../utils/command'); -var fs = require('../utils/fs'); -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var WebsiteOutput = require('./website'); -var assetsInliner = require('./assets-inliner'); - -function _EbookOutput() { - WebsiteOutput.apply(this, arguments); - - // ebook-convert does not support link like "./" - this.opts.directoryIndex = false; -} -util.inherits(_EbookOutput, WebsiteOutput); - -var EbookOutput = assetsInliner(_EbookOutput); - -EbookOutput.prototype.name = 'ebook'; - -// Return context for templating -// Incldue type of ebbook generated -EbookOutput.prototype.getSelfContext = function() { - var ctx = EbookOutput.super_.prototype.getSelfContext.apply(this); - ctx.format = this.opts.format; - - return ctx; -}; - -// Finish generation, create ebook using ebook-convert -EbookOutput.prototype.finish = function() { - var that = this; - if (that.book.isMultilingual()) { - return EbookOutput.super_.prototype.finish.apply(that); - } - - return Promise() - .then(function() { - return EbookOutput.super_.prototype.finish.apply(that); - }) - - // Generate SUMMARY.html - .then(function() { - return that.render('summary', 'SUMMARY.html', that.getContext()); - }) - - // Start ebook-convert - .then(function() { - return that.ebookConvertOption(); - }) - - .then(function(options) { - if (!that.opts.format) return; - - var cmd = [ - 'ebook-convert', - that.resolve('SUMMARY.html'), - that.resolve('index.'+that.opts.format), - command.optionsToShellArgs(options) - ].join(' '); - - return command.exec(cmd) - .progress(function(data) { - that.book.log.debug(data); - }) - .fail(function(err) { - if (err.code == 127) { - throw error.RequireInstallError({ - cmd: 'ebook-convert', - install: 'Install it from Calibre: https://calibre-ebook.com' - }); - } - - throw error.EbookError(err); - }); - }); -}; - -// Generate header/footer for PDF -EbookOutput.prototype.getPDFTemplate = function(tpl) { - var that = this; - var context = _.extend( - { - // Nunjucks context mapping to ebook-convert templating - page: { - num: '_PAGENUM_', - title: '_TITLE_', - section: '_SECTION_' - } - }, - this.getContext() - ); - - return this.renderAsString('pdf_'+tpl, context) - - // Inline css, include css relative to the output folder - .then(function(output) { - return Promise.nfcall(juice.juiceResources, output, { - webResources: { - relativeTo: that.root() - } - }); - }); -}; - -// Locate the cover file to use -// Use configuration or search a "cover.jpg" file -// For multi-lingual book, it can use the one from the main book -EbookOutput.prototype.locateCover = function() { - var cover = this.book.config.get('cover', 'cover.jpg'); - - // Resolve to absolute - cover = this.resolve(cover); - - // Cover doesn't exist and multilingual? - if (!fs.existsSync(cover)) { - if (this.parent) return this.parent.locateCover(); - else return undefined; - } - - return cover; -}; - -// Generate options for ebook-convert -EbookOutput.prototype.ebookConvertOption = function() { - var that = this; - - var options = { - '--cover': this.locateCover(), - '--title': that.book.config.get('title'), - '--comments': that.book.config.get('description'), - '--isbn': that.book.config.get('isbn'), - '--authors': that.book.config.get('author'), - '--language': that.book.config.get('language'), - '--book-producer': 'GitBook', - '--publisher': 'GitBook', - '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]', - '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]', - '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]', - '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]', - '--no-chapters-in-toc': true, - '--max-levels': '1', - '--breadth-first': true - }; - - if (that.opts.format == 'epub') { - options = _.extend(options, { - '--dont-split-on-page-breaks': true - }); - } - - if (that.opts.format != 'pdf') return Promise(options); - - var pdfOptions = that.book.config.get('pdf'); - - options = _.extend(options, { - '--chapter-mark': String(pdfOptions.chapterMark), - '--page-breaks-before': String(pdfOptions.pageBreaksBefore), - '--margin-left': String(pdfOptions.margin.left), - '--margin-right': String(pdfOptions.margin.right), - '--margin-top': String(pdfOptions.margin.top), - '--margin-bottom': String(pdfOptions.margin.bottom), - '--pdf-default-font-size': String(pdfOptions.fontSize), - '--pdf-mono-font-size': String(pdfOptions.fontSize), - '--paper-size': String(pdfOptions.paperSize), - '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers), - '--pdf-header-template': that.getPDFTemplate('header'), - '--pdf-footer-template': that.getPDFTemplate('footer'), - '--pdf-sans-family': String(pdfOptions.fontFamily) - }); - - return that.getPDFTemplate('header') - .then(function(tpl) { - options['--pdf-header-template'] = tpl; - - return that.getPDFTemplate('footer'); - }) - .then(function(tpl) { - options['--pdf-footer-template'] = tpl; - - return options; - }); -}; - -// Don't write multi-lingual index for wbook -EbookOutput.prototype.outputMultilingualIndex = function() { - -}; - -module.exports = EbookOutput; diff --git a/lib/output2/folder.js b/lib/output2/folder.js deleted file mode 100644 index 8303ed284..000000000 --- a/lib/output2/folder.js +++ /dev/null @@ -1,152 +0,0 @@ -var _ = require('lodash'); -var util = require('util'); -var path = require('path'); - -var Output = require('./base'); -var fs = require('../utils/fs'); -var pathUtil = require('../utils/path'); -var Promise = require('../utils/promise'); - -/* -This output requires the native fs module to output -book as a directory (mapping assets and pages) -*/ - -module.exports = function folderOutput(Base) { - Base = Base || Output; - - function FolderOutput() { - Base.apply(this, arguments); - - this.opts.root = path.resolve(this.opts.root || this.book.resolve('_book')); - } - util.inherits(FolderOutput, Base); - - // Copy an asset file (non-parsable), ex: images, etc - FolderOutput.prototype.onAsset = function(filename) { - return this.copyFile( - this.book.resolve(filename), - filename - ); - }; - - // Prepare the generation by creating the output folder - FolderOutput.prototype.prepare = function() { - var that = this; - - return Promise() - .then(function() { - return FolderOutput.super_.prototype.prepare.apply(that); - }) - - // Cleanup output folder - .then(function() { - that.log.debug.ln('removing previous output directory'); - return fs.rmDir(that.root()) - .fail(function() { - return Promise(); - }); - }) - - // Create output folder - .then(function() { - that.log.debug.ln('creating output directory'); - return fs.mkdirp(that.root()); - }) - - // Add output folder to ignored files - .then(function() { - that.ignore.addPattern([ - path.relative(that.book.root, that.root()) - ]); - }); - }; - - // Prepare output for a language book - FolderOutput.prototype.onLanguageBook = function(book) { - return new this.constructor(book, _.extend({}, this.opts, { - - // Language output should be output in sub-directory of output - root: path.resolve(this.root(), book.language) - }), this); - }; - - // ----- Utility methods ----- - - // Return path to the root folder - FolderOutput.prototype.root = function() { - return this.opts.root; - }; - - // Resolve a file in the output directory - FolderOutput.prototype.resolve = function(filename) { - return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments))); - }; - - // Copy a file to the output - FolderOutput.prototype.copyFile = function(from, to) { - var that = this; - - return Promise() - .then(function() { - to = that.resolve(to); - var folder = path.dirname(to); - - // Ensure folder exists - return fs.mkdirp(folder); - }) - .then(function() { - return fs.copy(from, to); - }); - }; - - // Write a file/buffer to the output folder - FolderOutput.prototype.writeFile = function(filename, buf) { - var that = this; - - return Promise() - .then(function() { - filename = that.resolve(filename); - var folder = path.dirname(filename); - - // Ensure folder exists - return fs.mkdirp(folder); - }) - - // Write the file - .then(function() { - return fs.writeFile(filename, buf); - }); - }; - - // Return true if a file exists in the output folder - FolderOutput.prototype.hasFile = function(filename) { - var that = this; - - return Promise() - .then(function() { - return fs.exists(that.resolve(filename)); - }); - }; - - // Create a new unique file - // Returns its filename - FolderOutput.prototype.createNewFile = function(base, filename) { - var that = this; - - if (!filename) { - filename = path.basename(filename); - base = path.dirname(base); - } - - return fs.uniqueFilename(this.resolve(base), filename) - .then(function(out) { - out = path.join(base, out); - - return fs.ensure(that.resolve(out)) - .thenResolve(out); - }); - }; - - return FolderOutput; -}; diff --git a/lib/output2/json.js b/lib/output2/json.js deleted file mode 100644 index 706114177..000000000 --- a/lib/output2/json.js +++ /dev/null @@ -1,47 +0,0 @@ -var conrefsLoader = require('./conrefs'); - -var JSONOutput = conrefsLoader(); - -JSONOutput.prototype.name = 'json'; - -// Don't copy asset on JSON output -JSONOutput.prototype.onAsset = function(filename) {}; - -// Write a page (parsable file) -JSONOutput.prototype.onPage = function(page) { - var that = this; - - // Parse the page - return page.toHTML(this) - - // Write as json - .then(function() { - var json = page.getOutputContext(that); - - // Delete some private properties - delete json.config; - - // Specify JSON output version - json.version = '3'; - - return that.writeFile( - page.withExtension('.json'), - JSON.stringify(json, null, 4) - ); - }); -}; - -// At the end of generation, generate README.json for multilingual books -JSONOutput.prototype.finish = function() { - if (!this.book.isMultilingual()) return; - - // Copy README.json from main book - var mainLanguage = this.book.langs.getDefault().id; - return this.copyFile( - this.resolve(mainLanguage, 'README.json'), - 'README.json' - ); -}; - - -module.exports = JSONOutput; diff --git a/lib/output2/website/index.js b/lib/output2/website/index.js deleted file mode 100644 index 0a8618c4a..000000000 --- a/lib/output2/website/index.js +++ /dev/null @@ -1,225 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var util = require('util'); -var I18n = require('i18n-t'); - -var Promise = require('../../utils/promise'); -var location = require('../../utils/location'); -var fs = require('../../utils/fs'); -var conrefsLoader = require('../conrefs'); -var Output = require('../base'); -var setupTemplateEnv = require('./templateEnv'); - -function _WebsiteOutput() { - Output.apply(this, arguments); - - // Nunjucks environment - this.env; - - // Plugin instance for the main theme - this.theme; - - // Plugin instance for the default theme - this.defaultTheme; - - // Resources loaded from plugins - this.resources; - - // i18n for themes - this.i18n = new I18n(); -} -util.inherits(_WebsiteOutput, Output); - -var WebsiteOutput = conrefsLoader(_WebsiteOutput); - -// Name of the generator -// It's being used as a prefix for templates -WebsiteOutput.prototype.name = 'website'; - -// Load and setup the theme -WebsiteOutput.prototype.prepare = function() { - var that = this; - - return Promise() - .then(function() { - return WebsiteOutput.super_.prototype.prepare.apply(that); - }) - - .then(function() { - // This list is ordered to give priority to templates in the book - var searchPaths = _.pluck(that.plugins.list(), 'root'); - - // The book itself can contains a "_layouts" folder - searchPaths.unshift(that.book.root); - - // Load i18n - _.each(searchPaths.concat().reverse(), function(searchPath) { - var i18nRoot = path.resolve(searchPath, '_i18n'); - - if (!fs.existsSync(i18nRoot)) return; - that.i18n.load(i18nRoot); - }); - - that.searchPaths = searchPaths; - }) - - // Copy assets from themes before copying files from book - .then(function() { - if (that.book.isLanguageBook()) return; - - // Assets from the book are already copied - // Copy assets from plugins (start with default plugins) - return Promise.serie(that.plugins.list().reverse(), function(plugin) { - // Copy assets only if exists (don't fail otherwise) - var assetFolder = path.join(plugin.root, '_assets', that.name); - if (!fs.existsSync(assetFolder)) return; - - that.log.debug.ln('copy assets from theme', assetFolder); - return fs.copyDir( - assetFolder, - that.resolve('gitbook'), - { - deleteFirst: false, - overwrite: true, - confirm: true - } - ); - }); - }) - - // Load resources for plugins - .then(function() { - return that.plugins.getResources(that.name) - .then(function(resources) { - that.resources = resources; - }); - }); -}; - -// Write a page (parsable file) -WebsiteOutput.prototype.onPage = function(page) { - var that = this; - - // Parse the page - return page.toHTML(this) - - // Render the page template with the same context as the json output - .then(function() { - return that.render('page', that.outputPath(page.path), page.getOutputContext(that)); - }); -}; - -// Finish generation, create ebook using ebook-convert -WebsiteOutput.prototype.finish = function() { - var that = this; - - return Promise() - .then(function() { - return WebsiteOutput.super_.prototype.finish.apply(that); - }) - - // Copy assets from plugins - .then(function() { - if (that.book.isLanguageBook()) return; - return that.plugins.copyResources(that.name, that.resolve('gitbook')); - }) - - // Generate homepage to select languages - .then(function() { - if (!that.book.isMultilingual()) return; - return that.outputMultilingualIndex(); - }); -}; - -// ----- Utilities ---- - -// Write multi-languages index -WebsiteOutput.prototype.outputMultilingualIndex = function() { - var that = this; - - return that.render('languages', 'index.html', that.getContext()); -}; - -/* - Render a template as an HTML string - Templates are stored in `_layouts` folders - - - @param {String} tpl: template name (ex: "page") - @param {String} outputFile: filename to write, relative to the output - @param {Object} context: context for the page - @return {Promise} -*/ -WebsiteOutput.prototype.renderAsString = function(tpl, context) { - // Calcul template name - var filename = this.templateName(tpl); - - context = _.extend(context, { - plugins: { - resources: this.resources - }, - - options: this.opts - }); - - // Create environment - var env = setupTemplateEnv(this, context); - - return Promise.nfcall(env.render.bind(env), filename, context); -}; - -/* - Render a template using nunjucks - Templates are stored in `_layouts` folders - - - @param {String} tpl: template name (ex: "page") - @param {String} outputFile: filename to write, relative to the output - @param {Object} context: context for the page - @return {Promise} -*/ -WebsiteOutput.prototype.render = function(tpl, outputFile, context) { - var that = this; - - // Calcul relative path to the root - var outputDirName = path.dirname(outputFile); - var basePath = location.normalize(path.relative(outputDirName, './')); - - // Setup complete context - context = _.extend(context, { - basePath: basePath, - - template: { - getJSContext: function() { - return { - page: _.omit(context.page, 'content'), - config: context.config, - file: context.file, - gitbook: context.gitbook, - basePath: basePath, - book: { - language: context.book.language - } - }; - } - } - }); - - return this.renderAsString(tpl, context) - .then(function(html) { - return that.writeFile( - outputFile, - html - ); - }); -}; - -// Return a complete name for a template -WebsiteOutput.prototype.templateName = function(name) { - return path.join(this.name, name+'.html'); -}; - -module.exports = WebsiteOutput; - - - diff --git a/lib/output2/website/templateEnv.js b/lib/output2/website/templateEnv.js deleted file mode 100644 index d385108a8..000000000 --- a/lib/output2/website/templateEnv.js +++ /dev/null @@ -1,95 +0,0 @@ -var _ = require('lodash'); -var nunjucks = require('nunjucks'); -var path = require('path'); -var fs = require('fs'); -var DoExtension = require('nunjucks-do')(nunjucks); - - -var location = require('../../utils/location'); -var defaultFilters = require('../../template/filters'); - -var ThemeLoader = require('./themeLoader'); - -// Directory for a theme with the templates -function templatesPath(dir) { - return path.join(dir, '_layouts'); -} - -/* - Create and setup at Nunjucks template environment - - @return {Nunjucks.Environment} -*/ -function setupTemplateEnv(output, context) { - context = _.defaults(context || {}, { - // Required by ThemeLoader - template: {} - }); - - var loader = new ThemeLoader( - _.map(output.searchPaths, templatesPath) - ); - var env = new nunjucks.Environment(loader); - - env.addExtension('DoExtension', new DoExtension()); - - // Add context as global - _.each(context, function(value, key) { - env.addGlobal(key, value); - }); - - // Add GitBook default filters - _.each(defaultFilters, function(fn, filter) { - env.addFilter(filter, fn); - }); - - // Translate using _i18n locales - env.addFilter('t', function t(s) { - return output.i18n.t(output.book.config.get('language'), s); - }); - - // Transform an absolute path into a relative path - // using this.ctx.page.path - env.addFilter('resolveFile', function resolveFile(href) { - return location.normalize(output.resolveForPage(context.file.path, href)); - }); - - // Test if a file exists - env.addFilter('fileExists', function fileExists(href) { - return fs.existsSync(output.resolve(href)); - }); - - // Transform a '.md' into a '.html' (README -> index) - env.addFilter('contentURL', function contentURL(s) { - return output.toURL(s); - }); - - // Get an article using its path - env.addFilter('getArticleByPath', function getArticleByPath(s) { - var article = output.book.summary.getArticle(s); - if (!article) return undefined; - - return article.getContext(); - }); - - // Relase path to an asset - env.addFilter('resolveAsset', function resolveAsset(href) { - href = path.join('gitbook', href); - - // Resolve for current file - if (context.file) { - href = output.resolveForPage(context.file.path, '/' + href); - } - - // Use assets from parent - if (output.book.isLanguageBook()) { - href = path.join('../', href); - } - - return location.normalize(href); - }); - - return env; -} - -module.exports = setupTemplateEnv; diff --git a/lib/output2/website/themeLoader.js b/lib/output2/website/themeLoader.js deleted file mode 100644 index 774a39efd..000000000 --- a/lib/output2/website/themeLoader.js +++ /dev/null @@ -1,127 +0,0 @@ -var _ = require('lodash'); -var fs = require('fs'); -var path = require('path'); -var nunjucks = require('nunjucks'); - -/* - Nunjucks loader similar to FileSystemLoader, but avoid infinite looping -*/ - -/* - Return true if a filename is relative. -*/ -function isRelative(filename) { - return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); -} - -var ThemeLoader = nunjucks.Loader.extend({ - init: function(searchPaths) { - this.searchPaths = _.map(searchPaths, path.normalize); - }, - - /* - Read source of a resolved filepath - - @param {String} - @return {Object} - */ - getSource: function(fullpath) { - if (!fullpath) return null; - - fullpath = this.resolve(null, fullpath); - var templateName = this.getTemplateName(fullpath); - - if(!fullpath) { - return null; - } - - var src = fs.readFileSync(fullpath, 'utf-8'); - - src = '{% do %}var template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' + - src + - '\n{% do %}template.self = template.stack.pop();{% enddo %}'; - - return { - src: src, - path: fullpath, - noCache: true - }; - }, - - /* - Nunjucks calls "isRelative" to determine when to call "resolve". - We handle absolute paths ourselves in ".resolve" so we always return true - */ - isRelative: function() { - return true; - }, - - /* - Get original search path containing a template - - @param {String} filepath - @return {String} searchPath - */ - getSearchPath: function(filepath) { - return _.chain(this.searchPaths) - .sortBy(function(s) { - return -s.length; - }) - .find(function(basePath) { - return (filepath && filepath.indexOf(basePath) === 0); - }) - .value(); - }, - - /* - Get template name from a filepath - - @param {String} filepath - @return {String} name - */ - getTemplateName: function(filepath) { - var originalSearchPath = this.getSearchPath(filepath); - return originalSearchPath? path.relative(originalSearchPath, filepath) : null; - }, - - /* - Resolve a template from a current template - - @param {String|null} from - @param {String} to - @return {String|null} - */ - resolve: function(from, to) { - var searchPaths = this.searchPaths; - - // Relative template like "./test.html" - if (isRelative(to) && from) { - return path.resolve(path.dirname(from), to); - } - - // Determine in which search folder we currently are - var originalSearchPath = this.getSearchPath(from); - var originalFilename = this.getTemplateName(from); - - // If we are including same file from a different search path - // Slice the search paths to avoid including from previous ones - if (originalFilename == to) { - var currentIndex = searchPaths.indexOf(originalSearchPath); - searchPaths = searchPaths.slice(currentIndex + 1); - } - - // Absolute template to resolve in root folder - var resultFolder = _.find(searchPaths, function(basePath) { - var p = path.resolve(basePath, to); - - return ( - p.indexOf(basePath) === 0 - && fs.existsSync(p) - ); - }); - if (!resultFolder) return null; - return path.resolve(resultFolder, to); - } -}); - -module.exports = ThemeLoader; diff --git a/lib/page/html.js b/lib/page/html.js deleted file mode 100644 index e8d3a8567..000000000 --- a/lib/page/html.js +++ /dev/null @@ -1,290 +0,0 @@ -var _ = require('lodash'); -var url = require('url'); -var cheerio = require('cheerio'); -var domSerializer = require('dom-serializer'); -var slug = require('github-slugid'); - -var Promise = require('../utils/promise'); -var location = require('../utils/location'); - -// Selector to ignore -var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6'; - -function HTMLPipeline(htmlString, opts) { - _.bindAll(this); - - this.opts = _.defaults(opts || {}, { - // Called once the description has been found - onDescription: function(description) { }, - - // Calcul new href for a relative link - onRelativeLink: _.identity, - - // Output an image - onImage: _.identity, - - // Syntax highlighting - onCodeBlock: _.identity, - - // Output a svg, if returns null the svg is kept inlined - onOutputSVG: _.constant(null), - - // Words to annotate - annotations: [], - - // When an annotation is applied - onAnnotation: function () { } - }); - - this.$ = cheerio.load(htmlString, { - // We should parse html without trying to normalize too much - xmlMode: false, - - // SVG need some attributes to use uppercases - lowerCaseAttributeNames: false, - lowerCaseTags: false - }); -} - -// Transform a query of elements in the page -HTMLPipeline.prototype._transform = function(query, fn) { - var that = this; - - var $elements = this.$(query); - - return Promise.serie($elements, function(el) { - var $el = that.$(el); - return fn.call(that, $el); - }); -}; - -// Normalize links -HTMLPipeline.prototype.transformLinks = function() { - return this._transform('a', function($a) { - var href = $a.attr('href'); - if (!href) return; - - if (location.isAnchor(href)) { - // Don't "change" anchor links - } else if (location.isRelative(href)) { - // Preserve anchor - var parsed = url.parse(href); - var filename = this.opts.onRelativeLink(parsed.pathname); - - $a.attr('href', filename + (parsed.hash || '')); - } else { - // External links - $a.attr('target', '_blank'); - } - }); -}; - -// Normalize images -HTMLPipeline.prototype.transformImages = function() { - return this._transform('img', function($img) { - return Promise(this.opts.onImage($img.attr('src'))) - .then(function(filename) { - $img.attr('src', filename); - }); - }); -}; - -// Normalize code blocks -HTMLPipeline.prototype.transformCodeBlocks = function() { - return this._transform('code', function($code) { - // Extract language - var lang = _.chain( - ($code.attr('class') || '').split(' ') - ) - .map(function(cl) { - // Markdown - if (cl.search('lang-') === 0) return cl.slice('lang-'.length); - - // Asciidoc - if (cl.search('language-') === 0) return cl.slice('language-'.length); - - return null; - }) - .compact() - .first() - .value(); - - var source = $code.text(); - - return Promise(this.opts.onCodeBlock(source, lang)) - .then(function(blk) { - if (blk.html === false) { - $code.text(blk.body); - } else { - $code.html(blk.body); - } - }); - }); -}; - -// Add ID to headings -HTMLPipeline.prototype.transformHeadings = function() { - var that = this; - - this.$('h1,h2,h3,h4,h5,h6').each(function() { - var $h = that.$(this); - - // Already has an ID? - if ($h.attr('id')) return; - $h.attr('id', slug($h.text())); - }); -}; - -// Outline SVG from the HML -HTMLPipeline.prototype.transformSvgs = function() { - var that = this; - - return this._transform('svg', function($svg) { - var content = [ - '', - renderDOM(that.$, $svg) - ].join('\n'); - - return Promise(that.opts.onOutputSVG(content)) - .then(function(filename) { - if (!filename) return; - - $svg.replaceWith(that.$('').attr('src', filename)); - }); - }); -}; - -// Annotate the content -HTMLPipeline.prototype.applyAnnotations = function() { - var that = this; - - _.each(this.opts.annotations, function(annotation) { - var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' ); - - that.$('*').each(function() { - var $this = that.$(this); - - if ( - $this.is(ANNOTATION_IGNORE) || - $this.parents(ANNOTATION_IGNORE).length > 0 - ) return; - - replaceText(that.$, this, searchRegex, function(match) { - that.opts.onAnnotation(annotation); - - return '' - + match - + ''; - }); - }); - }); -}; - -// Extract page description from html -// This can totally be improved -HTMLPipeline.prototype.extractDescription = function() { - var $ = this.$; - var $p = $('p').first(); - var $next = $p.nextUntil('h1,h2,h3,h4,h5,h6,pre,blockquote,ul,ol,div'); - - var description = $p.text().trim(); - - $next.each(function() { - description += ' ' + $(this).text().trim(); - }); - - // Truncate description - description = _.trunc(description, 300); - - this.opts.onDescription(description); -}; - -// Write content to the pipeline -HTMLPipeline.prototype.output = function() { - var that = this; - - return Promise() - .then(this.extractDescription) - .then(this.transformImages) - .then(this.transformHeadings) - .then(this.transformCodeBlocks) - .then(this.transformSvgs) - .then(this.applyAnnotations) - - // Transform of links should be applied after annotations - // because annotations are created as links - .then(this.transformLinks) - - .then(function() { - return renderDOM(that.$); - }); -}; - - -// 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); -} - -// Replace text in an element -function replaceText($, el, search, replace, text_only ) { - return $(el).each(function(){ - var node = this.firstChild, - val, - new_val, - - // Elements to be removed at the end. - remove = []; - - // Only continue if firstChild exists. - if ( node ) { - - // Loop over all childNodes. - while (node) { - - // Only process text nodes. - if ( node.nodeType === 3 ) { - - // The original node value. - val = node.nodeValue; - - // The new value. - new_val = val.replace( search, replace ); - - // Only replace text if the new value is actually different! - if ( new_val !== val ) { - - if ( !text_only && /\|\:])/g, '\\$1'); -} - -module.exports = HTMLPipeline; diff --git a/lib/page/index.js b/lib/page/index.js deleted file mode 100644 index f0d7f57fb..000000000 --- a/lib/page/index.js +++ /dev/null @@ -1,246 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var direction = require('direction'); -var fm = require('front-matter'); - -var error = require('../utils/error'); -var pathUtil = require('../utils/path'); -var location = require('../utils/location'); -var parsers = require('../parsers'); -var pluginCompatibility = require('../plugins/compatibility'); -var HTMLPipeline = require('./html'); - -/* -A page represent a parsable file in the book (Markdown, Asciidoc, etc) -*/ - -function Page(book, filename) { - if (!(this instanceof Page)) return new Page(book, filename); - var extension; - _.bindAll(this); - - this.book = book; - this.log = this.book.log; - - // Map of attributes from YAML frontmatter - // Description is also extracted by default from content - this.attributes = {}; - - // Current content - this.content = ''; - - // Relative path to the page - this.path = location.normalize(filename); - - // Absolute path to the page - this.rawPath = this.book.resolve(filename); - - // Last modification date - this.mtime = 0; - - // Can we parse it? - extension = path.extname(this.path); - this.parser = parsers.getByExt(extension); - if (!this.parser) throw error.ParsingError(new Error('Can\'t parse file "'+this.path+'"')); - - this.type = this.parser.name; -} - -// Return the filename of the page with another extension -// "README.md" -> "README.html" -Page.prototype.withExtension = function(ext) { - return pathUtil.setExtension(this.path, ext); -}; - -// Resolve a filename relative to this page -// It returns a path relative to the book root folder -Page.prototype.resolveLocal = function() { - var dir = path.dirname(this.path); - var file = path.join.apply(path, _.toArray(arguments)); - - return location.toAbsolute(file, dir, ''); -}; - -// Resolve a filename relative to this page -// It returns an absolute path for the FS -Page.prototype.resolve = function() { - return this.book.resolve(this.resolveLocal.apply(this, arguments)); -}; - -// Convert an absolute path (in the book) to a relative path from this page -Page.prototype.relative = function(name) { - // Convert /test.png -> test.png - name = location.toAbsolute(name, '', ''); - - return location.relative( - this.resolve('.') + '/', - this.book.resolve(name) - ); -}; - -// Return a page result of a relative page from this page -Page.prototype.followPage = function(filename) { - var absPath = this.resolveLocal(filename); - return this.book.getPage(absPath); -}; - -// Update content of the page -Page.prototype.update = function(content) { - this.content = content; -}; - -// Read the page as a string -Page.prototype.read = function() { - var that = this; - - return this.book.statFile(this.path) - .then(function(stat) { - that.mtime = stat.mtime; - return that.book.readFile(that.path); - }) - .then(this.update); -}; - -// Return templating context for this page -// This is used both for themes and page parsing -Page.prototype.getContext = function() { - var article = this.book.summary.getArticle(this); - var next = article? article.next() : null; - var prev = article? article.prev() : null; - - // Detect text direction in this page - var dir = this.book.config.get('direction'); - if (!dir) { - dir = direction(this.content); - if (dir == 'neutral') dir = null; - } - - return { - file: { - path: this.path, - mtime: this.mtime, - type: this.type - }, - page: _.extend({}, this.attributes, { - title: article? article.title : null, - next: next? next.getContext() : null, - previous: prev? prev.getContext() : null, - level: article? article.level : null, - depth: article? article.depth() : 0, - content: this.content, - dir: dir - }) - }; -}; - -// Return complete context for templating (page + book + summary + ...) -Page.prototype.getOutputContext = function(output) { - return _.extend({}, this.getContext(), output.getContext()); -}; - -// Parse the page and return its content -Page.prototype.toHTML = function(output) { - var that = this; - - this.log.debug.ln('start parsing file', this.path); - - // Call a hook in the output - // using an utility to "keep" compatibility with gitbook 2 - function hook(name) { - return pluginCompatibility.pageHook(that, function(ctx) { - return output.plugins.hook(name, ctx); - }) - .then(function(result) { - if(_.isString(result)) that.update(result); - }); - } - - return this.read() - - // Parse yaml front matter - .then(function() { - var parsed = fm(that.content); - - // Extract attributes - that.attributes = parsed.attributes; - - // Keep only the body - that.update(parsed.body); - }) - - .then(function() { - return hook('page:before'); - }) - - // Pre-process page with parser - .then(function() { - return that.parser.page.prepare(that.content) - .then(that.update); - }) - - // Render template - .then(function() { - return output.template.render(that.content, that.getOutputContext(output), { - path: that.path - }) - .then(that.update); - }) - - // Render markup using the parser - .then(function() { - return that.parser.page(that.content) - .then(function(out) { - that.update(out.content); - }); - }) - - // Post process templating - .then(function() { - return output.template.postProcess(that.content) - .then(that.update); - }) - - // Normalize HTML output - .then(function() { - var pipelineOpts = { - onRelativeLink: _.partial(output.onRelativeLink, that), - onImage: _.partial(output.onOutputImage, that), - onOutputSVG: _.partial(output.onOutputSVG, that), - - // Use 'code' template block - onCodeBlock: function(source, lang) { - return output.template.applyBlock('code', { - body: source, - kwargs: { - language: lang - } - }); - }, - - // Extract description from page's content if no frontmatter - onDescription: function(description) { - if (that.attributes.description) return; - that.attributes.description = description; - }, - - // Convert glossary entries to annotations - annotations: that.book.glossary.annotations() - }; - var pipeline = new HTMLPipeline(that.content, pipelineOpts); - - return pipeline.output() - .then(that.update); - }) - - .then(function() { - return hook('page'); - }) - - // Return content itself - .then(function() { - return that.content; - }); -}; - - -module.exports = Page; diff --git a/lib/parse/parsePage.js b/lib/parse/parsePage.js index 75bcf6154..1d515d6f6 100644 --- a/lib/parse/parsePage.js +++ b/lib/parse/parsePage.js @@ -1,4 +1,6 @@ +var Immutable = require('immutable'); var fm = require('front-matter'); +var direction = require('direction'); /** Parse a page, read its content and parse the YAMl header @@ -15,10 +17,11 @@ function parsePage(book, page) { .then(function(content) { var parsed = fm(content); - page = page.set('content', parsed.body); - page = page.set('attributes', parsed.attributes); - - return page; + return page.merge({ + content: parsed.body, + attributes: Immutable.fromJS(parsed.attributes), + dir: direction(parsed.body) + }); }); } diff --git a/lib/plugins2/compatibility.js b/lib/plugins2/compatibility.js deleted file mode 100644 index 77f4be238..000000000 --- a/lib/plugins2/compatibility.js +++ /dev/null @@ -1,61 +0,0 @@ -var _ = require('lodash'); -var error = require('../utils/error'); - -/* - Return the context for a plugin. - It tries to keep compatibilities with GitBook v2 -*/ -function pluginCtx(plugin) { - var book = plugin.book; - var ctx = book; - - return ctx; -} - -/* - Call a function "fn" with a context of page similar to the one in GitBook v2 - - @params {Page} - @returns {String|undefined} new content of the page -*/ -function pageHook(page, fn) { - // Get page context - var ctx = page.getContext().page; - - // Add other informations - ctx.type = page.type; - ctx.rawPath = page.rawPath; - ctx.path = page.path; - - // Deprecate sections - error.deprecateField(ctx, 'sections', [ - { content: ctx.content, type: 'normal' } - ], '"sections" property is deprecated, use page.content instead'); - - // Keep reference of original content for compatibility - var originalContent = ctx.content; - - return fn(ctx) - .then(function(result) { - // No returned value - // Existing content will be used - if (!result) return undefined; - - // GitBook 3 - // Use returned page.content if different from original content - if (result.content != originalContent) { - return result.content; - } - - // GitBook 2 compatibility - // Finally, use page.sections - if (result.sections) { - return _.pluck(result.sections, 'content').join('\n'); - } - }); -} - -module.exports = { - pluginCtx: pluginCtx, - pageHook: pageHook -}; diff --git a/lib/plugins2/index.js b/lib/plugins2/index.js deleted file mode 100644 index c6f1686a3..000000000 --- a/lib/plugins2/index.js +++ /dev/null @@ -1,188 +0,0 @@ -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/plugins2/plugin.js b/lib/plugins2/plugin.js deleted file mode 100644 index d1c00d822..000000000 --- a/lib/plugins2/plugin.js +++ /dev/null @@ -1,288 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var url = require('url'); -var resolve = require('resolve'); -var mergeDefaults = require('merge-defaults'); -var jsonschema = require('jsonschema'); -var jsonSchemaDefaults = require('json-schema-defaults'); - -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var gitbook = require('../gitbook'); -var registry = require('./registry'); -var compatibility = require('./compatibility'); - -var HOOKS = [ - 'init', 'finish', 'finish:before', 'config', 'page', 'page:before' -]; - -var RESOURCES = ['js', 'css']; - -// 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; -} - -function BookPlugin(book, pluginId, pluginFolder) { - this.book = book; - this.log = this.book.log.prefix(pluginId); - - - this.id = pluginId; - this.npmId = registry.npmId(pluginId); - this.root = pluginFolder; - - this.packageInfos = undefined; - this.content = undefined; - - // Cache for resources - this._resources = {}; - - _.bindAll(this); -} - -// Return true if plugin has been loaded correctly -BookPlugin.prototype.isLoaded = function() { - return Boolean(this.packageInfos && this.content); -}; - -// Bind a function to the plugin's context -BookPlugin.prototype.bind = function(fn) { - return fn.bind(compatibility.pluginCtx(this)); -}; - -// Load this plugin from its root folder -BookPlugin.prototype.load = function(folder) { - var that = this; - - if (this.isLoaded()) { - return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded')); - } - - // Try loading plugins from different location - var p = Promise() - .then(function() { - // Locate plugin and load pacjage.json - try { - var res = resolve.sync('./package.json', { basedir: that.root }); - - that.root = path.dirname(res); - that.packageInfos = require(res); - } catch (err) { - if (!isModuleNotFound(err)) throw err; - - that.packageInfos = undefined; - that.content = undefined; - - return; - } - - // Load plugin JS content - try { - that.content = require(that.root); - } catch(err) { - // It's no big deal if the plugin doesn't have an "index.js" - // (For example: themes) - if (isModuleNotFound(err)) { - that.content = {}; - } else { - throw new error.PluginError(err, { - plugin: that.id - }); - } - } - }) - - .then(that.validate) - - // 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); - }); - - this.log.info('loading plugin "' + this.id + '"... '); - return this.log.info.promise(p); -}; - -// Verify the definition of a plugin -// Also verify that the plugin accepts the current gitbook version -// This method throws erros if plugin is invalid -BookPlugin.prototype.validate = function() { - var isValid = ( - this.isLoaded() && - this.packageInfos && - this.packageInfos.name && - this.packageInfos.engines && - this.packageInfos.engines.gitbook - ); - - if (!isValid) { - throw new Error('Error loading plugin "' + this.id + '" at "' + this.root + '"'); - } - - if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) { - throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook); - } -}; - -// Normalize, validate configuration for this plugin using its schema -// Throw an error when shcema is not respected -BookPlugin.prototype.validateConfig = function(config) { - var that = this; - - return Promise() - .then(function() { - var schema = that.packageInfos.gitbook || {}; - if (!schema) return config; - - // Normalize schema - schema.id = '/'+that.getConfigKey(); - schema.type = 'object'; - - // Validate and throw if invalid - var v = new jsonschema.Validator(); - var result = v.validate(config, schema, { - propertyName: that.getConfigKey() - }); - - // Throw error - if (result.errors.length > 0) { - throw new error.ConfigurationError(new Error(result.errors[0].stack)); - } - - // Insert default values - var defaults = jsonSchemaDefaults(schema); - return mergeDefaults(config, defaults); - }); -}; - -// Return key for configuration -BookPlugin.prototype.getConfigKey = function() { - return 'pluginsConfig.'+this.id; -}; - -// Call a hook and returns its result -BookPlugin.prototype.hook = function(name, input) { - var that = this; - var hookFunc = this.content.hooks? this.content.hooks[name] : null; - input = input || {}; - - if (!hookFunc) return Promise(input); - - this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"'); - if (!_.contains(HOOKS, name)) { - this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions'); - } - - return Promise() - .then(function() { - return that.bind(hookFunc)(input); - }); -}; - -// Return resources without normalization -BookPlugin.prototype._getResources = function(base) { - var that = this; - - return Promise() - .then(function() { - if (that._resources[base]) return that._resources[base]; - - var book = that.content[base]; - - // Compatibility with version 1.x.x - if (base == 'website') book = book || that.content.book; - - // Nothing specified, fallback to default - if (!book) { - return Promise({}); - } - - // Dynamic function - if(typeof book === 'function') { - // Call giving it the context of our book - return that.bind(book)(); - } - - // Plain data object - return book; - }) - - .then(function(resources) { - that._resources[base] = resources; - return _.cloneDeep(resources); - }); -}; - -// Normalize a specific resource -BookPlugin.prototype.normalizeResource = function(resource) { - // Parse the resource path - var parsed = url.parse(resource); - - // This is a remote resource - // so we will simply link to using it's URL - if (parsed.protocol) { - return { - 'url': resource - }; - } - - // This will be copied over from disk - // and shipped with the book's build - return { 'path': this.npmId+'/'+resource }; -}; - - -// Normalize resources and return them -BookPlugin.prototype.getResources = function(base) { - var that = this; - - return this._getResources(base) - .then(function(resources) { - _.each(RESOURCES, function(resourceType) { - resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource); - }); - - return resources; - }); -}; - -// Normalize filters and return them -BookPlugin.prototype.getFilters = function() { - var that = this; - - return _.mapValues(this.content.filters || {}, function(fn, filter) { - return function() { - var ctx = _.extend(compatibility.pluginCtx(that), this); - - return fn.apply(ctx, arguments); - }; - }); -}; - -// Normalize blocks and return them -BookPlugin.prototype.getBlocks = function() { - var that = this; - - return _.mapValues(this.content.blocks || {}, function(block, blockName) { - block = _.isFunction(block)? { process: block } : block; - - var fn = block.process; - block.process = function() { - var ctx = _.extend(compatibility.pluginCtx(that), this); - - return fn.apply(ctx, arguments); - }; - - return block; - }); -}; - -module.exports = BookPlugin; -module.exports.RESOURCES = RESOURCES; - diff --git a/lib/plugins2/registry.js b/lib/plugins2/registry.js deleted file mode 100644 index fe9406dde..000000000 --- a/lib/plugins2/registry.js +++ /dev/null @@ -1,172 +0,0 @@ -var npm = require('npm'); -var npmi = require('npmi'); -var path = require('path'); -var semver = require('semver'); -var _ = require('lodash'); -var readInstalled = require('read-installed'); - -var Promise = require('../utils/promise'); -var gitbook = require('../gitbook'); - -var PLUGIN_PREFIX = 'gitbook-plugin-'; - -// Return an absolute name for the plugin (the one on NPM) -function npmId(name) { - if (name.indexOf(PLUGIN_PREFIX) === 0) return name; - return [PLUGIN_PREFIX, name].join(''); -} - -// Return a plugin ID 9the one on GitBook -function pluginId(name) { - return name.replace(PLUGIN_PREFIX, ''); -} - -// Validate an NPM plugin ID -function validateId(name) { - return name && name.indexOf(PLUGIN_PREFIX) === 0; -} - -// Initialize NPM for operations -var initNPM = _.memoize(function() { - return Promise.nfcall(npm.load, { - silent: true, - loglevel: 'silent' - }); -}); - -// Link a plugin for use in a specific book -function linkPlugin(book, pluginPath) { - book.log('linking', pluginPath); -} - -// Resolve the latest version for a plugin -function resolveVersion(plugin) { - var npnName = npmId(plugin); - - return initNPM() - .then(function() { - return Promise.nfcall(npm.commands.view, [npnName+'@*', 'engines'], true); - }) - .then(function(versions) { - return _.chain(versions) - .pairs() - .map(function(v) { - return { - version: v[0], - gitbook: (v[1].engines || {}).gitbook - }; - }) - .filter(function(v) { - return v.gitbook && gitbook.satisfies(v.gitbook); - }) - .sort(function(v1, v2) { - return semver.lt(v1.version, v2.version)? 1 : -1; - }) - .pluck('version') - .first() - .value(); - }); -} - - -// Install a plugin in a book -function installPlugin(book, plugin, version) { - book.log.info.ln('installing plugin', plugin); - - var npnName = npmId(plugin); - - return Promise() - .then(function() { - if (version) return version; - - book.log.info.ln('No version specified, resolve plugin "' + plugin + '"'); - return resolveVersion(plugin); - }) - - // Install the plugin with the resolved version - .then(function(version) { - if (!version) { - throw new Error('Found no satisfactory version for plugin "' + plugin + '"'); - } - - book.log.info.ln('install plugin "' + plugin +'" from npm ('+npnName+') with version', version); - return Promise.nfcall(npmi, { - 'name': npnName, - 'version': version, - 'path': book.root, - 'npmLoad': { - 'loglevel': 'silent', - 'loaded': true, - 'prefix': book.root - } - }); - }) - .then(function() { - book.log.info.ok('plugin "' + plugin + '" installed with success'); - }); -} - -// List all packages installed inside a folder -// Returns an ordered list of plugins -function listInstalled(folder) { - var options = { - dev: false, - log: function() {}, - depth: 4 - }; - var results = []; - - function onPackage(pkg, isRoot) { - if (!validateId(pkg.name)){ - if (!isRoot) return; - } else { - results.push({ - name: pluginId(pkg.name), - version: pkg.version, - path: pkg.realPath, - depth: pkg.depth - }); - } - - _.each(pkg.dependencies, function(dep) { - onPackage(dep); - }); - } - - return Promise.nfcall(readInstalled, folder, options) - .then(function(data) { - onPackage(data, true); - return _.uniq(results, 'name'); - }); -} - -// List installed plugins for a book (defaults and installed) -function listPlugins(book) { - return Promise.all([ - listInstalled(path.resolve(__dirname, '../..')), - listInstalled(book.root), - book.originalRoot? listInstalled(book.originalRoot) : Promise([]), - book.isLanguageBook()? listInstalled(book.parent.root) : Promise([]) - ]) - .spread(function() { - var args = _.toArray(arguments); - - var results = _.reduce(args, function(out, a) { - return out.concat(a); - }, []); - - return _.uniq(results, 'name'); - }); -} - -module.exports = { - npmId: npmId, - pluginId: pluginId, - validateId: validateId, - - resolve: resolveVersion, - link: linkPlugin, - install: installPlugin, - list: listPlugins, - listInstalled: listInstalled -}; diff --git a/lib/template/blocks.js b/lib/template/blocks.js deleted file mode 100644 index 5dfb0c8c1..000000000 --- a/lib/template/blocks.js +++ /dev/null @@ -1,36 +0,0 @@ -var _ = require('lodash'); - -module.exports = { - // Return non-parsed html - // since blocks are by default non-parsable, a simple identity method works fine - html: _.identity, - - // Highlight a code block - // This block can be replaced by plugins - code: function(blk) { - return { - html: false, - body: blk.body - }; - }, - - // Render some markdown to HTML - markdown: function(blk) { - return this.book.renderInline('markdown', blk.body) - .then(function(out) { - return { body: out }; - }); - }, - asciidoc: function(blk) { - return this.book.renderInline('asciidoc', blk.body) - .then(function(out) { - return { body: out }; - }); - }, - markup: function(blk) { - return this.book.renderInline(this.ctx.file.type, blk.body) - .then(function(out) { - return { body: out }; - }); - } -}; diff --git a/lib/template/filters.js b/lib/template/filters.js deleted file mode 100644 index ac68b82ac..000000000 --- a/lib/template/filters.js +++ /dev/null @@ -1,15 +0,0 @@ -var moment = require('moment'); - - -module.exports = { - // Format a date - // ex: 'MMMM Do YYYY, h:mm:ss a - date: function(time, format) { - return moment(time).format(format); - }, - - // Relative Time - dateFromNow: function(time) { - return moment(time).fromNow(); - } -}; diff --git a/lib/template/index.js b/lib/template/index.js deleted file mode 100644 index ae11bc9bc..000000000 --- a/lib/template/index.js +++ /dev/null @@ -1,552 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var nunjucks = require('nunjucks'); -var escapeStringRegexp = require('escape-string-regexp'); - -var Promise = require('../utils/promise'); -var error = require('../utils/error'); -var parsers = require('../parsers'); -var defaultBlocks = require('./blocks'); -var defaultFilters = require('./filters'); -var Loader = require('./loader'); - -var NODE_ENDARGS = '%%endargs%%'; - -// Return extension name for a specific block -function blockExtName(name) { - return 'Block'+name+'Extension'; -} - -// Normalize the result of block process function -function normBlockResult(blk) { - if (_.isString(blk)) blk = { body: blk }; - return blk; -} - -// Extract kwargs from an arguments array -function extractKwargs(args) { - var last = _.last(args); - return (_.isObject(last) && last.__keywords)? args.pop() : {}; -} - -function TemplateEngine(output) { - this.output = output; - this.book = output.book; - this.log = this.book.log; - - // Create file loader - this.loader = new Loader(this); - - // Create nunjucks instance - this.env = new nunjucks.Environment( - this.loader, - { - // Escaping is done after by the asciidoc/markdown parser - autoescape: false, - - // Syntax - tags: { - blockStart: '{%', - blockEnd: '%}', - variableStart: '{{', - variableEnd: '}}', - commentStart: '{###', - commentEnd: '###}' - } - } - ); - - // List of tags shortcuts - this.shortcuts = []; - - // Map of blocks bodies (that requires post-processing) - this.blockBodies = {}; - - // Map of added blocks - this.blocks = {}; - - // Bind methods - _.bindAll(this); - - // Add default blocks and filters - this.addBlocks(defaultBlocks); - this.addFilters(defaultFilters); - - // Build context for this book with depreacted fields - this.ctx = { - template: this, - book: this.book, - output: this.output - }; - error.deprecateField(this.ctx, 'generator', this.output.name, '"generator" property is deprecated, use "output.generator" instead'); -} - -/* - Bind a function to a context - Filters and blocks are binded to this context. - - @param {Function} - @param {Function} -*/ -TemplateEngine.prototype.bindContext = function(func) { - var that = this; - - return function() { - var ctx = _.extend({ - ctx: this.ctx - }, that.ctx); - - return func.apply(ctx, arguments); - }; -}; - -/* - Interpolate a string content to replace shortcuts according to the filetype. - - @param {String} filepath - @param {String} source - @param {String} -*/ -TemplateEngine.prototype.interpolate = function(filepath, source) { - var parser = parsers.getByExt(path.extname(filepath)); - var type = parser? parser.name : null; - - return this.applyShortcuts(type, source); -}; - -/* - Add a new custom filter, it bind to the right context - - @param {String} - @param {Function} -*/ -TemplateEngine.prototype.addFilter = function(filterName, func) { - try { - this.env.getFilter(filterName); - this.log.error.ln('conflict in filters, "'+filterName+'" is already set'); - return false; - } catch(e) { - // Filter doesn't exist - } - - this.log.debug.ln('add filter "'+filterName+'"'); - this.env.addFilter(filterName, this.bindContext(function() { - var ctx = this; - var args = Array.prototype.slice.apply(arguments); - var callback = _.last(args); - - Promise() - .then(function() { - return func.apply(ctx, args.slice(0, -1)); - }) - .nodeify(callback); - }), true); - return true; -}; - -/* - Add multiple filters at once - - @param {Map} -*/ -TemplateEngine.prototype.addFilters = function(filters) { - _.each(filters, function(filter, name) { - this.addFilter(name, filter); - }, this); -}; - -/* - Return true if a block is defined - - @param {String} -*/ -TemplateEngine.prototype.hasBlock = function(name) { - return this.env.hasExtension(blockExtName(name)); -}; - -/* - Remove/Disable a block - - @param {String} -*/ -TemplateEngine.prototype.removeBlock = function(name) { - if (!this.hasBlock(name)) return; - - // Remove nunjucks extension - this.env.removeExtension(blockExtName(name)); - - // Cleanup shortcuts - this.shortcuts = _.reject(this.shortcuts, { - block: name - }); -}; - -/* - Add a block. - Using the extensions of nunjucks: https://mozilla.github.io/nunjucks/api.html#addextension - - @param {String} name - @param {BlockDescriptor|Function} block - @param {Function} block.process: function to be called to render the block - @param {String} block.end: name of the end tag of this block (default to "end") - @param {Array} block.blocks: list of inner blocks to parse - @param {Array} block.shortcuts: list of shortcuts to parse this block -*/ -TemplateEngine.prototype.addBlock = function(name, block) { - var that = this, Ext, extName; - - // Block can be a simple function - if (_.isFunction(block)) block = { process: block }; - - block = _.defaults(block || {}, { - shortcuts: [], - end: 'end'+name, - blocks: [] - }); - - extName = blockExtName(name); - - if (!block.process) { - throw new Error('Invalid block "' + name + '", it should have a "process" method'); - } - - if (this.hasBlock(name) && !defaultBlocks[name]) { - this.log.warn.ln('conflict in blocks, "'+name+'" is already defined'); - } - - // Cleanup previous block - this.removeBlock(name); - - this.log.debug.ln('add block \''+name+'\''); - this.blocks[name] = block; - - Ext = function () { - this.tags = [name]; - - this.parse = function(parser, nodes) { - var lastBlockName = null; - var lastBlockArgs = null; - var allBlocks = block.blocks.concat([block.end]); - - // Parse first block - var tok = parser.nextToken(); - lastBlockArgs = parser.parseSignature(null, true); - parser.advanceAfterBlockEnd(tok.value); - - var args = new nodes.NodeList(); - var bodies = []; - var blockNamesNode = new nodes.Array(tok.lineno, tok.colno); - var blockArgCounts = new nodes.Array(tok.lineno, tok.colno); - - // Parse while we found "end" - do { - // Read body - var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); - - // Handle body with previous block name and args - blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName)); - blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length)); - bodies.push(currentBody); - - // Append arguments of this block as arguments of the run function - _.each(lastBlockArgs.children, function(child) { - args.addChild(child); - }); - - // Read new block - lastBlockName = parser.nextToken().value; - - // Parse signature and move to the end of the block - if (lastBlockName != block.end) { - lastBlockArgs = parser.parseSignature(null, true); - } - - parser.advanceAfterBlockEnd(lastBlockName); - } while (lastBlockName != block.end); - - args.addChild(blockNamesNode); - args.addChild(blockArgCounts); - args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS)); - - return new nodes.CallExtensionAsync(this, 'run', args, bodies); - }; - - this.run = function(context) { - var fnArgs = Array.prototype.slice.call(arguments, 1); - - var args; - var blocks = []; - var bodies = []; - var blockNames; - var blockArgCounts; - var callback; - - // Extract callback - callback = fnArgs.pop(); - - // Detect end of arguments - var endArgIndex = fnArgs.indexOf(NODE_ENDARGS); - - // Extract arguments and bodies - args = fnArgs.slice(0, endArgIndex); - bodies = fnArgs.slice(endArgIndex + 1); - - // Extract block counts - blockArgCounts = args.pop(); - blockNames = args.pop(); - - // Recreate list of blocks - _.each(blockNames, function(name, i) { - var countArgs = blockArgCounts[i]; - var blockBody = bodies.shift(); - - var blockArgs = countArgs > 0? args.slice(0, countArgs) : []; - args = args.slice(countArgs); - var blockKwargs = extractKwargs(blockArgs); - - blocks.push({ - name: name, - body: blockBody(), - args: blockArgs, - kwargs: blockKwargs - }); - }); - - var mainBlock = blocks.shift(); - mainBlock.blocks = blocks; - - Promise() - .then(function() { - return that.applyBlock(name, mainBlock, context); - }) - - // Process the block returned - .then(that.processBlock) - .nodeify(callback); - }; - }; - - // Add the Extension - this.env.addExtension(extName, new Ext()); - - // Add shortcuts if any - if (!_.isArray(block.shortcuts)) { - block.shortcuts = [block.shortcuts]; - } - - _.each(block.shortcuts, function(shortcut) { - this.log.debug.ln('add template shortcut from "'+shortcut.start+'" to block "'+name+'" for parsers ', shortcut.parsers); - this.shortcuts.push({ - block: name, - parsers: shortcut.parsers, - start: shortcut.start, - end: shortcut.end, - tag: { - start: name, - end: block.end - } - }); - }, this); -}; - -/* - Add multiple blocks at once - - @param {Array} -*/ -TemplateEngine.prototype.addBlocks = function(blocks) { - _.each(blocks, function(block, name) { - this.addBlock(name, block); - }, this); -}; - -/* - Apply a block to some content - This method result depends on the type of block (async or sync) - - - @param {String} name: name of the block type to apply - @param {Block} blk: content of the block - @param {Object} ctx: context of execution of the block - @return {Block|Promise} -*/ -TemplateEngine.prototype.applyBlock = function(name, blk, ctx) { - var func, block, r; - - block = this.blocks[name]; - if (!block) throw new Error('Block not found "'+name+'"'); - if (_.isString(blk)) { - blk = { - body: blk - }; - } - - blk = _.defaults(blk, { - args: [], - kwargs: {}, - blocks: [] - }); - - // Bind and call block processor - func = this.bindContext(block.process); - r = func.call(ctx || {}, blk); - - if (Promise.isPromiseAlike(r)) return Promise(r).then(normBlockResult); - else return normBlockResult(r); -}; - -/* - Process the result of block in a context. It returns the content to append to the output. - It can return an "anchor" that will be replaced by "replaceBlocks" in "postProcess" - - @param {Block} - @return {String} -*/ -TemplateEngine.prototype.processBlock = function(blk) { - blk = _.defaults(blk, { - parse: false, - post: undefined - }); - blk.id = _.uniqueId('blk'); - - var toAdd = (!blk.parse) || (blk.post !== undefined); - - // Add to global map - if (toAdd) this.blockBodies[blk.id] = blk; - - // Parsable block, just return it - if (blk.parse) { - return blk.body; - } - - // Return it as a position marker - return '{{-%'+blk.id+'%-}}'; -}; - -/* - Render a string (without post processing) - - @param {String} content: template's content to render - @param {Object} context - @param {Object} options - @param {String} options.path: pathname to the template - @return {Promise} -*/ -TemplateEngine.prototype.render = function(content, context, options) { - options = _.defaults(options || {}, { - path: null - }); - var filename = options.path; - - // Setup path and type - if (options.path) { - options.path = this.book.resolve(options.path); - } - - // Replace shortcuts - content = this.applyShortcuts(options.type, content); - - return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options) - .fail(function(err) { - throw error.TemplateError(err, { - filename: filename || '' - }); - }); -}; - -/* - Render a string (with post processing) - - @param {String} content: template's content to render - @param {Object} context - @param {Object} options - @return {Promise} -*/ -TemplateEngine.prototype.renderString = function(content, context, options) { - return this.render(content, context, options) - .then(this.postProcess); -}; - -/* - Apply a shortcut of block to a template - - @param {String} content - @param {Shortcut} shortcut - @return {String} -*/ -TemplateEngine.prototype.applyShortcut = function(content, shortcut) { - var regex = new RegExp( - escapeStringRegexp(shortcut.start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(shortcut.end), - 'g' - ); - return content.replace(regex, function(all, match) { - return '{% '+shortcut.tag.start+' %}'+ match + '{% '+shortcut.tag.end+' %}'; - }); -}; - - -/* - Apply all shortcut of blocks to a template - - @param {String} type: type of template ("markdown", "asciidoc") - @param {String} content - @return {String} -*/ -TemplateEngine.prototype.applyShortcuts = function(type, content) { - return _.chain(this.shortcuts) - .filter(function(shortcut) { - return _.contains(shortcut.parsers, type); - }) - .reduce(this.applyShortcut, content) - .value(); -}; - -/* - Replace position markers of blocks by body after processing - This is done to avoid that markdown/asciidoc processer parse the block content - - @param {String} content - @return {String} -*/ -TemplateEngine.prototype.replaceBlocks = function(content) { - var that = this; - - return content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) { - var blk = that.blockBodies[key]; - if (!blk) return match; - - var body = blk.body; - - return body; - }); -}; - - - -/* - Post process templating result: remplace block's anchors and apply "post" - - @param {String} content - @return {Promise} -*/ -TemplateEngine.prototype.postProcess = function(content) { - var that = this; - - return Promise(content) - .then(that.replaceBlocks) - .then(function(_content) { - return Promise.serie(that.blockBodies, function(blk, blkId) { - return Promise() - .then(function() { - if (!blk.post) return; - return blk.post(); - }) - .then(function() { - delete that.blockBodies[blkId]; - }); - }) - .thenResolve(_content); - }); -}; - -module.exports = TemplateEngine; diff --git a/lib/template/loader.js b/lib/template/loader.js deleted file mode 100644 index 23d179a83..000000000 --- a/lib/template/loader.js +++ /dev/null @@ -1,42 +0,0 @@ -var nunjucks = require('nunjucks'); -var location = require('../utils/location'); - -/* -Simple nunjucks loader which is passing the reponsability to the Output -*/ - -var Loader = nunjucks.Loader.extend({ - async: true, - - init: function(engine, opts) { - this.engine = engine; - this.output = engine.output; - }, - - getSource: function(sourceURL, callback) { - var that = this; - - this.output.onGetTemplate(sourceURL) - .then(function(out) { - // We disable cache since content is modified (shortcuts, ...) - out.noCache = true; - - // Transform template before runnign it - out.source = that.engine.interpolate(out.path, out.source); - - return out; - }) - .nodeify(callback); - }, - - resolve: function(from, to) { - return this.output.onResolveTemplate(from, to); - }, - - // Handle all files as relative, so that nunjucks pass responsability to 'resolve' - isRelative: function(filename) { - return location.isRelative(filename); - } -}); - -module.exports = Loader; From a3df6c6f0c1068762f9d48cdff97ab5d4c583082 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 23 Apr 2016 21:10:53 +0200 Subject: [PATCH 008/118] Add assets inliner modifier for HTML --- lib/output/ebook/index.js | 5 + lib/output/ebook/onPage.js | 24 +++++ lib/output/index.js | 3 +- lib/output/json/onPage.js | 3 +- .../modifiers/__tests__/addHeadingId.js | 29 ++++++ lib/output/modifiers/addHeadingId.js | 22 ++++- lib/output/modifiers/annotateText.js | 94 +++++++++++++++++++ lib/output/modifiers/editHTMLElement.js | 15 +++ lib/output/modifiers/fetchRemoteImages.js | 38 ++++++++ lib/output/modifiers/htmlTransform.js | 16 ---- lib/output/modifiers/index.js | 11 ++- lib/output/modifiers/inlineAssets.js | 16 +++- lib/output/modifiers/modifyHTML.js | 15 ++- lib/output/modifiers/resolveLinks.js | 13 +++ lib/output/modifiers/svgToImg.js | 40 ++++++-- lib/output/modifiers/svgToPng.js | 38 ++++++++ lib/utils/fs.js | 17 ++++ package.json | 1 + 18 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 lib/output/ebook/index.js create mode 100644 lib/output/ebook/onPage.js create mode 100644 lib/output/modifiers/__tests__/addHeadingId.js create mode 100644 lib/output/modifiers/annotateText.js create mode 100644 lib/output/modifiers/editHTMLElement.js create mode 100644 lib/output/modifiers/fetchRemoteImages.js delete mode 100644 lib/output/modifiers/htmlTransform.js create mode 100644 lib/output/modifiers/resolveLinks.js create mode 100644 lib/output/modifiers/svgToPng.js diff --git a/lib/output/ebook/index.js b/lib/output/ebook/index.js new file mode 100644 index 000000000..46a94e3f1 --- /dev/null +++ b/lib/output/ebook/index.js @@ -0,0 +1,5 @@ + +module.exports = { + + +}; diff --git a/lib/output/ebook/onPage.js b/lib/output/ebook/onPage.js new file mode 100644 index 000000000..ab1513365 --- /dev/null +++ b/lib/output/ebook/onPage.js @@ -0,0 +1,24 @@ +var website = require('../website'); +var Modifier = require('../modifier'); + +/** + Write a page for ebook output + + @param {Output} output + @param {Page} page +*/ +function onPage(output, page) { + var options = output.getOptions(); + + // Inline assets + return Modifier.modifyHTML(page, [ + Modifier.inlineAssets(options.get('root')) + ]) + + // Write page using website generator + .then(function(resultPage) { + return website.onPage(output, resultPage); + }); +} + +module.exports = onPage; diff --git a/lib/output/index.js b/lib/output/index.js index 45bdf6579..d3534908d 100644 --- a/lib/output/index.js +++ b/lib/output/index.js @@ -1,4 +1,5 @@ module.exports = { generate: require('./generateBook'), - JSONGenerator: require('./json') + JSONGenerator: require('./json'), + EbookGenerator: require('./ebook') }; diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index a84e297c5..1143aaae8 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -10,7 +10,8 @@ var Writer = require('../writer'); */ function onPage(output, page) { return Modifier.modifyHTML(page, [ - Modifier.HTML.resolveLinks() + Modifier.addHeadingId, + Modifier.resolveLinks ]) .then(function(resultPage) { // Generate the JSON diff --git a/lib/output/modifiers/__tests__/addHeadingId.js b/lib/output/modifiers/__tests__/addHeadingId.js new file mode 100644 index 000000000..7277440ea --- /dev/null +++ b/lib/output/modifiers/__tests__/addHeadingId.js @@ -0,0 +1,29 @@ +jest.autoMockOff(); + +var cheerio = require('cheerio'); + +describe('addHeadingId', function() { + var addHeadingId = require('../addHeadingId'); + + pit('should add an ID if none', function() { + var $ = cheerio.load('

Hello World

Cool !!

'); + + return addHeadingId($) + .then(function() { + var html = $.html(); + expect(html).toBe('

Hello World

Cool !!

'); + }); + }); + + pit('should not change existing IDs', function() { + var $ = cheerio.load('

Hello World

'); + + return addHeadingId($) + .then(function() { + var html = $.html(); + expect(html).toBe('

Hello World

'); + }); + }); +}); + + diff --git a/lib/output/modifiers/addHeadingId.js b/lib/output/modifiers/addHeadingId.js index 751f4b82d..e2e27200d 100644 --- a/lib/output/modifiers/addHeadingId.js +++ b/lib/output/modifiers/addHeadingId.js @@ -1,9 +1,23 @@ var slug = require('github-slugid'); -var HTMLModifier = require('./html'); +var editHTMLElement = require('./editHTMLElement'); -var addHeadingID = HTMLModifier('h1,h2,h3,h4,h5,h6', function(heading) { +/** + Add ID to an heading + + @param {HTMLElement} heading +*/ +function addId(heading) { if (heading.attr('id')) return; heading.attr('id', slug(heading.text())); -}); +} -module.exports = addHeadingID; +/** + Add ID to all headings + + @param {HTMLDom} $ +*/ +function addHeadingId($) { + return editHTMLElement($, 'h1,h2,h3,h4,h5,h6', addId); +} + +module.exports = addHeadingId; diff --git a/lib/output/modifiers/annotateText.js b/lib/output/modifiers/annotateText.js new file mode 100644 index 000000000..2832f9152 --- /dev/null +++ b/lib/output/modifiers/annotateText.js @@ -0,0 +1,94 @@ +var escape = require('escape-html'); + +// Selector to ignore +var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6'; + +function pregQuote( str ) { + return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); +} + +function replaceText($, el, search, replace, text_only ) { + return $(el).each(function(){ + var node = this.firstChild, + val, + new_val, + + // Elements to be removed at the end. + remove = []; + + // Only continue if firstChild exists. + if ( node ) { + + // Loop over all childNodes. + while (node) { + + // Only process text nodes. + if ( node.nodeType === 3 ) { + + // The original node value. + val = node.nodeValue; + + // The new value. + new_val = val.replace( search, replace ); + + // Only replace text if the new value is actually different! + if ( new_val !== val ) { + + if ( !text_only && /} + @param {HTMLDom} $ +*/ +function annotateText(entries, $) { + entries.forEach(function(entry) { + var entryId = entry.getID(); + var name = entry.getName(); + var description = entry.getDescription(); + + var searchRegex = new RegExp( '\\b(' + pregQuote(name.toLowerCase()) + ')\\b' , 'gi' ); + + $('*').each(function() { + var $this = $(this); + + if ( + $this.is(ANNOTATION_IGNORE) || + $this.parents(ANNOTATION_IGNORE).length > 0 + ) return; + + replaceText($, this, searchRegex, function(match) { + return '' + + match + + ''; + }); + }); + + }); +} + +module.exports = annotateText; diff --git a/lib/output/modifiers/editHTMLElement.js b/lib/output/modifiers/editHTMLElement.js new file mode 100644 index 000000000..9897dcc7e --- /dev/null +++ b/lib/output/modifiers/editHTMLElement.js @@ -0,0 +1,15 @@ +var Promise = require('../../utils/promise'); + +/** + Edit all elements matching a selector +*/ +function editHTMLElement($, selector, fn) { + var $elements = $(selector); + + return Promise.forEach($elements, function(el) { + var $el = $(el); + fn($el); + }); +} + +module.exports = editHTMLElement; diff --git a/lib/output/modifiers/fetchRemoteImages.js b/lib/output/modifiers/fetchRemoteImages.js new file mode 100644 index 000000000..792bbb6be --- /dev/null +++ b/lib/output/modifiers/fetchRemoteImages.js @@ -0,0 +1,38 @@ +var path = require('path'); +var crc = require('crc'); + +var editHTMLElement = require('./editHTMLElement'); +var fs = require('../../utils/fs'); +var location = require('../../utils/location'); + +/** + Fetch all remote images + + @param {String} rootFolder + @param {HTMLDom} $ + @return {Promise} +*/ +function fetchRemoteImages(rootFolder, $) { + return editHTMLElement($, 'img', function($img) { + var src = $img.attr('src'); + var extension = path.extname(src); + + if (!location.isExternal(src)) { + return; + } + + // We avoid generating twice the same PNG + var hash = crc.crc32(src).toString(16); + var fileName = hash + extension; + var filePath = path.join(rootFolder, fileName); + + return fs.assertFile(filePath, function() { + return fs.download(src, filePath); + }) + .then(function() { + $img.replaceWith(''); + }); + }); +} + +module.exports = fetchRemoteImages; diff --git a/lib/output/modifiers/htmlTransform.js b/lib/output/modifiers/htmlTransform.js deleted file mode 100644 index 528b08d63..000000000 --- a/lib/output/modifiers/htmlTransform.js +++ /dev/null @@ -1,16 +0,0 @@ -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 index ced87164d..75b59274a 100644 --- a/lib/output/modifiers/index.js +++ b/lib/output/modifiers/index.js @@ -1,4 +1,13 @@ module.exports = { - modifyHTML: require('./modifyHTML') + modifyHTML: require('./modifyHTML'), + inlineAssets: require('./inlineAssets'), + + // HTML transformations + addHeadingId: require('./addHeadingId'), + svgToImg: require('./svgToImg'), + fetchRemoteImages: require('./fetchRemoteImages'), + svgToPng: require('./svgToPng'), + resolveLinks: require('./resolveLinks'), + annotateText: require('./annotateText') }; diff --git a/lib/output/modifiers/inlineAssets.js b/lib/output/modifiers/inlineAssets.js index 190a94509..044953b99 100644 --- a/lib/output/modifiers/inlineAssets.js +++ b/lib/output/modifiers/inlineAssets.js @@ -1,11 +1,21 @@ +var svgToImg = require('./svgToImg'); +var svgToPng = require('./svgToPng'); +var fetchRemoteImages = require('./fetchRemoteImages'); +var Promise = require('../../utils/promise'); /** + Inline all assets in a page + @param {String} rootFolder */ -function inlineAssets() { - - +function inlineAssets(rootFolder) { + return function($) { + return Promise() + .then(fetchRemoteImages.bind(null, rootFolder)) + .then(svgToImg.bind(null, rootFolder)) + .then(svgToPng.bind(null, rootFolder)); + }; } module.exports = inlineAssets; diff --git a/lib/output/modifiers/modifyHTML.js b/lib/output/modifiers/modifyHTML.js index c1dad7480..d46a44f1a 100644 --- a/lib/output/modifiers/modifyHTML.js +++ b/lib/output/modifiers/modifyHTML.js @@ -1,15 +1,24 @@ - +var cheerio = require('cheerio'); +var Promise = require('../../utils/promise'); /** Apply a list of operations to a page and output the new page. @param {Page} - @param {List} + @param {List|Array} + @return {Promise} */ function modifyHTML(page, operations) { + var html = page.getContent(); + var $ = cheerio.load(html); + return Promise.forEach(operations, function(op) { + op($); + }) + .then(function(resultHTML) { + return page.set('content', resultHTML); + }); } - module.exports = modifyHTML; diff --git a/lib/output/modifiers/resolveLinks.js b/lib/output/modifiers/resolveLinks.js new file mode 100644 index 000000000..5c2514f13 --- /dev/null +++ b/lib/output/modifiers/resolveLinks.js @@ -0,0 +1,13 @@ + + +/** + Resolve all HTML links + + @param {String} + @param {HTMLDom} $ +*/ +function resolveLinks() { + +} + +module.exports = resolveLinks; diff --git a/lib/output/modifiers/svgToImg.js b/lib/output/modifiers/svgToImg.js index b36770a85..d088e3e00 100644 --- a/lib/output/modifiers/svgToImg.js +++ b/lib/output/modifiers/svgToImg.js @@ -1,7 +1,17 @@ -var cheerio = require('cheerio'); +var path = require('path'); var domSerializer = require('dom-serializer'); -// Render a cheerio DOM as html +var editHTMLElement = require('./editHTMLElement'); +var fs = require('../../utils/fs'); + +/** + Render a cheerio DOM as html + + @param {HTMLDom} $ + @param {HTMLElement} dom + @param {Object} + @return {String} +*/ function renderDOM($, dom, options) { if (!dom && $._root && $._root.children) { dom = $._root.children; @@ -11,18 +21,30 @@ function renderDOM($, dom, options) { } /** + Replace SVG tag by IMG + @param {String} baseFolder + @param {HTMLDom} $ */ -var svgToImg = HTMLModifier('svg', function($svg, $) { - var content = '' + renderDOM($, $svg); +function svgToImg(baseFolder, $) { + return editHTMLElement($, 'svg', function($svg) { + var content = '' + + renderDOM($, $svg); + // Generate a filename + return fs.uniqueFilename(baseFolder, 'image.svg') + .then(function(fileName) { + var filePath = path.join(baseFolder, fileName); + // Write the svg to the file + return fs.writeFile(filePath, content, 'utf8') -}); - -function svgToImg(page) { - var $ = cheerio.load(page.content); - + // Return as image + .then(function() { + $svg.replaceWith(''); + }); + }); + }); } module.exports = svgToImg; diff --git a/lib/output/modifiers/svgToPng.js b/lib/output/modifiers/svgToPng.js new file mode 100644 index 000000000..4f8644a98 --- /dev/null +++ b/lib/output/modifiers/svgToPng.js @@ -0,0 +1,38 @@ +var crc = require('crc'); +var path = require('path'); + +var imagesUtil = require('../../utils/images'); +var fs = require('../../utils/fs'); + +var editHTMLElement = require('./editHTMLElement'); + +/** + Convert all SVG images to PNG + + @param {String} rootFolder + @param {HTMLDom} $ + @return {Promise} +*/ +function svgToPng(rootFolder, $) { + return editHTMLElement($, 'img', function($img) { + var src = $img.attr('src'); + if (path.extname(src) !== '.svg') { + return; + } + + // We avoid generating twice the same PNG + var hash = crc.crc32(src).toString(16); + var fileName = hash + '.png'; + var filePath = path.join(rootFolder, fileName); + + return fs.assertFile(filePath, function() { + return imagesUtil.convertSVGToPNG(src, filePath); + }) + .then(function() { + $img.replaceWith(''); + }); + }); +} + + +module.exports = svgToPng; diff --git a/lib/utils/fs.js b/lib/utils/fs.js index 42fd3c612..96a0e49b0 100644 --- a/lib/utils/fs.js +++ b/lib/utils/fs.js @@ -97,12 +97,29 @@ function rmDir(base) { }); } +/** + Assert a file, if it doesn't exist, call "generator" + + @param {String} filePath + @param {Function} generator + @return {Promise} +*/ +function assertFile(filePath, generator) { + return fileExists(filePath) + .then(function(exists) { + if (exists) return; + + generator(); + }); +} + module.exports = { exists: fileExists, existsSync: fs.existsSync, mkdirp: Promise.nfbind(mkdirp), readFile: Promise.nfbind(fs.readFile), writeFile: Promise.nfbind(fs.writeFile), + assertFile: assertFile, stat: Promise.nfbind(fs.stat), statSync: fs.statSync, readdir: Promise.nfbind(fs.readdir), diff --git a/package.json b/package.json index 5f35410a6..71674d44c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "direction": "0.1.5", "dom-serializer": "0.1.0", "error": "7.0.2", + "escape-html": "^1.0.3", "escape-string-regexp": "1.0.5", "eslint": "^2.2.0", "front-matter": "2.0.7", From 927185d95c9875a9b74d176b93669ebdbceecd14 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 23 Apr 2016 23:58:08 +0200 Subject: [PATCH 009/118] add test for modifier "annotateText" --- .../modifiers/__tests__/annotateText.js | 49 +++++++++++++++++++ lib/output/modifiers/annotateText.js | 2 +- lib/output/modifiers/resolveLinks.js | 31 ++++++++++-- lib/utils/location.js | 21 ++++++-- 4 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 lib/output/modifiers/__tests__/annotateText.js diff --git a/lib/output/modifiers/__tests__/annotateText.js b/lib/output/modifiers/__tests__/annotateText.js new file mode 100644 index 000000000..15d4c30e3 --- /dev/null +++ b/lib/output/modifiers/__tests__/annotateText.js @@ -0,0 +1,49 @@ +jest.autoMockOff(); + +var Immutable = require('immutable'); +var cheerio = require('cheerio'); +var GlossaryEntry = require('../../../models/glossaryEntry'); + +describe('annotateText', function() { + var annotateText = require('../annotateText'); + + var entries = Immutable.List([ + GlossaryEntry({ name: 'Word' }), + GlossaryEntry({ name: 'Multiple Words' }) + ]); + + it('should annotate text', function() { + var $ = cheerio.load('

This is a word, and multiple words

'); + + annotateText(entries, $); + + var links = $('a'); + expect(links.length).toBe(2); + + var word = $(links.get(0)); + expect(word.attr('href')).toBe('/GLOSSARY.md#word'); + expect(word.text()).toBe('word'); + expect(word.hasClass('glossary-term')).toBeTruthy(); + + var words = $(links.get(1)); + expect(words.attr('href')).toBe('/GLOSSARY.md#multiple-words'); + expect(words.text()).toBe('multiple words'); + expect(words.hasClass('glossary-term')).toBeTruthy(); + }); + + it('should not annotate scripts', function() { + var $ = cheerio.load(''); + + annotateText(entries, $); + expect($('a').length).toBe(0); + }); + + it('should not annotate when has class "no-glossary"', function() { + var $ = cheerio.load('

This is a word, and multiple words

'); + + annotateText(entries, $); + expect($('a').length).toBe(0); + }); +}); + + diff --git a/lib/output/modifiers/annotateText.js b/lib/output/modifiers/annotateText.js index 2832f9152..d8443cf7f 100644 --- a/lib/output/modifiers/annotateText.js +++ b/lib/output/modifiers/annotateText.js @@ -81,7 +81,7 @@ function annotateText(entries, $) { ) return; replaceText($, this, searchRegex, function(match) { - return '' + match + ''; diff --git a/lib/output/modifiers/resolveLinks.js b/lib/output/modifiers/resolveLinks.js index 5c2514f13..b5ad4f305 100644 --- a/lib/output/modifiers/resolveLinks.js +++ b/lib/output/modifiers/resolveLinks.js @@ -1,13 +1,38 @@ +var path = require('path'); +var LocationUtils = require('../../utils/location'); +var editHTMLElement = require('./editHTMLElement'); /** - Resolve all HTML links + Resolve all HTML links: + - /test.md in hello -> ../test.html - @param {String} + @param {String} currentFile + @param {Function(String) -> String} resolveFile @param {HTMLDom} $ */ -function resolveLinks() { +function resolveLinks(currentFile, resolveFile, $) { + var currentDirectory = path.dirname(currentFile); + return editHTMLElement($, 'a', function($a) { + var href = $a.attr('href'); + + if (location.isExternal(href)) { + $a.attr('_target', 'blank'); + return; + } + + // Calcul absolute path for this + href = LocationUtils.toAbsolute(href, currentDirectory, currentDirectory); + + // Resolve file + href = resolveFile(href); + + // Convert back to relative + href = LocationUtils.relative(currentDirectory, href); + + $a.attr('href', href); + }); } module.exports = resolveLinks; diff --git a/lib/utils/location.js b/lib/utils/location.js index ba43644df..f1159f1c6 100644 --- a/lib/utils/location.js +++ b/lib/utils/location.js @@ -30,9 +30,14 @@ function normalize(s) { return path.normalize(s).replace(/\\/g, '/'); } -// Convert relative to absolute path -// dir: directory parent of the file currently in rendering process -// outdir: directory parent from the html output +/** + Convert relative to absolute path + + @param {String} href + @param {String} dir: directory parent of the file currently in rendering process + @param {String} outdir: directory parent from the html output + @return {String} +*/ function toAbsolute(_href, dir, outdir) { if (isExternal(_href)) return _href; outdir = outdir == undefined? dir : outdir; @@ -54,8 +59,14 @@ function toAbsolute(_href, dir, outdir) { return _href; } -// Convert an absolute path to a relative path for a specific folder (dir) -// ('test/', 'hello.md') -> '../hello.md' +/** + Convert an absolute path to a relative path for a specific folder (dir) + ('test/', 'hello.md') -> '../hello.md' + + @param {String} dir: current directory + @param {String} file: absolute path of file + @return {String} +*/ function relative(dir, file) { return normalize(path.relative(dir, file)); } From c3275a4aa985710c0fcc9d3f7104bc5ebed2eb04 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sun, 24 Apr 2016 00:15:38 +0200 Subject: [PATCH 010/118] Parse multilingual books --- lib/models/book.js | 41 +++++++++++++++++++++++++-- lib/parse/index.js | 3 +- lib/parse/parseBook.js | 55 +++++++++++++++++++++++++++++++++---- lib/parse/parseLanguages.js | 22 +++++++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 lib/parse/parseLanguages.js diff --git a/lib/models/book.js b/lib/models/book.js index da0deee62..62faba6c1 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -27,7 +27,14 @@ var Book = Immutable.Record({ readme: Readme(), summary: Summary(), glossary: Glossary(), - languages: Languages() + languages: Languages(), + + + // Parent of this book, if multilingual + parent: null, + + // List of children, if multilingual + books: Immutable.List() }); Book.prototype.getLogger = function() { @@ -62,8 +69,12 @@ Book.prototype.getLanguages = function() { return this.get('languages'); }; -Book.prototype.getPages = function() { - return this.get('pages'); +Book.prototype.getParent = function() { + return this.get('parent'); +}; + +Book.prototype.getBooks = function() { + return this.get('books'); }; /** @@ -131,6 +142,15 @@ Book.prototype.getPage = function(ref) { return this.getPages().get(ref); }; +/** + Is this book the parent of language's books + + @return {Boolean} +*/ +Book.prototype.isMultilingual = function() { + return (this.getLanguages().getList().size > 0); +}; + /** Create a book using a filesystem @@ -143,4 +163,19 @@ Book.createForFS = function createForFS(fs) { }); }; +/** + Create a language book from a parent + + @param {Book} parent + @param {String} basePath: folder o the book in the parent + @return {Book} +*/ +Book.createFromParent = function createFromParent(parent, basePath) { + return new Book({ + logger: parent.getLogger(), + parent: parent, + fs: FS.reduceScope(book.getFS(), basePath) + }); +}; + module.exports = Book; diff --git a/lib/parse/index.js b/lib/parse/index.js index 58561fbe5..785b67efb 100644 --- a/lib/parse/index.js +++ b/lib/parse/index.js @@ -7,5 +7,6 @@ module.exports = { parseConfig: require('./parseConfig'), parsePagesList: require('./parsePagesList'), parseIgnore: require('./parseIgnore'), - listAssets: require('./listAssets') + listAssets: require('./listAssets'), + parseLanguages: require('./parseLanguages') }; diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index 4af176841..3a7d845e0 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -1,11 +1,52 @@ var Promise = require('../utils/promise'); +var Book = require('../models/book'); var parseIgnore = require('./parseIgnore'); var parseConfig = require('./parseConfig'); var parseGlossary = require('./parseGlossary'); var parseSummary = require('./parseSummary'); var parseReadme = require('./parseReadme'); -//var parseLanguages = require('./parseLanguages'); +var parseLanguages = require('./parseLanguages'); + +/** + Parse content of a book + + @param {Book} book + @return {Promise} +*/ +function parseBookContent(book) { + return Promise(book) + .then(parseReadme) + .then(parseSummary) + .then(parseGlossary); +} + +/** + Parse a multilingual book + + @param {Book} book + @return {Promise} +*/ +function parseMultilingualBook(book) { + var languages = book.getLanguages(); + var langList = languages.getList(); + + return Promise.reduce(langList, function(currentBook, lang) { + var child = Book.createFromParent(currentBook, lang.getPath()); + + return Promise(child) + .then(parseIgnore) + .then(parseConfig) + .then(parseBookContent) + .then(function(result) { + var books = currentBook.getBooks(); + books = books.push(result); + + return currentBook.set('books', books); + }); + }, book); +} + /** Parse a whole book from a filesystem @@ -17,10 +58,14 @@ function parseBook(book) { return Promise(book) .then(parseIgnore) .then(parseConfig) - //.then(parseLanguages) - .then(parseReadme) - .then(parseSummary) - .then(parseGlossary); + .then(parseLanguages) + .then(function(resultBook) { + if (book.isMultilingual()) { + return parseMultilingualBook(resultBook); + } else { + return parseBookContent(resultBook); + } + }); } module.exports = parseBook; diff --git a/lib/parse/parseLanguages.js b/lib/parse/parseLanguages.js new file mode 100644 index 000000000..d78881f1c --- /dev/null +++ b/lib/parse/parseLanguages.js @@ -0,0 +1,22 @@ +var parseStructureFile = require('./parseStructureFile'); +var Languages = require('../models/languages'); + +/** + Parse languages list from book + + @param {Book} book + @return {Promise} +*/ +function parseLanguages(book) { + return parseStructureFile(book, 'langs') + .spread(function(file, result) { + if (!file) { + return; + } + + var languages = Languages.createFromList(file, result); + return book.set('languages', languages); + }); +} + +module.exports = parseLanguages; From 19a1db8cb43431352d30dbc48aebb9cc8bf58eec Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sun, 24 Apr 2016 18:52:37 +0200 Subject: [PATCH 011/118] Add base structure for templating --- lib/models/book.js | 2 +- lib/models/plugin.js | 24 +++ lib/models/templateBlock.js | 264 +++++++++++++++++++++++++++++ lib/models/templateEngine.js | 106 ++++++++++++ lib/output/createTemplateEngine.js | 31 ++++ lib/utils/genKey.js | 13 ++ 6 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 lib/models/templateBlock.js create mode 100644 lib/models/templateEngine.js create mode 100644 lib/output/createTemplateEngine.js create mode 100644 lib/utils/genKey.js diff --git a/lib/models/book.js b/lib/models/book.js index 62faba6c1..9b9f7690f 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -174,7 +174,7 @@ Book.createFromParent = function createFromParent(parent, basePath) { return new Book({ logger: parent.getLogger(), parent: parent, - fs: FS.reduceScope(book.getFS(), basePath) + fs: FS.reduceScope(parent.getFS(), basePath) }); }; diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 6d322f435..2f791dcc6 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -1,5 +1,6 @@ var Immutable = require('immutable'); +var TemplateBlock = require('./templateBlock'); var PREFIX = require('../constants/pluginPrefix'); var DEFAULT_VERSION = '*'; @@ -72,6 +73,29 @@ Plugin.prototype.getHooks = function() { return this.getContent().get('hooks'); }; +/** + Return map of filters + @return {Map} +*/ +Plugin.prototype.getFilters = function() { + return this.getContent().get('filters'); +}; + +/** + Return map of blocks + @return {List} +*/ +Plugin.prototype.getBlocks = function() { + var blocks = this.getContent().get('blocks'); + + return blocks + .map(function(block, blockName) { + block.name = blockName; + return new TemplateBlock(block); + }) + .toList(); +}; + /** Return a specific hook diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js new file mode 100644 index 000000000..ec7dc7c5a --- /dev/null +++ b/lib/models/templateBlock.js @@ -0,0 +1,264 @@ +var is = require('is'); +var Immutable = require('immutable'); + +var Promise = require('../utils/promise'); +var genKey = require('../utils/genKey'); + +var NODE_ENDARGS = '%%endargs%%'; + +var blockBodies = {}; + +var TemplateBlock = Immutable.Record({ + name: String(), + end: String(), + process: Function(), + blocks: Immutable.List(), + shortcuts: Immutable.List(), + post: null, + parse: true +}); + +TemplateBlock.prototype.getName = function() { + return this.get('name'); +}; + +TemplateBlock.prototype.getPost = function() { + return this.get('post'); +}; + +TemplateBlock.prototype.getParse = function() { + return this.get('parse'); +}; + +TemplateBlock.prototype.getEndTag = function() { + return this.get('end') || ('end' + this.getName()); +}; + +TemplateBlock.prototype.getProcess = function() { + return this.get('process'); +}; + +TemplateBlock.prototype.getBlocks = function() { + return this.get('blocks'); +}; + +TemplateBlock.prototype.getShortcuts = function() { + return this.get('shortcuts'); +}; + +/** + Return name for the nunjucks extension + + @return {String} +*/ +TemplateBlock.prototype.getExtensionName = function() { + return 'Block' + this.getName() + 'Extension'; +}; + +/** + Return a nunjucks extension to represents this block + + @return {Nunjucks.Extension} +*/ +TemplateBlock.prototype.toNunjucksExt = function() { + var that = this; + var name = this.getName(); + var endTag = this.getEndTag(); + var blocks = this.getBlocks(); + + var Ext = function () { + this.tags = [name]; + + this.parse = function(parser, nodes) { + var lastBlockName = null; + var lastBlockArgs = null; + var allBlocks = blocks.concat([endTag]); + + // Parse first block + var tok = parser.nextToken(); + lastBlockArgs = parser.parseSignature(null, true); + parser.advanceAfterBlockEnd(tok.value); + + var args = new nodes.NodeList(); + var bodies = []; + var blockNamesNode = new nodes.Array(tok.lineno, tok.colno); + var blockArgCounts = new nodes.Array(tok.lineno, tok.colno); + + // Parse while we found "end" + do { + // Read body + var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); + + // Handle body with previous block name and args + blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName)); + blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length)); + bodies.push(currentBody); + + // Append arguments of this block as arguments of the run function + lastBlockArgs.children.forEach(function(child) { + args.addChild(child); + }); + + // Read new block + lastBlockName = parser.nextToken().value; + + // Parse signature and move to the end of the block + if (lastBlockName != endTag) { + lastBlockArgs = parser.parseSignature(null, true); + } + + parser.advanceAfterBlockEnd(lastBlockName); + } while (lastBlockName != endTag); + + args.addChild(blockNamesNode); + args.addChild(blockArgCounts); + args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS)); + + return new nodes.CallExtensionAsync(this, 'run', args, bodies); + }; + + this.run = function(context) { + var fnArgs = Array.prototype.slice.call(arguments, 1); + + var args; + var blocks = []; + var bodies = []; + var blockNames; + var blockArgCounts; + var callback; + + // Extract callback + callback = fnArgs.pop(); + + // Detect end of arguments + var endArgIndex = fnArgs.indexOf(NODE_ENDARGS); + + // Extract arguments and bodies + args = fnArgs.slice(0, endArgIndex); + bodies = fnArgs.slice(endArgIndex + 1); + + // Extract block counts + blockArgCounts = args.pop(); + blockNames = args.pop(); + + // Recreate list of blocks + blockNames.forEach(function(name, i) { + var countArgs = blockArgCounts[i]; + var blockBody = bodies.shift(); + + var blockArgs = countArgs > 0? args.slice(0, countArgs) : []; + args = args.slice(countArgs); + var blockKwargs = extractKwargs(blockArgs); + + blocks.push({ + name: name, + body: blockBody(), + args: blockArgs, + kwargs: blockKwargs + }); + }); + + var mainBlock = blocks.shift(); + mainBlock.blocks = blocks; + + Promise() + .then(function() { + return that.applyBlock(mainBlock, context); + }) + .then(function(result) { + return that.blockResultToHtml(result); + }) + .nodeify(callback); + }; + }; + + return Ext; +}; + +/** + Apply a block to a content + @param {Object} inner + @param {Object} context + @return {Promise|String} +*/ +TemplateBlock.prototype.applyBlock = function(inner, context) { + var processFn = this.getProcess(); + + inner = inner || {}; + inner.args = inner.args || []; + inner.kwargs = inner.kwargs || {}; + inner.blocks = inner.blocks || []; + + var r = processFn.call(context, inner); + + if (Promise.isPromiseAlike(r)) { + return r.then(this.handleBlockResult); + } else { + return this.handleBlockResult(r); + } +}; + +/** + Handle result from a block process function + + @param {Object} result + @return {Object} +*/ +TemplateBlock.prototype.handleBlockResult = function(result) { + if (is.string(result)) { + result = { body: result }; + } + + return result; +}; + +/** + Convert a block result to HTML + + @param {Object} result + @return {String} +*/ +TemplateBlock.prototype.blockResultToHtml = function(result) { + var parse = this.getParse(); + var indexedKey; + var toIndex = (!parse) || (this.getPost() !== undefined); + + if (toIndex) { + indexedKey = TemplateBlock.indexBlockResult(result); + } + + // Parsable block, just return it + if (parse) { + return result.body; + } + + // Return it as a position marker + return '{{-%' + indexedKey + '%-}}'; + +}; + +/** + Index a block result, and return the indexed key + + @param {Object} blk + @return {String} +*/ +TemplateBlock.indexBlockResult = function(blk) { + var key = genKey(); + blockBodies[key] = blk; + + return key; +}; + +/** + Extract kwargs from an arguments array + + @param {Array} args + @return {Object} +*/ +function extractKwargs(args) { + var last = args[args.length - 1]; + return (is.object(last) && last.__keywords)? args.pop() : {}; +} + +module.exports = TemplateBlock; diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js new file mode 100644 index 000000000..1f9387971 --- /dev/null +++ b/lib/models/templateEngine.js @@ -0,0 +1,106 @@ +var nunjucks = require('nunjucks'); +var Immutable = require('immutable'); + +var TemplateEngine = Immutable.Record({ + // List of {TemplateBlock} + blocks: Immutable.List(), + + // Map of filters: {String} name -> {Function} fn + filters: Immutable.Map(), + + // Map of globals: {String} name -> {Mixed} + globals: Immutable.Map(), + + // Context for filters / blocks + context: Object(), + + // Nunjucks loader + loader: nunjucks.FileSystemLoader('views') +}); + +TemplateEngine.prototype.getBlocks = function() { + return this.get('blocks'); +}; + +TemplateEngine.prototype.getGlobals = function() { + return this.get('globals'); +}; + +TemplateEngine.prototype.getFilters = function() { + return this.get('filters'); +}; + +TemplateEngine.prototype.getShortcuts = function() { + return this.get('shortcuts'); +}; + +TemplateEngine.prototype.getLoader = function() { + return this.get('loader'); +}; + +TemplateEngine.prototype.getContext = function() { + return this.get('context'); +}; + +/** + Return a nunjucks environment from this configuration + + @return {Nunjucks.Environment} +*/ +TemplateEngine.prototype.toNunjucks = function() { + var that = this; + var loader = this.getLoader(); + var blocks = this.getBlocks(); + var filters = this.getFilters(); + var globals = this.getGlobals(); + + var env = new nunjucks.Environment( + loader, + { + // Escaping is done after by the asciidoc/markdown parser + autoescape: false, + + // Syntax + tags: { + blockStart: '{%', + blockEnd: '%}', + variableStart: '{{', + variableEnd: '}}', + commentStart: '{###', + commentEnd: '###}' + } + } + ); + + // Add filters + filters.forEach(function(filterFn, filterName) { + env.addFilter(filterName, that.bindToContext(filterFn)); + }); + + // Add blocks + blocks.forEach(function(block) { + var extName = block.getExtensionName(); + var Ext = block.toNunjucksExt(); + + env.addExtension(extName, new Ext()); + }); + + // Add globals + globals.forEach(function(globalName, globalValue) { + env.addGlobal(globalName, globalValue); + }); + + return env; +}; + +/** + Bind a function to the context + + @param {Function} fn + @return {Function} +*/ +TemplateEngine.prototype.bindToContext = function(fn) { + return fn.bind(this.getContext()); +}; + +module.exports = TemplateEngine; diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js new file mode 100644 index 000000000..810c41e03 --- /dev/null +++ b/lib/output/createTemplateEngine.js @@ -0,0 +1,31 @@ +var Immutable = require('immutable'); +var TemplateEngine = require('../models/templateEngine'); + +/** + Create template engine for an output. + It adds default filters/blocks, then add the ones from plugins + + @param {Output} output + @return {TemplateEngine} +*/ +function createTemplateEngine(output) { + var plugins = output.getPlugins(); + + var filters = plugins + .reduce(function(result, plugin) { + return result.merge(plugin.getFilters()); + }, Immutable.Map()); + + var blocks = plugins + .map(function(plugin) { + return plugin.getBlocks(); + }) + .flatten(); + + return new TemplateEngine({ + filters: filters, + blocks: blocks + }); +} + +module.exports = createTemplateEngine; diff --git a/lib/utils/genKey.js b/lib/utils/genKey.js new file mode 100644 index 000000000..065001192 --- /dev/null +++ b/lib/utils/genKey.js @@ -0,0 +1,13 @@ +var lastKey = 0; + +/* + Generate a random key + @return {String} +*/ +function generateKey() { + lastKey += 1; + var str = lastKey.toString(16); + return '00000'.slice(str.length) + str; +} + +module.exports = generateKey; From 93b257314aa9a6ba240ff7d9534ddb1a6ded6bc0 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sun, 24 Apr 2016 19:12:03 +0200 Subject: [PATCH 012/118] Add default filters and blocks --- lib/book.js | 396 ----------------------------- lib/constants/defaultBlocks.js | 51 ++++ lib/constants/defaultFilters.js | 15 ++ lib/output/createTemplateEngine.js | 14 +- lib/output/loaders/conrefs.js | 8 + 5 files changed, 87 insertions(+), 397 deletions(-) delete mode 100644 lib/book.js create mode 100644 lib/constants/defaultBlocks.js create mode 100644 lib/constants/defaultFilters.js create mode 100644 lib/output/loaders/conrefs.js diff --git a/lib/book.js b/lib/book.js deleted file mode 100644 index 77e973ac5..000000000 --- a/lib/book.js +++ /dev/null @@ -1,396 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var Ignore = require('ignore'); - -var Config = require('./config'); -var Readme = require('./backbone/readme'); -var Glossary = require('./backbone/glossary'); -var Summary = require('./backbone/summary'); -var Langs = require('./backbone/langs'); -var Page = require('./page'); -var pathUtil = require('./utils/path'); -var error = require('./utils/error'); -var Promise = require('./utils/promise'); -var Logger = require('./utils/logger'); -var parsers = require('./parsers'); -var initBook = require('./init'); - - -/* -The Book class is an interface for parsing books content. -It does not require to run on Node.js, isnce it only depends on the fs implementation -*/ - -function Book(opts) { - if (!(this instanceof Book)) return new Book(opts); - - this.opts = _.defaults(opts || {}, { - fs: null, - - // Root path for the book - root: '', - - // Extend book configuration - config: {}, - - // Log function - log: function(msg) { - process.stdout.write(msg); - }, - - // Log level - logLevel: 'info' - }); - - if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance')); - - // Root path for the book - this.root = opts.root; - - // If multi-lingual, book can have a parent - this.parent = opts.parent; - if (this.parent) { - this.language = path.relative(this.parent.root, this.root); - } - - // A book is linked to an fs, to access its content - this.fs = opts.fs; - - // Rules to ignore some files - this.ignore = Ignore(); - this.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' - ]); - - // Create a logger for the book - this.log = new Logger(opts.log, opts.logLevel); - - // Create an interface to access the configuration - this.config = new Config(this, opts.config); - - // Interfaces for the book structure - this.readme = new Readme(this); - this.summary = new Summary(this); - this.glossary = new Glossary(this); - - // Multilinguals book - this.langs = new Langs(this); - this.books = []; - - // List of page in the book - this.pages = {}; - - // Deprecation for templates - Object.defineProperty(this, 'options', { - get: function () { - this.log.warn.ln('"options" property is deprecated, use config.get(key) instead'); - return this.config.options; - } - }); - - _.bindAll(this); - - // Loop for template filters/blocks - error.deprecateField(this, 'book', this, '"book" property is deprecated, use "this" directly instead'); -} - -// Return templating context for the book -Book.prototype.getContext = function() { - var variables = this.config.get('variables', {}); - - return { - book: _.extend({ - language: this.language - }, variables) - }; -}; - -// Parse and prepare the configuration, fail if invalid -Book.prototype.prepareConfig = function() { - var that = this; - - return this.config.load() - .then(function() { - var rootFolder = that.config.get('root'); - if (!rootFolder) return; - - that.originalRoot = that.root; - that.root = path.resolve(that.root, rootFolder); - }); -}; - -// Resolve a path in the book source -// Enforce that the output path is in the scope -Book.prototype.resolve = function() { - var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments))); - if (!this.isFileInScope(filename)) { - throw error.FileOutOfScopeError({ - filename: filename, - root: this.root - }); - } - - return filename; -}; - -// Return false if a file is outside the book' scope -Book.prototype.isFileInScope = function(filename) { - filename = path.resolve(this.root, filename); - - // Is the file in the scope of the parent? - if (this.parent && this.parent.isFileInScope(filename)) return true; - - // Is file in the root folder? - return pathUtil.isInRoot(this.root, filename); -}; - -// Parse .gitignore, etc to extract rules -Book.prototype.parseIgnoreRules = function() { - var that = this; - - return Promise.serie([ - '.ignore', - '.gitignore', - '.bookignore' - ], function(filename) { - return that.readFile(filename) - .then(function(content) { - that.ignore.addPattern(content.toString().split(/\r?\n/)); - }, function() { - return Promise(); - }); - }); -}; - -// Parse the whole book -Book.prototype.parse = function() { - var that = this; - - return Promise() - .then(this.prepareConfig) - .then(this.parseIgnoreRules) - - // Parse languages - .then(function() { - return that.langs.load(); - }) - - .then(function() { - if (that.isMultilingual()) { - if (that.isLanguageBook()) { - throw error.ParsingError(new Error('A multilingual book as a language book is forbidden')); - } - - that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages'); - - // Create a new book for each language and parse it - return Promise.serie(that.langs.list(), function(lang) { - that.log.debug.ln('Preparing book for language', lang.id); - var langBook = new Book(_.extend({}, that.opts, { - parent: that, - config: that.config.dump(), - root: that.resolve(lang.id) - })); - - that.books.push(langBook); - - return langBook.parse(); - }); - } - - return Promise() - - // Parse the readme - .then(that.readme.load) - .then(function() { - if (!that.readme.exists()) { - throw new error.FileNotFoundError({ filename: 'README' }); - } - - // Default configuration to infos extracted from readme - if (!that.config.get('title')) that.config.set('title', that.readme.title); - if (!that.config.get('description')) that.config.set('description', that.readme.description); - }) - - // Parse the summary - .then(that.summary.load) - .then(function() { - if (!that.summary.exists()) { - that.log.warn.ln('no summary file in this book'); - } - - // Index summary's articles - that.summary.walk(function(article) { - if (!article.hasLocation() || article.isExternal()) return; - that.addPage(article.path); - }); - }) - - // Parse the glossary - .then(that.glossary.load) - - // Add the glossary as a page - .then(function() { - if (!that.glossary.exists()) return; - that.addPage(that.glossary.path); - }); - }); -}; - -// Mark a filename as being parsable -Book.prototype.addPage = function(filename) { - if (this.hasPage(filename)) return this.getPage(filename); - - filename = pathUtil.normalize(filename); - this.pages[filename] = new Page(this, filename); - return this.pages[filename]; -}; - -// Return a page by its filename (or undefined) -Book.prototype.getPage = function(filename) { - filename = pathUtil.normalize(filename); - return this.pages[filename]; -}; - - -// Return true, if has a specific page -Book.prototype.hasPage = function(filename) { - return Boolean(this.getPage(filename)); -}; - -// Test if a file is ignored, return true if it is -Book.prototype.isFileIgnored = function(filename) { - return this.ignore.filter([filename]).length == 0; -}; - -// Read a file in the book, throw error if ignored -Book.prototype.readFile = function(filename) { - if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename })); - return this.fs.readAsString(this.resolve(filename)); -}; - -// Get stat infos about a file -Book.prototype.statFile = function(filename) { - if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename })); - return this.fs.stat(this.resolve(filename)); -}; - -// Find a parsable file using a filename -Book.prototype.findParsableFile = function(filename) { - var that = this; - - var ext = path.extname(filename); - var basename = path.basename(filename, ext); - - // Ordered list of extensions to test - var exts = parsers.extensions; - if (ext) exts = _.uniq([ext].concat(exts)); - - return _.reduce(exts, function(prev, ext) { - return prev.then(function(output) { - // Stop if already find a parser - if (output) return output; - - var filepath = basename+ext; - - return that.fs.findFile(that.root, filepath) - .then(function(realFilepath) { - if (!realFilepath) return null; - - return { - parser: parsers.getByExt(ext), - path: realFilepath - }; - }); - }); - }, Promise(null)); -}; - -// Return true if book is associated to a language -Book.prototype.isLanguageBook = function() { - return Boolean(this.parent); -}; -Book.prototype.isSubBook = Book.prototype.isLanguageBook; - -// Return true if the book is main instance of a multilingual book -Book.prototype.isMultilingual = function() { - return this.langs.count() > 0; -}; - -// Return true if file is in the scope of this book -Book.prototype.isInBook = function(filename) { - return pathUtil.isInRoot( - this.root, - filename - ); -}; - -// Return true if file is in the scope of a child book -Book.prototype.isInLanguageBook = function(filename) { - var that = this; - - return _.some(this.langs.list(), function(lang) { - return pathUtil.isInRoot( - that.resolve(lang.id), - that.resolve(filename) - ); - }); -}; - -// ----- Parser Methods - -// Render a markup string in inline mode -Book.prototype.renderInline = function(type, src) { - var parser = parsers.get(type); - return parser.inline(src) - .get('content'); -}; - -// Render a markup string in block mode -Book.prototype.renderBlock = function(type, src) { - var parser = parsers.get(type); - return parser.page(src) - .get('content'); -}; - - -// ----- DEPRECATED METHODS - -Book.prototype.contentLink = error.deprecateMethod(function(s) { - return this.output.toURL(s); -}, '.contentLink() is deprecated, use ".output.toURL()" instead'); - -Book.prototype.contentPath = error.deprecateMethod(function(s) { - return this.output.toURL(s); -}, '.contentPath() is deprecated, use ".output.toURL()" instead'); - -Book.prototype.isSubBook = error.deprecateMethod(function() { - return this.isLanguageBook(); -}, '.isSubBook() is deprecated, use ".isLanguageBook()" instead'); - - -// Initialize a book -Book.init = function(fs, root, opts) { - var book = new Book(_.extend(opts || {}, { - root: root, - fs: fs - })); - - return initBook(book); -}; - - -module.exports = Book; diff --git a/lib/constants/defaultBlocks.js b/lib/constants/defaultBlocks.js new file mode 100644 index 000000000..5d9135950 --- /dev/null +++ b/lib/constants/defaultBlocks.js @@ -0,0 +1,51 @@ +var Immutable = require('immutable'); +var TemplateBlock = require('../models/templateBlock'); + +module.exports = Immutable.List([ + TemplateBlock({ + name: 'html', + process: function(blk) { + return blk; + } + }), + + TemplateBlock({ + name: 'code', + process: function(blk) { + return { + html: false, + body: blk.body + }; + } + }), + + TemplateBlock({ + name: 'markdown', + process: function(blk) { + return this.book.renderInline('markdown', blk.body) + .then(function(out) { + return { body: out }; + }); + } + }), + + TemplateBlock({ + name: 'asciidoc', + process: function(blk) { + return this.book.renderInline('asciidoc', blk.body) + .then(function(out) { + return { body: out }; + }); + } + }), + + TemplateBlock({ + name: 'markup', + process: function(blk) { + return this.book.renderInline(this.ctx.file.type, blk.body) + .then(function(out) { + return { body: out }; + }); + } + }), +]); diff --git a/lib/constants/defaultFilters.js b/lib/constants/defaultFilters.js new file mode 100644 index 000000000..35025cc19 --- /dev/null +++ b/lib/constants/defaultFilters.js @@ -0,0 +1,15 @@ +var Immutable = require('immutable'); +var moment = require('moment'); + +module.exports = Immutable.Map({ + // Format a date + // ex: 'MMMM Do YYYY, h:mm:ss a + date: function(time, format) { + return moment(time).format(format); + }, + + // Relative Time + dateFromNow: function(time) { + return moment(time).fromNow(); + } +}); diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index 810c41e03..27dbebad4 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -1,5 +1,10 @@ var Immutable = require('immutable'); + var TemplateEngine = require('../models/templateEngine'); +var ConrefsLoader = require('./loaders/conrefs'); + +var defaultBlocks = require('../constants/defaultBlocks'); +var defaultFilters = require('../constants/defaultFilters'); /** Create template engine for an output. @@ -22,9 +27,16 @@ function createTemplateEngine(output) { }) .flatten(); + // Extend with default + blocks = defaultBlocks.concat(blocks); + filters = defaultFilters.merge(filters); + + var loader = new ConrefsLoader(); + return new TemplateEngine({ filters: filters, - blocks: blocks + blocks: blocks, + loader: loader }); } diff --git a/lib/output/loaders/conrefs.js b/lib/output/loaders/conrefs.js new file mode 100644 index 000000000..7580826c4 --- /dev/null +++ b/lib/output/loaders/conrefs.js @@ -0,0 +1,8 @@ +var nunjucks = require('nunjucks'); + +var Loader = nunjucks.Loader.extend({ + async: true, + +}); + +module.exports = Loader; From 3f5c8acb3cdbf71c429ad331d23698cc53358746 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sun, 24 Apr 2016 23:45:47 +0200 Subject: [PATCH 013/118] Add fiel for testing resolveLinks --- lib/constants/defaultBlocks.js | 2 +- lib/output/getModifiers.js | 26 +++++++++++++++++++ lib/output/json/onPage.js | 6 ++--- .../modifiers/__tests__/resolveLinks.js | 11 ++++++++ lib/output/resolveFile.js | 21 +++++++++++++++ 5 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 lib/output/getModifiers.js create mode 100644 lib/output/modifiers/__tests__/resolveLinks.js create mode 100644 lib/output/resolveFile.js diff --git a/lib/constants/defaultBlocks.js b/lib/constants/defaultBlocks.js index 5d9135950..ed1911565 100644 --- a/lib/constants/defaultBlocks.js +++ b/lib/constants/defaultBlocks.js @@ -47,5 +47,5 @@ module.exports = Immutable.List([ return { body: out }; }); } - }), + }) ]); diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js new file mode 100644 index 000000000..96784feab --- /dev/null +++ b/lib/output/getModifiers.js @@ -0,0 +1,26 @@ +var Modifier = require('../modifier'); +var resolveFile = require('./resolveFile'); + +/** + Return default modifier to prepare a page + + @return +*/ +function getModifiers(output, page) { + var book = output.getBook(); + var glossary = book.getGlossary(); + var entries = glossary.getEntries(); + + var file = page.getFile(); + + return [ + Modifier.addHeadingId, + Modifier.resolveLinks.bind(null, + file.getPath(), + resolveFile.bind(null, output) + ), + Modifier.annotateText(entries) + ]; +} + +module.exports = getModifiers; diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 1143aaae8..8216bb90d 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -1,6 +1,7 @@ var JSONUtils = require('../../json'); var Modifier = require('../modifier'); var Writer = require('../writer'); +var getModifiers = require('../getModifiers'); /** Write a page as a json file @@ -9,10 +10,7 @@ var Writer = require('../writer'); @param {Page} page */ function onPage(output, page) { - return Modifier.modifyHTML(page, [ - Modifier.addHeadingId, - Modifier.resolveLinks - ]) + return Modifier.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { // Generate the JSON var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); diff --git a/lib/output/modifiers/__tests__/resolveLinks.js b/lib/output/modifiers/__tests__/resolveLinks.js new file mode 100644 index 000000000..36511a216 --- /dev/null +++ b/lib/output/modifiers/__tests__/resolveLinks.js @@ -0,0 +1,11 @@ +jest.autoMockOff(); + +var cheerio = require('cheerio'); + +describe('resolveLinks', function() { + var resolveLinks = require('../resolveLinks'); + + +}); + + diff --git a/lib/output/resolveFile.js b/lib/output/resolveFile.js new file mode 100644 index 000000000..a3a49c54a --- /dev/null +++ b/lib/output/resolveFile.js @@ -0,0 +1,21 @@ +var PathUtils = require('../utils/path'); + +/** + Resolve an absolute path (extracted from a link) + + @param {Output} output + @param {String} filePath + @return {String} +*/ +function resolveFile(output, filePath) { + var pages = output.getPages(); + var page = pages.get(filePath); + + if (!page) { + return filePath; + } + + return PathUtils.setExtension(filePath, '.html'); +} + +module.exports = resolveFile; From 4646ed8f4af184a33280d8fe1f978c7da5404725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 10:32:44 +0200 Subject: [PATCH 014/118] Fix plugins loading --- book.js | 2 +- lib/json/encodeBookWithPage.js | 2 +- lib/json/encodeGlossary.js | 2 +- lib/json/encodeReadme.js | 2 +- lib/models/output.js | 9 +++++++ lib/models/plugin.js | 2 +- lib/models/summary.js | 11 +++++++++ lib/output/callHook.js | 6 +++++ lib/output/generateAssets.js | 3 +++ lib/output/generateBook.js | 39 ++++++++++++++++++++++++++++-- lib/output/generatePages.js | 9 +++++-- lib/output/getModifiers.js | 8 +++--- lib/output/json/onPage.js | 4 +-- lib/output/modifiers/modifyHTML.js | 3 ++- lib/output/prepareAssets.js | 3 +++ lib/output/preparePages.js | 3 +++ lib/output/writer/writePage.js | 6 ++++- lib/parse/parseGlossary.js | 5 ++++ lib/parse/parseLanguages.js | 8 +++++- lib/parse/parseReadme.js | 4 +++ lib/parse/parseSummary.js | 4 +++ lib/plugins/loadForBook.js | 8 +++--- lib/plugins/loadPlugin.js | 1 - lib/plugins/validatePlugin.js | 13 +++++----- lib/utils/promise.js | 36 +++++++++++++++++++++++++-- test.js | 5 +++- 26 files changed, 166 insertions(+), 32 deletions(-) diff --git a/book.js b/book.js index d6afa1f58..a1df67cde 100644 --- a/book.js +++ b/book.js @@ -9,7 +9,7 @@ module.exports = { gitbook: '>=3.0.0-pre.0', // Use the "official" theme - plugins: ['theme-official', 'sitemap'], + //plugins: ['theme-official', 'sitemap'], variables: { version: pkg.version diff --git a/lib/json/encodeBookWithPage.js b/lib/json/encodeBookWithPage.js index 26ab174af..5600a8218 100644 --- a/lib/json/encodeBookWithPage.js +++ b/lib/json/encodeBookWithPage.js @@ -13,7 +13,7 @@ function encodeBookWithPage(book, page) { var file = page.getFile(); var result = encodeBook(book); - result.page = encodePage(page); + result.page = encodePage(page, book.getSummary()); result.file = encodeFile(file); return result; diff --git a/lib/json/encodeGlossary.js b/lib/json/encodeGlossary.js index fcaf4d2fd..e9bcfc9d1 100644 --- a/lib/json/encodeGlossary.js +++ b/lib/json/encodeGlossary.js @@ -1,4 +1,4 @@ -var encodeFile = require('./encodeFileToJson'); +var encodeFile = require('./encodeFile'); var encodeGlossaryEntry = require('./encodeGlossaryEntry'); /** diff --git a/lib/json/encodeReadme.js b/lib/json/encodeReadme.js index 01277880f..96176a3ba 100644 --- a/lib/json/encodeReadme.js +++ b/lib/json/encodeReadme.js @@ -1,4 +1,4 @@ -var encodeFile = require('./encodeFileToJson'); +var encodeFile = require('./encodeFile'); /** Encode a readme to JSON diff --git a/lib/models/output.js b/lib/models/output.js index 957d9d389..3e8dab8e3 100644 --- a/lib/models/output.js +++ b/lib/models/output.js @@ -30,6 +30,15 @@ Output.prototype.getAssets = function() { return this.get('assets'); }; +/** + Return logegr for this output (same as book) + + @return {Logger} +*/ +Output.prototype.getLogger = function() { + return this.getBook().getLogger(); +}; + /** Create an Output instance from a book and a set of options diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 2f791dcc6..6891d7d4c 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -70,7 +70,7 @@ Plugin.prototype.isLoaded = function() { @return {Map} */ Plugin.prototype.getHooks = function() { - return this.getContent().get('hooks'); + return this.getContent().get('hooks') || Immutable.Map(); }; /** diff --git a/lib/models/summary.js b/lib/models/summary.js index f295a16cb..4ab2d680d 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -27,6 +27,17 @@ Summary.prototype.getByLevel = function(level) { return SummaryArticle.getByLevel(this, level, 'getParts'); }; +/** + Return an article by its path + + @param {String} filePath + @return {Part|Article} +*/ +Summary.prototype.getByPath = function(filePath) { + // todo + + return undefined; +}; /** Create a new summary for a list of parts diff --git a/lib/output/callHook.js b/lib/output/callHook.js index f6119fda0..8d34bdc20 100644 --- a/lib/output/callHook.js +++ b/lib/output/callHook.js @@ -21,12 +21,18 @@ function callHook(name, getArgument, handleResult, output) { getArgument = getArgument || defaultGetArgument; handleResult = handleResult || defaultHandleResult; + var logger = output.getLogger(); var plugins = output.getPlugins(); + logger.debug.ln('calling hook "' + name + '"'); + return Promise(getArgument(output)) .then(function(arg) { return Promise.reduce(plugins, function(prev, plugin) { var hook = plugin.getHook(name); + if (!hook) { + return prev; + } return hook(prev); }, arg); diff --git a/lib/output/generateAssets.js b/lib/output/generateAssets.js index 04ca05d17..7a6e104f5 100644 --- a/lib/output/generateAssets.js +++ b/lib/output/generateAssets.js @@ -9,6 +9,7 @@ var Promise = require('../utils/promise'); */ function generateAssets(generator, output) { var assets = output.getAssets(); + var logger = output.getLogger(); // Is generator ignoring assets? if (!generator.onAsset) { @@ -16,6 +17,8 @@ function generateAssets(generator, output) { } return Promise.reduce(assets, function(out, assetFile) { + logger.debug.ln('copy asset "' + assetFile + '"'); + return generator.onAsset(out, assetFile); }, output); } diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index c35719b8a..ca19067d0 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -62,7 +62,17 @@ function generateBook(generator, book, options) { ) ) - .then(callHook.bind(null, 'init')) + .then( + callHook.bind(null, + 'init', + function(output) { + return {}; + }, + function(output) { + return output; + } + ) + ) .then(function(output) { if (!generator.onInit) { @@ -75,7 +85,16 @@ function generateBook(generator, book, options) { .then(generateAssets.bind(null, generator)) .then(generatePages.bind(null, generator)) - .then(callHook.bind(null, 'finish:before')) + .then(callHook.bind(null, + 'finish:before', + function(output) { + return {}; + }, + function(output) { + return output; + } + ) + ) .then(function(output) { if (!generator.onFinish) { @@ -83,6 +102,22 @@ function generateBook(generator, book, options) { } return generator.onFinish(output); + }) + + /*.then(callHook.bind(null, + 'finish', + function(output) { + return {}; + }, + function(output) { + return output; + } + ) + )*/ + + .then(function(output) { + var logger = output.getLogger(); + logger.info.ok('generation finished with success!'); }); } diff --git a/lib/output/generatePages.js b/lib/output/generatePages.js index 406f24204..5ec46f8e4 100644 --- a/lib/output/generatePages.js +++ b/lib/output/generatePages.js @@ -9,14 +9,19 @@ var Promise = require('../utils/promise'); */ function generatePages(generator, output) { var pages = output.getPages(); + var logger = output.getLogger(); // Is generator ignoring assets? if (!generator.onPage) { return Promise(output); } - return Promise.reduce(pages, function(out, assetFile) { - return generator.onPage(out, assetFile); + return Promise.reduce(pages, function(out, page) { + var file = page.getFile(); + + logger.debug.ln('generate page "' + file.getPath() + '"'); + + return generator.onPage(out, page); }, output); } diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 96784feab..21a069225 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -1,4 +1,4 @@ -var Modifier = require('../modifier'); +var Modifiers = require('./modifiers'); var resolveFile = require('./resolveFile'); /** @@ -14,12 +14,12 @@ function getModifiers(output, page) { var file = page.getFile(); return [ - Modifier.addHeadingId, - Modifier.resolveLinks.bind(null, + Modifiers.addHeadingId, + Modifiers.resolveLinks.bind(null, file.getPath(), resolveFile.bind(null, output) ), - Modifier.annotateText(entries) + Modifiers.annotateText.bind(null, entries) ]; } diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 8216bb90d..5e456620d 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -1,5 +1,5 @@ var JSONUtils = require('../../json'); -var Modifier = require('../modifier'); +var Modifiers = require('../modifiers'); var Writer = require('../writer'); var getModifiers = require('../getModifiers'); @@ -10,7 +10,7 @@ var getModifiers = require('../getModifiers'); @param {Page} page */ function onPage(output, page) { - return Modifier.modifyHTML(page, getModifiers(output, page)) + return Modifiers.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { // Generate the JSON var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); diff --git a/lib/output/modifiers/modifyHTML.js b/lib/output/modifiers/modifyHTML.js index d46a44f1a..0fcf994fc 100644 --- a/lib/output/modifiers/modifyHTML.js +++ b/lib/output/modifiers/modifyHTML.js @@ -16,7 +16,8 @@ function modifyHTML(page, operations) { return Promise.forEach(operations, function(op) { op($); }) - .then(function(resultHTML) { + .then(function() { + var resultHTML = $.html(); return page.set('content', resultHTML); }); } diff --git a/lib/output/prepareAssets.js b/lib/output/prepareAssets.js index 19aabef1d..ae9b55acb 100644 --- a/lib/output/prepareAssets.js +++ b/lib/output/prepareAssets.js @@ -9,9 +9,12 @@ var Parse = require('../parse'); function prepareAssets(output) { var book = output.getBook(); var pages = output.getPages(); + var logger = output.getLogger(); return Parse.listAssets(book, pages) .then(function(assets) { + logger.info.ln('found', assets.size, 'asset files'); + return output.set('assets', assets); }); } diff --git a/lib/output/preparePages.js b/lib/output/preparePages.js index 396edde31..8ad5f8c48 100644 --- a/lib/output/preparePages.js +++ b/lib/output/preparePages.js @@ -8,9 +8,12 @@ var Parse = require('../parse'); */ function preparePages(output) { var book = output.getBook(); + var logger = book.getLogger(); return Parse.parsePagesList(book) .then(function(pages) { + logger.info.ln('found', pages.size, 'pages'); + return output.set('pages', pages); }); } diff --git a/lib/output/writer/writePage.js b/lib/output/writer/writePage.js index 23e37d008..6bcc1ba08 100644 --- a/lib/output/writer/writePage.js +++ b/lib/output/writer/writePage.js @@ -27,7 +27,11 @@ function writePage(output, page, content, options) { var fileName = PathUtils.setExtension(file.getPath(), options.get('extension')); var filePath = path.join(rootFolder, fileName); - return fs.writeFile(filePath, content); + return fs.ensure(filePath) + .then(function() { + return fs.writeFile(filePath, content); + }) + .thenResolve(output); } module.exports = writePage; diff --git a/lib/parse/parseGlossary.js b/lib/parse/parseGlossary.js index f56c751cf..1dbfbe022 100644 --- a/lib/parse/parseGlossary.js +++ b/lib/parse/parseGlossary.js @@ -8,12 +8,17 @@ var Glossary = require('../models/glossary'); @return {Promise} */ function parseGlossary(book) { + var logger = book.getLogger(); + return parseStructureFile(book, 'glossary') .spread(function(file, entries) { if (!file) { return book; } + + logger.debug.ln('glossary index file found at', file.getPath()); + var glossary = Glossary.createFromEntries(file, entries); return book.set('glossary', glossary); }); diff --git a/lib/parse/parseLanguages.js b/lib/parse/parseLanguages.js index d78881f1c..346f3a36d 100644 --- a/lib/parse/parseLanguages.js +++ b/lib/parse/parseLanguages.js @@ -8,13 +8,19 @@ var Languages = require('../models/languages'); @return {Promise} */ function parseLanguages(book) { + var logger = book.getLogger(); + return parseStructureFile(book, 'langs') .spread(function(file, result) { if (!file) { - return; + return book; } var languages = Languages.createFromList(file, result); + + logger.debug.ln('languages index file found at', file.getPath()); + logger.info.ln('parsing multilingual book, with', languages.getList().size, 'languages'); + return book.set('languages', languages); }); } diff --git a/lib/parse/parseReadme.js b/lib/parse/parseReadme.js index ea6ef59fd..a2ede77fa 100644 --- a/lib/parse/parseReadme.js +++ b/lib/parse/parseReadme.js @@ -10,12 +10,16 @@ var error = require('../utils/error'); @return {Promise} */ function parseReadme(book) { + var logger = book.getLogger(); + return parseStructureFile(book, 'readme') .spread(function(file, result) { if (!file) { throw new error.FileNotFoundError({ filename: 'README' }); } + logger.debug.ln('readme found at', file.getPath()); + var readme = Readme.create(file, result); return book.set('readme', readme); }); diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index 3fb471ed1..fc344462f 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -8,11 +8,15 @@ var Summary = require('../models/summary'); @return {Promise} */ function parseSummary(book) { + var logger = book.getLogger(); + return parseStructureFile(book, 'summary') .spread(function(file, result) { if (!file) { + logger.warn.ln('no summary file in this book'); return book; } + logger.debug.ln('summary file found at', file.getPath()); var summary = Summary.createFromParts(file, result.parts); return book.set('summary', summary); diff --git a/lib/plugins/loadForBook.js b/lib/plugins/loadForBook.js index fcfac08c9..949469667 100644 --- a/lib/plugins/loadForBook.js +++ b/lib/plugins/loadForBook.js @@ -1,7 +1,7 @@ var Promise = require('../utils/promise'); var listForBook = require('./listForBook'); -var listInstalledForBook = require('./listInstalledForBook'); +var findForBook = require('./findForBook'); var loadPlugin = require('./loadPlugin'); @@ -14,9 +14,9 @@ var loadPlugin = require('./loadPlugin'); function loadForBook(book) { var logger = book.getLogger(); var requirements = listForBook(book); - var requirementsKeys = requirements.keys().toList(); + var requirementsKeys = requirements.keySeq().toList(); - return listInstalledForBook(book) + return findForBook(book) .then(function(installed) { // Filter out plugins not listed of first level // (aka pre-installed plugins) @@ -48,7 +48,7 @@ function loadForBook(book) { } return Promise.map(installed, function(plugin) { - return loadPlugin(plugin); + return loadPlugin(book, plugin); }); }); } diff --git a/lib/plugins/loadPlugin.js b/lib/plugins/loadPlugin.js index 375329e61..39e2e3381 100644 --- a/lib/plugins/loadPlugin.js +++ b/lib/plugins/loadPlugin.js @@ -26,7 +26,6 @@ function loadPlugin(book, plugin) { var name = plugin.getName(); var pkgPath = plugin.getPath(); - // Try loading plugins from different location var p = Promise() .then(function() { diff --git a/lib/plugins/validatePlugin.js b/lib/plugins/validatePlugin.js index 37f69007a..4baa91139 100644 --- a/lib/plugins/validatePlugin.js +++ b/lib/plugins/validatePlugin.js @@ -14,20 +14,21 @@ function validatePlugin(plugin) { var isValid = ( plugin.isLoaded() && packageInfos && - packageInfos.name && - packageInfos.engines && - packageInfos.engines.gitbook + packageInfos.get('name') && + packageInfos.get('engines') && + packageInfos.get('engines').get('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)); + var engine = packageInfos.get('engines').get('gitbook'); + if (!gitbook.satisfies(engine)) { + return Promise.reject(new Error('GitBook doesn\'t satisfy the requirements of this plugin: ' + engine)); } - return Promise(); + return Promise(plugin); } module.exports = validatePlugin; diff --git a/lib/utils/promise.js b/lib/utils/promise.js index b0cfb34a9..b8ab63c71 100644 --- a/lib/utils/promise.js +++ b/lib/utils/promise.js @@ -9,7 +9,7 @@ var Immutable = require('immutable'); @return {Promise} */ function reduce(arr, iter, base) { - arr = Immutable.List(arr); + arr = Immutable.Iterable.isIterable(arr)? arr : Immutable.List(arr); return arr.reduce(function(prev, elem, i) { return prev.then(function(val) { @@ -74,7 +74,7 @@ function some(arr, iter) { @param {Function(element, index)} @return {Promise} */ -function map(arr, iter) { +function mapAsList(arr, iter) { return reduce(arr, function(prev, entry, i) { return Q(iter(entry, i)) .then(function(out) { @@ -84,6 +84,38 @@ function map(arr, iter) { }, []); } +/** + Map an array or map + + @param {Array|List|Map|OrderedMap} arr + @param {Function(element, key)} + @return {Promise} +*/ +function map(arr, iter) { + if (Immutable.Map.isMap(arr)) { + var type = 'Map'; + if (Immutable.OrderedMap.isOrderedMap(arr)) { + type = 'OrderedMap'; + } + + return mapAsList(arr, function(value, key) { + return Q(iter(value, key)) + .then(function(result) { + return [key, result]; + }); + }) + .then(function(result) { + return Immutable[type](result); + }); + } else { + return mapAsList(arr, iter) + .then(function(result) { + return Immutable.List(result); + }); + } +} + + /** Wrap a fucntion in a promise diff --git a/test.js b/test.js index 82656834a..0de26b788 100644 --- a/test.js +++ b/test.js @@ -1,6 +1,7 @@ var path = require('path'); var gitbook = require('./lib'); +var Output = require('./lib/output'); var NodeFS = require('./lib/fs/node'); @@ -15,7 +16,9 @@ var book = gitbook.Book.createForFS(fs); // Parse the book gitbook.Parse.parseBook(book) .then(function(_book) { - return gitbook.Parse.parsePagesList(_book); + return Output.generate(Output.JSONGenerator, _book, { + root: path.join(__dirname, '_book') + }); }) .then(function(pages) { //console.log('parsed', pages); From 3aa17ab1f4a25a9bcfd8babdcf98a37ac6c2496c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 10:58:10 +0200 Subject: [PATCH 015/118] Convert markdown to html for pages --- lib/output/generatePage.js | 38 +++++++++++++++++++++++++++++++++++++ lib/output/generatePages.js | 6 +++++- lib/parse/index.js | 3 ++- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 lib/output/generatePage.js diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js new file mode 100644 index 000000000..6fe0514dd --- /dev/null +++ b/lib/output/generatePage.js @@ -0,0 +1,38 @@ +var Promise = require('../utils/promise'); +var error = require('../utils/error'); + +var Parse = require('../parse'); + +/** + Prepare and generate HTML for a page + + @param {Output} output + @param {Page} page + @return {Promise} +*/ +function generatePage(output, page) { + var book = output.getBook(); + + return Parse.parsePage(book, page) + .then(function(resultPage) { + var file = resultPage.getFile(); + var filePath = file.getPath(); + var parser = file.getParser(); + + if (!parser) { + return Promise.reject(error.FileNotParsableError({ + filename: filePath + })); + } + + return Promise(resultPage.getContent()) + .then(parser.page.prepare) + .then(parser.page) + .get('content') + .then(function(content) { + return resultPage.set('content', content); + }); + }); +} + +module.exports = generatePage; diff --git a/lib/output/generatePages.js b/lib/output/generatePages.js index 5ec46f8e4..5b6851e56 100644 --- a/lib/output/generatePages.js +++ b/lib/output/generatePages.js @@ -1,4 +1,5 @@ var Promise = require('../utils/promise'); +var generatePage = require('./generatePage'); /** Output all pages using a generator @@ -21,7 +22,10 @@ function generatePages(generator, output) { logger.debug.ln('generate page "' + file.getPath() + '"'); - return generator.onPage(out, page); + return generatePage(out, page) + .then(function(resultPage) { + return generator.onPage(out, resultPage); + }); }, output); } diff --git a/lib/parse/index.js b/lib/parse/index.js index 785b67efb..ac27fcfbe 100644 --- a/lib/parse/index.js +++ b/lib/parse/index.js @@ -8,5 +8,6 @@ module.exports = { parsePagesList: require('./parsePagesList'), parseIgnore: require('./parseIgnore'), listAssets: require('./listAssets'), - parseLanguages: require('./parseLanguages') + parseLanguages: require('./parseLanguages'), + parsePage: require('./parsePage') }; From 814b6ff489bc1f0bfd03db6409d49185e8c7b93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 11:05:06 +0200 Subject: [PATCH 016/118] Complete json format --- lib/json/encodeBook.js | 4 +--- lib/json/encodeConfig.js | 12 ------------ lib/json/encodeFile.js | 7 ++++++- lib/json/encodeSummaryArticle.js | 3 +++ lib/json/index.js | 3 +-- lib/models/summaryArticle.js | 15 ++++++++++++++- lib/plugins/validateConfig.js | 4 ++-- 7 files changed, 27 insertions(+), 21 deletions(-) delete mode 100644 lib/json/encodeConfig.js diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index eed5e5f3b..6c1b023b1 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -1,7 +1,6 @@ var encodeSummary = require('./encodeSummary'); var encodeGlossary = require('./encodeGlossary'); var encodeReadme = require('./encodeReadme'); -var encodeConfig = require('./encodeConfig'); /** Encode a book to JSON @@ -13,8 +12,7 @@ function encodeBookToJson(book) { return { summary: encodeSummary(book.getSummary()), glossary: encodeGlossary(book.getGlossary()), - readme: encodeReadme(book.getReadme()), - config: encodeConfig(book.getConfig()) + readme: encodeReadme(book.getReadme()) }; } diff --git a/lib/json/encodeConfig.js b/lib/json/encodeConfig.js deleted file mode 100644 index 195d43bac..000000000 --- a/lib/json/encodeConfig.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - Encode configuration to JSON - - @param {Config} - @return {Object} -*/ -function encodeConfig(config) { - var values = config.getValues(); - return values.toJS(); -} - -module.exports = encodeConfig; diff --git a/lib/json/encodeFile.js b/lib/json/encodeFile.js index 03b34505c..d2c9e8a7d 100644 --- a/lib/json/encodeFile.js +++ b/lib/json/encodeFile.js @@ -6,8 +6,13 @@ @return {Object} */ function encodeFileToJson(file) { + var filePath = file.getPath(); + if (!filePath) { + return undefined; + } + return { - path: file.getPath(), + path: filePath, mtime: file.getMTime(), type: file.getType() }; diff --git a/lib/json/encodeSummaryArticle.js b/lib/json/encodeSummaryArticle.js index 485d20990..b3f977afe 100644 --- a/lib/json/encodeSummaryArticle.js +++ b/lib/json/encodeSummaryArticle.js @@ -10,6 +10,9 @@ function encodeSummaryArticle(article) { title: article.getTitle(), level: article.getLevel(), depth: article.getDepth(), + anchor: article.getAnchor(), + url: article.getUrl(), + path: article.getPath(), articles: article.getArticles() .map(encodeSummaryArticle).toJS() }; diff --git a/lib/json/index.js b/lib/json/index.js index a026a7fc6..92b105a34 100644 --- a/lib/json/index.js +++ b/lib/json/index.js @@ -5,6 +5,5 @@ module.exports = { encodeFile: require('./encodeFile'), encodePage: require('./encodePage'), encodeSummary: require('./encodeSummary'), - encodeReadme: require('./encodeReadme'), - encodeConfig: require('./encodeConfig') + encodeReadme: require('./encodeReadme') }; diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index e3d85ef50..945a16caa 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -54,6 +54,10 @@ SummaryArticle.prototype.getByLevel = function(level) { @return {String} */ SummaryArticle.prototype.getPath = function() { + if (this.isExternal()) { + return undefined; + } + var ref = this.getRef(); var parts = ref.split('#'); @@ -63,6 +67,15 @@ SummaryArticle.prototype.getPath = function() { return location.normalize(pathname); }; +/** + Return url if article is external + + @return {String} +*/ +SummaryArticle.prototype.getUrl = function() { + return this.isExternal()? this.getRef() : undefined; +}; + /** Get anchor for this article (or undefined) @@ -72,7 +85,7 @@ SummaryArticle.prototype.getAnchor = function() { var ref = this.getRef(); var parts = ref.split('#'); - var anchor = (parts.length > 1? '#' + parts[parts.length - 1] : null); + var anchor = (parts.length > 1? '#' + parts[parts.length - 1] : undefined); return anchor; }; diff --git a/lib/plugins/validateConfig.js b/lib/plugins/validateConfig.js index fc32344fb..cb6c38a3a 100644 --- a/lib/plugins/validateConfig.js +++ b/lib/plugins/validateConfig.js @@ -32,7 +32,7 @@ function validatePluginConfig(book, plugin) { // Validate and throw if invalid var v = new jsonschema.Validator(); - var result = v.validate(config, schema, { + var result = v.validate(pluginConfig, schema, { propertyName: configKey }); @@ -43,7 +43,7 @@ function validatePluginConfig(book, plugin) { // Insert default values var defaults = jsonSchemaDefaults(schema); - pluginConfig = mergeDefaults(config, defaults); + pluginConfig = mergeDefaults(pluginConfig, defaults); // Update configuration From e1e4e7f01177d968e63f0b3a1830eda1adacb56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 11:28:21 +0200 Subject: [PATCH 017/118] Add tests for link resolution --- .../modifiers/__tests__/resolveLinks.js | 60 +++++++++++++++++++ lib/output/modifiers/resolveLinks.js | 4 +- lib/utils/__tests__/location.js | 29 +++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 lib/utils/__tests__/location.js diff --git a/lib/output/modifiers/__tests__/resolveLinks.js b/lib/output/modifiers/__tests__/resolveLinks.js index 36511a216..3d50d80ac 100644 --- a/lib/output/modifiers/__tests__/resolveLinks.js +++ b/lib/output/modifiers/__tests__/resolveLinks.js @@ -1,10 +1,70 @@ jest.autoMockOff(); +var path = require('path'); var cheerio = require('cheerio'); describe('resolveLinks', function() { var resolveLinks = require('../resolveLinks'); + function resolveFileBasic(href) { + return href; + } + + function resolveFileCustom(href) { + if (path.extname(href) == '.md') { + return href.slice(0, -3) + '.html'; + } + + return href; + } + + describe('Absolute path', function() { + var TEST = '

This is a

'; + + pit('should resolve path starting by "/" in root directory', function() { + var $ = cheerio.load(TEST); + + return resolveLinks('hello.md', resolveFileBasic, $) + .then(function() { + var link = $('a'); + expect(link.attr('href')).toBe('test/cool.md'); + }); + }); + + pit('should resolve path starting by "/" in child directory', function() { + var $ = cheerio.load(TEST); + + return resolveLinks('afolder/hello.md', resolveFileBasic, $) + .then(function() { + var link = $('a'); + expect(link.attr('href')).toBe('../test/cool.md'); + }); + }); + }); + + describe('Custom Resolver', function() { + var TEST = '

This is a

'; + + pit('should resolve path correctly for absolute path', function() { + var $ = cheerio.load(TEST); + + return resolveLinks('hello.md', resolveFileCustom, $) + .then(function() { + var link = $('a').first(); + expect(link.attr('href')).toBe('test/cool.html'); + }); + }); + + pit('should resolve path correctly for absolute path (2)', function() { + var $ = cheerio.load(TEST); + + return resolveLinks('afodler/hello.md', resolveFileCustom, $) + .then(function() { + var link = $('a').first(); + expect(link.attr('href')).toBe('../test/cool.html'); + }); + }); + }); }); diff --git a/lib/output/modifiers/resolveLinks.js b/lib/output/modifiers/resolveLinks.js index b5ad4f305..bf3fd1066 100644 --- a/lib/output/modifiers/resolveLinks.js +++ b/lib/output/modifiers/resolveLinks.js @@ -17,13 +17,13 @@ function resolveLinks(currentFile, resolveFile, $) { return editHTMLElement($, 'a', function($a) { var href = $a.attr('href'); - if (location.isExternal(href)) { + if (LocationUtils.isExternal(href)) { $a.attr('_target', 'blank'); return; } // Calcul absolute path for this - href = LocationUtils.toAbsolute(href, currentDirectory, currentDirectory); + href = LocationUtils.toAbsolute(href, currentDirectory, '.'); // Resolve file href = resolveFile(href); diff --git a/lib/utils/__tests__/location.js b/lib/utils/__tests__/location.js new file mode 100644 index 000000000..183086d7c --- /dev/null +++ b/lib/utils/__tests__/location.js @@ -0,0 +1,29 @@ +jest.autoMockOff(); + +describe('LocationUtils', function() { + var LocationUtils = require('../location'); + + describe('toAbsolute', function() { + + it('should resolve path starting by "/" in root directory', function() { + expect( + LocationUtils.toAbsolute('/test/hello.md', './', './') + ).toBe('test/hello.md'); + }); + + it('should resolve path starting by "/" in child directory', function() { + expect( + LocationUtils.toAbsolute('/test/hello.md', './hello', './') + ).toBe('test/hello.md'); + }); + + it('should resolve path starting by "/" in child directory, with same output directory', function() { + expect( + LocationUtils.toAbsolute('/test/hello.md', './hello', './hello') + ).toBe('../test/hello.md'); + }); + }); + +}); + + From f6e123f1ed36019a2ec5da1f97b27d22352b689a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 11:58:55 +0200 Subject: [PATCH 018/118] Add base rendering for template --- lib/models/plugin.js | 5 +++-- lib/models/templateBlock.js | 20 +++++++++++++++++++- lib/models/templateEngine.js | 26 +++++++++++++++++++++++++- lib/output/createTemplateEngine.js | 2 +- lib/output/generatePage.js | 16 ++++++++++++++-- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 6891d7d4c..ed7ad827d 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -87,12 +87,13 @@ Plugin.prototype.getFilters = function() { */ Plugin.prototype.getBlocks = function() { var blocks = this.getContent().get('blocks'); + blocks = blocks || Immutable.Map(); return blocks .map(function(block, blockName) { - block.name = blockName; - return new TemplateBlock(block); + return TemplateBlock.create(blockName, block); }) + .valueSeq() .toList(); }; diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js index ec7dc7c5a..cce636ed0 100644 --- a/lib/models/templateBlock.js +++ b/lib/models/templateBlock.js @@ -16,7 +16,7 @@ var TemplateBlock = Immutable.Record({ shortcuts: Immutable.List(), post: null, parse: true -}); +}, 'TemplateBlock'); TemplateBlock.prototype.getName = function() { return this.get('name'); @@ -250,6 +250,24 @@ TemplateBlock.indexBlockResult = function(blk) { return key; }; +/** + Create a template block from a function or an object + + @param {String} blockName + @param {Object} block + @return {TemplateBlock} +*/ +TemplateBlock.create = function(blockName, block) { + if (is.fn(block)) { + block = new Immutable.Map({ + process: block + }); + } + + block = block.set('name', blockName); + return new TemplateBlock(block); +}; + /** Extract kwargs from an arguments array diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index 1f9387971..aaa70fd2a 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -1,6 +1,8 @@ var nunjucks = require('nunjucks'); var Immutable = require('immutable'); +var Promise = require('../utils/promise'); + var TemplateEngine = Immutable.Record({ // List of {TemplateBlock} blocks: Immutable.List(), @@ -16,7 +18,7 @@ var TemplateEngine = Immutable.Record({ // Nunjucks loader loader: nunjucks.FileSystemLoader('views') -}); +}, 'TemplateEngine'); TemplateEngine.prototype.getBlocks = function() { return this.get('blocks'); @@ -103,4 +105,26 @@ TemplateEngine.prototype.bindToContext = function(fn) { return fn.bind(this.getContext()); }; +/** + Render a template + + @param {String} filePath + @param {String} content + @param {Object} ctx + @return {Promise} +*/ +TemplateEngine.prototype.render = function renderTemplate(filePath, content, context) { + context = context || {}; + var env = this.toNunjucks(); + + return Promise.nfcall( + env.renderString.bind(env), + content, + context, + { + path: filePath + } + ); +}; + module.exports = TemplateEngine; diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index 27dbebad4..fbf39d34e 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -25,7 +25,7 @@ function createTemplateEngine(output) { .map(function(plugin) { return plugin.getBlocks(); }) - .flatten(); + .flatten(1); // Extend with default blocks = defaultBlocks.concat(blocks); diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index 6fe0514dd..072c32719 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -2,6 +2,7 @@ var Promise = require('../utils/promise'); var error = require('../utils/error'); var Parse = require('../parse'); +var createTemplateEngine = require('./createTemplateEngine'); /** Prepare and generate HTML for a page @@ -26,9 +27,20 @@ function generatePage(output, page) { } return Promise(resultPage.getContent()) + + // Escape code blocks with raw tags .then(parser.page.prepare) - .then(parser.page) - .get('content') + + // Render templating syntax + .then(function(content) { + var engine = createTemplateEngine(output); + return engine.render(filePath, content); + }) + + // Render page using parser (markdown -> HTML) + .then(parser.page).get('content') + + // Return new page .then(function(content) { return resultPage.set('content', content); }); From 244fb0ca28f29ac429f58e0e885d21cc6c40beba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 12:24:59 +0200 Subject: [PATCH 019/118] Replace shortcuts in templating --- lib/models/templateEngine.js | 24 ------------------- lib/output/generatePage.js | 3 ++- lib/templating/index.js | 4 ++++ lib/templating/listShortcuts.js | 31 +++++++++++++++++++++++++ lib/templating/render.js | 30 ++++++++++++++++++++++++ lib/templating/replaceShortcuts.js | 37 ++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 lib/templating/index.js create mode 100644 lib/templating/listShortcuts.js create mode 100644 lib/templating/render.js create mode 100644 lib/templating/replaceShortcuts.js diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index aaa70fd2a..9a18bd442 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -1,8 +1,6 @@ var nunjucks = require('nunjucks'); var Immutable = require('immutable'); -var Promise = require('../utils/promise'); - var TemplateEngine = Immutable.Record({ // List of {TemplateBlock} blocks: Immutable.List(), @@ -105,26 +103,4 @@ TemplateEngine.prototype.bindToContext = function(fn) { return fn.bind(this.getContext()); }; -/** - Render a template - - @param {String} filePath - @param {String} content - @param {Object} ctx - @return {Promise} -*/ -TemplateEngine.prototype.render = function renderTemplate(filePath, content, context) { - context = context || {}; - var env = this.toNunjucks(); - - return Promise.nfcall( - env.renderString.bind(env), - content, - context, - { - path: filePath - } - ); -}; - module.exports = TemplateEngine; diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index 072c32719..4507f2eaf 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -2,6 +2,7 @@ var Promise = require('../utils/promise'); var error = require('../utils/error'); var Parse = require('../parse'); +var Templating = require('../templating'); var createTemplateEngine = require('./createTemplateEngine'); /** @@ -34,7 +35,7 @@ function generatePage(output, page) { // Render templating syntax .then(function(content) { var engine = createTemplateEngine(output); - return engine.render(filePath, content); + return Templating.render(engine, filePath, content); }) // Render page using parser (markdown -> HTML) diff --git a/lib/templating/index.js b/lib/templating/index.js new file mode 100644 index 000000000..312146a17 --- /dev/null +++ b/lib/templating/index.js @@ -0,0 +1,4 @@ + +module.exports = { + render: require('./render') +}; diff --git a/lib/templating/listShortcuts.js b/lib/templating/listShortcuts.js new file mode 100644 index 000000000..8f2388b5f --- /dev/null +++ b/lib/templating/listShortcuts.js @@ -0,0 +1,31 @@ +var Immutable = require('immutable'); +var parsers = require('../parsers'); + +/** + Return a list of all shortcuts that can apply + to a file for a TemplatEngine + + @param {TemplateEngine} engine + @param {String} filePath + @return {List} +*/ +function listShortcuts(engine, filePath) { + var blocks = engine.getBlocks(); + var parser = parsers.getForFile(filePath); + if (!parser) { + return Immutable.List(); + } + + return blocks + .map(function(block) { + var shortcuts = block.getShortcuts(); + + return shortcuts.filter(function(shortcut) { + var parsers = shortcut.get('parsers'); + return parsers.includes(parser.name); + }); + }) + .flatten(1); +} + +module.exports = listShortcuts; diff --git a/lib/templating/render.js b/lib/templating/render.js new file mode 100644 index 000000000..87d5096ed --- /dev/null +++ b/lib/templating/render.js @@ -0,0 +1,30 @@ +var Promise = require('../utils/promise'); + +var replaceShortcuts = require('./replaceShortcuts'); + +/** + Render a template + + @param {TemplateEngine} engine + @param {String} filePath + @param {String} content + @param {Object} ctx + @return {Promise} +*/ +function renderTemplate(engine, filePath, content, context) { + context = context || {}; + var env = engine.toNunjucks(); + + content = replaceShortcuts(engine, filePath, content); + + return Promise.nfcall( + env.renderString.bind(env), + content, + context, + { + path: filePath + } + ); +} + +module.exports = renderTemplate; diff --git a/lib/templating/replaceShortcuts.js b/lib/templating/replaceShortcuts.js new file mode 100644 index 000000000..f6a51cb47 --- /dev/null +++ b/lib/templating/replaceShortcuts.js @@ -0,0 +1,37 @@ +var escapeStringRegexp = require('escape-string-regexp'); +var listShortcuts = require('./listShortcuts'); + +/* + Apply a shortcut of block to a template + @param {String} content + @param {Shortcut} shortcut + @return {String} +*/ +function applyShortcut(content, shortcut) { + var tags = shortcut.get('tag'); + var start = shortcut.get('start'); + var end = shortcut.get('end'); + + var regex = new RegExp( + escapeStringRegexp(start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(end), + 'g' + ); + return content.replace(regex, function(all, match) { + return '{% ' + tags.start + ' %}' + match + '{% ' + tags.end + ' %}'; + }); +} + +/** + Replace shortcuts from blocks in a string + + @param {TemplateEngine} engine + @param {String} filePath + @param {String} content + @return {String} +*/ +function replaceShortcuts(engine, filePath, content) { + var shortcuts = listShortcuts(engine, filePath); + return shortcuts.reduce(applyShortcut, content); +} + +module.exports = replaceShortcuts; From 4aed2cf65240abb26384d7d86ccb27b171e6bb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 13:52:56 +0200 Subject: [PATCH 020/118] Add post processing for templates --- lib/models/templateBlock.js | 23 ++++++++++++++++++++++ lib/models/templateEngine.js | 13 +++++++++++++ lib/output/generatePage.js | 7 ++++++- lib/templating/index.js | 3 ++- lib/templating/postRender.js | 28 +++++++++++++++++++++++++++ lib/templating/replaceBlocks.js | 34 +++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 lib/templating/postRender.js create mode 100644 lib/templating/replaceBlocks.js diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js index cce636ed0..4094b8997 100644 --- a/lib/models/templateBlock.js +++ b/lib/models/templateBlock.js @@ -9,12 +9,24 @@ var NODE_ENDARGS = '%%endargs%%'; var blockBodies = {}; var TemplateBlock = Immutable.Record({ + // Name of block, also the start tag name: String(), + + // End tag, default to "end" end: String(), + + // Function to process the block content process: Function(), + + // List of String, for inner block tags blocks: Immutable.List(), + + // List of shortcuts to replace with this block shortcuts: Immutable.List(), + + // Function to execute in post processing post: null, + parse: true }, 'TemplateBlock'); @@ -208,6 +220,7 @@ TemplateBlock.prototype.handleBlockResult = function(result) { if (is.string(result)) { result = { body: result }; } + result.name = this.getName(); return result; }; @@ -250,6 +263,16 @@ TemplateBlock.indexBlockResult = function(blk) { return key; }; +/** + Get a block results indexed for a specific key + + @param {String} key + @return {Object|undefined} +*/ +TemplateBlock.getBlockResultByKey = function(key) { + return blockBodies[key]; +}; + /** Create a template block from a function or an object diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index 9a18bd442..28322ead5 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -42,6 +42,19 @@ TemplateEngine.prototype.getContext = function() { return this.get('context'); }; +/** + Return a block by its name (or undefined) + + @param {String} name + @return {TemplateBlock} +*/ +TemplateEngine.prototype.getBlock = function(name) { + var blocks = this.getBlocks(); + return blocks.find(function(block) { + return block.getName() === name; + }); +}; + /** Return a nunjucks environment from this configuration diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index 4507f2eaf..113bf2284 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -14,6 +14,7 @@ var createTemplateEngine = require('./createTemplateEngine'); */ function generatePage(output, page) { var book = output.getBook(); + var engine = createTemplateEngine(output); return Parse.parsePage(book, page) .then(function(resultPage) { @@ -34,13 +35,17 @@ function generatePage(output, page) { // Render templating syntax .then(function(content) { - var engine = createTemplateEngine(output); return Templating.render(engine, filePath, content); }) // Render page using parser (markdown -> HTML) .then(parser.page).get('content') + // Post processing for templating syntax + .then(function(content) { + return Templating.postRender(engine, content); + }) + // Return new page .then(function(content) { return resultPage.set('content', content); diff --git a/lib/templating/index.js b/lib/templating/index.js index 312146a17..3a8f9896b 100644 --- a/lib/templating/index.js +++ b/lib/templating/index.js @@ -1,4 +1,5 @@ module.exports = { - render: require('./render') + render: require('./render'), + postRender: require('./postRender') }; diff --git a/lib/templating/postRender.js b/lib/templating/postRender.js new file mode 100644 index 000000000..6928e825b --- /dev/null +++ b/lib/templating/postRender.js @@ -0,0 +1,28 @@ +var Promise = require('../utils/promise'); +var replaceBlocks = require('./replaceBlocks'); + +/** + Post render a template: + - Execute "post" for blocks + - Replace block content + + @param {TemplateEngine} engine + @param {String} content + @return {Promise} +*/ +function postRender(engine, content) { + var result = replaceBlocks(content); + + return Promise.forEach(result.blocks, function(blockType) { + var block = engine.getBlock(); + var post = block.getPost(); + if (!post) { + return; + } + + return post(); + }) + .thenResolve(result.content); +} + +module.exports = postRender; diff --git a/lib/templating/replaceBlocks.js b/lib/templating/replaceBlocks.js new file mode 100644 index 000000000..4b1c37f92 --- /dev/null +++ b/lib/templating/replaceBlocks.js @@ -0,0 +1,34 @@ +var Immutable = require('immutable'); +var TemplateBlock = require('../models/templateBlock'); + +/** + Replace position markers of blocks by body after processing + This is done to avoid that markdown/asciidoc processer parse the block content + + @param {String} content + @return {Object} {blocks: Set, content: String} +*/ +function replaceBlocks(content) { + var blockTypes = new Immutable.Set(); + var newContent = content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) { + var replacedWith = match; + + var block = TemplateBlock.getBlockResultByKey(key); + if (block) { + var result = replaceBlocks(block.body); + + blockTypes = blockTypes.add(block.name); + blockTypes = blockTypes.concat(result.blocks); + replacedWith = result.content; + } + + return replacedWith; + }); + + return { + content: newContent, + blocks: blockTypes + }; +} + +module.exports = replaceBlocks; From 14ce495e9e6f162a74ef1e202fef62855da2a5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 14:15:28 +0200 Subject: [PATCH 021/118] Add more tests for LocationUtils --- lib/utils/__tests__/location.js | 51 ++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/utils/__tests__/location.js b/lib/utils/__tests__/location.js index 183086d7c..f2037ffa3 100644 --- a/lib/utils/__tests__/location.js +++ b/lib/utils/__tests__/location.js @@ -3,7 +3,56 @@ jest.autoMockOff(); describe('LocationUtils', function() { var LocationUtils = require('../location'); - describe('toAbsolute', function() { + it('should correctly test external location', function() { + expect(LocationUtils.isExternal('http://google.fr')).toBe(true); + expect(LocationUtils.isExternal('https://google.fr')).toBe(true); + expect(LocationUtils.isExternal('test.md')).toBe(false); + expect(LocationUtils.isExternal('folder/test.md')).toBe(false); + expect(LocationUtils.isExternal('/folder/test.md')).toBe(false); + }); + + it('should correctly detect anchor location', function() { + expect(LocationUtils.isAnchor('#test')).toBe(true); + expect(LocationUtils.isAnchor(' #test')).toBe(true); + expect(LocationUtils.isAnchor('https://google.fr#test')).toBe(false); + expect(LocationUtils.isAnchor('test.md#test')).toBe(false); + }); + + describe('.relative', function() { + it('should resolve to a relative path (same folder)', function() { + expect(LocationUtils.relative('links/', 'links/test.md')).toBe('test.md'); + }); + + it('should resolve to a relative path (parent folder)', function() { + expect(LocationUtils.relative('links/', 'test.md')).toBe('../test.md'); + }); + + it('should resolve to a relative path (child folder)', function() { + expect(LocationUtils.relative('links/', 'links/hello/test.md')).toBe('hello/test.md'); + }); + }); + + describe('.toAbsolute', function() { + it('should correctly transform as absolute', function() { + expect(LocationUtils.toAbsolute('http://google.fr')).toBe('http://google.fr'); + expect(LocationUtils.toAbsolute('test.md', './', './')).toBe('test.md'); + expect(LocationUtils.toAbsolute('folder/test.md', './', './')).toBe('folder/test.md'); + }); + + it('should correctly handle windows path', function() { + expect(LocationUtils.toAbsolute('folder\\test.md', './', './')).toBe('folder/test.md'); + }); + + it('should correctly handle absolute path', function() { + expect(LocationUtils.toAbsolute('/test.md', './', './')).toBe('test.md'); + expect(LocationUtils.toAbsolute('/test.md', 'test', 'test')).toBe('../test.md'); + expect(LocationUtils.toAbsolute('/sub/test.md', 'test', 'test')).toBe('../sub/test.md'); + expect(LocationUtils.toAbsolute('/test.png', 'folder', '')).toBe('test.png'); + }); + + it('should correctly handle absolute path (windows)', function() { + expect(LocationUtils.toAbsolute('\\test.png', 'folder', '')).toBe('test.png'); + }); it('should resolve path starting by "/" in root directory', function() { expect( From ac2ad8d8b5b539cc86c13229d230caa2dd461afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 14:29:51 +0200 Subject: [PATCH 022/118] Add conrefs loader for templating --- lib/output/createTemplateEngine.js | 8 ++- lib/output/json/index.js | 2 +- lib/output/loaders/conrefs.js | 8 --- lib/output/website/createTemplateEngine.js | 15 +++++ lib/output/website/index.js | 6 ++ lib/output/website/onAsset.js | 23 +++++++ lib/output/website/onPage.js | 32 ++++++++++ lib/output/website/options.js | 8 +++ lib/templating/conrefsLoader.js | 72 ++++++++++++++++++++++ lib/templating/index.js | 5 +- lib/templating/renderFile.js | 27 ++++++++ 11 files changed, 194 insertions(+), 12 deletions(-) delete mode 100644 lib/output/loaders/conrefs.js create mode 100644 lib/output/website/createTemplateEngine.js create mode 100644 lib/output/website/index.js create mode 100644 lib/output/website/onAsset.js create mode 100644 lib/output/website/onPage.js create mode 100644 lib/output/website/options.js create mode 100644 lib/templating/conrefsLoader.js create mode 100644 lib/templating/renderFile.js diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index fbf39d34e..a79f93f0d 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -1,7 +1,7 @@ var Immutable = require('immutable'); +var Templating = require('../templating'); var TemplateEngine = require('../models/templateEngine'); -var ConrefsLoader = require('./loaders/conrefs'); var defaultBlocks = require('../constants/defaultBlocks'); var defaultFilters = require('../constants/defaultFilters'); @@ -15,6 +15,9 @@ var defaultFilters = require('../constants/defaultFilters'); */ function createTemplateEngine(output) { var plugins = output.getPlugins(); + var book = output.getBook(); + var rootFolder = book.getRoot(); + var logger = book.getLogger(); var filters = plugins .reduce(function(result, plugin) { @@ -31,7 +34,8 @@ function createTemplateEngine(output) { blocks = defaultBlocks.concat(blocks); filters = defaultFilters.merge(filters); - var loader = new ConrefsLoader(); + // Create loader + var loader = new Templating.ConrefsLoader(rootFolder, logger); return new TemplateEngine({ filters: filters, diff --git a/lib/output/json/index.js b/lib/output/json/index.js index f721d224b..e24c127b8 100644 --- a/lib/output/json/index.js +++ b/lib/output/json/index.js @@ -1,6 +1,6 @@ - module.exports = { + name: 'json', Options: require('./options'), onPage: require('./onPage') }; diff --git a/lib/output/loaders/conrefs.js b/lib/output/loaders/conrefs.js deleted file mode 100644 index 7580826c4..000000000 --- a/lib/output/loaders/conrefs.js +++ /dev/null @@ -1,8 +0,0 @@ -var nunjucks = require('nunjucks'); - -var Loader = nunjucks.Loader.extend({ - async: true, - -}); - -module.exports = Loader; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js new file mode 100644 index 000000000..9aed746a1 --- /dev/null +++ b/lib/output/website/createTemplateEngine.js @@ -0,0 +1,15 @@ +var TemplateEngine = require('../../models/templateEngine'); + +/** + Create templating engine to render themes + + @param {Output} + @return {TemplateEngine} +*/ +function createTemplateEngine(output) { + return new TemplateEngine({ + + }); +} + +module.exports = createTemplateEngine; diff --git a/lib/output/website/index.js b/lib/output/website/index.js new file mode 100644 index 000000000..e24c127b8 --- /dev/null +++ b/lib/output/website/index.js @@ -0,0 +1,6 @@ + +module.exports = { + name: 'json', + Options: require('./options'), + onPage: require('./onPage') +}; diff --git a/lib/output/website/onAsset.js b/lib/output/website/onAsset.js new file mode 100644 index 000000000..a2342aeb8 --- /dev/null +++ b/lib/output/website/onAsset.js @@ -0,0 +1,23 @@ +var path = require('path'); +var fs = require('../../utils/fs'); + +/** + Copy an asset to the outptu folder + + @param {Output} output + @param {Page} page +*/ +function onAsset(output, asset) { + var book = output.getBook(); + var options = output.getOptions(); + + var rootFolder = book.getRoot(); + var outputFolder = options.get('root'); + + var filePath = path.resolve(rootFolder, asset); + var outputPath = path.resolve(outputFolder, asset); + + return fs.copy(filePath, outputPath); +} + +module.exports = onAsset; diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js new file mode 100644 index 000000000..47274b3a4 --- /dev/null +++ b/lib/output/website/onPage.js @@ -0,0 +1,32 @@ +var Templating = require('../../templating'); +var JSONUtils = require('../../json'); +var Modifiers = require('../modifiers'); +var Writer = require('../writer'); +var getModifiers = require('../getModifiers'); +var createTemplateEngine = require('./createTemplateEngine'); + +/** + Write a page as a json file + + @param {Output} output + @param {Page} page +*/ +function onPage(output, page) { + var engine = createTemplateEngine(output); + + return Modifiers.modifyHTML(page, getModifiers(output, page)) + .then(function(resultPage) { + // Generate the context + var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + + // Render the theme + return Templating.renderFile(engine, 'website/page.html', context) + + // Write it to the disk + .then(function(html) { + return Writer.writePage(output, resultPage, html); + }); + }); +} + +module.exports = onPage; diff --git a/lib/output/website/options.js b/lib/output/website/options.js new file mode 100644 index 000000000..79167b180 --- /dev/null +++ b/lib/output/website/options.js @@ -0,0 +1,8 @@ +var Immutable = require('immutable'); + +var Options = Immutable.Record({ + // Root folder for the output + root: String() +}); + +module.exports = Options; diff --git a/lib/templating/conrefsLoader.js b/lib/templating/conrefsLoader.js new file mode 100644 index 000000000..e46b4d0fc --- /dev/null +++ b/lib/templating/conrefsLoader.js @@ -0,0 +1,72 @@ +var path = require('path'); +var nunjucks = require('nunjucks'); + +var fs = require('../utils/fs'); +var Git = require('../utils/git'); +var LocationUtils = require('../utils/location'); +var PathUtils = require('../utils/path'); + + +/** + Template loader resolving both: + - relative url ("./test.md") + - absolute url ("/test.md") + - git url ("") +*/ +var ConrefsLoader = nunjucks.Loader.extend({ + async: true, + + init: function(rootFolder, logger) { + this.rootFolder = rootFolder; + this.logger = logger; + this.git = new Git(); + }, + + getSource: function(sourceURL, callback) { + var that = this; + + this.git.resolve(sourceURL) + .then(function(filepath) { + // Is local file + if (!filepath) { + filepath = path.resolve(sourceURL); + } else { + if (that.logger) that.logger.debug.ln('resolve from git', sourceURL, 'to', filepath); + } + + // Read file from absolute path + return fs.readFile(filepath) + .then(function(source) { + return { + src: source.toString('utf8'), + path: filepath + }; + }); + }) + .nodeify(callback); + }, + + resolve: function(from, to) { + // If origin is in the book, we enforce result file to be in the book + if (PathUtils.isInRoot(this.root, from)) { + var href = LocationUtils.toAbsolute(to, path.dirname(from), ''); + return PathUtils.resolveInRoot(this.root, href); + } + + // If origin is in a git repository, we resolve file in the git repository + var gitRoot = this.git.resolveRoot(from); + if (gitRoot) { + return PathUtils.resolveInRoot(gitRoot, to); + } + + // If origin is not in the book (include from a git content ref) + return path.resolve(path.dirname(from), to); + }, + + // Handle all files as relative, so that nunjucks pass responsability to 'resolve' + isRelative: function(filename) { + return LocationUtils.isRelative(filename); + } +}); + +module.exports = ConrefsLoader; diff --git a/lib/templating/index.js b/lib/templating/index.js index 3a8f9896b..4cdf6efaf 100644 --- a/lib/templating/index.js +++ b/lib/templating/index.js @@ -1,5 +1,8 @@ module.exports = { render: require('./render'), - postRender: require('./postRender') + renderFile: require('./renderFile'), + postRender: require('./postRender'), + + ConrefsLoader: require('./conrefsLoader') }; diff --git a/lib/templating/renderFile.js b/lib/templating/renderFile.js new file mode 100644 index 000000000..13a9c5b30 --- /dev/null +++ b/lib/templating/renderFile.js @@ -0,0 +1,27 @@ +var Promise = require('../utils/promise'); + +var replaceShortcuts = require('./replaceShortcuts'); + +/** + Render a template + + @param {TemplateEngine} engine + @param {String} filePath + @param {String} content + @param {Object} ctx + @return {Promise} +*/ +function renderTemplateFile(engine, filePath, content, context) { + context = context || {}; + var env = engine.toNunjucks(); + + content = replaceShortcuts(engine, filePath, content); + + return Promise.nfcall( + env.render.bind(env), + content, + context + ); +} + +module.exports = renderTemplateFile; From 525bcd8295f72ffa09d485f86c7adea2187cde55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 14:37:49 +0200 Subject: [PATCH 023/118] Disable travis tests on 0.12 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8de84e728..45ba3e145 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: node_js node_js: - "stable" - "4.1" - - "0.12" before_install: - npm install svgexport -g after_success: From 4552ccd7d76a1bf958481e24a695dd3b68e6c232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 14:44:49 +0200 Subject: [PATCH 024/118] Add loader for themes --- lib/models/book.js | 10 ++ lib/output/createTemplateEngine.js | 2 +- lib/output/website/createTemplateEngine.js | 7 +- lib/output/website/listSearchPaths.js | 23 ++++ lib/output/website/onAsset.js | 2 +- lib/templating/index.js | 3 +- lib/templating/themesLoader.js | 117 +++++++++++++++++++++ lib/utils/path.js | 15 ++- 8 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 lib/output/website/listSearchPaths.js create mode 100644 lib/templating/themesLoader.js diff --git a/lib/models/book.js b/lib/models/book.js index 9b9f7690f..eb8011b13 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -104,6 +104,16 @@ Book.prototype.getRoot = function() { return fs.getRoot(); }; +/** + Return root for content of the book + + @return {String} +*/ +Book.prototype.getContentRoot = function() { + var fs = this.getContentFS(); + return fs.getRoot(); +}; + /** Check if a file is ignore (should not being parsed, etc) diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index a79f93f0d..9b29b2a3e 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -16,7 +16,7 @@ var defaultFilters = require('../constants/defaultFilters'); function createTemplateEngine(output) { var plugins = output.getPlugins(); var book = output.getBook(); - var rootFolder = book.getRoot(); + var rootFolder = book.getContentRoot(); var logger = book.getLogger(); var filters = plugins diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 9aed746a1..1a706679e 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -1,4 +1,6 @@ +var Templating = require('../../templating'); var TemplateEngine = require('../../models/templateEngine'); +var listSearchPaths = require('./listSearchPaths'); /** Create templating engine to render themes @@ -7,8 +9,11 @@ var TemplateEngine = require('../../models/templateEngine'); @return {TemplateEngine} */ function createTemplateEngine(output) { - return new TemplateEngine({ + var searchPaths = listSearchPaths(output); + var loader = new Templating.ThemesLoader(searchPaths); + return new TemplateEngine({ + loader: loader }); } diff --git a/lib/output/website/listSearchPaths.js b/lib/output/website/listSearchPaths.js new file mode 100644 index 000000000..c45f39c5d --- /dev/null +++ b/lib/output/website/listSearchPaths.js @@ -0,0 +1,23 @@ + +/** + List search paths for templates / i18n, etc + + @param {Output} output + @return {List} +*/ +function listSearchPaths(output) { + var book = output.getBook(); + var plugins = output.getPlugins(); + + var searchPaths = plugins + .valueSeq() + .map(function(plugin) { + return plugin.getPath(); + }) + .toList(); + + return searchPaths.unshift(book.getContentRoot()); +} + + +module.exports = listSearchPaths; diff --git a/lib/output/website/onAsset.js b/lib/output/website/onAsset.js index a2342aeb8..8659ca629 100644 --- a/lib/output/website/onAsset.js +++ b/lib/output/website/onAsset.js @@ -11,7 +11,7 @@ function onAsset(output, asset) { var book = output.getBook(); var options = output.getOptions(); - var rootFolder = book.getRoot(); + var rootFolder = book.getContentRoot(); var outputFolder = options.get('root'); var filePath = path.resolve(rootFolder, asset); diff --git a/lib/templating/index.js b/lib/templating/index.js index 4cdf6efaf..a33965dde 100644 --- a/lib/templating/index.js +++ b/lib/templating/index.js @@ -4,5 +4,6 @@ module.exports = { renderFile: require('./renderFile'), postRender: require('./postRender'), - ConrefsLoader: require('./conrefsLoader') + ConrefsLoader: require('./conrefsLoader'), + ThemesLoader: require('./themesLoader') }; diff --git a/lib/templating/themesLoader.js b/lib/templating/themesLoader.js new file mode 100644 index 000000000..2b436a4f2 --- /dev/null +++ b/lib/templating/themesLoader.js @@ -0,0 +1,117 @@ +var Immutable = require('immutable'); +var nunjucks = require('nunjucks'); +var fs = require('fs'); +var path = require('path'); + +var PathUtils = require('../utils/path'); + +/** + +*/ +var ThemesLoader = nunjucks.Loader.extend({ + init: function(searchPaths) { + this.searchPaths = Immutable.List(searchPaths) + .map(path.normalize); + }, + + /* + Read source of a resolved filepath + @param {String} + @return {Object} + */ + getSource: function(fullpath) { + if (!fullpath) return null; + + fullpath = this.resolve(null, fullpath); + var templateName = this.getTemplateName(fullpath); + + if(!fullpath) { + return null; + } + + var src = fs.readFileSync(fullpath, 'utf-8'); + + src = '{% do %}var template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' + + src + + '\n{% do %}template.self = template.stack.pop();{% enddo %}'; + + return { + src: src, + path: fullpath, + noCache: true + }; + }, + + /* + Nunjucks calls "isRelative" to determine when to call "resolve". + We handle absolute paths ourselves in ".resolve" so we always return true + */ + isRelative: function() { + return true; + }, + + /* + Get original search path containing a template + @param {String} filepath + @return {String} searchPath + */ + getSearchPath: function(filepath) { + return this.searchPaths + .sortBy(function(s) { + return -s.length; + }) + .find(function(basePath) { + return (filepath && filepath.indexOf(basePath) === 0); + }); + }, + + /* + Get template name from a filepath + @param {String} filepath + @return {String} name + */ + getTemplateName: function(filepath) { + var originalSearchPath = this.getSearchPath(filepath); + return originalSearchPath? path.relative(originalSearchPath, filepath) : null; + }, + + /* + Resolve a template from a current template + @param {String|null} from + @param {String} to + @return {String|null} + */ + resolve: function(from, to) { + var searchPaths = this.searchPaths; + + // Relative template like "./test.html" + if (PathUtils.isPureRelative(to) && from) { + return path.resolve(path.dirname(from), to); + } + + // Determine in which search folder we currently are + var originalSearchPath = this.getSearchPath(from); + var originalFilename = this.getTemplateName(from); + + // If we are including same file from a different search path + // Slice the search paths to avoid including from previous ones + if (originalFilename == to) { + var currentIndex = searchPaths.indexOf(originalSearchPath); + searchPaths = searchPaths.slice(currentIndex + 1); + } + + // Absolute template to resolve in root folder + var resultFolder = searchPaths.find(function(basePath) { + var p = path.resolve(basePath, to); + + return ( + p.indexOf(basePath) === 0 + && fs.existsSync(p) + ); + }); + if (!resultFolder) return null; + return path.resolve(resultFolder, to); + } +}); + +module.exports = ThemesLoader; diff --git a/lib/utils/path.js b/lib/utils/path.js index c233c923f..a4968c8ca 100644 --- a/lib/utils/path.js +++ b/lib/utils/path.js @@ -42,7 +42,7 @@ function resolveInRoot(root) { return result; } -// Chnage extension +// Chnage extension of a file function setExtension(filename, ext) { return path.join( path.dirname(filename), @@ -50,9 +50,20 @@ function setExtension(filename, ext) { ); } +/* + Return true if a filename is relative. + + @param {String} + @return {Boolean} +*/ +function isPureRelative(filename) { + return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); +} + module.exports = { isInRoot: isInRoot, resolveInRoot: resolveInRoot, normalize: normalizePath, - setExtension: setExtension + setExtension: setExtension, + isPureRelative: isPureRelative }; From 0c2dc06cb721a437480d5a10611511b8f84b00a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 15:36:18 +0200 Subject: [PATCH 025/118] Fix resolution of theme's templates --- lib/constants/templatesFolder.js | 2 ++ lib/output/index.js | 1 + lib/output/website/createTemplateEngine.js | 17 +++++++++- lib/output/website/index.js | 2 +- lib/output/website/onPage.js | 4 ++- lib/output/website/options.js | 5 ++- lib/templating/render.js | 2 +- lib/templating/renderFile.js | 37 ++++++++++++++-------- test.js | 2 +- 9 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 lib/constants/templatesFolder.js diff --git a/lib/constants/templatesFolder.js b/lib/constants/templatesFolder.js new file mode 100644 index 000000000..aad6a72a9 --- /dev/null +++ b/lib/constants/templatesFolder.js @@ -0,0 +1,2 @@ + +module.exports = '_layouts'; diff --git a/lib/output/index.js b/lib/output/index.js index d3534908d..9112666c7 100644 --- a/lib/output/index.js +++ b/lib/output/index.js @@ -1,5 +1,6 @@ module.exports = { generate: require('./generateBook'), JSONGenerator: require('./json'), + WebsiteGenerator: require('./website'), EbookGenerator: require('./ebook') }; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 1a706679e..b90c3ad25 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -1,7 +1,18 @@ +var path = require('path'); + +var TEMPLATES_FOLDER = require('../../constants/templatesFolder'); + var Templating = require('../../templating'); var TemplateEngine = require('../../models/templateEngine'); var listSearchPaths = require('./listSearchPaths'); +/** + Directory for a theme with the templates +*/ +function templateFolder(dir) { + return path.join(dir, TEMPLATES_FOLDER); +} + /** Create templating engine to render themes @@ -10,7 +21,11 @@ var listSearchPaths = require('./listSearchPaths'); */ function createTemplateEngine(output) { var searchPaths = listSearchPaths(output); - var loader = new Templating.ThemesLoader(searchPaths); + + // Search paths for templates + var tplSearchPaths = searchPaths.map(templateFolder); + + var loader = new Templating.ThemesLoader(tplSearchPaths); return new TemplateEngine({ loader: loader diff --git a/lib/output/website/index.js b/lib/output/website/index.js index e24c127b8..bb01ed604 100644 --- a/lib/output/website/index.js +++ b/lib/output/website/index.js @@ -1,6 +1,6 @@ module.exports = { - name: 'json', + name: 'website', Options: require('./options'), onPage: require('./onPage') }; diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index 47274b3a4..b6856b3ad 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -12,6 +12,8 @@ var createTemplateEngine = require('./createTemplateEngine'); @param {Page} page */ function onPage(output, page) { + var options = output.getOptions(); + var prefix = options.get('prefix'); var engine = createTemplateEngine(output); return Modifiers.modifyHTML(page, getModifiers(output, page)) @@ -20,7 +22,7 @@ function onPage(output, page) { var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); // Render the theme - return Templating.renderFile(engine, 'website/page.html', context) + return Templating.renderFile(engine, prefix + '/page.html', context) // Write it to the disk .then(function(html) { diff --git a/lib/output/website/options.js b/lib/output/website/options.js index 79167b180..eca8a8dc1 100644 --- a/lib/output/website/options.js +++ b/lib/output/website/options.js @@ -2,7 +2,10 @@ var Immutable = require('immutable'); var Options = Immutable.Record({ // Root folder for the output - root: String() + root: String(), + + // Prefix for generation + prefix: String('website') }); module.exports = Options; diff --git a/lib/templating/render.js b/lib/templating/render.js index 87d5096ed..bf21cfe05 100644 --- a/lib/templating/render.js +++ b/lib/templating/render.js @@ -8,7 +8,7 @@ var replaceShortcuts = require('./replaceShortcuts'); @param {TemplateEngine} engine @param {String} filePath @param {String} content - @param {Object} ctx + @param {Object} context @return {Promise} */ function renderTemplate(engine, filePath, content, context) { diff --git a/lib/templating/renderFile.js b/lib/templating/renderFile.js index 13a9c5b30..9b74e5bcd 100644 --- a/lib/templating/renderFile.js +++ b/lib/templating/renderFile.js @@ -1,27 +1,38 @@ var Promise = require('../utils/promise'); - -var replaceShortcuts = require('./replaceShortcuts'); +var error = require('../utils/error'); +var render = require('./render'); /** Render a template @param {TemplateEngine} engine @param {String} filePath - @param {String} content - @param {Object} ctx + @param {Object} context @return {Promise} */ -function renderTemplateFile(engine, filePath, content, context) { - context = context || {}; - var env = engine.toNunjucks(); +function renderTemplateFile(engine, filePath, context) { + var loader = engine.getLoader(); - content = replaceShortcuts(engine, filePath, content); + return Promise() + .then(function() { + if (!loader.async) { + return loader.getSource(filePath); + } + + var deferred = Promise.defer(); + loader.getSource(filePath, deferred.makeNodeResolver()); + return deferred.promise; + }) + .then(function(result) { + if (!result) { + throw error.TemplateError(new Error('Not found'), { + filename: filePath + }); + } + + return render(engine, result.path, result.src, context); + }); - return Promise.nfcall( - env.render.bind(env), - content, - context - ); } module.exports = renderTemplateFile; diff --git a/test.js b/test.js index 0de26b788..e1f3ac96d 100644 --- a/test.js +++ b/test.js @@ -16,7 +16,7 @@ var book = gitbook.Book.createForFS(fs); // Parse the book gitbook.Parse.parseBook(book) .then(function(_book) { - return Output.generate(Output.JSONGenerator, _book, { + return Output.generate(Output.WebsiteGenerator, _book, { root: path.join(__dirname, '_book') }); }) From f5df41c75f6e6d25ffc9f86feeac29892fb9f856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 25 Apr 2016 15:44:20 +0200 Subject: [PATCH 026/118] Enable extension "do" --- lib/models/templateEngine.js | 30 ++++++++++++++++++++++ lib/output/website/createTemplateEngine.js | 9 +++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index 28322ead5..114aa7322 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -5,6 +5,9 @@ var TemplateEngine = Immutable.Record({ // List of {TemplateBlock} blocks: Immutable.List(), + // List of Extension + extensions: Immutable.Map(), + // Map of filters: {String} name -> {Function} fn filters: Immutable.Map(), @@ -42,6 +45,10 @@ TemplateEngine.prototype.getContext = function() { return this.get('context'); }; +TemplateEngine.prototype.getExtensions = function() { + return this.get('extensions'); +}; + /** Return a block by its name (or undefined) @@ -66,6 +73,7 @@ TemplateEngine.prototype.toNunjucks = function() { var blocks = this.getBlocks(); var filters = this.getFilters(); var globals = this.getGlobals(); + var extensions = this.getExtensions(); var env = new nunjucks.Environment( loader, @@ -103,6 +111,11 @@ TemplateEngine.prototype.toNunjucks = function() { env.addGlobal(globalName, globalValue); }); + // Add other extensions + extensions.forEach(function(ext, extName) { + env.addExtension(extName, ext); + }); + return env; }; @@ -116,4 +129,21 @@ TemplateEngine.prototype.bindToContext = function(fn) { return fn.bind(this.getContext()); }; +/** + Create a template engine + + @param {Object} def + @return {TemplateEngine} +*/ +TemplateEngine.create = function(def) { + return new TemplateEngine({ + blocks: Immutable.List(def.blocks || []), + extensions: Immutable.Map(def.extensions || {}), + filters: Immutable.Map(def.filters || {}), + globals: Immutable.Map(def.globals || {}), + context: def.context, + loader: def.loader + }); +}; + module.exports = TemplateEngine; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index b90c3ad25..5c5acee8b 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -1,4 +1,6 @@ var path = require('path'); +var nunjucks = require('nunjucks'); +var DoExtension = require('nunjucks-do')(nunjucks); var TEMPLATES_FOLDER = require('../../constants/templatesFolder'); @@ -27,8 +29,11 @@ function createTemplateEngine(output) { var loader = new Templating.ThemesLoader(tplSearchPaths); - return new TemplateEngine({ - loader: loader + return TemplateEngine.create({ + loader: loader, + extensions: { + 'DoExtension': new DoExtension() + } }); } From c34a9f6213c204c43c78dd691a5295d7e5a757e8 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Mon, 25 Apr 2016 21:13:17 +0200 Subject: [PATCH 027/118] Copy assets from themes to output folder --- lib/constants/pluginAssetsFolder.js | 2 + lib/models/fs.js | 7 +-- lib/models/page.js | 9 ++++ lib/output/website/createTemplateEngine.js | 27 ++++++++++- lib/output/website/index.js | 4 +- lib/output/website/onAsset.js | 6 ++- lib/output/website/onFinish.js | 0 lib/output/website/onInit.js | 53 ++++++++++++++++++++++ lib/output/website/onPage.js | 8 +++- lib/output/website/options.js | 7 ++- 10 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 lib/constants/pluginAssetsFolder.js create mode 100644 lib/output/website/onFinish.js create mode 100644 lib/output/website/onInit.js diff --git a/lib/constants/pluginAssetsFolder.js b/lib/constants/pluginAssetsFolder.js new file mode 100644 index 000000000..cd4472231 --- /dev/null +++ b/lib/constants/pluginAssetsFolder.js @@ -0,0 +1,2 @@ + +module.exports = '_assets'; diff --git a/lib/models/fs.js b/lib/models/fs.js index 2400ff271..c74f3cda9 100644 --- a/lib/models/fs.js +++ b/lib/models/fs.js @@ -182,7 +182,7 @@ FS.prototype.listAllFiles = function(folder) { var isDirectory = pathIsFolder(file); if (!isDirectory) { - return out.push(file); + return out.push(path.join(folder, file)); } return that.listAllFiles(path.join(folder, file)) @@ -190,11 +190,6 @@ FS.prototype.listAllFiles = function(folder) { return out.concat(inner); }); }, Immutable.List()); - }) - .then(function(files) { - return files.map(function(file) { - return path.join(folder, file); - }); }); }; diff --git a/lib/models/page.js b/lib/models/page.js index 0a9cf3853..ffb960116 100644 --- a/lib/models/page.js +++ b/lib/models/page.js @@ -31,6 +31,15 @@ Page.prototype.getDir = function() { return this.get('dir'); }; +/** + Return path of the page + + @return {String} +*/ +Page.prototype.getPath = function() { + return this.getFile().getPath(); +}; + /** Create a page for a file diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 5c5acee8b..e66150971 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -18,10 +18,11 @@ function templateFolder(dir) { /** Create templating engine to render themes - @param {Output} + @param {Output} output + @param {String} currentFile @return {TemplateEngine} */ -function createTemplateEngine(output) { +function createTemplateEngine(output, currentFile) { var searchPaths = listSearchPaths(output); // Search paths for templates @@ -31,6 +32,28 @@ function createTemplateEngine(output) { return TemplateEngine.create({ loader: loader, + + filters: { + t: function(s) { + return s; + }, + resolveFile: function(s) { + return s; + }, + resolveAsset: function(s) { + return s; + }, + fileExists: function() { + return false; + }, + contentURL: function(s) { + return s; + }, + getArticleByPath: function(s) { + return undefined; + } + }, + extensions: { 'DoExtension': new DoExtension() } diff --git a/lib/output/website/index.js b/lib/output/website/index.js index bb01ed604..9c5614e57 100644 --- a/lib/output/website/index.js +++ b/lib/output/website/index.js @@ -2,5 +2,7 @@ module.exports = { name: 'website', Options: require('./options'), - onPage: require('./onPage') + onInit: require('./onInit'), + onPage: require('./onPage'), + onAsset: require('./onAsset') }; diff --git a/lib/output/website/onAsset.js b/lib/output/website/onAsset.js index 8659ca629..169db5691 100644 --- a/lib/output/website/onAsset.js +++ b/lib/output/website/onAsset.js @@ -17,7 +17,11 @@ function onAsset(output, asset) { var filePath = path.resolve(rootFolder, asset); var outputPath = path.resolve(outputFolder, asset); - return fs.copy(filePath, outputPath); + return fs.ensure(outputPath) + .then(function() { + return fs.copy(filePath, outputPath); + }) + .thenResolve(output); } module.exports = onAsset; diff --git a/lib/output/website/onFinish.js b/lib/output/website/onFinish.js new file mode 100644 index 000000000..e69de29bb diff --git a/lib/output/website/onInit.js b/lib/output/website/onInit.js new file mode 100644 index 000000000..632c6d627 --- /dev/null +++ b/lib/output/website/onInit.js @@ -0,0 +1,53 @@ +var path = require('path'); + +var ASSET_FOLDER = require('../../constants/pluginAssetsFolder'); +var Promise = require('../../utils/promise'); +var fs = require('../../utils/fs'); + +/** + Initialize the generator +*/ +function onInit(output) { + return Promise(output) + .then(copyPluginAssets) + .thenResolve(output); +} + + +/** + Copy all assets from plugins + + @param {Output} + @return {Promise} +*/ +function copyPluginAssets(output) { + var options = output.getOptions(); + var plugins = output.getPlugins(); + var logger = output.getLogger(); + + var outputRoot = options.get('root'); + var assetOutputFolder = path.join(outputRoot, 'gitbook'); + var prefix = options.get('prefix'); + + return Promise.forEach(plugins, function(plugin) { + var pluginRoot = plugin.getPath(); + var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix); + + if (!fs.existsSync(assetFolder)) { + return; + } + + logger.debug.ln('copy assets from theme', assetFolder); + return fs.copyDir( + assetFolder, + assetOutputFolder, + { + deleteFirst: false, + overwrite: true, + confirm: true + } + ); + }); +} + +module.exports = onInit; diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index b6856b3ad..aedb8c0b7 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -14,13 +14,19 @@ var createTemplateEngine = require('./createTemplateEngine'); function onPage(output, page) { var options = output.getOptions(); var prefix = options.get('prefix'); - var engine = createTemplateEngine(output); + var engine = createTemplateEngine(output, page.getPath()); return Modifiers.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { // Generate the context var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + context.template = { + getJSContext: function() { + return {}; + } + }; + // Render the theme return Templating.renderFile(engine, prefix + '/page.html', context) diff --git a/lib/output/website/options.js b/lib/output/website/options.js index eca8a8dc1..ac9cdad58 100644 --- a/lib/output/website/options.js +++ b/lib/output/website/options.js @@ -2,10 +2,13 @@ var Immutable = require('immutable'); var Options = Immutable.Record({ // Root folder for the output - root: String(), + root: String(), // Prefix for generation - prefix: String('website') + prefix: String('website'), + + // Use directory index url instead of "index.html" + directoryIndex: Boolean(true) }); module.exports = Options; From ab2ebefe2bb6dee75e064c1ad572749e3f8104d1 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Mon, 25 Apr 2016 22:30:41 +0200 Subject: [PATCH 028/118] Copy resources from plugins to output folder --- lib/models/plugin.js | 17 +++++++ lib/output/website/onInit.js | 99 ++++++++++++++++++++++++++++-------- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/lib/models/plugin.js b/lib/models/plugin.js index ed7ad827d..372558b16 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -73,6 +73,23 @@ Plugin.prototype.getHooks = function() { return this.getContent().get('hooks') || Immutable.Map(); }; +/** + Return infos about resources for a specific type + + @param {String} type + @return {Map} +*/ +Plugin.prototype.getResources = function(type) { + if (type != 'website' && type != 'ebook') { + throw new Error('Invalid assets type ' + type); + } + + var content = this.getContent(); + return (content.get(type) + || (type == 'website'? content.get('book') : null) + || Immutable.Map()); +}; + /** Return map of filters @return {Map} diff --git a/lib/output/website/onInit.js b/lib/output/website/onInit.js index 632c6d627..d348984e0 100644 --- a/lib/output/website/onInit.js +++ b/lib/output/website/onInit.js @@ -9,45 +9,104 @@ var fs = require('../../utils/fs'); */ function onInit(output) { return Promise(output) - .then(copyPluginAssets) + .then(copyAllPluginAssets) .thenResolve(output); } /** - Copy all assets from plugins + Copy all assets from plugins. + Assets are files stored in "_assets" + nd resources declared in the plugin itself. @param {Output} @return {Promise} */ -function copyPluginAssets(output) { - var options = output.getOptions(); +function copyAllPluginAssets(output) { var plugins = output.getPlugins(); + + return Promise.forEach(plugins, function(plugin) { + return copyAssets(output, plugin) + .then(function() { + return copyResources(output, plugin); + }); + }); +} + +/** + Copy assets from a plugin + + @param {Plugin} + @return {Promise} +*/ +function copyAssets(output, plugin) { var logger = output.getLogger(); + var pluginRoot = plugin.getPath(); + var options = output.getOptions(); var outputRoot = options.get('root'); var assetOutputFolder = path.join(outputRoot, 'gitbook'); var prefix = options.get('prefix'); - return Promise.forEach(plugins, function(plugin) { - var pluginRoot = plugin.getPath(); - var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix); + var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix); - if (!fs.existsSync(assetFolder)) { - return; + if (!fs.existsSync(assetFolder)) { + return Promise(); + } + + logger.debug.ln('copy assets from theme', assetFolder); + return fs.copyDir( + assetFolder, + assetOutputFolder, + { + deleteFirst: false, + overwrite: true, + confirm: true } + ); +} - logger.debug.ln('copy assets from theme', assetFolder); - return fs.copyDir( - assetFolder, - assetOutputFolder, - { - deleteFirst: false, - overwrite: true, - confirm: true - } - ); - }); +/** + Copy resources from a plugin + + @param {Plugin} + @return {Promise} +*/ +function copyResources(output, plugin) { + var logger = output.getLogger(); + + var options = output.getOptions(); + var prefix = options.get('prefix'); + var outputRoot = options.get('root'); + + var pluginRoot = plugin.getPath(); + var resources = plugin.getResources(prefix); + + var assetsFolder = resources.get('assets'); + var assetOutputFolder = path.join(outputRoot, 'gitbook', plugin.getNpmID()); + + if (!assetsFolder) { + return Promise(); + } + + // Resolve assets folder + assetsFolder = path.resolve(pluginRoot, assetsFolder); + if (!fs.existsSync(assetsFolder)) { + logger.warn.ln('assets folder for plugin "' + plugin.getName() + '" doesn\'t exist'); + return Promise(); + } + + logger.debug.ln('copy resources from plugin', assetsFolder); + + return fs.copyDir( + assetsFolder, + assetOutputFolder, + { + deleteFirst: false, + overwrite: true, + confirm: true + } + ); } module.exports = onInit; From 374ebd6f7a77bfbde00f5d1e730403afd86e018f Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 10:23:37 +0200 Subject: [PATCH 029/118] Add mock fs and tests for it --- .eslintrc | 3 +- lib/fs/__tests__/mock.js | 83 ++++++++++++++++++++ lib/fs/mock.js | 101 +++++++++++++++++++++++++ lib/models/fs.js | 3 +- lib/plugins/__tests__/findInstalled.js | 1 + 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 lib/fs/__tests__/mock.js create mode 100644 lib/fs/mock.js diff --git a/.eslintrc b/.eslintrc index 2dc8891aa..4e73ee74d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,8 @@ "node": true, "mocha": true, "browser": true, - "jest": true + "jest": true, + "jasmine": true }, "extends": "eslint:recommended" } \ No newline at end of file diff --git a/lib/fs/__tests__/mock.js b/lib/fs/__tests__/mock.js new file mode 100644 index 000000000..784201179 --- /dev/null +++ b/lib/fs/__tests__/mock.js @@ -0,0 +1,83 @@ +jest.autoMockOff(); + +describe('MockFS', function() { + var createMockFS = require('../mock'); + var fs = createMockFS({ + 'README.md': 'Hello World', + 'SUMMARY.md': '# Summary', + 'folder': { + 'test.md': 'Cool', + 'folder2': { + 'hello.md': 'Hello', + 'world.md': 'World' + } + } + }); + + describe('exists', function() { + pit('must return true for a file', function() { + return fs.exists('README.md') + .then(function(result) { + expect(result).toBeTruthy(); + }); + }); + + pit('must return false for a non existing file', function() { + return fs.exists('README_NOTEXISTS.md') + .then(function(result) { + expect(result).toBeFalsy(); + }); + }); + + pit('must return true for a directory', function() { + return fs.exists('folder') + .then(function(result) { + expect(result).toBeTruthy(); + }); + }); + + pit('must return true for a deep file', function() { + return fs.exists('folder/test.md') + .then(function(result) { + expect(result).toBeTruthy(); + }); + }); + + pit('must return true for a deep file (2)', function() { + return fs.exists('folder/folder2/hello.md') + .then(function(result) { + expect(result).toBeTruthy(); + }); + }); + }); + + describe('readAsString', function() { + pit('must return content for a file', function() { + return fs.readAsString('README.md') + .then(function(result) { + expect(result).toBe('Hello World'); + }); + }); + + pit('must return content for a deep file', function() { + return fs.readAsString('folder/test.md') + .then(function(result) { + expect(result).toBe('Cool'); + }); + }); + }); + + describe('readDir', function() { + pit('must return content for a directory', function() { + return fs.readDir('./') + .then(function(files) { + expect(files.size).toBe(3); + expect(files.includes('README.md')).toBeTruthy(); + expect(files.includes('SUMMARY.md')).toBeTruthy(); + expect(files.includes('folder/')).toBeTruthy(); + }); + }); + }); +}); + + diff --git a/lib/fs/mock.js b/lib/fs/mock.js new file mode 100644 index 000000000..6c2670bb8 --- /dev/null +++ b/lib/fs/mock.js @@ -0,0 +1,101 @@ +var path = require('path'); +var is = require('is'); +var Buffer = require('buffer').Buffer; +var Immutable = require('immutable'); + +var FS = require('../models/fs'); +var error = require('../utils/error'); + +/** + Create a fake filesystem for testing books + + @param {Map} +*/ +function createMockFS(files) { + files = Immutable.fromJS(files); + var mtime = new Date(); + + /** + Get a file by resolving its path + + @param {String} + @return {String|Map|null} + */ + function getFile(filePath) { + var parts = path.normalize(filePath).split('/'); + return parts.reduce(function(list, part, i) { + if (!list) return null; + + var file; + + if (!part || part === '.') file = list; + else file = list.get(part); + + if (!file) return null; + + if (is.string(file)) { + if (i === (parts.length - 1)) return file; + else return null; + } + + return file; + }, files); + } + + function fsExists(filePath) { + return Boolean(getFile(filePath) !== null); + } + + function fsReadFile(filePath) { + var file = getFile(filePath); + if (!is.string(file)) { + throw error.FileNotFoundError({ + filename: filePath + }); + } + + return new Buffer(file, 'utf8'); + } + + function fsStatFile(filePath) { + var file = getFile(filePath); + if (!file) { + throw error.FileNotFoundError({ + filename: filePath + }); + } + + return { + mtime: mtime + }; + } + + function fsReadDir(filePath) { + var dir = getFile(filePath); + if (!dir || is.string(dir)) { + throw error.FileNotFoundError({ + filename: filePath + }); + } + + return dir + .map(function(content, name) { + if (!is.string(content)) { + name = name + '/'; + } + + return name; + }) + .valueSeq(); + } + + return FS.create({ + root: '', + fsExists: fsExists, + fsReadFile: fsReadFile, + fsStatFile: fsStatFile, + fsReadDir: fsReadDir + }); +} + +module.exports = createMockFS; diff --git a/lib/models/fs.js b/lib/models/fs.js index c74f3cda9..f124da8bc 100644 --- a/lib/models/fs.js +++ b/lib/models/fs.js @@ -46,7 +46,8 @@ FS.prototype.isInScope = function(filename) { FS.prototype.resolve = function() { var rootPath = this.getRoot(); var args = Array.prototype.slice.call(arguments); - var filename = path.resolve.apply(path, [rootPath].concat(args)); + var filename = path.join.apply(path, [rootPath].concat(args)); + filename = path.normalize(filename); if (!this.isInScope(filename)) { throw error.FileOutOfScopeError({ diff --git a/lib/plugins/__tests__/findInstalled.js b/lib/plugins/__tests__/findInstalled.js index 956e73faa..fca8295b1 100644 --- a/lib/plugins/__tests__/findInstalled.js +++ b/lib/plugins/__tests__/findInstalled.js @@ -1,4 +1,5 @@ jest.autoMockOff(); +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; var path = require('path'); From 8a3f80ba82bd706083015f228c551d64768a287e Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 11:00:36 +0200 Subject: [PATCH 030/118] Add test for parsing glossary --- lib/constants/configDefault.js | 6 +++++ lib/models/config.js | 3 ++- lib/models/file.js | 9 +++++++ lib/parse/__tests__/parseGlossary.js | 37 ++++++++++++++++++++++++++++ lib/parse/parseGlossary.js | 1 - 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 lib/constants/configDefault.js create mode 100644 lib/parse/__tests__/parseGlossary.js diff --git a/lib/constants/configDefault.js b/lib/constants/configDefault.js new file mode 100644 index 000000000..0d9588391 --- /dev/null +++ b/lib/constants/configDefault.js @@ -0,0 +1,6 @@ +var Immutable = require('immutable'); +var jsonSchemaDefaults = require('json-schema-defaults'); + +var schema = require('./configSchema'); + +module.exports = Immutable.fromJS(jsonSchemaDefaults(schema)); diff --git a/lib/models/config.js b/lib/models/config.js index 8684a03e5..720ac57e4 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -2,10 +2,11 @@ var is = require('is'); var Immutable = require('immutable'); var File = require('./file'); +var configDefault = require('../constants/configDefault'); var Config = Immutable.Record({ file: File(), - values: Immutable.Map() + values: configDefault }, 'Config'); Config.prototype.getPath = function() { diff --git a/lib/models/file.js b/lib/models/file.js index b6b06eee0..ff7b899cc 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -19,6 +19,15 @@ File.prototype.getMTime = function() { return this.get('mtime'); }; +/** + Does the file exists / is set + + @return {Boolean} +*/ +File.prototype.exists = function() { + return Boolean(this.getPath()); +}; + /** Return type of file ('markdown' or 'asciidoc') diff --git a/lib/parse/__tests__/parseGlossary.js b/lib/parse/__tests__/parseGlossary.js new file mode 100644 index 000000000..1cb813c0c --- /dev/null +++ b/lib/parse/__tests__/parseGlossary.js @@ -0,0 +1,37 @@ +jest.autoMockOff(); + +describe('parseGlossary', function() { + var parseGlossary = require('../parseGlossary'); + var Book = require('../../models/book'); + var createMockFS = require('../../fs/mock'); + + pit('should parse glossary if exists', function() { + var fs = createMockFS({ + 'GLOSSARY.md': '# Glossary\n\n## Hello\nDescription for hello' + }); + var book = Book.createForFS(fs); + + return parseGlossary(book) + .then(function(resultBook) { + var glossary = resultBook.getGlossary(); + var file = glossary.getFile(); + var entries = glossary.getEntries(); + + expect(file.exists()).toBeTruthy(); + expect(entries.size).toBe(1); + }); + }); + + pit('should not fail if doesn\'t exist', function() { + var fs = createMockFS({}); + var book = Book.createForFS(fs); + + return parseGlossary(book) + .then(function(resultBook) { + var glossary = resultBook.getGlossary(); + var file = glossary.getFile(); + + expect(file.exists()).toBeFalsy(); + }); + }); +}); diff --git a/lib/parse/parseGlossary.js b/lib/parse/parseGlossary.js index 1dbfbe022..a96e5fcf6 100644 --- a/lib/parse/parseGlossary.js +++ b/lib/parse/parseGlossary.js @@ -16,7 +16,6 @@ function parseGlossary(book) { return book; } - logger.debug.ln('glossary index file found at', file.getPath()); var glossary = Glossary.createFromEntries(file, entries); From 57d83f082a6d08e1ac2a727fb6645ba2ba12342e Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 11:15:50 +0200 Subject: [PATCH 031/118] Add basic tests for summary parsing --- lib/parse/__tests__/parseGlossary.js | 5 ++-- lib/parse/__tests__/parseSummary.js | 36 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 lib/parse/__tests__/parseSummary.js diff --git a/lib/parse/__tests__/parseGlossary.js b/lib/parse/__tests__/parseGlossary.js index 1cb813c0c..14ff7cc85 100644 --- a/lib/parse/__tests__/parseGlossary.js +++ b/lib/parse/__tests__/parseGlossary.js @@ -1,9 +1,10 @@ jest.autoMockOff(); +var Book = require('../../models/book'); +var createMockFS = require('../../fs/mock'); + describe('parseGlossary', function() { var parseGlossary = require('../parseGlossary'); - var Book = require('../../models/book'); - var createMockFS = require('../../fs/mock'); pit('should parse glossary if exists', function() { var fs = createMockFS({ diff --git a/lib/parse/__tests__/parseSummary.js b/lib/parse/__tests__/parseSummary.js new file mode 100644 index 000000000..31e5d357d --- /dev/null +++ b/lib/parse/__tests__/parseSummary.js @@ -0,0 +1,36 @@ +jest.autoMockOff(); + +var Book = require('../../models/book'); +var createMockFS = require('../../fs/mock'); + +describe('parseSummary', function() { + var parseSummary = require('../parseSummary'); + + pit('should parse summary if exists', function() { + var fs = createMockFS({ + 'SUMMARY.md': '# Summary\n\n* [Hello](hello.md)' + }); + var book = Book.createForFS(fs); + + return parseSummary(book) + .then(function(resultBook) { + var summary = resultBook.getSummary(); + var file = summary.getFile(); + + expect(file.exists()).toBeTruthy(); + }); + }); + + pit('should not fail if doesn\'t exist', function() { + var fs = createMockFS({}); + var book = Book.createForFS(fs); + + return parseSummary(book) + .then(function(resultBook) { + var summary = resultBook.getSummary(); + var file = summary.getFile(); + + expect(file.exists()).toBeFalsy(); + }); + }); +}); From ea5e8891a833b20a2b03cc1e150fbe0d1df3a506 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 11:17:47 +0200 Subject: [PATCH 032/118] Add test for readme parsing --- lib/models/readme.js | 8 +++++++ lib/parse/__tests__/parseReadme.js | 38 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/parse/__tests__/parseReadme.js diff --git a/lib/models/readme.js b/lib/models/readme.js index 7a184c44f..4ad399277 100644 --- a/lib/models/readme.js +++ b/lib/models/readme.js @@ -12,6 +12,14 @@ Readme.prototype.getFile = function() { return this.get('file'); }; +Readme.prototype.getTitle = function() { + return this.get('title'); +}; + +Readme.prototype.getDescription = function() { + return this.get('description'); +}; + /** Create a new readme diff --git a/lib/parse/__tests__/parseReadme.js b/lib/parse/__tests__/parseReadme.js new file mode 100644 index 000000000..7d7c631f9 --- /dev/null +++ b/lib/parse/__tests__/parseReadme.js @@ -0,0 +1,38 @@ +jest.autoMockOff(); + +var Promise = require('../../utils/promise'); +var Book = require('../../models/book'); +var createMockFS = require('../../fs/mock'); + +describe('parseReadme', function() { + var parseReadme = require('../parseReadme'); + + pit('should parse summary if exists', function() { + var fs = createMockFS({ + 'README.md': '# Hello\n\nAnd here is the description.' + }); + var book = Book.createForFS(fs); + + return parseReadme(book) + .then(function(resultBook) { + var readme = resultBook.getReadme(); + var file = readme.getFile(); + + expect(file.exists()).toBeTruthy(); + expect(readme.getTitle()).toBe('Hello'); + expect(readme.getDescription()).toBe('And here is the description.'); + }); + }); + + pit('should fail if doesn\'t exist', function() { + var fs = createMockFS({}); + var book = Book.createForFS(fs); + + return parseReadme(book) + .then(function(resultBook) { + throw new Error('It should have fail'); + }, function() { + return Promise(); + }); + }); +}); From 1057bb15d3b7250dc541a126754750ec6b8a79a2 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 13:03:53 +0200 Subject: [PATCH 033/118] Complete method getByPath of Summary --- lib/models/summary.js | 10 ++++++---- lib/models/summaryArticle.js | 36 +++++++++++------------------------- lib/models/summaryPart.js | 10 ---------- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/lib/models/summary.js b/lib/models/summary.js index 4ab2d680d..d0dc8af42 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -24,7 +24,9 @@ Summary.prototype.getParts = function() { @return {Part|Article} */ Summary.prototype.getByLevel = function(level) { - return SummaryArticle.getByLevel(this, level, 'getParts'); + return SummaryArticle.findArticle(this, function(article) { + return (article.getLevel() === level); + }, 'getParts'); }; /** @@ -34,9 +36,9 @@ Summary.prototype.getByLevel = function(level) { @return {Part|Article} */ Summary.prototype.getByPath = function(filePath) { - // todo - - return undefined; + return SummaryArticle.findArticle(this, function(article) { + return (article.getPath() === filePath); + }, 'getParts'); }; /** diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index 945a16caa..d6cd045e0 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -38,16 +38,6 @@ SummaryArticle.prototype.getDepth = function() { return this.getLevel().split('.').length; }; -/** - Return an article by its level - - @param {String} level - @return {Article} -*/ -SummaryArticle.prototype.getByLevel = function(level) { - return SummaryArticle.getByLevel(this, level); -}; - /** Get path (without anchor) to the pointing file @@ -131,31 +121,27 @@ SummaryArticle.create = function(def, level) { /** - Return an article by its level + Find an article from a base one @param {Article|Part} base - @param {String} level + @param {Function(article)} iter @param {String} method @return {Article} */ -SummaryArticle.getByLevel = function(base, level, method) { +SummaryArticle.findArticle = function(base, iter, method) { method = method || 'getArticles'; var articles = base[method](); - var levelParts = level.split('.'); - var baseLevel = levelParts.shift(); - var result = articles.find(function(a) { - return a.getLevel() === baseLevel; - }); + return articles.reduce(function(result, article) { + if (result) return result; - if (!result) { - return undefined; - } - if (levelParts.length === 0) { - return result; - } + if (iter(article)) { + return article; + } - return SummaryArticle.getByLevel(result, levelParts.join('.')); + return SummaryArticle.findArticle(article, iter); + }, null); }; + module.exports = SummaryArticle; diff --git a/lib/models/summaryPart.js b/lib/models/summaryPart.js index 345cf83f2..c8c608dc7 100644 --- a/lib/models/summaryPart.js +++ b/lib/models/summaryPart.js @@ -24,16 +24,6 @@ SummaryPart.prototype.getArticles = function() { return this.get('articles'); }; -/** - Return an article by its level - - @param {String} level - @return {Article} -*/ -SummaryPart.prototype.getByLevel = function(level) { - return SummaryArticle.getByLevel(this, level); -}; - /** Create a SummaryPart From be828cf004056d143c1e46c10ab29cc147fc14f8 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 14:12:26 +0200 Subject: [PATCH 034/118] Add duration to final log for generation --- lib/api/index.js | 5 +++++ lib/models/summary.js | 31 ++++++++++++++++++++++++------- lib/models/summaryArticle.js | 8 +++----- lib/output/callPageHook.js | 14 +++++++++----- lib/output/generateBook.js | 6 +++++- lib/output/generatePage.js | 13 +++++++++++-- 6 files changed, 57 insertions(+), 20 deletions(-) create mode 100644 lib/api/index.js diff --git a/lib/api/index.js b/lib/api/index.js new file mode 100644 index 000000000..213ef1ad9 --- /dev/null +++ b/lib/api/index.js @@ -0,0 +1,5 @@ + +module.exports = { + encodePage: require('./encodePage'), + decodePage: require('./decodePage') +}; diff --git a/lib/models/summary.js b/lib/models/summary.js index d0dc8af42..d1df91fe2 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -7,7 +7,7 @@ var SummaryArticle = require('./summaryArticle'); var Summary = Immutable.Record({ file: File(), parts: Immutable.List() -}); +}, 'Summary'); Summary.prototype.getFile = function() { return this.get('file'); @@ -17,28 +17,45 @@ Summary.prototype.getParts = function() { return this.get('parts'); }; +/** + Return an article using an iterator to find it + + @param {Function} iter + @return {Article} +*/ +Summary.prototype.getArticle = function(iter) { + var parts = this.getParts(); + + return parts.reduce(function(result, part) { + if (result) return result; + + return SummaryArticle.findArticle(part, iter); + }, null); +}; + + /** Return a part/article by its level @param {String} level - @return {Part|Article} + @return {Article} */ Summary.prototype.getByLevel = function(level) { - return SummaryArticle.findArticle(this, function(article) { + return this.getArticle(function(article) { return (article.getLevel() === level); - }, 'getParts'); + }); }; /** Return an article by its path @param {String} filePath - @return {Part|Article} + @return {Article} */ Summary.prototype.getByPath = function(filePath) { - return SummaryArticle.findArticle(this, function(article) { + return this.getArticle(function(article) { return (article.getPath() === filePath); - }, 'getParts'); + }); }; /** diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index d6cd045e0..ec1104cd8 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -11,7 +11,7 @@ var SummaryArticle = Immutable.Record({ title: String(), ref: String(), articles: Immutable.List() -}); +}, 'SummaryArticle'); SummaryArticle.prototype.getLevel = function() { return this.get('level'); @@ -125,12 +125,10 @@ SummaryArticle.create = function(def, level) { @param {Article|Part} base @param {Function(article)} iter - @param {String} method @return {Article} */ -SummaryArticle.findArticle = function(base, iter, method) { - method = method || 'getArticles'; - var articles = base[method](); +SummaryArticle.findArticle = function(base, iter) { + var articles = base.getArticles(); return articles.reduce(function(result, article) { if (result) return result; diff --git a/lib/output/callPageHook.js b/lib/output/callPageHook.js index ed80823c1..6f6bcbbab 100644 --- a/lib/output/callPageHook.js +++ b/lib/output/callPageHook.js @@ -1,6 +1,8 @@ var Api = require('../api'); var callHook = require('./callHook'); +var Promise = require('../utils/promise'); + /** Call a hook for a specific page @@ -10,19 +12,21 @@ var callHook = require('./callHook'); @return {Promise} */ function callPageHook(name, output, page) { - return callHook( + return Promise(page); + + /*return callHook( name, function(out) { - return Api.encodePage(output, page); + return Api.encodePage(out, page); }, - function(result) { - return Api.decodePage(output, page, result); + function(out, result) { + return Api.decodePage(out, page, result); }, output - ); + );*/ } module.exports = callPageHook; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index ca19067d0..1fe75fdfb 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -33,6 +33,7 @@ var generatePages = require('./generatePages'); */ function generateBook(generator, book, options) { options = generator.Options(options); + var start = Date.now(); return Promise( Output.createForBook(book, options) @@ -117,7 +118,10 @@ function generateBook(generator, book, options) { .then(function(output) { var logger = output.getLogger(); - logger.info.ok('generation finished with success!'); + var end = Date.now(); + var duration = (end - start)/1000; + + logger.info.ok('generation finished with success in ' + duration.toFixed(1) + 's !'); }); } diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index 113bf2284..aa3b37689 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -4,6 +4,7 @@ var error = require('../utils/error'); var Parse = require('../parse'); var Templating = require('../templating'); var createTemplateEngine = require('./createTemplateEngine'); +var callPageHook = require('./callPageHook'); /** Prepare and generate HTML for a page @@ -28,10 +29,13 @@ function generatePage(output, page) { })); } - return Promise(resultPage.getContent()) + // Call hook "page:before" + return callPageHook('page:before', output, resultPage) // Escape code blocks with raw tags - .then(parser.page.prepare) + .then(function(currentPage) { + return parser.page.prepare(currentPage.getContent()); + }) // Render templating syntax .then(function(content) { @@ -49,6 +53,11 @@ function generatePage(output, page) { // Return new page .then(function(content) { return resultPage.set('content', content); + }) + + // Call final hook + .then(function(currentPage) { + return callPageHook('page', output, currentPage); }); }); } From ce99c8c7061eb9b4a2a5f4ff534d7fbeaf37cad0 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 14:25:54 +0200 Subject: [PATCH 035/118] Add more tests for summary --- lib/models/__tests__/summary.js | 77 +++++++++++++++++++++++---------- lib/models/summary.js | 17 +++++--- lib/models/summaryArticle.js | 2 +- lib/models/summaryPart.js | 2 +- 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/lib/models/__tests__/summary.js b/lib/models/__tests__/summary.js index e641efefa..b80830728 100644 --- a/lib/models/__tests__/summary.js +++ b/lib/models/__tests__/summary.js @@ -4,35 +4,68 @@ describe('Summary', function() { var File = require('../file'); var Summary = require('../summary'); - describe('createFromEntries', function() { - var summary = Summary.createFromParts(File(), [ - { - articles: [ - { - title: 'My First Article', - path: 'README.md' - }, - { - title: 'My Second Article', - path: 'article.md' - } - ] - }, - { - title: 'Test' - } - ]); + var summary = Summary.createFromParts(File(), [ + { + articles: [ + { + title: 'My First Article', + path: 'README.md' + }, + { + title: 'My Second Article', + path: 'article.md' + } + ] + }, + { + title: 'Test' + } + ]); + describe('createFromEntries', function() { it('must add all parts', function() { var parts = summary.getParts(); expect(parts.size).toBe(2); }); + }); - it('must index by level', function() { - var part1 = summary.getByLevel('0'); + describe('getByLevel', function() { + it('can return a Part', function() { + var part = summary.getByLevel('1'); - expect(part1).toBeDefined(); - expect(part1.getArticles().size).toBe(2); + expect(part).toBeDefined(); + expect(part.getArticles().size).toBe(2); + }); + + it('can return a Part (2)', function() { + var part = summary.getByLevel('2'); + + expect(part).toBeDefined(); + expect(part.getTitle()).toBe('Test'); + expect(part.getArticles().size).toBe(0); + }); + + it('can return an Article', function() { + var article = summary.getByLevel('1.1'); + + expect(article).toBeDefined(); + expect(article.getTitle()).toBe('My First Article'); + }); + }); + + describe('getByPath', function() { + it('return correct article', function() { + var article = summary.getByPath('README.md'); + + expect(article).toBeDefined(); + expect(article.getTitle()).toBe('My First Article'); + }); + + it('return correct article', function() { + var article = summary.getByPath('article.md'); + + expect(article).toBeDefined(); + expect(article.getTitle()).toBe('My Second Article'); }); }); }); diff --git a/lib/models/summary.js b/lib/models/summary.js index d1df91fe2..d8d0603e9 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -18,17 +18,20 @@ Summary.prototype.getParts = function() { }; /** - Return an article using an iterator to find it + Return an article using an iterator to find it. + if "partIter" is set, it can also return a Part. @param {Function} iter - @return {Article} + @param {Function} partIter + @return {Article|Part} */ -Summary.prototype.getArticle = function(iter) { +Summary.prototype.getArticle = function(iter, partIter) { var parts = this.getParts(); return parts.reduce(function(result, part) { if (result) return result; + if (partIter && partIter(part)) return part; return SummaryArticle.findArticle(part, iter); }, null); }; @@ -41,9 +44,11 @@ Summary.prototype.getArticle = function(iter) { @return {Article} */ Summary.prototype.getByLevel = function(level) { - return this.getArticle(function(article) { + function iterByLevel(article) { return (article.getLevel() === level); - }); + } + + return this.getArticle(iterByLevel, iterByLevel); }; /** @@ -70,7 +75,7 @@ Summary.createFromParts = function createFromParts(file, parts) { return part; } - return SummaryPart.create(part, i); + return SummaryPart.create(part, i + 1); }); return new Summary({ diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index ec1104cd8..d2fac68c0 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -108,7 +108,7 @@ SummaryArticle.create = function(def, level) { if (article instanceof SummaryArticle) { return article; } - return SummaryArticle.create(article, [level, i].join('.')); + return SummaryArticle.create(article, [level, i + 1].join('.')); }); return new SummaryArticle({ diff --git a/lib/models/summaryPart.js b/lib/models/summaryPart.js index c8c608dc7..f7a82ced6 100644 --- a/lib/models/summaryPart.js +++ b/lib/models/summaryPart.js @@ -35,7 +35,7 @@ SummaryPart.create = function(def, level) { if (article instanceof SummaryArticle) { return article; } - return SummaryArticle.create(article, [level, i].join('.')); + return SummaryArticle.create(article, [level, i + 1].join('.')); }); return new SummaryPart({ From fa17f1671685a97e88c84fdba6a94744c54a09b8 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 15:23:28 +0200 Subject: [PATCH 036/118] Add base for commands Improve plugins installation --- book.js | 2 +- lib/cli/build.js | 30 +++++ lib/cli/getBook.js | 23 ++++ lib/cli/index.js | 198 +-------------------------------- lib/cli/indexe.js | 199 ++++++++++++++++++++++++++++++++++ lib/cli/install.js | 21 ++++ lib/cli/options.js | 23 ++++ lib/cli/parse.js | 68 ++++++++++++ lib/cli/serve.js | 19 ++++ lib/cli/watch.js | 7 +- lib/index.js | 4 +- lib/models/config.js | 4 +- lib/output/index.js | 24 +++- lib/plugins/installPlugins.js | 35 ++++-- lib/plugins/listAll.js | 4 +- 15 files changed, 451 insertions(+), 210 deletions(-) create mode 100644 lib/cli/build.js create mode 100644 lib/cli/getBook.js create mode 100644 lib/cli/indexe.js create mode 100644 lib/cli/install.js create mode 100644 lib/cli/options.js create mode 100644 lib/cli/parse.js create mode 100644 lib/cli/serve.js diff --git a/book.js b/book.js index a1df67cde..d6afa1f58 100644 --- a/book.js +++ b/book.js @@ -9,7 +9,7 @@ module.exports = { gitbook: '>=3.0.0-pre.0', // Use the "official" theme - //plugins: ['theme-official', 'sitemap'], + plugins: ['theme-official', 'sitemap'], variables: { version: pkg.version diff --git a/lib/cli/build.js b/lib/cli/build.js new file mode 100644 index 000000000..49931bdf2 --- /dev/null +++ b/lib/cli/build.js @@ -0,0 +1,30 @@ +var path = require('path'); + +var options = require('./options'); +var getBook = require('./getBook'); + +var Parse = require('../parse'); +var Output = require('../output'); + +module.exports = { + name: 'build [book] [output]', + description: 'build a book', + options: [ + options.log, + options.format + ], + exec: function(args, kwargs) { + var book = getBook(args, kwargs); + var Generator = Output.getGenerator(kwargs.format); + + return Parse.parseBook(book) + .then(function(resultBook) { + var defaultOutputRoot = path.join(resultBook.getRoot(), '_book'); + var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot; + + return Output.generate(Generator, resultBook, { + root: outputFolder + }); + }); + } +}; diff --git a/lib/cli/getBook.js b/lib/cli/getBook.js new file mode 100644 index 000000000..5c2907875 --- /dev/null +++ b/lib/cli/getBook.js @@ -0,0 +1,23 @@ +var path = require('path'); +var Book = require('../models/book'); +var createNodeFS = require('../fs/node'); + +/** + Return a book instance to work on from + command line args/kwargs + + @param {Array} args + @param {Object} kwargs + @return {Book} +*/ +function getBook(args, kwargs) { + var input = path.resolve(args[0] || process.cwd()); + var logLevel = kwargs.log; + + var fs = createNodeFS(input); + var book = Book.createForFS(fs); + + return book; +} + +module.exports = getBook; diff --git a/lib/cli/index.js b/lib/cli/index.js index eea707f57..b93b7b106 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,199 +1,9 @@ -/* eslint-disable no-console */ - -var _ = require('lodash'); -var path = require('path'); -var tinylr = require('tiny-lr'); - -var Promise = require('../utils/promise'); -var PluginsManager = require('../plugins'); -var Book = require('../book'); - -var helper = require('./helper'); -var Server = require('./server'); -var watch = require('./watch'); module.exports = { commands: [ - { - name: 'init [book]', - description: 'setup and create files for chapters', - options: [ - helper.options.log - ], - exec: function(args) { - var input = path.resolve(args[0] || process.cwd()); - return Book.init(helper.nodeFS, input); - } - }, - - { - name: 'parse [book]', - description: 'parse and returns debug information for a book', - options: [ - helper.options.log - ], - exec: helper.bookCmd(function(book) { - return book.parse() - .then(function() { - book.log.info.ln('Book located in:', book.root); - book.log.info.ln(''); - - if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path); - - if (book.isMultilingual()) { - book.log.info.ln('Multilingual book detected:', book.langs.path); - } else { - book.log.info.ln('Readme:', book.readme.path); - book.log.info.ln('Summary:', book.summary.path); - if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path); - - book.log.info.ln('Pages:'); - _.each(book.pages, function(page) { - book.log.info.ln('\t-', page.path); - }); - } - }); - }) - }, - - { - name: 'install [book]', - description: 'install all plugins dependencies', - options: [ - helper.options.log - ], - exec: helper.bookCmd(function(book, args) { - var plugins = new PluginsManager(book); - - return book.config.load() - .then(function() { - return plugins.install(); - }); - }) - }, - - { - name: 'build [book] [output]', - description: 'build a book', - options: [ - helper.options.log, - helper.options.format - ], - exec: helper.outputCmd(function(output, args, kwargs) { - return output.book.parse() - .then(function() { - return output.generate(); - }); - }) - }, - - helper.ebookCmd('pdf'), - helper.ebookCmd('epub'), - helper.ebookCmd('mobi'), - - { - name: 'serve [book]', - description: 'Build then serve a book from a directory', - options: [ - { - name: 'port', - description: 'Port for server to listen on', - defaults: 4000 - }, - { - name: 'lrport', - description: 'Port for livereload server to listen on', - defaults: 35729 - }, - { - name: 'watch', - description: 'Enable/disable file watcher', - defaults: true - }, - helper.options.format, - helper.options.log - ], - exec: function(args, kwargs) { - var input = path.resolve(args[0] || process.cwd()); - var server = new Server(); - - // Init livereload server - var lrServer = tinylr({}); - var port = kwargs.port; - var lrPath; - - var generate = function() { - - // Stop server if running - if (server.isRunning()) console.log('Stopping server'); - return server.stop() - - // Generate the book - .then(function() { - var book = new Book({ - fs: helper.nodeFS, - root: input, - logLevel: kwargs.log - }); - - return book.parse() - .then(function() { - // Add livereload plugin - book.config.set('plugins', - book.config.get('plugins') - .concat([ - { name: 'livereload' } - ]) - ); - - var Out = helper.FORMATS[kwargs.format]; - var output = new Out(book); - - return output.generate() - .thenResolve(output); - }); - }) - - // Start server and watch changes - .then(function(output) { - console.log(); - console.log('Starting server ...'); - return server.start(output.root(), port) - .then(function() { - console.log('Serving book on http://localhost:'+port); - - if (lrPath) { - // trigger livereload - lrServer.changed({ - body: { - files: [lrPath] - } - }); - } - - if (!kwargs.watch) return; - - return watch(output.book.root) - .then(function(filepath) { - // set livereload path - lrPath = filepath; - console.log('Restart after change in file', filepath); - console.log(''); - return generate(); - }); - }); - }); - }; - - return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport) - .then(function() { - console.log('Live reload server started on port:', kwargs.lrport); - console.log('Press CTRL+C to quit ...'); - console.log(''); - return generate(); - }); - } - } - + require('./parse'), + require('./build'), + require('./serve'), + require('./install') ] }; diff --git a/lib/cli/indexe.js b/lib/cli/indexe.js new file mode 100644 index 000000000..eea707f57 --- /dev/null +++ b/lib/cli/indexe.js @@ -0,0 +1,199 @@ +/* eslint-disable no-console */ + +var _ = require('lodash'); +var path = require('path'); +var tinylr = require('tiny-lr'); + +var Promise = require('../utils/promise'); +var PluginsManager = require('../plugins'); +var Book = require('../book'); + +var helper = require('./helper'); +var Server = require('./server'); +var watch = require('./watch'); + +module.exports = { + commands: [ + { + name: 'init [book]', + description: 'setup and create files for chapters', + options: [ + helper.options.log + ], + exec: function(args) { + var input = path.resolve(args[0] || process.cwd()); + return Book.init(helper.nodeFS, input); + } + }, + + { + name: 'parse [book]', + description: 'parse and returns debug information for a book', + options: [ + helper.options.log + ], + exec: helper.bookCmd(function(book) { + return book.parse() + .then(function() { + book.log.info.ln('Book located in:', book.root); + book.log.info.ln(''); + + if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path); + + if (book.isMultilingual()) { + book.log.info.ln('Multilingual book detected:', book.langs.path); + } else { + book.log.info.ln('Readme:', book.readme.path); + book.log.info.ln('Summary:', book.summary.path); + if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path); + + book.log.info.ln('Pages:'); + _.each(book.pages, function(page) { + book.log.info.ln('\t-', page.path); + }); + } + }); + }) + }, + + { + name: 'install [book]', + description: 'install all plugins dependencies', + options: [ + helper.options.log + ], + exec: helper.bookCmd(function(book, args) { + var plugins = new PluginsManager(book); + + return book.config.load() + .then(function() { + return plugins.install(); + }); + }) + }, + + { + name: 'build [book] [output]', + description: 'build a book', + options: [ + helper.options.log, + helper.options.format + ], + exec: helper.outputCmd(function(output, args, kwargs) { + return output.book.parse() + .then(function() { + return output.generate(); + }); + }) + }, + + helper.ebookCmd('pdf'), + helper.ebookCmd('epub'), + helper.ebookCmd('mobi'), + + { + name: 'serve [book]', + description: 'Build then serve a book from a directory', + options: [ + { + name: 'port', + description: 'Port for server to listen on', + defaults: 4000 + }, + { + name: 'lrport', + description: 'Port for livereload server to listen on', + defaults: 35729 + }, + { + name: 'watch', + description: 'Enable/disable file watcher', + defaults: true + }, + helper.options.format, + helper.options.log + ], + exec: function(args, kwargs) { + var input = path.resolve(args[0] || process.cwd()); + var server = new Server(); + + // Init livereload server + var lrServer = tinylr({}); + var port = kwargs.port; + var lrPath; + + var generate = function() { + + // Stop server if running + if (server.isRunning()) console.log('Stopping server'); + return server.stop() + + // Generate the book + .then(function() { + var book = new Book({ + fs: helper.nodeFS, + root: input, + logLevel: kwargs.log + }); + + return book.parse() + .then(function() { + // Add livereload plugin + book.config.set('plugins', + book.config.get('plugins') + .concat([ + { name: 'livereload' } + ]) + ); + + var Out = helper.FORMATS[kwargs.format]; + var output = new Out(book); + + return output.generate() + .thenResolve(output); + }); + }) + + // Start server and watch changes + .then(function(output) { + console.log(); + console.log('Starting server ...'); + return server.start(output.root(), port) + .then(function() { + console.log('Serving book on http://localhost:'+port); + + if (lrPath) { + // trigger livereload + lrServer.changed({ + body: { + files: [lrPath] + } + }); + } + + if (!kwargs.watch) return; + + return watch(output.book.root) + .then(function(filepath) { + // set livereload path + lrPath = filepath; + console.log('Restart after change in file', filepath); + console.log(''); + return generate(); + }); + }); + }); + }; + + return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport) + .then(function() { + console.log('Live reload server started on port:', kwargs.lrport); + console.log('Press CTRL+C to quit ...'); + console.log(''); + return generate(); + }); + } + } + + ] +}; diff --git a/lib/cli/install.js b/lib/cli/install.js new file mode 100644 index 000000000..c0017117b --- /dev/null +++ b/lib/cli/install.js @@ -0,0 +1,21 @@ +var options = require('./options'); +var getBook = require('./getBook'); + +var Parse = require('../parse'); +var Plugins = require('../plugins'); + +module.exports = { + name: 'install [book]', + description: 'install all plugins dependencies', + options: [ + options.log + ], + exec: function(args, kwargs) { + var book = getBook(args, kwargs); + + return Parse.parseConfig(book) + .then(function(resultBook) { + return Plugins.installPlugins(resultBook); + }); + } +}; diff --git a/lib/cli/options.js b/lib/cli/options.js new file mode 100644 index 000000000..26581cba7 --- /dev/null +++ b/lib/cli/options.js @@ -0,0 +1,23 @@ +var Logger = require('../utils/logger'); + +var logOptions = { + name: 'log', + description: 'Minimum log level to display', + values: Object.keys(Logger.LEVELS) + .map(function(s) { + return s.toLowerCase(); + }), + defaults: 'info' +}; + +var formatOption = { + name: 'format', + description: 'Format to build to', + values: ['website', 'json', 'ebook'], + defaults: 'website' +}; + +module.exports = { + log: logOptions, + format: formatOption +}; diff --git a/lib/cli/parse.js b/lib/cli/parse.js new file mode 100644 index 000000000..0421b3ee8 --- /dev/null +++ b/lib/cli/parse.js @@ -0,0 +1,68 @@ +var options = require('./options'); +var getBook = require('./getBook'); + +var Parse = require('../parse'); + +function printBook(book) { + var logger = book.getLogger(); + + var config = book.getConfig(); + var configFile = config.getFile(); + + var summary = book.getSummary(); + var summaryFile = summary.getFile(); + + var readme = book.getReadme(); + var readmeFile = readme.getFile(); + + var glossary = book.getGlossary(); + var glossaryFile = glossary.getFile(); + + if (configFile.exists()) { + logger.info.ln('configuration file is', configFile.getPath()); + } + + if (readmeFile.exists()) { + logger.info.ln('Introduction file is', readmeFile.getPath()); + } + + if (glossaryFile.exists()) { + logger.info.ln('Glossary file is', glossaryFile.getPath()); + } + + if (summaryFile.exists()) { + logger.info.ln('Table of Contents file is', summaryFile.getPath()); + } + + //logger.info.ln('Table of Contents:'); +} + + +module.exports = { + name: 'parse [book]', + description: 'parse and print debug information about a book', + options: [ + options.log + ], + exec: function(args, kwargs) { + var book = getBook(args, kwargs); + var logger = book.getLogger(); + + return Parse.parseBook(book) + .then(function(resultBook) { + var rootFolder = book.getRoot(); + var contentFolder = book.getContentRoot(); + + logger.info.ln('Book located in:', rootFolder); + if (contentFolder != rootFolder) { + logger.info.ln('Content located in:', contentFolder); + } + + if (resultBook.isMultilingual()) { + + } else { + printBook(resultBook); + } + }); + } +}; diff --git a/lib/cli/serve.js b/lib/cli/serve.js new file mode 100644 index 000000000..d97cfedff --- /dev/null +++ b/lib/cli/serve.js @@ -0,0 +1,19 @@ +var path = require('path'); + +var options = require('./options'); +var getBook = require('./getBook'); + +var Parse = require('../parse'); +var Output = require('../output'); + +module.exports = { + name: 'serve [book] [output]', + description: 'serve the book as a website for testing', + options: [ + options.log, + options.format + ], + exec: function(args, kwargs) { + + } +}; diff --git a/lib/cli/watch.js b/lib/cli/watch.js index 130b0d477..0d1ab1747 100644 --- a/lib/cli/watch.js +++ b/lib/cli/watch.js @@ -5,7 +5,12 @@ var chokidar = require('chokidar'); var Promise = require('../utils/promise'); var parsers = require('../parsers'); -// Watch a folder and resolve promise once a file is modified +/** + Watch a folder and resolve promise once a file is modified + + @param {String} dir + @return {Promise} +*/ function watch(dir) { var d = Promise.defer(); dir = path.resolve(dir); diff --git a/lib/index.js b/lib/index.js index 5a3c00e8f..3a1802ac1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,9 @@ var Book = require('./models/book'); var Parse = require('./parse'); +var cli = require('./cli'); module.exports = { Book: Book, - Parse: Parse + Parse: Parse, + commands: cli.commands }; diff --git a/lib/models/config.js b/lib/models/config.js index 720ac57e4..547007a8b 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -9,8 +9,8 @@ var Config = Immutable.Record({ values: configDefault }, 'Config'); -Config.prototype.getPath = function() { - return this.get('path'); +Config.prototype.getFile = function() { + return this.get('file'); }; Config.prototype.getValues = function() { diff --git a/lib/output/index.js b/lib/output/index.js index 9112666c7..9b8ec1756 100644 --- a/lib/output/index.js +++ b/lib/output/index.js @@ -1,6 +1,24 @@ +var Immutable = require('immutable'); + +var generators = Immutable.List([ + require('./json'), + require('./website'), + require('./ebook') +]); + +/** + Return a specific generator by its name + + @param {String} + @return {Generator} +*/ +function getGenerator(name) { + return generators.find(function(generator) { + return generator.name == name; + }); +} + module.exports = { generate: require('./generateBook'), - JSONGenerator: require('./json'), - WebsiteGenerator: require('./website'), - EbookGenerator: require('./ebook') + getGenerator: getGenerator }; diff --git a/lib/plugins/installPlugins.js b/lib/plugins/installPlugins.js index 77ecfced9..05a5316f9 100644 --- a/lib/plugins/installPlugins.js +++ b/lib/plugins/installPlugins.js @@ -3,9 +3,12 @@ var npmi = require('npmi'); var semver = require('semver'); var Immutable = require('immutable'); +var pkg = require('../../package.json'); +var DEFAULT_PLUGINS = require('../constants/defaultPlugins'); var Promise = require('../utils/promise'); var Plugin = require('../models/plugin'); var gitbook = require('../gitbook'); +var listForBook = require('./listForBook'); var npmIsReady; @@ -42,7 +45,7 @@ function resolveVersion(plugin) { return Promise.nfcall(npm.commands.view, [npmId + '@' + requiredVersion, 'engines'], true); }) .then(function(versions) { - versions = Immutable.Map(versions).entries().toList(); + versions = Immutable.Map(versions).entrySeq(); var result = versions .map(function(entry) { @@ -62,7 +65,7 @@ function resolveVersion(plugin) { if (!result) { return undefined; } else { - return result.get('version'); + return result.version; } }); } @@ -82,7 +85,7 @@ function installPlugin(book, plugin) { var name = plugin.getName(); var requirement = plugin.getVersion(); - logger.info.ln('installing plugin', name); + logger.info.ln('installing plugin "' + name + '"'); // Find a version to install return resolveVersion(plugin) @@ -91,7 +94,7 @@ function installPlugin(book, plugin) { throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"'); } - logger.info.ln('install plugin "' + plugin +'" from NPM with version', requirement); + logger.info.ln('install plugin "' + name +'" from NPM with version', requirement); return Promise.nfcall(npmi, { 'name': plugin.getNpmID(), 'version': version, @@ -104,7 +107,7 @@ function installPlugin(book, plugin) { }); }) .then(function() { - logger.info.ok('plugin "' + plugin + '" installed with success'); + logger.info.ok('plugin "' + name + '" installed with success'); }); } @@ -113,10 +116,28 @@ function installPlugin(book, plugin) { Install plugin requirements for a book @param {Book} - @param {OrderedMap} @return {Promise} */ -function installPlugins(book, plugins) { +function installPlugins(book) { + var logger = book.getLogger(); + var plugins = listForBook(book); + + // Remove default plugins + // (only if version is same as installed) + plugins = plugins.filterNot(function(plugin) { + return ( + DEFAULT_PLUGINS.includes(plugin.getName()) && + plugin.getVersion() === pkg.dependencies[plugin.getNpmID()] + ); + }); + + if (plugins.size == 0) { + logger.info.ln('nothing to install!'); + return Promise(); + } + + logger.info.ln('installing', plugins.size, 'plugins'); + return Promise.forEach(plugins, function(plugin) { return installPlugin(book, plugin); }); diff --git a/lib/plugins/listAll.js b/lib/plugins/listAll.js index 46eaea0bd..8a0fc0731 100644 --- a/lib/plugins/listAll.js +++ b/lib/plugins/listAll.js @@ -2,6 +2,7 @@ var is = require('is'); var Immutable = require('immutable'); var Plugin = require('../models/plugin'); +var pkg = require('../../package.json'); var DEFAULT_PLUGINS = require('../constants/defaultPlugins'); /** @@ -41,7 +42,8 @@ function listAll(plugins) { if (plugins.has(pluginName)) return; plugins = plugins.set(pluginName, new Plugin({ - name: pluginName + name: pluginName, + version: pkg.dependencies[Plugin.nameToNpmID(pluginName)] })); }); From 357aa92e31df8f4809ebb01438878ec3ace96ae6 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 15:50:11 +0200 Subject: [PATCH 037/118] Add default filters to theme templating engine --- lib/output/website/createTemplateEngine.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index e66150971..7f91ba2bf 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -6,6 +6,7 @@ var TEMPLATES_FOLDER = require('../../constants/templatesFolder'); var Templating = require('../../templating'); var TemplateEngine = require('../../models/templateEngine'); +var defaultFilters = require('../../constants/defaultFilters'); var listSearchPaths = require('./listSearchPaths'); /** @@ -33,7 +34,7 @@ function createTemplateEngine(output, currentFile) { return TemplateEngine.create({ loader: loader, - filters: { + filters: defaultFilters.merge({ t: function(s) { return s; }, @@ -52,7 +53,7 @@ function createTemplateEngine(output, currentFile) { getArticleByPath: function(s) { return undefined; } - }, + }), extensions: { 'DoExtension': new DoExtension() From 7a46d3b4977bcd1e115324880a409e88032899a7 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 15:51:52 +0200 Subject: [PATCH 038/118] Add command "serve" --- lib/cli/build.js | 13 +-- lib/cli/getOutputFolder.js | 17 ++++ lib/cli/helper.js | 140 -------------------------- lib/cli/indexe.js | 199 ------------------------------------- lib/cli/serve.js | 80 ++++++++++++++- lib/cli/server.js | 4 +- lib/models/plugin.js | 2 +- lib/plugins/loadForBook.js | 2 +- lib/plugins/loadPlugin.js | 7 +- 9 files changed, 109 insertions(+), 355 deletions(-) create mode 100644 lib/cli/getOutputFolder.js delete mode 100644 lib/cli/helper.js delete mode 100644 lib/cli/indexe.js diff --git a/lib/cli/build.js b/lib/cli/build.js index 49931bdf2..978bc878c 100644 --- a/lib/cli/build.js +++ b/lib/cli/build.js @@ -1,11 +1,13 @@ var path = require('path'); -var options = require('./options'); -var getBook = require('./getBook'); - var Parse = require('../parse'); var Output = require('../output'); +var options = require('./options'); +var getBook = require('./getBook'); +var getOutputFolder = require('./getOutputFolder'); + + module.exports = { name: 'build [book] [output]', description: 'build a book', @@ -15,13 +17,12 @@ module.exports = { ], exec: function(args, kwargs) { var book = getBook(args, kwargs); + var outputFolder = getOutputFolder(args); + var Generator = Output.getGenerator(kwargs.format); return Parse.parseBook(book) .then(function(resultBook) { - var defaultOutputRoot = path.join(resultBook.getRoot(), '_book'); - var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot; - return Output.generate(Generator, resultBook, { root: outputFolder }); diff --git a/lib/cli/getOutputFolder.js b/lib/cli/getOutputFolder.js new file mode 100644 index 000000000..272dff901 --- /dev/null +++ b/lib/cli/getOutputFolder.js @@ -0,0 +1,17 @@ +var path = require('path'); + +/** + Return path to output folder + + @param {Array} args + @return {String} +*/ +function getOutputFolder(args) { + var bookRoot = path.resolve(args[0] || process.cwd()); + var defaultOutputRoot = path.join(bookRoot, '_book'); + var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot; + + return outputFolder; +} + +module.exports = getOutputFolder; diff --git a/lib/cli/helper.js b/lib/cli/helper.js deleted file mode 100644 index 02cede65e..000000000 --- a/lib/cli/helper.js +++ /dev/null @@ -1,140 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); - -var Book = require('../book'); -var NodeFS = require('../fs/node'); -var Logger = require('../utils/logger'); -var Promise = require('../utils/promise'); -var fs = require('../utils/fs'); -var JSONOutput = require('../output/json'); -var WebsiteOutput = require('../output/website'); -var EBookOutput = require('../output/ebook'); - -var nodeFS = new NodeFS(); - -var LOG_OPTION = { - name: 'log', - description: 'Minimum log level to display', - values: _.chain(Logger.LEVELS) - .keys() - .map(function(s) { - return s.toLowerCase(); - }) - .value(), - defaults: 'info' -}; - -var FORMAT_OPTION = { - name: 'format', - description: 'Format to build to', - values: ['website', 'json', 'ebook'], - defaults: 'website' -}; - -var FORMATS = { - json: JSONOutput, - website: WebsiteOutput, - ebook: EBookOutput -}; - -// Commands which is processing a book -// the root of the book is the first argument (or current directory) -function bookCmd(fn) { - return function(args, kwargs) { - var input = path.resolve(args[0] || process.cwd()); - var book = new Book({ - fs: nodeFS, - root: input, - logLevel: kwargs.log - }); - - return fn(book, args.slice(1), kwargs); - }; -} - -// Commands which is working on a Output instance -function outputCmd(fn) { - return bookCmd(function(book, args, kwargs) { - var Out = FORMATS[kwargs.format]; - var outputFolder = undefined; - - // Set output folder - if (args[0]) { - outputFolder = path.resolve(process.cwd(), args[0]); - } - - return fn(new Out(book, { - root: outputFolder - }), args); - }); -} - -// Command to generate an ebook -function ebookCmd(format) { - return { - name: format + ' [book] [output] [file]', - description: 'generates ebook '+format, - options: [ - LOG_OPTION - ], - exec: bookCmd(function(book, args, kwargs) { - return fs.tmpDir() - .then(function(dir) { - var ext = '.'+format; - var outputFile = path.resolve(process.cwd(), args[0] || ('book' + ext)); - var output = new EBookOutput(book, { - root: dir, - format: format - }); - - return output.book.parse() - .then(function() { - return output.generate(); - }) - - // Copy the ebook files - .then(function() { - if (output.book.isMultilingual()) { - return Promise.serie(output.book.langs.list(), function(lang) { - var _outputFile = path.join( - path.dirname(outputFile), - path.basename(outputFile, ext) + '_' + lang.id + ext - ); - - return fs.copy( - path.resolve(dir, lang.id, 'index' + ext), - _outputFile - ); - }) - .thenResolve(output.book.langs.count()); - } else { - return fs.copy( - path.resolve(dir, 'index' + ext), - outputFile - ).thenResolve(1); - } - }) - .then(function(n) { - output.book.log.info.ok(n+' file(s) generated'); - - output.book.log.info('cleaning up... '); - return output.book.log.info.promise(fs.rmDir(dir)); - }); - }); - }) - }; -} - -module.exports = { - nodeFS: nodeFS, - bookCmd: bookCmd, - outputCmd: outputCmd, - ebookCmd: ebookCmd, - - options: { - log: LOG_OPTION, - format: FORMAT_OPTION - }, - - FORMATS: FORMATS -}; diff --git a/lib/cli/indexe.js b/lib/cli/indexe.js deleted file mode 100644 index eea707f57..000000000 --- a/lib/cli/indexe.js +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable no-console */ - -var _ = require('lodash'); -var path = require('path'); -var tinylr = require('tiny-lr'); - -var Promise = require('../utils/promise'); -var PluginsManager = require('../plugins'); -var Book = require('../book'); - -var helper = require('./helper'); -var Server = require('./server'); -var watch = require('./watch'); - -module.exports = { - commands: [ - { - name: 'init [book]', - description: 'setup and create files for chapters', - options: [ - helper.options.log - ], - exec: function(args) { - var input = path.resolve(args[0] || process.cwd()); - return Book.init(helper.nodeFS, input); - } - }, - - { - name: 'parse [book]', - description: 'parse and returns debug information for a book', - options: [ - helper.options.log - ], - exec: helper.bookCmd(function(book) { - return book.parse() - .then(function() { - book.log.info.ln('Book located in:', book.root); - book.log.info.ln(''); - - if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path); - - if (book.isMultilingual()) { - book.log.info.ln('Multilingual book detected:', book.langs.path); - } else { - book.log.info.ln('Readme:', book.readme.path); - book.log.info.ln('Summary:', book.summary.path); - if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path); - - book.log.info.ln('Pages:'); - _.each(book.pages, function(page) { - book.log.info.ln('\t-', page.path); - }); - } - }); - }) - }, - - { - name: 'install [book]', - description: 'install all plugins dependencies', - options: [ - helper.options.log - ], - exec: helper.bookCmd(function(book, args) { - var plugins = new PluginsManager(book); - - return book.config.load() - .then(function() { - return plugins.install(); - }); - }) - }, - - { - name: 'build [book] [output]', - description: 'build a book', - options: [ - helper.options.log, - helper.options.format - ], - exec: helper.outputCmd(function(output, args, kwargs) { - return output.book.parse() - .then(function() { - return output.generate(); - }); - }) - }, - - helper.ebookCmd('pdf'), - helper.ebookCmd('epub'), - helper.ebookCmd('mobi'), - - { - name: 'serve [book]', - description: 'Build then serve a book from a directory', - options: [ - { - name: 'port', - description: 'Port for server to listen on', - defaults: 4000 - }, - { - name: 'lrport', - description: 'Port for livereload server to listen on', - defaults: 35729 - }, - { - name: 'watch', - description: 'Enable/disable file watcher', - defaults: true - }, - helper.options.format, - helper.options.log - ], - exec: function(args, kwargs) { - var input = path.resolve(args[0] || process.cwd()); - var server = new Server(); - - // Init livereload server - var lrServer = tinylr({}); - var port = kwargs.port; - var lrPath; - - var generate = function() { - - // Stop server if running - if (server.isRunning()) console.log('Stopping server'); - return server.stop() - - // Generate the book - .then(function() { - var book = new Book({ - fs: helper.nodeFS, - root: input, - logLevel: kwargs.log - }); - - return book.parse() - .then(function() { - // Add livereload plugin - book.config.set('plugins', - book.config.get('plugins') - .concat([ - { name: 'livereload' } - ]) - ); - - var Out = helper.FORMATS[kwargs.format]; - var output = new Out(book); - - return output.generate() - .thenResolve(output); - }); - }) - - // Start server and watch changes - .then(function(output) { - console.log(); - console.log('Starting server ...'); - return server.start(output.root(), port) - .then(function() { - console.log('Serving book on http://localhost:'+port); - - if (lrPath) { - // trigger livereload - lrServer.changed({ - body: { - files: [lrPath] - } - }); - } - - if (!kwargs.watch) return; - - return watch(output.book.root) - .then(function(filepath) { - // set livereload path - lrPath = filepath; - console.log('Restart after change in file', filepath); - console.log(''); - return generate(); - }); - }); - }); - }; - - return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport) - .then(function() { - console.log('Live reload server started on port:', kwargs.lrport); - console.log('Press CTRL+C to quit ...'); - console.log(''); - return generate(); - }); - } - } - - ] -}; diff --git a/lib/cli/serve.js b/lib/cli/serve.js index d97cfedff..628f59196 100644 --- a/lib/cli/serve.js +++ b/lib/cli/serve.js @@ -1,19 +1,93 @@ -var path = require('path'); +/* eslint-disable no-console */ -var options = require('./options'); -var getBook = require('./getBook'); +var tinylr = require('tiny-lr'); var Parse = require('../parse'); var Output = require('../output'); +var options = require('./options'); +var getBook = require('./getBook'); +var getOutputFolder = require('./getOutputFolder'); +var Server = require('./server'); +var watch = require('./watch'); + +var server, lrServer, lrPath; + +function generateBook(args, kwargs) { + var port = kwargs.port; + var outputFolder = getOutputFolder(args); + var book = getBook(args, kwargs); + var Generator = Output.getGenerator(kwargs.format); + + // Stop server if running + if (server.isRunning()) console.log('Stopping server'); + + return server.stop() + .then(function() { + return Parse.parseBook(book) + .then(function(resultBook) { + return Output.generate(Generator, resultBook, { + root: outputFolder + }); + }); + }) + .then(function() { + console.log(); + console.log('Starting server ...'); + return server.start(outputFolder, port); + }) + .then(function() { + console.log('Serving book on http://localhost:'+port); + + if (lrPath) { + // trigger livereload + lrServer.changed({ + body: { + files: [lrPath] + } + }); + } + }) + .then(function() { + if (!kwargs.watch) return; + + return watch(book.getRoot()) + .then(function(filepath) { + // set livereload path + lrPath = filepath; + console.log('Restart after change in file', filepath); + console.log(''); + return generateBook(args, kwargs); + }); + }); +} + module.exports = { name: 'serve [book] [output]', description: 'serve the book as a website for testing', options: [ + { + name: 'port', + description: 'Port for server to listen on', + defaults: 4000 + }, + { + name: 'lrport', + description: 'Port for livereload server to listen on', + defaults: 35729 + }, + { + name: 'watch', + description: 'Enable/disable file watcher', + defaults: true + }, options.log, options.format ], exec: function(args, kwargs) { + server = new Server(); + lrServer = tinylr({}); + return generateBook(args, kwargs); } }; diff --git a/lib/cli/server.js b/lib/cli/server.js index 8d3d7ce3d..936ff4e92 100644 --- a/lib/cli/server.js +++ b/lib/cli/server.js @@ -6,12 +6,12 @@ var url = require('url'); var Promise = require('../utils/promise'); -var Server = function() { +function Server() { this.running = null; this.dir = null; this.port = 0; this.sockets = []; -}; +} util.inherits(Server, events.EventEmitter); // Return true if the server is running diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 372558b16..909ca0b6a 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -62,7 +62,7 @@ Plugin.prototype.getNpmID = function() { @return {Boolean} */ Plugin.prototype.isLoaded = function() { - return Boolean(this.getPackage().size > 0 && this.getContent().size > 0); + return Boolean(this.getPackage().size > 0); }; /** diff --git a/lib/plugins/loadForBook.js b/lib/plugins/loadForBook.js index 949469667..c1f19d02a 100644 --- a/lib/plugins/loadForBook.js +++ b/lib/plugins/loadForBook.js @@ -33,7 +33,7 @@ function loadForBook(book) { }); // Log state - logger.info.ln(installed.size + ' are installed'); + logger.info.ln(installed.size + ' plugins are installed'); if (requirements.size != installed.size) { logger.info.ln(requirements.size + ' explicitly listed'); } diff --git a/lib/plugins/loadPlugin.js b/lib/plugins/loadPlugin.js index 39e2e3381..a1c41b425 100644 --- a/lib/plugins/loadPlugin.js +++ b/lib/plugins/loadPlugin.js @@ -1,5 +1,6 @@ var path = require('path'); var resolve = require('resolve'); +var Immutable = require('immutable'); var Promise = require('../utils/promise'); var error = require('../utils/error'); @@ -9,7 +10,7 @@ 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; + return err.code == 'MODULE_NOT_FOUND' || err.message.indexOf('Cannot find module') >= 0; } /** @@ -64,8 +65,8 @@ function loadPlugin(book, plugin) { // Update plugin return plugin.merge({ - 'package': packageContent, - 'content': content + 'package': Immutable.fromJS(packageContent), + 'content': Immutable.fromJS(content) }); }) From 8bac111acf121adf03cc2dff09bd0a1ce8cea19b Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 17:02:59 +0200 Subject: [PATCH 039/118] Add base for plugin JS api --- lib/api/decodeConfig.js | 15 +++++++++ lib/api/decodeGlobal.js | 21 ++++++++++++ lib/api/decodePage.js | 10 +++--- lib/api/deprecate.js | 67 ++++++++++++++++++++++++++++++++++++++ lib/api/encodeConfig.js | 30 +++++++++++++++++ lib/api/encodeGlobal.js | 46 ++++++++++++++++++++++++++ lib/api/encodePage.js | 20 ++++++++++++ lib/api/index.js | 5 ++- lib/output/callHook.js | 12 ++++++- lib/output/callPageHook.js | 6 ++-- lib/output/generateBook.js | 4 +-- lib/utils/error.js | 22 +++---------- package.json | 2 +- 13 files changed, 228 insertions(+), 32 deletions(-) create mode 100644 lib/api/decodeConfig.js create mode 100644 lib/api/decodeGlobal.js create mode 100644 lib/api/deprecate.js create mode 100644 lib/api/encodeConfig.js create mode 100644 lib/api/encodeGlobal.js diff --git a/lib/api/decodeConfig.js b/lib/api/decodeConfig.js new file mode 100644 index 000000000..886ba455d --- /dev/null +++ b/lib/api/decodeConfig.js @@ -0,0 +1,15 @@ +var Config = require('../models/config'); + +/** + Decode changes from a JS API to a config object + + @param {Config} config + @param {Object} result: result from API + @return {Config} +*/ +function decodeGlobal(config, result) { + var values = result.values; + return Config.updateValues(config, values); +} + +module.exports = decodeGlobal; diff --git a/lib/api/decodeGlobal.js b/lib/api/decodeGlobal.js new file mode 100644 index 000000000..1b0b1355b --- /dev/null +++ b/lib/api/decodeGlobal.js @@ -0,0 +1,21 @@ +var decodeConfig = require('./decodeConfig'); + +/** + Decode changes from a JS API to a output object + + @param {Output} output + @param {Object} result: result from API + @return {Output} +*/ +function decodeGlobal(output, result) { + var book = output.getBook(); + var config = book.getConfig(); + + // Update config + config = decodeConfig(config, result.config); + book = book.set('config', config); + + return output.set('book', book); +} + +module.exports = decodeGlobal; diff --git a/lib/api/decodePage.js b/lib/api/decodePage.js index b9941c15d..5cce56c5a 100644 --- a/lib/api/decodePage.js +++ b/lib/api/decodePage.js @@ -1,15 +1,15 @@ /** - Decode changes from a JS API to a page boject + Decode changes from a JS API to a page object @param {Output} output - @param {Page} page - @param {Object} result + @param {Page} page: page instance to edit + @param {Object} result: result from API @return {Page} */ function decodePage(output, page, result) { - - + // todo + return page; } module.exports = decodePage; diff --git a/lib/api/deprecate.js b/lib/api/deprecate.js new file mode 100644 index 000000000..b89077745 --- /dev/null +++ b/lib/api/deprecate.js @@ -0,0 +1,67 @@ +var logged = {}; + +/** + Log a deprecated notice + + @param {Book|Output} book + @param {String} key + @param {String} message +*/ +function logNotice(book, key, message) { + if (logged[key]) return; + + logged[key] = true; + + var logger = book.getLogger(); + logger.warn.ln(message); +} + +/** + Deprecate a function + + @param {Book|Output} book + @param {String} key: unique identitifer for the deprecated + @param {Function} fn + @param {String} msg: message to print when called + @return {Function} +*/ +function deprecateMethod(book, key, fn, msg) { + return function() { + logNotice(book, key, msg); + + return fn.apply(this, arguments); + }; +} + +/** + Deprecate a property of an object + + @param {Book|Output} book + @param {String} key: unique identitifer for the deprecated + @param {Object} instance + @param {String} property + @param {String} msg: message to print when called + @return {Function} +*/ +function deprecateField(book, key, instance, property, value, msg) { + var getter = function(){ + logNotice(book, key, msg); + return value; + }; + var setter = function(v) { + logNotice(book, key, msg); + value = v; + return value; + }; + + Object.defineProperty(instance, property, { + get: getter, + set: setter, + enumerable: true + }); +} + +module.exports = { + method: deprecateMethod, + field: deprecateField +}; diff --git a/lib/api/encodeConfig.js b/lib/api/encodeConfig.js new file mode 100644 index 000000000..d83f81abb --- /dev/null +++ b/lib/api/encodeConfig.js @@ -0,0 +1,30 @@ +var objectPath = require('object-path'); +var deprecate = require('./deprecate'); + +/** + Encode a config object into a JS config api + + @param {Output} output + @param {Config} config + @return {Object} +*/ +function encodeConfig(output, config) { + var result = { + values: config.getValues().toJS(), + + get: function(key, defaultValue) { + return objectPath.get(result.values, key, defaultValue); + }, + + set: function(key, value) { + return objectPath.set(result.values, key, value); + } + }; + + deprecate.field(output, 'config.options', result, 'options', + result.values, '"config.options" property is deprecated, use "config.get(key)" instead'); + + return result; +} + +module.exports = encodeConfig; diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js new file mode 100644 index 000000000..b2dd77a38 --- /dev/null +++ b/lib/api/encodeGlobal.js @@ -0,0 +1,46 @@ +var path = require('path'); + +var fs = require('../utils/fs'); + +var deprecate = require('./deprecate'); +var encodeConfig = require('./encodeConfig'); + +/** + Encode a global context into a JS object + It's the context for page's hook, etc + + @param {Output} output + @return {Object} +*/ +function encodeGlobal(output) { + var book = output.getBook(); + var logger = output.getLogger(); + + var outputFolder = output.getOptions().get('root'); + + var result = { + log: logger, + config: encodeConfig(output, book.getConfig()) + }; + + result.output = { + name: 'website', + + toURL: function(s) { + return s; + }, + + writeFile: function(fileName, content) { + var filePath = path.join(outputFolder, fileName); + return fs.writeFile(filePath, content); + } + }; + + result.isLanguageBook = function() { + return false; + }; + + return result; +} + +module.exports = encodeGlobal; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index 9ec55bc65..98d740946 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -1,3 +1,5 @@ +var JSONUtils = require('../json'); +var deprecate = require('./deprecate'); /** Encode a page in a context to a JS API @@ -7,8 +9,26 @@ @return {Object} */ function encodePage(output, page) { + var book = output.getBook(); + var summary = book.getSummary(); + var fs = book.getContentFS(); + var file = page.getFile(); + // JS Page is based on the JSON output + var result = JSONUtils.encodePage(page, summary); + result.type = file.getType(); + result.path = file.getPath(); + result.rawPath = fs.resolve(result.path); + + deprecate.field(output, 'page.section', result, 'sections', [ + { + content: result.content, + type: 'normal' + } + ], '"sections" property is deprecated, use page.content instead'); + + return result; } module.exports = encodePage; diff --git a/lib/api/index.js b/lib/api/index.js index 213ef1ad9..5e675253e 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -1,5 +1,8 @@ module.exports = { encodePage: require('./encodePage'), - decodePage: require('./decodePage') + decodePage: require('./decodePage'), + + encodeGlobal: require('./encodeGlobal'), + decodeGlobal: require('./decodeGlobal') }; diff --git a/lib/output/callHook.js b/lib/output/callHook.js index 8d34bdc20..1ff1a4165 100644 --- a/lib/output/callHook.js +++ b/lib/output/callHook.js @@ -1,4 +1,5 @@ var Promise = require('../utils/promise'); +var Api = require('../api'); function defaultGetArgument() { return undefined; @@ -26,7 +27,13 @@ function callHook(name, getArgument, handleResult, output) { logger.debug.ln('calling hook "' + name + '"'); + // Create the JS context for plugins + var context = Api.encodeGlobal(output); + + // Get the arguments return Promise(getArgument(output)) + + // Call the hooks in serie .then(function(arg) { return Promise.reduce(plugins, function(prev, plugin) { var hook = plugin.getHook(name); @@ -34,10 +41,13 @@ function callHook(name, getArgument, handleResult, output) { return prev; } - return hook(prev); + return hook.call(context, prev); }, arg); }) + + // Handle final result .then(function(result) { + output = Api.decodeGlobal(output, context); return handleResult(output, result); }); } diff --git a/lib/output/callPageHook.js b/lib/output/callPageHook.js index 6f6bcbbab..0582da95c 100644 --- a/lib/output/callPageHook.js +++ b/lib/output/callPageHook.js @@ -12,9 +12,7 @@ var Promise = require('../utils/promise'); @return {Promise} */ function callPageHook(name, output, page) { - return Promise(page); - - /*return callHook( + return callHook( name, function(out) { @@ -26,7 +24,7 @@ function callPageHook(name, output, page) { }, output - );*/ + ); } module.exports = callPageHook; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index 1fe75fdfb..ab07e4139 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -105,7 +105,7 @@ function generateBook(generator, book, options) { return generator.onFinish(output); }) - /*.then(callHook.bind(null, + .then(callHook.bind(null, 'finish', function(output) { return {}; @@ -114,7 +114,7 @@ function generateBook(generator, book, options) { return output; } ) - )*/ + ) .then(function(output) { var logger = output.getLogger(); diff --git a/lib/utils/error.js b/lib/utils/error.js index d34c17ef4..768677931 100644 --- a/lib/utils/error.js +++ b/lib/utils/error.js @@ -1,15 +1,12 @@ -var _ = require('lodash'); +var is = require('is'); + var TypedError = require('error/typed'); var WrappedError = require('error/wrapped'); -var deprecated = require('deprecated'); -var Logger = require('./logger'); - -var log = new Logger(); // Enforce as an Error object, and cleanup message function enforce(err) { - if (_.isString(err)) err = new Error(err); + if (is.string(err)) err = new Error(err); err.message = err.message.replace(/^Error: /, ''); return err; @@ -84,14 +81,6 @@ var EbookError = WrappedError({ stdout: '' }); -// Deprecate methods/fields -function deprecateMethod(fn, msg) { - return deprecated.method(msg, log.warn.ln, fn); -} -function deprecateField(obj, prop, value, msg) { - return deprecated.field(msg, log.warn.ln, obj, prop, value); -} - module.exports = { enforce: enforce, @@ -106,8 +95,5 @@ module.exports = { TemplateError: TemplateError, PluginError: PluginError, ConfigurationError: ConfigurationError, - EbookError: EbookError, - - deprecateMethod: deprecateMethod, - deprecateField: deprecateField + EbookError: EbookError }; diff --git a/package.json b/package.json index 71674d44c..fa9714694 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "cp": "0.2.0", "cpr": "1.1.1", "crc": "3.4.0", - "deprecated": "0.0.1", "destroy": "1.0.4", "direction": "0.1.5", "dom-serializer": "0.1.0", @@ -46,6 +45,7 @@ "npmi": "1.0.1", "nunjucks": "2.4.1", "nunjucks-do": "1.0.0", + "object-path": "^0.9.2", "q": "1.4.1", "read-installed": "^4.0.3", "request": "2.70.0", From 7b7e4f60bc12e01d397487424635a9426990884c Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 20:49:20 +0200 Subject: [PATCH 040/118] Correctly set log level --- lib/api/encodeGlobal.js | 4 +++ lib/cli/getBook.js | 2 +- lib/models/book.js | 12 +++++++++ lib/utils/logger.js | 56 +++++++++++++++++++++++++++++------------ 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index b2dd77a38..05e9384cb 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -36,6 +36,10 @@ function encodeGlobal(output) { } }; + result.isMultilingual = function() { + return book.isMultilingual(); + }; + result.isLanguageBook = function() { return false; }; diff --git a/lib/cli/getBook.js b/lib/cli/getBook.js index 5c2907875..9038669eb 100644 --- a/lib/cli/getBook.js +++ b/lib/cli/getBook.js @@ -17,7 +17,7 @@ function getBook(args, kwargs) { var fs = createNodeFS(input); var book = Book.createForFS(fs); - return book; + return Book.setLogLevel(book, logLevel); } module.exports = getBook; diff --git a/lib/models/book.js b/lib/models/book.js index eb8011b13..3c5144f9d 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -161,6 +161,18 @@ Book.prototype.isMultilingual = function() { return (this.getLanguages().getList().size > 0); }; +/** + Change log level + + @param {Book} book + @param {String} level + @return {Book} +*/ +Book.setLogLevel = function setLogLevel(book, level) { + book.getLogger().setLevel(level); + return book; +}; + /** Create a book using a filesystem diff --git a/lib/utils/logger.js b/lib/utils/logger.js index 60215af4c..a91370932 100644 --- a/lib/utils/logger.js +++ b/lib/utils/logger.js @@ -17,14 +17,13 @@ var COLORS = { ERROR: color.red }; -function Logger(write, logLevel, prefix) { +function Logger(write, logLevel) { if (!(this instanceof Logger)) return new Logger(write, logLevel); this._write = write || function(msg) { process.stdout.write(msg); }; this.lastChar = '\n'; - // Define log level - this.setLevel(logLevel); + this.setLevel(logLevel || 'info'); _.bindAll(this); @@ -40,35 +39,48 @@ function Logger(write, logLevel, prefix) { }, this); } -// Create a new logger prefixed from this logger -Logger.prototype.prefix = function(prefix) { - return (new Logger(this._write, this.logLevel, prefix)); -}; +/** + Change minimum level -// Change minimum level + @param {String} logLevel +*/ Logger.prototype.setLevel = function(logLevel) { if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()]; this.logLevel = logLevel; }; -// Print a simple string +/** + Print a simple string + + @param {String} +*/ Logger.prototype.write = function(msg) { msg = msg.toString(); this.lastChar = _.last(msg); return this._write(msg); }; -// Format a string using the first argument as a printf-like format. +/** + Format a string using the first argument as a printf-like format. +*/ Logger.prototype.format = function() { return util.format.apply(util, arguments); }; -// Print a line +/** + Print a line + + @param {String} +*/ Logger.prototype.writeLn = function(msg) { return this.write((msg || '')+'\n'); }; -// Log/Print a message if level is allowed +/** + Log/Print a message if level is allowed + + @param {Number} level +*/ Logger.prototype.log = function(level) { if (level < this.logLevel) return; @@ -83,7 +95,9 @@ Logger.prototype.log = function(level) { return this.write(msg); }; -// Log/Print a line if level is allowed +/** + Log/Print a line if level is allowed +*/ Logger.prototype.logLn = function() { if (this.lastChar != '\n') this.write('\n'); @@ -92,7 +106,9 @@ Logger.prototype.logLn = function() { return this.log.apply(this, args); }; -// Log a confirmation [OK] +/** + Log a confirmation [OK] +*/ Logger.prototype.ok = function(level) { var args = Array.prototype.slice.apply(arguments, [1]); var msg = this.format.apply(this, args); @@ -103,12 +119,20 @@ Logger.prototype.ok = function(level) { } }; -// Log a "FAIL" +/** + Log a "FAIL" +*/ Logger.prototype.fail = function(level) { return this.log(level, color.red('ERROR') + '\n'); }; -// Log state of a promise +/** + Log state of a promise + + @param {Number} level + @param {Promise} + @return {Promise} +*/ Logger.prototype.promise = function(level, p) { var that = this; From 27d387e3e1d2389594670babad3afab6359c15fc Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 21:48:56 +0200 Subject: [PATCH 041/118] Load i18n translations --- lib/models/output.js | 30 ++++-- lib/output/generateBook.js | 9 +- lib/output/website/copyPluginAssets.js | 103 +++++++++++++++++++ lib/output/website/createTemplateEngine.js | 12 ++- lib/output/website/index.js | 2 + lib/output/website/onFinish.js | 13 +++ lib/output/website/onInit.js | 110 ++------------------- lib/output/website/prepareI18n.js | 30 ++++++ lib/output/website/state.js | 12 +++ 9 files changed, 207 insertions(+), 114 deletions(-) create mode 100644 lib/output/website/copyPluginAssets.js create mode 100644 lib/output/website/prepareI18n.js create mode 100644 lib/output/website/state.js diff --git a/lib/models/output.js b/lib/models/output.js index 3e8dab8e3..10df67bd7 100644 --- a/lib/models/output.js +++ b/lib/models/output.js @@ -4,10 +4,21 @@ var Book = require('./book'); var Output = Immutable.Record({ book: Book(), + + // Map of plugins to use (String -> Plugin) plugins: Immutable.OrderedMap(), + + // Map pages to generation (String -> Page) pages: Immutable.OrderedMap(), + + // List assets (String) assets: Immutable.List(), - options: Immutable.Map() + + // Option for the generation + options: Immutable.Map(), + + // Internal state for the generation + state: Immutable.Map() }); Output.prototype.getBook = function() { @@ -30,6 +41,10 @@ Output.prototype.getAssets = function() { return this.get('assets'); }; +Output.prototype.getState = function() { + return this.get('state'); +}; + /** Return logegr for this output (same as book) @@ -40,17 +55,14 @@ Output.prototype.getLogger = function() { }; /** - Create an Output instance from a book and a set of options + Update state of output - @param {Book} book - @param {Object} options + @param {Output} output + @param {Map} newState @return {Output} */ -Output.createForBook = function(book, options) { - return new Output({ - book: book, - options: options - }); +Output.updateState = function(output, newState) { + return output.set('state', newState); }; module.exports = Output; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index ab07e4139..ef7d5f16e 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -1,3 +1,5 @@ +var Immutable = require('immutable'); + var Output = require('../models/output'); var Config = require('../models/config'); var Promise = require('../utils/promise'); @@ -33,10 +35,15 @@ var generatePages = require('./generatePages'); */ function generateBook(generator, book, options) { options = generator.Options(options); + var state = generator.State? generator.State({}) : Immutable.Map(); var start = Date.now(); return Promise( - Output.createForBook(book, options) + new Output({ + book: book, + options: options, + state: state + }) ) .then(preparePlugins) .then(preparePages) diff --git a/lib/output/website/copyPluginAssets.js b/lib/output/website/copyPluginAssets.js new file mode 100644 index 000000000..0b795826a --- /dev/null +++ b/lib/output/website/copyPluginAssets.js @@ -0,0 +1,103 @@ +var path = require('path'); + +var ASSET_FOLDER = require('../../constants/pluginAssetsFolder'); +var Promise = require('../../utils/promise'); +var fs = require('../../utils/fs'); + +/** + Copy all assets from plugins. + Assets are files stored in "_assets" + nd resources declared in the plugin itself. + + @param {Output} + @return {Promise} +*/ +function copyPluginAssets(output) { + var plugins = output.getPlugins(); + + return Promise.forEach(plugins, function(plugin) { + return copyAssets(output, plugin) + .then(function() { + return copyResources(output, plugin); + }); + }) + .thenResolve(output); +} + +/** + Copy assets from a plugin + + @param {Plugin} + @return {Promise} +*/ +function copyAssets(output, plugin) { + var logger = output.getLogger(); + var pluginRoot = plugin.getPath(); + var options = output.getOptions(); + + var outputRoot = options.get('root'); + var assetOutputFolder = path.join(outputRoot, 'gitbook'); + var prefix = options.get('prefix'); + + var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix); + + if (!fs.existsSync(assetFolder)) { + return Promise(); + } + + logger.debug.ln('copy assets from theme', assetFolder); + return fs.copyDir( + assetFolder, + assetOutputFolder, + { + deleteFirst: false, + overwrite: true, + confirm: true + } + ); +} + +/** + Copy resources from a plugin + + @param {Plugin} + @return {Promise} +*/ +function copyResources(output, plugin) { + var logger = output.getLogger(); + + var options = output.getOptions(); + var prefix = options.get('prefix'); + var outputRoot = options.get('root'); + + var pluginRoot = plugin.getPath(); + var resources = plugin.getResources(prefix); + + var assetsFolder = resources.get('assets'); + var assetOutputFolder = path.join(outputRoot, 'gitbook', plugin.getNpmID()); + + if (!assetsFolder) { + return Promise(); + } + + // Resolve assets folder + assetsFolder = path.resolve(pluginRoot, assetsFolder); + if (!fs.existsSync(assetsFolder)) { + logger.warn.ln('assets folder for plugin "' + plugin.getName() + '" doesn\'t exist'); + return Promise(); + } + + logger.debug.ln('copy resources from plugin', assetsFolder); + + return fs.copyDir( + assetsFolder, + assetOutputFolder, + { + deleteFirst: false, + overwrite: true, + confirm: true + } + ); +} + +module.exports = copyPluginAssets; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 7f91ba2bf..06e919255 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -24,19 +24,27 @@ function templateFolder(dir) { @return {TemplateEngine} */ function createTemplateEngine(output, currentFile) { - var searchPaths = listSearchPaths(output); + var book = output.getBook(); + var state = output.getState(); + var i18n = state.getI18n(); + var config = book.getConfig(); // Search paths for templates + var searchPaths = listSearchPaths(output); var tplSearchPaths = searchPaths.map(templateFolder); + // Create loader var loader = new Templating.ThemesLoader(tplSearchPaths); + // Get languages + var language = config.get('language'); + return TemplateEngine.create({ loader: loader, filters: defaultFilters.merge({ t: function(s) { - return s; + return i18n.t(language, s); }, resolveFile: function(s) { return s; diff --git a/lib/output/website/index.js b/lib/output/website/index.js index 9c5614e57..0c9fed8b4 100644 --- a/lib/output/website/index.js +++ b/lib/output/website/index.js @@ -1,8 +1,10 @@ module.exports = { name: 'website', + State: require('./state'), Options: require('./options'), onInit: require('./onInit'), + onFinish: require('./onFinish'), onPage: require('./onPage'), onAsset: require('./onAsset') }; diff --git a/lib/output/website/onFinish.js b/lib/output/website/onFinish.js index e69de29bb..691a7759e 100644 --- a/lib/output/website/onFinish.js +++ b/lib/output/website/onFinish.js @@ -0,0 +1,13 @@ +var Promise = require('../../utils/promise'); + +/** + Initialize the generator + + @param {Output} + @return {Output} +*/ +function onFinish(output) { + return Promise(output); +} + +module.exports = onFinish; diff --git a/lib/output/website/onInit.js b/lib/output/website/onInit.js index d348984e0..979a90d4f 100644 --- a/lib/output/website/onInit.js +++ b/lib/output/website/onInit.js @@ -1,112 +1,18 @@ -var path = require('path'); - -var ASSET_FOLDER = require('../../constants/pluginAssetsFolder'); var Promise = require('../../utils/promise'); -var fs = require('../../utils/fs'); + +var copyPluginAssets = require('./copyPluginAssets'); +var prepareI18n = require('./prepareI18n'); /** Initialize the generator + + @param {Output} + @return {Output} */ function onInit(output) { return Promise(output) - .then(copyAllPluginAssets) - .thenResolve(output); -} - - -/** - Copy all assets from plugins. - Assets are files stored in "_assets" - nd resources declared in the plugin itself. - - @param {Output} - @return {Promise} -*/ -function copyAllPluginAssets(output) { - var plugins = output.getPlugins(); - - return Promise.forEach(plugins, function(plugin) { - return copyAssets(output, plugin) - .then(function() { - return copyResources(output, plugin); - }); - }); -} - -/** - Copy assets from a plugin - - @param {Plugin} - @return {Promise} -*/ -function copyAssets(output, plugin) { - var logger = output.getLogger(); - var pluginRoot = plugin.getPath(); - var options = output.getOptions(); - - var outputRoot = options.get('root'); - var assetOutputFolder = path.join(outputRoot, 'gitbook'); - var prefix = options.get('prefix'); - - var assetFolder = path.join(pluginRoot, ASSET_FOLDER, prefix); - - if (!fs.existsSync(assetFolder)) { - return Promise(); - } - - logger.debug.ln('copy assets from theme', assetFolder); - return fs.copyDir( - assetFolder, - assetOutputFolder, - { - deleteFirst: false, - overwrite: true, - confirm: true - } - ); -} - -/** - Copy resources from a plugin - - @param {Plugin} - @return {Promise} -*/ -function copyResources(output, plugin) { - var logger = output.getLogger(); - - var options = output.getOptions(); - var prefix = options.get('prefix'); - var outputRoot = options.get('root'); - - var pluginRoot = plugin.getPath(); - var resources = plugin.getResources(prefix); - - var assetsFolder = resources.get('assets'); - var assetOutputFolder = path.join(outputRoot, 'gitbook', plugin.getNpmID()); - - if (!assetsFolder) { - return Promise(); - } - - // Resolve assets folder - assetsFolder = path.resolve(pluginRoot, assetsFolder); - if (!fs.existsSync(assetsFolder)) { - logger.warn.ln('assets folder for plugin "' + plugin.getName() + '" doesn\'t exist'); - return Promise(); - } - - logger.debug.ln('copy resources from plugin', assetsFolder); - - return fs.copyDir( - assetsFolder, - assetOutputFolder, - { - deleteFirst: false, - overwrite: true, - confirm: true - } - ); + .then(prepareI18n) + .then(copyPluginAssets); } module.exports = onInit; diff --git a/lib/output/website/prepareI18n.js b/lib/output/website/prepareI18n.js new file mode 100644 index 000000000..b57d17861 --- /dev/null +++ b/lib/output/website/prepareI18n.js @@ -0,0 +1,30 @@ +var path = require('path'); + +var fs = require('../../utils/fs'); +var Promise = require('../../utils/promise'); +var listSearchPaths = require('./listSearchPaths'); + +/** + Prepare i18n, load translations from plugins and book + + @param {Output} + @return {Promise} +*/ +function prepareI18n(output) { + var state = output.getState(); + var i18n = state.getI18n(); + var searchPaths = listSearchPaths(output); + + searchPaths + .reverse() + .forEach(function(searchPath) { + var i18nRoot = path.resolve(searchPath, '_i18n'); + + if (!fs.existsSync(i18nRoot)) return; + i18n.load(i18nRoot); + }); + + return Promise(output); +} + +module.exports = prepareI18n; diff --git a/lib/output/website/state.js b/lib/output/website/state.js new file mode 100644 index 000000000..99e7f042e --- /dev/null +++ b/lib/output/website/state.js @@ -0,0 +1,12 @@ +var I18n = require('i18n-t'); +var Immutable = require('immutable'); + +var GeneratorState = Immutable.Record({ + i18n: I18n() +}); + +GeneratorState.prototype.getI18n = function() { + return this.get('i18n'); +}; + +module.exports = GeneratorState; From 878e7441f22a5ef2dc533b6979f73c75d402c0c0 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Tue, 26 Apr 2016 23:19:19 +0200 Subject: [PATCH 042/118] Stop running test on node 0.12 for windows --- appveyor.yml | 4 +-- lib/api/decodeGlobal.js | 3 +- lib/api/decodePage.js | 28 ++++++++++++++-- lib/fs/mock.js | 10 ++---- lib/json/index.js | 13 ++++---- lib/output/website/createTemplateEngine.js | 39 ++++++++++++++++++---- lib/output/website/onAsset.js | 2 +- lib/templating/themesLoader.js | 2 -- 8 files changed, 72 insertions(+), 29 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index b67977715..4de816dd4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,8 @@ init: # Test against these versions of Node.js. environment: matrix: - - nodejs_version: "0.12" - - nodejs_version: "4.1" + - nodejs_version: "5" + - nodejs_version: "4" # Install scripts. (runs after repo cloning) install: diff --git a/lib/api/decodeGlobal.js b/lib/api/decodeGlobal.js index 1b0b1355b..118afb29b 100644 --- a/lib/api/decodeGlobal.js +++ b/lib/api/decodeGlobal.js @@ -1,7 +1,8 @@ var decodeConfig = require('./decodeConfig'); /** - Decode changes from a JS API to a output object + Decode changes from a JS API to a output object. + Only the configuration can be edited by plugin's hooks @param {Output} output @param {Object} result: result from API diff --git a/lib/api/decodePage.js b/lib/api/decodePage.js index 5cce56c5a..e0b7d1e6a 100644 --- a/lib/api/decodePage.js +++ b/lib/api/decodePage.js @@ -1,6 +1,7 @@ /** - Decode changes from a JS API to a page object + Decode changes from a JS API to a page object. + Only the content can be edited by plugin's hooks. @param {Output} output @param {Page} page: page instance to edit @@ -8,7 +9,30 @@ @return {Page} */ function decodePage(output, page, result) { - // todo + var originalContent = page.getContent(); + + // No returned value + // Existing content will be used + if (!result) { + return page; + } + + // GitBook 3 + // Use returned page.content if different from original content + if (result.content != originalContent) { + return page.set('content', result.content); + } + + // GitBook 2 compatibility + // Finally, use page.sections + if (result.sections) { + return page.set('content', + result.sections.map(function(section) { + return section.content; + }).join('\n') + ); + } + return page; } diff --git a/lib/fs/mock.js b/lib/fs/mock.js index 6c2670bb8..2149e1d6b 100644 --- a/lib/fs/mock.js +++ b/lib/fs/mock.js @@ -7,20 +7,14 @@ var FS = require('../models/fs'); var error = require('../utils/error'); /** - Create a fake filesystem for testing books + Create a fake filesystem for unit testing GitBook. - @param {Map} + @param {Map} */ function createMockFS(files) { files = Immutable.fromJS(files); var mtime = new Date(); - /** - Get a file by resolving its path - - @param {String} - @return {String|Map|null} - */ function getFile(filePath) { var parts = path.normalize(filePath).split('/'); return parts.reduce(function(list, part, i) { diff --git a/lib/json/index.js b/lib/json/index.js index 92b105a34..e58beee3c 100644 --- a/lib/json/index.js +++ b/lib/json/index.js @@ -1,9 +1,10 @@ module.exports = { - encodeBookWithPage: require('./encodeBookWithPage'), - encodeBook: require('./encodeBook'), - encodeFile: require('./encodeFile'), - encodePage: require('./encodePage'), - encodeSummary: require('./encodeSummary'), - encodeReadme: require('./encodeReadme') + encodeBookWithPage: require('./encodeBookWithPage'), + encodeBook: require('./encodeBook'), + encodeFile: require('./encodeFile'), + encodePage: require('./encodePage'), + encodeSummary: require('./encodeSummary'), + encodeSummaryArticle: require('./encodeSummaryArticle'), + encodeReadme: require('./encodeReadme') }; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 06e919255..2c2ac8d80 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -2,18 +2,19 @@ var path = require('path'); var nunjucks = require('nunjucks'); var DoExtension = require('nunjucks-do')(nunjucks); -var TEMPLATES_FOLDER = require('../../constants/templatesFolder'); - -var Templating = require('../../templating'); +var JSONUtils = require('../../json'); +var LocationUtils = require('../../utils/location'); var TemplateEngine = require('../../models/templateEngine'); +var templatesFolder = require('../../constants/templatesFolder'); var defaultFilters = require('../../constants/defaultFilters'); +var Templating = require('../../templating'); var listSearchPaths = require('./listSearchPaths'); /** Directory for a theme with the templates */ function templateFolder(dir) { - return path.join(dir, TEMPLATES_FOLDER); + return path.join(dir, templatesFolder); } /** @@ -28,6 +29,7 @@ function createTemplateEngine(output, currentFile) { var state = output.getState(); var i18n = state.getI18n(); var config = book.getConfig(); + var summary = book.getSummary(); // Search paths for templates var searchPaths = listSearchPaths(output); @@ -43,11 +45,28 @@ function createTemplateEngine(output, currentFile) { loader: loader, filters: defaultFilters.merge({ - t: function(s) { + /** + Translate a sentence + */ + t: function t(s) { return i18n.t(language, s); }, + + /** + Resolve an absolute file path into a + relative path + */ resolveFile: function(s) { - return s; + // Convert /test.png -> test.png + s = LocationUtils.toAbsolute(s, '', ''); + + // Convert to relative + s = LocationUtils.relative( + this.resolve('.') + '/', + this.book.resolve(name) + ); + + return LocationUtils.normalize(s); }, resolveAsset: function(s) { return s; @@ -58,8 +77,14 @@ function createTemplateEngine(output, currentFile) { contentURL: function(s) { return s; }, + + /** + Return an article by its path + */ getArticleByPath: function(s) { - return undefined; + var article = summary.getByPath(s); + if (!article) return undefined; + return JSONUtils.encodeSummaryArticle(article); } }), diff --git a/lib/output/website/onAsset.js b/lib/output/website/onAsset.js index 169db5691..1311c9531 100644 --- a/lib/output/website/onAsset.js +++ b/lib/output/website/onAsset.js @@ -2,7 +2,7 @@ var path = require('path'); var fs = require('../../utils/fs'); /** - Copy an asset to the outptu folder + Copy an asset to the output folder @param {Output} output @param {Page} page diff --git a/lib/templating/themesLoader.js b/lib/templating/themesLoader.js index 2b436a4f2..69c387957 100644 --- a/lib/templating/themesLoader.js +++ b/lib/templating/themesLoader.js @@ -5,9 +5,7 @@ var path = require('path'); var PathUtils = require('../utils/path'); -/** -*/ var ThemesLoader = nunjucks.Loader.extend({ init: function(searchPaths) { this.searchPaths = Immutable.List(searchPaths) From 93c26a839bc8102cf8d12a24ff69381db5a5c864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 09:42:40 +0200 Subject: [PATCH 043/118] Add this.book and this.options as deprecated --- lib/api/encodeGlobal.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 05e9384cb..0e0f82c67 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -44,6 +44,12 @@ function encodeGlobal(output) { return false; }; + deprecate.field(output, 'this.book', result, 'book', + result, '"book" property is deprecated, use "this" directly instead'); + + deprecate.field(output, 'this.options', result, 'options', + result.config.values, '"options" property is deprecated, use config.get(key) instead'); + return result; } From f4de7748f596a060e9a59df0447315b59d7c47de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 09:59:09 +0200 Subject: [PATCH 044/118] Complete generator name in plugins API --- lib/api/encodeConfig.js | 6 ++++++ lib/api/encodeGlobal.js | 44 ++++++++++++++++++++++++++++++++++------- lib/models/output.js | 16 +++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/lib/api/encodeConfig.js b/lib/api/encodeConfig.js index d83f81abb..2a05528f6 100644 --- a/lib/api/encodeConfig.js +++ b/lib/api/encodeConfig.js @@ -24,6 +24,12 @@ function encodeConfig(output, config) { deprecate.field(output, 'config.options', result, 'options', result.values, '"config.options" property is deprecated, use "config.get(key)" instead'); + deprecate.field(output, 'config.options.generator', result.values, 'generator', + output.getGenerator(), '"options.generator" property is deprecated, use "output.name" instead'); + + deprecate.field(output, 'config.options.generator', result.values, 'output', + output.getRoot(), '"options.output" property is deprecated, use "output.root()" instead'); + return result; } diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 0e0f82c67..915350cb5 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -1,6 +1,6 @@ -var path = require('path'); - var fs = require('../utils/fs'); +var Promise = require('../utils/promise'); +var PathUtils = require('../utils/promise'); var deprecate = require('./deprecate'); var encodeConfig = require('./encodeConfig'); @@ -15,8 +15,7 @@ var encodeConfig = require('./encodeConfig'); function encodeGlobal(output) { var book = output.getBook(); var logger = output.getLogger(); - - var outputFolder = output.getOptions().get('root'); + var outputFolder = output.getRoot(); var result = { log: logger, @@ -24,15 +23,46 @@ function encodeGlobal(output) { }; result.output = { - name: 'website', + /** + Name of the generator being used + {String} + */ + name: output.getGenerator(), + /** + Return absolute path to the root folder of output + @return {String} + */ + root: function() { + return outputFolder; + }, + + /** + Convert a filepath into an url + @return {String} + */ toURL: function(s) { return s; }, + /** + Write a file to the output folder, + It creates the required folder + + @param {String} fileName + @param {Buffer} content + @return {Promise} + */ writeFile: function(fileName, content) { - var filePath = path.join(outputFolder, fileName); - return fs.writeFile(filePath, content); + return Promise() + .then(function() { + var filePath = PathUtils.resolveInRoot(outputFolder, fileName); + + return fs.ensure(filePath) + .then(function() { + return fs.writeFile(filePath, content); + }); + }); } }; diff --git a/lib/models/output.js b/lib/models/output.js index 10df67bd7..c11017e12 100644 --- a/lib/models/output.js +++ b/lib/models/output.js @@ -5,6 +5,9 @@ var Book = require('./book'); var Output = Immutable.Record({ book: Book(), + // Name of the generator being used + generator: String(), + // Map of plugins to use (String -> Plugin) plugins: Immutable.OrderedMap(), @@ -25,6 +28,10 @@ Output.prototype.getBook = function() { return this.get('book'); }; +Output.prototype.getGenerator = function() { + return this.get('generator'); +}; + Output.prototype.getPlugins = function() { return this.get('plugins'); }; @@ -45,6 +52,15 @@ Output.prototype.getState = function() { return this.get('state'); }; +/** + Get root folder for output + + @return {String} +*/ +Output.prototype.getRoot = function() { + return this.getOptions().get('root'); +}; + /** Return logegr for this output (same as book) From b05eb8440b59db6d39ae06712ddda5651786e00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 10:38:54 +0200 Subject: [PATCH 045/118] Don;t log deprecation warning for own logic --- lib/api/decodeConfig.js | 4 ++++ lib/api/decodePage.js | 11 ++++++++--- lib/api/deprecate.js | 28 +++++++++++++++++++++++++--- lib/api/encodePage.js | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/api/decodeConfig.js b/lib/api/decodeConfig.js index 886ba455d..351ed0554 100644 --- a/lib/api/decodeConfig.js +++ b/lib/api/decodeConfig.js @@ -9,6 +9,10 @@ var Config = require('../models/config'); */ function decodeGlobal(config, result) { var values = result.values; + + delete values.generator; + delete values.output; + return Config.updateValues(config, values); } diff --git a/lib/api/decodePage.js b/lib/api/decodePage.js index e0b7d1e6a..c85dd1be9 100644 --- a/lib/api/decodePage.js +++ b/lib/api/decodePage.js @@ -1,3 +1,4 @@ +var deprecate = require('./deprecate'); /** Decode changes from a JS API to a page object. @@ -17,22 +18,26 @@ function decodePage(output, page, result) { return page; } + deprecate.disable('page.sections'); + // GitBook 3 // Use returned page.content if different from original content if (result.content != originalContent) { - return page.set('content', result.content); + page = page.set('content', result.content); } // GitBook 2 compatibility // Finally, use page.sections - if (result.sections) { - return page.set('content', + else if (result.sections) { + page = page.set('content', result.sections.map(function(section) { return section.content; }).join('\n') ); } + deprecate.enable('page.sections'); + return page; } diff --git a/lib/api/deprecate.js b/lib/api/deprecate.js index b89077745..73ca8bef5 100644 --- a/lib/api/deprecate.js +++ b/lib/api/deprecate.js @@ -1,4 +1,5 @@ var logged = {}; +var disabled = {}; /** Log a deprecated notice @@ -8,7 +9,7 @@ var logged = {}; @param {String} message */ function logNotice(book, key, message) { - if (logged[key]) return; + if (logged[key] || disabled[key]) return; logged[key] = true; @@ -57,11 +58,32 @@ function deprecateField(book, key, instance, property, value, msg) { Object.defineProperty(instance, property, { get: getter, set: setter, - enumerable: true + enumerable: true, + configurable: true }); } +/** + Enable a deprecation + + @param {String} key: unique identitifer +*/ +function enableDeprecation(key) { + disabled[key] = false; +} + +/** + Disable a deprecation + + @param {String} key: unique identitifer +*/ +function disableDeprecation(key) { + disabled[key] = true; +} + module.exports = { method: deprecateMethod, - field: deprecateField + field: deprecateField, + enable: enableDeprecation, + disable: disableDeprecation }; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index 98d740946..1f3a01b44 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -21,7 +21,7 @@ function encodePage(output, page) { result.path = file.getPath(); result.rawPath = fs.resolve(result.path); - deprecate.field(output, 'page.section', result, 'sections', [ + deprecate.field(output, 'page.sections', result, 'sections', [ { content: result.content, type: 'normal' From 4ca23b84a4ce5c7c03c72b0cb78357aaff1be27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 12:08:00 +0200 Subject: [PATCH 046/118] Fix filters for theme templates --- lib/api/encodeGlobal.js | 2 +- lib/models/book.js | 4 +- lib/output/getModifiers.js | 4 +- lib/output/helper/fileToOutput.js | 32 ++++++++++++++++ lib/output/helper/fileToURL.js | 31 +++++++++++++++ lib/output/helper/index.js | 2 + lib/output/helper/resolveFileToUrl.js | 27 +++++++++++++ lib/output/helper/writeFile.js | 23 +++++++++++ lib/output/json/onPage.js | 17 +++++---- lib/output/resolveFile.js | 21 ----------- lib/output/website/createTemplateEngine.js | 44 +++++++++++++--------- lib/output/website/onPage.js | 9 ++++- lib/output/writer/index.js | 4 -- lib/output/writer/writePage.js | 37 ------------------ lib/utils/location.js | 15 +++++++- 15 files changed, 178 insertions(+), 94 deletions(-) create mode 100644 lib/output/helper/fileToOutput.js create mode 100644 lib/output/helper/fileToURL.js create mode 100644 lib/output/helper/index.js create mode 100644 lib/output/helper/resolveFileToUrl.js create mode 100644 lib/output/helper/writeFile.js delete mode 100644 lib/output/resolveFile.js delete mode 100644 lib/output/writer/index.js delete mode 100644 lib/output/writer/writePage.js diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 915350cb5..9e91c73a0 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -1,6 +1,6 @@ var fs = require('../utils/fs'); var Promise = require('../utils/promise'); -var PathUtils = require('../utils/promise'); +var PathUtils = require('../utils/path'); var deprecate = require('./deprecate'); var encodeConfig = require('./encodeConfig'); diff --git a/lib/models/book.js b/lib/models/book.js index 3c5144f9d..6daa68410 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -33,8 +33,8 @@ var Book = Immutable.Record({ // Parent of this book, if multilingual parent: null, - // List of children, if multilingual - books: Immutable.List() + // List of children, if multilingual (String -> Book) + books: Immutable.OrderedMap() }); Book.prototype.getLogger = function() { diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 21a069225..34a0b9e6a 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -1,5 +1,5 @@ var Modifiers = require('./modifiers'); -var resolveFile = require('./resolveFile'); +var resolveFileToURL = require('./helper/resolveFileToURL'); /** Return default modifier to prepare a page @@ -17,7 +17,7 @@ function getModifiers(output, page) { Modifiers.addHeadingId, Modifiers.resolveLinks.bind(null, file.getPath(), - resolveFile.bind(null, output) + resolveFileToURL.bind(null, output) ), Modifiers.annotateText.bind(null, entries) ]; diff --git a/lib/output/helper/fileToOutput.js b/lib/output/helper/fileToOutput.js new file mode 100644 index 000000000..967316292 --- /dev/null +++ b/lib/output/helper/fileToOutput.js @@ -0,0 +1,32 @@ +var path = require('path'); + +var PathUtils = require('../../utils/path'); +var LocationUtils = require('../../utils/location'); + +var OUTPUT_EXTENSION = '.html'; + +/** + Convert a filePath (absolute) to a filename for output + + @param {Output} output + @param {String} filePath + @return {String} +*/ +function fileToOutput(output, filePath) { + var book = output.getBook(); + var readme = book.getReadme(); + var fileReadme = readme.getFile(); + + if ( + path.basename(filePath, path.extname(filePath)) == 'README' || + (fileReadme.exists() && filePath == fileReadme.getPath()) + ) { + filePath = path.join(path.dirname(filePath), 'index' + OUTPUT_EXTENSION); + } else { + filePath = PathUtils.setExtension(filePath, OUTPUT_EXTENSION); + } + + return LocationUtils.normalize(filePath); +} + +module.exports = fileToOutput; diff --git a/lib/output/helper/fileToURL.js b/lib/output/helper/fileToURL.js new file mode 100644 index 000000000..44ad2d836 --- /dev/null +++ b/lib/output/helper/fileToURL.js @@ -0,0 +1,31 @@ +var path = require('path'); +var LocationUtils = require('../../utils/location'); + +var fileToOutput = require('./fileToOutput'); + +/** + Convert a filePath (absolute) to an url (without hostname). + It returns an absolute path. + + "README.md" -> "/" + "test/hello.md" -> "test/hello.html" + "test/README.md" -> "test/" + + @param {Output} output + @param {String} filePath + @return {String} +*/ +function fileToURL(output, filePath) { + var options = output.getOptions(); + var directoryIndex = options.get('directoryIndex'); + + filePath = fileToOutput(output, filePath); + + if (directoryIndex && path.basename(filePath) == 'index.html') { + filePath = path.dirname(filePath) + '/'; + } + + return LocationUtils.normalize(filePath); +} + +module.exports = fileToURL; diff --git a/lib/output/helper/index.js b/lib/output/helper/index.js new file mode 100644 index 000000000..f8bc109bd --- /dev/null +++ b/lib/output/helper/index.js @@ -0,0 +1,2 @@ + +module.exports = {}; diff --git a/lib/output/helper/resolveFileToUrl.js b/lib/output/helper/resolveFileToUrl.js new file mode 100644 index 000000000..3dba8f76f --- /dev/null +++ b/lib/output/helper/resolveFileToUrl.js @@ -0,0 +1,27 @@ +var LocationUtils = require('../../utils/location'); + +var fileToURL = require('./fileToURL'); + +/** + Resolve an absolute path (extracted from a link) + + @param {Output} output + @param {String} filePath + @return {String} +*/ +function resolveFileToURL(output, filePath) { + // Convert /test.png -> test.png + filePath = LocationUtils.toAbsolute(filePath, '', ''); + + var pages = output.getPages(); + var page = pages.get(filePath); + + // if file is a page, return correct .html url + if (page) { + filePath = fileToURL(output, filePath); + } + + return LocationUtils.normalize(filePath); +} + +module.exports = resolveFileToURL; diff --git a/lib/output/helper/writeFile.js b/lib/output/helper/writeFile.js new file mode 100644 index 000000000..3e2261d2a --- /dev/null +++ b/lib/output/helper/writeFile.js @@ -0,0 +1,23 @@ +var path = require('path'); +var fs = require('../../utils/fs'); + +/** + Write a file to the output folder + + @param {Output} output + @param {String} filePath + @param {Buffer|String} content + @return {Promise} +*/ +function writeFile(output, filePath, content) { + var rootFolder = output.getRoot(); + filePath = path.join(rootFolder, filePath); + + return fs.ensure(filePath) + .then(function() { + return fs.writeFile(filePath, content); + }) + .thenResolve(output); +} + +module.exports = writeFile; diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 5e456620d..54179c624 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -1,6 +1,7 @@ var JSONUtils = require('../../json'); +var PathUtils = require('../../utils/path'); var Modifiers = require('../modifiers'); -var Writer = require('../writer'); +var writeFile = require('../helper/writeFile'); var getModifiers = require('../getModifiers'); /** @@ -10,19 +11,21 @@ var getModifiers = require('../getModifiers'); @param {Page} page */ function onPage(output, page) { + var file = page.getFile(); + return Modifiers.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { // Generate the JSON var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + // File path in the output folder + var filePath = PathUtils.setExtension(file.getPath(), '.json'); + // Write it to the disk - return Writer.writePage( + return writeFile( output, - page, - JSON.stringify(json, null, 4), - { - extension: '.json' - } + filePath, + JSON.stringify(json, null, 4) ); }); } diff --git a/lib/output/resolveFile.js b/lib/output/resolveFile.js deleted file mode 100644 index a3a49c54a..000000000 --- a/lib/output/resolveFile.js +++ /dev/null @@ -1,21 +0,0 @@ -var PathUtils = require('../utils/path'); - -/** - Resolve an absolute path (extracted from a link) - - @param {Output} output - @param {String} filePath - @return {String} -*/ -function resolveFile(output, filePath) { - var pages = output.getPages(); - var page = pages.get(filePath); - - if (!page) { - return filePath; - } - - return PathUtils.setExtension(filePath, '.html'); -} - -module.exports = resolveFile; diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 2c2ac8d80..7d8724804 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -4,12 +4,17 @@ var DoExtension = require('nunjucks-do')(nunjucks); var JSONUtils = require('../../json'); var LocationUtils = require('../../utils/location'); +var fs = require('../../utils/fs'); +var PathUtils = require('../../utils/path'); var TemplateEngine = require('../../models/templateEngine'); var templatesFolder = require('../../constants/templatesFolder'); var defaultFilters = require('../../constants/defaultFilters'); var Templating = require('../../templating'); var listSearchPaths = require('./listSearchPaths'); +var fileToURL = require('../helper/fileToURL'); +var resolveFileToURL = require('../helper/resolveFileToURL'); + /** Directory for a theme with the templates */ @@ -30,6 +35,8 @@ function createTemplateEngine(output, currentFile) { var i18n = state.getI18n(); var config = book.getConfig(); var summary = book.getSummary(); + var pages = output.getPages(); + var outputFolder = output.getRoot(); // Search paths for templates var searchPaths = listSearchPaths(output); @@ -54,28 +61,31 @@ function createTemplateEngine(output, currentFile) { /** Resolve an absolute file path into a - relative path + relative path. + it also resolve pages */ - resolveFile: function(s) { - // Convert /test.png -> test.png - s = LocationUtils.toAbsolute(s, '', ''); + resolveFile: function(filePath) { + filePath = resolveFileToURL(output, filePath); + return LocationUtils.relativeForFile(currentFile, filePath); + }, - // Convert to relative - s = LocationUtils.relative( - this.resolve('.') + '/', - this.book.resolve(name) - ); + resolveAsset: function(filePath) { + filePath = LocationUtils.toAbsolute(filePath, '', ''); + filePath = path.join('gitbook', filePath); - return LocationUtils.normalize(s); + return LocationUtils.relativeForFile(currentFile, filePath); }, - resolveAsset: function(s) { - return s; + + /** + Check if a file exists + */ + fileExists: function(fileName) { + var filePath = PathUtils.resolveInRoot(outputFolder, fileName); + return fs.existsSync(filePath); }, - fileExists: function() { - return false; - }, - contentURL: function(s) { - return s; + + contentURL: function(filePath) { + return fileToURL(output, filePath); }, /** diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index aedb8c0b7..a5598e42e 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -1,9 +1,10 @@ var Templating = require('../../templating'); var JSONUtils = require('../../json'); var Modifiers = require('../modifiers'); -var Writer = require('../writer'); +var writeFile = require('../helper/writeFile'); var getModifiers = require('../getModifiers'); var createTemplateEngine = require('./createTemplateEngine'); +var fileToOutput = require('../helper/fileToOutput'); /** Write a page as a json file @@ -13,6 +14,7 @@ var createTemplateEngine = require('./createTemplateEngine'); */ function onPage(output, page) { var options = output.getOptions(); + var file = page.getFile(); var prefix = options.get('prefix'); var engine = createTemplateEngine(output, page.getPath()); @@ -27,12 +29,15 @@ function onPage(output, page) { } }; + // Output file path + var filePath = fileToOutput(output, file.getPath()); + // Render the theme return Templating.renderFile(engine, prefix + '/page.html', context) // Write it to the disk .then(function(html) { - return Writer.writePage(output, resultPage, html); + return writeFile(output, filePath, html); }); }); } diff --git a/lib/output/writer/index.js b/lib/output/writer/index.js deleted file mode 100644 index 2ef336442..000000000 --- a/lib/output/writer/index.js +++ /dev/null @@ -1,4 +0,0 @@ - -module.exports = { - writePage: require('./writePage') -}; diff --git a/lib/output/writer/writePage.js b/lib/output/writer/writePage.js deleted file mode 100644 index 6bcc1ba08..000000000 --- a/lib/output/writer/writePage.js +++ /dev/null @@ -1,37 +0,0 @@ -var path = require('path'); -var Immutable = require('immutable'); - -var fs = require('../../utils/fs'); -var PathUtils = require('../../utils/path'); - -var WriteOptions = Immutable.Record({ - extension: '.html' -}); - -/** - Write a file to the output folder - - @param {Output} output - @param {Page} page - @param {Buffer|String} content - @return {Promise} -*/ -function writePage(output, page, content, options) { - var file = page.getFile(); - var outputOpts = output.getOptions(); - var rootFolder = outputOpts.get('root'); - - options = WriteOptions(options); - - // Get filename for file to write - var fileName = PathUtils.setExtension(file.getPath(), options.get('extension')); - var filePath = path.join(rootFolder, fileName); - - return fs.ensure(filePath) - .then(function() { - return fs.writeFile(filePath, content); - }) - .thenResolve(output); -} - -module.exports = writePage; diff --git a/lib/utils/location.js b/lib/utils/location.js index f1159f1c6..94d9b2a22 100644 --- a/lib/utils/location.js +++ b/lib/utils/location.js @@ -71,11 +71,24 @@ function relative(dir, file) { return normalize(path.relative(dir, file)); } +/** + Convert an absolute path to a relative path for a specific folder (dir) + ('test/test.md', 'hello.md') -> '../hello.md' + + @param {String} baseFile: current file + @param {String} file: absolute path of file + @return {String} +*/ +function relativeForFile(baseFile, file) { + return relative(path.dirname(baseFile), file); +} + module.exports = { isExternal: isExternal, isRelative: isRelative, isAnchor: isAnchor, normalize: normalize, toAbsolute: toAbsolute, - relative: relative + relative: relative, + relativeForFile: relativeForFile }; From 1906691a35b69c2fbfc38feb35aafa613e7d6810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 12:48:40 +0200 Subject: [PATCH 047/118] Fix order of plugins assets copy --- lib/output/website/copyPluginAssets.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/output/website/copyPluginAssets.js b/lib/output/website/copyPluginAssets.js index 0b795826a..a1a8cf4eb 100644 --- a/lib/output/website/copyPluginAssets.js +++ b/lib/output/website/copyPluginAssets.js @@ -13,7 +13,11 @@ var fs = require('../../utils/fs'); @return {Promise} */ function copyPluginAssets(output) { - var plugins = output.getPlugins(); + var plugins = output.getPlugins() + + // We reverse the order of plugins to copy + // so that first plugins can replace assets from other plugins. + .reverse(); return Promise.forEach(plugins, function(plugin) { return copyAssets(output, plugin) From 262ce03e46bf5f888ba792e4bb44df7ee7ccb751 Mon Sep 17 00:00:00 2001 From: Soreine Date: Wed, 27 Apr 2016 13:11:45 +0200 Subject: [PATCH 048/118] Exposes FS in public interface --- lib/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/index.js b/lib/index.js index 3a1802ac1..a772efe69 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,11 @@ var Book = require('./models/book'); +var FS = require('./models/fs'); var Parse = require('./parse'); var cli = require('./cli'); module.exports = { Book: Book, Parse: Parse, + FS: FS, commands: cli.commands }; From 98e739a12b4ad211f6a213cb63ecf7ebcc6dc2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 13:13:27 +0200 Subject: [PATCH 049/118] Add JS context for template --- lib/gitbook.js | 12 +----------- lib/json/encodeBook.js | 8 +++++++- lib/output/website/onPage.js | 29 +++++++++++++++++++++++++---- package.json | 1 + 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/gitbook.js b/lib/gitbook.js index f847899bd..bafd3b839 100644 --- a/lib/gitbook.js +++ b/lib/gitbook.js @@ -21,18 +21,8 @@ function satisfies(condition) { return semver.satisfies(VERSION_STABLE, condition); } -// Return templating/json context for gitbook itself -function getContext() { - return { - gitbook: { - version: pkg.version, - time: START_TIME - } - }; -} - module.exports = { version: pkg.version, satisfies: satisfies, - getContext: getContext + START_TIME: START_TIME }; diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index 6c1b023b1..3bbbe0fd0 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -1,3 +1,5 @@ +var gitbook = require('../gitbook'); + var encodeSummary = require('./encodeSummary'); var encodeGlossary = require('./encodeGlossary'); var encodeReadme = require('./encodeReadme'); @@ -12,7 +14,11 @@ function encodeBookToJson(book) { return { summary: encodeSummary(book.getSummary()), glossary: encodeGlossary(book.getGlossary()), - readme: encodeReadme(book.getReadme()) + readme: encodeReadme(book.getReadme()), + gitbook: { + version: gitbook.version, + time: gitbook.START_TIME + } }; } diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index a5598e42e..8af362204 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -1,5 +1,9 @@ +var path = require('path'); +var omit = require('omit-keys'); + var Templating = require('../../templating'); var JSONUtils = require('../../json'); +var LocationUtils = require('../../utils/location'); var Modifiers = require('../modifiers'); var writeFile = require('../helper/writeFile'); var getModifiers = require('../getModifiers'); @@ -16,22 +20,39 @@ function onPage(output, page) { var options = output.getOptions(); var file = page.getFile(); var prefix = options.get('prefix'); + var book = output.getBook(); + var config = book.getConfig(); + var engine = createTemplateEngine(output, page.getPath()); + // Output file path + var filePath = fileToOutput(output, file.getPath()); + + // Calcul relative path to the root + var outputDirName = path.dirname(filePath); + var basePath = LocationUtils.normalize(path.relative(outputDirName, './')); + return Modifiers.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { // Generate the context var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + context.config = config.getValues().toJS(); context.template = { getJSContext: function() { - return {}; + return { + page: omit(context.page, 'content'), + config: context.config, + file: context.file, + gitbook: context.gitbook, + basePath: basePath, + book: { + language: null //context.book.language + } + }; } }; - // Output file path - var filePath = fileToOutput(output, file.getPath()); - // Render the theme return Templating.renderFile(engine, prefix + '/page.html', context) diff --git a/package.json b/package.json index fa9714694..90f5f7c9b 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "nunjucks": "2.4.1", "nunjucks-do": "1.0.0", "object-path": "^0.9.2", + "omit-keys": "^0.1.0", "q": "1.4.1", "read-installed": "^4.0.3", "request": "2.70.0", From 7298dec35314065e965b5738e751ad25e4ff2a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 13:47:12 +0200 Subject: [PATCH 050/118] Add plugins.resources for template context --- lib/constants/pluginResources.js | 6 +++-- lib/output/generateBook.js | 3 ++- lib/output/website/onPage.js | 8 +++++- lib/plugins/index.js | 3 ++- lib/plugins/listResources.js | 45 ++++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 lib/plugins/listResources.js diff --git a/lib/constants/pluginResources.js b/lib/constants/pluginResources.js index de5eb18ee..ae283bf4a 100644 --- a/lib/constants/pluginResources.js +++ b/lib/constants/pluginResources.js @@ -1,4 +1,6 @@ -module.exports = [ +var Immutable = require('immutable'); + +module.exports = Immutable.List([ 'js', 'css' -]; +]); diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index ef7d5f16e..5138f883a 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -42,7 +42,8 @@ function generateBook(generator, book, options) { new Output({ book: book, options: options, - state: state + state: state, + generator: generator.name }) ) .then(preparePlugins) diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index 8af362204..ddae986a4 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -2,6 +2,7 @@ var path = require('path'); var omit = require('omit-keys'); var Templating = require('../../templating'); +var Plugins = require('../../plugins'); var JSONUtils = require('../../json'); var LocationUtils = require('../../utils/location'); var Modifiers = require('../modifiers'); @@ -22,6 +23,7 @@ function onPage(output, page) { var prefix = options.get('prefix'); var book = output.getBook(); var config = book.getConfig(); + var plugins = output.getPlugins(); var engine = createTemplateEngine(output, page.getPath()); @@ -36,7 +38,11 @@ function onPage(output, page) { .then(function(resultPage) { // Generate the context var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + context.config = config.getValues().toJS(); + context.plugins = { + resources: Plugins.listResources(plugins, prefix).toJS() + }; context.template = { getJSContext: function() { @@ -47,7 +53,7 @@ function onPage(output, page) { gitbook: context.gitbook, basePath: basePath, book: { - language: null //context.book.language + language: null // context.book.language } }; } diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 622f00c9b..8842342cf 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -2,6 +2,7 @@ module.exports = { loadForBook: require('./loadForBook'), validateConfig: require('./validateConfig'), - installPlugins: require('./installPlugins') + installPlugins: require('./installPlugins'), + listResources: require('./listResources') }; diff --git a/lib/plugins/listResources.js b/lib/plugins/listResources.js new file mode 100644 index 000000000..b4a16cb4d --- /dev/null +++ b/lib/plugins/listResources.js @@ -0,0 +1,45 @@ +var Immutable = require('immutable'); +var path = require('path'); + +var LocationUtils = require('../utils/location'); +var PLUGIN_RESOURCES = require('../constants/pluginResources'); + +/** + Encode plugins as JSON + + @param {OrderedMap} + @param {String} type + @return {Map} +*/ +function listResources(plugins, type) { + return plugins.reduce(function(result, plugin) { + var npmId = plugin.getNpmID(); + var resources = plugin.getResources(type); + + PLUGIN_RESOURCES.forEach(function(resourceType) { + var assets = resources.get(resourceType); + if (!assets) return; + + var list = result.get(resourceType) || Immutable.List(); + + assets = assets.map(function(assetFile) { + if (LocationUtils.isExternal(assetFile)) { + return { + url: assetFile + }; + } else { + return { + path: LocationUtils.normalize(path.join(npmId, assetFile)) + }; + } + }); + + list = list.concat(assets); + result = result.set(resourceType, list); + }); + + return result; + }, Immutable.Map()); +} + +module.exports = listResources; From 8291eb58dffad58ab51abcdf13bc9cfd96d30b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 13:53:32 +0200 Subject: [PATCH 051/118] Complete api method output.toURL --- lib/api/encodeGlobal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 9e91c73a0..20b706e8e 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -4,6 +4,7 @@ var PathUtils = require('../utils/path'); var deprecate = require('./deprecate'); var encodeConfig = require('./encodeConfig'); +var fileToURL = require('../output/helper/fileToURL'); /** Encode a global context into a JS object @@ -41,8 +42,8 @@ function encodeGlobal(output) { Convert a filepath into an url @return {String} */ - toURL: function(s) { - return s; + toURL: function(filePath) { + return fileToURL(output, filePath); }, /** From a9d558e89181b3b18151f6344a309ea444526e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 14:11:45 +0200 Subject: [PATCH 052/118] Add method ".toText" on glossary and summary --- lib/models/__tests__/glossary.js | 31 +++++++++++++++++--------- lib/models/__tests__/summary.js | 9 ++++++++ lib/models/glossary.js | 23 +++++++++++++++++++ lib/models/summary.js | 24 ++++++++++++++++++++ lib/plugins/__tests__/findInstalled.js | 2 +- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/lib/models/__tests__/glossary.js b/lib/models/__tests__/glossary.js index 03456275a..2ce224c23 100644 --- a/lib/models/__tests__/glossary.js +++ b/lib/models/__tests__/glossary.js @@ -5,18 +5,18 @@ describe('Glossary', function() { var Glossary = require('../glossary'); var GlossaryEntry = require('../glossaryEntry'); - describe('createFromEntries', function() { - var glossary = Glossary.createFromEntries(File(), [ - { - name: 'Hello World', - description: 'Awesome!' - }, - { - name: 'JavaScript', - description: 'This is a cool language' - } - ]); + var glossary = Glossary.createFromEntries(File(), [ + { + name: 'Hello World', + description: 'Awesome!' + }, + { + name: 'JavaScript', + description: 'This is a cool language' + } + ]); + describe('createFromEntries', function() { it('must add all entries', function() { var entries = glossary.getEntries(); expect(entries.size).toBe(2); @@ -28,6 +28,15 @@ describe('Glossary', function() { expect(entry instanceof GlossaryEntry).toBeTruthy(); }); }); + + describe('toText', function() { + pit('return as markdown', function() { + return glossary.toText('.md') + .then(function(text) { + expect(text).toContain('# Glossary'); + }); + }); + }); }); diff --git a/lib/models/__tests__/summary.js b/lib/models/__tests__/summary.js index b80830728..e7b496e22 100644 --- a/lib/models/__tests__/summary.js +++ b/lib/models/__tests__/summary.js @@ -68,6 +68,15 @@ describe('Summary', function() { expect(article.getTitle()).toBe('My Second Article'); }); }); + + describe('toText', function() { + pit('return as markdown', function() { + return summary.toText('.md') + .then(function(text) { + expect(text).toContain('# Summary'); + }); + }); + }); }); diff --git a/lib/models/glossary.js b/lib/models/glossary.js index 51aa37053..bb4407d63 100644 --- a/lib/models/glossary.js +++ b/lib/models/glossary.js @@ -1,7 +1,9 @@ var Immutable = require('immutable'); +var error = require('../utils/error'); var File = require('./file'); var GlossaryEntry = require('./glossaryEntry'); +var parsers = require('../parsers'); var Glossary = Immutable.Record({ file: File(), @@ -29,6 +31,27 @@ Glossary.prototype.getEntry = function(name) { return entries.get(id); }; +/** + Render glossary as text + + @return {Promise} +*/ +Glossary.prototype.toText = function(parser) { + var file = this.getFile(); + var entries = this.getEntries(); + + parser = parser? parsers.getByExt(parser) : file.getParser(); + + if (!parser) { + throw error.FileNotParsableError({ + filename: file.getPath() + }); + } + + return parser.glossary.toText(entries.toJS()); +}; + + /** Add/Replace an entry to a glossary diff --git a/lib/models/summary.js b/lib/models/summary.js index d8d0603e9..079c53514 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -1,8 +1,10 @@ var Immutable = require('immutable'); +var error = require('../utils/error'); var File = require('./file'); var SummaryPart = require('./summaryPart'); var SummaryArticle = require('./summaryArticle'); +var parsers = require('../parsers'); var Summary = Immutable.Record({ file: File(), @@ -63,6 +65,28 @@ Summary.prototype.getByPath = function(filePath) { }); }; +/** + Render summary as text + + @return {Promise} +*/ +Summary.prototype.toText = function(parser) { + var file = this.getFile(); + var parts = this.getParts(); + + parser = parser? parsers.getByExt(parser) : file.getParser(); + + if (!parser) { + throw error.FileNotParsableError({ + filename: file.getPath() + }); + } + + return parser.summary.toText({ + parts: parts.toJS() + }); +}; + /** Create a new summary for a list of parts diff --git a/lib/plugins/__tests__/findInstalled.js b/lib/plugins/__tests__/findInstalled.js index fca8295b1..ce9da0aef 100644 --- a/lib/plugins/__tests__/findInstalled.js +++ b/lib/plugins/__tests__/findInstalled.js @@ -9,7 +9,7 @@ describe('findInstalled', function() { pit('must list default plugins for gitbook directory', function() { return findInstalled(path.resolve(__dirname, '../../../')) .then(function(plugins) { - expect(plugins.size).toBe(7); + expect(plugins.size > 7).toBeTruthy(); expect(plugins.has('fontsettings')).toBe(true); expect(plugins.has('search')).toBe(true); From 978bdd149e394fdcb1717d1f8b8bd6c8852442c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 14:18:23 +0200 Subject: [PATCH 053/118] Add browser specific index.js --- lib/browser.js | 9 +++++++++ package.json | 1 + 2 files changed, 10 insertions(+) create mode 100644 lib/browser.js diff --git a/lib/browser.js b/lib/browser.js new file mode 100644 index 000000000..42f8d0a16 --- /dev/null +++ b/lib/browser.js @@ -0,0 +1,9 @@ +var Book = require('./models/book'); +var FS = require('./models/fs'); +var Parse = require('./parse'); + +module.exports = { + Book: Book, + Parse: Parse, + FS: FS +}; diff --git a/package.json b/package.json index 90f5f7c9b..54adddf17 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "homepage": "https://www.gitbook.com", "description": "Library and cmd utility to generate GitBooks", "main": "lib/index.js", + "browser": "./lib/browser.js", "dependencies": { "bash-color": "0.0.3", "cheerio": "0.20.0", From 50f535b910dc9092378e343a5f948104564cb462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 14:31:45 +0200 Subject: [PATCH 054/118] Fix plugin's config validation --- lib/plugins/validateConfig.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/plugins/validateConfig.js b/lib/plugins/validateConfig.js index cb6c38a3a..6303dae66 100644 --- a/lib/plugins/validateConfig.js +++ b/lib/plugins/validateConfig.js @@ -1,3 +1,4 @@ +var Immutable = require('immutable'); var jsonschema = require('jsonschema'); var jsonSchemaDefaults = require('json-schema-defaults'); var mergeDefaults = require('merge-defaults'); @@ -23,7 +24,7 @@ function validatePluginConfig(book, plugin) { var pluginConfig = config.getValue(configKey, {}); - var schema = packageInfos.gitbook || {}; + var schema = (packageInfos.get('gitbook') || Immutable.Map()).toJS(); if (!schema) return book; // Normalize schema From d585269228393a398cce6dae66d1ab63ea1d5d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 14:37:02 +0200 Subject: [PATCH 055/118] Correctly ignore summary and templates from assets --- lib/parse/__tests__/parseBook.js | 21 +++++++++++++++++++++ lib/parse/listAssets.js | 10 +++++++++- lib/parse/parseIgnore.js | 5 ++++- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 lib/parse/__tests__/parseBook.js diff --git a/lib/parse/__tests__/parseBook.js b/lib/parse/__tests__/parseBook.js new file mode 100644 index 000000000..37f7f02a5 --- /dev/null +++ b/lib/parse/__tests__/parseBook.js @@ -0,0 +1,21 @@ +jest.autoMockOff(); + +var Book = require('../../models/book'); +var createMockFS = require('../../fs/mock'); + +describe('parseBook', function() { + var parseBook = require('../parseBook'); + + pit('should parse glossary if exists', function() { + var fs = createMockFS({ + 'README.md': 'Hello World', + 'GLOSSARY.md': '# Glossary\n\n## Hello\nDescription for hello' + }); + var book = Book.createForFS(fs); + + return parseBook(book) + .then(function(resultBook) { + + }); + }); +}); diff --git a/lib/parse/listAssets.js b/lib/parse/listAssets.js index 6159eda0c..a0dbbc9be 100644 --- a/lib/parse/listAssets.js +++ b/lib/parse/listAssets.js @@ -10,12 +10,20 @@ function listAssets(book, pages) { var fs = book.getContentFS(); + var summary = book.getSummary(); + var summaryFile = summary.getFile().getPath(); + + var glossary = book.getGlossary(); + var glossaryFile = glossary.getFile().getPath(); + return fs.listAllFiles() .then(function(files) { return files.filterNot(function(file) { return ( book.isContentFileIgnored(file) || - pages.has(file) + pages.has(file) || + file !== summaryFile || + file !== glossaryFile ); }); }); diff --git a/lib/parse/parseIgnore.js b/lib/parse/parseIgnore.js index 3ffe89ed7..3dd44f2d2 100644 --- a/lib/parse/parseIgnore.js +++ b/lib/parse/parseIgnore.js @@ -25,7 +25,10 @@ function parseIgnore(book) { '_book', '*.pdf', '*.epub', - '*.mobi' + '*.mobi', + + // Ignore files in the templates folder + '_layouts' ]); return Promise.serie(IGNORE_FILES, function(filename) { From 503cc4a7297cdba2a3d6310c030a894603105f2e Mon Sep 17 00:00:00 2001 From: Soreine Date: Wed, 27 Apr 2016 14:52:26 +0200 Subject: [PATCH 056/118] Check for stdout, for browser compatibility --- lib/utils/logger.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/utils/logger.js b/lib/utils/logger.js index a91370932..fc9c39454 100644 --- a/lib/utils/logger.js +++ b/lib/utils/logger.js @@ -20,7 +20,11 @@ var COLORS = { function Logger(write, logLevel) { if (!(this instanceof Logger)) return new Logger(write, logLevel); - this._write = write || function(msg) { process.stdout.write(msg); }; + this._write = write || function(msg) { + if(process.stdout) { + process.stdout.write(msg); + } + }; this.lastChar = '\n'; this.setLevel(logLevel || 'info'); From cc9fdd0032e8d0e6effe7ab7a4da71e8e7d11af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 15:12:18 +0200 Subject: [PATCH 057/118] Generate multilingual books --- lib/output/generateBook.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index 5138f883a..cd20c8d29 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -1,3 +1,4 @@ +var path = require('path'); var Immutable = require('immutable'); var Output = require('../models/output'); @@ -94,6 +95,21 @@ function generateBook(generator, book, options) { .then(generateAssets.bind(null, generator)) .then(generatePages.bind(null, generator)) + .tap(function(output) { + var book = output.getBook(); + var books = book.getBooks(); + var outputRoot = output.getRoot(); + + if (!book.isMultilingual()) { + return; + } + + return Promise.forEach(books, function(langBook, language) { + var langOptions = options.set('root', path.join(outputRoot, language)); + return generateBook(generator, langBook, langOptions); + }); + }) + .then(callHook.bind(null, 'finish:before', function(output) { From 999882e72327e06dd2fd346ca13eccb1c7e8781f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 15:42:55 +0200 Subject: [PATCH 058/118] Add test for highlightCode html modifier --- .../modifiers/__tests__/highlightCode.js | 63 +++++++++++++++++++ lib/output/modifiers/editHTMLElement.js | 2 +- lib/output/modifiers/highlightCode.js | 44 +++++++++++++ lib/output/modifiers/index.js | 3 +- 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 lib/output/modifiers/__tests__/highlightCode.js create mode 100644 lib/output/modifiers/highlightCode.js diff --git a/lib/output/modifiers/__tests__/highlightCode.js b/lib/output/modifiers/__tests__/highlightCode.js new file mode 100644 index 000000000..bd7d42202 --- /dev/null +++ b/lib/output/modifiers/__tests__/highlightCode.js @@ -0,0 +1,63 @@ +jest.autoMockOff(); + +var cheerio = require('cheerio'); +var Promise = require('../../../utils/promise'); + +describe('highlightCode', function() { + var highlightCode = require('../highlightCode'); + + function doHighlight(lang, code) { + return { + text: '' + (lang || '') + '$' + code + }; + } + + function doHighlightAsync(lang, code) { + return Promise() + .then(function() { + return doHighlight(lang, code); + }); + } + + pit('should call it for normal code element', function() { + var $ = cheerio.load('

This is a test

'); + + return highlightCode(doHighlight, $) + .then(function() { + var $code = $('code'); + expect($code.text()).toBe('$test'); + }); + }); + + pit('should call it for markdown code block', function() { + var $ = cheerio.load('
test
'); + + return highlightCode(doHighlight, $) + .then(function() { + var $code = $('code'); + expect($code.text()).toBe('js$test'); + }); + }); + + pit('should call it for asciidoc code block', function() { + var $ = cheerio.load('
test
'); + + return highlightCode(doHighlight, $) + .then(function() { + var $code = $('code'); + expect($code.text()).toBe('python$test'); + }); + }); + + pit('should accept async highlighter', function() { + var $ = cheerio.load('
test
'); + + return highlightCode(doHighlightAsync, $) + .then(function() { + var $code = $('code'); + expect($code.text()).toBe('python$test'); + }); + }); +}); + + diff --git a/lib/output/modifiers/editHTMLElement.js b/lib/output/modifiers/editHTMLElement.js index 9897dcc7e..755598e8c 100644 --- a/lib/output/modifiers/editHTMLElement.js +++ b/lib/output/modifiers/editHTMLElement.js @@ -8,7 +8,7 @@ function editHTMLElement($, selector, fn) { return Promise.forEach($elements, function(el) { var $el = $(el); - fn($el); + return fn($el); }); } diff --git a/lib/output/modifiers/highlightCode.js b/lib/output/modifiers/highlightCode.js new file mode 100644 index 000000000..deabd6817 --- /dev/null +++ b/lib/output/modifiers/highlightCode.js @@ -0,0 +1,44 @@ +var Promise = require('../../utils/promise'); +var editHTMLElement = require('./editHTMLElement'); + +/** + Highlight all code elements + + @param {Function(lang, body) -> String} highlight + @param {HTMLDom} $ + @return {Promise} +*/ +function highlightCode(highlight, $) { + return editHTMLElement($, 'code', function($code) { + var classNames = ($code.attr('class') || '').split(' '); + var lang = classNames + .map(function(cl) { + // Markdown + if (cl.search('lang-') === 0) { + return cl.slice('lang-'.length); + } + + // Asciidoc + if (cl.search('language-') === 0) { + return cl.slice('language-'.length); + } + + return null; + }) + .find(function(cl) { + return Boolean(cl); + }); + var source = $code.text(); + + return Promise(highlight(lang, source)) + .then(function(r) { + if (r.html) { + $code.html(r.html); + } else { + $code.text(r.text); + } + }); + }); +} + +module.exports = highlightCode; diff --git a/lib/output/modifiers/index.js b/lib/output/modifiers/index.js index 75b59274a..4cdb01bce 100644 --- a/lib/output/modifiers/index.js +++ b/lib/output/modifiers/index.js @@ -9,5 +9,6 @@ module.exports = { fetchRemoteImages: require('./fetchRemoteImages'), svgToPng: require('./svgToPng'), resolveLinks: require('./resolveLinks'), - annotateText: require('./annotateText') + annotateText: require('./annotateText'), + highlightCode: require('./highlightCode') }; From e50422bce588ec5a0b5581bf3963b0995138de4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 16:22:17 +0200 Subject: [PATCH 059/118] Use code TemplateBlock to highlight code --- lib/constants/defaultBlocks.js | 14 ++++---- lib/models/plugin.js | 6 ++-- lib/models/templateBlock.js | 9 +++-- lib/models/templateEngine.js | 22 ++++-------- lib/output/createTemplateEngine.js | 16 +++------ lib/output/getModifiers.js | 42 +++++++++++++++++++++-- lib/output/modifiers/highlightCode.js | 48 +++++++++++++++++---------- lib/plugins/index.js | 4 ++- lib/plugins/listBlocks.js | 17 ++++++++++ lib/plugins/listFilters.js | 17 ++++++++++ lib/plugins/listResources.js | 2 +- package.json | 1 + 12 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 lib/plugins/listBlocks.js create mode 100644 lib/plugins/listFilters.js diff --git a/lib/constants/defaultBlocks.js b/lib/constants/defaultBlocks.js index ed1911565..74d1f1f36 100644 --- a/lib/constants/defaultBlocks.js +++ b/lib/constants/defaultBlocks.js @@ -1,15 +1,15 @@ var Immutable = require('immutable'); var TemplateBlock = require('../models/templateBlock'); -module.exports = Immutable.List([ - TemplateBlock({ +module.exports = Immutable.Map({ + html: TemplateBlock({ name: 'html', process: function(blk) { return blk; } }), - TemplateBlock({ + code: TemplateBlock({ name: 'code', process: function(blk) { return { @@ -19,7 +19,7 @@ module.exports = Immutable.List([ } }), - TemplateBlock({ + markdown: TemplateBlock({ name: 'markdown', process: function(blk) { return this.book.renderInline('markdown', blk.body) @@ -29,7 +29,7 @@ module.exports = Immutable.List([ } }), - TemplateBlock({ + asciidoc: TemplateBlock({ name: 'asciidoc', process: function(blk) { return this.book.renderInline('asciidoc', blk.body) @@ -39,7 +39,7 @@ module.exports = Immutable.List([ } }), - TemplateBlock({ + markup: TemplateBlock({ name: 'markup', process: function(blk) { return this.book.renderInline(this.ctx.file.type, blk.body) @@ -48,4 +48,4 @@ module.exports = Immutable.List([ }); } }) -]); +}); diff --git a/lib/models/plugin.js b/lib/models/plugin.js index 909ca0b6a..dd7bc903a 100644 --- a/lib/models/plugin.js +++ b/lib/models/plugin.js @@ -100,7 +100,7 @@ Plugin.prototype.getFilters = function() { /** Return map of blocks - @return {List} + @return {Map} */ Plugin.prototype.getBlocks = function() { var blocks = this.getContent().get('blocks'); @@ -109,9 +109,7 @@ Plugin.prototype.getBlocks = function() { return blocks .map(function(block, blockName) { return TemplateBlock.create(blockName, block); - }) - .valueSeq() - .toList(); + }); }; /** diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js index 4094b8997..92a0bf3b7 100644 --- a/lib/models/templateBlock.js +++ b/lib/models/templateBlock.js @@ -1,4 +1,5 @@ var is = require('is'); +var extend = require('extend'); var Immutable = require('immutable'); var Promise = require('../utils/promise'); @@ -72,7 +73,7 @@ TemplateBlock.prototype.getExtensionName = function() { @return {Nunjucks.Extension} */ -TemplateBlock.prototype.toNunjucksExt = function() { +TemplateBlock.prototype.toNunjucksExt = function(mainContext) { var that = this; var name = this.getName(); var endTag = this.getEndTag(); @@ -175,7 +176,11 @@ TemplateBlock.prototype.toNunjucksExt = function() { Promise() .then(function() { - return that.applyBlock(mainBlock, context); + var ctx = extend({ + ctx: context + }, mainContext || {}); + + return that.applyBlock(mainBlock, ctx); }) .then(function(result) { return that.blockResultToHtml(result); diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index 114aa7322..0d0a015a6 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -2,10 +2,10 @@ var nunjucks = require('nunjucks'); var Immutable = require('immutable'); var TemplateEngine = Immutable.Record({ - // List of {TemplateBlock} - blocks: Immutable.List(), + // Map of {TemplateBlock} + blocks: Immutable.Map(), - // List of Extension + // Map of Extension extensions: Immutable.Map(), // Map of filters: {String} name -> {Function} fn @@ -68,12 +68,12 @@ TemplateEngine.prototype.getBlock = function(name) { @return {Nunjucks.Environment} */ TemplateEngine.prototype.toNunjucks = function() { - var that = this; var loader = this.getLoader(); var blocks = this.getBlocks(); var filters = this.getFilters(); var globals = this.getGlobals(); var extensions = this.getExtensions(); + var context = this.getContext(); var env = new nunjucks.Environment( loader, @@ -95,13 +95,13 @@ TemplateEngine.prototype.toNunjucks = function() { // Add filters filters.forEach(function(filterFn, filterName) { - env.addFilter(filterName, that.bindToContext(filterFn)); + env.addFilter(filterName, filterFn.bind(context)); }); // Add blocks blocks.forEach(function(block) { var extName = block.getExtensionName(); - var Ext = block.toNunjucksExt(); + var Ext = block.toNunjucksExt(context); env.addExtension(extName, new Ext()); }); @@ -119,16 +119,6 @@ TemplateEngine.prototype.toNunjucks = function() { return env; }; -/** - Bind a function to the context - - @param {Function} fn - @return {Function} -*/ -TemplateEngine.prototype.bindToContext = function(fn) { - return fn.bind(this.getContext()); -}; - /** Create a template engine diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index 9b29b2a3e..523c850af 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -3,6 +3,8 @@ var Immutable = require('immutable'); var Templating = require('../templating'); var TemplateEngine = require('../models/templateEngine'); +var Plugins = require('../plugins'); + var defaultBlocks = require('../constants/defaultBlocks'); var defaultFilters = require('../constants/defaultFilters'); @@ -19,19 +21,11 @@ function createTemplateEngine(output) { var rootFolder = book.getContentRoot(); var logger = book.getLogger(); - var filters = plugins - .reduce(function(result, plugin) { - return result.merge(plugin.getFilters()); - }, Immutable.Map()); - - var blocks = plugins - .map(function(plugin) { - return plugin.getBlocks(); - }) - .flatten(1); + var filters = Plugins.listFilters(plugins); + var blocks = Plugins.listBlocks(plugins); // Extend with default - blocks = defaultBlocks.concat(blocks); + blocks = defaultBlocks.merge(blocks); filters = defaultFilters.merge(filters); // Create loader diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 34a0b9e6a..1dd62ba13 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -1,25 +1,61 @@ var Modifiers = require('./modifiers'); var resolveFileToURL = require('./helper/resolveFileToURL'); +var Api = require('../api'); +var Plugins = require('../plugins'); +var Promise = require('../utils/promise'); +var defaultBlocks = require('../constants/defaultBlocks'); + +var CODEBLOCK = 'code'; /** - Return default modifier to prepare a page + Return default modifier to prepare a page for + rendering. @return */ function getModifiers(output, page) { var book = output.getBook(); + var plugins = output.getPlugins(); var glossary = book.getGlossary(); var entries = glossary.getEntries(); - var file = page.getFile(); + // Get TemplateBlock for highlighting + var blocks = Plugins.listBlocks(plugins); + var code = blocks.get(CODEBLOCK) || defaultBlocks.get(CODEBLOCK); + + // Current context + var context = Api.encodeGlobal(output); + return [ + // Normalize IDs on headings Modifiers.addHeadingId, + + // Resolve links (.md -> .html) Modifiers.resolveLinks.bind(null, file.getPath(), resolveFileToURL.bind(null, output) ), - Modifiers.annotateText.bind(null, entries) + + // Annotate text with glossary entries + Modifiers.annotateText.bind(null, entries), + + // Highlight code blocks using "code" block + Modifiers.highlightCode.bind(null, function(lang, source) { + return Promise(code.applyBlock({ + body: source, + kwargs: { + language: lang + } + }, context)) + .then(function(result) { + if (result.html === false) { + return { text: result.body }; + } else { + return { html: result.body }; + } + }); + }) ]; } diff --git a/lib/output/modifiers/highlightCode.js b/lib/output/modifiers/highlightCode.js index deabd6817..dcd9d24b3 100644 --- a/lib/output/modifiers/highlightCode.js +++ b/lib/output/modifiers/highlightCode.js @@ -1,6 +1,34 @@ +var is = require('is'); var Promise = require('../../utils/promise'); var editHTMLElement = require('./editHTMLElement'); +/** + Return language for a code blocks from a list of class names + + @param {Array} + @return {String} +*/ +function getLanguageForClass(classNames) { + return classNames + .map(function(cl) { + // Markdown + if (cl.search('lang-') === 0) { + return cl.slice('lang-'.length); + } + + // Asciidoc + if (cl.search('language-') === 0) { + return cl.slice('language-'.length); + } + + return null; + }) + .find(function(cl) { + return Boolean(cl); + }); +} + + /** Highlight all code elements @@ -11,28 +39,12 @@ var editHTMLElement = require('./editHTMLElement'); function highlightCode(highlight, $) { return editHTMLElement($, 'code', function($code) { var classNames = ($code.attr('class') || '').split(' '); - var lang = classNames - .map(function(cl) { - // Markdown - if (cl.search('lang-') === 0) { - return cl.slice('lang-'.length); - } - - // Asciidoc - if (cl.search('language-') === 0) { - return cl.slice('language-'.length); - } - - return null; - }) - .find(function(cl) { - return Boolean(cl); - }); + var lang = getLanguageForClass(classNames); var source = $code.text(); return Promise(highlight(lang, source)) .then(function(r) { - if (r.html) { + if (is.string(r.html)) { $code.html(r.html); } else { $code.text(r.text); diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 8842342cf..607a7f121 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -3,6 +3,8 @@ module.exports = { loadForBook: require('./loadForBook'), validateConfig: require('./validateConfig'), installPlugins: require('./installPlugins'), - listResources: require('./listResources') + listResources: require('./listResources'), + listBlocks: require('./listBlocks'), + listFilters: require('./listFilters') }; diff --git a/lib/plugins/listBlocks.js b/lib/plugins/listBlocks.js new file mode 100644 index 000000000..f738937a0 --- /dev/null +++ b/lib/plugins/listBlocks.js @@ -0,0 +1,17 @@ +var Immutable = require('immutable'); + +/** + List blocks from a list of plugins + + @param {OrderedMap} + @return {Map} +*/ +function listBlocks(plugins) { + return plugins + .reverse() + .reduce(function(result, plugin) { + return result.merge(plugin.getBlocks()); + }, Immutable.Map()); +} + +module.exports = listBlocks; diff --git a/lib/plugins/listFilters.js b/lib/plugins/listFilters.js new file mode 100644 index 000000000..4d8a47148 --- /dev/null +++ b/lib/plugins/listFilters.js @@ -0,0 +1,17 @@ +var Immutable = require('immutable'); + +/** + List filters from a list of plugins + + @param {OrderedMap} + @return {Map} +*/ +function listFilters(plugins) { + return plugins + .reverse() + .reduce(function(result, plugin) { + return result.merge(plugin.getFilters()); + }, Immutable.Map()); +} + +module.exports = listFilters; diff --git a/lib/plugins/listResources.js b/lib/plugins/listResources.js index b4a16cb4d..4a73a2c02 100644 --- a/lib/plugins/listResources.js +++ b/lib/plugins/listResources.js @@ -5,7 +5,7 @@ var LocationUtils = require('../utils/location'); var PLUGIN_RESOURCES = require('../constants/pluginResources'); /** - Encode plugins as JSON + List all resources from a list of plugins @param {OrderedMap} @param {String} type diff --git a/package.json b/package.json index 54adddf17..c35c7f98f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "escape-html": "^1.0.3", "escape-string-regexp": "1.0.5", "eslint": "^2.2.0", + "extend": "^3.0.0", "front-matter": "2.0.7", "gitbook-asciidoc": "1.1.0", "gitbook-markdown": "1.2.0", From 869822d180f3fa7bf3a263bc29e280789abfed7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 16:26:16 +0200 Subject: [PATCH 060/118] Use orderedMap for language books --- lib/models/book.js | 14 ++++++++++++++ lib/parse/parseBook.js | 5 +---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/models/book.js b/lib/models/book.js index 6daa68410..183503218 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -161,6 +161,20 @@ Book.prototype.isMultilingual = function() { return (this.getLanguages().getList().size > 0); }; +/** + Add a new language book + + @param {String} language + @param {Book} book + @return {Book} +*/ +Book.prototype.addLanguageBook = function(language, book) { + var books = this.getBooks(); + books = books.set(language, book); + + return this.set('books', books); +}; + /** Change log level diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index 3a7d845e0..c30027c38 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -39,10 +39,7 @@ function parseMultilingualBook(book) { .then(parseConfig) .then(parseBookContent) .then(function(result) { - var books = currentBook.getBooks(); - books = books.push(result); - - return currentBook.set('books', books); + return currentBook.addLanguageBook(lang, result); }); }, book); } From 5b342b50ce862a4d60002451e6c1abd09a7b20ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 16:41:17 +0200 Subject: [PATCH 061/118] Add language property to book for multilingual --- lib/api/encodeGlobal.js | 90 +++++++++++++++++++++-------------------- lib/models/book.js | 30 +++++++++----- lib/parse/parseBook.js | 5 ++- 3 files changed, 69 insertions(+), 56 deletions(-) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 20b706e8e..fa336962b 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -20,61 +20,65 @@ function encodeGlobal(output) { var result = { log: logger, - config: encodeConfig(output, book.getConfig()) - }; + config: encodeConfig(output, book.getConfig()), - result.output = { - /** - Name of the generator being used - {String} - */ - name: output.getGenerator(), - - /** - Return absolute path to the root folder of output - @return {String} - */ - root: function() { - return outputFolder; + isMultilingual: function() { + return book.isMultilingual(); }, - /** - Convert a filepath into an url - @return {String} - */ - toURL: function(filePath) { - return fileToURL(output, filePath); + isLanguageBook: function() { + return book.isLanguageBook(); }, - /** - Write a file to the output folder, - It creates the required folder + isSubBook: deprecate.method(output, 'this.isSubBook', function() { + return book.isLanguageBook(); + }, '"isSubBook" is deprecated, use "isLanguageBook()" instead'), - @param {String} fileName - @param {Buffer} content - @return {Promise} - */ - writeFile: function(fileName, content) { - return Promise() - .then(function() { - var filePath = PathUtils.resolveInRoot(outputFolder, fileName); + output: { + /** + Name of the generator being used + {String} + */ + name: output.getGenerator(), - return fs.ensure(filePath) + /** + Return absolute path to the root folder of output + @return {String} + */ + root: function() { + return outputFolder; + }, + + /** + Convert a filepath into an url + @return {String} + */ + toURL: function(filePath) { + return fileToURL(output, filePath); + }, + + /** + Write a file to the output folder, + It creates the required folder + + @param {String} fileName + @param {Buffer} content + @return {Promise} + */ + writeFile: function(fileName, content) { + return Promise() .then(function() { - return fs.writeFile(filePath, content); + var filePath = PathUtils.resolveInRoot(outputFolder, fileName); + + return fs.ensure(filePath) + .then(function() { + return fs.writeFile(filePath, content); + }); }); - }); + } } }; - result.isMultilingual = function() { - return book.isMultilingual(); - }; - - result.isLanguageBook = function() { - return false; - }; - deprecate.field(output, 'this.book', result, 'book', result, '"book" property is deprecated, use "this" directly instead'); diff --git a/lib/models/book.js b/lib/models/book.js index 183503218..50276fac6 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -29,9 +29,8 @@ var Book = Immutable.Record({ glossary: Glossary(), languages: Languages(), - - // Parent of this book, if multilingual - parent: null, + // ID of the language for language books + language: String(), // List of children, if multilingual (String -> Book) books: Immutable.OrderedMap() @@ -69,14 +68,14 @@ Book.prototype.getLanguages = function() { return this.get('languages'); }; -Book.prototype.getParent = function() { - return this.get('parent'); -}; - Book.prototype.getBooks = function() { return this.get('books'); }; +Book.prototype.getLanguage = function() { + return this.get('language'); +}; + /** Return FS instance to access the content @@ -161,6 +160,15 @@ Book.prototype.isMultilingual = function() { return (this.getLanguages().getList().size > 0); }; +/** + Return true if book is associated to a language + + @return {Boolean} +*/ +Book.prototype.isLanguageBook = function() { + return Boolean(this.getLanguage()); +}; + /** Add a new language book @@ -203,14 +211,14 @@ Book.createForFS = function createForFS(fs) { Create a language book from a parent @param {Book} parent - @param {String} basePath: folder o the book in the parent + @param {String} language @return {Book} */ -Book.createFromParent = function createFromParent(parent, basePath) { +Book.createFromParent = function createFromParent(parent, language) { return new Book({ logger: parent.getLogger(), - parent: parent, - fs: FS.reduceScope(parent.getFS(), basePath) + language: language, + fs: FS.reduceScope(parent.getFS(), language) }); }; diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index c30027c38..33f19f2ea 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -32,14 +32,15 @@ function parseMultilingualBook(book) { var langList = languages.getList(); return Promise.reduce(langList, function(currentBook, lang) { - var child = Book.createFromParent(currentBook, lang.getPath()); + var langID = lang.getID(); + var child = Book.createFromParent(currentBook, langID); return Promise(child) .then(parseIgnore) .then(parseConfig) .then(parseBookContent) .then(function(result) { - return currentBook.addLanguageBook(lang, result); + return currentBook.addLanguageBook(langID, result); }); }, book); } From e781d6d2b1705f44fcdd950f5541917fde41323f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 17:58:35 +0200 Subject: [PATCH 062/118] Add test for assets inliner --- docs/README.md | 2 ++ jest/customMatchers.js | 24 ++++++++++++++ lib/models/output.js | 31 ++++++++++++------- lib/output/ebook/index.js | 12 ++++--- lib/output/ebook/onFinish.js | 19 ++++++++++++ lib/output/ebook/onInit.js | 21 +++++++++++++ lib/output/ebook/onPage.js | 8 ++--- .../modifiers/__tests__/fetchRemoteImages.js | 25 +++++++++++++++ lib/output/modifiers/__tests__/svgToImg.js | 25 +++++++++++++++ lib/output/website/createTemplateEngine.js | 1 - lib/output/website/onPage.js | 2 +- package.json | 10 +++--- 12 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 jest/customMatchers.js create mode 100644 lib/output/ebook/onFinish.js create mode 100644 lib/output/ebook/onInit.js create mode 100644 lib/output/modifiers/__tests__/fetchRemoteImages.js create mode 100644 lib/output/modifiers/__tests__/svgToImg.js diff --git a/docs/README.md b/docs/README.md index d62623173..16917dda9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,7 @@ # GitBook Toolchain Documentation +![image](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg) + This document aims to be a comprehensive guide to GitBook. It contains the full documentation for version **{{ book.version }}**. Help for GitBook.com specific questions can be found at [help.gitbook.com](https://help.gitbook.com). ### What is GitBook? diff --git a/jest/customMatchers.js b/jest/customMatchers.js new file mode 100644 index 000000000..a0dd7f96b --- /dev/null +++ b/jest/customMatchers.js @@ -0,0 +1,24 @@ +var path = require('path'); +var fs = require('fs'); + +var matchers = { + /** + Verify that a file exists in a directory + */ + toHaveFile: function () { + return { + compare: function (actual, expected) { + var filePath = path.join(actual, expected); + var exists = fs.existsSync(filePath); + + return { + pass: exists + }; + } + }; + } +}; + +jasmine.getEnv().beforeEach(function () { + jasmine.addMatchers(matchers); +}); \ No newline at end of file diff --git a/lib/models/output.js b/lib/models/output.js index c11017e12..43e36f8d6 100644 --- a/lib/models/output.js +++ b/lib/models/output.js @@ -61,6 +61,26 @@ Output.prototype.getRoot = function() { return this.getOptions().get('root'); }; +/** + Update state of output + + @param {Map} newState + @return {Output} +*/ +Output.prototype.setState = function(newState) { + return this.set('state', newState); +}; + +/** + Update options + + @param {Map} newOptions + @return {Output} +*/ +Output.prototype.setOptions = function(newOptions) { + return this.set('options', newOptions); +}; + /** Return logegr for this output (same as book) @@ -70,15 +90,4 @@ Output.prototype.getLogger = function() { return this.getBook().getLogger(); }; -/** - Update state of output - - @param {Output} output - @param {Map} newState - @return {Output} -*/ -Output.updateState = function(output, newState) { - return output.set('state', newState); -}; - module.exports = Output; diff --git a/lib/output/ebook/index.js b/lib/output/ebook/index.js index 46a94e3f1..3aaa7e778 100644 --- a/lib/output/ebook/index.js +++ b/lib/output/ebook/index.js @@ -1,5 +1,9 @@ +var extend = require('extend'); +var websiteGenerator = require('../website'); -module.exports = { - - -}; +module.exports = extend({}, websiteGenerator, { + name: 'ebook', + onInit: require('./onInit'), + onPage: require('./onPage'), + onFinish: require('./onFinish') +}); diff --git a/lib/output/ebook/onFinish.js b/lib/output/ebook/onFinish.js new file mode 100644 index 000000000..5ea514838 --- /dev/null +++ b/lib/output/ebook/onFinish.js @@ -0,0 +1,19 @@ +var websiteGenerator = require('../website'); + +/** + Finish the generation, generate the ebook file using ebook-convert + + @param {Output} + @return {Output} +*/ +function onFinish(output) { + return websiteGenerator.onFinish(output) + .then(function(resultOutput) { + + // todo + + return resultOutput; + }); +} + +module.exports = onFinish; diff --git a/lib/output/ebook/onInit.js b/lib/output/ebook/onInit.js new file mode 100644 index 000000000..9cee141a4 --- /dev/null +++ b/lib/output/ebook/onInit.js @@ -0,0 +1,21 @@ +var websiteGenerator = require('../website'); + +/** + Initialize the generator + + @param {Output} + @return {Output} +*/ +function onInit(output) { + return websiteGenerator.onInit(output) + .then(function(resultOutput) { + var options = resultOutput.getOptions(); + + options = options.set('directoryIndex', false); + options = options.set('prefix', 'ebook'); + + return resultOutput.setOptions(options); + }); +} + +module.exports = onInit; diff --git a/lib/output/ebook/onPage.js b/lib/output/ebook/onPage.js index ab1513365..13edf173f 100644 --- a/lib/output/ebook/onPage.js +++ b/lib/output/ebook/onPage.js @@ -1,18 +1,18 @@ var website = require('../website'); -var Modifier = require('../modifier'); +var Modifiers = require('../modifiers'); /** Write a page for ebook output @param {Output} output - @param {Page} page + @param {Output} */ function onPage(output, page) { var options = output.getOptions(); // Inline assets - return Modifier.modifyHTML(page, [ - Modifier.inlineAssets(options.get('root')) + return Modifiers.modifyHTML(page, [ + Modifiers.inlineAssets(options.get('root')) ]) // Write page using website generator diff --git a/lib/output/modifiers/__tests__/fetchRemoteImages.js b/lib/output/modifiers/__tests__/fetchRemoteImages.js new file mode 100644 index 000000000..543aca09c --- /dev/null +++ b/lib/output/modifiers/__tests__/fetchRemoteImages.js @@ -0,0 +1,25 @@ +var cheerio = require('cheerio'); +var tmp = require('tmp'); + +describe('fetchRemoteImages', function() { + var dir; + var fetchRemoteImages = require('../fetchRemoteImages'); + + beforeEach(function() { + dir = tmp.dirSync(); + }); + + pit('should download image file', function() { + var $ = cheerio.load(''); + + return fetchRemoteImages(dir.name, $) + .then(function() { + var $img = $('img'); + var src = '.' + $img.attr('src'); + + expect(dir.name).toHaveFile(src); + }); + }); +}); + + diff --git a/lib/output/modifiers/__tests__/svgToImg.js b/lib/output/modifiers/__tests__/svgToImg.js new file mode 100644 index 000000000..762a02e3b --- /dev/null +++ b/lib/output/modifiers/__tests__/svgToImg.js @@ -0,0 +1,25 @@ +var cheerio = require('cheerio'); +var tmp = require('tmp'); + +describe('svgToImg', function() { + var dir; + var svgToImg = require('../svgToImg'); + + beforeEach(function() { + dir = tmp.dirSync(); + }); + + pit('should write svg as a file', function() { + var $ = cheerio.load(''); + + return svgToImg(dir.name, $) + .then(function() { + var $img = $('img'); + var src = '.' + $img.attr('src'); + + expect(dir.name).toHaveFile(src); + }); + }); +}); + + diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 7d8724804..24990b651 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -35,7 +35,6 @@ function createTemplateEngine(output, currentFile) { var i18n = state.getI18n(); var config = book.getConfig(); var summary = book.getSummary(); - var pages = output.getPages(); var outputFolder = output.getRoot(); // Search paths for templates diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index ddae986a4..751a4307e 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -53,7 +53,7 @@ function onPage(output, page) { gitbook: context.gitbook, basePath: basePath, book: { - language: null // context.book.language + language: book.getLanguage() } }; } diff --git a/package.json b/package.json index c35c7f98f..7840e8133 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,7 @@ }, "devDependencies": { "eslint": "2.7.0", - "jest-cli": "^11.0.2", - "mocha": "2.4.5", - "should": "8.3.0" + "jest-cli": "^11.0.2" }, "scripts": { "test": "node_modules/.bin/jest --bail", @@ -97,5 +95,9 @@ "name": "Samy Pessé", "email": "samy@gitbook.com" } - ] + ], + "jest": { + "automock": false, + "setupTestFrameworkScriptFile": "/jest/customMatchers.js" + } } From 9a1a6f2625612c18745957c4331be27dfdc2fb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 27 Apr 2016 18:02:49 +0200 Subject: [PATCH 063/118] Document cli/server.js --- lib/cli/server.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/cli/server.js b/lib/cli/server.js index 936ff4e92..555bbb7e9 100644 --- a/lib/cli/server.js +++ b/lib/cli/server.js @@ -14,12 +14,20 @@ function Server() { } util.inherits(Server, events.EventEmitter); -// Return true if the server is running +/** + Return true if the server is running + + @return {Boolean} +*/ Server.prototype.isRunning = function() { return !!this.running; }; -// Stop the server +/** + Stop the server + + @return {Promise} +*/ Server.prototype.stop = function() { var that = this; if (!this.isRunning()) return Promise(); @@ -40,6 +48,11 @@ Server.prototype.stop = function() { return d.promise; }; +/** + Start the server + + @return {Promise} +*/ Server.prototype.start = function(dir, port) { var that = this, pre = Promise(); port = port || 8004; From 9bb0a3fbd355b6dda2fe33e3a83884baa8f30917 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 27 Apr 2016 22:18:33 +0200 Subject: [PATCH 064/118] Add tests for svg inliner --- lib/output/getModifiers.js | 5 +++- lib/output/modifiers/__tests__/svgToImg.js | 4 +-- lib/output/modifiers/__tests__/svgToPng.js | 32 ++++++++++++++++++++++ lib/output/modifiers/inlineAssets.js | 8 +++--- lib/output/modifiers/svgToImg.js | 28 +++++++++++-------- lib/output/modifiers/svgToPng.js | 21 ++++++++++++-- lib/utils/fs.js | 2 +- 7 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 lib/output/modifiers/__tests__/svgToPng.js diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 1dd62ba13..19790631f 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -20,6 +20,9 @@ function getModifiers(output, page) { var entries = glossary.getEntries(); var file = page.getFile(); + // Current file path + var currentFilePath = file.getPath(); + // Get TemplateBlock for highlighting var blocks = Plugins.listBlocks(plugins); var code = blocks.get(CODEBLOCK) || defaultBlocks.get(CODEBLOCK); @@ -33,7 +36,7 @@ function getModifiers(output, page) { // Resolve links (.md -> .html) Modifiers.resolveLinks.bind(null, - file.getPath(), + currentFilePath, resolveFileToURL.bind(null, output) ), diff --git a/lib/output/modifiers/__tests__/svgToImg.js b/lib/output/modifiers/__tests__/svgToImg.js index 762a02e3b..793395e45 100644 --- a/lib/output/modifiers/__tests__/svgToImg.js +++ b/lib/output/modifiers/__tests__/svgToImg.js @@ -12,10 +12,10 @@ describe('svgToImg', function() { pit('should write svg as a file', function() { var $ = cheerio.load(''); - return svgToImg(dir.name, $) + return svgToImg(dir.name, 'index.html', $) .then(function() { var $img = $('img'); - var src = '.' + $img.attr('src'); + var src = $img.attr('src'); expect(dir.name).toHaveFile(src); }); diff --git a/lib/output/modifiers/__tests__/svgToPng.js b/lib/output/modifiers/__tests__/svgToPng.js new file mode 100644 index 000000000..163d72ec4 --- /dev/null +++ b/lib/output/modifiers/__tests__/svgToPng.js @@ -0,0 +1,32 @@ +var cheerio = require('cheerio'); +var tmp = require('tmp'); +var path = require('path'); + +describe('svgToPng', function() { + var dir; + var svgToImg = require('../svgToImg'); + var svgToPng = require('../svgToPng'); + + beforeEach(function() { + dir = tmp.dirSync(); + }); + + pit('should write svg as png file', function() { + var $ = cheerio.load(''); + var fileName = 'index.html'; + + return svgToImg(dir.name, fileName, $) + .then(function() { + return svgToPng(dir.name, fileName, $); + }) + .then(function() { + var $img = $('img'); + var src = $img.attr('src'); + + expect(dir.name).toHaveFile(src); + expect(path.extname(src)).toBe('.png'); + }); + }); +}); + + diff --git a/lib/output/modifiers/inlineAssets.js b/lib/output/modifiers/inlineAssets.js index 044953b99..4106d6991 100644 --- a/lib/output/modifiers/inlineAssets.js +++ b/lib/output/modifiers/inlineAssets.js @@ -9,12 +9,12 @@ var Promise = require('../../utils/promise'); @param {String} rootFolder */ -function inlineAssets(rootFolder) { +function inlineAssets(rootFolder, currentFile) { return function($) { return Promise() - .then(fetchRemoteImages.bind(null, rootFolder)) - .then(svgToImg.bind(null, rootFolder)) - .then(svgToPng.bind(null, rootFolder)); + .then(fetchRemoteImages.bind(null, rootFolder, currentFile)) + .then(svgToImg.bind(null, rootFolder, currentFile)) + .then(svgToPng.bind(null, rootFolder, currentFile)); }; } diff --git a/lib/output/modifiers/svgToImg.js b/lib/output/modifiers/svgToImg.js index d088e3e00..f31b06d1a 100644 --- a/lib/output/modifiers/svgToImg.js +++ b/lib/output/modifiers/svgToImg.js @@ -1,8 +1,10 @@ var path = require('path'); +var crc = require('crc'); var domSerializer = require('dom-serializer'); var editHTMLElement = require('./editHTMLElement'); var fs = require('../../utils/fs'); +var LocationUtils = require('../../utils/location'); /** Render a cheerio DOM as html @@ -26,23 +28,27 @@ function renderDOM($, dom, options) { @param {String} baseFolder @param {HTMLDom} $ */ -function svgToImg(baseFolder, $) { +function svgToImg(baseFolder, currentFile, $) { + var currentDirectory = path.dirname(currentFile); + return editHTMLElement($, 'svg', function($svg) { var content = '' + renderDOM($, $svg); - // Generate a filename - return fs.uniqueFilename(baseFolder, 'image.svg') - .then(function(fileName) { - var filePath = path.join(baseFolder, fileName); + // We avoid generating twice the same PNG + var hash = crc.crc32(content).toString(16); + var fileName = hash + '.svg'; + var filePath = path.join(baseFolder, fileName); - // Write the svg to the file - return fs.writeFile(filePath, content, 'utf8') + // Write the svg to the file + return fs.assertFile(filePath, function() { + return fs.writeFile(filePath, content, 'utf8'); + }) - // Return as image - .then(function() { - $svg.replaceWith(''); - }); + // Return as image + .then(function() { + var src = LocationUtils.relative(currentDirectory, fileName); + $svg.replaceWith(''); }); }); } diff --git a/lib/output/modifiers/svgToPng.js b/lib/output/modifiers/svgToPng.js index 4f8644a98..109310639 100644 --- a/lib/output/modifiers/svgToPng.js +++ b/lib/output/modifiers/svgToPng.js @@ -3,6 +3,7 @@ var path = require('path'); var imagesUtil = require('../../utils/images'); var fs = require('../../utils/fs'); +var LocationUtils = require('../../utils/location'); var editHTMLElement = require('./editHTMLElement'); @@ -13,23 +14,37 @@ var editHTMLElement = require('./editHTMLElement'); @param {HTMLDom} $ @return {Promise} */ -function svgToPng(rootFolder, $) { +function svgToPng(rootFolder, currentFile, $) { + var currentDirectory = path.dirname(currentFile); + return editHTMLElement($, 'img', function($img) { var src = $img.attr('src'); if (path.extname(src) !== '.svg') { return; } + // Calcul absolute path for this + src = LocationUtils.toAbsolute(src, currentDirectory, '.'); + // We avoid generating twice the same PNG var hash = crc.crc32(src).toString(16); var fileName = hash + '.png'; + + // Input file path + var inputPath = path.join(rootFolder, src); + + // Result file path var filePath = path.join(rootFolder, fileName); return fs.assertFile(filePath, function() { - return imagesUtil.convertSVGToPNG(src, filePath); + return imagesUtil.convertSVGToPNG(inputPath, filePath); }) .then(function() { - $img.replaceWith(''); + // Convert filename to a relative filename + fileName = LocationUtils.relative(currentDirectory, fileName); + + // Replace src + $img.attr('src', fileName); }); }); } diff --git a/lib/utils/fs.js b/lib/utils/fs.js index 96a0e49b0..008dd4018 100644 --- a/lib/utils/fs.js +++ b/lib/utils/fs.js @@ -109,7 +109,7 @@ function assertFile(filePath, generator) { .then(function(exists) { if (exists) return; - generator(); + return generator(); }); } From 139e487998a620af06837ab75396be3e169b00c0 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 27 Apr 2016 22:36:59 +0200 Subject: [PATCH 065/118] Correctly resolve images starting with / --- lib/output/getModifiers.js | 3 ++ .../modifiers/__tests__/fetchRemoteImages.js | 21 ++++++++++-- lib/output/modifiers/fetchRemoteImages.js | 14 +++++--- lib/output/modifiers/index.js | 1 + lib/output/modifiers/inlineAssets.js | 6 ++++ lib/output/modifiers/resolveImages.js | 33 +++++++++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 lib/output/modifiers/resolveImages.js diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 19790631f..23cfb1cf6 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -40,6 +40,9 @@ function getModifiers(output, page) { resolveFileToURL.bind(null, output) ), + // Resolve images + Modifiers.resolveImages.bind(null, currentFilePath), + // Annotate text with glossary entries Modifiers.annotateText.bind(null, entries), diff --git a/lib/output/modifiers/__tests__/fetchRemoteImages.js b/lib/output/modifiers/__tests__/fetchRemoteImages.js index 543aca09c..f5610a23f 100644 --- a/lib/output/modifiers/__tests__/fetchRemoteImages.js +++ b/lib/output/modifiers/__tests__/fetchRemoteImages.js @@ -1,5 +1,8 @@ var cheerio = require('cheerio'); var tmp = require('tmp'); +var path = require('path'); + +var URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png'; describe('fetchRemoteImages', function() { var dir; @@ -10,16 +13,28 @@ describe('fetchRemoteImages', function() { }); pit('should download image file', function() { - var $ = cheerio.load(''); + var $ = cheerio.load(''); - return fetchRemoteImages(dir.name, $) + return fetchRemoteImages(dir.name, 'index.html', $) .then(function() { var $img = $('img'); - var src = '.' + $img.attr('src'); + var src = $img.attr('src'); expect(dir.name).toHaveFile(src); }); }); + + pit('should download image file and replace with relative path', function() { + var $ = cheerio.load(''); + + return fetchRemoteImages(dir.name, 'test/index.html', $) + .then(function() { + var $img = $('img'); + var src = $img.attr('src'); + + expect(dir.name).toHaveFile(path.join('test', src)); + }); + }); }); diff --git a/lib/output/modifiers/fetchRemoteImages.js b/lib/output/modifiers/fetchRemoteImages.js index 792bbb6be..ef868b91a 100644 --- a/lib/output/modifiers/fetchRemoteImages.js +++ b/lib/output/modifiers/fetchRemoteImages.js @@ -3,21 +3,24 @@ var crc = require('crc'); var editHTMLElement = require('./editHTMLElement'); var fs = require('../../utils/fs'); -var location = require('../../utils/location'); +var LocationUtils = require('../../utils/location'); /** Fetch all remote images @param {String} rootFolder + @param {String} currentFile @param {HTMLDom} $ @return {Promise} */ -function fetchRemoteImages(rootFolder, $) { +function fetchRemoteImages(rootFolder, currentFile, $) { + var currentDirectory = path.dirname(currentFile); + return editHTMLElement($, 'img', function($img) { var src = $img.attr('src'); var extension = path.extname(src); - if (!location.isExternal(src)) { + if (!LocationUtils.isExternal(src)) { return; } @@ -30,7 +33,10 @@ function fetchRemoteImages(rootFolder, $) { return fs.download(src, filePath); }) .then(function() { - $img.replaceWith(''); + // Convert to relative + src = LocationUtils.relative(currentDirectory, fileName); + + $img.replaceWith(''); }); }); } diff --git a/lib/output/modifiers/index.js b/lib/output/modifiers/index.js index 4cdb01bce..f1daa2ba8 100644 --- a/lib/output/modifiers/index.js +++ b/lib/output/modifiers/index.js @@ -9,6 +9,7 @@ module.exports = { fetchRemoteImages: require('./fetchRemoteImages'), svgToPng: require('./svgToPng'), resolveLinks: require('./resolveLinks'), + resolveImages: require('./resolveImages'), annotateText: require('./annotateText'), highlightCode: require('./highlightCode') }; diff --git a/lib/output/modifiers/inlineAssets.js b/lib/output/modifiers/inlineAssets.js index 4106d6991..9f19fd7f2 100644 --- a/lib/output/modifiers/inlineAssets.js +++ b/lib/output/modifiers/inlineAssets.js @@ -1,5 +1,6 @@ var svgToImg = require('./svgToImg'); var svgToPng = require('./svgToPng'); +var resolveImages = require('./resolveImages'); var fetchRemoteImages = require('./fetchRemoteImages'); var Promise = require('../../utils/promise'); @@ -12,7 +13,12 @@ var Promise = require('../../utils/promise'); function inlineAssets(rootFolder, currentFile) { return function($) { return Promise() + + // Resolving images and fetching external images should be + // done before svg conversion + .then(resolveImages.bind(null, currentFile)) .then(fetchRemoteImages.bind(null, rootFolder, currentFile)) + .then(svgToImg.bind(null, rootFolder, currentFile)) .then(svgToPng.bind(null, rootFolder, currentFile)); }; diff --git a/lib/output/modifiers/resolveImages.js b/lib/output/modifiers/resolveImages.js new file mode 100644 index 000000000..e401cf500 --- /dev/null +++ b/lib/output/modifiers/resolveImages.js @@ -0,0 +1,33 @@ +var path = require('path'); + +var LocationUtils = require('../../utils/location'); +var editHTMLElement = require('./editHTMLElement'); + +/** + Resolve all HTML images: + - /test.png in hello -> ../test.html + + @param {String} currentFile + @param {HTMLDom} $ +*/ +function resolveImages(currentFile, $) { + var currentDirectory = path.dirname(currentFile); + + return editHTMLElement($, 'img', function($img) { + var src = $img.attr('src'); + + if (LocationUtils.isExternal(src)) { + return; + } + + // Calcul absolute path for this + src = LocationUtils.toAbsolute(src, currentDirectory, '.'); + + // Convert back to relative + src = LocationUtils.relative(currentDirectory, src); + + $img.attr('src', src); + }); +} + +module.exports = resolveImages; From 8dd5e0c2507fde0b423c12c4e347881fae991491 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Wed, 27 Apr 2016 22:42:09 +0200 Subject: [PATCH 066/118] Add comments for todos --- lib/output/ebook/onFinish.js | 4 +++- lib/output/json/onFinish.js | 15 +++++++++++++++ lib/output/website/onFinish.js | 4 +++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lib/output/json/onFinish.js diff --git a/lib/output/ebook/onFinish.js b/lib/output/ebook/onFinish.js index 5ea514838..81fa45d6b 100644 --- a/lib/output/ebook/onFinish.js +++ b/lib/output/ebook/onFinish.js @@ -10,7 +10,9 @@ function onFinish(output) { return websiteGenerator.onFinish(output) .then(function(resultOutput) { - // todo + // todo: + // - render SUMMARY.html + // - Build ebook using ebook-convert return resultOutput; }); diff --git a/lib/output/json/onFinish.js b/lib/output/json/onFinish.js new file mode 100644 index 000000000..107285aec --- /dev/null +++ b/lib/output/json/onFinish.js @@ -0,0 +1,15 @@ +var Promise = require('../../utils/promise'); + +/** + Finish the generation + + @param {Output} + @return {Output} +*/ +function onFinish(output) { + // todo: copy README.json from main language + + return Promise(output); +} + +module.exports = onFinish; diff --git a/lib/output/website/onFinish.js b/lib/output/website/onFinish.js index 691a7759e..a42425000 100644 --- a/lib/output/website/onFinish.js +++ b/lib/output/website/onFinish.js @@ -1,12 +1,14 @@ var Promise = require('../../utils/promise'); /** - Initialize the generator + Finish the generation @param {Output} @return {Output} */ function onFinish(output) { + // todo: render language index + return Promise(output); } From 35d352bbebee0b5adebf3f877342ff28e57a7a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 09:59:34 +0200 Subject: [PATCH 067/118] Handle correctly ignored file in multilanguages book --- lib/models/book.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/models/book.js b/lib/models/book.js index 50276fac6..c3096f68d 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -121,6 +121,14 @@ Book.prototype.getContentRoot = function() { */ Book.prototype.isFileIgnored = function(filename) { var ignore = this.getIgnore(); + var language = this.getLanguage(); + + // Ignore is always relative to the root of the main book + if (language) { + filename = path.join(language, filename); + } + + return ignore.filter([filename]).length == 0; }; @@ -217,6 +225,7 @@ Book.createForFS = function createForFS(fs) { Book.createFromParent = function createFromParent(parent, language) { return new Book({ logger: parent.getLogger(), + ignore: parent.getIgnore(), language: language, fs: FS.reduceScope(parent.getFS(), language) }); From 9413e53b001c3a7ee9d07a362a7bdd69df4d4c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 10:37:52 +0200 Subject: [PATCH 068/118] Add tests for parseIgnore --- lib/models/book.js | 4 ++- lib/parse/__tests__/parseBook.js | 2 -- lib/parse/__tests__/parseGlossary.js | 2 -- lib/parse/__tests__/parseIgnore.js | 40 ++++++++++++++++++++++++++++ lib/parse/__tests__/parseReadme.js | 2 -- lib/parse/__tests__/parseSummary.js | 2 -- lib/parse/parseBook.js | 6 ++++- lib/parse/parseIgnore.js | 6 ++++- 8 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 lib/parse/__tests__/parseIgnore.js diff --git a/lib/models/book.js b/lib/models/book.js index c3096f68d..bd7a0d05f 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -223,9 +223,11 @@ Book.createForFS = function createForFS(fs) { @return {Book} */ Book.createFromParent = function createFromParent(parent, language) { + var ignore = parent.getIgnore(); + return new Book({ logger: parent.getLogger(), - ignore: parent.getIgnore(), + ignore: Ignore().add(ignore), language: language, fs: FS.reduceScope(parent.getFS(), language) }); diff --git a/lib/parse/__tests__/parseBook.js b/lib/parse/__tests__/parseBook.js index 37f7f02a5..441b7c04f 100644 --- a/lib/parse/__tests__/parseBook.js +++ b/lib/parse/__tests__/parseBook.js @@ -1,5 +1,3 @@ -jest.autoMockOff(); - var Book = require('../../models/book'); var createMockFS = require('../../fs/mock'); diff --git a/lib/parse/__tests__/parseGlossary.js b/lib/parse/__tests__/parseGlossary.js index 14ff7cc85..53805fecd 100644 --- a/lib/parse/__tests__/parseGlossary.js +++ b/lib/parse/__tests__/parseGlossary.js @@ -1,5 +1,3 @@ -jest.autoMockOff(); - var Book = require('../../models/book'); var createMockFS = require('../../fs/mock'); diff --git a/lib/parse/__tests__/parseIgnore.js b/lib/parse/__tests__/parseIgnore.js new file mode 100644 index 000000000..bee4236a6 --- /dev/null +++ b/lib/parse/__tests__/parseIgnore.js @@ -0,0 +1,40 @@ +var Book = require('../../models/book'); +var createMockFS = require('../../fs/mock'); + +describe('parseIgnore', function() { + var parseIgnore = require('../parseIgnore'); + var fs = createMockFS({ + '.ignore': 'test-1.js', + '.gitignore': 'test-2.js\ntest-3.js', + '.bookignore': '!test-3.js', + 'test-1.js': '1', + 'test-2.js': '2', + 'test-3.js': '3' + }); + + function getBook() { + var book = Book.createForFS(fs); + return parseIgnore(book); + } + + pit('should load rules from .ignore', function() { + return getBook() + .then(function(book) { + expect(book.isFileIgnored('test-1.js')).toBeTruthy(); + }); + }); + + pit('should load rules from .gitignore', function() { + return getBook() + .then(function(book) { + expect(book.isFileIgnored('test-2.js')).toBeTruthy(); + }); + }); + + pit('should load rules from .bookignore', function() { + return getBook() + .then(function(book) { + expect(book.isFileIgnored('test-3.js')).toBeFalsy(); + }); + }); +}); diff --git a/lib/parse/__tests__/parseReadme.js b/lib/parse/__tests__/parseReadme.js index 7d7c631f9..1b1567ba7 100644 --- a/lib/parse/__tests__/parseReadme.js +++ b/lib/parse/__tests__/parseReadme.js @@ -1,5 +1,3 @@ -jest.autoMockOff(); - var Promise = require('../../utils/promise'); var Book = require('../../models/book'); var createMockFS = require('../../fs/mock'); diff --git a/lib/parse/__tests__/parseSummary.js b/lib/parse/__tests__/parseSummary.js index 31e5d357d..4b4650d87 100644 --- a/lib/parse/__tests__/parseSummary.js +++ b/lib/parse/__tests__/parseSummary.js @@ -1,5 +1,3 @@ -jest.autoMockOff(); - var Book = require('../../models/book'); var createMockFS = require('../../fs/mock'); diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index 33f19f2ea..7dfa1eba1 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -34,12 +34,16 @@ function parseMultilingualBook(book) { return Promise.reduce(langList, function(currentBook, lang) { var langID = lang.getID(); var child = Book.createFromParent(currentBook, langID); + var ignore = currentBook.getIgnore(); return Promise(child) - .then(parseIgnore) .then(parseConfig) .then(parseBookContent) .then(function(result) { + // Ignore content of this book when generating parent book + ignore.add(langID + '/**'); + currentBook = currentBook.set('ignore', ignore); + return currentBook.addLanguageBook(langID, result); }); }, book); diff --git a/lib/parse/parseIgnore.js b/lib/parse/parseIgnore.js index 3dd44f2d2..b23bfd8b3 100644 --- a/lib/parse/parseIgnore.js +++ b/lib/parse/parseIgnore.js @@ -8,6 +8,10 @@ var IGNORE_FILES = require('../constants/ignoreFiles'); @return {Book} */ function parseIgnore(book) { + if (book.isLanguageBook()) { + return Promise.reject(new Error('Ignore files could be parsed for language books')); + } + var fs = book.getFS(); var ignore = book.getIgnore(); @@ -35,7 +39,7 @@ function parseIgnore(book) { return fs.readAsString(filename) .then(function(content) { ignore.addPattern(content.toString().split(/\r?\n/)); - }, function() { + }, function(err) { return Promise(); }); }) From 4763f1342c1b63d2f43ae2f5060b93d4c09271fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 11:15:51 +0200 Subject: [PATCH 069/118] Add base modifiers for summary as SummaryModifier --- lib/browser.js | 10 +++++++-- lib/index.js | 15 +++++++------ lib/models/summary.js | 11 ++++++++++ lib/modifiers/index.js | 4 ++++ lib/modifiers/summary/editPartTitle.js | 24 +++++++++++++++++++++ lib/modifiers/summary/index.js | 7 +++++++ lib/modifiers/summary/indexLevels.js | 13 ++++++++++++ lib/modifiers/summary/insertArticle.js | 17 +++++++++++++++ lib/modifiers/summary/unshiftArticle.js | 28 +++++++++++++++++++++++++ lib/parse/parseSummary.js | 26 ++++++++++++++++++++++- 10 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 lib/modifiers/index.js create mode 100644 lib/modifiers/summary/editPartTitle.js create mode 100644 lib/modifiers/summary/index.js create mode 100644 lib/modifiers/summary/indexLevels.js create mode 100644 lib/modifiers/summary/insertArticle.js create mode 100644 lib/modifiers/summary/unshiftArticle.js diff --git a/lib/browser.js b/lib/browser.js index 42f8d0a16..c2445308f 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -1,9 +1,15 @@ var Book = require('./models/book'); var FS = require('./models/fs'); var Parse = require('./parse'); +var Modifiers = require('./modifiers'); module.exports = { - Book: Book, Parse: Parse, - FS: FS + + // Models + Book: Book, + FS: FS, + + // Modifiers + SummaryModifier: Modifiers.Summary }; diff --git a/lib/index.js b/lib/index.js index a772efe69..ffaea258d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,11 +1,10 @@ -var Book = require('./models/book'); -var FS = require('./models/fs'); -var Parse = require('./parse'); +var extend = require('extend'); + +var common = require('./browser'); +var Output = require('./output'); var cli = require('./cli'); -module.exports = { - Book: Book, - Parse: Parse, - FS: FS, +module.exports = extend({ + Output: Output, commands: cli.commands -}; +}, common); diff --git a/lib/models/summary.js b/lib/models/summary.js index 079c53514..444c191f8 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -65,6 +65,17 @@ Summary.prototype.getByPath = function(filePath) { }); }; +/** + Return the first article + + @return {Article} +*/ +Summary.prototype.getFirstArticle = function() { + return this.getArticle(function(article) { + return true; + }); +}; + /** Render summary as text diff --git a/lib/modifiers/index.js b/lib/modifiers/index.js new file mode 100644 index 000000000..ed09e3188 --- /dev/null +++ b/lib/modifiers/index.js @@ -0,0 +1,4 @@ + +module.exports = { + Summary: require('./summary') +}; diff --git a/lib/modifiers/summary/editPartTitle.js b/lib/modifiers/summary/editPartTitle.js new file mode 100644 index 000000000..472399b78 --- /dev/null +++ b/lib/modifiers/summary/editPartTitle.js @@ -0,0 +1,24 @@ + +/** + Edit title of a part in the summary + + @param {Summary} summary + @param {Number} index + @param {String} newTitle + @return {Summary} +*/ +function editPartTitle(summary, index, newTitle) { + var parts = summary.getParts(); + + var part = parts.get(index); + if (!part) { + return summary; + } + + part = part.set('title', newTitle); + parts = parts.set(index, part); + + return summary.set('parts', parts); +} + +module.exports = editPartTitle; diff --git a/lib/modifiers/summary/index.js b/lib/modifiers/summary/index.js new file mode 100644 index 000000000..03f1098ca --- /dev/null +++ b/lib/modifiers/summary/index.js @@ -0,0 +1,7 @@ + +module.exports = { + insertArticle: require('./insertArticle'), + unshiftArticle: require('./unshiftArticle'), + + editPartTitle: require('./editPartTitle') +}; diff --git a/lib/modifiers/summary/indexLevels.js b/lib/modifiers/summary/indexLevels.js new file mode 100644 index 000000000..d29b04320 --- /dev/null +++ b/lib/modifiers/summary/indexLevels.js @@ -0,0 +1,13 @@ + +/** + Index all levels in the summary + + @param {Summary} + @return {Summary} +*/ +function indexLevels(summary) { + +} + + +module.exports = indexLevels; diff --git a/lib/modifiers/summary/insertArticle.js b/lib/modifiers/summary/insertArticle.js new file mode 100644 index 000000000..ec29978b8 --- /dev/null +++ b/lib/modifiers/summary/insertArticle.js @@ -0,0 +1,17 @@ +var SummaryArticle = require('../../models/summaryArticle'); + +/** + Insert an article in a summary, and reindex levels correctly + + @param {Summary} summary + @param {String} level: level to insert after + @param {Article} article + @return {Summary} +*/ +function insertArticle(summary, level, article) { + article = SummaryArticle(article); + + +} + +module.exports = insertArticle; diff --git a/lib/modifiers/summary/unshiftArticle.js b/lib/modifiers/summary/unshiftArticle.js new file mode 100644 index 000000000..6bb97c940 --- /dev/null +++ b/lib/modifiers/summary/unshiftArticle.js @@ -0,0 +1,28 @@ +var SummaryArticle = require('../../models/summaryArticle'); +var SummaryPart = require('../../models/summaryPart'); + +var indexLevels = require('./indexLevels'); + +/** + Insert an article at the + + @param {Summary} summary + @param {Article} article + @return {Summary} +*/ +function unshiftArticle(summary, article) { + article = SummaryArticle(article); + + var parts = summary.getParts(); + var part = parts.get(0) || SummaryPart(); + + var articles = part.getArticles(); + articles = articles.unshift(article); + + parts = parts.set(0, part); + summary = summary.set('parts', parts); + + return indexLevels(summary); +} + +module.exports = unshiftArticle; diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index fc344462f..6ac8d041a 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -1,14 +1,24 @@ +var Promise = require('../utils/promise'); + var parseStructureFile = require('./parseStructureFile'); var Summary = require('../models/summary'); +var SummaryModifier = require('../modifiers').Summary; /** - Parse summary in a book + Parse summary in a book, the summary can only be parsed + if the readme as be detected before. @param {Book} book @return {Promise} */ function parseSummary(book) { + var readme = book.getReadme(); var logger = book.getLogger(); + var readmeFile = readme.getFile(); + + if (!readmeFile.exists()) { + return Promise.reject(new Error('Summary parsing should be done after readme parsing')); + } return parseStructureFile(book, 'summary') .spread(function(file, result) { @@ -19,6 +29,20 @@ function parseSummary(book) { logger.debug.ln('summary file found at', file.getPath()); var summary = Summary.createFromParts(file, result.parts); + + + + // Insert readme as first entry + var firstArticle = summary.getFirstArticle(); + if (!firstArticle || firstArticle.getRef() !== readmeFile.getPath()) { + summary = SummaryModifier.unshiftArticle(summary, { + title: 'Introduction', + ref: readmeFile.getPath() + }); + } + + + // Set new summary return book.set('summary', summary); }); } From e62bc31e6363084fb3e138a5fcfd799105d353b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 11:17:33 +0200 Subject: [PATCH 070/118] Add method setSummary to book --- lib/models/book.js | 12 ++++++++++++ lib/parse/parseSummary.js | 5 +---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/models/book.js b/lib/models/book.js index bd7a0d05f..4dc0238a1 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -191,6 +191,18 @@ Book.prototype.addLanguageBook = function(language, book) { return this.set('books', books); }; + +/** + Set the summary for this book + + @param {Summary} + @return {Book} +*/ +Book.prototype.setSummary = function(summary) { + return this.set('summary', summary); +}; + + /** Change log level diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index 6ac8d041a..74df5633c 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -30,8 +30,6 @@ function parseSummary(book) { var summary = Summary.createFromParts(file, result.parts); - - // Insert readme as first entry var firstArticle = summary.getFirstArticle(); if (!firstArticle || firstArticle.getRef() !== readmeFile.getPath()) { @@ -41,9 +39,8 @@ function parseSummary(book) { }); } - // Set new summary - return book.set('summary', summary); + return book.setSummary(summary); }); } From 78e00db018f1f3e69a612fe8a7cf8787bdef4fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 11:35:30 +0200 Subject: [PATCH 071/118] Add tests for SummaryModifier.editPartTitle --- lib/models/__tests__/summary.js | 1 - lib/models/summary.js | 11 +++++ .../summary/__tests__/editPartTitle.js | 44 +++++++++++++++++++ lib/parse/parseSummary.js | 7 +-- 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 lib/modifiers/summary/__tests__/editPartTitle.js diff --git a/lib/models/__tests__/summary.js b/lib/models/__tests__/summary.js index e7b496e22..ad040cf17 100644 --- a/lib/models/__tests__/summary.js +++ b/lib/models/__tests__/summary.js @@ -1,4 +1,3 @@ -jest.autoMockOff(); describe('Summary', function() { var File = require('../file'); diff --git a/lib/models/summary.js b/lib/models/summary.js index 444c191f8..b04630f90 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -19,6 +19,17 @@ Summary.prototype.getParts = function() { return this.get('parts'); }; +/** + Return a part by its index + + @param {Number} + @return {Part} +*/ +Summary.prototype.getPart = function(i) { + var parts = this.getParts(); + return parts.get(i); +}; + /** Return an article using an iterator to find it. if "partIter" is set, it can also return a Part. diff --git a/lib/modifiers/summary/__tests__/editPartTitle.js b/lib/modifiers/summary/__tests__/editPartTitle.js new file mode 100644 index 000000000..d1b916b5e --- /dev/null +++ b/lib/modifiers/summary/__tests__/editPartTitle.js @@ -0,0 +1,44 @@ +var Summary = require('../../../models/summary'); +var File = require('../../../models/file'); + +describe('editPartTitle', function() { + var editPartTitle = require('../editPartTitle'); + var summary = Summary.createFromParts(File(), [ + { + articles: [ + { + title: 'My First Article', + path: 'README.md' + }, + { + title: 'My Second Article', + path: 'article.md' + } + ] + }, + { + title: 'Test' + } + ]); + + it('should correctly set title of first part', function() { + var newSummary = editPartTitle(summary, 0, 'Hello World'); + var part = newSummary.getPart(0); + + expect(part.getTitle()).toBe('Hello World'); + }); + + it('should correctly set title of second part', function() { + var newSummary = editPartTitle(summary, 1, 'Hello'); + var part = newSummary.getPart(1); + + expect(part.getTitle()).toBe('Hello'); + }); + + it('should not fail if part doesn\'t exist', function() { + var newSummary = editPartTitle(summary, 3, 'Hello'); + expect(newSummary.getParts().size).toBe(2); + }); +}); + + diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index 74df5633c..ebe47d208 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -16,10 +16,6 @@ function parseSummary(book) { var logger = book.getLogger(); var readmeFile = readme.getFile(); - if (!readmeFile.exists()) { - return Promise.reject(new Error('Summary parsing should be done after readme parsing')); - } - return parseStructureFile(book, 'summary') .spread(function(file, result) { if (!file) { @@ -32,7 +28,8 @@ function parseSummary(book) { // Insert readme as first entry var firstArticle = summary.getFirstArticle(); - if (!firstArticle || firstArticle.getRef() !== readmeFile.getPath()) { + if (readmeFile.exists() && + (!firstArticle || firstArticle.getRef() !== readmeFile.getPath())) { summary = SummaryModifier.unshiftArticle(summary, { title: 'Introduction', ref: readmeFile.getPath() From 3d405a1ac56e6c0e63276e0918ab8bd8fae82142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:03:58 +0200 Subject: [PATCH 072/118] Fix ref in summary when parsed without ref --- lib/models/summaryArticle.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/models/summaryArticle.js b/lib/models/summaryArticle.js index d2fac68c0..da8279040 100644 --- a/lib/models/summaryArticle.js +++ b/lib/models/summaryArticle.js @@ -49,6 +49,11 @@ SummaryArticle.prototype.getPath = function() { } var ref = this.getRef(); + if (!ref) { + return undefined; + } + + var parts = ref.split('#'); var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref); @@ -114,7 +119,7 @@ SummaryArticle.create = function(def, level) { return new SummaryArticle({ level: level, title: def.title, - ref: def.ref || def.path, + ref: def.ref || def.path || '', articles: Immutable.List(articles) }); }; From 5059e8df3466bf33c379f3ed0626f48904e9cbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:12:21 +0200 Subject: [PATCH 073/118] Add modifiers to edit article and insert in summary --- .../summary/__tests__/editArticle.js | 0 lib/modifiers/summary/editArticle.js | 70 +++++++++++++++++++ lib/modifiers/summary/editArticleTitle.js | 17 +++++ lib/modifiers/summary/index.js | 3 +- lib/modifiers/summary/indexArticleLevels.js | 23 ++++++ lib/modifiers/summary/indexLevels.js | 4 ++ lib/modifiers/summary/indexPartLevels.js | 24 +++++++ lib/modifiers/summary/insertArticle.js | 50 ++++++++++++- lib/parse/parseSummary.js | 2 - 9 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 lib/modifiers/summary/__tests__/editArticle.js create mode 100644 lib/modifiers/summary/editArticle.js create mode 100644 lib/modifiers/summary/editArticleTitle.js create mode 100644 lib/modifiers/summary/indexArticleLevels.js create mode 100644 lib/modifiers/summary/indexPartLevels.js diff --git a/lib/modifiers/summary/__tests__/editArticle.js b/lib/modifiers/summary/__tests__/editArticle.js new file mode 100644 index 000000000..e69de29bb diff --git a/lib/modifiers/summary/editArticle.js b/lib/modifiers/summary/editArticle.js new file mode 100644 index 000000000..162539884 --- /dev/null +++ b/lib/modifiers/summary/editArticle.js @@ -0,0 +1,70 @@ + +/** + Edit a list of articles + + @param {List
} articles + @param {String} level + @param {Article} newArticle + @return {List
} +*/ +function editArticleInList(articles, level, newArticle) { + return articles.map(function(article) { + var articleLevel = article.getLevel(); + + if (articleLevel == level) { + return article.merge(newArticle); + } + + if (level.indexOf(articleLevel) === 0) { + var articles = editArticleInList(article.getArticles(), level, newArticle); + return article.set('articles', articles); + } + + return article; + }); +} + + +/** + Edit an article in a part + + @param {Part} part + @param {String} level + @param {Article} newArticle + @return {Part} +*/ +function editArticleInPart(part, level, newArticle) { + var articles = part.getArticles(); + articles = editArticleInList(articles); + + return part.set('articles', articles); +} + + +/** + Edit an article in a summary + + @param {Summary} summary + @param {String} level + @param {Article} newArticle + @return {Summary} +*/ +function editArticle(summary, level, newArticle) { + var parts = summary.getParts(); + + var levelParts = level.split('.'); + var partIndex = Number(levelParts[0]); + + var part = parts.get(partIndex); + if (!part) { + return summary; + } + + part = editArticleInPart(part, level, newArticle); + parts = parts.set(partIndex, part); + + return summary.set('parts', parts); +} + + +module.exports = editArticle; diff --git a/lib/modifiers/summary/editArticleTitle.js b/lib/modifiers/summary/editArticleTitle.js new file mode 100644 index 000000000..bd9b6f26e --- /dev/null +++ b/lib/modifiers/summary/editArticleTitle.js @@ -0,0 +1,17 @@ +var editArticle = require('./editArticle'); + +/** + Edit title of an article + + @param {Summary} summary + @param {String} level + @param {String} newTitle + @return {Summary} +*/ +function editArticleTitle(summary, level, newTitle) { + return editArticle(summary, level, { + title: newTitle + }); +} + +module.exports = editArticleTitle; diff --git a/lib/modifiers/summary/index.js b/lib/modifiers/summary/index.js index 03f1098ca..855d7cc50 100644 --- a/lib/modifiers/summary/index.js +++ b/lib/modifiers/summary/index.js @@ -3,5 +3,6 @@ module.exports = { insertArticle: require('./insertArticle'), unshiftArticle: require('./unshiftArticle'), - editPartTitle: require('./editPartTitle') + editPartTitle: require('./editPartTitle'), + editArticleTitle: require('./editArticleTitle') }; diff --git a/lib/modifiers/summary/indexArticleLevels.js b/lib/modifiers/summary/indexArticleLevels.js new file mode 100644 index 000000000..02ca594af --- /dev/null +++ b/lib/modifiers/summary/indexArticleLevels.js @@ -0,0 +1,23 @@ + +/** + Index levels in an article tree + + @param {Article} + @param {String} baseLevel + @return {Article} +*/ +function indexArticleLevels(article, baseLevel) { + baseLevel = baseLevel || article.getLevel(); + var articles = article.getArticle(); + + articles = articles.map(function(inner, i) { + return indexArticleLevels(inner, baseLevel + '.' + (i + 1)); + }); + + return article.merge({ + level: baseLevel, + articles: articles + }); +} + +module.exports = indexArticleLevels; diff --git a/lib/modifiers/summary/indexLevels.js b/lib/modifiers/summary/indexLevels.js index d29b04320..604e9ff0d 100644 --- a/lib/modifiers/summary/indexLevels.js +++ b/lib/modifiers/summary/indexLevels.js @@ -1,3 +1,4 @@ +var indexPartLevels = require('./indexPartLevels'); /** Index all levels in the summary @@ -6,7 +7,10 @@ @return {Summary} */ function indexLevels(summary) { + var parts = summary.getParts(); + parts = parts.map(indexPartLevels); + return summary.set('parts', parts); } diff --git a/lib/modifiers/summary/indexPartLevels.js b/lib/modifiers/summary/indexPartLevels.js new file mode 100644 index 000000000..19ba08960 --- /dev/null +++ b/lib/modifiers/summary/indexPartLevels.js @@ -0,0 +1,24 @@ +var indexArticleLevels = require('./indexArticleLevels'); + +/** + Index levels in a part + + @param {Part} + @param {Number} index + @return {Part} +*/ +function indexPartLevels(part, index) { + var baseLevel = String(index + 1); + var articles = part.getArticle(); + + articles = articles.map(function(inner, i) { + return indexArticleLevels(inner, baseLevel + '.' + (i + 1)); + }); + + return part.merge({ + level: baseLevel, + articles: articles + }); +} + +module.exports = indexPartLevels; diff --git a/lib/modifiers/summary/insertArticle.js b/lib/modifiers/summary/insertArticle.js index ec29978b8..ae920c287 100644 --- a/lib/modifiers/summary/insertArticle.js +++ b/lib/modifiers/summary/insertArticle.js @@ -1,16 +1,62 @@ +var is = require('is'); var SummaryArticle = require('../../models/summaryArticle'); +var editArticle = require('./editArticle'); +var indexArticleLevels = require('./indexArticleLevels'); + /** - Insert an article in a summary, and reindex levels correctly + Get level of parent of an article + + @param {String} level + @return {String} +*/ +function getParentLevel(level) { + var parts = level.split('.'); + return parts.slice(0, -1).join('.'); +} + +/** + Insert an article in a summary at a specific position @param {Summary} summary - @param {String} level: level to insert after + @param {String|Article} level: level to insert after @param {Article} article @return {Summary} */ function insertArticle(summary, level, article) { article = SummaryArticle(article); + level = is.string(level)? level : level.getLevel(); + var parentLevel = getParentLevel(level); + + if (!parentLevel) { + // todo: insert new part + return summary; + } + + // Get parent of the position + var parentArticle = summary.getByLevel(parentLevel); + if (!parentLevel) { + return summary; + } + + // Find the index to insert at + var articles = parentArticle.getArticles(); + var index = articles.findIndex(function(art) { + return art.getLevel() === level; + }); + if (!index) { + return summary; + } + + // Insert the article at the right index + articles = articles.insert(index, article); + + // Reindex the level from here + parentArticle = parentArticle.set('articles', articles); + parentArticle = indexArticleLevels(parentArticle); + + return editArticle(summary, parentLevel, parentArticle); } diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index ebe47d208..0c07ac76e 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -1,5 +1,3 @@ -var Promise = require('../utils/promise'); - var parseStructureFile = require('./parseStructureFile'); var Summary = require('../models/summary'); var SummaryModifier = require('../modifiers').Summary; From 14fd6bbdce87a8b4c60a9dc571608428d738aafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:16:15 +0200 Subject: [PATCH 074/118] Add tests for PathUtils --- lib/utils/__tests__/path.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/utils/__tests__/path.js diff --git a/lib/utils/__tests__/path.js b/lib/utils/__tests__/path.js new file mode 100644 index 000000000..22bb01695 --- /dev/null +++ b/lib/utils/__tests__/path.js @@ -0,0 +1,17 @@ +var path = require('path'); + +describe('Paths', function() { + var PathUtils = require('..//path'); + + describe('setExtension', function() { + it('should correctly change extension of filename', function() { + expect(PathUtils.setExtension('test.md', '.html')).toBe('test.html'); + expect(PathUtils.setExtension('test.md', '.json')).toBe('test.json'); + }); + + it('should correctly change extension of path', function() { + expect(PathUtils.setExtension('hello/test.md', '.html')).toBe(path.normalize('hello/test.html')); + expect(PathUtils.setExtension('hello/test.md', '.json')).toBe(path.normalize('hello/test.json')); + }); + }); +}); From f946650ddb6b075aa626cc165b7b9ea53924a5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:28:41 +0200 Subject: [PATCH 075/118] Add "next" and "previous" to page JSON repr --- lib/json/encodePage.js | 13 ++++++++-- lib/json/encodeSummaryArticle.js | 12 +++++++--- lib/models/summary.js | 41 ++++++++++++++++++++++++++++++++ lib/output/callPageHook.js | 2 -- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/lib/json/encodePage.js b/lib/json/encodePage.js index d876c784e..be92117fe 100644 --- a/lib/json/encodePage.js +++ b/lib/json/encodePage.js @@ -1,3 +1,5 @@ +var encodeSummaryArticle = require('./encodeSummaryArticle'); + /** Return a JSON representation of a page @@ -10,7 +12,6 @@ function encodePage(page, summary) { var attributes = page.getAttributes(); var article = summary.getByPath(file.getPath()); - var result = attributes.toJS(); if (article) { @@ -18,7 +19,15 @@ function encodePage(page, summary) { result.level = article.getLevel(); result.depth = article.getDepth(); - // todo: next and prev + var nextArticle = summary.getNextArticle(article); + if (nextArticle) { + result.next = encodeSummaryArticle(nextArticle); + } + + var prevArticle = summary.getPrevArticle(article); + if (prevArticle) { + result.previous = encodeSummaryArticle(prevArticle); + } } result.content = page.getContent(); diff --git a/lib/json/encodeSummaryArticle.js b/lib/json/encodeSummaryArticle.js index b3f977afe..987e44a59 100644 --- a/lib/json/encodeSummaryArticle.js +++ b/lib/json/encodeSummaryArticle.js @@ -5,7 +5,14 @@ @param {SummaryArticle} @return {Object} */ -function encodeSummaryArticle(article) { +function encodeSummaryArticle(article, recursive) { + var articles = undefined; + if (recursive !== false) { + articles = article.getArticles() + .map(encodeSummaryArticle) + .toJS(); + } + return { title: article.getTitle(), level: article.getLevel(), @@ -13,8 +20,7 @@ function encodeSummaryArticle(article) { anchor: article.getAnchor(), url: article.getUrl(), path: article.getPath(), - articles: article.getArticles() - .map(encodeSummaryArticle).toJS() + articles: articles }; } diff --git a/lib/models/summary.js b/lib/models/summary.js index b04630f90..3b4694111 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -1,3 +1,4 @@ +var is = require('is'); var Immutable = require('immutable'); var error = require('../utils/error'); @@ -87,6 +88,46 @@ Summary.prototype.getFirstArticle = function() { }); }; +/** + Return next article of an article + + @param {Article} current + @return {Article} +*/ +Summary.prototype.getNextArticle = function(current) { + var level = is.string(current)? current : current.getLevel(); + var wasPrev = false; + + return this.getArticle(function(article) { + if (wasPrev) return true; + + wasPrev = article.getLevel() == level; + return false; + }); +}; + +/** + Return previous article of an article + + @param {Article} current + @return {Article} +*/ +Summary.prototype.getPrevArticle = function(current) { + var level = is.string(current)? current : current.getLevel(); + var prev = undefined; + + this.getArticle(function(article) { + if (article.getLevel() == level) { + return true; + } + + prev = article; + return false; + }); + + return prev; +}; + /** Render summary as text diff --git a/lib/output/callPageHook.js b/lib/output/callPageHook.js index 0582da95c..c66cef02f 100644 --- a/lib/output/callPageHook.js +++ b/lib/output/callPageHook.js @@ -1,8 +1,6 @@ var Api = require('../api'); var callHook = require('./callHook'); -var Promise = require('../utils/promise'); - /** Call a hook for a specific page From f08732095dca8c4b2913c20a4b8b6fd593f8c9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:37:19 +0200 Subject: [PATCH 076/118] Copy readme.json from main language for json output of multilingual --- lib/json/encodeBook.js | 1 + lib/output/json/onFinish.js | 21 +++++++++++++++++++-- lib/output/json/onPage.js | 8 ++++++++ lib/output/website/onPage.js | 2 -- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index 3bbbe0fd0..336b12a33 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -15,6 +15,7 @@ function encodeBookToJson(book) { summary: encodeSummary(book.getSummary()), glossary: encodeGlossary(book.getGlossary()), readme: encodeReadme(book.getReadme()), + config: book.getCOnfig().getValues().toJS(), gitbook: { version: gitbook.version, time: gitbook.START_TIME diff --git a/lib/output/json/onFinish.js b/lib/output/json/onFinish.js index 107285aec..ff336a2b0 100644 --- a/lib/output/json/onFinish.js +++ b/lib/output/json/onFinish.js @@ -1,4 +1,7 @@ +var path = require('path'); + var Promise = require('../../utils/promise'); +var fs = require('../../utils/fs'); /** Finish the generation @@ -7,9 +10,23 @@ var Promise = require('../../utils/promise'); @return {Output} */ function onFinish(output) { - // todo: copy README.json from main language + var book = output.getBook(); + var outputRoot = output.getRoot(); - return Promise(output); + if (!book.isMultilingual()) { + return Promise(output); + } + + // Get main language + var languages = book.getLanguages(); + var mainLanguage = languages.getDefaultLanguage(); + + // Copy README.json from it + return fs.copy( + path.resolve(outputRoot, mainLanguage.getID(), 'README.json'), + path.resolve(outputRoot, 'README.json') + ) + .thenResolve(output); } module.exports = onFinish; diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index 54179c624..c2ee0d08b 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -4,6 +4,8 @@ var Modifiers = require('../modifiers'); var writeFile = require('../helper/writeFile'); var getModifiers = require('../getModifiers'); +var JSON_VERSION = '3'; + /** Write a page as a json file @@ -18,6 +20,12 @@ function onPage(output, page) { // Generate the JSON var json = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); + // Delete some private properties + delete json.config; + + // Specify JSON output version + json.version = JSON_VERSION; + // File path in the output folder var filePath = PathUtils.setExtension(file.getPath(), '.json'); diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index 751a4307e..1e9a1319e 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -38,8 +38,6 @@ function onPage(output, page) { .then(function(resultPage) { // Generate the context var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage); - - context.config = config.getValues().toJS(); context.plugins = { resources: Plugins.listResources(plugins, prefix).toJS() }; From 94db0f6ec9e2487a77572fd70931cf10a4dff54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:39:11 +0200 Subject: [PATCH 077/118] Fix typo in JSONUtils.encodeBook --- lib/json/encodeBook.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index 336b12a33..ba199994e 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -15,7 +15,7 @@ function encodeBookToJson(book) { summary: encodeSummary(book.getSummary()), glossary: encodeGlossary(book.getGlossary()), readme: encodeReadme(book.getReadme()), - config: book.getCOnfig().getValues().toJS(), + config: book.getConfig().getValues().toJS(), gitbook: { version: gitbook.version, time: gitbook.START_TIME From f3058598dce590479d1f80ffbf486333e79fd5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 14:42:07 +0200 Subject: [PATCH 078/118] Always generate a README.json as introduction of json output --- lib/output/json/onPage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/output/json/onPage.js b/lib/output/json/onPage.js index c2ee0d08b..fece5403b 100644 --- a/lib/output/json/onPage.js +++ b/lib/output/json/onPage.js @@ -14,6 +14,7 @@ var JSON_VERSION = '3'; */ function onPage(output, page) { var file = page.getFile(); + var readme = output.getBook().getReadme().getFile(); return Modifiers.modifyHTML(page, getModifiers(output, page)) .then(function(resultPage) { @@ -27,7 +28,8 @@ function onPage(output, page) { json.version = JSON_VERSION; // File path in the output folder - var filePath = PathUtils.setExtension(file.getPath(), '.json'); + var filePath = file.getPath() == readme.getPath()? 'README.json' : file.getPath(); + filePath = PathUtils.setExtension(filePath, '.json'); // Write it to the disk return writeFile( From 1360a744a72fc84dacde22283a8dc2687ff6c3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 15:02:24 +0200 Subject: [PATCH 079/118] Export more models --- lib/browser.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/browser.js b/lib/browser.js index c2445308f..745a54489 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -1,14 +1,13 @@ -var Book = require('./models/book'); -var FS = require('./models/fs'); -var Parse = require('./parse'); var Modifiers = require('./modifiers'); module.exports = { - Parse: Parse, + Parse: require('./parse'), // Models - Book: Book, - FS: FS, + Book: require('./models/book'), + FS: require('./models/fs'), + Summary: require('./models/summary'), + Glossary: require('./models/glossary'), // Modifiers SummaryModifier: Modifiers.Summary From cb3ee423c976c93d35e7e7c8722787bd69cb747e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 15:20:25 +0200 Subject: [PATCH 080/118] Add basic tests for json generator --- lib/modifiers/summary/indexArticleLevels.js | 2 +- lib/modifiers/summary/indexPartLevels.js | 2 +- lib/modifiers/summary/unshiftArticle.js | 1 + lib/output/__tests__/json.js | 29 ++++++++++++++++++ lib/output/generateMock.js | 33 +++++++++++++++++++++ lib/output/getModifiers.js | 2 +- lib/parse/parseSummary.js | 10 ++++--- 7 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 lib/output/__tests__/json.js create mode 100644 lib/output/generateMock.js diff --git a/lib/modifiers/summary/indexArticleLevels.js b/lib/modifiers/summary/indexArticleLevels.js index 02ca594af..f311f7408 100644 --- a/lib/modifiers/summary/indexArticleLevels.js +++ b/lib/modifiers/summary/indexArticleLevels.js @@ -8,7 +8,7 @@ */ function indexArticleLevels(article, baseLevel) { baseLevel = baseLevel || article.getLevel(); - var articles = article.getArticle(); + var articles = article.getArticles(); articles = articles.map(function(inner, i) { return indexArticleLevels(inner, baseLevel + '.' + (i + 1)); diff --git a/lib/modifiers/summary/indexPartLevels.js b/lib/modifiers/summary/indexPartLevels.js index 19ba08960..d19c70a16 100644 --- a/lib/modifiers/summary/indexPartLevels.js +++ b/lib/modifiers/summary/indexPartLevels.js @@ -9,7 +9,7 @@ var indexArticleLevels = require('./indexArticleLevels'); */ function indexPartLevels(part, index) { var baseLevel = String(index + 1); - var articles = part.getArticle(); + var articles = part.getArticles(); articles = articles.map(function(inner, i) { return indexArticleLevels(inner, baseLevel + '.' + (i + 1)); diff --git a/lib/modifiers/summary/unshiftArticle.js b/lib/modifiers/summary/unshiftArticle.js index 6bb97c940..3f2ae4df0 100644 --- a/lib/modifiers/summary/unshiftArticle.js +++ b/lib/modifiers/summary/unshiftArticle.js @@ -18,6 +18,7 @@ function unshiftArticle(summary, article) { var articles = part.getArticles(); articles = articles.unshift(article); + part = part.set('articles', articles); parts = parts.set(0, part); summary = summary.set('parts', parts); diff --git a/lib/output/__tests__/json.js b/lib/output/__tests__/json.js new file mode 100644 index 000000000..94a036234 --- /dev/null +++ b/lib/output/__tests__/json.js @@ -0,0 +1,29 @@ +var generateMock = require('../generateMock'); +var JSONGenerator = require('../json'); + +describe('JSONGenerator', function() { + + pit('should generate a README.json', function() { + return generateMock(JSONGenerator, { + 'README.md': 'Hello World' + }) + .then(function(folder) { + expect(folder).toHaveFile('README.json'); + }); + }); + + pit('should generate a json file for each articles', function() { + return generateMock(JSONGenerator, { + 'README.md': 'Hello World', + 'SUMMARY.md': '# Summary\n\n* [Page](test/page.md)', + 'test': { + 'page.md': 'Hello 2' + } + }) + .then(function(folder) { + expect(folder).toHaveFile('README.json'); + expect(folder).toHaveFile('test/page.json'); + }); + }); +}); + diff --git a/lib/output/generateMock.js b/lib/output/generateMock.js new file mode 100644 index 000000000..b779e32d1 --- /dev/null +++ b/lib/output/generateMock.js @@ -0,0 +1,33 @@ +var tmp = require('tmp'); + +var Book = require('../models/book'); +var createMockFS = require('../fs/mock'); +var parseBook = require('../parse/parseBook'); +var generateBook = require('./generateBook'); + + +/** + Generate a book using JSON generator + And returns the path to the output dir. + + FOR TESTING PURPOSE ONLY + + @param {Generator} + @param {Map} files + @return {Promise} +*/ +function generateMock(Generator, files) { + var fs = createMockFS(files); + var book = Book.createForFS(fs); + var dir = tmp.dirSync(); + + return parseBook(book) + .then(function(resultBook) { + return generateBook(Generator, resultBook, { + root: dir.name + }); + }) + .thenResolve(dir.name); +} + +module.exports = generateMock; diff --git a/lib/output/getModifiers.js b/lib/output/getModifiers.js index 23cfb1cf6..e649df680 100644 --- a/lib/output/getModifiers.js +++ b/lib/output/getModifiers.js @@ -11,7 +11,7 @@ var CODEBLOCK = 'code'; Return default modifier to prepare a page for rendering. - @return + @return {Array} */ function getModifiers(output, page) { var book = output.getBook(); diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index 0c07ac76e..2c6186fa9 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -16,13 +16,15 @@ function parseSummary(book) { return parseStructureFile(book, 'summary') .spread(function(file, result) { + var summary; + if (!file) { logger.warn.ln('no summary file in this book'); - return book; + summary = Summary(); + } else { + logger.debug.ln('summary file found at', file.getPath()); + summary = Summary.createFromParts(file, result.parts); } - logger.debug.ln('summary file found at', file.getPath()); - - var summary = Summary.createFromParts(file, result.parts); // Insert readme as first entry var firstArticle = summary.getFirstArticle(); From b4072a06c4846219ad391956e06382f0b5c20e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 15:28:46 +0200 Subject: [PATCH 081/118] Add basic tests for website generator --- lib/output/__tests__/website.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/output/__tests__/website.js diff --git a/lib/output/__tests__/website.js b/lib/output/__tests__/website.js new file mode 100644 index 000000000..7afa737db --- /dev/null +++ b/lib/output/__tests__/website.js @@ -0,0 +1,29 @@ +var generateMock = require('../generateMock'); +var WebsiteGenerator = require('../website'); + +describe('WebsiteGenerator', function() { + + pit('should generate an index.html', function() { + return generateMock(WebsiteGenerator, { + 'README.md': 'Hello World' + }) + .then(function(folder) { + expect(folder).toHaveFile('index.html'); + }); + }); + + pit('should generate an HTML file for each articles', function() { + return generateMock(WebsiteGenerator, { + 'README.md': 'Hello World', + 'SUMMARY.md': '# Summary\n\n* [Page](test/page.md)', + 'test': { + 'page.md': 'Hello 2' + } + }) + .then(function(folder) { + expect(folder).toHaveFile('index.html'); + expect(folder).toHaveFile('test/page.html'); + }); + }); +}); + From 281f6d55354ee097d3a5f59f02c09ac469e47908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 15:36:54 +0200 Subject: [PATCH 082/118] Extend timeout for tests --- jest/customMatchers.js | 1 + lib/cli/getBook.js | 2 +- lib/models/book.js | 7 +++---- lib/output/generateMock.js | 2 ++ lib/plugins/__tests__/findInstalled.js | 3 --- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index a0dd7f96b..4dd4f68e6 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -20,5 +20,6 @@ var matchers = { }; jasmine.getEnv().beforeEach(function () { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; jasmine.addMatchers(matchers); }); \ No newline at end of file diff --git a/lib/cli/getBook.js b/lib/cli/getBook.js index 9038669eb..ac82187fb 100644 --- a/lib/cli/getBook.js +++ b/lib/cli/getBook.js @@ -17,7 +17,7 @@ function getBook(args, kwargs) { var fs = createNodeFS(input); var book = Book.createForFS(fs); - return Book.setLogLevel(book, logLevel); + return book.setLogLevel(logLevel); } module.exports = getBook; diff --git a/lib/models/book.js b/lib/models/book.js index 4dc0238a1..be055d765 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -206,13 +206,12 @@ Book.prototype.setSummary = function(summary) { /** Change log level - @param {Book} book @param {String} level @return {Book} */ -Book.setLogLevel = function setLogLevel(book, level) { - book.getLogger().setLevel(level); - return book; +Book.prototype.setLogLevel = function(level) { + this.getLogger().setLevel(level); + return this; }; /** diff --git a/lib/output/generateMock.js b/lib/output/generateMock.js index b779e32d1..47d29dc6a 100644 --- a/lib/output/generateMock.js +++ b/lib/output/generateMock.js @@ -21,6 +21,8 @@ function generateMock(Generator, files) { var book = Book.createForFS(fs); var dir = tmp.dirSync(); + book = book.setLogLevel('disabled'); + return parseBook(book) .then(function(resultBook) { return generateBook(Generator, resultBook, { diff --git a/lib/plugins/__tests__/findInstalled.js b/lib/plugins/__tests__/findInstalled.js index ce9da0aef..93912d35d 100644 --- a/lib/plugins/__tests__/findInstalled.js +++ b/lib/plugins/__tests__/findInstalled.js @@ -1,6 +1,3 @@ -jest.autoMockOff(); -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - var path = require('path'); describe('findInstalled', function() { From 7ae36f16c84238340dd6b39a75423f5e95f27bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 28 Apr 2016 15:45:25 +0200 Subject: [PATCH 083/118] Add base for deprecated page.naviation --- lib/api/encodeNavigation.js | 14 ++++++++++++++ lib/api/encodePage.js | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 lib/api/encodeNavigation.js diff --git a/lib/api/encodeNavigation.js b/lib/api/encodeNavigation.js new file mode 100644 index 000000000..fa9eb1665 --- /dev/null +++ b/lib/api/encodeNavigation.js @@ -0,0 +1,14 @@ + + +/** + page.navigation is a deprecated property from GitBook v2 + + @param {Output} + @return {Object} +*/ +function encodeNavigation(output) { + // todo + return {}; +} + +module.exports = encodeNavigation; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index 1f3a01b44..b212c291c 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -1,5 +1,6 @@ var JSONUtils = require('../json'); var deprecate = require('./deprecate'); +var encodeNavigation = require('./encodeNavigation'); /** Encode a page in a context to a JS API @@ -21,6 +22,14 @@ function encodePage(output, page) { result.path = file.getPath(); result.rawPath = fs.resolve(result.path); + // todo: (as deprecated) + // page.progress + // page.navigation + + // Deprecated properties + deprecate.field(output, 'page.navigation', result, 'sections', encodeNavigation(output), + '"navigation" property is deprecated'); + deprecate.field(output, 'page.sections', result, 'sections', [ { content: result.content, From 826f90505c1a70ad2a8cf3715bac6d5efaeba22c Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Thu, 28 Apr 2016 21:25:58 +0200 Subject: [PATCH 084/118] Add command "init" --- lib/__tests__/gitbook.js | 2 - lib/__tests__/init.js | 16 +++++ lib/api/encodeGlobal.js | 2 +- lib/cli/index.js | 5 +- lib/cli/init.js | 17 +++++ lib/index.js | 7 +- lib/init.js | 117 ++++++++++++++++++--------------- lib/models/book.js | 10 ++- lib/models/file.js | 11 ++++ lib/models/readme.js | 6 +- lib/models/summary.js | 15 +++++ lib/output/helper/writeFile.js | 2 +- lib/output/website/onAsset.js | 2 +- lib/utils/fs.js | 2 +- 14 files changed, 149 insertions(+), 65 deletions(-) create mode 100644 lib/__tests__/init.js create mode 100644 lib/cli/init.js diff --git a/lib/__tests__/gitbook.js b/lib/__tests__/gitbook.js index b45fe705c..c3669bb96 100644 --- a/lib/__tests__/gitbook.js +++ b/lib/__tests__/gitbook.js @@ -1,5 +1,3 @@ -jest.autoMockOff(); - var gitbook = require('../gitbook'); describe('satisfies', function() { diff --git a/lib/__tests__/init.js b/lib/__tests__/init.js new file mode 100644 index 000000000..5665cf1e0 --- /dev/null +++ b/lib/__tests__/init.js @@ -0,0 +1,16 @@ +var tmp = require('tmp'); +var initBook = require('../init'); + +describe('initBook', function() { + + pit('should create a README and SUMMARY for empty book', function() { + var dir = tmp.dirSync(); + + return initBook(dir.name) + .then(function() { + expect(dir.name).toHaveFile('README.md'); + expect(dir.name).toHaveFile('SUMMARY.md'); + }); + }); + +}); diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index fa336962b..bbd693b0c 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -70,7 +70,7 @@ function encodeGlobal(output) { .then(function() { var filePath = PathUtils.resolveInRoot(outputFolder, fileName); - return fs.ensure(filePath) + return fs.ensureFile(filePath) .then(function() { return fs.writeFile(filePath, content); }); diff --git a/lib/cli/index.js b/lib/cli/index.js index b93b7b106..4afb090ba 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,9 +1,10 @@ module.exports = { commands: [ - require('./parse'), require('./build'), require('./serve'), - require('./install') + require('./install'), + require('./parse'), + require('./init') ] }; diff --git a/lib/cli/init.js b/lib/cli/init.js new file mode 100644 index 000000000..9a1bff81d --- /dev/null +++ b/lib/cli/init.js @@ -0,0 +1,17 @@ +var path = require('path'); + +var options = require('./options'); +var initBook = require('../init'); + +module.exports = { + name: 'install [book]', + description: 'setup and create files for chapters', + options: [ + options.log + ], + exec: function(args, kwargs) { + var bookRoot = path.resolve(process.cwd(), args[0] || './'); + + return initBook(bookRoot); + } +}; diff --git a/lib/index.js b/lib/index.js index ffaea258d..0a76e08b4 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,8 +3,11 @@ var extend = require('extend'); var common = require('./browser'); var Output = require('./output'); var cli = require('./cli'); +var initBook = require('./initBook'); module.exports = extend({ - Output: Output, - commands: cli.commands + initBook: initBook, + createNodeFS: require('./fs/node'), + Output: Output, + commands: cli.commands }, common); diff --git a/lib/init.js b/lib/init.js index b7bb7f501..3e3cdca08 100644 --- a/lib/init.js +++ b/lib/init.js @@ -1,66 +1,79 @@ var path = require('path'); +var createNodeFS = require('./fs/node'); var fs = require('./utils/fs'); var Promise = require('./utils/promise'); +var File = require('./models/file'); +var Readme = require('./models/readme'); +var Book = require('./models/book'); +var Parse = require('./parse'); -// Initialize folder structure for a book -// Read SUMMARY to created the right chapter -function initBook(book) { - var extensionToUse = '.md'; +/** + Initialize folder structure for a book + Read SUMMARY to created the right chapter - book.log.info.ln('init book at', book.root); - return fs.mkdirp(book.root) + @param {Book} + @param {String} + @return {Promise} +*/ +function initBook(rootFolder) { + var extension = '.md'; + + return fs.mkdirp(rootFolder) + + // Parse the summary and readme .then(function() { - return book.config.load(); - }) - .then(function() { - book.log.info.ln('detect structure from SUMMARY (if it exists)'); - return book.summary.load(); - }) - .then(function() { - var summary = book.summary.path || 'SUMMARY.md'; - var articles = book.summary.flatten(); + var fs = createNodeFS(rootFolder); + var book = Book.createForFS(fs); - // Use extension of summary - extensionToUse = path.extname(summary); + return Parse.parseReadme(book) - // Readme doesn't have a path - if (!articles[0].path) { - articles[0].path = 'README' + extensionToUse; - } - - // Summary doesn't exists? create one - if (!book.summary.path) { - articles.push({ - title: 'Summary', - path: 'SUMMARY'+extensionToUse - }); - } - - // Create files that don't exist - return Promise.serie(articles, function(article) { - if (!article.path) return; - - var absolutePath = book.resolve(article.path); - - return fs.exists(absolutePath) - .then(function(exists) { - if(exists) { - book.log.info.ln('found', article.path); - return; - } else { - book.log.info.ln('create', article.path); - } - - return fs.mkdirp(path.dirname(absolutePath)) - .then(function() { - return fs.writeFile(absolutePath, '# '+article.title+'\n\n'); - }); - }); + // Setup default readme if doesn't found one + .fail(function() { + var readmeFile = File.createWithFilepath('README' + extension); + var readme = Readme.create(readmeFile); + return book.setReadme(readme); }); }) - .then(function() { - book.log.info.ln('initialization is finished'); + .then(Parse.parseSummary) + + .then(function(book) { + var logger = book.getLogger(); + var summary = book.getSummary(); + var summaryFile = summary.getFile(); + var summaryFilename = summaryFile.getPath() || ('SUMMARY' + extension); + + var articles = summary.getArticlesAsList(); + + // Write pages + return Promise.forEach(articles, function(article) { + var filePath = path.join(rootFolder, article.getPath()); + if (!filePath) return; + + return fs.assertFile(filePath, function() { + return fs.ensureFile(filePath) + .then(function() { + logger.info.ln('create', article.getPath()); + return fs.writeFile(filePath, '# ' + article.getTitle() + '\n\n'); + }); + }); + }) + + // Write summary + .then(function() { + var filePath = path.join(rootFolder, summaryFilename); + + return fs.ensureFile(filePath) + .then(function() { + logger.info.ln('create ' + path.basename(filePath)); + return fs.writeFile(filePath, summary.toText(extension)); + }); + }) + + // Log end + .then(function() { + logger.info.ln('initialization is finished'); + }); }); } diff --git a/lib/models/book.js b/lib/models/book.js index be055d765..030223f3b 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -191,7 +191,6 @@ Book.prototype.addLanguageBook = function(language, book) { return this.set('books', books); }; - /** Set the summary for this book @@ -202,6 +201,15 @@ Book.prototype.setSummary = function(summary) { return this.set('summary', summary); }; +/** + Set the readme for this book + + @param {Readme} + @return {Book} +*/ +Book.prototype.setReadme = function(readme) { + return this.set('readme', readme); +}; /** Change log level diff --git a/lib/models/file.js b/lib/models/file.js index ff7b899cc..d1726a7cd 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -74,5 +74,16 @@ File.createFromStat = function createFromStat(filepath, stat) { }); }; +/** + Create a file with only a path + + @param {String} filepath + @return {File} +*/ +File.createWithFilepath = function createWithFilepath(filepath) { + return new File({ + path: filepath + }); +}; module.exports = File; diff --git a/lib/models/readme.js b/lib/models/readme.js index 4ad399277..c655c8218 100644 --- a/lib/models/readme.js +++ b/lib/models/readme.js @@ -28,10 +28,12 @@ Readme.prototype.getDescription = function() { @return {Readme} */ Readme.create = function(file, def) { + def = def || {}; + return new Readme({ file: file, - title: def.title, - description: def.description + title: def.title || '', + description: def.description || '' }); }; diff --git a/lib/models/summary.js b/lib/models/summary.js index 3b4694111..5baebef37 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -150,6 +150,21 @@ Summary.prototype.toText = function(parser) { }); }; +/** + Return all articles as a list + + @return {List
} +*/ +Summary.prototype.getArticlesAsList = function() { + var accu = []; + + this.getArticle(function(article) { + accu.push(article); + }); + + return Immutable.List(accu); +}; + /** Create a new summary for a list of parts diff --git a/lib/output/helper/writeFile.js b/lib/output/helper/writeFile.js index 3e2261d2a..a6d4645ae 100644 --- a/lib/output/helper/writeFile.js +++ b/lib/output/helper/writeFile.js @@ -13,7 +13,7 @@ function writeFile(output, filePath, content) { var rootFolder = output.getRoot(); filePath = path.join(rootFolder, filePath); - return fs.ensure(filePath) + return fs.ensureFile(filePath) .then(function() { return fs.writeFile(filePath, content); }) diff --git a/lib/output/website/onAsset.js b/lib/output/website/onAsset.js index 1311c9531..17b6ba78d 100644 --- a/lib/output/website/onAsset.js +++ b/lib/output/website/onAsset.js @@ -17,7 +17,7 @@ function onAsset(output, asset) { var filePath = path.resolve(rootFolder, asset); var outputPath = path.resolve(outputFolder, asset); - return fs.ensure(outputPath) + return fs.ensureFile(outputPath) .then(function() { return fs.copy(filePath, outputPath); }) diff --git a/lib/utils/fs.js b/lib/utils/fs.js index 008dd4018..be3ec0ef7 100644 --- a/lib/utils/fs.js +++ b/lib/utils/fs.js @@ -130,6 +130,6 @@ module.exports = { tmpDir: genTmpDir, download: download, uniqueFilename: uniqueFilename, - ensure: ensureFile, + ensureFile: ensureFile, rmDir: rmDir }; From 68b9bf7e38867eed8763854a731f15959aaf65b5 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Thu, 28 Apr 2016 22:45:50 +0200 Subject: [PATCH 085/118] Encode navigation for compatibility with gitbook v2 --- lib/api/encodeGlobal.js | 6 +++++ lib/api/encodeNavigation.js | 52 ++++++++++++++++++++++++++++++++++--- lib/api/encodePage.js | 8 +++--- lib/api/encodeProgress.js | 16 ++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 lib/api/encodeProgress.js diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index bbd693b0c..78919d3a9 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -4,6 +4,7 @@ var PathUtils = require('../utils/path'); var deprecate = require('./deprecate'); var encodeConfig = require('./encodeConfig'); +var encodeNavigation = require('./encodeNavigation'); var fileToURL = require('../output/helper/fileToURL'); /** @@ -79,6 +80,11 @@ function encodeGlobal(output) { } }; + // Deprecated properties + + deprecate.field(output, 'this.navigation', result, 'sections', encodeNavigation(output), + '"navigation" property is deprecated'); + deprecate.field(output, 'this.book', result, 'book', result, '"book" property is deprecated, use "this" directly instead'); diff --git a/lib/api/encodeNavigation.js b/lib/api/encodeNavigation.js index fa9eb1665..c48ba5011 100644 --- a/lib/api/encodeNavigation.js +++ b/lib/api/encodeNavigation.js @@ -1,14 +1,58 @@ - /** - page.navigation is a deprecated property from GitBook v2 + Encode an article for next/prev + + @param {Map} + @param {Article} + @return {Object} +*/ +function encodeArticle(pages, article) { + var articlePath = article.getPath(); + + return { + path: articlePath, + title: article.getTitle(), + level: article.getLevel(), + exists: (articlePath && pages.has(articlePath)), + external: article.isExternal() + }; +} + +/** + this.navigation is a deprecated property from GitBook v2 @param {Output} @return {Object} */ function encodeNavigation(output) { - // todo - return {}; + var book = output.getBook(); + var summary = book.getSummary(); + var articles = summary.getArticlesAsList(); + + return articles + .map(function(article, i) { + var ref = article.getRef(); + if (ref) { + return undefined; + } + + var prev = articles.get(i - 1); + var next = articles.get(i + 1); + + return [ + ref, + { + index: i, + title: article.getTitle(), + introduction: (i === 0), + prev: prev? encodeArticle(prev) : undefined, + next: next? encodeArticle(next) : undefined, + level: article.getLevel() + } + ]; + }) + .toMap() + .toJS(); } module.exports = encodeNavigation; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index b212c291c..1465137b6 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -1,6 +1,6 @@ var JSONUtils = require('../json'); var deprecate = require('./deprecate'); -var encodeNavigation = require('./encodeNavigation'); +var encodeProgress = require('./encodeProgress'); /** Encode a page in a context to a JS API @@ -24,11 +24,9 @@ function encodePage(output, page) { // todo: (as deprecated) // page.progress - // page.navigation - // Deprecated properties - deprecate.field(output, 'page.navigation', result, 'sections', encodeNavigation(output), - '"navigation" property is deprecated'); + deprecate.field(output, 'page.progress', result, 'sections', encodeProgress(output, page), + '"page.progress" property is deprecated'); deprecate.field(output, 'page.sections', result, 'sections', [ { diff --git a/lib/api/encodeProgress.js b/lib/api/encodeProgress.js new file mode 100644 index 000000000..f32c8d8cd --- /dev/null +++ b/lib/api/encodeProgress.js @@ -0,0 +1,16 @@ + + +/** + page.progress is a deprecated property from GitBook v2 + + @param {Output} + @param {Page} + @return {Object} +*/ +function encodeProgress(output, page) { + // todo + return {}; +} + +module.exports = encodeProgress; + From 93e701f4712ab9e476061f8f81fb03eda1c2af2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 10:17:16 +0200 Subject: [PATCH 086/118] Complete deprecated page.progress and this.navigation --- lib/api/deprecate.js | 23 ++++++++++++--- lib/api/encodeGlobal.js | 5 ++-- lib/api/encodeNavigation.js | 18 ++++++++---- lib/api/encodePage.js | 8 ++--- lib/api/encodeProgress.js | 58 +++++++++++++++++++++++++++++++++++-- lib/cli/index.js | 16 +++++----- lib/index.js | 9 ++---- 7 files changed, 102 insertions(+), 35 deletions(-) diff --git a/lib/api/deprecate.js b/lib/api/deprecate.js index 73ca8bef5..fd9280f06 100644 --- a/lib/api/deprecate.js +++ b/lib/api/deprecate.js @@ -1,3 +1,5 @@ +var is = require('is'); + var logged = {}; var disabled = {}; @@ -40,19 +42,32 @@ function deprecateMethod(book, key, fn, msg) { @param {Book|Output} book @param {String} key: unique identitifer for the deprecated @param {Object} instance - @param {String} property + @param {String|Function} property @param {String} msg: message to print when called @return {Function} */ function deprecateField(book, key, instance, property, value, msg) { + var store = undefined; + + var prepare = function() { + if (!is.undefined(store)) return; + + if (is.fn(value)) store = value(); + else store = value; + } + var getter = function(){ + prepare(); + logNotice(book, key, msg); - return value; + return store; }; var setter = function(v) { + prepare(); + logNotice(book, key, msg); - value = v; - return value; + store = v; + return store; }; Object.defineProperty(instance, property, { diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 78919d3a9..0191c500c 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -82,8 +82,9 @@ function encodeGlobal(output) { // Deprecated properties - deprecate.field(output, 'this.navigation', result, 'sections', encodeNavigation(output), - '"navigation" property is deprecated'); + deprecate.field(output, 'this.navigation', result, 'navigation', function() { + return encodeNavigation(output); + }, '"navigation" property is deprecated'); deprecate.field(output, 'this.book', result, 'book', result, '"book" property is deprecated, use "this" directly instead'); diff --git a/lib/api/encodeNavigation.js b/lib/api/encodeNavigation.js index c48ba5011..8e329a18b 100644 --- a/lib/api/encodeNavigation.js +++ b/lib/api/encodeNavigation.js @@ -1,3 +1,4 @@ +var Immutable = require('immutable'); /** Encode an article for next/prev @@ -26,13 +27,15 @@ function encodeArticle(pages, article) { */ function encodeNavigation(output) { var book = output.getBook(); + var pages = output.getPages(); var summary = book.getSummary(); var articles = summary.getArticlesAsList(); - return articles + + var navigation = articles .map(function(article, i) { var ref = article.getRef(); - if (ref) { + if (!ref) { return undefined; } @@ -45,14 +48,17 @@ function encodeNavigation(output) { index: i, title: article.getTitle(), introduction: (i === 0), - prev: prev? encodeArticle(prev) : undefined, - next: next? encodeArticle(next) : undefined, + prev: prev? encodeArticle(pages, prev) : undefined, + next: next? encodeArticle(pages, next) : undefined, level: article.getLevel() } ]; }) - .toMap() - .toJS(); + .filter(function(e) { + return Boolean(e); + }); + + return Immutable.Map(navigation).toJS(); } module.exports = encodeNavigation; diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index 1465137b6..8ad39c5e7 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -22,11 +22,9 @@ function encodePage(output, page) { result.path = file.getPath(); result.rawPath = fs.resolve(result.path); - // todo: (as deprecated) - // page.progress - - deprecate.field(output, 'page.progress', result, 'sections', encodeProgress(output, page), - '"page.progress" property is deprecated'); + deprecate.field(output, 'page.progress', result, 'sections', function() { + return encodeProgress(output, page); + }, '"page.progress" property is deprecated'); deprecate.field(output, 'page.sections', result, 'sections', [ { diff --git a/lib/api/encodeProgress.js b/lib/api/encodeProgress.js index f32c8d8cd..2522eb584 100644 --- a/lib/api/encodeProgress.js +++ b/lib/api/encodeProgress.js @@ -1,4 +1,5 @@ - +var Immutable = require('immutable'); +var encodeNavigation = require('./encodeNavigation'); /** page.progress is a deprecated property from GitBook v2 @@ -8,8 +9,59 @@ @return {Object} */ function encodeProgress(output, page) { - // todo - return {}; + var book = output.getBook(); + var pages = output.getPages(); + var summary = book.getSummary(); + var articles = summary.getArticlesAsList(); + + var current = page.getPath(); + var navigation = encodeNavigation(output); + navigation = Immutable.Map(navigation); + + var n = navigation.size; + var percent = 0, prevPercent = 0, currentChapter = null; + var done = true; + + var chapters = navigation + .map(function(nav, chapterPath) { + nav.path = chapterPath; + return nav; + }) + .valueSeq() + .sortBy(function(nav) { + return nav.index; + }) + .map(function(nav, i) { + // Calcul percent + nav.percent = (i * 100) / Math.max((n - 1), 1); + + // Is it done + nav.done = done; + if (nav.path == current) { + currentChapter = nav; + percent = nav.percent; + done = false; + } else if (done) { + prevPercent = nav.percent; + } + + return nav; + }) + .toJS(); + + return { + // Previous percent + prevPercent: prevPercent, + + // Current percent + percent: percent, + + // List of chapter with progress + chapters: chapters, + + // Current chapter + current: currentChapter + }; } module.exports = encodeProgress; diff --git a/lib/cli/index.js b/lib/cli/index.js index 4afb090ba..bb6557877 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,10 +1,8 @@ -module.exports = { - commands: [ - require('./build'), - require('./serve'), - require('./install'), - require('./parse'), - require('./init') - ] -}; +module.exports = [ + require('./build'), + require('./serve'), + require('./install'), + require('./parse'), + require('./init') +]; diff --git a/lib/index.js b/lib/index.js index 0a76e08b4..1f683e2dc 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,10 @@ var extend = require('extend'); var common = require('./browser'); -var Output = require('./output'); -var cli = require('./cli'); -var initBook = require('./initBook'); module.exports = extend({ - initBook: initBook, + initBook: require('./init'), createNodeFS: require('./fs/node'), - Output: Output, - commands: cli.commands + Output: require('./output'), + commands: require('./cli') }, common); From 73c190891d4aba49c6fbd43943be4ceb31a78a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 10:58:25 +0200 Subject: [PATCH 087/118] Fix parsing of multilingual books --- lib/models/book.js | 2 +- lib/models/fs.js | 3 ++- lib/models/languages.js | 26 ++++++++++++++++++++++++++ lib/parse/__tests__/parseBook.js | 16 +++++++++++++--- lib/parse/parseBook.js | 2 +- lib/plugins/findForBook.js | 1 + 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/models/book.js b/lib/models/book.js index 030223f3b..8f8ab74c7 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -248,7 +248,7 @@ Book.createFromParent = function createFromParent(parent, language) { logger: parent.getLogger(), ignore: Ignore().add(ignore), language: language, - fs: FS.reduceScope(parent.getFS(), language) + fs: FS.reduceScope(parent.getContentFS(), language) }); }; diff --git a/lib/models/fs.js b/lib/models/fs.js index f124da8bc..ab65dd5bb 100644 --- a/lib/models/fs.js +++ b/lib/models/fs.js @@ -33,7 +33,8 @@ FS.prototype.getRoot = function() { */ FS.prototype.isInScope = function(filename) { var rootPath = this.getRoot(); - filename = path.resolve(rootPath, filename); + filename = path.join(rootPath, filename); + return PathUtil.isInRoot(rootPath, filename); }; diff --git a/lib/models/languages.js b/lib/models/languages.js index c64857fb1..6c19cba48 100644 --- a/lib/models/languages.js +++ b/lib/models/languages.js @@ -1,6 +1,7 @@ var Immutable = require('immutable'); var File = require('./file'); +var Language = require('./language'); var Languages = Immutable.Record({ file: File(), @@ -34,4 +35,29 @@ Languages.prototype.getLanguage = function(lang) { return this.getList().get(lang); }; + +/** + Create a languages list from a JS object + + @param {File} + @param {Array} + @return {Language} +*/ +Languages.createFromList = function(file, langs) { + var list = Immutable.OrderedMap(); + + langs.forEach(function(lang) { + lang = Language({ + title: lang.title, + path: lang.path + }); + list = list.set(lang.getID(), lang); + }); + + return Languages({ + file: file, + list: list + }); +} + module.exports = Languages; diff --git a/lib/parse/__tests__/parseBook.js b/lib/parse/__tests__/parseBook.js index 441b7c04f..735ba57f3 100644 --- a/lib/parse/__tests__/parseBook.js +++ b/lib/parse/__tests__/parseBook.js @@ -4,16 +4,26 @@ var createMockFS = require('../../fs/mock'); describe('parseBook', function() { var parseBook = require('../parseBook'); - pit('should parse glossary if exists', function() { + pit('should parse multilingual book', function() { var fs = createMockFS({ - 'README.md': 'Hello World', - 'GLOSSARY.md': '# Glossary\n\n## Hello\nDescription for hello' + 'LANGS.md': '# Languages\n\n* [en](en)\n* [fr](fr)', + 'en': { + 'README.md': 'Hello' + }, + 'fr': { + 'README.md': 'Bonjour' + } }); var book = Book.createForFS(fs); return parseBook(book) .then(function(resultBook) { + var languages = resultBook.getLanguages(); + var books = resultBook.getBooks(); + expect(resultBook.isMultilingual()).toBe(true); + expect(languages.getList().size).toBe(2); + expect(books.size).toBe(2); }); }); }); diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index 7dfa1eba1..69cd66e79 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -62,7 +62,7 @@ function parseBook(book) { .then(parseConfig) .then(parseLanguages) .then(function(resultBook) { - if (book.isMultilingual()) { + if (resultBook.isMultilingual()) { return parseMultilingualBook(resultBook); } else { return parseBookContent(resultBook); diff --git a/lib/plugins/findForBook.js b/lib/plugins/findForBook.js index 75a498836..af0c1eee8 100644 --- a/lib/plugins/findForBook.js +++ b/lib/plugins/findForBook.js @@ -14,6 +14,7 @@ function findForBook(book) { return Promise.all([ findInstalled(path.resolve(__dirname, '../..')), findInstalled(book.getRoot()) + // todo: list for parent of multilingual ]) .then(function(results) { return Immutable.List(results) From 4f8bdbb5189ddb40923c1e9fdce2ccc833446187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 11:11:42 +0200 Subject: [PATCH 088/118] Add tests for parsing a book with option "root" --- lib/parse/__tests__/parseBook.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/parse/__tests__/parseBook.js b/lib/parse/__tests__/parseBook.js index 735ba57f3..25d1802df 100644 --- a/lib/parse/__tests__/parseBook.js +++ b/lib/parse/__tests__/parseBook.js @@ -26,4 +26,30 @@ describe('parseBook', function() { expect(books.size).toBe(2); }); }); + + pit('should parse book in a directory', function() { + var fs = createMockFS({ + 'book.json': JSON.stringify({ + root: './test' + }), + 'test': { + 'README.md': 'Hello World', + 'SUMMARY.md': '# Summary\n\n* [Page](page.md)\n', + 'page.md': 'Page' + } + }); + var book = Book.createForFS(fs); + + return parseBook(book) + .then(function(resultBook) { + var readme = resultBook.getReadme(); + var summary = resultBook.getSummary(); + var articles = summary.getArticlesAsList(); + + expect(summary.getFile().exists()).toBe(true); + expect(readme.getFile().exists()).toBe(true); + expect(articles.size).toBe(2); + }); + }); + }); From fb37a6bcabf9ec5d9b8b86f1641eb20275b9484d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 11:31:54 +0200 Subject: [PATCH 089/118] Inherit plugins list from parent output when generating multilingual book --- lib/output/generateBook.js | 90 ++++++++++++++++++++++++-------------- lib/plugins/findForBook.js | 3 +- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index cd20c8d29..6e878e81e 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -13,40 +13,15 @@ var generateAssets = require('./generateAssets'); var generatePages = require('./generatePages'); /** - Generate a book using a generator. - - The overall process is: - 1. List and load plugins for this book - 2. Call hook "config" - 3. Call hook "init" - 4. Initialize generator - 5. List all assets and pages - 6. Copy all assets to output - 7. Generate all pages - 8. Call hook "finish:before" - 9. Finish generation - 10. Call hook "finish" - + Process an output to generate the book @param {Generator} generator - @param {Book} book - @param {Object} options + @param {Output} output @return {Promise} */ -function generateBook(generator, book, options) { - options = generator.Options(options); - var state = generator.State? generator.State({}) : Immutable.Map(); - var start = Date.now(); - - return Promise( - new Output({ - book: book, - options: options, - state: state, - generator: generator.name - }) - ) +function processOutput(generator, startOutput) { + return Pormise(startOutput) .then(preparePlugins) .then(preparePages) .then(prepareAssets) @@ -97,16 +72,28 @@ function generateBook(generator, book, options) { .tap(function(output) { var book = output.getBook(); - var books = book.getBooks(); - var outputRoot = output.getRoot(); if (!book.isMultilingual()) { return; } + var books = book.getBooks(); + var outputRoot = output.getRoot(); + var plugins = output.getPlugins(); + var state = output.getState(); + return Promise.forEach(books, function(langBook, language) { + // Inherits plugins list, options and state var langOptions = options.set('root', path.join(outputRoot, language)); - return generateBook(generator, langBook, langOptions); + var langOutput = new Output({ + book: langBook, + options: langOptions, + state: state, + generator: generator.name, + plugins: plugins + }); + + return processOutput(generator, langOutput); }); }) @@ -139,13 +126,52 @@ function generateBook(generator, book, options) { } ) ) +} +/** + Generate a book using a generator. + + The overall process is: + 1. List and load plugins for this book + 2. Call hook "config" + 3. Call hook "init" + 4. Initialize generator + 5. List all assets and pages + 6. Copy all assets to output + 7. Generate all pages + 8. Call hook "finish:before" + 9. Finish generation + 10. Call hook "finish" + + + @param {Generator} generator + @param {Book} book + @param {Object} options + + @return {Promise} +*/ +function generateBook(generator, book, options) { + options = generator.Options(options); + var state = generator.State? generator.State({}) : Immutable.Map(); + var start = Date.now(); + + return Promise( + new Output({ + book: book, + options: options, + state: state, + generator: generator.name + }) + ) + .then(processOutput) .then(function(output) { var logger = output.getLogger(); var end = Date.now(); var duration = (end - start)/1000; logger.info.ok('generation finished with success in ' + duration.toFixed(1) + 's !'); + + return output; }); } diff --git a/lib/plugins/findForBook.js b/lib/plugins/findForBook.js index af0c1eee8..47b8fc457 100644 --- a/lib/plugins/findForBook.js +++ b/lib/plugins/findForBook.js @@ -14,8 +14,9 @@ function findForBook(book) { return Promise.all([ findInstalled(path.resolve(__dirname, '../..')), findInstalled(book.getRoot()) - // todo: list for parent of multilingual ]) + + // Merge all plugins .then(function(results) { return Immutable.List(results) .reduce(function(out, result) { From 9955fa8c29383dcbaaaf17e98c442d2d6b07aea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 12:46:31 +0200 Subject: [PATCH 090/118] Use plugin assets from parent when language book --- lib/output/website/copyPluginAssets.js | 8 ++++++++ lib/output/website/createTemplateEngine.js | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/output/website/copyPluginAssets.js b/lib/output/website/copyPluginAssets.js index a1a8cf4eb..9dc876fac 100644 --- a/lib/output/website/copyPluginAssets.js +++ b/lib/output/website/copyPluginAssets.js @@ -13,6 +13,14 @@ var fs = require('../../utils/fs'); @return {Promise} */ function copyPluginAssets(output) { + var book = output.getBook(); + + // Don't copy plugins assets for language book + // It'll be resolved to the parent folder + if (book.isLanguageBook()) { + return Promise(output); + } + var plugins = output.getPlugins() // We reverse the order of plugins to copy diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 24990b651..048439639 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -71,8 +71,14 @@ function createTemplateEngine(output, currentFile) { resolveAsset: function(filePath) { filePath = LocationUtils.toAbsolute(filePath, '', ''); filePath = path.join('gitbook', filePath); + filePath = LocationUtils.relativeForFile(currentFile, filePath); - return LocationUtils.relativeForFile(currentFile, filePath); + // Use assets from parent if language book + if (book.isLanguageBook()) { + filePath = path.join('../', filePath); + } + + return LocationUtils.normalize(filePath); }, /** From 3ea4e334bff428c60db175db58d042ba8e44cd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 13:04:54 +0200 Subject: [PATCH 091/118] It should generate index for languages --- lib/__tests__/module.js | 6 ++++++ lib/json/encodeOutput.js | 22 ++++++++++++++++++++++ lib/json/index.js | 1 + lib/output/__tests__/website.js | 27 +++++++++++++++++++++++++++ lib/output/generateBook.js | 11 ++++++----- lib/output/website/onFinish.js | 26 +++++++++++++++++++++++--- lib/utils/promise.js | 12 ++++++------ test.js | 28 ---------------------------- 8 files changed, 91 insertions(+), 42 deletions(-) create mode 100644 lib/__tests__/module.js create mode 100644 lib/json/encodeOutput.js delete mode 100644 test.js diff --git a/lib/__tests__/module.js b/lib/__tests__/module.js new file mode 100644 index 000000000..d9220f514 --- /dev/null +++ b/lib/__tests__/module.js @@ -0,0 +1,6 @@ + +describe('GitBook', function() { + it('should correctly export', function() { + require('../'); + }); +}); diff --git a/lib/json/encodeOutput.js b/lib/json/encodeOutput.js new file mode 100644 index 000000000..bb53a628d --- /dev/null +++ b/lib/json/encodeOutput.js @@ -0,0 +1,22 @@ +var encodeBook = require('./encodeBook'); + +/** + Encode an output to JSON + + @param {Output} + @return {Object} +*/ +function encodeOutputToJson(output) { + var book = output.getBook(); + var generator = output.getGenerator(); + + var result = encodeBook(book); + + result.output = { + name: generator + }; + + return result; +} + +module.exports = encodeOutputToJson; diff --git a/lib/json/index.js b/lib/json/index.js index e58beee3c..39cac9944 100644 --- a/lib/json/index.js +++ b/lib/json/index.js @@ -1,5 +1,6 @@ module.exports = { + encodeOutput: require('./encodeOutput'), encodeBookWithPage: require('./encodeBookWithPage'), encodeBook: require('./encodeBook'), encodeFile: require('./encodeFile'), diff --git a/lib/output/__tests__/website.js b/lib/output/__tests__/website.js index 7afa737db..54938c855 100644 --- a/lib/output/__tests__/website.js +++ b/lib/output/__tests__/website.js @@ -25,5 +25,32 @@ describe('WebsiteGenerator', function() { expect(folder).toHaveFile('test/page.html'); }); }); + + pit('should generate a multilingual book', function() { + return generateMock(WebsiteGenerator, { + 'LANGS.md': '# Languages\n\n* [en](en)\n* [fr](fr)', + 'en': { + 'README.md': 'Hello' + }, + 'fr': { + 'README.md': 'Bonjour' + } + }) + .then(function(folder) { + // It should generate languages + expect(folder).toHaveFile('en/index.html'); + expect(folder).toHaveFile('fr/index.html'); + + // Should not copy languages as assets + expect(folder).not.toHaveFile('en/README.md'); + expect(folder).not.toHaveFile('fr/README.md'); + + // Should copy assets only once + expect(folder).toHaveFile('gitbook/style.css'); + expect(folder).not.toHaveFile('en/gitbook/style.css'); + + expect(folder).toHaveFile('index.html'); + }); + }); }); diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index 6e878e81e..c5d61f819 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -21,7 +21,7 @@ var generatePages = require('./generatePages'); @return {Promise} */ function processOutput(generator, startOutput) { - return Pormise(startOutput) + return Promise(startOutput) .then(preparePlugins) .then(preparePages) .then(prepareAssets) @@ -81,10 +81,11 @@ function processOutput(generator, startOutput) { var outputRoot = output.getRoot(); var plugins = output.getPlugins(); var state = output.getState(); + var options = output.getOptions(); - return Promise.forEach(books, function(langBook, language) { + return Promise.forEach(books, function(langBook) { // Inherits plugins list, options and state - var langOptions = options.set('root', path.join(outputRoot, language)); + var langOptions = options.set('root', path.join(outputRoot, langBook.getLanguage())); var langOutput = new Output({ book: langBook, options: langOptions, @@ -155,7 +156,8 @@ function generateBook(generator, book, options) { var state = generator.State? generator.State({}) : Immutable.Map(); var start = Date.now(); - return Promise( + return processOutput( + generator, new Output({ book: book, options: options, @@ -163,7 +165,6 @@ function generateBook(generator, book, options) { generator: generator.name }) ) - .then(processOutput) .then(function(output) { var logger = output.getLogger(); var end = Date.now(); diff --git a/lib/output/website/onFinish.js b/lib/output/website/onFinish.js index a42425000..e3560e22f 100644 --- a/lib/output/website/onFinish.js +++ b/lib/output/website/onFinish.js @@ -1,15 +1,35 @@ var Promise = require('../../utils/promise'); +var JSONUtils = require('../../json'); +var Templating = require('../../templating'); +var writeFile = require('../helper/writeFile'); +var createTemplateEngine = require('./createTemplateEngine'); /** - Finish the generation + Finish the generation, write the languages index @param {Output} @return {Output} */ function onFinish(output) { - // todo: render language index + var book = output.getBook(); + var options = output.getOptions(); + var prefix = options.get('prefix'); - return Promise(output); + if (!book.isMultilingual()) { + return Promise(output); + } + + var filePath = 'index.html'; + var engine = createTemplateEngine(output, filePath); + var context = JSONUtils.encodeOutput(output); + + // Render the theme + return Templating.renderFile(engine, prefix + '/languages.html', context) + + // Write it to the disk + .then(function(html) { + return writeFile(output, filePath, html); + }); } module.exports = onFinish; diff --git a/lib/utils/promise.js b/lib/utils/promise.js index b8ab63c71..f27276718 100644 --- a/lib/utils/promise.js +++ b/lib/utils/promise.js @@ -11,9 +11,9 @@ var Immutable = require('immutable'); function reduce(arr, iter, base) { arr = Immutable.Iterable.isIterable(arr)? arr : Immutable.List(arr); - return arr.reduce(function(prev, elem, i) { + return arr.reduce(function(prev, elem, key) { return prev.then(function(val) { - return iter(val, elem, i); + return iter(val, elem, key); }); }, Q(base)); } @@ -26,8 +26,8 @@ function reduce(arr, iter, base) { @return {Promise} */ function forEach(arr, iter) { - return reduce(arr, function(val, el) { - return iter(el); + return reduce(arr, function(val, el, key) { + return iter(el, key); }); } @@ -39,8 +39,8 @@ function forEach(arr, iter) { @return {Promise} */ function serie(arr, iter, base) { - return reduce(arr, function(before, item, i) { - return Q(iter(item, i)) + return reduce(arr, function(before, item, key) { + return Q(iter(item, key)) .then(function(r) { before.push(r); return before; diff --git a/test.js b/test.js deleted file mode 100644 index e1f3ac96d..000000000 --- a/test.js +++ /dev/null @@ -1,28 +0,0 @@ -var path = require('path'); - -var gitbook = require('./lib'); -var Output = require('./lib/output'); -var NodeFS = require('./lib/fs/node'); - - -var BASE_PATH = path.join(__dirname); //, 'docs'); - -// Create a filesystem to read the book -var fs = NodeFS(BASE_PATH); - -// Create a book instance -var book = gitbook.Book.createForFS(fs); - -// Parse the book -gitbook.Parse.parseBook(book) -.then(function(_book) { - return Output.generate(Output.WebsiteGenerator, _book, { - root: path.join(__dirname, '_book') - }); -}) -.then(function(pages) { - //console.log('parsed', pages); -}, function(err) { - console.log('error:', err.stack); -}); - From aca963befc74313c5015853b1d497c671f549eba Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Fri, 29 Apr 2016 14:35:07 +0200 Subject: [PATCH 092/118] Fix typo --- lib/utils/promise.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/promise.js b/lib/utils/promise.js index f27276718..19d7554d1 100644 --- a/lib/utils/promise.js +++ b/lib/utils/promise.js @@ -117,7 +117,7 @@ function map(arr, iter) { /** - Wrap a fucntion in a promise + Wrap a function in a promise @param {Function} func @return {Funciton} From b8d7a00f58401d827ef06b76f4a9f189b89c7606 Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Fri, 29 Apr 2016 14:35:28 +0200 Subject: [PATCH 093/118] Find installed gitbook-plugins in node_modules folder --- lib/plugins/findInstalled.js | 27 ++++++++++++++++++++++++--- lib/plugins/loadForBook.js | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/plugins/findInstalled.js b/lib/plugins/findInstalled.js index 5e13c7984..2259230d3 100644 --- a/lib/plugins/findInstalled.js +++ b/lib/plugins/findInstalled.js @@ -1,7 +1,9 @@ var readInstalled = require('read-installed'); var Immutable = require('immutable'); +var path = require('path'); var Promise = require('../utils/promise'); +var fs = require('../utils/fs'); var Plugin = require('../models/plugin'); var PREFIX = require('../constants/pluginPrefix'); @@ -56,9 +58,28 @@ function findInstalled(folder) { }); } - return Promise.nfcall(readInstalled, folder, options) - .then(function(data) { - onPackage(data, true); + // Search for gitbook-plugins in node_modules folder + var node_modules = path.join(folder, 'node_modules'); + + // List all folders in node_modules + return fs.readdir(node_modules) + .then(function(modules) { + return Promise.serie(modules, function(module) { + // Not a gitbook-plugin + if (!validateId(module)) { + return Promise(); + } + + // Read gitbook-plugin package details + var module_folder = path.join(node_modules, module); + return Promise.nfcall(readInstalled, module_folder, options) + .then(function(data) { + onPackage(data, true); + }); + }); + }) + .then(function() { + // Return installed plugins return results; }); } diff --git a/lib/plugins/loadForBook.js b/lib/plugins/loadForBook.js index c1f19d02a..c4acb5f5e 100644 --- a/lib/plugins/loadForBook.js +++ b/lib/plugins/loadForBook.js @@ -22,7 +22,7 @@ function loadForBook(book) { // (aka pre-installed plugins) installed = installed.filter(function(plugin) { return ( - plugin.getDepth() > 1 || + plugin.getDepth() > 0 || requirements.has(plugin.getName()) ); }); From 94199af7b293b9c6f82a2e1c341587adee5b0792 Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Fri, 29 Apr 2016 14:35:56 +0200 Subject: [PATCH 094/118] Add location.areIdenticalPaths(p1,p2) to effectively compare two paths --- lib/parse/parseSummary.js | 4 +++- lib/utils/location.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/parse/parseSummary.js b/lib/parse/parseSummary.js index 2c6186fa9..72bf224f1 100644 --- a/lib/parse/parseSummary.js +++ b/lib/parse/parseSummary.js @@ -1,6 +1,7 @@ var parseStructureFile = require('./parseStructureFile'); var Summary = require('../models/summary'); var SummaryModifier = require('../modifiers').Summary; +var location = require('../utils/location'); /** Parse summary in a book, the summary can only be parsed @@ -28,8 +29,9 @@ function parseSummary(book) { // Insert readme as first entry var firstArticle = summary.getFirstArticle(); + if (readmeFile.exists() && - (!firstArticle || firstArticle.getRef() !== readmeFile.getPath())) { + (!firstArticle || !location.areIdenticalPaths(firstArticle.getRef(), readmeFile.getPath()))) { summary = SummaryModifier.unshiftArticle(summary, { title: 'Introduction', ref: readmeFile.getPath() diff --git a/lib/utils/location.js b/lib/utils/location.js index 94d9b2a22..84a71ad6c 100644 --- a/lib/utils/location.js +++ b/lib/utils/location.js @@ -83,7 +83,20 @@ function relativeForFile(baseFile, file) { return relative(path.dirname(baseFile), file); } +/** + Compare two paths, return true if they are identical + ('README.md', './README.md') -> true + + @param {String} p1: first path + @param {String} p2: second path + @return {Boolean} +*/ +function areIdenticalPaths(p1, p2) { + return normalize(p1) === normalize(p2); +} + module.exports = { + areIdenticalPaths: areIdenticalPaths, isExternal: isExternal, isRelative: isRelative, isAnchor: isAnchor, From 0ed524bce508f3f9ebb475d06c662cad948e224a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 15:08:54 +0200 Subject: [PATCH 095/118] Correctly setup configuration for plugins in multilingual books --- lib/models/book.js | 3 +++ lib/output/generateBook.js | 6 ++++-- lib/output/preparePlugins.js | 14 +++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/models/book.js b/lib/models/book.js index 8f8ab74c7..693e83053 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -245,8 +245,11 @@ Book.createFromParent = function createFromParent(parent, language) { var ignore = parent.getIgnore(); return new Book({ + // Inherits config. logegr and list of ignored files logger: parent.getLogger(), + config: parent.getConfig(), ignore: Ignore().add(ignore), + language: language, fs: FS.reduceScope(parent.getContentFS(), language) }); diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index c5d61f819..4e64eb004 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -156,8 +156,7 @@ function generateBook(generator, book, options) { var state = generator.State? generator.State({}) : Immutable.Map(); var start = Date.now(); - return processOutput( - generator, + return Promise( new Output({ book: book, options: options, @@ -165,6 +164,9 @@ function generateBook(generator, book, options) { generator: generator.name }) ) + .then(processOutput.bind(null, generator)) + + // Log duration and end message .then(function(output) { var logger = output.getLogger(); var end = Date.now(); diff --git a/lib/output/preparePlugins.js b/lib/output/preparePlugins.js index 5768e685a..54837edb3 100644 --- a/lib/output/preparePlugins.js +++ b/lib/output/preparePlugins.js @@ -1,4 +1,5 @@ var Plugins = require('../plugins'); +var Promise = require('../utils/promise'); /** Load and setup plugins @@ -9,7 +10,18 @@ var Plugins = require('../plugins'); function preparePlugins(output) { var book = output.getBook(); - return Plugins.loadForBook(book) + return Promise() + + // Only load plugins for main book + .then(function() { + if (book.isLanguageBook()) { + return output.getPlugins(); + } else { + return Plugins.loadForBook(book); + } + }) + + // Update book's configuration using the plugins .then(function(plugins) { return Plugins.validateConfig(book, plugins) .then(function(newBook) { From 07a5daefb025868ecf41054713a18d50e01511c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 15:09:11 +0200 Subject: [PATCH 096/118] Generate SUMMARY.html for ebook output --- lib/output/__tests__/ebook.js | 16 ++++++++++++++++ lib/output/createTemplateEngine.js | 2 ++ lib/output/ebook/index.js | 4 ++-- lib/output/ebook/onFinish.js | 25 +++++++++++++++++-------- lib/output/ebook/onInit.js | 4 ++-- lib/output/ebook/onPage.js | 4 ++-- lib/output/website/index.js | 16 +++++++++------- 7 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 lib/output/__tests__/ebook.js diff --git a/lib/output/__tests__/ebook.js b/lib/output/__tests__/ebook.js new file mode 100644 index 000000000..dabf360af --- /dev/null +++ b/lib/output/__tests__/ebook.js @@ -0,0 +1,16 @@ +var generateMock = require('../generateMock'); +var EbookGenerator = require('../ebook'); + +describe('EbookGenerator', function() { + + pit('should generate a SUMMARY.html', function() { + return generateMock(EbookGenerator, { + 'README.md': 'Hello World' + }) + .then(function(folder) { + expect(folder).toHaveFile('SUMMARY.html'); + expect(folder).toHaveFile('index.html'); + }); + }); +}); + diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index 523c850af..05718ead0 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -35,6 +35,8 @@ function createTemplateEngine(output) { filters: filters, blocks: blocks, loader: loader + + // todo: build context for filters/blocks }); } diff --git a/lib/output/ebook/index.js b/lib/output/ebook/index.js index 3aaa7e778..344a6c536 100644 --- a/lib/output/ebook/index.js +++ b/lib/output/ebook/index.js @@ -1,7 +1,7 @@ var extend = require('extend'); -var websiteGenerator = require('../website'); +var WebsiteGenerator = require('../website'); -module.exports = extend({}, websiteGenerator, { +module.exports = extend({}, WebsiteGenerator, { name: 'ebook', onInit: require('./onInit'), onPage: require('./onPage'), diff --git a/lib/output/ebook/onFinish.js b/lib/output/ebook/onFinish.js index 81fa45d6b..e82f679e1 100644 --- a/lib/output/ebook/onFinish.js +++ b/lib/output/ebook/onFinish.js @@ -1,20 +1,29 @@ -var websiteGenerator = require('../website'); +var WebsiteGenerator = require('../website'); +var JSONUtils = require('../../json'); +var Templating = require('../../templating'); +var writeFile = require('../helper/writeFile'); /** - Finish the generation, generate the ebook file using ebook-convert + Finish the generation, generates the SUMMARY.html @param {Output} @return {Output} */ function onFinish(output) { - return websiteGenerator.onFinish(output) - .then(function(resultOutput) { + var book = output.getBook(); + var options = output.getOptions(); + var prefix = options.get('prefix'); - // todo: - // - render SUMMARY.html - // - Build ebook using ebook-convert + var filePath = 'SUMMARY.html'; + var engine = WebsiteGenerator.createTemplateEngine(output, filePath); + var context = JSONUtils.encodeOutput(output); - return resultOutput; + // Render the theme + return Templating.renderFile(engine, prefix + '/SUMMARY.html', context) + + // Write it to the disk + .then(function(html) { + return writeFile(output, filePath, html); }); } diff --git a/lib/output/ebook/onInit.js b/lib/output/ebook/onInit.js index 9cee141a4..402e3183d 100644 --- a/lib/output/ebook/onInit.js +++ b/lib/output/ebook/onInit.js @@ -1,4 +1,4 @@ -var websiteGenerator = require('../website'); +var WebsiteGenerator = require('../website'); /** Initialize the generator @@ -7,7 +7,7 @@ var websiteGenerator = require('../website'); @return {Output} */ function onInit(output) { - return websiteGenerator.onInit(output) + return WebsiteGenerator.onInit(output) .then(function(resultOutput) { var options = resultOutput.getOptions(); diff --git a/lib/output/ebook/onPage.js b/lib/output/ebook/onPage.js index 13edf173f..21fd34c85 100644 --- a/lib/output/ebook/onPage.js +++ b/lib/output/ebook/onPage.js @@ -1,4 +1,4 @@ -var website = require('../website'); +var WebsiteGenerator = require('../website'); var Modifiers = require('../modifiers'); /** @@ -17,7 +17,7 @@ function onPage(output, page) { // Write page using website generator .then(function(resultPage) { - return website.onPage(output, resultPage); + return WebsiteGenerator.onPage(output, resultPage); }); } diff --git a/lib/output/website/index.js b/lib/output/website/index.js index 0c9fed8b4..000d417c6 100644 --- a/lib/output/website/index.js +++ b/lib/output/website/index.js @@ -1,10 +1,12 @@ module.exports = { - name: 'website', - State: require('./state'), - Options: require('./options'), - onInit: require('./onInit'), - onFinish: require('./onFinish'), - onPage: require('./onPage'), - onAsset: require('./onAsset') + name: 'website', + State: require('./state'), + Options: require('./options'), + onInit: require('./onInit'), + onFinish: require('./onFinish'), + onPage: require('./onPage'), + onAsset: require('./onAsset'), + onAsset: require('./onAsset'), + createTemplateEngine: require('./createTemplateEngine') }; From 8578f7d0d1b493e3999e9c31880567db252fe988 Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Fri, 29 Apr 2016 15:16:56 +0200 Subject: [PATCH 097/118] lib/api/encodePage.js: Fix deprecated page.progress assignment --- lib/api/encodePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/encodePage.js b/lib/api/encodePage.js index 8ad39c5e7..379d3d504 100644 --- a/lib/api/encodePage.js +++ b/lib/api/encodePage.js @@ -22,7 +22,7 @@ function encodePage(output, page) { result.path = file.getPath(); result.rawPath = fs.resolve(result.path); - deprecate.field(output, 'page.progress', result, 'sections', function() { + deprecate.field(output, 'page.progress', result, 'progress', function() { return encodeProgress(output, page); }, '"page.progress" property is deprecated'); From 730298110cea46e0c861e65d0c2c3fd82c0edd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 15:27:53 +0200 Subject: [PATCH 098/118] Add ebook commands pdf/mobi/epub --- lib/cli/buildEbook.js | 76 +++++++++++++++++++++++++++++++++++++++++ lib/cli/index.js | 6 +++- lib/models/book.js | 2 +- lib/models/languages.js | 8 +++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 lib/cli/buildEbook.js diff --git a/lib/cli/buildEbook.js b/lib/cli/buildEbook.js new file mode 100644 index 000000000..ce1d836e1 --- /dev/null +++ b/lib/cli/buildEbook.js @@ -0,0 +1,76 @@ +var path = require('path'); +var tmp = require('tmp'); + +var Promise = require('../utils/promise'); +var fs = require('../utils/fs'); +var Parse = require('../parse'); +var Output = require('../output'); + +var options = require('./options'); +var getBook = require('./getBook'); + + +module.exports = function(format) { + return { + name: (format + ' [book] [output]'), + description: 'build a book into an ebook file', + options: [ + options.log + ], + exec: function(args, kwargs) { + // Output file will be stored in + var outputFile = args[1] || ('book.' + format); + + // Create temporary directory + var outputFolder = tmp.dirSync().name; + + var book = getBook(args, kwargs); + var logger = book.getLogger(); + var Generator = Output.getGenerator('ebook'); + + return Parse.parseBook(book) + .then(function(resultBook) { + return Output.generate(Generator, resultBook, { + root: outputFolder, + format: format + }); + }) + + // Extract ebook file + .then(function(output) { + var book = output.getBook(); + var languages = book.getLanguages(); + + if (book.isMultilingual()) { + return Promise.ForEach(languages, function(lang) { + var langID = lang.getID(); + + var langOutputFile = path.join( + path.dirname(outputFile), + path.basename(outputFile, format) + '_' + langID + format + ); + + return fs.copy( + path.resolve(outputFolder, langID, 'index' + format), + langOutputFile + ); + }) + .thenResolve(languages.getCount()); + } else { + return fs.copy( + path.resolve(outputFolder, 'index' + format), + outputFile + ).thenResolve(1); + } + }) + + // Log end + .then(function(count) { + logger.info.ok(count + ' file(s) generated'); + + logger.debug('cleaning up... '); + return logger.debug.promise(fs.rmDir(outputFolder)); + }); + } + }; +}; diff --git a/lib/cli/index.js b/lib/cli/index.js index bb6557877..f1fca1d39 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,8 +1,12 @@ +var buildEbook = require('./buildEbook'); module.exports = [ require('./build'), require('./serve'), require('./install'), require('./parse'), - require('./init') + require('./init'), + buildEbook('pdf'), + buildEbook('epub'), + buildEbook('mobi') ]; diff --git a/lib/models/book.js b/lib/models/book.js index 693e83053..f960df1f1 100644 --- a/lib/models/book.js +++ b/lib/models/book.js @@ -165,7 +165,7 @@ Book.prototype.getPage = function(ref) { @return {Boolean} */ Book.prototype.isMultilingual = function() { - return (this.getLanguages().getList().size > 0); + return (this.getLanguages().getCount() > 0); }; /** diff --git a/lib/models/languages.js b/lib/models/languages.js index 6c19cba48..069c58919 100644 --- a/lib/models/languages.js +++ b/lib/models/languages.js @@ -35,6 +35,14 @@ Languages.prototype.getLanguage = function(lang) { return this.getList().get(lang); }; +/** + Return count of langs + + @return {Number} +*/ +Languages.prototype.getCount = function() { + return this.getList().size; +}; /** Create a languages list from a JS object From ceb4627f4e700b0e8058fae6760dbf9bb2ca7333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 15:30:56 +0200 Subject: [PATCH 099/118] Use API to create context for filters/blocks --- lib/output/website/createTemplateEngine.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/output/website/createTemplateEngine.js b/lib/output/website/createTemplateEngine.js index 048439639..334ec13ea 100644 --- a/lib/output/website/createTemplateEngine.js +++ b/lib/output/website/createTemplateEngine.js @@ -2,6 +2,7 @@ var path = require('path'); var nunjucks = require('nunjucks'); var DoExtension = require('nunjucks-do')(nunjucks); +var Api = require('../../api'); var JSONUtils = require('../../json'); var LocationUtils = require('../../utils/location'); var fs = require('../../utils/fs'); @@ -47,9 +48,14 @@ function createTemplateEngine(output, currentFile) { // Get languages var language = config.get('language'); + // Create API context + var context = Api.encodeGlobal(output); + return TemplateEngine.create({ loader: loader, + context: context, + filters: defaultFilters.merge({ /** Translate a sentence From 98f70879bde88d32e898ff057f4dc7ceaff7e25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 16:50:50 +0200 Subject: [PATCH 100/118] Complete ebook generation using ebook-convert --- lib/cli/buildEbook.js | 6 +-- lib/json/encodeOutput.js | 3 ++ lib/output/ebook/getConvertOptions.js | 73 +++++++++++++++++++++++++++ lib/output/ebook/getCoverPath.js | 30 +++++++++++ lib/output/ebook/getPDFTemplate.js | 42 +++++++++++++++ lib/output/ebook/index.js | 2 +- lib/output/ebook/onFinish.js | 66 ++++++++++++++++++++++-- lib/output/ebook/onInit.js | 21 -------- lib/output/ebook/options.js | 17 +++++++ lib/utils/fs.js | 17 +++++++ 10 files changed, 249 insertions(+), 28 deletions(-) create mode 100644 lib/output/ebook/getConvertOptions.js create mode 100644 lib/output/ebook/getCoverPath.js create mode 100644 lib/output/ebook/getPDFTemplate.js delete mode 100644 lib/output/ebook/onInit.js create mode 100644 lib/output/ebook/options.js diff --git a/lib/cli/buildEbook.js b/lib/cli/buildEbook.js index ce1d836e1..405d838c8 100644 --- a/lib/cli/buildEbook.js +++ b/lib/cli/buildEbook.js @@ -47,18 +47,18 @@ module.exports = function(format) { var langOutputFile = path.join( path.dirname(outputFile), - path.basename(outputFile, format) + '_' + langID + format + path.basename(outputFile, format) + '_' + langID + '.' + format ); return fs.copy( - path.resolve(outputFolder, langID, 'index' + format), + path.resolve(outputFolder, langID, 'index.' + format), langOutputFile ); }) .thenResolve(languages.getCount()); } else { return fs.copy( - path.resolve(outputFolder, 'index' + format), + path.resolve(outputFolder, 'index.' + format), outputFile ).thenResolve(1); } diff --git a/lib/json/encodeOutput.js b/lib/json/encodeOutput.js index bb53a628d..9054124d1 100644 --- a/lib/json/encodeOutput.js +++ b/lib/json/encodeOutput.js @@ -9,6 +9,7 @@ var encodeBook = require('./encodeBook'); function encodeOutputToJson(output) { var book = output.getBook(); var generator = output.getGenerator(); + var options = output.getOptions(); var result = encodeBook(book); @@ -16,6 +17,8 @@ function encodeOutputToJson(output) { name: generator }; + result.options = options.toJS(); + return result; } diff --git a/lib/output/ebook/getConvertOptions.js b/lib/output/ebook/getConvertOptions.js new file mode 100644 index 000000000..bc80493a8 --- /dev/null +++ b/lib/output/ebook/getConvertOptions.js @@ -0,0 +1,73 @@ +var extend = require('extend'); + +var Promise = require('../../utils/promise'); +var getPDFTemplate = require('./getPDFTemplate'); +var getCoverPath = require('./getCoverPath'); + +/** + Generate options for ebook-convert + + @param {Output} + @return {Promise} +*/ +function getConvertOptions(output) { + var options = output.getOptions(); + var format = options.get('format'); + + var book = output.getBook(); + var config = book.getConfig(); + + return Promise() + .then(function() { + var coverPath = getCoverPath(output); + var options = { + '--cover': coverPath, + '--title': config.getValue('title'), + '--comments': config.getValue('description'), + '--isbn': config.getValue('isbn'), + '--authors': config.getValue('author'), + '--language': book.getLanguage() || config.getValue('language'), + '--book-producer': 'GitBook', + '--publisher': 'GitBook', + '--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]', + '--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]', + '--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]', + '--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]', + '--max-levels': '1', + '--no-chapters-in-toc': true, + '--breadth-first': true, + '--dont-split-on-page-breaks': format === 'epub'? true : undefined + }; + + if (format !== 'pdf') { + return options; + } + + return Promise.all([ + getPDFTemplate(output, 'header'), + getPDFTemplate(output, 'footer') + ]) + .spread(function(headerTpl, footerTpl) { + var pdfOptions = config.getValue('pdf').toJS(); + + return options = extend(options, { + '--chapter-mark': String(pdfOptions.chapterMark), + '--page-breaks-before': String(pdfOptions.pageBreaksBefore), + '--margin-left': String(pdfOptions.margin.left), + '--margin-right': String(pdfOptions.margin.right), + '--margin-top': String(pdfOptions.margin.top), + '--margin-bottom': String(pdfOptions.margin.bottom), + '--pdf-default-font-size': String(pdfOptions.fontSize), + '--pdf-mono-font-size': String(pdfOptions.fontSize), + '--paper-size': String(pdfOptions.paperSize), + '--pdf-page-numbers': Boolean(pdfOptions.pageNumbers), + '--pdf-sans-family': String(pdfOptions.fontFamily), + '--pdf-header-template': headerTpl, + '--pdf-footer-template': footerTpl + }); + }); + }); +} + + +module.exports = getConvertOptions; diff --git a/lib/output/ebook/getCoverPath.js b/lib/output/ebook/getCoverPath.js new file mode 100644 index 000000000..c2192d486 --- /dev/null +++ b/lib/output/ebook/getCoverPath.js @@ -0,0 +1,30 @@ +var path = require('path'); +var fs = require('../../utils/fs'); + +/** + Resolve path to cover file to use + + @param {Output} + @return {String} +*/ +function getCoverPath(output) { + var outputRoot = output.getRoot(); + var book = output.getBook(); + var config = book.getConfig(); + var cover = config.getValue('cover', 'cover.jpg'); + + // Resolve to absolute + cover = fs.pickFile(outputRoot, cover); + if (cover) { + return cover; + } + + // Multilingual? try parent folder + if (book.isLanguageBook()) { + cover = fs.pickFile(path.join(outputRoot, '..'), cover); + } + + return cover; +} + +module.exports = getCoverPath; diff --git a/lib/output/ebook/getPDFTemplate.js b/lib/output/ebook/getPDFTemplate.js new file mode 100644 index 000000000..f7a450d27 --- /dev/null +++ b/lib/output/ebook/getPDFTemplate.js @@ -0,0 +1,42 @@ +var juice = require('juice'); + +var WebsiteGenerator = require('../website'); +var JSONUtils = require('../../json'); +var Templating = require('../../templating'); +var Promise = require('../../utils/promise'); + + +/** + Generate PDF header/footer templates + + @param {Output} output + @param {String} type + @return {String} +*/ +function getPDFTemplate(output, type) { + var filePath = 'pdf_' + type + '.html'; + var outputRoot = output.getRoot(); + var engine = WebsiteGenerator.createTemplateEngine(output, filePath); + + // Generate context + var context = JSONUtils.encodeOutput(output); + context.page = { + num: '_PAGENUM_', + title: '_TITLE_', + section: '_SECTION_' + }; + + // Render the theme + return Templating.renderFile(engine, 'ebook/' + filePath, context) + + // Inline css and assets + .then(function(html) { + return Promise.nfcall(juice.juiceResources, html, { + webResources: { + relativeTo: outputRoot + } + }); + }); +} + +module.exports = getPDFTemplate; diff --git a/lib/output/ebook/index.js b/lib/output/ebook/index.js index 344a6c536..786a10af5 100644 --- a/lib/output/ebook/index.js +++ b/lib/output/ebook/index.js @@ -3,7 +3,7 @@ var WebsiteGenerator = require('../website'); module.exports = extend({}, WebsiteGenerator, { name: 'ebook', - onInit: require('./onInit'), + Options: require('./options'), onPage: require('./onPage'), onFinish: require('./onFinish') }); diff --git a/lib/output/ebook/onFinish.js b/lib/output/ebook/onFinish.js index e82f679e1..17a8e5ebc 100644 --- a/lib/output/ebook/onFinish.js +++ b/lib/output/ebook/onFinish.js @@ -1,16 +1,22 @@ +var path = require('path'); + var WebsiteGenerator = require('../website'); var JSONUtils = require('../../json'); var Templating = require('../../templating'); +var Promise = require('../../utils/promise'); +var error = require('../../utils/error'); +var command = require('../../utils/command'); var writeFile = require('../helper/writeFile'); +var getConvertOptions = require('./getConvertOptions'); + /** - Finish the generation, generates the SUMMARY.html + Write the SUMMARY.html @param {Output} @return {Output} */ -function onFinish(output) { - var book = output.getBook(); +function writeSummary(output) { var options = output.getOptions(); var prefix = options.get('prefix'); @@ -27,4 +33,58 @@ function onFinish(output) { }); } +/** + Generate the ebook file as "index.pdf" + + @param {Output} + @return {Output} +*/ +function runEbookConvert(output) { + var logger = output.getLogger(); + var options = output.getOptions(); + var format = options.get('format'); + var outputFolder = output.getRoot(); + + if (!format) { + return Promise(output); + } + + return getConvertOptions(output) + .then(function(options) { + var cmd = [ + 'ebook-convert', + path.resolve(outputFolder, 'SUMMARY.html'), + path.resolve(outputFolder, 'index.' + format), + command.optionsToShellArgs(options) + ].join(' '); + + return command.exec(cmd) + .progress(function(data) { + logger.debug(data); + }) + .fail(function(err) { + if (err.code == 127) { + throw error.RequireInstallError({ + cmd: 'ebook-convert', + install: 'Install it from Calibre: https://calibre-ebook.com' + }); + } + + throw error.EbookError(err); + }); + }) + .thenResolve(output); +} + +/** + Finish the generation, generates the SUMMARY.html + + @param {Output} + @return {Output} +*/ +function onFinish(output) { + return writeSummary(output) + .then(runEbookConvert); +} + module.exports = onFinish; diff --git a/lib/output/ebook/onInit.js b/lib/output/ebook/onInit.js deleted file mode 100644 index 402e3183d..000000000 --- a/lib/output/ebook/onInit.js +++ /dev/null @@ -1,21 +0,0 @@ -var WebsiteGenerator = require('../website'); - -/** - Initialize the generator - - @param {Output} - @return {Output} -*/ -function onInit(output) { - return WebsiteGenerator.onInit(output) - .then(function(resultOutput) { - var options = resultOutput.getOptions(); - - options = options.set('directoryIndex', false); - options = options.set('prefix', 'ebook'); - - return resultOutput.setOptions(options); - }); -} - -module.exports = onInit; diff --git a/lib/output/ebook/options.js b/lib/output/ebook/options.js new file mode 100644 index 000000000..ea7b8b497 --- /dev/null +++ b/lib/output/ebook/options.js @@ -0,0 +1,17 @@ +var Immutable = require('immutable'); + +var Options = Immutable.Record({ + // Root folder for the output + root: String(), + + // Prefix for generation + prefix: String('ebook'), + + // Format to generate using ebook-convert + format: String(), + + // Force use of absolute urls ("index.html" instead of "/") + directoryIndex: Boolean(false) +}); + +module.exports = Options; diff --git a/lib/utils/fs.js b/lib/utils/fs.js index be3ec0ef7..3f970962f 100644 --- a/lib/utils/fs.js +++ b/lib/utils/fs.js @@ -113,6 +113,22 @@ function assertFile(filePath, generator) { }); } +/** + Pick a file, returns the absolute path if exists, undefined otherwise + + @param {String} rootFolder + @param {String} fileName + @return {String} +*/ +function pickFile(rootFolder, fileName) { + var result = path.join(rootFolder, fileName); + if (fs.existsSync(result)) { + return result; + } + + return undefined; +} + module.exports = { exists: fileExists, existsSync: fs.existsSync, @@ -120,6 +136,7 @@ module.exports = { readFile: Promise.nfbind(fs.readFile), writeFile: Promise.nfbind(fs.writeFile), assertFile: assertFile, + pickFile: pickFile, stat: Promise.nfbind(fs.stat), statSync: fs.statSync, readdir: Promise.nfbind(fs.readdir), From a5eec818197b434506da5d6fa61b37d21c4709f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 17:34:33 +0200 Subject: [PATCH 101/118] Fix context for page template --- lib/json/encodeBook.js | 13 +++++++++++-- lib/models/config.js | 8 +++++++- lib/output/ebook/getConvertOptions.js | 2 +- lib/output/generatePage.js | 4 +++- lib/output/website/onPage.js | 1 - 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index ba199994e..c13431215 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -1,5 +1,6 @@ -var gitbook = require('../gitbook'); +var extend = require('extend'); +var gitbook = require('../gitbook'); var encodeSummary = require('./encodeSummary'); var encodeGlossary = require('./encodeGlossary'); var encodeReadme = require('./encodeReadme'); @@ -11,6 +12,11 @@ var encodeReadme = require('./encodeReadme'); @return {Object} */ function encodeBookToJson(book) { + var config = book.getConfig(); + var language = book.getLanguage(); + + var variables = config.getValue('variables', {}); + return { summary: encodeSummary(book.getSummary()), glossary: encodeGlossary(book.getGlossary()), @@ -19,7 +25,10 @@ function encodeBookToJson(book) { gitbook: { version: gitbook.version, time: gitbook.START_TIME - } + }, + book: extend({ + language: language? language : undefined + }, variables) }; } diff --git a/lib/models/config.js b/lib/models/config.js index 547007a8b..663f4aa52 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -31,7 +31,13 @@ Config.prototype.getValue = function(keyPath, def) { return def; } - return values.getIn(keyPath); + var value = values.getIn(keyPath); + + if (Immutable.Iterable.isIterable(value)) { + value = value.toJS(); + } + + return value }; /** diff --git a/lib/output/ebook/getConvertOptions.js b/lib/output/ebook/getConvertOptions.js index bc80493a8..75be36494 100644 --- a/lib/output/ebook/getConvertOptions.js +++ b/lib/output/ebook/getConvertOptions.js @@ -48,7 +48,7 @@ function getConvertOptions(output) { getPDFTemplate(output, 'footer') ]) .spread(function(headerTpl, footerTpl) { - var pdfOptions = config.getValue('pdf').toJS(); + var pdfOptions = config.getValue('pdf'); return options = extend(options, { '--chapter-mark': String(pdfOptions.chapterMark), diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index aa3b37689..30a798a92 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -3,6 +3,7 @@ var error = require('../utils/error'); var Parse = require('../parse'); var Templating = require('../templating'); +var JSONUtils = require('../json'); var createTemplateEngine = require('./createTemplateEngine'); var callPageHook = require('./callPageHook'); @@ -22,6 +23,7 @@ function generatePage(output, page) { var file = resultPage.getFile(); var filePath = file.getPath(); var parser = file.getParser(); + var context = JSONUtils.encodeBookWithPage(book, resultPage); if (!parser) { return Promise.reject(error.FileNotParsableError({ @@ -39,7 +41,7 @@ function generatePage(output, page) { // Render templating syntax .then(function(content) { - return Templating.render(engine, filePath, content); + return Templating.render(engine, filePath, content, context); }) // Render page using parser (markdown -> HTML) diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index 1e9a1319e..dcdd95aa7 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -22,7 +22,6 @@ function onPage(output, page) { var file = page.getFile(); var prefix = options.get('prefix'); var book = output.getBook(); - var config = book.getConfig(); var plugins = output.getPlugins(); var engine = createTemplateEngine(output, page.getPath()); From c817acff098a538e8e4cdcdeded45a2c35890fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 17:40:35 +0200 Subject: [PATCH 102/118] Add context for page templates --- lib/output/createTemplateEngine.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/output/createTemplateEngine.js b/lib/output/createTemplateEngine.js index 05718ead0..37b3c2715 100644 --- a/lib/output/createTemplateEngine.js +++ b/lib/output/createTemplateEngine.js @@ -1,8 +1,7 @@ -var Immutable = require('immutable'); - var Templating = require('../templating'); var TemplateEngine = require('../models/templateEngine'); +var Api = require('../api'); var Plugins = require('../plugins'); var defaultBlocks = require('../constants/defaultBlocks'); @@ -31,12 +30,14 @@ function createTemplateEngine(output) { // Create loader var loader = new Templating.ConrefsLoader(rootFolder, logger); - return new TemplateEngine({ - filters: filters, - blocks: blocks, - loader: loader + // Create API context + var context = Api.encodeGlobal(output); - // todo: build context for filters/blocks + return new TemplateEngine({ + filters: filters, + blocks: blocks, + loader: loader, + context: context }); } From 0229272b0a1a1e7e739d45f1c3650b5092d96bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Fri, 29 Apr 2016 17:46:59 +0200 Subject: [PATCH 103/118] Add api this.readFile and this.readFileAsString --- lib/api/encodeGlobal.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 0191c500c..d66fd692f 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -16,6 +16,7 @@ var fileToURL = require('../output/helper/fileToURL'); */ function encodeGlobal(output) { var book = output.getBook(); + var fs = book.getContentFS(); var logger = output.getLogger(); var outputFolder = output.getRoot(); @@ -35,6 +36,26 @@ function encodeGlobal(output) { return book.isLanguageBook(); }, '"isSubBook" is deprecated, use "isLanguageBook()" instead'), + /** + Read a file from the book + + @param {String} fileName + @return {Promise} + */ + readFile: function(fileName) { + return fs.read(fileName); + }, + + /** + Read a file from the book as a string + + @param {String} fileName + @return {Promise} + */ + readFileAsString: function(fileName) { + return fs.readAsString(fileName); + }, + output: { /** Name of the generator being used From 01a26fda2596fc7ffefec4a7e0a79d5fc97d44ba Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 20:18:28 +0200 Subject: [PATCH 104/118] Fix linting error --- .eslintignore | 4 +++- .eslintrc | 1 - lib/api/deprecate.js | 2 +- lib/api/encodeGlobal.js | 1 - lib/api/encodeProgress.js | 5 ----- lib/cli/build.js | 2 -- lib/cli/parse.js | 19 +++++++++++++++---- lib/models/config.js | 2 +- lib/models/languages.js | 2 +- lib/output/generateBook.js | 2 +- lib/output/website/index.js | 1 - 11 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.eslintignore b/.eslintignore index c6e8e2155..e593ae781 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ docs/**/* -test/node_modules/**/* +_book/**/* +node_modules/**/* +test/**/* diff --git a/.eslintrc b/.eslintrc index 4e73ee74d..ed1fc565a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,7 +12,6 @@ }, "env": { "node": true, - "mocha": true, "browser": true, "jest": true, "jasmine": true diff --git a/lib/api/deprecate.js b/lib/api/deprecate.js index fd9280f06..d8d6ac148 100644 --- a/lib/api/deprecate.js +++ b/lib/api/deprecate.js @@ -54,7 +54,7 @@ function deprecateField(book, key, instance, property, value, msg) { if (is.fn(value)) store = value(); else store = value; - } + }; var getter = function(){ prepare(); diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index d66fd692f..073bc132c 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -1,4 +1,3 @@ -var fs = require('../utils/fs'); var Promise = require('../utils/promise'); var PathUtils = require('../utils/path'); diff --git a/lib/api/encodeProgress.js b/lib/api/encodeProgress.js index 2522eb584..afa034114 100644 --- a/lib/api/encodeProgress.js +++ b/lib/api/encodeProgress.js @@ -9,11 +9,6 @@ var encodeNavigation = require('./encodeNavigation'); @return {Object} */ function encodeProgress(output, page) { - var book = output.getBook(); - var pages = output.getPages(); - var summary = book.getSummary(); - var articles = summary.getArticlesAsList(); - var current = page.getPath(); var navigation = encodeNavigation(output); navigation = Immutable.Map(navigation); diff --git a/lib/cli/build.js b/lib/cli/build.js index 978bc878c..e64ab4427 100644 --- a/lib/cli/build.js +++ b/lib/cli/build.js @@ -1,5 +1,3 @@ -var path = require('path'); - var Parse = require('../parse'); var Output = require('../output'); diff --git a/lib/cli/parse.js b/lib/cli/parse.js index 0421b3ee8..0fa509a36 100644 --- a/lib/cli/parse.js +++ b/lib/cli/parse.js @@ -19,7 +19,7 @@ function printBook(book) { var glossaryFile = glossary.getFile(); if (configFile.exists()) { - logger.info.ln('configuration file is', configFile.getPath()); + logger.info.ln('Configuration file is', configFile.getPath()); } if (readmeFile.exists()) { @@ -33,10 +33,21 @@ function printBook(book) { if (summaryFile.exists()) { logger.info.ln('Table of Contents file is', summaryFile.getPath()); } - - //logger.info.ln('Table of Contents:'); } +function printMultingualBook(book) { + var logger = book.getLogger(); + var languages = book.getLanguages(); + var books = book.getBooks(); + + logger.info.ln(languages.size + ' languages'); + + languages.forEach(function(lang) { + logger.info.ln('Language:', lang.getTitle()); + printBook(books.get(lang.getID())); + logger.info.ln(''); + }); +} module.exports = { name: 'parse [book]', @@ -59,7 +70,7 @@ module.exports = { } if (resultBook.isMultilingual()) { - + printMultingualBook(resultBook); } else { printBook(resultBook); } diff --git a/lib/models/config.js b/lib/models/config.js index 663f4aa52..f4735dd88 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -37,7 +37,7 @@ Config.prototype.getValue = function(keyPath, def) { value = value.toJS(); } - return value + return value; }; /** diff --git a/lib/models/languages.js b/lib/models/languages.js index 069c58919..1e58d88b3 100644 --- a/lib/models/languages.js +++ b/lib/models/languages.js @@ -66,6 +66,6 @@ Languages.createFromList = function(file, langs) { file: file, list: list }); -} +}; module.exports = Languages; diff --git a/lib/output/generateBook.js b/lib/output/generateBook.js index 4e64eb004..6fcade032 100644 --- a/lib/output/generateBook.js +++ b/lib/output/generateBook.js @@ -126,7 +126,7 @@ function processOutput(generator, startOutput) { return output; } ) - ) + ); } /** diff --git a/lib/output/website/index.js b/lib/output/website/index.js index 000d417c6..7818a2831 100644 --- a/lib/output/website/index.js +++ b/lib/output/website/index.js @@ -7,6 +7,5 @@ module.exports = { onFinish: require('./onFinish'), onPage: require('./onPage'), onAsset: require('./onAsset'), - onAsset: require('./onAsset'), createTemplateEngine: require('./createTemplateEngine') }; From db1a0aedf88ef467058408a1a607ada182174c33 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 23:13:31 +0200 Subject: [PATCH 105/118] Fix listing of plugins when contains "-" --- lib/api/encodeGlobal.js | 7 ++++--- lib/json/encodeBook.js | 2 +- lib/models/config.js | 8 +------- lib/output/ebook/getConvertOptions.js | 2 +- lib/plugins/__tests__/listAll.js | 17 +++++++++++++++++ lib/plugins/listAll.js | 17 +++++++++++++---- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 073bc132c..89cea598b 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -1,5 +1,6 @@ var Promise = require('../utils/promise'); var PathUtils = require('../utils/path'); +var fs = require('../utils/fs'); var deprecate = require('./deprecate'); var encodeConfig = require('./encodeConfig'); @@ -15,7 +16,7 @@ var fileToURL = require('../output/helper/fileToURL'); */ function encodeGlobal(output) { var book = output.getBook(); - var fs = book.getContentFS(); + var bookFS = book.getContentFS(); var logger = output.getLogger(); var outputFolder = output.getRoot(); @@ -42,7 +43,7 @@ function encodeGlobal(output) { @return {Promise} */ readFile: function(fileName) { - return fs.read(fileName); + return bookFS.read(fileName); }, /** @@ -52,7 +53,7 @@ function encodeGlobal(output) { @return {Promise} */ readFileAsString: function(fileName) { - return fs.readAsString(fileName); + return bookFS.readAsString(fileName); }, output: { diff --git a/lib/json/encodeBook.js b/lib/json/encodeBook.js index c13431215..9bcb6ee81 100644 --- a/lib/json/encodeBook.js +++ b/lib/json/encodeBook.js @@ -28,7 +28,7 @@ function encodeBookToJson(book) { }, book: extend({ language: language? language : undefined - }, variables) + }, variables.toJS()) }; } diff --git a/lib/models/config.js b/lib/models/config.js index f4735dd88..547007a8b 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -31,13 +31,7 @@ Config.prototype.getValue = function(keyPath, def) { return def; } - var value = values.getIn(keyPath); - - if (Immutable.Iterable.isIterable(value)) { - value = value.toJS(); - } - - return value; + return values.getIn(keyPath); }; /** diff --git a/lib/output/ebook/getConvertOptions.js b/lib/output/ebook/getConvertOptions.js index 75be36494..bc80493a8 100644 --- a/lib/output/ebook/getConvertOptions.js +++ b/lib/output/ebook/getConvertOptions.js @@ -48,7 +48,7 @@ function getConvertOptions(output) { getPDFTemplate(output, 'footer') ]) .spread(function(headerTpl, footerTpl) { - var pdfOptions = config.getValue('pdf'); + var pdfOptions = config.getValue('pdf').toJS(); return options = extend(options, { '--chapter-mark': String(pdfOptions.chapterMark), diff --git a/lib/plugins/__tests__/listAll.js b/lib/plugins/__tests__/listAll.js index 6da5b8d89..71483a7e5 100644 --- a/lib/plugins/__tests__/listAll.js +++ b/lib/plugins/__tests__/listAll.js @@ -51,4 +51,21 @@ describe('listAll', function() { expect(great.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git'); }); + it('must list from array with -', function() { + var plugins = listAll(['ga', '-great']); + + expect(plugins.size).toBe(7); + + expect(plugins.has('ga')).toBe(true); + expect(plugins.has('great')).toBe(false); + }); + + it('must remove default plugins using -', function() { + var plugins = listAll(['ga', '-search']); + + expect(plugins.size).toBe(6); + + expect(plugins.has('ga')).toBe(true); + expect(plugins.has('search')).toBe(false); + }); }); diff --git a/lib/plugins/listAll.js b/lib/plugins/listAll.js index 8a0fc0731..65b8d7fe3 100644 --- a/lib/plugins/listAll.js +++ b/lib/plugins/listAll.js @@ -31,12 +31,21 @@ function listAll(plugins) { // Extract list of plugins to disable (starting with -) var toRemove = plugins.toList() .filter(function(plugin) { - return plugin.getName()[0] == '-'; + return plugin.getName()[0] === '-'; }) .map(function(plugin) { - return plugin.slice(1); + return plugin.getName().slice(1); }); + // Remove the '-' + plugins = plugins.mapKeys(function(name) { + if (name[0] === '-') { + return name.slice(1); + } else { + return name; + } + }); + // Append default plugins DEFAULT_PLUGINS.forEach(function(pluginName) { if (plugins.has(pluginName)) return; @@ -48,8 +57,8 @@ function listAll(plugins) { }); // Remove plugins - plugins = plugins.filterNot(function(plugin) { - return toRemove.includes(plugin.getName()); + plugins = plugins.filterNot(function(plugin, name) { + return toRemove.includes(name); }); return plugins; From 9acccad23add2670169652bd88e2045cb15ef6f7 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 23:19:33 +0200 Subject: [PATCH 106/118] Fix validation of configuration for plugins --- lib/models/config.js | 2 +- lib/plugins/validateConfig.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/config.js b/lib/models/config.js index 547007a8b..6ee03e426 100644 --- a/lib/models/config.js +++ b/lib/models/config.js @@ -28,7 +28,7 @@ Config.prototype.getValue = function(keyPath, def) { keyPath = Config.keyToKeyPath(keyPath); if (!values.hasIn(keyPath)) { - return def; + return Immutable.fromJS(def); } return values.getIn(keyPath); diff --git a/lib/plugins/validateConfig.js b/lib/plugins/validateConfig.js index 6303dae66..37f3c965a 100644 --- a/lib/plugins/validateConfig.js +++ b/lib/plugins/validateConfig.js @@ -22,7 +22,7 @@ function validatePluginConfig(book, plugin) { plugin.getName() ].join('.'); - var pluginConfig = config.getValue(configKey, {}); + var pluginConfig = config.getValue(configKey, {}).toJS(); var schema = (packageInfos.get('gitbook') || Immutable.Map()).toJS(); if (!schema) return book; From faf18476b159c81959a9b79b14e15e296c2af2ed Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 23:23:54 +0200 Subject: [PATCH 107/118] Don't fail if page doesn't exist --- lib/output/__tests__/website.js | 15 +++++++++++++++ lib/parse/parsePagesList.js | 2 ++ 2 files changed, 17 insertions(+) diff --git a/lib/output/__tests__/website.js b/lib/output/__tests__/website.js index 54938c855..6b949a47a 100644 --- a/lib/output/__tests__/website.js +++ b/lib/output/__tests__/website.js @@ -26,6 +26,21 @@ describe('WebsiteGenerator', function() { }); }); + pit('should not generate file if entry file doesn\'t exist', function() { + return generateMock(WebsiteGenerator, { + 'README.md': 'Hello World', + 'SUMMARY.md': '# Summary\n\n* [Page 1](page.md)\n* [Page 2](test/page.md)', + 'test': { + 'page.md': 'Hello 2' + } + }) + .then(function(folder) { + expect(folder).toHaveFile('index.html'); + expect(folder).not.toHaveFile('page.html'); + expect(folder).toHaveFile('test/page.html'); + }); + }); + pit('should generate a multilingual book', function() { return generateMock(WebsiteGenerator, { 'LANGS.md': '# Languages\n\n* [en](en)\n* [fr](fr)', diff --git a/lib/parse/parsePagesList.js b/lib/parse/parsePagesList.js index 36fcdec64..e356e88d6 100644 --- a/lib/parse/parsePagesList.js +++ b/lib/parse/parsePagesList.js @@ -28,6 +28,8 @@ function parsePagesList(book) { filepath, Page.createForFile(file) ); + }, function() { + // file doesn't exist }); }) .then(function() { From 8f7967acd0efcbf9d1483ac33cf26a27baaf4e74 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 23:32:41 +0200 Subject: [PATCH 108/118] Log page causing error when a bug occurs during generation --- lib/output/generatePages.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/output/generatePages.js b/lib/output/generatePages.js index 5b6851e56..73c5c0950 100644 --- a/lib/output/generatePages.js +++ b/lib/output/generatePages.js @@ -25,6 +25,10 @@ function generatePages(generator, output) { return generatePage(out, page) .then(function(resultPage) { return generator.onPage(out, resultPage); + }) + .fail(function(err) { + logger.error.ln('error while generating page "' + file.getPath() + '":'); + throw err; }); }, output); } From 310691bca2459112a586da97573e9a806b673941 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Fri, 29 Apr 2016 23:43:01 +0200 Subject: [PATCH 109/118] Use areIdenticalPaths for Summary.getByPath --- lib/models/summary.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/models/summary.js b/lib/models/summary.js index 5baebef37..5314bb069 100644 --- a/lib/models/summary.js +++ b/lib/models/summary.js @@ -2,6 +2,7 @@ var is = require('is'); var Immutable = require('immutable'); var error = require('../utils/error'); +var LocationUtils = require('../utils/location'); var File = require('./file'); var SummaryPart = require('./summaryPart'); var SummaryArticle = require('./summaryArticle'); @@ -73,7 +74,7 @@ Summary.prototype.getByLevel = function(level) { */ Summary.prototype.getByPath = function(filePath) { return this.getArticle(function(article) { - return (article.getPath() === filePath); + return (LocationUtils.areIdenticalPaths(article.getPath(), filePath)); }); }; From 5bad08b9a0756c46eebf6d8460c959ec388d7e74 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 12:14:43 +0200 Subject: [PATCH 110/118] Add this.generator as deprecated --- lib/api/encodeGlobal.js | 6 ++++++ lib/output/website/onPage.js | 3 +++ 2 files changed, 9 insertions(+) diff --git a/lib/api/encodeGlobal.js b/lib/api/encodeGlobal.js index 89cea598b..4688cca43 100644 --- a/lib/api/encodeGlobal.js +++ b/lib/api/encodeGlobal.js @@ -101,8 +101,14 @@ function encodeGlobal(output) { } }; + // todo + // template.applyBlock + // Deprecated properties + deprecate.field(output, 'this.generator', result, 'generator', + output.getGenerator(), '"this.generator" property is deprecated, use "this.output.name" instead'); + deprecate.field(output, 'this.navigation', result, 'navigation', function() { return encodeNavigation(output); }, '"navigation" property is deprecated'); diff --git a/lib/output/website/onPage.js b/lib/output/website/onPage.js index dcdd95aa7..64b4e047a 100644 --- a/lib/output/website/onPage.js +++ b/lib/output/website/onPage.js @@ -56,6 +56,9 @@ function onPage(output, page) { } }; + // We should probabbly move it to "template" or a "site" namespace + context.basePath = basePath; + // Render the theme return Templating.renderFile(engine, prefix + '/page.html', context) From fbe028273828bc8518e92e87fcbd2a6718dc91e2 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 12:30:56 +0200 Subject: [PATCH 111/118] Add tests for git utility --- lib/utils/__tests__/git.js | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/utils/__tests__/git.js diff --git a/lib/utils/__tests__/git.js b/lib/utils/__tests__/git.js new file mode 100644 index 000000000..6eed81e93 --- /dev/null +++ b/lib/utils/__tests__/git.js @@ -0,0 +1,58 @@ +var should = require('should'); +var path = require('path'); +var os = require('os'); + +var Git = require('../git'); + +describe('Git', function() { + + describe('URL parsing', function() { + + it('should correctly validate git urls', function() { + // HTTPS + expect(Git.isUrl('git+https://github.com/Hello/world.git')).toBeTruthy(); + + // SSH + expect(Git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1')).toBeTruthy(); + + // Non valid + expect(Git.isUrl('https://github.com/Hello/world.git')).not.toBeTruthy(); + expect(Git.isUrl('README.md')).not.toBeTruthy(); + }); + + it('should parse HTTPS urls', function() { + var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md'); + + expect(parts.host).toBe('https://gist.github.com/69ea4542e4c8967d2fa7.git'); + expect(parts.ref).toBe(null); + expect(parts.filepath).toBe('test.md'); + }); + + it('should parse HTTPS urls with a reference', function() { + var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#1.0.0'); + + expect(parts.host).toBe('https://gist.github.com/69ea4542e4c8967d2fa7.git'); + expect(parts.ref).toBe('1.0.0'); + expect(parts.filepath).toBe('test.md'); + }); + + it('should parse SSH urls', function() { + var parts = Git.parseUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1'); + + expect(parts.host).toBe('git@github.com:GitbookIO/gitbook.git'); + expect(parts.ref).toBe('e1594cde2c32e4ff48f6c4eff3d3d461743d74e1'); + expect(parts.filepath).toBe('directory/README.md'); + }); + }); + + describe('Cloning and resolving', function() { + pit('should clone an HTTPS url', function() { + var git = new Git(path.join(os.tmpdir(), 'test-git-'+Date.now())); + return git.resolve('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md') + .then(function(filename) { + expect(path.extname(filename)).toBe('.md'); + }); + }); + }); + +}); From 30bce5f9bb1f8ceee867770386fa6f7fdffd27ee Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 14:37:44 +0200 Subject: [PATCH 112/118] Add option "--timing" to mesure gitbook performances --- lib/cli/build.js | 7 ++- lib/cli/options.js | 9 +++- lib/output/callHook.js | 39 +++++++++------- lib/output/generatePage.js | 84 +++++++++++++++++----------------- lib/parse/listAssets.js | 26 ++++++----- lib/parse/parseBook.js | 8 +++- lib/parse/parsePagesList.js | 40 +++++++++-------- lib/plugins/findForBook.js | 27 ++++++----- lib/plugins/loadPlugin.js | 3 ++ lib/utils/timing.js | 89 +++++++++++++++++++++++++++++++++++++ 10 files changed, 231 insertions(+), 101 deletions(-) create mode 100644 lib/utils/timing.js diff --git a/lib/cli/build.js b/lib/cli/build.js index e64ab4427..023901e2a 100644 --- a/lib/cli/build.js +++ b/lib/cli/build.js @@ -1,5 +1,6 @@ var Parse = require('../parse'); var Output = require('../output'); +var timing = require('../utils/timing'); var options = require('./options'); var getBook = require('./getBook'); @@ -11,7 +12,8 @@ module.exports = { description: 'build a book', options: [ options.log, - options.format + options.format, + options.timing ], exec: function(args, kwargs) { var book = getBook(args, kwargs); @@ -24,6 +26,9 @@ module.exports = { return Output.generate(Generator, resultBook, { root: outputFolder }); + }) + .fin(function() { + if (kwargs.timing) timing.dump(book.getLogger()); }); } }; diff --git a/lib/cli/options.js b/lib/cli/options.js index 26581cba7..ddcb5c584 100644 --- a/lib/cli/options.js +++ b/lib/cli/options.js @@ -17,7 +17,14 @@ var formatOption = { defaults: 'website' }; +var timingOption = { + name: 'timing', + description: 'Print timing debug information', + defaults: false +}; + module.exports = { log: logOptions, - format: formatOption + format: formatOption, + timing: timingOption }; diff --git a/lib/output/callHook.js b/lib/output/callHook.js index 1ff1a4165..4914e5206 100644 --- a/lib/output/callHook.js +++ b/lib/output/callHook.js @@ -1,4 +1,5 @@ var Promise = require('../utils/promise'); +var timing = require('../utils/timing'); var Api = require('../api'); function defaultGetArgument() { @@ -30,26 +31,30 @@ function callHook(name, getArgument, handleResult, output) { // Create the JS context for plugins var context = Api.encodeGlobal(output); - // Get the arguments - return Promise(getArgument(output)) + return timing.measure( + 'call.hook.' + name, - // Call the hooks in serie - .then(function(arg) { - return Promise.reduce(plugins, function(prev, plugin) { - var hook = plugin.getHook(name); - if (!hook) { - return prev; - } + // Get the arguments + Promise(getArgument(output)) - return hook.call(context, prev); - }, arg); - }) + // Call the hooks in serie + .then(function(arg) { + return Promise.reduce(plugins, function(prev, plugin) { + var hook = plugin.getHook(name); + if (!hook) { + return prev; + } - // Handle final result - .then(function(result) { - output = Api.decodeGlobal(output, context); - return handleResult(output, result); - }); + return hook.call(context, prev); + }, arg); + }) + + // Handle final result + .then(function(result) { + output = Api.decodeGlobal(output, context); + return handleResult(output, result); + }) + ); } module.exports = callHook; diff --git a/lib/output/generatePage.js b/lib/output/generatePage.js index 30a798a92..a93d4b071 100644 --- a/lib/output/generatePage.js +++ b/lib/output/generatePage.js @@ -1,5 +1,6 @@ var Promise = require('../utils/promise'); var error = require('../utils/error'); +var timing = require('../utils/timing'); var Parse = require('../parse'); var Templating = require('../templating'); @@ -18,50 +19,53 @@ function generatePage(output, page) { var book = output.getBook(); var engine = createTemplateEngine(output); - return Parse.parsePage(book, page) - .then(function(resultPage) { - var file = resultPage.getFile(); - var filePath = file.getPath(); - var parser = file.getParser(); - var context = JSONUtils.encodeBookWithPage(book, resultPage); + return timing.measure( + 'page.generate', + Parse.parsePage(book, page) + .then(function(resultPage) { + var file = resultPage.getFile(); + var filePath = file.getPath(); + var parser = file.getParser(); + var context = JSONUtils.encodeBookWithPage(book, resultPage); - if (!parser) { - return Promise.reject(error.FileNotParsableError({ - filename: filePath - })); - } + if (!parser) { + return Promise.reject(error.FileNotParsableError({ + filename: filePath + })); + } - // Call hook "page:before" - return callPageHook('page:before', output, resultPage) + // Call hook "page:before" + return callPageHook('page:before', output, resultPage) - // Escape code blocks with raw tags - .then(function(currentPage) { - return parser.page.prepare(currentPage.getContent()); + // Escape code blocks with raw tags + .then(function(currentPage) { + return parser.page.prepare(currentPage.getContent()); + }) + + // Render templating syntax + .then(function(content) { + return Templating.render(engine, filePath, content, context); + }) + + // Render page using parser (markdown -> HTML) + .then(parser.page).get('content') + + // Post processing for templating syntax + .then(function(content) { + return Templating.postRender(engine, content); + }) + + // Return new page + .then(function(content) { + return resultPage.set('content', content); + }) + + // Call final hook + .then(function(currentPage) { + return callPageHook('page', output, currentPage); + }); }) - - // Render templating syntax - .then(function(content) { - return Templating.render(engine, filePath, content, context); - }) - - // Render page using parser (markdown -> HTML) - .then(parser.page).get('content') - - // Post processing for templating syntax - .then(function(content) { - return Templating.postRender(engine, content); - }) - - // Return new page - .then(function(content) { - return resultPage.set('content', content); - }) - - // Call final hook - .then(function(currentPage) { - return callPageHook('page', output, currentPage); - }); - }); + ); } module.exports = generatePage; diff --git a/lib/parse/listAssets.js b/lib/parse/listAssets.js index a0dbbc9be..c43b05469 100644 --- a/lib/parse/listAssets.js +++ b/lib/parse/listAssets.js @@ -1,3 +1,4 @@ +var timing = require('../utils/timing'); /** List all assets in a book @@ -16,17 +17,20 @@ function listAssets(book, pages) { var glossary = book.getGlossary(); var glossaryFile = glossary.getFile().getPath(); - return fs.listAllFiles() - .then(function(files) { - return files.filterNot(function(file) { - return ( - book.isContentFileIgnored(file) || - pages.has(file) || - file !== summaryFile || - file !== glossaryFile - ); - }); - }); + return timing.measure( + 'parse.listAssets', + fs.listAllFiles() + .then(function(files) { + return files.filterNot(function(file) { + return ( + book.isContentFileIgnored(file) || + pages.has(file) || + file !== summaryFile || + file !== glossaryFile + ); + }); + }) + ); } module.exports = listAssets; diff --git a/lib/parse/parseBook.js b/lib/parse/parseBook.js index 69cd66e79..84a403872 100644 --- a/lib/parse/parseBook.js +++ b/lib/parse/parseBook.js @@ -1,4 +1,5 @@ var Promise = require('../utils/promise'); +var timing = require('../utils/timing'); var Book = require('../models/book'); var parseIgnore = require('./parseIgnore'); @@ -57,7 +58,9 @@ function parseMultilingualBook(book) { @return {Promise} */ function parseBook(book) { - return Promise(book) + return timing.measure( + 'parse.book', + Promise(book) .then(parseIgnore) .then(parseConfig) .then(parseLanguages) @@ -67,7 +70,8 @@ function parseBook(book) { } else { return parseBookContent(resultBook); } - }); + }) + ); } module.exports = parseBook; diff --git a/lib/parse/parsePagesList.js b/lib/parse/parsePagesList.js index e356e88d6..a3a52f842 100644 --- a/lib/parse/parsePagesList.js +++ b/lib/parse/parsePagesList.js @@ -1,5 +1,6 @@ var Immutable = require('immutable'); +var timing = require('../utils/timing'); var Page = require('../models/page'); var walkSummary = require('./walkSummary'); @@ -14,27 +15,30 @@ function parsePagesList(book) { var summary = book.getSummary(); var map = Immutable.OrderedMap(); - return walkSummary(summary, function(article) { - if (!article.isPage()) return; + return timing.measure( + 'parse.listPages', + walkSummary(summary, function(article) { + if (!article.isPage()) return; - var filepath = article.getPath(); + var filepath = article.getPath(); - // Is the page ignored? - if (book.isContentFileIgnored(filepath)) return; + // Is the page ignored? + if (book.isContentFileIgnored(filepath)) return; - return fs.statFile(filepath) - .then(function(file) { - map = map.set( - filepath, - Page.createForFile(file) - ); - }, function() { - // file doesn't exist - }); - }) - .then(function() { - return map; - }); + return fs.statFile(filepath) + .then(function(file) { + map = map.set( + filepath, + Page.createForFile(file) + ); + }, function() { + // file doesn't exist + }); + }) + .then(function() { + return map; + }) + ); } diff --git a/lib/plugins/findForBook.js b/lib/plugins/findForBook.js index 47b8fc457..14ccc0585 100644 --- a/lib/plugins/findForBook.js +++ b/lib/plugins/findForBook.js @@ -2,6 +2,7 @@ var path = require('path'); var Immutable = require('immutable'); var Promise = require('../utils/promise'); +var timing = require('../utils/timing'); var findInstalled = require('./findInstalled'); /** @@ -11,18 +12,22 @@ var findInstalled = require('./findInstalled'); @return {Promise>} */ function findForBook(book) { - return Promise.all([ - findInstalled(path.resolve(__dirname, '../..')), - findInstalled(book.getRoot()) - ]) + return timing.measure( + 'plugins.findForBook', - // Merge all plugins - .then(function(results) { - return Immutable.List(results) - .reduce(function(out, result) { - return out.merge(result); - }, Immutable.OrderedMap()); - }); + Promise.all([ + findInstalled(path.resolve(__dirname, '../..')), + findInstalled(book.getRoot()) + ]) + + // Merge all plugins + .then(function(results) { + return Immutable.List(results) + .reduce(function(out, result) { + return out.merge(result); + }, Immutable.OrderedMap()); + }) + ); } diff --git a/lib/plugins/loadPlugin.js b/lib/plugins/loadPlugin.js index a1c41b425..400146e81 100644 --- a/lib/plugins/loadPlugin.js +++ b/lib/plugins/loadPlugin.js @@ -4,6 +4,7 @@ var Immutable = require('immutable'); var Promise = require('../utils/promise'); var error = require('../utils/error'); +var timing = require('../utils/timing'); var validatePlugin = require('./validatePlugin'); @@ -72,6 +73,8 @@ function loadPlugin(book, plugin) { .then(validatePlugin); + p = timing.measure('plugin.load', p); + logger.info('loading plugin "' + name + '"... '); return logger.info.promise(p); } diff --git a/lib/utils/timing.js b/lib/utils/timing.js new file mode 100644 index 000000000..21a4b91f3 --- /dev/null +++ b/lib/utils/timing.js @@ -0,0 +1,89 @@ +var Immutable = require('immutable'); +var is = require('is'); + +var timers = {}; +var startDate = Date.now(); + +/** + Mesure an operation + + @parqm {String} type + @param {Promise} p + @return {Promise} +*/ +function measure(type, p) { + timers[type] = timers[type] || { + type: type, + count: 0, + total: 0, + min: undefined, + max: 0 + }; + + var start = Date.now(); + + return p + .fin(function() { + var end = Date.now(); + var duration = (end - start); + + timers[type].count ++; + timers[type].total += duration; + + if (is.undefined(timers[type].min)) { + timers[type].min = duration; + } else { + timers[type].min = Math.min(timers[type].min, duration); + } + + timers[type].max = Math.max(timers[type].max, duration); + }); +} + +/** + Return a milliseconds number as a second string + + @param {Number} ms + @return {String} +*/ +function time(ms) { + if (ms < 1000) { + return (ms.toFixed(0)) + 'ms'; + } + + return (ms/1000).toFixed(2) + 's'; +} + +/** + Dump all timers to a logger + + @param {Logger} logger +*/ +function dump(logger) { + var prefix = ' > '; + var measured = 0; + var totalDuration = Date.now() - startDate; + + Immutable.Map(timers) + .valueSeq() + .sortBy(function(timer) { + measured += timer.total; + return timer.total; + }) + .forEach(function(timer) { + logger.debug.ln('Timer "' + timer.type + '" (' + timer.count + ' times) :'); + logger.debug.ln(prefix + 'Total: ' + time(timer.total)); + logger.debug.ln(prefix + 'Average: ' + time(timer.total / timer.count)); + logger.debug.ln(prefix + 'Min: ' + time(timer.min)); + logger.debug.ln(prefix + 'Max: ' + time(timer.max)); + logger.debug.ln('---------------------------'); + }); + + + logger.debug.ln(time(totalDuration - measured) + ' spent in non-mesured sections'); +} + +module.exports = { + measure: measure, + dump: dump +}; From d9d7ab4ee59fad5c20e33cbf6125420fee5efffd Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Sat, 30 Apr 2016 17:03:19 +0200 Subject: [PATCH 113/118] lib/models/templateBlock.js: Add test file lib/models/__tests__/templateBlock.js --- lib/models/__tests__/templateBlock.js | 107 ++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 lib/models/__tests__/templateBlock.js diff --git a/lib/models/__tests__/templateBlock.js b/lib/models/__tests__/templateBlock.js new file mode 100644 index 000000000..9f0e9b778 --- /dev/null +++ b/lib/models/__tests__/templateBlock.js @@ -0,0 +1,107 @@ +var nunjucks = require('nunjucks'); +var Immutable = require('immutable'); +var Promise = require('../../utils/promise'); + +describe('TemplateBlock', function() { + var TemplateBlock = require('../templateBlock'); + var TemplateEngine = require('../templateEngine'); + + describe('create', function() { + pit('must initialize a simple TemplateBlock from a function', function() { + var templateBlock = TemplateBlock.create('sayhello', function(block) { + return '

Hello, World!

'; + }); + + // Check basic templateBlock properties + expect(templateBlock.getName()).toBe('sayhello'); + expect(templateBlock.getPost()).toBeNull(); + expect(templateBlock.getParse()).toBeTruthy(); + expect(templateBlock.getEndTag()).toBe('endsayhello'); + expect(templateBlock.getBlocks().size).toBe(0); + expect(templateBlock.getShortcuts().size).toBe(0); + expect(templateBlock.getExtensionName()).toBe('BlocksayhelloExtension'); + + // Check result of applying block + return Promise() + .then(function() { + return templateBlock.applyBlock(); + }) + .then(function(result) { + expect(result.name).toBe('sayhello'); + expect(result.body).toBe('

Hello, World!

'); + }); + }); + }); + + describe('toNunjucksExt()', function() { + pit('must create a valid nunjucks extension', function() { + var templateBlock = TemplateBlock.create('sayhello', function(block) { + return '

Hello, World!

'; + }); + + // Create a fresh Nunjucks environment + var env = new nunjucks.Environment(null, { autoescape: false }); + + // Add template block to environement + var Ext = templateBlock.toNunjucksExt(); + env.addExtension(templateBlock.getExtensionName(), new Ext()); + + // Render a template using the block + var src = '{% sayhello %}{% endsayhello %}'; + return Promise.nfcall(env.renderString.bind(env), src) + .then(function(res) { + expect(res).toBe('

Hello, World!

'); + }); + }); + + pit('must apply block arguments correctly', function() { + var templateBlock = TemplateBlock.create('sayhello', function(block) { + return '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!'; + }); + + // Create a fresh Nunjucks environment + var env = new nunjucks.Environment(null, { autoescape: false }); + + // Add template block to environement + var Ext = templateBlock.toNunjucksExt(); + env.addExtension(templateBlock.getExtensionName(), new Ext()); + + // Render a template using the block + var src = '{% sayhello name="Samy", tag="p" %}{% endsayhello %}'; + return Promise.nfcall(env.renderString.bind(env), src) + .then(function(res) { + expect(res).toBe('

Hello, Samy!

'); + }); + }); + + pit('must handle nested blocks', function() { + var templateBlock = new TemplateBlock({ + name: 'yoda', + blocks: Immutable.List(['start', 'end']), + process: function(block) { + var nested = {}; + + block.blocks.forEach(function(blk) { + nested[blk.name] = blk.body.trim(); + }); + + return '

'+nested.end+' '+nested.start+'

'; + } + }); + + // Create a fresh Nunjucks environment + var env = new nunjucks.Environment(null, { autoescape: false }); + + // Add template block to environement + var Ext = templateBlock.toNunjucksExt(); + env.addExtension(templateBlock.getExtensionName(), new Ext()); + + // Render a template using the block + var src = '{% yoda %}{% start %}this sentence should be{% end %}inverted{% endyoda %}'; + return Promise.nfcall(env.renderString.bind(env), src) + .then(function(res) { + expect(res).toBe('

inverted this sentence should be

'); + }); + }); + }); +}); \ No newline at end of file From 8b05885d57d3753193950a17331b4d32b23abe64 Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Sat, 30 Apr 2016 17:03:57 +0200 Subject: [PATCH 114/118] lib/models/templateBlock.js: Use function for Ext() instead of var --- lib/models/templateBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js index 92a0bf3b7..999898cd1 100644 --- a/lib/models/templateBlock.js +++ b/lib/models/templateBlock.js @@ -79,7 +79,7 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext) { var endTag = this.getEndTag(); var blocks = this.getBlocks(); - var Ext = function () { + function Ext() { this.tags = [name]; this.parse = function(parser, nodes) { From 5f3c3760c06e516d694c0f218bc86ea286ede410 Mon Sep 17 00:00:00 2001 From: Johan Preynat Date: Sat, 30 Apr 2016 17:04:44 +0200 Subject: [PATCH 115/118] lib/models/templateBlock.js: Fix convert blocks to pure JS array before passing to parser.parseUntilBlocks() --- lib/models/templateBlock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/templateBlock.js b/lib/models/templateBlock.js index 999898cd1..4e47da789 100644 --- a/lib/models/templateBlock.js +++ b/lib/models/templateBlock.js @@ -77,7 +77,7 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext) { var that = this; var name = this.getName(); var endTag = this.getEndTag(); - var blocks = this.getBlocks(); + var blocks = this.getBlocks().toJS(); function Ext() { this.tags = [name]; From a5f4596f8d863d8eb7061ef50f78346bde25d7ac Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 19:00:35 +0200 Subject: [PATCH 116/118] Add tests for templateEngine --- lib/models/__tests__/templateEngine.js | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/models/__tests__/templateEngine.js diff --git a/lib/models/__tests__/templateEngine.js b/lib/models/__tests__/templateEngine.js new file mode 100644 index 000000000..6f18b1848 --- /dev/null +++ b/lib/models/__tests__/templateEngine.js @@ -0,0 +1,51 @@ + +describe('TemplateBlock', function() { + var TemplateEngine = require('../templateEngine'); + + describe('create', function() { + it('must initialize with a list of filters', function() { + var engine = TemplateEngine.create({ + filters: { + hello: function(name) { + return 'Hello ' + name + '!'; + } + } + }); + var env = engine.toNunjucks(); + var res = env.renderString('{{ "Luke"|hello }}'); + + expect(res).toBe('Hello Luke!'); + }); + + it('must initialize with a list of globals', function() { + var engine = TemplateEngine.create({ + globals: { + hello: function(name) { + return 'Hello ' + name + '!'; + } + } + }); + var env = engine.toNunjucks(); + var res = env.renderString('{{ hello("Luke") }}'); + + expect(res).toBe('Hello Luke!'); + }); + + it('must pass context to filters and blocks', function() { + var engine = TemplateEngine.create({ + filters: { + hello: function(name) { + return 'Hello ' + name + ' ' + this.lastName + '!'; + } + }, + context: { + lastName: 'Skywalker' + } + }); + var env = engine.toNunjucks(); + var res = env.renderString('{{ "Luke"|hello }}'); + + expect(res).toBe('Hello Luke Skywalker!'); + }); + }); +}); \ No newline at end of file From b51470db398e7da090805da666d2a6cc9c0b3deb Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 20:06:43 +0200 Subject: [PATCH 117/118] Add tests for conrefs loader --- lib/models/__tests__/templateBlock.js | 1 - lib/templating/__tests__/conrefsLoader.js | 34 +++++++++++++++++++++++ lib/templating/conrefsLoader.js | 4 +-- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 lib/templating/__tests__/conrefsLoader.js diff --git a/lib/models/__tests__/templateBlock.js b/lib/models/__tests__/templateBlock.js index 9f0e9b778..44d53de63 100644 --- a/lib/models/__tests__/templateBlock.js +++ b/lib/models/__tests__/templateBlock.js @@ -4,7 +4,6 @@ var Promise = require('../../utils/promise'); describe('TemplateBlock', function() { var TemplateBlock = require('../templateBlock'); - var TemplateEngine = require('../templateEngine'); describe('create', function() { pit('must initialize a simple TemplateBlock from a function', function() { diff --git a/lib/templating/__tests__/conrefsLoader.js b/lib/templating/__tests__/conrefsLoader.js new file mode 100644 index 000000000..3480a48bd --- /dev/null +++ b/lib/templating/__tests__/conrefsLoader.js @@ -0,0 +1,34 @@ +var TemplateEngine = require('../../models/templateEngine'); +var renderTemplate = require('../render'); + +describe('ConrefsLoader', function() { + var ConrefsLoader = require('../conrefsLoader'); + + var engine = TemplateEngine({ + loader: new ConrefsLoader(__dirname) + }); + + describe('Git', function() { + pit('should include content from git', function() { + return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}') + .then(function(str) { + expect(str).toBe('Hello from git'); + }); + }); + + pit('should handle deep inclusion (1)', function() { + return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}') + .then(function(str) { + expect(str).toBe('First Hello. Hello from git'); + }); + }); + + pit('should handle deep inclusion (2)', function() { + return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}') + .then(function(str) { + expect(str).toBe('First Hello. Hello from git'); + }); + }); + }); +}); + diff --git a/lib/templating/conrefsLoader.js b/lib/templating/conrefsLoader.js index e46b4d0fc..c3e5048d6 100644 --- a/lib/templating/conrefsLoader.js +++ b/lib/templating/conrefsLoader.js @@ -48,9 +48,9 @@ var ConrefsLoader = nunjucks.Loader.extend({ resolve: function(from, to) { // If origin is in the book, we enforce result file to be in the book - if (PathUtils.isInRoot(this.root, from)) { + if (PathUtils.isInRoot(this.rootFolder, from)) { var href = LocationUtils.toAbsolute(to, path.dirname(from), ''); - return PathUtils.resolveInRoot(this.root, href); + return PathUtils.resolveInRoot(this.rootFolder, href); } // If origin is in a git repository, we resolve file in the git repository From 80b8e340dadc54377ff40500f86b1de631395806 Mon Sep 17 00:00:00 2001 From: Samy Pesse Date: Sat, 30 Apr 2016 20:08:04 +0200 Subject: [PATCH 118/118] Fix add global env to templateEngine --- lib/models/templateEngine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/templateEngine.js b/lib/models/templateEngine.js index 0d0a015a6..243bfc633 100644 --- a/lib/models/templateEngine.js +++ b/lib/models/templateEngine.js @@ -107,7 +107,7 @@ TemplateEngine.prototype.toNunjucks = function() { }); // Add globals - globals.forEach(function(globalName, globalValue) { + globals.forEach(function(globalValue, globalName) { env.addGlobal(globalName, globalValue); });