summaryrefslogtreecommitdiffstats
path: root/lib/book.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/book.js')
-rw-r--r--lib/book.js929
1 files changed, 232 insertions, 697 deletions
diff --git a/lib/book.js b/lib/book.js
index 9ba27f3..400296e 100644
--- a/lib/book.js
+++ b/lib/book.js
@@ -1,25 +1,34 @@
-var Q = require('q');
var _ = require('lodash');
var path = require('path');
-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 Ignore = require('ignore');
+
+var Config = require('./config');
+var Readme = require('./backbone/readme');
+var Glossary = require('./backbone/glossary');
+var Summary = require('./backbone/summary');
+var Langs = require('./backbone/langs');
+var Page = require('./page');
var pathUtil = require('./utils/path');
-var links = require('./utils/links');
-var i18n = require('./utils/i18n');
-var logger = require('./utils/logger');
+var error = require('./utils/error');
+var Promise = require('./utils/promise');
+var Logger = require('./utils/logger');
+var parsers = require('./parsers');
+
+
+/*
+The Book class is an interface for parsing books content.
+It does not require to run on Node.js, isnce it only depends on the fs implementation
+*/
+
+function Book(opts) {
+ if (!(this instanceof Book)) return new Book(opts);
-var Configuration = require('./configuration');
-var TemplateEngine = require('./template');
-var PluginsList = require('./pluginslist');
+ this.opts = _.defaults(opts || {}, {
+ fs: null,
-var generators = require('./generators');
+ // Root path for the book
+ root: '',
-var Book = function(root, context, parent) {
- this.context = _.defaults(context || {}, {
// Extend book configuration
config: {},
@@ -32,572 +41,234 @@ var Book = function(root, context, parent) {
logLevel: 'info'
});
- // Log
- this.log = logger(this.context.log, this.context.logLevel);
+ if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));
- // Root folder of the book
- this.root = path.resolve(root);
+ // Root path for the book
+ this.root = opts.root;
- // Parent book
- this.parent = parent;
+ // If multi-lingual, book can have a parent
+ this.parent = opts.parent;
+ if (this.parent) {
+ this.language = path.relative(this.parent.root, this.root);
+ }
- // Configuration
- this.config = new Configuration(this, this.context.config);
- Object.defineProperty(this, 'options', {
- get: function () {
- return this.config.options;
- }
- });
+ // A book is linked to an fs, to access its content
+ this.fs = opts.fs;
- // Template
- this.template = new TemplateEngine(this);
+ // Rules to ignore some files
+ this.ignore = Ignore();
+ this.ignore.addPattern([
+ // Skip Git stuff
+ '.git/',
- // Summary
- this.summary = {};
- this.navigation = [];
+ // Skip OS X meta data
+ '.DS_Store',
- // Glossary
- this.glossary = [];
+ // Skip stuff installed by plugins
+ 'node_modules',
- // Langs
- this.langs = [];
+ // Skip book outputs
+ '_book',
+ '*.pdf',
+ '*.epub',
+ '*.mobi'
+ ]);
- // Sub-books
- this.books = [];
+ // Create a logger for the book
+ this.log = new Logger(opts.log, opts.logLevel);
- // Files in the book
- this.files = [];
+ // Create an interface to access the configuration
+ this.config = new Config(this, opts.config);
- // List of plugins
- this.plugins = new PluginsList(this);
+ // Interfaces for the book structure
+ this.readme = new Readme(this);
+ this.summary = new Summary(this);
+ this.glossary = new Glossary(this);
- // Structure files
- this.summaryFile = null;
- this.glossaryFile = null;
- this.readmeFile = null;
- this.langsFile = null;
+ // Multilinguals book
+ this.langs = new Langs(this);
+ this.books = [];
- // Bind methods
- _.bindAll(this);
-};
+ // List of page in the book
+ this.pages = {};
-// Return string representation
-Book.prototype.toString = function() {
- return '[Book '+this.root+']';
-};
+ _.bindAll(this);
+}
-// Initialize and parse the book: config, summary, glossary
-Book.prototype.parse = function() {
- var that = this;
- var multilingual = false;
+// Return templating context for the book
+Book.prototype.getContext = function() {
+ var variables = this.config.get('variables', {});
- return this.parseConfig()
+ return {
+ book: _.extend({
+ language: this.language
+ }, variables)
+ };
+};
- .then(function() {
- return that.parsePlugins();
- })
+// Parse and prepare the configuration, fail if invalid
+Book.prototype.prepareConfig = function() {
+ return this.config.load();
+};
- .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
- );
- });
+// Resolve a path in the book source
+// Enforce that the output path is in the scope
+Book.prototype.resolve = function() {
+ var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
+ if (!this.isFileInScope(filename)) {
+ throw error.FileOutOfScopeError({
+ filename: filename,
+ root: this.root
});
- })
+ }
- .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();
- })
+ return filename;
+};
- .then(function() {
- // Init sub-books
- return _.reduce(that.books, function(prev, book) {
- return prev.then(function() {
- return book.parse();
- });
- }, Q());
- })
+// Return false if a file is outside the book' scope
+Book.prototype.isFileInScope = function(filename) {
+ filename = path.resolve(this.root, filename);
- .thenResolve(this);
+ // Is the file in the scope of the parent?
+ if (this.parent && this.parent.isFileInScope(filename)) return true;
+
+ // Is file in the root folder?
+ return pathUtil.isInRoot(this.root, filename);
};
-// 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+')');
- })
- .then(function() {
- that.log.info.ok();
+ return Promise.serie([
+ '.ignore',
+ '.gitignore',
+ '.bookignore'
+ ], function(filename) {
+ return that.readFile(filename)
+ .then(function(content) {
+ that.ignore.addPattern(content.toString().split(/\r?\n/));
+ }, function() {
+ return Promise();
});
- })
+ });
+};
- // Create generator
- .then(function() {
- var Generator = generators[generator];
- if (!Generator) throw 'Generator \''+that.options.generator+'\' doesn\'t exist';
- generator = new Generator(that);
+// Parse the whole book
+Book.prototype.parse = function() {
+ var that = this;
- return generator.prepare();
- })
+ return Promise()
+ .then(this.prepareConfig)
+ .then(this.parseIgnoreRules)
- // Transform configuration
+ // Parse languages
.then(function() {
- return that.callHook('config', that.config.dump())
- .then(function(newConfig) {
- that.config.replace(newConfig);
- });
+ return that.langs.load();
})
- // 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()
+ if (that.isLanguageBook()) {
+ throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
+ }
+
+ that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
+
+ // Create a new book for each language and parse it
+ return Promise.serie(that.langs.list(), function(lang) {
+ that.log.debug.ln('Preparing book for language', lang.id);
+ var langBook = new Book(_.extend({}, that.opts, {
+ parent: that,
+ config: that.config.dump(),
+ root: that.resolve(lang.id)
+ }));
- // 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());
- })
+ that.books.push(langBook);
- // 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());
+ return langBook.parse();
});
}
- })
- // 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');
- });
-};
+ return Promise()
-// Generate the output for a multilingual book
-Book.prototype.generateMultiLingual = 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());
- });
-};
+ // Parse the readme
+ .then(that.readme.load)
+ .then(function() {
+ if (!that.readme.exists()) {
+ throw new error.FileNotFoundError({ filename: 'README' });
+ }
-// Extract files from ebook generated
-Book.prototype.generateFile = function(output, options) {
- var book = this;
+ // Default configuration to infos extracted from readme
+ if (!that.config.get('title')) that.config.set('title', that.readme.title);
+ if (!that.config.get('description')) that.config.set('description', that.readme.description);
+ })
- options = _.defaults(options || {}, {
- ebookFormat: path.extname(output).slice(1)
- });
- output = output || path.resolve(book.root, 'book.'+options.ebookFormat);
+ // Parse the summary
+ .then(that.summary.load)
+ .then(function() {
+ if (!that.summary.exists()) {
+ that.log.warn.ln('no summary file in this book');
+ }
+
+ // Index summary's articles
+ that.summary.walk(function(article) {
+ if (!article.hasLocation() || article.isExternal()) return;
+ that.addPage(article.path);
+ });
+ })
- return fs.tmp.dir()
- .then(function(tmpDir) {
- book.setOutput(tmpDir);
+ // Parse the glossary
+ .then(that.glossary.load)
- return book.generate(options.ebookFormat)
+ // Add the glossary as a page
.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);
- });
+ if (!that.glossary.exists()) return;
+ that.addPage(that.glossary.path);
});
});
};
-// Parse configuration
-Book.prototype.parseConfig = function() {
- var that = this;
+// Mark a filename as being parsable
+Book.prototype.addPage = function(filename) {
+ if (this.hasPage(filename)) return this.getPage(filename);
- that.log.info('loading book configuration....');
- return that.config.load()
- .then(function() {
- that.log.info.ok();
- });
+ filename = pathUtil.normalize(filename);
+ this.pages[filename] = new Page(this, filename);
+ return this.pages[filename];
};
-// 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');
- });
+// Return a page by its filename (or undefined)
+Book.prototype.getPage = function(filename) {
+ filename = pathUtil.normalize(filename);
+ return this.pages[filename];
};
-// 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
- });
- });
- });
- })
- .then(function(readme) {
- that.options.title = that.options.title || readme.title;
- that.options.description = that.options.description || readme.description;
- });
+// Return true, if has a specific page
+Book.prototype.hasPage = function(filename) {
+ return Boolean(this.getPage(filename));
};
-
-// 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;
- });
+// Test if a file is ignored, return true if it is
+Book.prototype.isFileIgnored = function(filename) {
+ return this.ignore.filter([filename]).length == 0;
};
-// Parse summary to extract list of chapters
-Book.prototype.parseSummary = function() {
- var that = this;
-
- var filename = that.config.getStructure('summary', true);
- that.log.debug.ln('start parsing summary:', filename);
-
- return that.findFile(filename)
- .then(function(summary) {
- if (!summary) throw 'No SUMMARY file';
-
- // 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);
-
- 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);
- });
-};
-
-// 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;
- });
+// Read a file in the book, throw error if ignored
+Book.prototype.readFile = function(filename) {
+ if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
+ return this.fs.readAsString(this.resolve(filename));
};
-// 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;
- });
+// Get stat infos about a file
+Book.prototype.statFile = function(filename) {
+ if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
+ return this.fs.stat(this.resolve(filename));
};
-// Find 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 +285,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;
@@ -624,200 +295,64 @@ Book.prototype.findFile = function(filename) {
};
});
});
- }, Q(null));
+ }, Promise(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)
- );
+// Return true if book is associated to a language
+Book.prototype.isLanguageBook = function() {
+ return Boolean(this.parent);
};
+Book.prototype.isSubBook = Book.prototype.isLanguageBook;
-// Check if a file path is inside the book
-Book.prototype.fileIsInBook = function(filename) {
- return pathUtil.isInRoot(this.root, filename);
+// Return true if the book is main instance of a multilingual book
+Book.prototype.isMultilingual = function() {
+ return this.langs.count() > 0;
};
-// Read a file
-Book.prototype.readFile = function(filename) {
- return fs.readFile(
- this.resolve(filename),
- { encoding: 'utf8' }
+// Return true if file is in the scope of this book
+Book.prototype.isInBook = function(filename) {
+ return pathUtil.isInRoot(
+ this.root,
+ filename
);
};
-// 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() {
+// Return true if file is in the scope of a child book
+Book.prototype.isInLanguageBook = function(filename) {
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 _.some(this.langs.list(), function(lang) {
+ return pathUtil.isInRoot(
+ that.resolve(lang.id),
+ that.resolve(filename)
+ );
});
};
-// Return true if the book is 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));
+// Locate a book in a folder
+// - Read the ".gitbook" is exists
+// - Try the folder itself
+// - Try a "docs" folder
+Book.locate = function(fs, root) {
+ return fs.readAsString(path.join(root, '.gitbook'))
+ .then(function(content) {
+ return path.join(root, content);
+ }, function() {
+ // .gitbook doesn't exists, fall back to the root folder
+ return Promise(root);
});
};
-// 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;
+// Locate and setup a book
+Book.setup = function(fs, root, opts) {
+ return Book.locate(fs, root)
+ .then(function(_root) {
+ return new Book(_.extend(opts || {}, {
+ root: _root,
+ fs: fs
+ }));
+ });
};
-// Call a hook in plugins
-Book.prototype.callHook = function(name, data) {
- return this.plugins.hook(name, data);
-};
-module.exports= Book;
+module.exports = Book;