diff options
author | Samy Pessé <samypesse@gmail.com> | 2016-01-22 21:04:36 +0100 |
---|---|---|
committer | Samy Pessé <samypesse@gmail.com> | 2016-01-22 21:04:36 +0100 |
commit | 877f2e477b010f9f37a9044606f110a90f077680 (patch) | |
tree | 5cd61cf3b00ba10dc6110535ed9fdf67d8baba72 | |
parent | c8e2fc0e57d223c01a51d6ee186fc1662cd74d13 (diff) | |
download | gitbook-877f2e477b010f9f37a9044606f110a90f077680.zip gitbook-877f2e477b010f9f37a9044606f110a90f077680.tar.gz gitbook-877f2e477b010f9f37a9044606f110a90f077680.tar.bz2 |
Start rewrite
130 files changed, 856 insertions, 6004 deletions
@@ -26,3 +26,6 @@ node_modules # vim swapfile *.swp + +lib2 +test2
\ No newline at end of file diff --git a/lib/backbone/file.js b/lib/backbone/file.js new file mode 100644 index 0000000..71fc78c --- /dev/null +++ b/lib/backbone/file.js @@ -0,0 +1,61 @@ + +function BackboneFile(book) { + if (!(this instanceof BackboneFile)) return new BackboneFile(book); + + this.book = book; + this.log = this.book.log; + + // Filename in the book + this.filename = ''; + this.parser; +} + +// Type of the backbone file +BackboneFile.prototype.type = ''; + +// Parse a backbone file +BackboneFile.prototype.parse = function() { + +}; + +// Return true if backbone file exists +BackboneFile.prototype.exists = function() { + return Boolean(this.filename); +}; + +// 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.filename = 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.filename); + + return this.locate() + .then(function() { + if (!that.filename) return; + + that.log.debug.ln(that.type, 'located at', that.filename); + + return that.book.readFile(that.filename) + + // Parse it + .then(function(content) { + return that.parse(content); + }); + }); +}; + +module.exports = BackboneFile; diff --git a/lib/backbone/glossary.js b/lib/backbone/glossary.js new file mode 100644 index 0000000..aba83cf --- /dev/null +++ b/lib/backbone/glossary.js @@ -0,0 +1,8 @@ + +function Glossary() { + if (!(this instanceof Glossary)) return new Glossary(); +} + +Glossary.prototype.type = 'glossary'; + +module.exports = Glossary; diff --git a/lib/backbone/index.js b/lib/backbone/index.js new file mode 100644 index 0000000..4c3c3f3 --- /dev/null +++ b/lib/backbone/index.js @@ -0,0 +1,8 @@ + +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 new file mode 100644 index 0000000..2818519 --- /dev/null +++ b/lib/backbone/langs.js @@ -0,0 +1,15 @@ + +function Langs() { + if (!(this instanceof Langs)) return new Langs(); + + this.languages = []; +} + +Langs.prototype.type = 'langs'; + +// Return the count of languages +Langs.prototype.count = function() { + return this.languages.length; +}; + +module.exports = Langs; diff --git a/lib/backbone/readme.js b/lib/backbone/readme.js new file mode 100644 index 0000000..a4cd9d8 --- /dev/null +++ b/lib/backbone/readme.js @@ -0,0 +1,26 @@ +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'; + +// Parse the readme content +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 new file mode 100644 index 0000000..e2cd485 --- /dev/null +++ b/lib/backbone/summary.js @@ -0,0 +1,9 @@ + +function Summary() { + if (!(this instanceof Summary)) return new Summary(); +} + +Summary.prototype.type = 'summary'; + + +module.exports = Summary; diff --git a/lib/blocks.js b/lib/blocks.js deleted file mode 100644 index 92097a7..0000000 --- a/lib/blocks.js +++ /dev/null @@ -1,11 +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 extent by plugins - code: _.identity -}; diff --git a/lib/book.js b/lib/book.js index 9ba27f3..3bb5bf1 100644 --- a/lib/book.js +++ b/lib/book.js @@ -1,25 +1,27 @@ -var Q = require('q'); var _ = require('lodash'); +var Q = require('q'); var path = require('path'); +var Ignore = require('ignore'); var parsers = require('gitbook-parsers'); -var fs = require('./utils/fs'); -var parseNavigation = require('./utils/navigation'); -var parseProgress = require('./utils/progress'); -var pageUtil = require('./utils/page'); +var Logger = require('./logger'); +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 links = require('./utils/links'); -var i18n = require('./utils/i18n'); -var logger = require('./utils/logger'); -var Configuration = require('./configuration'); -var TemplateEngine = require('./template'); -var PluginsList = require('./pluginslist'); +function Book(opts) { + if (!(this instanceof Book)) return new Book(opts); -var generators = require('./generators'); + opts = _.defaults(opts || {}, { + fs: null, + + // Root path for the book + root: '', -var Book = function(root, context, parent) { - this.context = _.defaults(context || {}, { // Extend book configuration config: {}, @@ -32,572 +34,116 @@ var Book = function(root, context, parent) { logLevel: 'info' }); - // Log - this.log = logger(this.context.log, this.context.logLevel); - - // Root folder of the book - this.root = path.resolve(root); + if (!opts.fs) throw new Error('Book requires a fs instance'); - // Parent book - this.parent = parent; + // Root path for the book + this.root = opts.root; - // Configuration - this.config = new Configuration(this, this.context.config); - Object.defineProperty(this, 'options', { - get: function () { - return this.config.options; - } - }); - - // Template - this.template = new TemplateEngine(this); + // If multi-lingual, book can have a parent + this.parent = opts.parent; - // Summary - this.summary = {}; - this.navigation = []; + // A book is linked to an fs, to access its content + this.fs = opts.fs; - // Glossary - this.glossary = []; + // Rules to ignore some files + this.ignore = Ignore(); + this.ignore.addPattern(['.git', '.svn', '.DS_Store']); - // Langs - this.langs = []; + // Create a logger for the book + this.log = new Logger(opts.log, opts.logLevel); - // Sub-books - this.books = []; + // Create an interface to access the configuration + this.config = new Config(this, opts.config); - // Files in the book - this.files = []; + // Interfaces for the book structure + this.readme = new Readme(this); + this.summary = new Summary(this); + this.glossary = new Glossary(this); + this.langs = new Langs(this); - // List of plugins - this.plugins = new PluginsList(this); + // Map of pages in the bok + this.pages = {}; - // Structure files - this.summaryFile = null; - this.glossaryFile = null; - this.readmeFile = null; - this.langsFile = null; - - // Bind methods _.bindAll(this); -}; +} -// Return string representation -Book.prototype.toString = function() { - return '[Book '+this.root+']'; +// Parse and prepare the configuration, fail if invalid +Book.prototype.prepareConfig = function() { + return this.config.load(); }; -// Initialize and parse the book: config, summary, glossary -Book.prototype.parse = function() { - var that = this; - var multilingual = false; - - return this.parseConfig() - - .then(function() { - return that.parsePlugins(); - }) - - .then(function() { - return that.parseLangs() - .then(function() { - multilingual = that.langs.length > 0; - if (multilingual) that.log.info.ln('Parsing multilingual book, with', that.langs.length, 'languages'); - - // Sub-books that inherit from the current book configuration - that.books = _.map(that.langs, function(lang) { - that.log.info.ln('Preparing language book', lang.lang); - return new Book( - path.join(that.root, lang.path), - _.merge({}, that.context, { - config: _.extend({}, that.options, { - 'output': path.join(that.options.output, lang.lang), - 'language': lang.lang - }) - }), - that - ); - }); - }); - }) - - .then(function() { - if (multilingual) return; - return that.listAllFiles(); - }) - .then(function() { - if (multilingual) return; - return that.parseReadme(); - }) - .then(function() { - if (multilingual) return; - return that.parseSummary(); - }) - .then(function() { - if (multilingual) return; - return that.parseGlossary(); - }) - - .then(function() { - // Init sub-books - return _.reduce(that.books, function(prev, book) { - return prev.then(function() { - return book.parse(); - }); - }, Q()); - }) - - .thenResolve(this); +// Resolve a path in the book source +// Enforce that the output path in the root folder +Book.prototype.resolve = function() { + return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments))); }; -// Generate the output -Book.prototype.generate = function(generator) { +// Parse .gitignore, etc to extract rules +Book.prototype.parseIgnoreRules = function() { var that = this; - that.options.generator = generator || that.options.generator; - that.log.info.ln('start generation with', that.options.generator, 'generator'); - return Q() - - // Clean output folder - .then(function() { - that.log.info('clean', that.options.generator, 'generator'); - return fs.clean(that.options.output) - .progress(function(p) { - that.log.debug.ln('remove', p.file, '('+p.i+'/'+p.count+')'); + return _.reduce([ + '.ignore', + '.gitignore', + '.bookignore' + ], function(prev, filename) { + return prev.then(function() { + return that.readFile(filename); }) - .then(function() { - that.log.info.ok(); - }); - }) - - // Create generator - .then(function() { - var Generator = generators[generator]; - if (!Generator) throw 'Generator \''+that.options.generator+'\' doesn\'t exist'; - generator = new Generator(that); - - return generator.prepare(); - }) - - // Transform configuration - .then(function() { - return that.callHook('config', that.config.dump()) - .then(function(newConfig) { - that.config.replace(newConfig); + .then(function(content) { + that.ignore.addPattern(content.toString().split(/\r?\n/)); }); - }) - - // Generate content - .then(function() { - if (that.isMultilingual()) { - return that.generateMultiLingual(generator); - } else { - // Separate list of files into the different operations needed - var ops = _.groupBy(that.files, function(file) { - if (file[file.length -1] == '/') { - return 'directories'; - } else if (_.contains(parsers.extensions, path.extname(file)) && that.navigation[file]) { - return 'content'; - } else { - return 'files'; - } - }); - - - return Q() - - // First, let's create folder - .then(function() { - return _.reduce(ops.directories || [], function(prev, folder) { - return prev.then(function() { - that.log.debug.ln('transferring folder', folder); - return Q(generator.transferFolder(folder)); - }); - }, Q()); - }) - - // Then, let's copy other files - .then(function() { - return Q.all(_.map(ops.files || [], function(file) { - that.log.debug.ln('transferring file', file); - return Q(generator.transferFile(file)); - })); - }) - - // Finally let's generate content - .then(function() { - var nFiles = (ops.content || []).length; - return _.reduce(ops.content || [], function(prev, file, i) { - return prev.then(function() { - var p = ((i*100)/nFiles).toFixed(0)+'%'; - that.log.debug.ln('processing', file, p); - - return Q(generator.convertFile(file)) - .fail(function(err) { - // Transform error message to signal file - throw that.normError(err, { - fileName: file - }); - }); - }); - }, Q()); - }); - } - }) - - // Finish generation - .then(function() { - return that.callHook('finish:before'); - }) - .then(function() { - return generator.finish(); - }) - .then(function() { - return that.callHook('finish'); - }) - .then(function() { - that.log.info.ln('generation is finished'); - }); + }, Q()); }; -// Generate the output for a multilingual book -Book.prototype.generateMultiLingual = function() { +// Parse the whole book +Book.prototype.parse = function() { var that = this; return Q() - .then(function() { - // Generate sub-books - return _.reduce(that.books, function(prev, book) { - return prev.then(function() { - return book.generate(that.options.generator); - }); - }, Q()); - }); -}; - -// Extract files from ebook generated -Book.prototype.generateFile = function(output, options) { - var book = this; - - options = _.defaults(options || {}, { - ebookFormat: path.extname(output).slice(1) - }); - output = output || path.resolve(book.root, 'book.'+options.ebookFormat); - - return fs.tmp.dir() - .then(function(tmpDir) { - book.setOutput(tmpDir); - - return book.generate(options.ebookFormat) - .then(function() { - var copyFile = function(lang) { - var _outputFile = output; - var _tmpDir = tmpDir; - - if (lang) { - _outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+'_'+lang+path.extname(_outputFile); - _tmpDir = path.join(_tmpDir, lang); - } - - book.log.debug.ln('copy ebook to', _outputFile); - return fs.copy( - path.join(_tmpDir, 'index.'+options.ebookFormat), - _outputFile - ); - }; - - // Multi-langs book - return Q() - .then(function() { - if (book.isMultilingual()) { - return Q.all( - _.map(book.langs, function(lang) { - return copyFile(lang.lang); - }) - ) - .thenResolve(book.langs.length); - } else { - return copyFile().thenResolve(1); - } - }) - .then(function(n) { - book.log.info.ok(n+' file(s) generated'); - - return fs.remove(tmpDir); - }); - }); - }); -}; + .then(this.prepareConfig) + .then(this.parseIgnoreRules) -// Parse configuration -Book.prototype.parseConfig = function() { - var that = this; - - that.log.info('loading book configuration....'); - return that.config.load() + // Parse languages .then(function() { - that.log.info.ok(); - }); -}; - -// Parse list of plugins -Book.prototype.parsePlugins = function() { - var that = this; - - // Load plugins - return that.plugins.load(that.options.plugins) - .then(function() { - if (_.size(that.plugins.failed) > 0) return Q.reject(new Error('Error loading plugins: '+that.plugins.failed.join(',')+'. Run \'gitbook install\' to install plugins from NPM.')); - - that.log.info.ok(that.plugins.count()+' plugins loaded'); - that.log.debug.ln('normalize plugins list'); - }); -}; - -// Parse readme to extract defaults title and description -Book.prototype.parseReadme = function() { - var that = this; - var filename = that.config.getStructure('readme', true); - that.log.debug.ln('start parsing readme:', filename); - - return that.findFile(filename) - .then(function(readme) { - if (!readme) throw 'No README file'; - if (!_.contains(that.files, readme.path)) throw 'README file is ignored'; - - that.readmeFile = readme.path; - that._defaultsStructure(that.readmeFile); - - that.log.debug.ln('readme located at', that.readmeFile); - return that.template.renderFile(that.readmeFile) - .then(function(content) { - return readme.parser.readme(content) - .fail(function(err) { - throw that.normError(err, { - name: err.name || 'Readme Parse Error', - fileName: that.readmeFile - }); - }); - }); + return that.langs.load(); }) - .then(function(readme) { - that.options.title = that.options.title || readme.title; - that.options.description = that.options.description || readme.description; - }); -}; - -// Parse langs to extract list of sub-books -Book.prototype.parseLangs = function() { - var that = this; - - var filename = that.config.getStructure('langs', true); - that.log.debug.ln('start parsing languages index:', filename); - - return that.findFile(filename) - .then(function(langs) { - if (!langs) return []; - - that.langsFile = langs.path; - that._defaultsStructure(that.langsFile); - - that.log.debug.ln('languages index located at', that.langsFile); - return that.template.renderFile(that.langsFile) - .then(function(content) { - return langs.parser.langs(content) - .fail(function(err) { - throw that.normError(err, { - name: err.name || 'Langs Parse Error', - fileName: that.langsFile - }); - }); - }); - }) - .then(function(langs) { - that.langs = langs; - }); -}; + .then(function() { + if (that.isMultilingual()) return; -// Parse summary to extract list of chapters -Book.prototype.parseSummary = function() { - var that = this; + return Q() + .then(that.readme.load) + .then(function() { + if (that.readme.exists()) return; - var filename = that.config.getStructure('summary', true); - that.log.debug.ln('start parsing summary:', filename); + throw new Error('No README file (or is ignored)'); + }) - return that.findFile(filename) - .then(function(summary) { - if (!summary) throw 'No SUMMARY file'; + .then(that.summary.load) + .then(function() { + if (that.summary.exists()) return; - // Remove the summary from the list of files to parse - that.summaryFile = summary.path; - that._defaultsStructure(that.summaryFile); - that.files = _.without(that.files, that.summaryFile); + throw new Error('No SUMMARY file (or is ignored)'); + }) - that.log.debug.ln('summary located at', that.summaryFile); - return that.template.renderFile(that.summaryFile) - .then(function(content) { - return summary.parser.summary(content, { - entryPoint: that.readmeFile, - entryPointTitle: that.i18n('SUMMARY_INTRODUCTION'), - files: that.files - }) - .fail(function(err) { - throw that.normError(err, { - name: err.name || 'Summary Parse Error', - fileName: that.summaryFile - }); - }); - }); - }) - .then(function(summary) { - that.summary = summary; - that.navigation = parseNavigation(that.summary, that.files); + .then(that.glossary.load); }); }; -// Parse glossary to extract terms -Book.prototype.parseGlossary = function() { - var that = this; - - var filename = that.config.getStructure('glossary', true); - that.log.debug.ln('start parsing glossary: ', filename); - - return that.findFile(filename) - .then(function(glossary) { - if (!glossary) return []; - - // Remove the glossary from the list of files to parse - that.glossaryFile = glossary.path; - that._defaultsStructure(that.glossaryFile); - that.files = _.without(that.files, that.glossaryFile); - - that.log.debug.ln('glossary located at', that.glossaryFile); - return that.template.renderFile(that.glossaryFile) - .then(function(content) { - return glossary.parser.glossary(content) - .fail(function(err) { - throw that.normError(err, { - name: err.name || 'Glossary Parse Error', - fileName: that.glossaryFile - }); - }); - }); - }) - .then(function(glossary) { - that.glossary = glossary; - }); +// Test if a file is ignored, return true if it is +Book.prototype.isFileIgnored = function(filename) { + return this.ignore.filter([filename]).length == 0; }; -// Parse a page -Book.prototype.parsePage = function(filename, options) { - var that = this, page = {}; - options = _.defaults(options || {}, { - // Transform svg images - convertImages: false, - - // Interpolate before templating - interpolateTemplate: _.identity, - - // Interpolate after templating - interpolateContent: _.identity - }); - - var interpolate = function(fn) { - return Q(fn(page)) - .then(function(_page) { - page = _page || page; - }); - }; - - that.log.debug.ln('start parsing file', filename); - - var extension = path.extname(filename); - var filetype = parsers.get(extension); - - if (!filetype) return Q.reject(new Error('Can\'t parse file: '+filename)); - - // Type of parser used - page.type = filetype.name; - - // Path relative to book - page.path = filename; - - // Path absolute in the system - page.rawPath = path.resolve(that.root, filename); - - // Progress in the book - page.progress = parseProgress(that.navigation, filename); - - that.log.debug.ln('render template', filename); - - // Read file content - return that.readFile(page.path) - .then(function(content) { - page.content = content; - - return interpolate(options.interpolateTemplate); - }) - - // Prepare page markup - .then(function() { - return filetype.page.prepare(page.content) - .then(function(content) { - page.content = content; - }); - }) - - // Generate template - .then(function() { - return that.template.renderPage(page); - }) - - // Prepare and Parse markup - .then(function(content) { - page.content = content; - - that.log.debug.ln('use file parser', filetype.name, 'for', filename); - return filetype.page(page.content); - }) - - // Post process sections - .then(function(_page) { - return _.reduce(_page.sections, function(prev, section) { - return prev.then(function(_sections) { - return that.template.postProcess(section.content || '') - .then(function(content) { - section.content = content; - return _sections.concat([section]); - }); - }); - }, Q([])); - }) - - // Prepare html - .then(function(_sections) { - return pageUtil.normalize(_sections, { - book: that, - convertImages: options.convertImages, - input: filename, - navigation: that.navigation, - base: path.dirname(filename) || './', - output: path.dirname(filename) || './', - glossary: that.glossary - }); - }) - - // Interpolate output - .then(function(_sections) { - page.sections = _sections; - return interpolate(options.interpolateContent); - }) - - .then(function() { - return page; - }); +// Read a file in the book, throw error if ignored +Book.prototype.readFile = function(filename) { + if (this.isFileIgnored(filename)) return Q.reject(new Error('File "'+filename+'" is ignored')); + return this.fs.readAsString(this.resolve(filename)); }; -// Find file that can be parsed with a specific filename -Book.prototype.findFile = function(filename) { +// Find a parsable file using a filename +Book.prototype.findParsableFile = function(filename) { var that = this; var ext = path.extname(filename); @@ -614,7 +160,7 @@ Book.prototype.findFile = function(filename) { var filepath = basename+ext; - return fs.findFile(that.root, filepath) + return that.fs.findFile(that.root, filepath) .then(function(realFilepath) { if (!realFilepath) return null; @@ -627,197 +173,15 @@ Book.prototype.findFile = function(filename) { }, Q(null)); }; -// Format a string using a specific markup language -Book.prototype.formatString = function(extension, content) { - return Q() - .then(function() { - var filetype = parsers.get(extension); - if (!filetype) throw new Error('Filetype doesn\'t exist: '+filetype); - - return filetype.page(content); - }) - - // Merge sections - .then(function(page) { - return _.reduce(page.sections, function(content, section) { - return content + section.content; - }, ''); - }); -}; - -// Check if a file exists in the book -Book.prototype.fileExists = function(filename) { - return fs.exists( - this.resolve(filename) - ); -}; - -// Check if a file path is inside the book -Book.prototype.fileIsInBook = function(filename) { - return pathUtil.isInRoot(this.root, filename); -}; - -// Read a file -Book.prototype.readFile = function(filename) { - return fs.readFile( - this.resolve(filename), - { encoding: 'utf8' } - ); -}; - -// Return stat for a file -Book.prototype.statFile = function(filename) { - return fs.stat(this.resolve(filename)); -}; - -// List all files in the book -Book.prototype.listAllFiles = function() { - var that = this; - - return fs.list(this.root, { - ignoreFiles: ['.ignore', '.gitignore', '.bookignore'], - ignoreRules: [ - // Skip Git stuff - '.git/', - '.gitignore', - - // Skip OS X meta data - '.DS_Store', - - // Skip stuff installed by plugins - 'node_modules', - - // Skip book outputs - '_book', - '*.pdf', - '*.epub', - '*.mobi', - - // Skip config files - '.ignore', - '.bookignore', - 'book.json', - ] - }) - .then(function(_files) { - that.files = _files; - }); +// 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 a multilingual book +// Return true if the book is main instance of a multilingual book Book.prototype.isMultilingual = function() { - return this.books.length > 0; -}; - -// Return root of the parent -Book.prototype.parentRoot = function() { - if (this.parent) return this.parent.parentRoot(); - return this.root; -}; - -// Return true if it's a sub-book -Book.prototype.isSubBook = function() { - return !!this.parent; -}; - -// Test if the file is the entry point -Book.prototype.isEntryPoint = function(fp) { - return fp == this.readmeFile; -}; - -// Alias to book.config.get -Book.prototype.getConfig = function(key, def) { - return this.config.get(key, def); -}; - -// Resolve a path in the book source -// Enforce that the output path in the root folder -Book.prototype.resolve = function() { - return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments))); -}; - -// Resolve a path in the book output -// Enforce that the output path in the output folder -Book.prototype.resolveOutput = function() { - return pathUtil.resolveInRoot.apply(null, [this.options.output].concat(_.toArray(arguments))); -}; - -// Convert an abslute path into a relative path to this -Book.prototype.relative = function(p) { - return path.relative(this.root, p); -}; - -// Normalize a path to .html and convert README -> index -Book.prototype.contentPath = function(link) { - if ( - path.basename(link, path.extname(link)) == 'README' || - link == this.readmeFile - ) { - link = path.join(path.dirname(link), 'index'+path.extname(link)); - } - - link = links.changeExtension(link, '.html'); - return link; -}; - -// Normalize a link to .html and convert README -> index -Book.prototype.contentLink = function(link) { - return links.normalize(this.contentPath(link)); -}; - -// Default structure paths to an extension -Book.prototype._defaultsStructure = function(filename) { - var that = this; - var extension = path.extname(filename); - - that.readmeFile = that.readmeFile || that.config.getStructure('readme')+extension; - that.summaryFile = that.summaryFile || that.config.getStructure('summary')+extension; - that.glossaryFile = that.glossaryFile || that.config.getStructure('glossary')+extension; - that.langsFile = that.langsFile || that.config.getStructure('langs')+extension; -}; - -// Change output path -Book.prototype.setOutput = function(p) { - var that = this; - this.options.output = path.resolve(p); - - _.each(this.books, function(book) { - book.setOutput(path.join(that.options.output, book.options.language)); - }); -}; - -// Translate a strign according to the book language -Book.prototype.i18n = function() { - var args = Array.prototype.slice.call(arguments); - return i18n.__.apply({}, [this.config.normalizeLanguage()].concat(args)); -}; - -// Normalize error -Book.prototype.normError = function(err, opts, defs) { - if (_.isString(err)) err = new Error(err); - - // Extend err - _.extend(err, opts || {}); - _.defaults(err, defs || {}); - - err.lineNumber = err.lineNumber || err.lineno; - err.columnNumber = err.columnNumber || err.colno; - - err.toString = function() { - var attributes = []; - - if (this.fileName) attributes.push('In file \''+this.fileName+'\''); - if (this.lineNumber) attributes.push('Line '+this.lineNumber); - if (this.columnNumber) attributes.push('Column '+this.columnNumber); - return (this.name || 'Error')+': '+this.message+((attributes.length > 0)? ' ('+attributes.join(', ')+')' : ''); - }; - - return err; -}; - -// Call a hook in plugins -Book.prototype.callHook = function(name, data) { - return this.plugins.hook(name, data); + return this.langs.count() > 0; }; -module.exports= Book; +module.exports = Book; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..c6d05da --- /dev/null +++ b/lib/config.js @@ -0,0 +1,110 @@ +var Q = require('q'); +var _ = require('lodash'); +var semver = require('semver'); +var path = require('path'); + +var gitbook = require('./gitbook'); +var configDefault = require('./config_default'); + +/* +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.replace(baseConfig || {}); +} + +// Load configuration of the book +// and verify that the configuration is satisfying +Config.prototype.load = function() { + var that = this; + + this.log.debug.ln('loading configuration'); + return this.fs.loadAsObject(this.book.resolve('book')) + .fail(function(err) { + if (err.code != 'MODULE_NOT_FOUND') throw(err); + else return Q({}); + }) + .then(function(_config) { + return that.replace(_config); + }) + .then(function() { + if (!that.book.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.output = path.resolve(that.options.output || that.book.resolve('_book')); + //that.options.plugins = normalizePluginsList(that.options.plugins); + //that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false); + //that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins); + //that.options.plugins = _.uniq(that.options.plugins, 'name'); + + // Default value for text direction (from language) + /*if (!that.options.direction) { + var lang = i18n.getCatalog(that.options.language); + if (lang) that.options.direction = lang.direction; + }*/ + + that.options.gitbook = gitbook.version; + }); +}; + +// Replace the whole configuration +Config.prototype.replace = function(options) { + var that = this; + + this.options = _.cloneDeep(configDefault); + this.options = _.merge(this.options, 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; + } + }); + + // options.originalOutput == book.parent.options.output + Object.defineProperty(this.options, 'originalOutput', { + get: function () { + return that.book.parent? that.book.parent.options.output : undefined; + } + }); +}; + +// 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); +}; + +module.exports = Config; diff --git a/lib/configuration.js b/lib/configuration.js deleted file mode 100644 index dd95585..0000000 --- a/lib/configuration.js +++ /dev/null @@ -1,210 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var path = require('path'); -var semver = require('semver'); - -var pkg = require('../package.json'); -var i18n = require('./utils/i18n'); -var version = require('./version'); - -var DEFAULT_CONFIG = require('./config_default'); - -// Default plugins added to each books -var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings']; - -// Check if a plugin is a default plugin -// Plugin should be in the list -// And version from book.json specified for this plugin should be satisfied -function isDefaultPlugin(name, version) { - if (!_.contains(DEFAULT_PLUGINS, name)) return false; - - try { - var pluginPkg = require('gitbook-plugin-'+name+'/package.json'); - return semver.satisfies(pluginPkg.version, version || '*'); - } catch(e) { - return false; - } -} - -// Normalize a list of plugins to use -function normalizePluginsList(plugins, addDefaults) { - // 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[1]; - return { - 'name': name, - 'version': version, // optional - 'isDefault': isDefaultPlugin(name, version) - }; - }); - - // 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 - if (addDefaults !== false) { - _.each(DEFAULT_PLUGINS, function(plugin) { - if (_.find(plugins, { name: plugin })) { - return; - } - - plugins.push({ - 'name': plugin, - 'isDefault': true - }); - }); - } - - // 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; -} - -var Configuration = function(book, options) { - this.book = book; - this.replace(options); -}; - -// Read and parse the configuration -Configuration.prototype.load = function() { - var that = this; - - return Q() - .then(function() { - var configPath, _config; - - try { - configPath = require.resolve( - that.book.resolve(that.options.configFile) - ); - - // Invalidate node.js cache for livreloading - delete require.cache[configPath]; - - _config = require(configPath); - that.options = _.merge( - that.options, - _.omit(_config, 'configFile', 'defaultsPlugins', 'generator', 'extension') - ); - } - catch(err) { - if (err instanceof SyntaxError) return Q.reject(err); - return Q(); - } - }) - .then(function() { - if (!that.book.isSubBook()) { - if (!version.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(pkg.version, 'patch'), that.options.gitbook)) { - that.book.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(pkg.version.split('.'))+'.x.x')+'\' is more adequate'); - } - } - - that.options.output = path.resolve(that.options.output || that.book.resolve('_book')); - that.options.plugins = normalizePluginsList(that.options.plugins); - that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false); - that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins); - that.options.plugins = _.uniq(that.options.plugins, 'name'); - - // Default value for text direction (from language) - if (!that.options.direction) { - var lang = i18n.getCatalog(that.options.language); - if (lang) that.options.direction = lang.direction; - } - - that.options.gitbook = pkg.version; - }); -}; - -// Extend the configuration -Configuration.prototype.extend = function(options) { - _.extend(this.options, options); -}; - -// Replace the whole configuration -Configuration.prototype.replace = function(options) { - var that = this; - - this.options = _.cloneDeep(DEFAULT_CONFIG); - this.options = _.merge(this.options, 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; - } - }); - - // options.originalOutput == book.parent.options.output - Object.defineProperty(this.options, 'originalOutput', { - get: function () { - return that.book.parent? that.book.parent.options.output : undefined; - } - }); -}; - -// Dump configuration as json object -Configuration.prototype.dump = function() { - return _.cloneDeep(this.options); -}; - -// Get structure file -Configuration.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 normalized language -Configuration.prototype.normalizeLanguage = function() { - return i18n.normalizeLanguage(this.options.language); -}; - -// Return a configuration -Configuration.prototype.get = function(key, def) { - return _.get(this.options, key, def); -}; - -// Update a configuration -Configuration.prototype.set = function(key, value) { - return _.set(this.options, key, value); -}; - -// Default configuration -Configuration.DEFAULT = DEFAULT_CONFIG; - -module.exports= Configuration; diff --git a/lib/conrefs_loader.js b/lib/conrefs_loader.js deleted file mode 100644 index 255bf06..0000000 --- a/lib/conrefs_loader.js +++ /dev/null @@ -1,73 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var nunjucks = require('nunjucks'); - -var git = require('./utils/git'); -var fs = require('./utils/fs'); -var pathUtil = require('./utils/path'); - -// The loader should handle relative and git url -var BookLoader = nunjucks.Loader.extend({ - async: true, - - init: function(book, opts) { - this.opts = _.defaults(opts || {}, { - interpolate: _.identity - }); - this.book = book; - }, - - getSource: function(fileurl, callback) { - var that = this; - - git.resolveFile(fileurl) - .then(function(filepath) { - // Is local file - if (!filepath) filepath = path.resolve(fileurl); - else that.book.log.debug.ln('resolve from git', fileurl, 'to', filepath); - - // Read file from absolute path - return fs.readFile(filepath) - .then(function(source) { - return that.opts.interpolate(filepath, source.toString()); - }) - .then(function(source) { - return { - src: source, - path: filepath, - - // We disable cache sincde content is modified (shortcuts, ...) - noCache: true - }; - }); - }) - .nodeify(callback); - }, - - resolve: function(from, to) { - // If origin is in the book, we enforce result file to be in the book - if (this.book.fileIsInBook(from)) { - return this.book.resolve( - this.book.relative(path.dirname(from)), - to - ); - } - - // If origin is in a git repository, we resolve file in the git repository - var gitRoot = 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); - }, - - // Handle all files as relative, so that nunjucks pass responsability to 'resolve' - // Only git urls are considered as absolute - isRelative: function(filename) { - return !git.checkUrl(filename); - } -}); - -module.exports = BookLoader; diff --git a/lib/fs/index.js b/lib/fs/index.js new file mode 100644 index 0000000..1961b07 --- /dev/null +++ b/lib/fs/index.js @@ -0,0 +1,148 @@ +var Q = require('q'); +var _ = require('lodash'); +var path = require('path'); +var Buffer = require('buffer').Buffer; +var destroy = require('destroy'); + +/* +A filesystem is an interface to read/write 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 +}; + +// Write a file and returns a promise +FS.prototype.write = function(filename, buffer) { + // 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); + })); + }); + } + }); + }, Q([])); + }); +}; + +// 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'); + }); +}; + +// Write a stream to a file and returns a promise +FS.prototype.writeStream = function(filename, stream) { + var bufs = []; + var d = Q.defer(); + + var cleanup = function() { + destroy(stream); + stream.removeAllListeners(); + }; + + stream.on('data', function(d) { + bufs.push(d); + }); + + stream.on('error', function(err) { + cleanup(); + + d.reject(err); + }); + + stream.on('end', function(){ + cleanup(); + + var buf = Buffer.concat(bufs); + d.resolve(buf); + }); + + return d.promise; +}; + +// Copy a file +FS.prototype.copy = function(from, to) { + var that = this; + + return this.read(from) + .then(function(buf) { + return that.write(to, buf); + }); +}; + +// 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 new file mode 100644 index 0000000..0c470d7 --- /dev/null +++ b/lib/fs/node.js @@ -0,0 +1,72 @@ +var Q = require('q'); +var _ = require('lodash'); +var util = require('util'); +var path = require('path'); +var fs = require('fs'); + +var BaseFS = require('./'); + +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) { + var d = Q.defer(); + + fs.exists(filename, function(exists) { + d.resolve(exists); + }); + + return d.promise; +}; + +// Read a file and returns a promise with the content as a buffer +NodeFS.prototype.read = function(filename) { + return Q.nfcall(fs.readFile, filename); +}; + +// Write a file and returns a promise +NodeFS.prototype.write = function(filename, buffer) { + return Q.nfcall(fs.writeFile, filename, buffer); +}; + +// List files in a directory +NodeFS.prototype.readdir = function(folder) { + return Q.nfcall(fs.readdir, folder) + .then(function(files) { + return _.chain(files) + .map(function(file) { + if (file == '.' || file == '..') return; + + var stat = fs.statSync(path.join(folder, file)); + if (stat.isDirectory()) file = file + path.sep; + return file; + }) + .compact() + .value(); + }); +}; + +// Load a JSON/JS file +NodeFS.prototype.loadAsObject = function(filename) { + return Q() + .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 Q.reject(err); + } + }); +}; + +module.exports = NodeFS; diff --git a/lib/generator.js b/lib/generator.js deleted file mode 100644 index 4e280d8..0000000 --- a/lib/generator.js +++ /dev/null @@ -1,76 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var Q = require('q'); -var fs = require('./utils/fs'); - -var BaseGenerator = function(book) { - this.book = book; - - Object.defineProperty(this, 'options', { - get: function () { - return this.book.options; - } - }); - - _.bindAll(this); -}; - -BaseGenerator.prototype.callHook = function(name, data) { - return this.book.callHook(name, data); -}; - -// Prepare the genertor -BaseGenerator.prototype.prepare = function() { - var that = this; - - return that.callHook('init'); -}; - -// Write a parsed file to the output -BaseGenerator.prototype.convertFile = function(input) { - return Q.reject(new Error('Could not convert '+input)); -}; - -// Copy file to the output (non parsable) -BaseGenerator.prototype.transferFile = function(input) { - return fs.copy( - this.book.resolve(input), - path.join(this.options.output, input) - ); -}; - -// Copy a folder to the output -BaseGenerator.prototype.transferFolder = function(input) { - return fs.mkdirp( - path.join(this.book.options.output, input) - ); -}; - -// Copy the cover picture -BaseGenerator.prototype.copyCover = function() { - var that = this; - - return Q.all([ - fs.copy(that.book.resolve('cover.jpg'), path.join(that.options.output, 'cover.jpg')), - fs.copy(that.book.resolve('cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg')) - ]) - .fail(function() { - // If orignaly from multi-lang, try copy from parent - if (!that.book.isSubBook()) return; - - return Q.all([ - fs.copy(path.join(that.book.parentRoot(), 'cover.jpg'), path.join(that.options.output, 'cover.jpg')), - fs.copy(path.join(that.book.parentRoot(), 'cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg')) - ]); - }) - .fail(function() { - return Q(); - }); -}; - -// At teh end of the generation -BaseGenerator.prototype.finish = function() { - return Q.reject(new Error('Could not finish generation')); -}; - -module.exports = BaseGenerator; diff --git a/lib/generators/ebook.js b/lib/generators/ebook.js deleted file mode 100644 index ff804c6..0000000 --- a/lib/generators/ebook.js +++ /dev/null @@ -1,172 +0,0 @@ -var util = require('util'); -var path = require('path'); -var Q = require('q'); -var _ = require('lodash'); -var juice = require('juice'); -var exec = require('child_process').exec; - -var fs = require('../utils/fs'); -var stringUtils = require('../utils/string'); -var BaseGenerator = require('./website'); - -var Generator = function(book, format) { - BaseGenerator.apply(this, arguments); - - // eBook format - this.ebookFormat = format; - - // Resources namespace - this.namespace = 'ebook'; - - // Styles to use - this.styles = _.compact(['print', 'ebook', this.ebookFormat]); - - // Convert images (svg -> png) - this.convertImages = true; -}; -util.inherits(Generator, BaseGenerator); - -Generator.prototype.prepareTemplates = function() { - this.templates.page = this.book.plugins.template('ebook:page') || path.resolve(this.options.theme, 'templates/ebook/page.html'); - this.templates.summary = this.book.plugins.template('ebook:summary') || path.resolve(this.options.theme, 'templates/ebook/summary.html'); - this.templates.glossary = this.book.plugins.template('ebook:glossary') || path.resolve(this.options.theme, 'templates/ebook/glossary.html'); - - return Q(); -}; - -// Generate table of contents -Generator.prototype.writeSummary = function() { - var that = this; - - that.book.log.info.ln('write SUMMARY.html'); - return this._writeTemplate(this.templates.summary, {}, path.join(this.options.output, 'SUMMARY.html')); -}; - -// Return template for footer/header with inlined css -Generator.prototype.getPDFTemplate = function(id) { - var tpl = this.options.pdf[id+'Template']; - var defaultTpl = path.resolve(this.options.theme, 'templates/ebook/'+id+'.html'); - var defaultCSS = path.resolve(this.options.theme, 'assets/ebook/pdf.css'); - - // Default template from theme - if (!tpl && fs.existsSync(defaultTpl)) { - tpl = fs.readFileSync(defaultTpl, { encoding: 'utf-8' }); - } - - // Inline CSS using juice - var stylesheets = []; - - // From theme - if (fs.existsSync(defaultCSS)) { - stylesheets.push(fs.readFileSync(defaultCSS, { encoding: 'utf-8' })); - } - - // Custom PDF style - if (this.styles.pdf) { - stylesheets.push(fs.readFileSync(this.book.resolveOutput(this.styles.pdf), { encoding: 'utf-8' })); - } - - tpl = juice(tpl, { - extraCss: stylesheets.join('\n\n') - }); - - return tpl; -}; - -Generator.prototype.finish = function() { - var that = this; - - return Q() - .then(this.copyAssets) - .then(this.copyCover) - .then(this.writeGlossary) - .then(this.writeSummary) - .then(function() { - if (!that.ebookFormat) return Q(); - - if (!that.options.cover && fs.existsSync(path.join(that.options.output, 'cover.jpg'))) { - that.options.cover = path.join(that.options.output, 'cover.jpg'); - } - - var d = Q.defer(); - - var _options = { - '--cover': that.options.cover, - '--title': that.options.title, - '--comments': that.options.description, - '--isbn': that.options.isbn, - '--authors': that.options.author, - '--language': that.options.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.ebookFormat == 'pdf') { - var pdfOptions = that.options.pdf; - - _.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) - }); - } else if (that.ebookFormat == 'epub') { - _.extend(_options, { - '--dont-split-on-page-breaks': true - }); - } - - var command = [ - 'ebook-convert', - path.join(that.options.output, 'SUMMARY.html'), - path.join(that.options.output, 'index.'+that.ebookFormat), - stringUtils.optionsToShellArgs(_options) - ].join(' '); - - that.book.log.info('start conversion to', that.ebookFormat, '....'); - - var child = exec(command, function (error, stdout) { - if (error) { - that.book.log.info.fail(); - - if (error.code == 127) { - error.message = 'Need to install ebook-convert from Calibre'; - } else { - error.message = error.message + ' '+stdout; - } - return d.reject(error); - } - - that.book.log.info.ok(); - d.resolve(); - }); - - child.stdout.on('data', function (data) { - that.book.log.debug(data); - }); - - child.stderr.on('data', function (data) { - that.book.log.debug(data); - }); - - return d.promise; - }); -}; - -module.exports = Generator; diff --git a/lib/generators/index.js b/lib/generators/index.js deleted file mode 100644 index 068d0d9..0000000 --- a/lib/generators/index.js +++ /dev/null @@ -1,11 +0,0 @@ -var _ = require("lodash"); -var EbookGenerator = require("./ebook"); - -module.exports = { - json: require("./json"), - website: require("./website"), - ebook: EbookGenerator, - pdf: _.partialRight(EbookGenerator, "pdf"), - mobi: _.partialRight(EbookGenerator, "mobi"), - epub: _.partialRight(EbookGenerator, "epub") -}; diff --git a/lib/generators/json.js b/lib/generators/json.js deleted file mode 100644 index 37ffa0b..0000000 --- a/lib/generators/json.js +++ /dev/null @@ -1,76 +0,0 @@ -var util = require('util'); -var path = require('path'); -var Q = require('q'); -var _ = require('lodash'); - -var fs = require('../utils/fs'); -var BaseGenerator = require('../generator'); -var links = require('../utils/links'); - -var Generator = function() { - BaseGenerator.apply(this, arguments); -}; -util.inherits(Generator, BaseGenerator); - -// Ignore some methods -Generator.prototype.transferFile = function() { }; - -// Convert an input file -Generator.prototype.convertFile = function(input) { - var that = this; - - return that.book.parsePage(input) - .then(function(page) { - var json = { - progress: page.progress, - sections: page.sections - }; - - var output = links.changeExtension(page.path, '.json'); - output = path.join(that.options.output, output); - - return fs.writeFile( - output, - JSON.stringify(json, null, 4) - ); - }); -}; - -// Finish generation -Generator.prototype.finish = function() { - return this.writeReadme(); -}; - -// Write README.json -Generator.prototype.writeReadme = function() { - var that = this; - var mainLang, langs, readme; - - return Q() - .then(function() { - langs = that.book.langs; - mainLang = langs.length > 0? _.first(langs).lang : null; - - readme = links.changeExtension(that.book.readmeFile, '.json'); - - // Read readme from main language - return fs.readFile( - mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme) - ); - }) - .then(function(content) { - // Extend it with infos about the languages - var json = JSON.parse(content); - _.extend(json, { - langs: langs - }); - - // Write it as README.json - return fs.writeFile( - path.join(that.options.output, 'README.json'), - JSON.stringify(json, null, 4) - ); - }); -}; - -module.exports = Generator; diff --git a/lib/generators/website.js b/lib/generators/website.js deleted file mode 100644 index efb7c0f..0000000 --- a/lib/generators/website.js +++ /dev/null @@ -1,268 +0,0 @@ -var util = require('util'); -var path = require('path'); -var Q = require('q'); -var _ = require('lodash'); - -var nunjucks = require('nunjucks'); -var AutoEscapeExtension = require('nunjucks-autoescape')(nunjucks); -var FilterExtension = require('nunjucks-filter')(nunjucks); - -var fs = require('../utils/fs'); -var BaseGenerator = require('../generator'); -var links = require('../utils/links'); -var i18n = require('../utils/i18n'); - -var pkg = require('../../package.json'); - -var Generator = function() { - BaseGenerator.apply(this, arguments); - - // Revision - this.revision = new Date(); - - // Resources namespace - this.namespace = 'website'; - - // Style to integrates in the output - this.styles = ['website']; - - // Convert images (svg -> png) - this.convertImages = false; - - // Templates - this.templates = {}; -}; -util.inherits(Generator, BaseGenerator); - -// Prepare the genertor -Generator.prototype.prepare = function() { - return BaseGenerator.prototype.prepare.apply(this) - .then(this.prepareStyles) - .then(this.prepareTemplates) - .then(this.prepareTemplateEngine); -}; - -// Prepare all styles -Generator.prototype.prepareStyles = function() { - var that = this; - - this.styles = _.chain(this.styles) - .map(function(style) { - var stylePath = that.options.styles[style]; - var styleExists = ( - fs.existsSync(that.book.resolveOutput(stylePath)) || - fs.existsSync(that.book.resolve(stylePath)) - ); - - if (stylePath && styleExists) { - return [style, stylePath]; - } - return null; - }) - .compact() - .object() - .value(); - - return Q(); -}; - -// Prepare templates -Generator.prototype.prepareTemplates = function() { - this.templates.page = this.book.plugins.template('site:page') || path.resolve(this.options.theme, 'templates/website/page.html'); - this.templates.langs = this.book.plugins.template('site:langs') || path.resolve(this.options.theme, 'templates/website/langs.html'); - this.templates.glossary = this.book.plugins.template('site:glossary') || path.resolve(this.options.theme, 'templates/website/glossary.html'); - - return Q(); -}; - -// Prepare template engine -Generator.prototype.prepareTemplateEngine = function() { - var that = this; - - return Q() - .then(function() { - var language = that.book.config.normalizeLanguage(); - - if (!i18n.hasLocale(language)) { - that.book.log.warn.ln('Language "'+language+'" is not available as a layout locales (en, '+i18n.getLocales().join(', ')+')'); - } - - var folders = _.chain(that.templates) - .values() - .map(path.dirname) - .uniq() - .value(); - - that.env = new nunjucks.Environment( - new nunjucks.FileSystemLoader(folders), - { - autoescape: true - } - ); - - // Add filter - that.env.addFilter('contentLink', that.book.contentLink.bind(that.book)); - that.env.addFilter('lvl', function(lvl) { - return lvl.split('.').length; - }); - - // Add extension - that.env.addExtension('AutoEscapeExtension', new AutoEscapeExtension(that.env)); - that.env.addExtension('FilterExtension', new FilterExtension(that.env)); - }); -}; - -// Finis generation -Generator.prototype.finish = function() { - return this.copyAssets() - .then(this.copyCover) - .then(this.writeGlossary) - .then(this.writeLangsIndex); -}; - -// Convert an input file -Generator.prototype.convertFile = function(input) { - var that = this; - - return that.book.parsePage(input, { - convertImages: that.convertImages, - interpolateTemplate: function(page) { - return that.callHook('page:before', page); - }, - interpolateContent: function(page) { - return that.callHook('page', page); - } - }) - .then(function(page) { - var relativeOutput = that.book.contentPath(page.path); - var output = path.join(that.options.output, relativeOutput); - - var basePath = path.relative(path.dirname(output), that.options.output) || '.'; - if (process.platform === 'win32') basePath = basePath.replace(/\\/g, '/'); - - that.book.log.debug.ln('write parsed file', page.path, 'to', relativeOutput); - - return that._writeTemplate(that.templates.page, { - progress: page.progress, - - _input: page.path, - content: page.sections, - - basePath: basePath, - staticBase: links.join(basePath, 'gitbook') - }, output); - }); -}; - -// Write the index for langs -Generator.prototype.writeLangsIndex = function() { - if (!this.book.langs.length) return Q(); - - return this._writeTemplate(this.templates.langs, { - langs: this.book.langs - }, path.join(this.options.output, 'index.html')); -}; - -// Write glossary -Generator.prototype.writeGlossary = function() { - // No glossary - if (this.book.glossary.length === 0) return Q(); - - return this._writeTemplate(this.templates.glossary, {}, path.join(this.options.output, 'GLOSSARY.html')); -}; - -// Convert a page into a normalized data set -Generator.prototype.normalizePage = function(page) { - var that = this; - - var _callHook = function(name) { - return that.callHook(name, page) - .then(function(_page) { - page = _page; - return page; - }); - }; - - return Q() - .then(function() { - return _callHook('page'); - }) - .then(function() { - return page; - }); -}; - -// Generate a template -Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) { - var that = this; - - interpolate = interpolate || _.identity; - return Q() - .then(function() { - return that.env.render( - tpl, - _.extend({ - gitbook: { - version: pkg.version - }, - - styles: that.styles, - - revision: that.revision, - - title: that.options.title, - description: that.options.description, - language: that.book.config.normalizeLanguage(), - innerlanguage: that.book.isSubBook()? that.book.config.get('language') : null, - - glossary: that.book.glossary, - - summary: that.book.summary, - allNavigation: that.book.navigation, - - plugins: { - resources: that.book.plugins.resources(that.namespace) - }, - pluginsConfig: JSON.stringify(that.options.pluginsConfig), - htmlSnippet: _.partial(_.partialRight(that.book.plugins.html, that, options), that.namespace), - - options: that.options, - - basePath: '.', - staticBase: path.join('.', 'gitbook'), - - '__': that.book.i18n.bind(that.book) - }, options) - ); - }) - .then(interpolate) - .then(function(html) { - return fs.writeFile( - output, - html - ); - }); -}; - -// Copy assets -Generator.prototype.copyAssets = function() { - var that = this; - - // Copy gitbook assets - return fs.copy( - path.join(that.options.theme, 'assets/'+this.namespace), - path.join(that.options.output, 'gitbook') - ) - - // Copy plugins assets - .then(function() { - return Q.all( - _.map(that.book.plugins.list, function(plugin) { - var pluginAssets = path.join(that.options.output, 'gitbook/plugins/', plugin.name); - return plugin.copyAssets(pluginAssets, that.namespace); - }) - ); - }); -}; - -module.exports = Generator; diff --git a/lib/version.js b/lib/gitbook.js index f0ae187..cbdcaed 100644 --- a/lib/version.js +++ b/lib/gitbook.js @@ -4,7 +4,7 @@ var pkg = require('../package.json'); var VERSION = pkg.version; var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, ''); -// Test if current current gitbook version satisfies a condition +// 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) function satisfies(condition) { // Test with real version @@ -14,6 +14,8 @@ function satisfies(condition) { return semver.satisfies(VERSION_STABLE, condition); } + module.exports = { + version: pkg.version, satisfies: satisfies }; diff --git a/lib/index.js b/lib/index.js index a23ec3f..13a572d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,215 +1,5 @@ -/* eslint no-console: 0 */ - -var Q = require('q'); -var _ = require('lodash'); -var path = require('path'); -var tinylr = require('tiny-lr'); -var color = require('bash-color'); - var Book = require('./book'); -var initBook = require('./init'); -var Server = require('./utils/server'); -var stringUtils = require('./utils/string'); -var watch = require('./utils/watch'); -var logger = require('./utils/logger'); - -var LOG_OPTION = { - name: 'log', - description: 'Minimum log level to display', - values: _.chain(logger.LEVELS).keys().map(stringUtils.toLowerCase).value(), - defaults: 'info' -}; - -var FORMAT_OPTION = { - name: 'format', - description: 'Format to build to', - values: ['website', 'json', 'ebook'], - defaults: 'website' -}; - -// Export init to gitbook library -Book.init = initBook; module.exports = { - Book: Book, - LOG_LEVELS: logger.LEVELS, - - commands: _.flatten([ - { - name: 'build [book] [output]', - description: 'build a book', - options: [ - FORMAT_OPTION, - LOG_OPTION - ], - exec: function(args, kwargs) { - var input = args[0] || process.cwd(); - var output = args[1] || path.join(input, '_book'); - - var book = new Book(input, _.extend({}, { - 'config': { - 'output': output - }, - 'logLevel': kwargs.log - })); - - return book.parse() - .then(function() { - return book.generate(kwargs.format); - }) - .then(function(){ - console.log(''); - console.log(color.green('Done, without error')); - }); - } - }, - - _.map(['pdf', 'epub', 'mobi'], function(ebookType) { - return { - name: ebookType+' [book] [output]', - description: 'build a book to '+ebookType, - options: [ - LOG_OPTION - ], - exec: function(args, kwargs) { - var input = args[0] || process.cwd(); - var output = args[1]; - - var book = new Book(input, _.extend({}, { - 'logLevel': kwargs.log - })); - - return book.parse() - .then(function() { - return book.generateFile(output, { - ebookFormat: ebookType - }); - }) - .then(function(){ - console.log(''); - console.log(color.green('Done, without error')); - }); - } - }; - }), - - { - name: 'serve [book]', - description: 'Build then serve a gitbook 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 - }, - FORMAT_OPTION, - LOG_OPTION - ], - exec: function(args, kwargs) { - var input = args[0] || process.cwd(); - var server = new Server(); - - // Init livereload server - var lrServer = tinylr({}); - var lrPath; - - var generate = function() { - if (server.isRunning()) console.log('Stopping server'); - - return server.stop() - .then(function() { - var book = new Book(input, _.extend({}, { - 'config': { - 'defaultsPlugins': ['livereload'] - }, - 'logLevel': kwargs.log - })); - - return book.parse() - .then(function() { - return book.generate(kwargs.format); - }) - .thenResolve(book); - }) - .then(function(book) { - console.log(); - console.log('Starting server ...'); - return server.start(book.options.output, kwargs.port) - .then(function() { - console.log('Serving book on http://localhost:'+kwargs.port); - - if (lrPath) { - // trigger livereload - lrServer.changed({ - body: { - files: [lrPath] - } - }); - } - - if (!kwargs.watch) return; - - return watch(book.root) - .then(function(filepath) { - // set livereload path - lrPath = filepath; - console.log('Restart after change in file', filepath); - console.log(''); - return generate(); - }); - }); - }); - }; - - return Q.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(); - }); - } - }, - - { - name: 'install [book]', - description: 'install plugins dependencies', - exec: function(args) { - var input = args[0] || process.cwd(); - - var book = new Book(input); - - return book.config.load() - .then(function() { - return book.plugins.install(); - }) - .then(function(){ - console.log(''); - console.log(color.green('Done, without error')); - }); - } - }, - - { - name: 'init [directory]', - description: 'create files and folders based on contents of SUMMARY.md', - exec: function(args) { - return initBook(args[0] || process.cwd()) - .then(function(){ - console.log(''); - console.log(color.green('Done, without error')); - }); - } - } - ]) + Book: Book }; diff --git a/lib/init.js b/lib/init.js deleted file mode 100644 index 2fc8016..0000000 --- a/lib/init.js +++ /dev/null @@ -1,83 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var path = require('path'); - -var Book = require('./book'); -var fs = require('./utils/fs'); - -// Initialize folder structure for a book -// Read SUMMARY to created the right chapter -function initBook(root, opts) { - var book = new Book(root, opts); - var extensionToUse = '.md'; - - var chaptersPaths = function(chapters) { - return _.reduce(chapters || [], function(accu, chapter) { - var o = { - title: chapter.title - }; - if (chapter.path) o.path = chapter.path; - - return accu.concat( - [o].concat(chaptersPaths(chapter.articles)) - ); - }, []); - }; - - book.log.info.ln('init book at', root); - return fs.mkdirp(root) - .then(function() { - book.log.info.ln('detect structure from SUMMARY (if it exists)'); - return book.parseSummary(); - }) - .fail(function() { - return Q(); - }) - .then(function() { - var summary = book.summaryFile || 'SUMMARY.md'; - var chapters = book.summary.chapters || []; - extensionToUse = path.extname(summary); - - if (chapters.length === 0) { - chapters = [ - { - title: 'Summary', - path: 'SUMMARY'+extensionToUse - }, - { - title: 'Introduction', - path: 'README'+extensionToUse - } - ]; - } - - return Q(chaptersPaths(chapters)); - }) - .then(function(chapters) { - // Create files that don't exist - return Q.all(_.map(chapters, function(chapter) { - if (!chapter.path) return Q(); - var absolutePath = path.resolve(book.root, chapter.path); - - return fs.exists(absolutePath) - .then(function(exists) { - if(exists) { - book.log.info.ln('found', chapter.path); - return; - } else { - book.log.info.ln('create', chapter.path); - } - - return fs.mkdirp(path.dirname(absolutePath)) - .then(function() { - return fs.writeFile(absolutePath, '# '+chapter.title+'\n'); - }); - }); - })); - }) - .then(function() { - book.log.info.ln('initialization is finished'); - }); -} - -module.exports = initBook; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..7428180 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,123 @@ +var _ = require('lodash'); +var util = require('util'); +var color = require('bash-color'); + +var LEVELS = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + DISABLED: 10 +}; + +var COLORS = { + DEBUG: color.purple, + INFO: color.cyan, + WARN: color.yellow, + ERROR: color.red +}; + +function Logger(write, logLevel) { + if (!(this instanceof Logger)) return new Logger(write, logLevel); + + this._write = write; + this.lastChar = '\n'; + + // Define log level + this.setLevel(logLevel); + + _.bindAll(this); + + // Create easy-to-use method like "logger.debug.ln('....')" + _.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) { + levelKey = levelKey.toLowerCase(); + + this[levelKey] = _.partial(this.log, level); + this[levelKey].ln = _.partial(this.logLn, level); + this[levelKey].ok = _.partial(this.ok, level); + this[levelKey].fail = _.partial(this.fail, level); + this[levelKey].promise = _.partial(this.promise, level); + }, this); +} + +// Change minimum level +Logger.prototype.setLevel = function(logLevel) { + if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()]; + this.logLevel = logLevel; +}; + +// Print a simple 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. +Logger.prototype.format = function() { + return util.format.apply(util, arguments); +}; + +// Print a line +Logger.prototype.writeLn = function(msg) { + return this.write((msg || '')+'\n'); +}; + +// Log/Print a message if level is allowed +Logger.prototype.log = function(level) { + if (level < this.logLevel) return; + + var levelKey = _.findKey(LEVELS, function(v) { return v == level; }); + var args = Array.prototype.slice.apply(arguments, [1]); + var msg = this.format.apply(this, args); + + if (this.lastChar == '\n') { + msg = COLORS[levelKey](levelKey.toLowerCase()+':')+' '+msg; + } + + return this.write(msg); +}; + +// Log/Print a line if level is allowed +Logger.prototype.logLn = function() { + if (this.lastChar != '\n') this.write('\n'); + + var args = Array.prototype.slice.apply(arguments); + args.push('\n'); + return this.log.apply(this, args); +}; + +// Log a confirmation [OK] +Logger.prototype.ok = function(level) { + var args = Array.prototype.slice.apply(arguments, [1]); + var msg = this.format.apply(this, args); + if (arguments.length > 1) { + this.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> '))); + } else { + this.log(level, color.green('OK'), '\n'); + } +}; + +// Log a "FAIL" +Logger.prototype.fail = function(level) { + return this.log(level, color.red('ERROR') + '\n'); +}; + +// Log state of a promise +Logger.prototype.promise = function(level, p) { + var that = this; + + return p. + then(function(st) { + that.ok(level); + return st; + }, function(err) { + that.fail(level); + throw err; + }); +}; + +Logger.LEVELS = LEVELS; +Logger.COLORS = COLORS; + +module.exports = Logger; diff --git a/lib/page.js b/lib/page.js new file mode 100644 index 0000000..a6c2caf --- /dev/null +++ b/lib/page.js @@ -0,0 +1,10 @@ + + +function Page() { + if (!(this instanceof Page)) return new Page(); + +} + + + +module.exports = Page; diff --git a/lib/plugin.js b/lib/plugin.js deleted file mode 100644 index b7e8260..0000000 --- a/lib/plugin.js +++ /dev/null @@ -1,241 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var path = require('path'); -var url = require('url'); -var fs = require('./utils/fs'); -var resolve = require('resolve'); -var mergeDefaults = require('merge-defaults'); -var jsonschema = require('jsonschema'); -var jsonSchemaDefaults = require('json-schema-defaults'); - -var version = require('./version'); - -var PLUGIN_PREFIX = 'gitbook-plugin-'; - -// Return an absolute name for the plugin (the one on NPM) -function absoluteName(name) { - if (name.indexOf(PLUGIN_PREFIX) === 0) return name; - return [PLUGIN_PREFIX, name].join(''); -} - - -var Plugin = function(book, name) { - this.book = book; - this.name = absoluteName(name); - this.packageInfos = {}; - this.infos = {}; - - // Bind methods - _.bindAll(this); - - _.each([ - absoluteName(name), - name - ], function(_name) { - // Load from the book - if (this.load(_name, book.root)) return false; - - // Load from default plugins - if (this.load(_name, __dirname)) return false; - }, this); -}; - -// Type of plugins resources -Plugin.RESOURCES = ['js', 'css']; -Plugin.HOOKS = [ - 'init', 'finish', 'finish:before', 'config', 'page', 'page:before' -]; - -// Return the reduce name for the plugin -// "gitbook-plugin-test" -> "test" -// Return a relative name for the plugin (the one on GitBook) -Plugin.prototype.reducedName = function() { - return this.name.replace(PLUGIN_PREFIX, ''); -}; - -// Load from a name -Plugin.prototype.load = function(name, baseDir) { - try { - var res = resolve.sync(name+'/package.json', { basedir: baseDir }); - - this.baseDir = path.dirname(res); - this.packageInfos = require(res); - this.infos = require(resolve.sync(name, { basedir: baseDir })); - this.name = this.packageInfos.name; - - return true; - } catch (e) { - this.packageInfos = {}; - this.infos = {}; - return false; - } -}; - -Plugin.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.name+'/'+resource }; -}; - -// Return resources -Plugin.prototype._getResources = function(base) { - base = base; - var book = this.infos[base]; - - // Compatibility with version 1.x.x - if (base == 'website') book = book || this.infos.book; - - // Nothing specified, fallback to default - if (!book) { - return Q({}); - } - - // Dynamic function - if(typeof book === 'function') { - // Call giving it the context of our book - return Q().then(book.bind(this.book)); - } - - // Plain data object - return Q(_.cloneDeep(book)); -}; - -// Normalize resources and return them -Plugin.prototype.getResources = function(base) { - var that = this; - - return this._getResources(base) - .then(function(resources) { - - _.each(Plugin.RESOURCES, function(resourceType) { - resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource); - }); - - return resources; - }); -}; - -// Normalize filters and return them -Plugin.prototype.getFilters = function() { - return this.infos.filters || {}; -}; - -// Normalize blocks and return them -Plugin.prototype.getBlocks = function() { - return this.infos.blocks || {}; -}; - -// Test if it's a valid plugin -Plugin.prototype.isValid = function() { - var that = this; - var isValid = ( - this.packageInfos && - this.packageInfos.name && - this.packageInfos.engines && - this.packageInfos.engines.gitbook && - version.satisfies(this.packageInfos.engines.gitbook) - ); - - // Valid hooks - _.each(this.infos.hooks, function(hook, hookName) { - if (_.contains(Plugin.HOOKS, hookName)) return; - that.book.log.warn.ln('Hook "'+hookName+'"" used by plugin "'+that.packageInfos.name+'" has been removed or is deprecated'); - }); - - return isValid; -}; - -// Normalize, validate configuration for this plugin using its schema -// Throw an error when shcema is not respected -Plugin.prototype.validateConfig = function(config) { - var that = this; - - return Q() - .then(function() { - var schema = that.packageInfos.gitbook || {}; - if (!schema) return config; - - // Normalize schema - schema.id = '/pluginsConfig.'+that.reducedName(); - schema.type = 'object'; - - // Validate and throw if invalid - var v = new jsonschema.Validator(); - var result = v.validate(config, schema, { - propertyName: 'pluginsConfig.'+that.reducedName() - }); - - // Throw error - if (result.errors.length > 0) { - throw new Error('Configuration Error: '+result.errors[0].stack); - } - - // Insert default values - var defaults = jsonSchemaDefaults(schema); - return mergeDefaults(config, defaults); - }); -}; - -// Resolve file path -Plugin.prototype.resolveFile = function(filename) { - return path.resolve(this.baseDir, filename); -}; - -// Resolve file path -Plugin.prototype.callHook = function(name, data) { - // Our book will be the context to apply - var context = this.book; - - var hookFunc = this.infos.hooks? this.infos.hooks[name] : null; - data = data || {}; - - if (!hookFunc) return Q(data); - - this.book.log.debug.ln('call hook', name); - if (!_.contains(Plugin.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 Q() - .then(function() { - return hookFunc.apply(context, [data]); - }); -}; - -// Copy plugin assets fodler -Plugin.prototype.copyAssets = function(out, base) { - var that = this; - - return this.getResources(base) - .get('assets') - .then(function(assets) { - // Assets are undefined - if(!assets) return false; - - return fs.copy( - that.resolveFile(assets), - out - ).then(_.constant(true)); - }, _.constant(false)); -}; - -// Get config from book -Plugin.prototype.getConfig = function() { - return this.book.config.get('pluginsConfig.'+this.reducedName(), {}); -}; - -// Set configuration for this plugin -Plugin.prototype.setConfig = function(values) { - return this.book.config.set('pluginsConfig.'+this.reducedName(), values); -}; - -module.exports = Plugin; diff --git a/lib/pluginslist.js b/lib/pluginslist.js deleted file mode 100644 index 290cd35..0000000 --- a/lib/pluginslist.js +++ /dev/null @@ -1,230 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var npmi = require('npmi'); -var npm = require('npm'); -var semver = require('semver'); - -var Plugin = require('./plugin'); -var version = require('./version'); - -var initNPM = _.memoize(function() { - return Q.nfcall(npm.load, { silent: true, loglevel: 'silent' }); -}); - - -var PluginsList = function(book, plugins) { - this.book = book; - this.log = this.book.log; - - // List of Plugin objects - this.list = []; - - // List of names of failed plugins - this.failed = []; - - // Namespaces - this.namespaces = _.chain(['website', 'ebook']) - .map(function(namespace) { - return [ - namespace, - { - html: {}, - resources: _.chain(Plugin.RESOURCES) - .map(function(type) { - return [type, []]; - }) - .object() - .value() - } - ]; - }) - .object() - .value(); - - // Bind methods - _.bindAll(this); - - if (plugins) this.load(plugins); -}; - -// return count of plugins -PluginsList.prototype.count = function() { - return this.list.length; -}; - -// Add and load a plugin -PluginsList.prototype.load = function(plugin) { - var that = this; - - if (_.isArray(plugin)) { - return _.reduce(plugin, function(prev, p) { - return prev.then(function() { - return that.load(p); - }); - }, Q()); - } - if (_.isObject(plugin) && !(plugin instanceof Plugin)) plugin = plugin.name; - if (_.isString(plugin)) plugin = new Plugin(this.book, plugin); - - that.log.info('load plugin', plugin.name, '....'); - if (!plugin.isValid()) { - that.log.info.fail(); - that.failed.push(plugin.name); - return Q(); - } else { - that.log.info.ok(); - - // Push in the list - that.list.push(plugin); - } - - return Q() - - // Validate and normalize configuration - .then(function() { - var config = plugin.getConfig(); - return plugin.validateConfig(config); - }) - .then(function(config) { - // Update configuration - plugin.setConfig(config); - - // Extract filters - that.book.template.addFilters(plugin.getFilters()); - - // Extract blocks - that.book.template.addBlocks(plugin.getBlocks()); - - return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) { - return prev.then(function() { - return plugin.getResources(namespaceName) - .then(function(plResources) { - var namespace = that.namespaces[namespaceName]; - - // Extract js and css - _.each(Plugin.RESOURCES, function(resourceType) { - namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []); - }); - - // Map of html resources by name added by each plugin - _.each(plResources.html || {}, function(value, tag) { - // Turn into function if not one already - if (!_.isFunction(value)) value = _.constant(value); - - namespace.html[tag] = namespace.html[tag] || []; - namespace.html[tag].push(value); - }); - }); - }); - }, Q()); - }); -}; - -// Call a hook -PluginsList.prototype.hook = function(name, data) { - return _.reduce(this.list, function(prev, plugin) { - return prev.then(function(ret) { - return plugin.callHook(name, ret); - }); - }, Q(data)); -}; - -// Return a template from a plugin -PluginsList.prototype.template = function(name) { - var withTpl = _.find(this.list, function(plugin) { - return ( - plugin.infos.templates && - plugin.infos.templates[name] - ); - }); - - if (!withTpl) return null; - return withTpl.resolveFile(withTpl.infos.templates[name]); -}; - -// Return an html snippet -PluginsList.prototype.html = function(namespace, tag, context, options) { - var htmlSnippets = this.namespaces[namespace].html[tag]; - return _.map(htmlSnippets || [], function(code) { - return code.call(context, options); - }).join('\n'); -}; - -// Return a resources map for a namespace -PluginsList.prototype.resources = function(namespace) { - return this.namespaces[namespace].resources; -}; - -// Install plugins from a book -PluginsList.prototype.install = function() { - var that = this; - - // Remove defaults (no need to install) - var plugins = _.reject(that.book.options.plugins, { - isDefault: true - }); - - // Install plugins one by one - that.book.log.info.ln(plugins.length+' plugins to install'); - return _.reduce(plugins, function(prev, plugin) { - return prev.then(function() { - var fullname = 'gitbook-plugin-'+plugin.name; - - return Q() - - // Resolve version if needed - .then(function() { - if (plugin.version) return plugin.version; - - that.book.log.info.ln('No version specified, resolve plugin', plugin.name); - return initNPM() - .then(function() { - return Q.nfcall(npm.commands.view, [fullname+'@*', '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 && version.satisfies(v.gitbook); - }) - .sort(function(v1, v2) { - return semver.lt(v1.version, v2.version)? 1 : -1; - }) - .pluck('version') - .first() - .value(); - }); - }) - - // Install the plugin with the resolved version - .then(function(version) { - if (!version) { - throw 'Found no satisfactory version for plugin '+plugin.name; - } - - that.book.log.info.ln('install plugin', plugin.name, 'from npm ('+fullname+') with version', version); - return Q.nfcall(npmi, { - 'name': fullname, - 'version': version, - 'path': that.book.root, - 'npmLoad': { - 'loglevel': 'silent', - 'loaded': true, - 'prefix': that.book.root - } - }); - }) - .then(function() { - that.book.log.info.ok('plugin', plugin.name, 'installed with success'); - }); - }); - }, Q()); -}; - -module.exports = PluginsList; diff --git a/lib/template.js b/lib/template.js deleted file mode 100644 index dac1201..0000000 --- a/lib/template.js +++ /dev/null @@ -1,466 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var path = require('path'); -var nunjucks = require('nunjucks'); -var parsers = require('gitbook-parsers'); -var escapeStringRegexp = require('escape-string-regexp'); - -var batch = require('./utils/batch'); -var pkg = require('../package.json'); -var defaultBlocks = require('./blocks'); -var BookLoader = require('./conrefs_loader'); - -// Normalize result from a block -function normBlockResult(blk) { - if (_.isString(blk)) blk = { body: blk }; - return blk; -} - - -var TemplateEngine = function(book) { - var that = this; - - this.book = book; - this.log = this.book.log; - - // Template loader - this.loader = new BookLoader(book, { - // Replace shortcuts in imported files - interpolate: function(filepath, source) { - var parser = parsers.get(path.extname(filepath)); - var type = parser? parser.name : null; - - return that.applyShortcuts(type, source); - } - }); - - // Nunjucks env - this.env = new nunjucks.Environment( - this.loader, - { - // Escaping is done after by the markdown parser - autoescape: false, - - // Tags - 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 - this.addBlocks(defaultBlocks); -}; - -// Process the result of block in a context -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+'@%@'; -}; - -// Replace position markers of blocks by body after processing -// This is done to avoid that markdown/asciidoc processer parse the block content -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; - }); -}; - -// Bind a function to a context -TemplateEngine.prototype.bindContext = function(func) { - var that = this; - - return function() { - var ctx = { - ctx: this.ctx, - book: that.book, - generator: that.book.options.generator - }; - - return func.apply(ctx, arguments); - }; -}; - -// Add filter -TemplateEngine.prototype.addFilter = function(filterName, func) { - try { - this.env.getFilter(filterName); - this.log.warn.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); - - Q() - .then(function() { - return func.apply(ctx, args.slice(0, -1)); - }) - .nodeify(callback); - }), true); - return true; -}; - -// Add multiple filters -TemplateEngine.prototype.addFilters = function(filters) { - _.each(filters, function(filter, name) { - this.addFilter(name, filter); - }, this); -}; - -// Return nunjucks extension name of a block -TemplateEngine.prototype.blockExtName = function(name) { - return 'Block'+name+'Extension'; -}; - -// Test if a block is defined -TemplateEngine.prototype.hasBlock = function(name) { - return this.env.hasExtension(this.blockExtName(name)); -}; - -// Remove a block -TemplateEngine.prototype.removeBlock = function(name) { - if (!this.hasBlock(name)) return; - - // Remove nunjucks extension - this.env.removeExtension(this.blockExtName(name)); - - // Cleanup shortcuts - this.shortcuts = _.reject(this.shortcuts, { - block: name - }); -}; - -// Add a block -TemplateEngine.prototype.addBlock = function(name, block) { - var that = this, Ext, extName; - - if (_.isFunction(block)) block = { process: block }; - - block = _.defaults(block || {}, { - shortcuts: [], - end: 'end'+name, - process: _.identity, - blocks: [] - }); - - extName = this.blockExtName(name); - - 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 body = null; - var lastBlockName = null; - var lastBlockArgs = null; - var allBlocks = block.blocks.concat([block.end]); - var subbodies = {}; - - var tok = parser.nextToken(); - var args = parser.parseSignature(null, true); - parser.advanceAfterBlockEnd(tok.value); - - while (1) { - // Read body - var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks); - - // Handle body with previous block name and args - if (lastBlockName) { - subbodies[lastBlockName] = subbodies[lastBlockName] || []; - subbodies[lastBlockName].push({ - body: currentBody, - args: lastBlockArgs - }); - } else { - body = currentBody; - } - - // Read new block - lastBlockName = parser.peekToken().value; - if (lastBlockName == block.end) { - break; - } - - // Parse signature and move to the end of the block - lastBlockArgs = parser.parseSignature(null, true); - parser.advanceAfterBlockEnd(lastBlockName); - } - parser.advanceAfterBlockEnd(); - - var bodies = [body]; - _.each(block.blocks, function(blockName) { - subbodies[blockName] = subbodies[blockName] || []; - if (subbodies[blockName].length === 0) { - subbodies[blockName].push({ - args: new nodes.NodeList(), - body: new nodes.NodeList() - }); - } - - bodies.push(subbodies[blockName][0].body); - }); - - return new nodes.CallExtensionAsync(this, 'run', args, bodies); - }; - - this.run = function(context) { - var args = Array.prototype.slice.call(arguments, 1); - var callback = args.pop(); - - // Extract blocks - var blocks = args - .concat([]) - .slice(-block.blocks.length); - - // Eliminate blocks from list - if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length); - - // Extract main body and kwargs - var body = args.pop(); - var kwargs = _.isObject(_.last(args))? args.pop() : {}; - - // Extract blocks body - var _blocks = _.map(block.blocks, function(blockName, i){ - return { - name: blockName, - body: blocks[i]() - }; - }); - - Q() - .then(function() { - return that.applyBlock(name, { - body: body(), - args: args, - kwargs: kwargs, - blocks: _blocks - }, context); - }) - - // process the block returned - .then(that.processBlock) - .nodeify(callback); - }; - }; - - // Add the Extension - this.env.addExtension(extName, new Ext()); - - // Add shortcuts - 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 -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) -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 (Q.isPromise(r)) return r.then(normBlockResult); - else return normBlockResult(r); -}; - -// Apply a shortcut to a string -TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) { - if (!_.contains(shortcut.parsers, parser)) return content; - 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 shortcuts to some template string -TemplateEngine.prototype.applyShortcuts = function(type, content) { - return _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), type), content); -}; - -// Render a string from the book -TemplateEngine.prototype.renderString = function(content, context, options) { - context = _.extend({}, context, { - // Variables from book.json - book: this.book.options.variables, - - // Complete book.json - config: this.book.options, - - // infos about gitbook - gitbook: { - version: pkg.version, - generator: this.book.options.generator - } - }); - options = _.defaults(options || {}, { - path: null, - type: null - }); - if (options.path) options.path = this.book.resolve(options.path); - if (!options.type && options.path) { - var parser = parsers.get(path.extname(options.path)); - options.type = parser? parser.name : null; - } - - // Replace shortcuts - content = this.applyShortcuts(options.type, content); - - return Q.nfcall(this.env.renderString.bind(this.env), content, context, options) - .fail(function(err) { - if (_.isString(err)) err = new Error(err); - err.message = err.message.replace(/^Error: /, ''); - - throw err; - }); -}; - -// Render a file from the book -TemplateEngine.prototype.renderFile = function(filename) { - var that = this; - - return that.book.readFile(filename) - .then(function(content) { - return that.renderString(content, {}, { - path: filename - }); - }); -}; - -// Render a page from the book -TemplateEngine.prototype.renderPage = function(page) { - var that = this; - - return that.book.statFile(page.path) - .then(function(stat) { - var context = { - // infos about the file - file: { - path: page.path, - mtime: stat.mtime - } - }; - - return that.renderString(page.content, context, { - path: page.path, - type: page.type - }); - }); -}; - -// Post process content -TemplateEngine.prototype.postProcess = function(content) { - var that = this; - - return Q(content) - .then(that.replaceBlocks) - .then(function(_content) { - return batch.execEach(that.blockBodies, { - max: 20, - fn: function(blk, blkId) { - return Q() - .then(function() { - if (!blk.post) return Q(); - return blk.post(); - }) - .then(function() { - delete that.blockBodies[blkId]; - }); - } - }) - .thenResolve(_content); - }); -}; - -module.exports = TemplateEngine; diff --git a/lib/utils/batch.js b/lib/utils/batch.js deleted file mode 100644 index 9069766..0000000 --- a/lib/utils/batch.js +++ /dev/null @@ -1,52 +0,0 @@ -var Q = require("q"); -var _ = require("lodash"); - -// Execute a method for all element -function execEach(items, options) { - if (_.size(items) === 0) return Q(); - var concurrents = 0, d = Q.defer(), pending = []; - - options = _.defaults(options || {}, { - max: 100, - fn: function() {} - }); - - - function startItem(item, i) { - if (concurrents >= options.max) { - pending.push([item, i]); - return; - } - - concurrents++; - Q() - .then(function() { - return options.fn(item, i); - }) - .then(function() { - concurrents--; - - // Next pending - var next = pending.shift(); - - if (concurrents === 0 && !next) { - d.resolve(); - } else if (next) { - startItem.apply(null, next); - } - }) - .fail(function(err) { - pending = []; - d.reject(err); - }); - } - - _.each(items, startItem); - - return d.promise; -} - -module.exports = { - execEach: execEach -}; - diff --git a/lib/utils/fs.js b/lib/utils/fs.js deleted file mode 100644 index b82701f..0000000 --- a/lib/utils/fs.js +++ /dev/null @@ -1,193 +0,0 @@ -var _ = require('lodash'); -var Q = require('q'); -var tmp = require('tmp'); -var path = require('path'); -var fs = require('graceful-fs'); -var fsExtra = require('fs-extra'); -var Ignore = require('fstream-ignore'); - -var fsUtils = { - tmp: { - file: function(opt) { - return Q.nfcall(tmp.file.bind(tmp), opt).get(0); - }, - dir: function() { - return Q.nfcall(tmp.dir.bind(tmp)).get(0); - } - }, - list: listFiles, - stat: Q.denodeify(fs.stat), - readdir: Q.denodeify(fs.readdir), - readFile: Q.denodeify(fs.readFile), - writeFile: writeFile, - writeStream: writeStream, - mkdirp: Q.denodeify(fsExtra.mkdirp), - copy: Q.denodeify(fsExtra.copy), - remove: Q.denodeify(fsExtra.remove), - symlink: Q.denodeify(fsExtra.symlink), - exists: function(path) { - var d = Q.defer(); - fs.exists(path, d.resolve); - return d.promise; - }, - findFile: findFile, - existsSync: fs.existsSync.bind(fs), - readFileSync: fs.readFileSync.bind(fs), - clean: cleanFolder, - getUniqueFilename: getUniqueFilename -}; - -// Write a file -function writeFile(filename, data, options) { - var d = Q.defer(); - - try { - fs.writeFileSync(filename, data, options); - } catch(err) { - d.reject(err); - } - d.resolve(); - - - return d.promise; -} - -// Write a stream to a file -function writeStream(filename, st) { - var d = Q.defer(); - - var wstream = fs.createWriteStream(filename); - - wstream.on('finish', function () { - d.resolve(); - }); - wstream.on('error', function (err) { - d.reject(err); - }); - - st.on('error', function(err) { - d.reject(err); - }); - - st.pipe(wstream); - - return d.promise; -} - -// Find a filename available -function getUniqueFilename(base, filename) { - if (!filename) { - filename = base; - base = '/'; - } - - filename = path.resolve(base, filename); - var ext = path.extname(filename); - filename = path.join(path.dirname(filename), path.basename(filename, ext)); - - var _filename = filename+ext; - - var i = 0; - while (fs.existsSync(filename)) { - _filename = filename+'_'+i+ext; - i = i + 1; - } - - return path.relative(base, _filename); -} - - -// List files in a directory -function listFiles(root, options) { - options = _.defaults(options || {}, { - ignoreFiles: [], - ignoreRules: [] - }); - - var d = Q.defer(); - - // Our list of files - var files = []; - - var ig = Ignore({ - path: root, - ignoreFiles: options.ignoreFiles - }); - - // Add extra rules to ignore common folders - ig.addIgnoreRules(options.ignoreRules, '__custom_stuff'); - - // Push each file to our list - ig.on('child', function (c) { - files.push( - c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '') - ); - }); - - ig.on('end', function() { - // Normalize paths on Windows - if(process.platform === 'win32') { - return d.resolve(files.map(function(file) { - return file.replace(/\\/g, '/'); - })); - } - - // Simply return paths otherwise - return d.resolve(files); - }); - - ig.on('error', d.reject); - - return d.promise; -} - -// Clean a folder without removing .git and .svn -// Creates it if non existant -function cleanFolder(root) { - if (!fs.existsSync(root)) return fsUtils.mkdirp(root); - - return listFiles(root, { - ignoreFiles: [], - ignoreRules: [ - // Skip Git and SVN stuff - '.git/', - '.svn/' - ] - }) - .then(function(files) { - var d = Q.defer(); - - _.reduce(files, function(prev, file, i) { - return prev.then(function() { - var _file = path.join(root, file); - - d.notify({ - i: i+1, - count: files.length, - file: _file - }); - return fsUtils.remove(_file); - }); - }, Q()) - .then(function() { - d.resolve(); - }, function(err) { - d.reject(err); - }); - - return d.promise; - }); -} - -// Find a file in a folder (case incensitive) -// Return the real filename -function findFile(root, filename) { - return Q.nfcall(fs.readdir, root) - .then(function(files) { - return _.find(files, function(file) { - return (file.toLowerCase() == filename.toLowerCase()); - }); - }); -} - -module.exports = fsUtils; diff --git a/lib/utils/git.js b/lib/utils/git.js deleted file mode 100644 index 72c8818..0000000 --- a/lib/utils/git.js +++ /dev/null @@ -1,127 +0,0 @@ -var Q = require('q'); -var _ = require('lodash'); -var path = require('path'); -var crc = require('crc'); -var exec = Q.denodeify(require('child_process').exec); -var URI = require('urijs'); -var pathUtil = require('./path'); - -var fs = require('./fs'); - -var GIT_PREFIX = 'git+'; -var GIT_TMP = null; - - -// Check if an url is a git dependency url -function checkGitUrl(giturl) { - return (giturl.indexOf(GIT_PREFIX) === 0); -} - -// Validates a SHA in hexadecimal -function validateSha(str) { - return (/[0-9a-f]{40}/).test(str); -} - -// Parse and extract infos -function parseGitUrl(giturl) { - var ref, uri, fileParts, filepath; - - if (!checkGitUrl(giturl)) return null; - giturl = giturl.slice(GIT_PREFIX.length); - - uri = new URI(giturl); - ref = uri.fragment() || 'master'; - uri.fragment(null); - - // Extract file inside the repo (after the .git) - fileParts =uri.path().split('.git'); - filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : ''; - if (filepath[0] == '/') filepath = filepath.slice(1); - - // Recreate pathname without the real filename - uri.path(_.first(fileParts)+'.git'); - - return { - host: uri.toString(), - ref: ref || 'master', - filepath: filepath - }; -} - -// Clone a git repo from a specific ref -function cloneGitRepo(host, ref) { - var isBranch = false; - - ref = ref || 'master'; - if (!validateSha(ref)) isBranch = true; - - return Q() - - // Create temporary folder to store git repos - .then(function() { - if (GIT_TMP) return; - return fs.tmp.dir() - .then(function(_tmp) { - GIT_TMP = _tmp; - }); - }) - - // Return or clone the git repo - .then(function() { - // Unique ID for repo/ref combinaison - var repoId = crc.crc32(host+'#'+ref).toString(16); - - // Absolute path to the folder - var repoPath = path.resolve(GIT_TMP, repoId); - - return fs.exists(repoPath) - .then(function(doExists) { - if (doExists) return; - - // Clone repo - return exec('git clone '+host+' '+repoPath) - .then(function() { - return exec('git checkout '+ref, { cwd: repoPath }); - }); - }) - .thenResolve(repoPath); - }); -} - -// Get file from a git repo -function resolveFileFromGit(giturl) { - if (_.isString(giturl)) giturl = parseGitUrl(giturl); - if (!giturl) return Q(null); - - // Clone or get from cache - return cloneGitRepo(giturl.host, giturl.ref) - .then(function(repo) { - - // Resolve relative path - return path.resolve(repo, giturl.filepath); - }); -} - -// Return root of git repo from a filepath -function resolveGitRoot(filepath) { - var relativeToGit, repoId; - - // No git repo cloned, or file is not in a git repository - if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null; - - // Extract first directory (is the repo id) - relativeToGit = path.relative(GIT_TMP, filepath); - repoId = _.first(relativeToGit.split(path.sep)); - if (!repoId) return; - - // Return an absolute file - return path.resolve(GIT_TMP, repoId); -} - - -module.exports = { - checkUrl: checkGitUrl, - parseUrl: parseGitUrl, - resolveFile: resolveFileFromGit, - resolveRoot: resolveGitRoot -}; diff --git a/lib/utils/i18n.js b/lib/utils/i18n.js deleted file mode 100644 index de64b49..0000000 --- a/lib/utils/i18n.js +++ /dev/null @@ -1,80 +0,0 @@ -var _ = require('lodash'); -var path = require('path'); -var fs = require('fs'); - -var i18n = require('i18n'); - -var I18N_PATH = path.resolve(__dirname, '../../theme/i18n/'); -var DEFAULT_LANGUAGE = 'en'; -var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) { - return path.basename(lang, '.json'); -}); - -i18n.configure({ - locales: LOCALES, - directory: I18N_PATH, - defaultLocale: DEFAULT_LANGUAGE, - updateFiles: false -}); - -function compareLocales(lang, locale) { - var langMain = _.first(lang.split('-')); - var langSecond = _.last(lang.split('-')); - - var localeMain = _.first(locale.split('-')); - var localeSecond = _.last(locale.split('-')); - - if (locale == lang) return 100; - if (localeMain == langMain) return 50; - if (localeSecond == langSecond) return 20; - return 0; -} - -var normalizeLanguage = _.memoize(function(lang) { - var language = _.chain(LOCALES) - .values() - .map(function(locale) { - return { - locale: locale, - score: compareLocales(lang, locale) - }; - }) - .filter(function(lang) { - return lang.score > 0; - }) - .sortBy('score') - .pluck('locale') - .last() - .value(); - return language || lang; -}); - -function translate(locale, phrase) { - var args = Array.prototype.slice.call(arguments, 2); - - return i18n.__.apply({}, [{ - locale: locale, - phrase: phrase - }].concat(args)); -} - -function getCatalog(locale) { - locale = normalizeLanguage(locale); - return i18n.getCatalog(locale); -} - -function getLocales() { - return LOCALES; -} - -function hasLocale(locale) { - return _.contains(LOCALES, locale); -} - -module.exports = { - __: translate, - normalizeLanguage: normalizeLanguage, - getCatalog: getCatalog, - getLocales: getLocales, - hasLocale: hasLocale -}; diff --git a/lib/utils/images.js b/lib/utils/images.js deleted file mode 100644 index a82b0a1..0000000 --- a/lib/utils/images.js +++ /dev/null @@ -1,37 +0,0 @@ -var _ = require("lodash"); -var Q = require("q"); -var fs = require("./fs"); -var spawn = require("spawn-cmd").spawn; - -// Convert a svg file -var convertSVG = function(source, dest, options) { - if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source)); - var d = Q.defer(); - - options = _.defaults(options || {}, { - - }); - - //var command = shellescape(["svgexport", source, dest]); - var child = spawn("svgexport", [source, dest]); - - child.on("error", function(error) { - if (error.code == "ENOENT") error = new Error("Need to install \"svgexport\" using \"npm install svgexport -g\""); - return d.reject(error); - }); - - child.on("close", function(code) { - if (code === 0 && fs.existsSync(dest)) { - d.resolve(); - } else { - d.reject(new Error("Error converting "+source+" into "+dest)); - } - }); - - return d.promise; -}; - -module.exports = { - convertSVG: convertSVG, - INVALID: [".svg"] -}; diff --git a/lib/utils/links.js b/lib/utils/links.js deleted file mode 100644 index 5122396..0000000 --- a/lib/utils/links.js +++ /dev/null @@ -1,81 +0,0 @@ -var url = require('url'); -var path = require('path'); - -// Is the link an external link -function isExternal(href) { - try { - return Boolean(url.parse(href).protocol); - } catch(err) { - return false; - } -} - -// Return true if the link is relative -function isRelative(href) { - try { - var parsed = url.parse(href); - - return !!(!parsed.protocol && parsed.path); - } catch(err) { - return true; - } -} - -// Return true if the link is an achor -function isAnchor(href) { - try { - var parsed = url.parse(href); - return !!(!parsed.protocol && !parsed.path && parsed.hash); - } catch(err) { - return false; - } -} - -// Normalize a path to be a link -function normalizeLink(s) { - return s.replace(/\\/g, '/'); -} - -// Relative to absolute path -// dir: directory parent of the file currently in rendering process -// outdir: directory parent from the html output -function toAbsolute(_href, dir, outdir) { - if (isExternal(_href)) return _href; - - // Path "_href" inside the base folder - var hrefInRoot = path.normalize(path.join(dir, _href)); - if (_href[0] == '/') hrefInRoot = path.normalize(_href.slice(1)); - - // Make it relative to output - _href = path.relative(outdir, hrefInRoot); - - // Normalize windows paths - _href = normalizeLink(_href); - - return _href; -} - -// Join links -function join() { - var _href = path.join.apply(path, arguments); - - return normalizeLink(_href); -}; - -// Change extension -function changeExtension(filename, newext) { - return path.join( - path.dirname(filename), - path.basename(filename, path.extname(filename))+newext - ); -} - -module.exports = { - isAnchor: isAnchor, - isRelative: isRelative, - isExternal: isExternal, - toAbsolute: toAbsolute, - join: join, - changeExtension: changeExtension, - normalize: normalizeLink -}; diff --git a/lib/utils/logger.js b/lib/utils/logger.js deleted file mode 100644 index db3d90e..0000000 --- a/lib/utils/logger.js +++ /dev/null @@ -1,102 +0,0 @@ -var _ = require("lodash"); -var util = require("util"); -var color = require("bash-color"); - -var LEVELS = { - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - DISABLED: 10 -}; - -var COLORS = { - DEBUG: color.purple, - INFO: color.cyan, - WARN: color.yellow, - ERROR: color.red -}; - -module.exports = function(_write, logLevel) { - var logger = {}; - var lastChar = "\n"; - if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()]; - - // Write a simple message - logger.write = function(msg) { - msg = msg.toString(); - lastChar = _.last(msg); - return _write(msg); - }; - - // Format a message - logger.format = function() { - return util.format.apply(util, arguments); - }; - - // Write a line - logger.writeLn = function(msg) { - return this.write((msg || "")+"\n"); - }; - - // Write a message with a certain level - logger.log = function(level) { - if (level < logLevel) return; - - var levelKey = _.findKey(LEVELS, function(v) { return v == level; }); - var args = Array.prototype.slice.apply(arguments, [1]); - var msg = logger.format.apply(logger, args); - - if (lastChar == "\n") { - msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg; - } - - return logger.write(msg); - }; - logger.logLn = function() { - if (lastChar != "\n") logger.write("\n"); - - var args = Array.prototype.slice.apply(arguments); - args.push("\n"); - logger.log.apply(logger, args); - }; - - // Write a OK - logger.ok = function(level) { - var args = Array.prototype.slice.apply(arguments, [1]); - var msg = logger.format.apply(logger, args); - if (arguments.length > 1) { - logger.logLn(level, color.green(">> ") + msg.trim().replace(/\n/g, color.green("\n>> "))); - } else { - logger.log(level, color.green("OK"), "\n"); - } - }; - - // Write an "FAIL" - logger.fail = function(level) { - return logger.log(level, color.red("ERROR")+"\n"); - }; - - _.each(_.omit(LEVELS, "DISABLED"), function(level, levelKey) { - levelKey = levelKey.toLowerCase(); - - logger[levelKey] = _.partial(logger.log, level); - logger[levelKey].ln = _.partial(logger.logLn, level); - logger[levelKey].ok = _.partial(logger.ok, level); - logger[levelKey].fail = _.partial(logger.fail, level); - logger[levelKey].promise = function(p) { - return p. - then(function(st) { - logger[levelKey].ok(); - return st; - }, function(err) { - logger[levelKey].fail(); - throw err; - }); - }; - }); - - return logger; -}; -module.exports.LEVELS = LEVELS; -module.exports.COLORS = COLORS; diff --git a/lib/utils/navigation.js b/lib/utils/navigation.js deleted file mode 100644 index d07eb35..0000000 --- a/lib/utils/navigation.js +++ /dev/null @@ -1,79 +0,0 @@ -var _ = require("lodash"); - -// Cleans up an article/chapter object -// remove "articles" attributes -function clean(obj) { - return obj && _.omit(obj, ["articles"]); -} - -function flattenChapters(chapters) { - return _.reduce(chapters, function(accu, chapter) { - return accu.concat([clean(chapter)].concat(flattenChapters(chapter.articles))); - }, []); -} - -// Returns from a summary a map of -/* - { - "file/path.md": { - prev: ..., - next: ..., - }, - ... - } -*/ -function navigation(summary, files) { - // Support single files as well as list - files = _.isArray(files) ? files : (_.isString(files) ? [files] : null); - - // List of all navNodes - // Flatten chapters - var navNodes = flattenChapters(summary.chapters); - - // Mapping of prev/next for a give path - var mapping = _.chain(navNodes) - .map(function(current, i) { - var prev = null, next = null; - - // Skip if no path - if(!current.exists) return null; - - // Find prev - prev = _.chain(navNodes.slice(0, i)) - .reverse() - .find(function(node) { - return node.exists && !node.external; - }) - .value(); - - // Find next - next = _.chain(navNodes.slice(i+1)) - .find(function(node) { - return node.exists && !node.external; - }) - .value(); - - return [current.path, { - index: i, - title: current.title, - introduction: current.introduction, - prev: prev, - next: next, - level: current.level, - }]; - }) - .compact() - .object() - .value(); - - // Filter for only files we want - if(files) { - return _.pick(mapping, files); - } - - return mapping; -} - - -// Exports -module.exports = navigation; diff --git a/lib/utils/page.js b/lib/utils/page.js deleted file mode 100644 index 010d703..0000000 --- a/lib/utils/page.js +++ /dev/null @@ -1,397 +0,0 @@ -var Q = require('q'); -var _ = require('lodash'); -var url = require('url'); -var path = require('path'); -var cheerio = require('cheerio'); -var domSerializer = require('dom-serializer'); -var request = require('request'); -var crc = require('crc'); -var slug = require('github-slugid'); - -var links = require('./links'); -var imgUtils = require('./images'); -var fs = require('./fs'); -var batch = require('./batch'); - -var parsableExtensions = require('gitbook-parsers').extensions; - -// Map of images that have been converted -var imgConversionCache = {}; - -// 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); -} - -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 && /</.test( new_val ) ) { - // The new value contains HTML, set it in a slower but far more - // robust way. - $(node).before( new_val ); - - // Don't remove the node yet, or the loop will lose its place. - remove.push( node ); - } else { - // The new value contains no HTML, so it can be set in this - // very fast, simple way. - node.nodeValue = new_val; - } - } - } - - node = node.nextSibling; - } - } - - // Time to remove those elements! - if (remove.length) $(remove).remove(); - }); -} - -function pregQuote( str ) { - return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1'); -} - - -// Adapt an html snippet to be relative to a base folder -function normalizeHtml(src, options) { - var $ = cheerio.load(src, { - // We should parse html without trying to normalize too much - xmlMode: false, - - // SVG need some attributes to use uppercases - lowerCaseAttributeNames: false, - lowerCaseTags: false - }); - var toConvert = []; - var svgContent = {}; - var outputRoot = options.book.options.output; - - imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {}; - - // Find svg images to extract and process - if (options.convertImages) { - $('svg').each(function() { - var content = renderDom($, $(this)); - var svgId = _.uniqueId('svg'); - var dest = svgId+'.svg'; - - // Generate filename - dest = '/'+fs.getUniqueFilename(outputRoot, dest); - - svgContent[dest] = '<?xml version="1.0" encoding="UTF-8"?>'+content; - $(this).replaceWith($('<img>').attr('src', dest)); - }); - } - - // Generate ID for headings - $('h1,h2,h3,h4,h5,h6').each(function() { - if ($(this).attr('id')) return; - - $(this).attr('id', slug($(this).text())); - }); - - // Find images to normalize - $('img').each(function() { - var origin; - var src = $(this).attr('src'); - - if (!src) return; - var isExternal = links.isExternal(src); - - // Transform as relative to the bases - if (links.isRelative(src)) { - src = links.toAbsolute(src, options.base, options.output); - } - - // Convert if needed - if (options.convertImages) { - // If image is external and ebook, then downlaod the images - if (isExternal) { - origin = src; - src = '/'+crc.crc32(origin).toString(16)+path.extname(url.parse(origin).pathname); - src = links.toAbsolute(src, options.base, options.output); - isExternal = false; - } - - var ext = path.extname(src); - var srcAbs = links.join('/', options.base, src); - - // Test image extension - if (_.contains(imgUtils.INVALID, ext)) { - if (imgConversionCache[outputRoot][srcAbs]) { - // Already converted - src = imgConversionCache[outputRoot][srcAbs]; - } else { - // Not converted yet - var dest = ''; - - // Replace extension - dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+'.png'); - dest = dest[0] == '/'? dest.slice(1) : dest; - - // Get a name that doesn't exists - dest = fs.getUniqueFilename(outputRoot, dest); - - options.book.log.debug.ln('detect invalid image (will be converted to png):', srcAbs); - - // Add to cache - imgConversionCache[outputRoot][srcAbs] = '/'+dest; - - // Push to convert - toConvert.push({ - origin: origin, - content: svgContent[srcAbs], - source: isExternal? srcAbs : path.join('./', srcAbs), - dest: path.join('./', dest) - }); - - src = links.join('/', dest); - } - - // Reset as relative to output - src = links.toAbsolute(src, options.base, options.output); - } - - else if (origin) { - // Need to downlaod image - toConvert.push({ - origin: origin, - source: path.join('./', srcAbs) - }); - } - } - - $(this).attr('src', src); - }); - - // Normalize links - $('a').each(function() { - var href = $(this).attr('href'); - if (!href) return; - - if (links.isAnchor(href)) { - // Keep it as it is - } else if (links.isRelative(href)) { - var parts = url.parse(href); - - var pathName = decodeURIComponent(parts.pathname); - var anchor = parts.hash || ''; - - // Calcul absolute path for this file (without the anchor) - var absolutePath = links.join(options.base, pathName); - - // If is in navigation relative: transform as content - if (options.navigation[absolutePath]) { - absolutePath = options.book.contentLink(absolutePath); - } - - // If md/adoc/rst files is not in summary - // or for ebook, signal all files that are outside the summary - else if (_.contains(parsableExtensions, path.extname(absolutePath)) || - _.contains(['epub', 'pdf', 'mobi'], options.book.options.generator)) { - options.book.log.warn.ln('page', options.input, 'contains an hyperlink to resource outside spine \''+href+'\''); - } - - // Transform as absolute - href = links.toAbsolute('/'+absolutePath, options.base, options.output)+anchor; - } else { - // External links - $(this).attr('target', '_blank'); - } - - // Transform extension - $(this).attr('href', href); - }); - - // Highlight code blocks - $('code').each(function() { - // Normalize language - var lang = _.chain( - ($(this).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 = $(this).text(); - var blk = options.book.template.applyBlock('code', { - body: source, - kwargs: { - language: lang - } - }); - - if (blk.html === false) $(this).text(blk.body); - else $(this).html(blk.body); - }); - - // Replace glossary terms - var glossary = _.sortBy(options.glossary, function(term) { - return -term.name.length; - }); - - _.each(glossary, function(term) { - var r = new RegExp( '\\b(' + pregQuote(term.name.toLowerCase()) + ')\\b' , 'gi' ); - var includedInFiles = false; - - $('*').each(function() { - // Ignore codeblocks - if (_.contains(['code', 'pre', 'a', 'script'], this.name.toLowerCase())) return; - - replaceText($, this, r, function(match) { - // Add to files index in glossary - if (!includedInFiles) { - includedInFiles = true; - term.files = term.files || []; - term.files.push(options.navigation[options.input]); - } - return '<a href=\''+links.toAbsolute('/GLOSSARY.html', options.base, options.output) + '#' + term.id+'\' class=\'glossary-term\' title=\''+_.escape(term.description)+'\'>'+match+'</a>'; - }); - }); - }); - - return { - html: renderDom($), - images: toConvert - }; -} - -// Convert svg images to png -function convertImages(images, options) { - if (!options.convertImages) return Q(); - - var downloaded = []; - options.book.log.debug.ln('convert ', images.length, 'images to png'); - - return batch.execEach(images, { - max: 100, - fn: function(image) { - var imgin = path.resolve(options.book.options.output, image.source); - - return Q() - - // Write image if need to be download - .then(function() { - if (!image.origin && !_.contains(downloaded, image.origin)) return; - options.book.log.debug('download image', image.origin, '...'); - downloaded.push(image.origin); - return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin))) - .fail(function(err) { - if (!_.isError(err)) err = new Error(err); - - err.message = 'Fail downloading '+image.origin+': '+err.message; - throw err; - }); - }) - - // Write svg if content - .then(function() { - if (!image.content) return; - return fs.writeFile(imgin, image.content); - }) - - // Convert - .then(function() { - if (!image.dest) return; - var imgout = path.resolve(options.book.options.output, image.dest); - options.book.log.debug('convert image', image.source, 'to', image.dest, '...'); - return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout)); - }); - } - }) - .then(function() { - options.book.log.debug.ok(images.length+' images converted with success'); - }); -} - -// Adapt page content to be relative to a base folder -function normalizePage(sections, options) { - options = _.defaults(options || {}, { - // Current book - book: null, - - // Do we need to convert svg? - convertImages: false, - - // Current file path - input: '.', - - // Navigation to use to transform path - navigation: {}, - - // Directory parent of the file currently in rendering process - base: './', - - // Directory parent from the html output - output: './', - - // Glossary terms - glossary: [] - }); - - // List of images to convert - var toConvert = []; - - sections = _.map(sections, function(section) { - if (section.type != 'normal') return section; - - var out = normalizeHtml(section.content, options); - - toConvert = toConvert.concat(out.images); - section.content = out.html; - return section; - }); - - return Q() - .then(function() { - toConvert = _.uniq(toConvert, 'source'); - return convertImages(toConvert, options); - }) - .thenResolve(sections); -} - - -module.exports = { - normalize: normalizePage -}; diff --git a/lib/utils/path.js b/lib/utils/path.js index 5285896..dc97d5d 100644 --- a/lib/utils/path.js +++ b/lib/utils/path.js @@ -1,5 +1,5 @@ -var _ = require("lodash"); -var path = require("path"); +var _ = require('lodash'); +var path = require('path'); // Return true if file path is inside a folder function isInRoot(root, filename) { @@ -26,8 +26,8 @@ function resolveInRoot(root) { result = path.resolve(root, input); if (!isInRoot(root, result)) { - err = new Error("EACCESS: \"" + result + "\" not in \"" + root + "\""); - err.code = "EACCESS"; + err = new Error('EACCESS: "' + result + '" not in "' + root + '"'); + err.code = 'EACCESS'; throw err; } diff --git a/lib/utils/progress.js b/lib/utils/progress.js deleted file mode 100644 index 8dda892..0000000 --- a/lib/utils/progress.js +++ /dev/null @@ -1,55 +0,0 @@ -var _ = require('lodash'); - -// Returns from a navigation and a current file, a snapshot of current detailed state -function calculProgress(navigation, current) { - var n = _.size(navigation); - var percent = 0, prevPercent = 0, currentChapter = null; - var done = true; - - var chapters = _.chain(navigation) - - // Transform as array - .map(function(nav, path) { - nav.path = path; - return nav; - }) - - // Sort entries - .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; - }) - .value(); - - return { - // Previous percent - prevPercent: prevPercent, - - // Current percent - percent: percent, - - // List of chapter with progress - chapters: chapters, - - // Current chapter - current: currentChapter - }; -} - -module.exports = calculProgress; diff --git a/lib/utils/server.js b/lib/utils/server.js deleted file mode 100644 index 1d6822f..0000000 --- a/lib/utils/server.js +++ /dev/null @@ -1,94 +0,0 @@ -var Q = require("q"); -var events = require("events"); -var http = require("http"); -var send = require("send"); -var util = require("util"); -var url = require("url"); - -var Server = function() { - this.running = null; - this.dir = null; - this.port = 0; - this.sockets = []; -}; -util.inherits(Server, events.EventEmitter); - -// Return true if the server is running -Server.prototype.isRunning = function() { - return !!this.running; -}; - -// Stop the server -Server.prototype.stop = function() { - var that = this; - if (!this.isRunning()) return Q(); - - var d = Q.defer(); - this.running.close(function(err) { - that.running = null; - that.emit("state", false); - - if (err) d.reject(err); - else d.resolve(); - }); - - for (var i = 0; i < this.sockets.length; i++) { - this.sockets[i].destroy(); - } - - return d.promise; -}; - -Server.prototype.start = function(dir, port) { - var that = this, pre = Q(); - port = port || 8004; - - if (that.isRunning()) pre = this.stop(); - return pre - .then(function() { - var d = Q.defer(); - - that.running = http.createServer(function(req, res){ - // Render error - function error(err) { - res.statusCode = err.status || 500; - res.end(err.message); - } - - // Redirect to directory"s index.html - function redirect() { - res.statusCode = 301; - res.setHeader("Location", req.url + "/"); - res.end("Redirecting to " + req.url + "/"); - } - - // Send file - send(req, url.parse(req.url).pathname) - .root(dir) - .on("error", error) - .on("directory", redirect) - .pipe(res); - }); - - that.running.on("connection", function (socket) { - that.sockets.push(socket); - socket.setTimeout(4000); - socket.on("close", function () { - that.sockets.splice(that.sockets.indexOf(socket), 1); - }); - }); - - that.running.listen(port, function(err) { - if (err) return d.reject(err); - - that.port = port; - that.dir = dir; - that.emit("state", true); - d.resolve(); - }); - - return d.promise; - }); -}; - -module.exports = Server; diff --git a/lib/utils/string.js b/lib/utils/string.js deleted file mode 100644 index caa2364..0000000 --- a/lib/utils/string.js +++ /dev/null @@ -1,27 +0,0 @@ -var _ = require("lodash"); - -function escapeShellArg(arg) { - var ret = ""; - - ret = arg.replace(/"/g, '\\"'); - - return "\"" + ret + "\""; -} - -function optionsToShellArgs(options) { - return _.chain(options) - .map(function(value, key) { - if (value === null || value === undefined || value === false) return null; - if (value === true) return key; - return key+"="+escapeShellArg(value); - }) - .compact() - .value() - .join(" "); -} - -module.exports = { - escapeShellArg: escapeShellArg, - optionsToShellArgs: optionsToShellArgs, - toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase) -}; diff --git a/lib/utils/watch.js b/lib/utils/watch.js deleted file mode 100644 index 4d1a752..0000000 --- a/lib/utils/watch.js +++ /dev/null @@ -1,40 +0,0 @@ -var Q = require("q"); -var _ = require("lodash"); -var path = require("path"); -var chokidar = require("chokidar"); - -var parsers = require("gitbook-parsers"); - -function watch(dir) { - var d = Q.defer(); - dir = path.resolve(dir); - - var toWatch = [ - "book.json", "book.js" - ]; - - _.each(parsers.extensions, function(ext) { - toWatch.push("**/*"+ext); - }); - - var watcher = chokidar.watch(toWatch, { - cwd: dir, - ignored: "_book/**", - ignoreInitial: true - }); - - watcher.once("all", function(e, filepath) { - watcher.close(); - - d.resolve(filepath); - }); - watcher.once("error", function(err) { - watcher.close(); - - d.reject(err); - }); - - return d.promise; -} - -module.exports = watch; diff --git a/package.json b/package.json index 0d11bca..14f9f24 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "jsonschema": "1.0.2", "json-schema-defaults": "0.1.1", "merge-defaults": "0.2.1", - "github-slugid": "1.0.0" + "github-slugid": "1.0.0", + "destroy": "1.0.4", + "ignore": "2.2.19" }, "devDependencies": { "eslint": "1.5.0", diff --git a/test/assertions.js b/test/assertions.js deleted file mode 100644 index f9c4ba3..0000000 --- a/test/assertions.js +++ /dev/null @@ -1,69 +0,0 @@ -var _ = require('lodash'); -var fs = require('fs'); -var path = require('path'); -var should = require('should'); -var cheerio = require('cheerio'); - -require('should-promised'); - -should.Assertion.add('file', function(file, description) { - this.params = { actual: this.obj.toString(), operator: 'have file ' + file, message: description }; - - this.obj.should.have.property('options').which.is.an.Object(); - this.obj.options.should.have.property('output').which.is.a.String(); - this.assert(fs.existsSync(path.resolve(this.obj.options.output, file))); -}); - -should.Assertion.add('jsonfile', function(file, description) { - this.params = { actual: this.obj.toString(), operator: 'have valid jsonfile ' + file, message: description }; - - this.obj.should.have.property('options').which.is.an.Object(); - this.obj.options.should.have.property('output').which.is.a.String(); - this.assert(JSON.parse(fs.readFileSync(path.resolve(this.obj.options.output, file), { encoding: 'utf-8' }))); -}); - -should.Assertion.add('html', function(rules, description) { - this.params = { actual: 'HTML string', operator: 'valid html', message: description }; - var $ = cheerio.load(this.obj); - - _.each(rules, function(validations, query) { - validations = _.defaults(validations || {}, { - // Select a specific element in the list of matched elements - index: null, - - // Check that there is the correct count of elements - count: 1, - - // Check attribute values - attributes: {}, - - // Trim inner text - trim: false, - - // Check inner text - text: undefined - }); - - var $el = $(query); - - // Select correct element - if (_.isNumber(validations.index)) $el = $($el.get(validations.index)); - - // Test number of elements - $el.length.should.be.equal(validations.count); - - // Test text - if (validations.text !== undefined) { - var text = $el.text(); - if (validations.trim) text = text.trim(); - text.should.be.equal(validations.text); - } - - // Test attributes - _.each(validations.attributes, function(value, name) { - var attr = $el.attr(name); - should(attr).be.ok(); - attr.should.be.equal(value); - }); - }); -}); diff --git a/test/books/basic/README.md b/test/books/basic/README.md deleted file mode 100644 index 09ade40..0000000 --- a/test/books/basic/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Readme - -Default description for the book. diff --git a/test/books/basic/SUMMARY.md b/test/books/basic/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/basic/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/config-js/README.md b/test/books/config-js/README.md deleted file mode 100644 index f395431..0000000 --- a/test/books/config-js/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme diff --git a/test/books/config-js/SUMMARY.md b/test/books/config-js/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/config-js/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/config-js/book.js b/test/books/config-js/book.js deleted file mode 100644 index 8731343..0000000 --- a/test/books/config-js/book.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - "title": "js-config" -}; diff --git a/test/books/config-json/README.md b/test/books/config-json/README.md deleted file mode 100644 index f395431..0000000 --- a/test/books/config-json/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme diff --git a/test/books/config-json/SUMMARY.md b/test/books/config-json/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/config-json/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/config-json/book.json b/test/books/config-json/book.json deleted file mode 100644 index eda10bb..0000000 --- a/test/books/config-json/book.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "json-config" -} diff --git a/test/books/conrefs/README.md b/test/books/conrefs/README.md deleted file mode 100644 index 324ee1f..0000000 --- a/test/books/conrefs/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Readme - -<p id="test-plugin-block-shortcuts-1">$$test_block1$$</p> -<p id="test-plugin-block-shortcuts-2">{% include "./block.md" %}</p> - -### Relative - -<p id="t1">{% include "./hello.md" %}</p> -<p id="t2">{% include "/hello.md" %}</p> - -### Git - -<p id="t3">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}</p> -<p id="t4">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}</p> -<p id="t5">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}</p> diff --git a/test/books/conrefs/SUMMARY.md b/test/books/conrefs/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/conrefs/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/conrefs/block.md b/test/books/conrefs/block.md deleted file mode 100644 index 3910cb6..0000000 --- a/test/books/conrefs/block.md +++ /dev/null @@ -1 +0,0 @@ -$$test_block2$$ diff --git a/test/books/conrefs/hello.md b/test/books/conrefs/hello.md deleted file mode 100644 index 557db03..0000000 --- a/test/books/conrefs/hello.md +++ /dev/null @@ -1 +0,0 @@ -Hello World diff --git a/test/books/glossary/GLOSSARY.md b/test/books/glossary/GLOSSARY.md deleted file mode 100644 index 18840d2..0000000 --- a/test/books/glossary/GLOSSARY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Glossary - -## test - -Just a simple and easy to understand test. - -## hakunamatata - -This word is not present in the content. - -## test long - -This is a test with a longer text that the first entry to test order. diff --git a/test/books/glossary/README.md b/test/books/glossary/README.md deleted file mode 100644 index d46da8b..0000000 --- a/test/books/glossary/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Readme - -The word test should be replaced by a glossary link. - -``` -But the word test should not be replaced in code blocks -``` - -The two words test long should be replaced by the correct glossary links. - diff --git a/test/books/glossary/SUMMARY.md b/test/books/glossary/SUMMARY.md deleted file mode 100644 index deaea20..0000000 --- a/test/books/glossary/SUMMARY.md +++ /dev/null @@ -1,3 +0,0 @@ -# Summary - -* [Page](folder/PAGE.md) diff --git a/test/books/glossary/folder/PAGE.md b/test/books/glossary/folder/PAGE.md deleted file mode 100644 index 33b17c2..0000000 --- a/test/books/glossary/folder/PAGE.md +++ /dev/null @@ -1,3 +0,0 @@ -# Page - -Just a page in a sub-directory to test relative link to glossary. diff --git a/test/books/headings/README.md b/test/books/headings/README.md deleted file mode 100644 index b08c485..0000000 --- a/test/books/headings/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Hello World - -## Hello {#hello-custom} diff --git a/test/books/headings/SUMMARY.md b/test/books/headings/SUMMARY.md deleted file mode 100644 index e69de29..0000000 --- a/test/books/headings/SUMMARY.md +++ /dev/null diff --git a/test/books/highlight/README.md b/test/books/highlight/README.md deleted file mode 100644 index f47ac83..0000000 --- a/test/books/highlight/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Readme - -Block without language - -``` -test 1 -``` - -Block with a language - -```lang -test 2 -``` - -Inline code: `test 3` -Inline code with html: `<test>`
\ No newline at end of file diff --git a/test/books/highlight/SUMMARY.md b/test/books/highlight/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/highlight/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/images/README.md b/test/books/images/README.md deleted file mode 100644 index 484f410..0000000 --- a/test/books/images/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Tests for Images - -# SVG relative image - - -### Convert SVG to PNG - - -### Should only convert it once - - -### Download remote images - - -### Remote images with same filename - - - diff --git a/test/books/images/SUMMARY.md b/test/books/images/SUMMARY.md deleted file mode 100644 index ec0c4fc..0000000 --- a/test/books/images/SUMMARY.md +++ /dev/null @@ -1,3 +0,0 @@ -# Summary - -* [Page](./folder/PAGE.md) diff --git a/test/books/images/folder/PAGE.md b/test/books/images/folder/PAGE.md deleted file mode 100644 index 8beb060..0000000 --- a/test/books/images/folder/PAGE.md +++ /dev/null @@ -1,3 +0,0 @@ -### Images from other folder - - diff --git a/test/books/images/test.svg b/test/books/images/test.svg deleted file mode 100644 index 48bba70..0000000 --- a/test/books/images/test.svg +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd"> -<svg version="1.1" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg-root" width="100%" height="100%" viewBox="0 0 480 360"> - <title id="test-title">basic SVG tiny doc</title> - <g id="test-body-content"> - <text font-family="Arial" font-size="14" text-anchor="middle" x="225" y="25">hello world</text> - </g> - <text id="revision" x="10" y="340" font-size="40" stroke="none" fill="black">Revision: 1.1</text> - <rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/> -</svg> - diff --git a/test/books/init/.gitignore b/test/books/init/.gitignore deleted file mode 100644 index 8a88b2a..0000000 --- a/test/books/init/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!SUMMARY.md -!.gitignore diff --git a/test/books/init/SUMMARY.md b/test/books/init/SUMMARY.md deleted file mode 100644 index 31c1561..0000000 --- a/test/books/init/SUMMARY.md +++ /dev/null @@ -1,9 +0,0 @@ -# Summary - -* [Hello](hello.md) -* [Hello 2](hello2.md) -* Hello 3 - * [Hello 4](hello3/hello4.md) - * Hello 5 - * [Hello 6](hello3/hello5/hello6.md) - diff --git a/test/books/languages/LANGS.md b/test/books/languages/LANGS.md deleted file mode 100644 index 4267b3c..0000000 --- a/test/books/languages/LANGS.md +++ /dev/null @@ -1,4 +0,0 @@ -# Language - -* [English](en) -* [French](fr) diff --git a/test/books/languages/README.md b/test/books/languages/README.md deleted file mode 100644 index f395431..0000000 --- a/test/books/languages/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme diff --git a/test/books/languages/en/README.md b/test/books/languages/en/README.md deleted file mode 100644 index e965047..0000000 --- a/test/books/languages/en/README.md +++ /dev/null @@ -1 +0,0 @@ -Hello diff --git a/test/books/languages/en/SUMMARY.md b/test/books/languages/en/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/languages/en/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/languages/fr/README.md b/test/books/languages/fr/README.md deleted file mode 100644 index 632e4fe..0000000 --- a/test/books/languages/fr/README.md +++ /dev/null @@ -1 +0,0 @@ -Bonjour diff --git a/test/books/languages/fr/SUMMARY.md b/test/books/languages/fr/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/languages/fr/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/links/README.md b/test/books/links/README.md deleted file mode 100644 index f395431..0000000 --- a/test/books/links/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme diff --git a/test/books/links/SUMMARY.md b/test/books/links/SUMMARY.md deleted file mode 100644 index a7debc2..0000000 --- a/test/books/links/SUMMARY.md +++ /dev/null @@ -1,4 +0,0 @@ -# Summary - -* [Folder1](folder1/README.md) -* [Folder2](folder2/README.md)
\ No newline at end of file diff --git a/test/books/links/folder1/README.md b/test/books/links/folder1/README.md deleted file mode 100644 index 99a83f6..0000000 --- a/test/books/links/folder1/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Folder 1 - -[Link to folder2](../folder2/README.md) diff --git a/test/books/links/folder2/README.md b/test/books/links/folder2/README.md deleted file mode 100644 index aa2056a..0000000 --- a/test/books/links/folder2/README.md +++ /dev/null @@ -1 +0,0 @@ -# Folder 2 diff --git a/test/books/structure/README.adoc b/test/books/structure/README.adoc deleted file mode 100644 index 354647f..0000000 --- a/test/books/structure/README.adoc +++ /dev/null @@ -1 +0,0 @@ -== Readme for the bookk diff --git a/test/books/structure/README.md b/test/books/structure/README.md deleted file mode 100644 index 94f18a8..0000000 --- a/test/books/structure/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme for GitHub diff --git a/test/books/structure/SUMMARY.md b/test/books/structure/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/structure/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/structure/book.json b/test/books/structure/book.json deleted file mode 100644 index 110e0ba..0000000 --- a/test/books/structure/book.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "structure": { - "readme": "README.adoc" - } -}
\ No newline at end of file diff --git a/test/books/structure/glossary.md b/test/books/structure/glossary.md deleted file mode 100644 index 8c6c0fd..0000000 --- a/test/books/structure/glossary.md +++ /dev/null @@ -1,5 +0,0 @@ -# Glossary - -### Hello - -Hello world diff --git a/test/books/style-print/README.md b/test/books/style-print/README.md deleted file mode 100644 index 09ade40..0000000 --- a/test/books/style-print/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Readme - -Default description for the book. diff --git a/test/books/style-print/SUMMARY.md b/test/books/style-print/SUMMARY.md deleted file mode 100644 index ac9323c..0000000 --- a/test/books/style-print/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -# Summary diff --git a/test/books/style-print/styles/print.css b/test/books/style-print/styles/print.css deleted file mode 100644 index b05faf8..0000000 --- a/test/books/style-print/styles/print.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - color: red; -} diff --git a/test/books/summary/PAGE1.md b/test/books/summary/PAGE1.md deleted file mode 100644 index 9608001..0000000 --- a/test/books/summary/PAGE1.md +++ /dev/null @@ -1 +0,0 @@ -# Page 1 diff --git a/test/books/summary/README.md b/test/books/summary/README.md deleted file mode 100644 index f395431..0000000 --- a/test/books/summary/README.md +++ /dev/null @@ -1 +0,0 @@ -# Readme diff --git a/test/books/summary/SUMMARY.md b/test/books/summary/SUMMARY.md deleted file mode 100644 index 38d0f6b..0000000 --- a/test/books/summary/SUMMARY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Summary - -* [Page 1](./PAGE1.md) -* [Page 2](./folder/PAGE2.md) -* [Don't exists](./NOTFOUND.md) diff --git a/test/books/summary/folder/PAGE2.md b/test/books/summary/folder/PAGE2.md deleted file mode 100644 index f310be3..0000000 --- a/test/books/summary/folder/PAGE2.md +++ /dev/null @@ -1 +0,0 @@ -# Page 2 diff --git a/test/codehighlighting.js b/test/codehighlighting.js deleted file mode 100644 index f167980..0000000 --- a/test/codehighlighting.js +++ /dev/null @@ -1,65 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var Plugin = require('../lib/plugin'); -var PLUGINS_ROOT = path.resolve(__dirname, 'plugins'); - -describe('Code Highlighting', function () { - var book, PAGE; - - before(function() { - return books.generate('highlight', 'website', { - prepare: function(_book) { - book = _book; - - var plugin = new Plugin(book, 'replace_highlight'); - plugin.load('./replace_highlight', PLUGINS_ROOT); - - book.plugins.load(plugin); - } - }) - .then(function() { - PAGE = fs.readFileSync( - path.join(book.options.output, 'index.html'), - { encoding: 'utf-8' } - ); - }); - }); - - it('should correctly replace highlighting', function() { - PAGE.should.be.html({ - 'code': { - index: 0, - text: 'code_test 1\n_code' - } - }); - }); - - it('should correctly replace highlighting with language', function() { - PAGE.should.be.html({ - 'code': { - index: 1, - text: 'lang_test 2\n_lang' - } - }); - }); - - it('should correctly replace highlighting for inline code', function() { - PAGE.should.be.html({ - 'code': { - index: 2, - text: 'code_test 3_code' - } - }); - }); - - it('should correctly replace highlighting for inline code with html tags', function() { - PAGE.should.be.html({ - 'code': { - index: 3, - text: 'code_<test>_code' - } - }); - }); -}); - diff --git a/test/config.js b/test/config.js new file mode 100644 index 0000000..474ffea --- /dev/null +++ b/test/config.js @@ -0,0 +1,39 @@ +var mock = require('./mock'); + +describe('Config', function() { + + describe('config.load()', function() { + it('should not fail if no configuration file', function() { + return mock.setupDefaultBook() + .then(function(book) { + return book.prepareConfig(); + }); + }); + + it('should load from a JSON file', function() { + return mock.setupDefaultBook({ + 'book.json': { title: 'Hello World' } + }) + .then(function(book) { + return book.prepareConfig() + .then(function() { + book.config.get('title', '').should.equal('Hello World'); + }); + }); + }); + + it('should load from a JS file', function() { + return mock.setupDefaultBook({ + 'book.js': 'module.exports = { title: "Hello World" };' + }) + .then(function(book) { + return book.prepareConfig() + .then(function() { + book.config.get('title', '').should.equal('Hello World'); + }); + }); + }); + }); + +}); + diff --git a/test/configuration.js b/test/configuration.js deleted file mode 100644 index d30fd61..0000000 --- a/test/configuration.js +++ /dev/null @@ -1,37 +0,0 @@ -describe('Configuration', function () { - it('should extract default title from README', function() { - return books.parse('basic') - .then(function(book) { - book.options.title.should.be.equal('Readme'); - }); - }); - - it('should extract default description from README', function() { - return books.parse('basic') - .then(function(book) { - book.options.description.should.be.equal('Default description for the book.'); - }); - }); - - it('should correctly load from json (book.json)', function() { - return books.parse('config-json') - .then(function(book) { - book.options.title.should.be.equal('json-config'); - }); - }); - - it('should correctly load from JavaScript (book.js)', function() { - return books.parse('config-js') - .then(function(book) { - book.options.title.should.be.equal('js-config'); - }); - }); - - it('should provide configuration on book.config.get', function() { - return books.parse('basic') - .then(function(book) { - book.config.get('description').should.be.equal('Default description for the book.'); - book.getConfig('description').should.be.equal('Default description for the book.'); - }); - }); -}); diff --git a/test/conrefs.js b/test/conrefs.js deleted file mode 100644 index 32e4058..0000000 --- a/test/conrefs.js +++ /dev/null @@ -1,68 +0,0 @@ -var fs = require('fs'); -var path = require('path'); - -describe('ConRefs', function () { - var book, readme; - - before(function() { - return books.generate('conrefs', 'website') - .then(function(_book) { - book = _book; - - readme = fs.readFileSync( - path.join(book.options.output, 'index.html'), - { encoding: 'utf-8' } - ); - }); - }); - - it('should handle local references', function() { - readme.should.be.html({ - '.page-inner p#t1': { - count: 1, - text: 'Hello World', - trim: true - } - }); - }); - - it('should handle local references with absolute paths', function() { - readme.should.be.html({ - '.page-inner p#t2': { - count: 1, - text: 'Hello World', - trim: true - } - }); - }); - - it('should correctly include file from git reference', function() { - readme.should.be.html({ - '.page-inner p#t3': { - count: 1, - text: 'Hello from git', - trim: true - } - }); - }); - - it('should correctly handle deep include in git reference', function() { - readme.should.be.html({ - '.page-inner p#t4': { - count: 1, - text: 'First Hello. Hello from git', - trim: true - } - }); - }); - - it('should correctly handle absolute include in git reference', function() { - readme.should.be.html({ - '.page-inner p#t5': { - count: 1, - text: 'First Hello. Hello from git', - trim: true - } - }); - }); -}); diff --git a/test/ebook.js b/test/ebook.js deleted file mode 100644 index 5caf28b..0000000 --- a/test/ebook.js +++ /dev/null @@ -1,70 +0,0 @@ -var fs = require('fs'); -var path = require('path'); - -describe('eBook generator', function () { - describe('Basic Book', function() { - var book; - - before(function() { - return books.generate('basic', 'ebook') - .then(function(_book) { - book = _book; - }); - }); - - it('should correctly output a SUMMARY.html', function() { - book.should.have.file('SUMMARY.html'); - }); - - it('should correctly copy assets', function() { - book.should.have.file('gitbook'); - book.should.have.file('gitbook/ebook.css'); - }); - - it('should not copy website assets', function() { - book.should.not.have.file('gitbook/style.css'); - }); - }); - - describe('Custom styles', function() { - var book; - - before(function() { - return books.generate('style-print', 'ebook') - .then(function(_book) { - book = _book; - }); - }); - - it('should correctly copy print.css', function() { - book.should.have.file('styles'); - book.should.have.file('styles/print.css'); - }); - - it('should remove default print.css', function() { - var PAGE = fs.readFileSync( - path.join(book.options.output, 'index.html'), - { encoding: 'utf-8' } - ); - - // There are 2 styles (one from plugin-highlight and the new style) - PAGE.should.be.html({ - 'link': { - count: 2 - } - }); - - PAGE.should.be.html({ - 'link[href=\'./styles/print.css\']': { - count: 1 - } - }); - - PAGE.should.be.html({ - 'link[href="gitbook/plugins/gitbook-plugin-highlight/ebook.css"]': { - count: 1 - } - }); - }); - }); -}); diff --git a/test/format.js b/test/format.js deleted file mode 100644 index 2ec1a6f..0000000 --- a/test/format.js +++ /dev/null @@ -1,11 +0,0 @@ -describe('Formatting', function () { - it('should provide formatting with book.formatString', function() { - return books.parse('basic') - .then(function(book) { - return book.formatString('markdown', 'this is a **test**'); - }) - .then(function(content) { - content.should.equal('<p>this is a <strong>test</strong></p>\n'); - }); - }); -}); diff --git a/test/git.js b/test/git.js deleted file mode 100644 index 6fd6b41..0000000 --- a/test/git.js +++ /dev/null @@ -1,31 +0,0 @@ -var should = require("should"); -var git = require("../lib/utils/git"); - -describe("GIT parser and getter", function () { - it("should correctly parse an https url", function() { - var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md"); - - should.exist(parts); - parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git"); - parts.ref.should.be.equal("master"); - parts.filepath.should.be.equal("test.md"); - }); - - it("should correctly parse an https url with a reference", function() { - var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#0.1.2"); - - should.exist(parts); - parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git"); - parts.ref.should.be.equal("0.1.2"); - parts.filepath.should.be.equal("test.md"); - }); - - it("should correctly parse an ssh url", function() { - var parts = git.parseUrl("git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1"); - - should.exist(parts); - parts.host.should.be.equal("git@github.com:GitbookIO/gitbook.git"); - parts.ref.should.be.equal("e1594cde2c32e4ff48f6c4eff3d3d461743d74e1"); - parts.filepath.should.be.equal("directory/README.md"); - }); -}); diff --git a/test/glossary.js b/test/glossary.js deleted file mode 100644 index f0f31a5..0000000 --- a/test/glossary.js +++ /dev/null @@ -1,87 +0,0 @@ -var fs = require("fs"); -var path = require("path"); - -describe("Glossary", function () { - describe("Parsing", function() { - var book; - - before(function() { - return books.parse("glossary") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly list items", function() { - book.should.have.property("glossary"); - book.glossary.should.have.lengthOf(3); - }); - }); - - describe("Generation", function() { - var book; - - before(function() { - return books.generate("glossary", "website") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly generate a GLOSSARY.html", function() { - book.should.have.file("GLOSSARY.html"); - }); - - describe("Page Integration", function() { - var readme, page; - - before(function() { - readme = fs.readFileSync( - path.join(book.options.output, "index.html"), - { encoding: "utf-8" } - ); - page = fs.readFileSync( - path.join(book.options.output, "folder/PAGE.html"), - { encoding: "utf-8" } - ); - }); - - it("should correctly replaced terms by links", function() { - readme.should.be.html({ - ".page-inner a[href=\"GLOSSARY.html#test\"]": { - count: 1, - text: "test", - attributes: { - title: "Just a simple and easy to understand test." - } - } - }); - }); - - it("should correctly replaced terms by links (relative)", function() { - page.should.be.html({ - ".page-inner a[href=\"../GLOSSARY.html#test\"]": { - count: 1 - } - }); - }); - - it("should not replace terms in codeblocks", function() { - readme.should.be.html({ - ".page-inner code a": { - count: 0 - } - }); - }); - - it("should correctly select the longest term", function() { - readme.should.be.html({ - ".page-inner a[href=\"GLOSSARY.html#test_long\"]": { - count: 1, - text: "test long" - } - }); - }); - }); - }); -}); diff --git a/test/heading.js b/test/heading.js deleted file mode 100644 index f6d65c3..0000000 --- a/test/heading.js +++ /dev/null @@ -1,37 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -describe('Headings', function () { - var book, PAGE; - - before(function() { - return books.generate('headings', 'website') - .then(function(_book) { - book = _book; - - PAGE = fs.readFileSync( - path.join(book.options.output, 'index.html'), - { encoding: 'utf-8' } - ); - }); - }); - - describe('IDs', function() { - it('should correctly generate an ID', function() { - PAGE.should.be.html({ - 'h1#hello-world': { - count: 1 - } - }); - }); - - it('should correctly accept custom ID', function() { - PAGE.should.be.html({ - 'h2#hello-custom': { - count: 1 - } - }); - }); - }); -}); - diff --git a/test/helper.js b/test/helper.js deleted file mode 100644 index bbe82de..0000000 --- a/test/helper.js +++ /dev/null @@ -1,78 +0,0 @@ -var os = require('os'); -var path = require('path'); -var Q = require('q'); -var _ = require('lodash'); - -var fsUtil = require('../lib/utils/fs'); -var Book = require('../').Book; -var LOG_LEVELS = require('../').LOG_LEVELS; - -require('./assertions'); - - -var BOOKS = {}; -var TMPDIR = os.tmpdir(); - - -// Generate and return a book -function generateBook(bookId, test, opts) { - opts = _.defaults(opts || {}, { - prepare: function() {} - }); - - return parseBook(bookId, test, opts) - .then(function(book) { - - return Q(opts.prepare(book)) - .then(function() { - return book.generate(test); - }) - .thenResolve(book); - }); -} - -// Generate and return a book -function parseBook(bookId, test, opts) { - opts = _.defaults(opts || {}, { - testId: '' - }); - - test = test || 'website'; - var testId = [test, opts.testId].join('-'); - - BOOKS[bookId] = BOOKS[bookId] || {}; - if (BOOKS[bookId][testId]) return Q(BOOKS[bookId][testId]); - - BOOKS[bookId][testId] = new Book(path.resolve(__dirname, 'books', bookId), { - logLevel: LOG_LEVELS.DISABLED, - config: { - output: path.resolve(TMPDIR, bookId+'-'+testId) - } - }); - - return BOOKS[bookId][testId].parse() - .then(function() { - return BOOKS[bookId][testId]; - }); -} - - -global.books = { - parse: parseBook, - generate: generateBook -}; - -// Cleanup all tests -after(function() { - return _.chain(BOOKS) - .map(function(types) { - return _.values(types); - }) - .flatten() - .reduce(function(prev, book) { - return prev.then(function() { - return fsUtil.remove(book.options.output); - }); - }, Q()) - .value(); -}); diff --git a/test/images.js b/test/images.js deleted file mode 100644 index de45066..0000000 --- a/test/images.js +++ /dev/null @@ -1,58 +0,0 @@ -var fs = require("fs"); -var _ = require("lodash"); -var path = require("path"); -var cheerio = require("cheerio"); - -describe("Images", function () { - var book, readme, $, $img, srcs; - - before(function() { - return books.generate("images", "ebook") - .then(function(_book) { - book = _book; - - readme = fs.readFileSync( - path.join(book.options.output, "index.html"), - { encoding: "utf-8" } - ); - $ = cheerio.load(readme); - $img = $("img"); - srcs = $img.map(function() { - return $(this).attr("src"); - }); - }); - }); - - it("should detect all images", function() { - _.uniq(srcs).should.have.lengthOf(4); - }); - - it("should keep image tags", function() { - srcs.should.have.lengthOf(5); - }); - - it("should not have .svg files", function() { - _.each(srcs, function(src) { - path.extname(src).should.not.equal(".svg"); - }); - }); - - it("should correctly convert svg images to png", function() { - _.each(srcs, function(src) { - book.should.have.file(src); - }); - }); - - it("should handle relative paths", function() { - var PAGE = fs.readFileSync( - path.join(book.options.output, "folder/PAGE.html"), - { encoding: "utf-8" } - ); - - PAGE.should.be.html({ - "img[src=\"../test.png\"]": { - count: 1 - } - }); - }); -}); diff --git a/test/init.js b/test/init.js deleted file mode 100644 index 625d77c..0000000 --- a/test/init.js +++ /dev/null @@ -1,24 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var should = require('should'); - -var Book = require('../').Book; -var LOG_LEVELS = require('../').LOG_LEVELS; - -describe('Init Books', function () { - var initRoot; - - before(function() { - initRoot = path.resolve(__dirname, 'books/init'); - return Book.init(initRoot, { - logLevel: LOG_LEVELS.DISABLED - }); - }); - - it('should create all chapters', function() { - should(fs.existsSync(path.resolve(initRoot, 'hello.md'))).be.ok(); - should(fs.existsSync(path.resolve(initRoot, 'hello2.md'))).be.ok(); - should(fs.existsSync(path.resolve(initRoot, 'hello3/hello4.md'))).be.ok(); - should(fs.existsSync(path.resolve(initRoot, 'hello3/hello5/hello6.md'))).be.ok(); - }); -}); diff --git a/test/json.js b/test/json.js deleted file mode 100644 index 60baf9a..0000000 --- a/test/json.js +++ /dev/null @@ -1,92 +0,0 @@ -var fs = require('fs'); -var path = require('path'); - -describe('JSON generator', function () { - describe('Basic Book', function() { - var book; - - before(function() { - return books.generate('basic', 'json') - .then(function(_book) { - book = _book; - }); - }); - - it('should correctly output a README.json', function() { - book.should.have.file('README.json'); - }); - - it('should output a valid json', function() { - book.should.have.jsonfile('README.json'); - }); - - describe('Page Format', function() { - var page; - - before(function() { - page = JSON.parse( - fs.readFileSync( - path.join(book.options.output, 'README.json'), - { encoding: 'utf-8' } - ) - ); - }); - - it('should contains valid section', function() { - page.should.have.property('sections').with.lengthOf(1); - page.sections[0].should.have.property('content').which.is.a.String(); - page.sections[0].should.have.property('type', 'normal'); - }); - - it('should contains valid progress', function() { - page.should.have.property('progress'); - page.progress.should.have.property('chapters').with.lengthOf(1); - page.progress.should.have.property('current'); - }); - - it('should contains no languages', function() { - page.should.have.property('langs').with.lengthOf(0); - }); - }); - }); - - describe('Multilingual Book', function() { - var book; - - before(function() { - return books.generate('languages', 'json') - .then(function(_book) { - book = _book; - }); - }); - - it('should correctly output READMEs', function() { - book.should.have.file('README.json'); - book.should.have.file('en/README.json'); - book.should.have.file('fr/README.json'); - }); - - it('should output valid json', function() { - book.should.have.jsonfile('README.json'); - book.should.have.jsonfile('en/README.json'); - book.should.have.jsonfile('fr/README.json'); - }); - - describe('Page Format', function() { - var page; - - before(function() { - page = JSON.parse( - fs.readFileSync( - path.join(book.options.output, 'README.json'), - { encoding: 'utf-8' } - ) - ); - }); - - it('should contains no languages', function() { - page.should.have.property('langs').with.lengthOf(2); - }); - }); - }); -}); diff --git a/test/languages.js b/test/languages.js deleted file mode 100644 index 0bde347..0000000 --- a/test/languages.js +++ /dev/null @@ -1,37 +0,0 @@ -describe("Languages", function () { - describe("Parsing", function() { - var book; - - before(function() { - return books.parse("languages") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly list languages", function() { - book.should.have.property("books"); - book.books.should.have.lengthOf(2); - - book.books[0].options.language.should.be.equal("en"); - book.books[1].options.language.should.be.equal("fr"); - }); - }); - - describe("Generation", function() { - var book; - - before(function() { - return books.generate("languages", "website") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly create books", function() { - book.should.have.file("index.html"); - book.should.have.file("en/index.html"); - book.should.have.file("fr/index.html"); - }); - }); -}); diff --git a/test/links.js b/test/links.js deleted file mode 100644 index baca9d1..0000000 --- a/test/links.js +++ /dev/null @@ -1,63 +0,0 @@ -var fs = require("fs"); -var path = require("path"); -var cheerio = require("cheerio"); - -var links = require("../lib/utils/links"); - -describe("Links", function () { - it("should correctly test external links", function() { - links.isExternal("http://google.fr").should.be.exactly(true); - links.isExternal("https://google.fr").should.be.exactly(true); - links.isExternal("test.md").should.be.exactly(false); - links.isExternal("folder/test.md").should.be.exactly(false); - links.isExternal("/folder/test.md").should.be.exactly(false); - }); - - it("should correctly detect anchor links", function() { - links.isAnchor("#test").should.be.exactly(true); - links.isAnchor(" #test").should.be.exactly(true); - links.isAnchor("https://google.fr#test").should.be.exactly(false); - links.isAnchor("test.md#test").should.be.exactly(false); - }); - - describe("toAbsolute", function() { - it("should correctly transform as absolute", function() { - links.toAbsolute("http://google.fr").should.be.equal("http://google.fr"); - links.toAbsolute("test.md", "./", "./").should.be.equal("test.md"); - links.toAbsolute("folder/test.md", "./", "./").should.be.equal("folder/test.md"); - }); - - it("should correctly handle windows path", function() { - links.toAbsolute("folder\\test.md", "./", "./").should.be.equal("folder/test.md"); - }); - - it("should correctly handle absolute path", function() { - links.toAbsolute("/test.md", "./", "./").should.be.equal("test.md"); - links.toAbsolute("/test.md", "test", "test").should.be.equal("../test.md"); - links.toAbsolute("/sub/test.md", "test", "test").should.be.equal("../sub/test.md"); - }); - }); - - describe("page", function() { - var book; - - before(function() { - return books.generate("links", "website") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly replace relative links", function() { - var readme = fs.readFileSync( - path.join(book.options.output, "folder1/index.html"), - { encoding: "utf-8" } - ); - var $ = cheerio.load(readme); - var $a = $(".page-inner a"); - - $a.attr("href").should.be.exactly("../folder2/index.html"); - }); - }); - -}); diff --git a/test/mock.js b/test/mock.js new file mode 100644 index 0000000..f562fa9 --- /dev/null +++ b/test/mock.js @@ -0,0 +1,61 @@ +var Q = require('q'); +var _ = require('lodash'); +var tmp = require('tmp'); +var path = require('path'); + +require('should'); +require('should-promised'); + +var Book = require('../').Book; +var NodeFS = require('../lib/fs/node'); + +// Create filesystem instance for testing +var fs = new NodeFS(); + +function setupFS(fs, rootFolder, files) { + return _.chain(_.pairs(files)) + .sortBy(0) + .reduce(function(prev, pair) { + return prev.then(function() { + var filename = path.resolve(rootFolder, pair[0]); + var buf = pair[1]; + + if (_.isObject(buf)) buf = JSON.stringify(buf); + if (_.isString(buf)) buf = new Buffer(buf, 'utf-8'); + + return fs.write(filename, buf); + }); + }, Q()) + .value() + .then(function() { + return fs; + }); +} + +// Setup a mock book for testing using a map of files +function setupBook(files, opts) { + opts = opts || {}; + + return Q.nfcall(tmp.dir.bind(tmp)).get(0) + .then(function(folder) { + opts.fs = fs; + opts.root = folder; + return setupFS(fs, folder, files); + }) + .then(function(fs) { + return new Book(opts); + }); +} + +// Setup a book with default README/SUMMARY +function setupDefaultBook(files, opts) { + return setupBook(_.defaults(files || {}, { + 'README.md': 'Hello', + 'SUMMARY.md': '# Summary' + }), opts); +} + +module.exports = { + setupBook: setupBook, + setupDefaultBook: setupDefaultBook +}; diff --git a/test/navigation.js b/test/navigation.js deleted file mode 100644 index 9118b3c..0000000 --- a/test/navigation.js +++ /dev/null @@ -1,61 +0,0 @@ -var should = require("should"); - -describe("Navigation", function () { - var book; - - before(function() { - return books.parse("summary") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly parse navigation as a map", function() { - book.should.have.property("navigation"); - book.navigation.should.have.property("README.md"); - book.navigation.should.have.property("README.md"); - }); - - it("should correctly include filenames", function() { - book.navigation.should.have.property("README.md"); - book.navigation.should.have.property("PAGE1.md"); - book.navigation.should.have.property("folder/PAGE2.md"); - book.navigation.should.not.have.property("NOTFOUND.md"); - }); - - it("should correctly detect next/prev for README", function() { - var README = book.navigation["README.md"]; - - README.index.should.equal(0); - README.should.have.property("next"); - should(README.prev).not.be.ok(); - - README.next.should.have.property("path"); - README.next.path.should.equal("PAGE1.md"); - }); - - it("should correctly detect next/prev a page", function() { - var PAGE = book.navigation["PAGE1.md"]; - - PAGE.index.should.equal(1); - PAGE.should.have.property("next"); - PAGE.should.have.property("prev"); - - PAGE.prev.should.have.property("path"); - PAGE.prev.path.should.equal("README.md"); - - PAGE.next.should.have.property("path"); - PAGE.next.path.should.equal("folder/PAGE2.md"); - }); - - it("should correctly detect next/prev for last page", function() { - var PAGE = book.navigation["folder/PAGE2.md"]; - - PAGE.index.should.equal(2); - PAGE.should.have.property("prev"); - should(PAGE.next).not.be.ok(); - - PAGE.prev.should.have.property("path"); - PAGE.prev.path.should.equal("PAGE1.md"); - }); -}); diff --git a/test/parse.js b/test/parse.js new file mode 100644 index 0000000..dca780e --- /dev/null +++ b/test/parse.js @@ -0,0 +1,24 @@ +var mock = require('./mock'); + +describe('Parsing', function() { + + it('should fail without SUMMARY', function() { + return mock.setupBook({ + 'README.md': '' + }) + .then(function(book) { + return book.parse().should.be.rejected; + }); + }); + + it('should fail without README', function() { + return mock.setupBook({ + 'SUMMARY.md': '' + }) + .then(function(book) { + return book.parse().should.be.rejected; + }); + }); + +}); + diff --git a/test/plugins.js b/test/plugins.js deleted file mode 100644 index 1600d0d..0000000 --- a/test/plugins.js +++ /dev/null @@ -1,330 +0,0 @@ -var _ = require('lodash'); -var fs = require('fs'); -var should = require('should'); -var path = require('path'); - -var Plugin = require('../lib/plugin'); -var parsers = require('gitbook-parsers'); -var PLUGINS_ROOT = path.resolve(__dirname, 'plugins'); - -describe('Plugins', function () { - var book; - - before(function() { - return books.parse('basic') - .then(function(_book) { - book = _book; - }); - }); - - describe('Invalid', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'invalid'); - plugin.load('./invalid', PLUGINS_ROOT); - }); - - it('should be detected', function() { - should(plugin.isValid()).be.exactly(false); - }); - }); - - describe('Empty', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'empty'); - plugin.load('./empty', PLUGINS_ROOT); - }); - - it('should valid a plugin', function() { - should(plugin.isValid()).be.exactly(true); - }); - - it('should return an empty list of resources', function() { - return plugin.getResources() - .then(function(resources) { - _.each(Plugin.RESOURCES, function(resName) { - resources[resName].should.have.lengthOf(0); - }); - }); - }); - }); - - describe('Configuration', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'testconfig'); - plugin.load('./config', PLUGINS_ROOT); - }); - - it('should throw error for invalid configuration', function() { - return plugin.validateConfig({}) - .should.be.rejectedWith('Configuration Error: pluginsConfig.testconfig.testRequired is required'); - }); - - it('should throw error for invalid types', function() { - return plugin.validateConfig({ - testRequired: 'hello' - }) - .should.be.rejectedWith('Configuration Error: pluginsConfig.testconfig.testRequired is not of a type(s) number'); - }); - - it('should extend with default values', function() { - return plugin.validateConfig({ - testRequired: 12 - }) - .should.be.fulfilledWith({ - hello: 'world', - testRequired: 12 - }); - }); - }); - - describe('Resources', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'resources'); - plugin.load('./resources', PLUGINS_ROOT); - - return book.plugins.load(plugin); - }); - - it('should valid a plugin', function() { - should(plugin.isValid()).be.exactly(true); - }); - - describe('Website', function() { - it('should return a valid list of resources', function() { - return plugin.getResources('website') - .then(function(resources) { - resources.js.should.have.lengthOf(1); - }); - }); - - it('should extend books plugins', function() { - var resources = book.plugins.resources('website'); - resources.js.should.have.lengthOf(5); - }); - }); - - describe('eBook', function() { - it('should return a valid list of resources', function() { - return plugin.getResources('ebook') - .then(function(resources) { - resources.css.should.have.lengthOf(1); - }); - }); - - it('should extend books plugins', function() { - var resources = book.plugins.resources('ebook'); - - // There is resources from highlight plugin and this plugin - resources.css.should.have.lengthOf(2); - should.exist(_.find(resources.css, { - path: 'gitbook-plugin-resources/test' - })); - }); - }); - }); - - describe('Filters', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'filters'); - plugin.load('./filters', PLUGINS_ROOT); - - return book.plugins.load(plugin); - }); - - it('should valid a plugin', function() { - should(plugin.isValid()).be.exactly(true); - }); - - it('should return a map of filters', function() { - var filters = plugin.getFilters(); - - _.size(filters).should.equal(2); - filters.should.have.property('hello'); - filters.should.have.property('helloCtx'); - }); - - it('should correctly extend template filters', function() { - return book.template.renderString('{{ \'World\'|hello }}') - .then(function(content) { - content.should.equal('Hello World'); - }); - }); - - it('should correctly set book as context', function() { - return book.template.renderString('{{ \'root\'|helloCtx }}') - .then(function(content) { - content.should.equal('root:'+book.root); - }); - }); - }); - - describe('Blocks', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'blocks'); - plugin.load('./blocks', PLUGINS_ROOT); - - return book.plugins.load(plugin); - }); - - var testTpl = function(str, args, options) { - return book.template.renderString(str, args, options) - .then(book.template.postProcess); - }; - - it('should valid a plugin', function() { - should(plugin.isValid()).be.exactly(true); - }); - - it('should correctly extend template blocks', function() { - return testTpl('{% test %}hello{% endtest %}') - .then(function(content) { - content.should.equal('testhellotest'); - }); - }); - - describe('Shortcuts', function() { - it('should correctly accept shortcuts', function() { - return testTpl('$$hello$$', {}, { - type: 'markdown' - }) - .then(function(content) { - content.should.equal('testhellotest'); - }); - }); - - it('should correctly apply shortcuts to included file', function() { - return books.generate('conrefs', 'website', { - testId: 'include-plugins', - prepare: function(bookConref) { - plugin = new Plugin(bookConref, 'blocks'); - plugin.load('./blocks', PLUGINS_ROOT); - - return bookConref.plugins.load(plugin); - } - }) - .then(function(bookConref) { - var readme = fs.readFileSync( - path.join(bookConref.options.output, 'index.html'), - { encoding: 'utf-8' } - ); - - readme.should.be.html({ - '.page-inner p#test-plugin-block-shortcuts-1': { - count: 1, - text: 'testtest_block1test', - trim: true - }, - '.page-inner p#test-plugin-block-shortcuts-2': { - count: 1, - text: 'testtest_block2test', - trim: true - } - }); - }); - }); - }); - - - it('should correctly extend template blocks with defined end', function() { - return testTpl('{% test2 %}hello{% endtest2end %}') - .then(function(content) { - content.should.equal('test2hellotest2'); - }); - }); - - it('should correctly extend template blocks with sub-blocks', function() { - return testTpl('{% test3join separator=";" %}hello{% also %}world{% endtest3join %}') - .then(function(content) { - content.should.equal('hello;world'); - }); - }); - - it('should correctly extend template blocks with different sub-blocks', function() { - return testTpl('{% test4join separator=";" %}hello{% also %}the{% finally %}world{% endtest4join %}') - .then(function(content) { - content.should.equal('hello;the;world'); - }); - }); - - it('should correctly extend template blocks with arguments (1)', function() { - return testTpl('{% test5args "a" %}{% endtest5args %}') - .then(function(content) { - content.should.equal('test5atest5'); - }); - }); - - it('should correctly extend template blocks with arguments (2)', function() { - return testTpl('{% test5args "a", "b" %}{% endtest5args %}') - .then(function(content) { - content.should.equal('test5a,btest5'); - }); - }); - - it('should correctly extend template blocks with arguments (3)', function() { - return testTpl('{% test5args "a", "b", "c" %}{% endtest5args %}') - .then(function(content) { - content.should.equal('test5a,b,ctest5'); - }); - }); - - it('should correctly extend template blocks with args and kwargs', function() { - return testTpl('{% test5kwargs "a", "b", "c", d="test", e="test2" %}{% endtest5kwargs %}') - .then(function(content) { - content.should.equal('test5a,b,c,d:test,e:test2,__keywords:truetest5'); - }); - }); - - it('should correctly extend template blocks with access to context', function() { - return testTpl('{% set name = "john" %}{% test6context %}{% endtest6context %}', {}) - .then(function(content) { - content.should.equal('test6johntest6'); - }); - }); - }); - - describe('Blocks without parsing', function() { - var plugin; - - before(function() { - plugin = new Plugin(book, 'blocks'); - plugin.load('./blocks', PLUGINS_ROOT); - - return book.plugins.load(plugin); - }); - - var testTpl = function(markup, str, args, options) { - var filetype = parsers.get(markup); - - return book.template.renderString(str, args, options) - .then(filetype.page).get('sections').get(0).get('content') - .then(book.template.postProcess); - }; - - it('should correctly process unparsable for markdown', function() { - return testTpl('.md', '{% test %}**hello**{% endtest %}') - .then(function(content) { - content.should.equal('<p>test**hello**test</p>\n'); - }); - }); - - it('should correctly process unparsable for asciidoc', function() { - return testTpl('.adoc', '{% test %}**hello**{% endtest %}') - .then(function(content) { - content.should.equal('<div class="paragraph">\n<p>test**hello**test</p>\n</div>'); - }); - }); - }); -}); - diff --git a/test/plugins/blocks/index.js b/test/plugins/blocks/index.js deleted file mode 100644 index 9bdbe86..0000000 --- a/test/plugins/blocks/index.js +++ /dev/null @@ -1,61 +0,0 @@ -var assert = require("assert"); - -module.exports = { - blocks: { - "test": { - shortcuts: { - parsers: ["markdown"], - start: "$$", - end: "$$" - }, - process: function(blk) { - return "test"+blk.body+"test"; - } - }, - "test2": { - end: "endtest2end", - process: function(blk) { - return "test2"+blk.body+"test2"; - } - }, - "test3join": { - blocks: [ - "also" - ], - process: function(blk) { - return [blk.body, blk.blocks[0].body].join(blk.kwargs.separator); - } - }, - "test4join": { - blocks: [ - "also", "finally" - ], - process: function(blk) { - assert(blk.blocks.length, 2); - assert(blk.blocks[0].name, "also"); - assert(blk.blocks[1].name, "finally"); - return [blk.body, blk.blocks[0].body, blk.blocks[1].body].join(blk.kwargs.separator); - } - }, - "test5args": { - process: function(blk) { - return "test5"+blk.args.join(",")+"test5"; - } - }, - "test5kwargs": { - process: function(blk) { - var s = blk.args.join(","); - for (var key in blk.kwargs) { - s = s + ","+key+":"+blk.kwargs[key]; - } - - return "test5"+s+"test5"; - } - }, - "test6context": { - process: function() { - return "test6"+(this.ctx.name)+"test6"; - } - }, - } -};
\ No newline at end of file diff --git a/test/plugins/blocks/package.json b/test/plugins/blocks/package.json deleted file mode 100644 index 7c41fd3..0000000 --- a/test/plugins/blocks/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-blocks", - "description": "Test blocks", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - } -}
\ No newline at end of file diff --git a/test/plugins/config/index.js b/test/plugins/config/index.js deleted file mode 100644 index f053ebf..0000000 --- a/test/plugins/config/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/test/plugins/config/package.json b/test/plugins/config/package.json deleted file mode 100644 index 03ef744..0000000 --- a/test/plugins/config/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "gitbook-plugin-testconfig", - "description": "Test plugin configuration", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - }, - "gitbook": { - "properties": { - "hello": { - "type": "string", - "default": "world" - }, - "testRequired": { - "type": "number", - "required": true - } - } - } -}
\ No newline at end of file diff --git a/test/plugins/empty/index.js b/test/plugins/empty/index.js deleted file mode 100644 index a099545..0000000 --- a/test/plugins/empty/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {};
\ No newline at end of file diff --git a/test/plugins/empty/package.json b/test/plugins/empty/package.json deleted file mode 100644 index 78c7e72..0000000 --- a/test/plugins/empty/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-empty", - "description": "Test empty plugin", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - } -}
\ No newline at end of file diff --git a/test/plugins/filters/index.js b/test/plugins/filters/index.js deleted file mode 100644 index 2cf53b1..0000000 --- a/test/plugins/filters/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - filters: { - hello: function(text) { - return "Hello "+text; - }, - helloCtx: function(text) { - return text+":"+this.book.root; - } - } -};
\ No newline at end of file diff --git a/test/plugins/filters/package.json b/test/plugins/filters/package.json deleted file mode 100644 index f1d4e45..0000000 --- a/test/plugins/filters/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-filters", - "description": "Test filters", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - } -}
\ No newline at end of file diff --git a/test/plugins/invalid/index.js b/test/plugins/invalid/index.js deleted file mode 100644 index a099545..0000000 --- a/test/plugins/invalid/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {};
\ No newline at end of file diff --git a/test/plugins/invalid/package.json b/test/plugins/invalid/package.json deleted file mode 100644 index da34090..0000000 --- a/test/plugins/invalid/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-invalid", - "description": "Test invalid plugin", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "<2.0.0" - } -}
\ No newline at end of file diff --git a/test/plugins/replace_highlight/index.js b/test/plugins/replace_highlight/index.js deleted file mode 100644 index 8586486..0000000 --- a/test/plugins/replace_highlight/index.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - blocks: { - "code": { - process: function(blk) { - var lang = blk.kwargs.language || "code"; - - return { - body: lang+"_"+blk.body+"_"+lang, - html: false - }; - } - } - } -};
\ No newline at end of file diff --git a/test/plugins/replace_highlight/package.json b/test/plugins/replace_highlight/package.json deleted file mode 100644 index 72d1033..0000000 --- a/test/plugins/replace_highlight/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-replace_highlight", - "description": "Test replacing default code highlighter", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - } -}
\ No newline at end of file diff --git a/test/plugins/resources/index.js b/test/plugins/resources/index.js deleted file mode 100644 index bafa54b..0000000 --- a/test/plugins/resources/index.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - website: { - js: [ - "https://cdn.mathjax.org/mathjax/2.4-latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" - ] - }, - ebook: { - css: [ - "test" - ] - } -}; diff --git a/test/plugins/resources/package.json b/test/plugins/resources/package.json deleted file mode 100644 index ab4320d..0000000 --- a/test/plugins/resources/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "gitbook-plugin-resources", - "description": "Test resources plugin", - "main": "index.js", - "version": "0.0.1", - "engines": { - "gitbook": "*" - } -}
\ No newline at end of file diff --git a/test/readme.js b/test/readme.js new file mode 100644 index 0000000..0ebb490 --- /dev/null +++ b/test/readme.js @@ -0,0 +1,34 @@ +var mock = require('./mock'); + +describe('Readme', function() { + + describe('Parsing', function() { + it('should parse empty readme', function() { + return mock.setupDefaultBook({ + 'README.md': '' + }) + .then(function(book) { + return book.prepareConfig() + + .then(function() { + return book.readme.load(); + }); + }); + }); + + it('should parse readme', function() { + return mock.setupDefaultBook({ + 'README.md': '# Hello World\nThis is my book' + }) + .then(function(book) { + return book.readme.load() + .then(function() { + book.readme.title.should.equal('Hello World'); + book.readme.description.should.equal('This is my book'); + }); + }); + }); + }); + +}); + diff --git a/test/resolve.js b/test/resolve.js deleted file mode 100644 index e474ed0..0000000 --- a/test/resolve.js +++ /dev/null @@ -1,60 +0,0 @@ -var path = require("path"); - -describe("Resolve Files", function () { - var book; - - before(function() { - return books.parse("basic") - .then(function(_book) { - book = _book; - }); - }); - - describe("book.fileIsInBook", function() { - it("should return true for correct paths", function() { - book.fileIsInBook(path.join(book.root, "README.md")).should.equal(true); - book.fileIsInBook(path.join(book.root, "styles/website.css")).should.equal(true); - }); - - it("should return true for root folder", function() { - book.fileIsInBook(path.join(book.root, "./")).should.equal(true); - book.fileIsInBook(book.root).should.equal(true); - }); - - it("should return false for files out of scope", function() { - book.fileIsInBook(path.join(book.root, "../")).should.equal(false); - book.fileIsInBook("README.md").should.equal(false); - book.fileIsInBook(path.resolve(book.root, "../README.md")).should.equal(false); - }); - - it("should correctly handle windows paths", function() { - book.fileIsInBook(path.join(book.root, "\\styles\\website.css")).should.equal(true); - }); - }); - - describe("book.resolve", function() { - it("should resolve a file to its absolute path", function() { - book.resolve("README.md").should.equal(path.resolve(book.root, "README.md")); - book.resolve("website/README.md").should.equal(path.resolve(book.root, "website/README.md")); - }); - - it("should correctly handle windows paths", function() { - book.resolve("styles\\website.css").should.equal(path.resolve(book.root, "styles\\website.css")); - }); - - it("should correctly resolve all arguments", function() { - book.resolve("test", "hello", "..", "README.md").should.equal(path.resolve(book.root, "test/README.md")); - }); - - it("should correctly resolve to root folder", function() { - book.resolve("test", "/README.md").should.equal(path.resolve(book.root, "README.md")); - book.resolve("test", "\\README.md").should.equal(path.resolve(book.root, "README.md")); - }); - - it("should throw an error for file out of book", function() { - (function() { - return book.resolve("../README.md"); - }).should.throw(); - }); - }); -}); diff --git a/test/structure.js b/test/structure.js deleted file mode 100644 index 90413cb..0000000 --- a/test/structure.js +++ /dev/null @@ -1,20 +0,0 @@ -describe('Structure', function () { - var book; - - before(function() { - return books.parse('structure') - .then(function(_book) { - book = _book; - }); - }); - - - it('should prioritize structure defined in book.json', function() { - book.readmeFile.should.equal('README.adoc'); - }); - - it('should be case incensitive', function() { - book.glossaryFile.should.equal('glossary.md'); - book.glossary.should.have.lengthOf(1); - }); -}); diff --git a/test/summary.js b/test/summary.js deleted file mode 100644 index 2d3a248..0000000 --- a/test/summary.js +++ /dev/null @@ -1,70 +0,0 @@ -var fs = require("fs"); -var path = require("path"); - -describe("Summary", function () { - describe("Parsing", function() { - var book; - - before(function() { - return books.parse("summary") - .then(function(_book) { - book = _book; - }); - }); - - it("should correctly list items", function() { - book.should.have.property("summary"); - book.summary.should.have.property("chapters"); - book.summary.chapters.should.have.lengthOf(4); - }); - - it("should correctly mark non-existant entries", function() { - book.summary.chapters[0].exists.should.have.equal(true); - book.summary.chapters[1].exists.should.have.equal(true); - book.summary.chapters[2].exists.should.have.equal(true); - book.summary.chapters[3].exists.should.have.equal(false); - }); - }); - - describe("Generation", function() { - var book; - - before(function() { - return books.generate("summary", "website") - .then(function(_book) { - book = _book; - }); - }); - - it("should create files according to summary", function() { - book.should.have.file("index.html"); - book.should.have.file("PAGE1.html"); - book.should.have.file("folder/PAGE2.html"); - }); - - it("should correctly output summary", function() { - var PAGE = fs.readFileSync( - path.join(book.options.output, "index.html"), - { encoding: "utf-8" } - ); - - PAGE.should.be.html({ - ".book-summary .chapter[data-level=\"0\"] a": { - attributes: { - href: "./index.html" - } - }, - ".book-summary .chapter[data-level=\"1\"] a": { - attributes: { - href: "./PAGE1.html" - } - }, - ".book-summary .chapter[data-level=\"2\"] a": { - attributes: { - href: "./folder/PAGE2.html" - } - } - }); - }); - }); -}); diff --git a/test/templating.js b/test/templating.js deleted file mode 100644 index f92154b..0000000 --- a/test/templating.js +++ /dev/null @@ -1,33 +0,0 @@ -var pkg = require("../package.json"); - -describe("Templating", function () { - var book; - - before(function() { - return books.parse("basic") - .then(function(_book) { - book = _book; - }); - }); - - var testTpl = function(str, args, options) { - return book.template.renderString(str, args, options) - .then(book.template.postProcess); - }; - - describe("Context", function() { - it("should correctly have access to generator", function() { - return testTpl("{{ gitbook.generator }}") - .then(function(content) { - content.should.equal("website"); - }); - }); - - it("should correctly have access to gitbook version", function() { - return testTpl("{{ gitbook.version }}") - .then(function(content) { - content.should.equal(pkg.version.toString()); - }); - }); - }); -}); diff --git a/test/website.js b/test/website.js deleted file mode 100644 index 6a0fd1c..0000000 --- a/test/website.js +++ /dev/null @@ -1,26 +0,0 @@ -describe('Website generator', function () { - describe('Basic Book', function() { - var book; - - before(function() { - return books.generate('basic', 'website') - .then(function(_book) { - book = _book; - }); - }); - - it('should correctly output an index.html', function() { - book.should.have.file('index.html'); - }); - - it('should correctly copy assets', function() { - book.should.have.file('gitbook'); - book.should.have.file('gitbook/app.js'); - book.should.have.file('gitbook/style.css'); - }); - - it('should not copy ebook assets', function() { - book.should.not.have.file('gitbook/ebook.css'); - }); - }); -}); |