mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Start rewrite
This commit is contained in:
@@ -26,3 +26,6 @@ node_modules
|
||||
|
||||
# vim swapfile
|
||||
*.swp
|
||||
|
||||
lib2
|
||||
test2
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
function Glossary() {
|
||||
if (!(this instanceof Glossary)) return new Glossary();
|
||||
}
|
||||
|
||||
Glossary.prototype.type = 'glossary';
|
||||
|
||||
module.exports = Glossary;
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
Readme: require('./readme'),
|
||||
Summary: require('./summary'),
|
||||
Glossary: require('./glossary'),
|
||||
Langs: require('./langs')
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
function Summary() {
|
||||
if (!(this instanceof Summary)) return new Summary();
|
||||
}
|
||||
|
||||
Summary.prototype.type = 'summary';
|
||||
|
||||
|
||||
module.exports = Summary;
|
||||
@@ -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
|
||||
};
|
||||
+103
-739
@@ -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);
|
||||
if (!opts.fs) throw 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;
|
||||
|
||||
// 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(['.git', '.svn', '.DS_Store']);
|
||||
|
||||
// Summary
|
||||
this.summary = {};
|
||||
this.navigation = [];
|
||||
// Create a logger for the book
|
||||
this.log = new Logger(opts.log, opts.logLevel);
|
||||
|
||||
// Glossary
|
||||
this.glossary = [];
|
||||
// Create an interface to access the configuration
|
||||
this.config = new Config(this, opts.config);
|
||||
|
||||
// Langs
|
||||
this.langs = [];
|
||||
// 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);
|
||||
|
||||
// Sub-books
|
||||
this.books = [];
|
||||
// Map of pages in the bok
|
||||
this.pages = {};
|
||||
|
||||
// Files in the book
|
||||
this.files = [];
|
||||
|
||||
// List of plugins
|
||||
this.plugins = new PluginsList(this);
|
||||
|
||||
// Structure files
|
||||
this.summaryFile = null;
|
||||
this.glossaryFile = null;
|
||||
this.readmeFile = null;
|
||||
this.langsFile = null;
|
||||
|
||||
// Bind methods
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
// Parse and prepare the configuration, fail if invalid
|
||||
Book.prototype.prepareConfig = function() {
|
||||
return this.config.load();
|
||||
};
|
||||
|
||||
// Return string representation
|
||||
Book.prototype.toString = function() {
|
||||
return '[Book '+this.root+']';
|
||||
// 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)));
|
||||
};
|
||||
|
||||
// Initialize and parse the book: config, summary, glossary
|
||||
// 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;
|
||||
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);
|
||||
};
|
||||
|
||||
// Generate the output
|
||||
Book.prototype.generate = function(generator) {
|
||||
var that = this;
|
||||
that.options.generator = generator || that.options.generator;
|
||||
|
||||
that.log.info.ln('start generation with', that.options.generator, 'generator');
|
||||
return Q()
|
||||
.then(this.prepareConfig)
|
||||
.then(this.parseIgnoreRules)
|
||||
|
||||
// Clean output folder
|
||||
// Parse languages
|
||||
.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 that.langs.load();
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
if (that.isMultilingual()) 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() {
|
||||
that.log.info.ok();
|
||||
});
|
||||
})
|
||||
if (that.summary.exists()) return;
|
||||
|
||||
// Create generator
|
||||
.then(function() {
|
||||
var Generator = generators[generator];
|
||||
if (!Generator) throw 'Generator \''+that.options.generator+'\' doesn\'t exist';
|
||||
generator = new Generator(that);
|
||||
throw new Error('No SUMMARY file (or is ignored)');
|
||||
})
|
||||
|
||||
return generator.prepare();
|
||||
})
|
||||
|
||||
// Transform configuration
|
||||
.then(function() {
|
||||
return that.callHook('config', that.config.dump())
|
||||
.then(function(newConfig) {
|
||||
that.config.replace(newConfig);
|
||||
});
|
||||
})
|
||||
|
||||
// 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');
|
||||
.then(that.glossary.load);
|
||||
});
|
||||
};
|
||||
|
||||
// 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());
|
||||
});
|
||||
// Test if a file is ignored, return true if it is
|
||||
Book.prototype.isFileIgnored = function(filename) {
|
||||
return this.ignore.filter([filename]).length == 0;
|
||||
};
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
// 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));
|
||||
};
|
||||
|
||||
// Parse configuration
|
||||
Book.prototype.parseConfig = function() {
|
||||
var that = this;
|
||||
|
||||
that.log.info('loading book configuration....');
|
||||
return that.config.load()
|
||||
.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
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.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;
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
}, '');
|
||||
});
|
||||
// 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 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 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 this.langs.count() > 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);
|
||||
};
|
||||
|
||||
module.exports= Book;
|
||||
module.exports = Book;
|
||||
|
||||
+110
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+148
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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")
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
+1
-211
@@ -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
|
||||
};
|
||||
|
||||
-83
@@ -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;
|
||||
+123
@@ -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;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
function Page() {
|
||||
if (!(this instanceof Page)) return new Page();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = Page;
|
||||
-241
@@ -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;
|
||||
@@ -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;
|
||||
-466
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
-193
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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"]
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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;
|
||||
+3
-1
@@ -40,7 +40,9 @@
|
||||
"jsonschema": "1.0.2",
|
||||
"json-schema-defaults": "0.1.1",
|
||||
"merge-defaults": "0.2.1",
|
||||
"github-slugid": "1.0.0"
|
||||
"github-slugid": "1.0.0",
|
||||
"destroy": "1.0.4",
|
||||
"ignore": "2.2.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "1.5.0",
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var should = require('should');
|
||||
var cheerio = require('cheerio');
|
||||
|
||||
require('should-promised');
|
||||
|
||||
should.Assertion.add('file', function(file, description) {
|
||||
this.params = { actual: this.obj.toString(), operator: 'have file ' + file, message: description };
|
||||
|
||||
this.obj.should.have.property('options').which.is.an.Object();
|
||||
this.obj.options.should.have.property('output').which.is.a.String();
|
||||
this.assert(fs.existsSync(path.resolve(this.obj.options.output, file)));
|
||||
});
|
||||
|
||||
should.Assertion.add('jsonfile', function(file, description) {
|
||||
this.params = { actual: this.obj.toString(), operator: 'have valid jsonfile ' + file, message: description };
|
||||
|
||||
this.obj.should.have.property('options').which.is.an.Object();
|
||||
this.obj.options.should.have.property('output').which.is.a.String();
|
||||
this.assert(JSON.parse(fs.readFileSync(path.resolve(this.obj.options.output, file), { encoding: 'utf-8' })));
|
||||
});
|
||||
|
||||
should.Assertion.add('html', function(rules, description) {
|
||||
this.params = { actual: 'HTML string', operator: 'valid html', message: description };
|
||||
var $ = cheerio.load(this.obj);
|
||||
|
||||
_.each(rules, function(validations, query) {
|
||||
validations = _.defaults(validations || {}, {
|
||||
// Select a specific element in the list of matched elements
|
||||
index: null,
|
||||
|
||||
// Check that there is the correct count of elements
|
||||
count: 1,
|
||||
|
||||
// Check attribute values
|
||||
attributes: {},
|
||||
|
||||
// Trim inner text
|
||||
trim: false,
|
||||
|
||||
// Check inner text
|
||||
text: undefined
|
||||
});
|
||||
|
||||
var $el = $(query);
|
||||
|
||||
// Select correct element
|
||||
if (_.isNumber(validations.index)) $el = $($el.get(validations.index));
|
||||
|
||||
// Test number of elements
|
||||
$el.length.should.be.equal(validations.count);
|
||||
|
||||
// Test text
|
||||
if (validations.text !== undefined) {
|
||||
var text = $el.text();
|
||||
if (validations.trim) text = text.trim();
|
||||
text.should.be.equal(validations.text);
|
||||
}
|
||||
|
||||
// Test attributes
|
||||
_.each(validations.attributes, function(value, name) {
|
||||
var attr = $el.attr(name);
|
||||
should(attr).be.ok();
|
||||
attr.should.be.equal(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
# Readme
|
||||
|
||||
Default description for the book.
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1 +0,0 @@
|
||||
# Readme
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
"title": "js-config"
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
# Readme
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "json-config"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Readme
|
||||
|
||||
<p id="test-plugin-block-shortcuts-1">$$test_block1$$</p>
|
||||
<p id="test-plugin-block-shortcuts-2">{% include "./block.md" %}</p>
|
||||
|
||||
### Relative
|
||||
|
||||
<p id="t1">{% include "./hello.md" %}</p>
|
||||
<p id="t2">{% include "/hello.md" %}</p>
|
||||
|
||||
### Git
|
||||
|
||||
<p id="t3">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}</p>
|
||||
<p id="t4">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}</p>
|
||||
<p id="t5">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}</p>
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1 +0,0 @@
|
||||
$$test_block2$$
|
||||
@@ -1 +0,0 @@
|
||||
Hello World
|
||||
@@ -1,13 +0,0 @@
|
||||
# Glossary
|
||||
|
||||
## test
|
||||
|
||||
Just a simple and easy to understand test.
|
||||
|
||||
## hakunamatata
|
||||
|
||||
This word is not present in the content.
|
||||
|
||||
## test long
|
||||
|
||||
This is a test with a longer text that the first entry to test order.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Readme
|
||||
|
||||
The word test should be replaced by a glossary link.
|
||||
|
||||
```
|
||||
But the word test should not be replaced in code blocks
|
||||
```
|
||||
|
||||
The two words test long should be replaced by the correct glossary links.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Page](folder/PAGE.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Page
|
||||
|
||||
Just a page in a sub-directory to test relative link to glossary.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Hello World
|
||||
|
||||
## Hello {#hello-custom}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Readme
|
||||
|
||||
Block without language
|
||||
|
||||
```
|
||||
test 1
|
||||
```
|
||||
|
||||
Block with a language
|
||||
|
||||
```lang
|
||||
test 2
|
||||
```
|
||||
|
||||
Inline code: `test 3`
|
||||
Inline code with html: `<test>`
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1,18 +0,0 @@
|
||||
# Tests for Images
|
||||
|
||||
# SVG relative image
|
||||
|
||||
|
||||
### Convert SVG to PNG
|
||||

|
||||
|
||||
### Should only convert it once
|
||||

|
||||
|
||||
### Download remote images
|
||||

|
||||
|
||||
### Remote images with same filename
|
||||

|
||||

|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Page](./folder/PAGE.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
### Images from other folder
|
||||

|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
|
||||
<svg version="1.1" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg-root" width="100%" height="100%" viewBox="0 0 480 360">
|
||||
<title id="test-title">basic SVG tiny doc</title>
|
||||
<g id="test-body-content">
|
||||
<text font-family="Arial" font-size="14" text-anchor="middle" x="225" y="25">hello world</text>
|
||||
</g>
|
||||
<text id="revision" x="10" y="340" font-size="40" stroke="none" fill="black">Revision: 1.1</text>
|
||||
<rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 730 B |
@@ -1,3 +0,0 @@
|
||||
*
|
||||
!SUMMARY.md
|
||||
!.gitignore
|
||||
@@ -1,9 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Hello](hello.md)
|
||||
* [Hello 2](hello2.md)
|
||||
* Hello 3
|
||||
* [Hello 4](hello3/hello4.md)
|
||||
* Hello 5
|
||||
* [Hello 6](hello3/hello5/hello6.md)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Language
|
||||
|
||||
* [English](en)
|
||||
* [French](fr)
|
||||
@@ -1 +0,0 @@
|
||||
# Readme
|
||||
@@ -1 +0,0 @@
|
||||
Hello
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1 +0,0 @@
|
||||
Bonjour
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1 +0,0 @@
|
||||
# Readme
|
||||
@@ -1,4 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Folder1](folder1/README.md)
|
||||
* [Folder2](folder2/README.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Folder 1
|
||||
|
||||
[Link to folder2](../folder2/README.md)
|
||||
@@ -1 +0,0 @@
|
||||
# Folder 2
|
||||
@@ -1 +0,0 @@
|
||||
== Readme for the bookk
|
||||
@@ -1 +0,0 @@
|
||||
# Readme for GitHub
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"structure": {
|
||||
"readme": "README.adoc"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Glossary
|
||||
|
||||
### Hello
|
||||
|
||||
Hello world
|
||||
@@ -1,3 +0,0 @@
|
||||
# Readme
|
||||
|
||||
Default description for the book.
|
||||
@@ -1 +0,0 @@
|
||||
# Summary
|
||||
@@ -1,3 +0,0 @@
|
||||
body {
|
||||
color: red;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Page 1
|
||||
@@ -1 +0,0 @@
|
||||
# Readme
|
||||
@@ -1,5 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Page 1](./PAGE1.md)
|
||||
* [Page 2](./folder/PAGE2.md)
|
||||
* [Don't exists](./NOTFOUND.md)
|
||||
@@ -1 +0,0 @@
|
||||
# Page 2
|
||||
@@ -1,65 +0,0 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
var Plugin = require('../lib/plugin');
|
||||
var PLUGINS_ROOT = path.resolve(__dirname, 'plugins');
|
||||
|
||||
describe('Code Highlighting', function () {
|
||||
var book, PAGE;
|
||||
|
||||
before(function() {
|
||||
return books.generate('highlight', 'website', {
|
||||
prepare: function(_book) {
|
||||
book = _book;
|
||||
|
||||
var plugin = new Plugin(book, 'replace_highlight');
|
||||
plugin.load('./replace_highlight', PLUGINS_ROOT);
|
||||
|
||||
book.plugins.load(plugin);
|
||||
}
|
||||
})
|
||||
.then(function() {
|
||||
PAGE = fs.readFileSync(
|
||||
path.join(book.options.output, 'index.html'),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly replace highlighting', function() {
|
||||
PAGE.should.be.html({
|
||||
'code': {
|
||||
index: 0,
|
||||
text: 'code_test 1\n_code'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly replace highlighting with language', function() {
|
||||
PAGE.should.be.html({
|
||||
'code': {
|
||||
index: 1,
|
||||
text: 'lang_test 2\n_lang'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly replace highlighting for inline code', function() {
|
||||
PAGE.should.be.html({
|
||||
'code': {
|
||||
index: 2,
|
||||
text: 'code_test 3_code'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly replace highlighting for inline code with html tags', function() {
|
||||
PAGE.should.be.html({
|
||||
'code': {
|
||||
index: 3,
|
||||
text: 'code_<test>_code'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
var mock = require('./mock');
|
||||
|
||||
describe('Config', function() {
|
||||
|
||||
describe('config.load()', function() {
|
||||
it('should not fail if no configuration file', function() {
|
||||
return mock.setupDefaultBook()
|
||||
.then(function(book) {
|
||||
return book.prepareConfig();
|
||||
});
|
||||
});
|
||||
|
||||
it('should load from a JSON file', function() {
|
||||
return mock.setupDefaultBook({
|
||||
'book.json': { title: 'Hello World' }
|
||||
})
|
||||
.then(function(book) {
|
||||
return book.prepareConfig()
|
||||
.then(function() {
|
||||
book.config.get('title', '').should.equal('Hello World');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should load from a JS file', function() {
|
||||
return mock.setupDefaultBook({
|
||||
'book.js': 'module.exports = { title: "Hello World" };'
|
||||
})
|
||||
.then(function(book) {
|
||||
return book.prepareConfig()
|
||||
.then(function() {
|
||||
book.config.get('title', '').should.equal('Hello World');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
describe('Configuration', function () {
|
||||
it('should extract default title from README', function() {
|
||||
return books.parse('basic')
|
||||
.then(function(book) {
|
||||
book.options.title.should.be.equal('Readme');
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract default description from README', function() {
|
||||
return books.parse('basic')
|
||||
.then(function(book) {
|
||||
book.options.description.should.be.equal('Default description for the book.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly load from json (book.json)', function() {
|
||||
return books.parse('config-json')
|
||||
.then(function(book) {
|
||||
book.options.title.should.be.equal('json-config');
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly load from JavaScript (book.js)', function() {
|
||||
return books.parse('config-js')
|
||||
.then(function(book) {
|
||||
book.options.title.should.be.equal('js-config');
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide configuration on book.config.get', function() {
|
||||
return books.parse('basic')
|
||||
.then(function(book) {
|
||||
book.config.get('description').should.be.equal('Default description for the book.');
|
||||
book.getConfig('description').should.be.equal('Default description for the book.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('ConRefs', function () {
|
||||
var book, readme;
|
||||
|
||||
before(function() {
|
||||
return books.generate('conrefs', 'website')
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
|
||||
readme = fs.readFileSync(
|
||||
path.join(book.options.output, 'index.html'),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle local references', function() {
|
||||
readme.should.be.html({
|
||||
'.page-inner p#t1': {
|
||||
count: 1,
|
||||
text: 'Hello World',
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle local references with absolute paths', function() {
|
||||
readme.should.be.html({
|
||||
'.page-inner p#t2': {
|
||||
count: 1,
|
||||
text: 'Hello World',
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly include file from git reference', function() {
|
||||
readme.should.be.html({
|
||||
'.page-inner p#t3': {
|
||||
count: 1,
|
||||
text: 'Hello from git',
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly handle deep include in git reference', function() {
|
||||
readme.should.be.html({
|
||||
'.page-inner p#t4': {
|
||||
count: 1,
|
||||
text: 'First Hello. Hello from git',
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly handle absolute include in git reference', function() {
|
||||
readme.should.be.html({
|
||||
'.page-inner p#t5': {
|
||||
count: 1,
|
||||
text: 'First Hello. Hello from git',
|
||||
trim: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('eBook generator', function () {
|
||||
describe('Basic Book', function() {
|
||||
var book;
|
||||
|
||||
before(function() {
|
||||
return books.generate('basic', 'ebook')
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly output a SUMMARY.html', function() {
|
||||
book.should.have.file('SUMMARY.html');
|
||||
});
|
||||
|
||||
it('should correctly copy assets', function() {
|
||||
book.should.have.file('gitbook');
|
||||
book.should.have.file('gitbook/ebook.css');
|
||||
});
|
||||
|
||||
it('should not copy website assets', function() {
|
||||
book.should.not.have.file('gitbook/style.css');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom styles', function() {
|
||||
var book;
|
||||
|
||||
before(function() {
|
||||
return books.generate('style-print', 'ebook')
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly copy print.css', function() {
|
||||
book.should.have.file('styles');
|
||||
book.should.have.file('styles/print.css');
|
||||
});
|
||||
|
||||
it('should remove default print.css', function() {
|
||||
var PAGE = fs.readFileSync(
|
||||
path.join(book.options.output, 'index.html'),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
// There are 2 styles (one from plugin-highlight and the new style)
|
||||
PAGE.should.be.html({
|
||||
'link': {
|
||||
count: 2
|
||||
}
|
||||
});
|
||||
|
||||
PAGE.should.be.html({
|
||||
'link[href=\'./styles/print.css\']': {
|
||||
count: 1
|
||||
}
|
||||
});
|
||||
|
||||
PAGE.should.be.html({
|
||||
'link[href="gitbook/plugins/gitbook-plugin-highlight/ebook.css"]': {
|
||||
count: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
describe('Formatting', function () {
|
||||
it('should provide formatting with book.formatString', function() {
|
||||
return books.parse('basic')
|
||||
.then(function(book) {
|
||||
return book.formatString('markdown', 'this is a **test**');
|
||||
})
|
||||
.then(function(content) {
|
||||
content.should.equal('<p>this is a <strong>test</strong></p>\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
var should = require("should");
|
||||
var git = require("../lib/utils/git");
|
||||
|
||||
describe("GIT parser and getter", function () {
|
||||
it("should correctly parse an https url", function() {
|
||||
var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md");
|
||||
|
||||
should.exist(parts);
|
||||
parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git");
|
||||
parts.ref.should.be.equal("master");
|
||||
parts.filepath.should.be.equal("test.md");
|
||||
});
|
||||
|
||||
it("should correctly parse an https url with a reference", function() {
|
||||
var parts = git.parseUrl("git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#0.1.2");
|
||||
|
||||
should.exist(parts);
|
||||
parts.host.should.be.equal("https://gist.github.com/69ea4542e4c8967d2fa7.git");
|
||||
parts.ref.should.be.equal("0.1.2");
|
||||
parts.filepath.should.be.equal("test.md");
|
||||
});
|
||||
|
||||
it("should correctly parse an ssh url", function() {
|
||||
var parts = git.parseUrl("git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1");
|
||||
|
||||
should.exist(parts);
|
||||
parts.host.should.be.equal("git@github.com:GitbookIO/gitbook.git");
|
||||
parts.ref.should.be.equal("e1594cde2c32e4ff48f6c4eff3d3d461743d74e1");
|
||||
parts.filepath.should.be.equal("directory/README.md");
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
describe("Glossary", function () {
|
||||
describe("Parsing", function() {
|
||||
var book;
|
||||
|
||||
before(function() {
|
||||
return books.parse("glossary")
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly list items", function() {
|
||||
book.should.have.property("glossary");
|
||||
book.glossary.should.have.lengthOf(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Generation", function() {
|
||||
var book;
|
||||
|
||||
before(function() {
|
||||
return books.generate("glossary", "website")
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate a GLOSSARY.html", function() {
|
||||
book.should.have.file("GLOSSARY.html");
|
||||
});
|
||||
|
||||
describe("Page Integration", function() {
|
||||
var readme, page;
|
||||
|
||||
before(function() {
|
||||
readme = fs.readFileSync(
|
||||
path.join(book.options.output, "index.html"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
page = fs.readFileSync(
|
||||
path.join(book.options.output, "folder/PAGE.html"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
});
|
||||
|
||||
it("should correctly replaced terms by links", function() {
|
||||
readme.should.be.html({
|
||||
".page-inner a[href=\"GLOSSARY.html#test\"]": {
|
||||
count: 1,
|
||||
text: "test",
|
||||
attributes: {
|
||||
title: "Just a simple and easy to understand test."
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly replaced terms by links (relative)", function() {
|
||||
page.should.be.html({
|
||||
".page-inner a[href=\"../GLOSSARY.html#test\"]": {
|
||||
count: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not replace terms in codeblocks", function() {
|
||||
readme.should.be.html({
|
||||
".page-inner code a": {
|
||||
count: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly select the longest term", function() {
|
||||
readme.should.be.html({
|
||||
".page-inner a[href=\"GLOSSARY.html#test_long\"]": {
|
||||
count: 1,
|
||||
text: "test long"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
describe('Headings', function () {
|
||||
var book, PAGE;
|
||||
|
||||
before(function() {
|
||||
return books.generate('headings', 'website')
|
||||
.then(function(_book) {
|
||||
book = _book;
|
||||
|
||||
PAGE = fs.readFileSync(
|
||||
path.join(book.options.output, 'index.html'),
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IDs', function() {
|
||||
it('should correctly generate an ID', function() {
|
||||
PAGE.should.be.html({
|
||||
'h1#hello-world': {
|
||||
count: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly accept custom ID', function() {
|
||||
PAGE.should.be.html({
|
||||
'h2#hello-custom': {
|
||||
count: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user