var _ = require('lodash'); var Q = require('q'); var path = require('path'); var Ignore = require('ignore'); var parsers = require('gitbook-parsers'); 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('./backbone/page'); var pathUtil = require('./utils/path'); function Book(opts) { if (!(this instanceof Book)) return new Book(opts); opts = _.defaults(opts || {}, { fs: null, // Root path for the book root: '', // Extend book configuration config: {}, // Log function log: function(msg) { process.stdout.write(msg); }, // Log level logLevel: 'info' }); if (!opts.fs) throw new Error('Book requires a fs instance'); // Root path for the book this.root = opts.root; // If multi-lingual, book can have a parent this.parent = opts.parent; // A book is linked to an fs, to access its content this.fs = opts.fs; // Rules to ignore some files this.ignore = Ignore(); this.ignore.addPattern([ // Skip Git stuff '.git/', '.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' ]); // Create a logger for the book this.log = new Logger(opts.log, opts.logLevel); // Create an interface to access the configuration this.config = new Config(this, opts.config); // Interfaces for the book structure this.readme = new Readme(this); this.summary = new Summary(this); this.glossary = new Glossary(this); // Multilinguals book this.langs = new Langs(this); this.books = []; // List of page in the book this.pages = {}; _.bindAll(this); } // Parse and prepare the configuration, fail if invalid Book.prototype.prepareConfig = function() { return this.config.load(); }; // 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))); }; // Parse .gitignore, etc to extract rules Book.prototype.parseIgnoreRules = function() { var that = this; return _.reduce([ '.ignore', '.gitignore', '.bookignore' ], function(prev, filename) { return prev.then(function() { return that.readFile(filename); }) .then(function(content) { that.ignore.addPattern(content.toString().split(/\r?\n/)); }); }, Q()); }; // Parse the whole book Book.prototype.parse = function() { var that = this; return Q() .then(this.prepareConfig) .then(this.parseIgnoreRules) // Parse languages .then(function() { return that.langs.load(); }) .then(function() { if (that.isMultilingual()) { that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages'); return; } return Q() .then(that.readme.load) .then(function() { if (that.readme.exists()) return; throw new Error('No README file (or is ignored)'); }) .then(that.summary.load) .then(function() { if (that.summary.exists()) return; throw new Error('No SUMMARY file (or is ignored)'); }) .then(that.glossary.load); }); }; // Mark a filename as being parsable Book.prototype.addPage = function(filename) { filename = pathUtil.normalize(filename); if (this.pages[filename]) return; this.pages[filename] = new Page(this, filename); }; // Return a page by its filename (or undefined) Book.prototype.getPage = function(filename) { filename = pathUtil.normalize(filename); return this.pages[filename]; }; // Return true, if has a specific page Book.prototype.hasPage = function(filename) { return Boolean(this.getPage(filename)); }; // Test if a file is ignored, return true if it is Book.prototype.isFileIgnored = function(filename) { return this.ignore.filter([filename]).length == 0; }; // Read a file in the book, throw error if ignored Book.prototype.readFile = function(filename) { if (this.isFileIgnored(filename)) return Q.reject(new Error('File "'+filename+'" is ignored')); return this.fs.readAsString(this.resolve(filename)); }; // Find a parsable file using a filename Book.prototype.findParsableFile = function(filename) { var that = this; var ext = path.extname(filename); var basename = path.basename(filename, ext); // Ordered list of extensions to test var exts = parsers.extensions; if (ext) exts = _.uniq([ext].concat(exts)); return _.reduce(exts, function(prev, ext) { return prev.then(function(output) { // Stop if already find a parser if (output) return output; var filepath = basename+ext; return that.fs.findFile(that.root, filepath) .then(function(realFilepath) { if (!realFilepath) return null; return { parser: parsers.get(ext), path: realFilepath }; }); }); }, Q(null)); }; // Return true if book is associated to a language Book.prototype.isLanguageBook = function() { return Boolean(this.parent); }; Book.prototype.isSubBook = Book.prototype.isLanguageBook; // Return true if the book is main instance of a multilingual book Book.prototype.isMultilingual = function() { return this.langs.count() > 0; }; module.exports = Book;