diff options
Diffstat (limited to 'lib')
40 files changed, 692 insertions, 4227 deletions
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; |