summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorSamy Pessé <samypesse@gmail.com>2015-03-09 10:43:12 +0100
committerSamy Pessé <samypesse@gmail.com>2015-03-09 10:43:12 +0100
commit34fc2831e0cf0fed01c71cec28d93472d87f455b (patch)
treea803cc907c20491ba02863b5d3dd5aedf6bfed10 /lib
parente1594cde2c32e4ff48f6c4eff3d3d461743d74e1 (diff)
parent1bf68a5aa0703b5a1815cfe4ebb731b5fb6ed9d2 (diff)
downloadgitbook-34fc2831e0cf0fed01c71cec28d93472d87f455b.zip
gitbook-34fc2831e0cf0fed01c71cec28d93472d87f455b.tar.gz
gitbook-34fc2831e0cf0fed01c71cec28d93472d87f455b.tar.bz2
Merge branch 'version/2.0'
Diffstat (limited to 'lib')
-rw-r--r--lib/book.js790
-rw-r--r--lib/configuration.js242
-rw-r--r--lib/generate/config.js137
-rw-r--r--lib/generate/fs.js92
-rw-r--r--lib/generate/generator.js87
-rw-r--r--lib/generate/index.js374
-rw-r--r--lib/generate/init.js69
-rw-r--r--lib/generate/json/index.js77
-rw-r--r--lib/generate/page/index.js84
-rw-r--r--lib/generate/plugin.js325
-rw-r--r--lib/generate/site/glossary_indexer.js101
-rw-r--r--lib/generate/site/index.js314
-rw-r--r--lib/generate/site/search_indexer.js71
-rw-r--r--lib/generate/template.js52
-rw-r--r--lib/generator.js78
-rw-r--r--lib/generators/ebook.js (renamed from lib/generate/ebook/index.js)69
-rw-r--r--lib/generators/index.js11
-rw-r--r--lib/generators/json.js76
-rw-r--r--lib/generators/website.js279
-rw-r--r--lib/index.js211
-rw-r--r--lib/parse/glossary.js48
-rw-r--r--lib/parse/include.js12
-rw-r--r--lib/parse/includer.js15
-rw-r--r--lib/parse/index.js11
-rw-r--r--lib/parse/is_exercise.js17
-rw-r--r--lib/parse/is_quiz.js87
-rw-r--r--lib/parse/langs.js25
-rw-r--r--lib/parse/lex.js79
-rw-r--r--lib/parse/page.js160
-rw-r--r--lib/parse/readme.js45
-rw-r--r--lib/parse/renderer.js141
-rw-r--r--lib/parse/summary.js167
-rw-r--r--lib/plugin.js183
-rw-r--r--lib/pluginslist.js225
-rw-r--r--lib/template.js394
-rw-r--r--lib/utils/batch.js52
-rw-r--r--lib/utils/fs.js178
-rw-r--r--lib/utils/git.js112
-rw-r--r--lib/utils/i18n.js72
-rw-r--r--lib/utils/images.js39
-rw-r--r--lib/utils/index.js4
-rw-r--r--lib/utils/lang.js19
-rw-r--r--lib/utils/links.js40
-rw-r--r--lib/utils/logger.js102
-rw-r--r--lib/utils/navigation.js (renamed from lib/parse/navigation.js)30
-rw-r--r--lib/utils/page.js343
-rw-r--r--lib/utils/progress.js (renamed from lib/parse/progress.js)2
-rw-r--r--lib/utils/server.js96
-rw-r--r--lib/utils/string.js8
-rw-r--r--lib/utils/watch.js38
50 files changed, 3637 insertions, 2646 deletions
diff --git a/lib/book.js b/lib/book.js
new file mode 100644
index 0000000..aee425c
--- /dev/null
+++ b/lib/book.js
@@ -0,0 +1,790 @@
+var Q = require("q");
+var _ = require("lodash");
+var path = require("path");
+var util = require("util");
+var lunr = require('lunr');
+var parsers = require("gitbook-parsers");
+var color = require('bash-color');
+
+var fs = require("./utils/fs");
+var parseNavigation = require("./utils/navigation");
+var parseProgress = require("./utils/progress");
+var pageUtil = require("./utils/page");
+var batch = require("./utils/batch");
+var links = require("./utils/links");
+var logger = require("./utils/logger");
+
+var Configuration = require("./configuration");
+var TemplateEngine = require("./template");
+var Plugin = require("./plugin");
+var PluginsList = require("./pluginslist");
+
+var generators = require("./generators");
+
+var Book = function(root, context, parent) {
+ var that = this;
+
+ this.context = _.defaults(context || {}, {
+ // Extend book configuration
+ config: {},
+
+ // Log function
+ log: function(msg) {
+ process.stdout.write(msg);
+ },
+
+ // Log level
+ logLevel: "info"
+ });
+
+ // Log
+ this.log = logger(this.context.log, this.context.logLevel);
+
+ // Root folder of the book
+ this.root = path.resolve(root);
+
+ // Parent book
+ this.parent = parent;
+
+ // Configuration
+ this.config = new Configuration(this, this.context.config);
+ Object.defineProperty(this, "options", {
+ get: function () {
+ return this.config.options;
+ }
+ });
+
+ // Template
+ this.template = new TemplateEngine(this);
+
+ // Summary
+ this.summary = {};
+ this.navigation = [];
+
+ // Glossary
+ this.glossary = [];
+
+ // Langs
+ this.langs = [];
+
+ // Sub-books
+ this.books = [];
+
+ // 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;
+
+ // Search Index
+ this.searchIndex = lunr(function () {
+ this.ref('url');
+
+ this.field('title', { boost: 10 });
+ this.field('body');
+ });
+
+ // Bind methods
+ _.bindAll(this);
+};
+
+// Initialize and parse the book: config, summary, glossary
+Book.prototype.parse = function() {
+ var that = this;
+ var multilingual = false;
+
+ return this.parseConfig()
+
+ .then(function() {
+ return that.parsePlugins();
+ })
+
+ .then(function() {
+ return that.parseLangs()
+ .then(function() {
+ multilingual = that.langs.length > 0;
+ if (multilingual) that.log.info.ln("Parsing multilingual book, with", that.langs.length, "lanuages");
+
+ // 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()
+
+ // Clean output folder
+ .then(function() {
+ that.log.info("clean", that.options.generator, "generator");
+ return fs.clean(that.options.output)
+ .progress(function(p) {
+ that.log.debug.ln("remove", p.file, "("+p.i+"/"+p.count+")");
+ })
+ .then(function() {
+ that.log.info.ok();
+ });
+ })
+
+ // Create generator
+ .then(function() {
+ var Generator = generators[generator];
+ if (!Generator) throw "Generator '"+that.options.generator+"' doesn't exist";
+ generator = new Generator(that);
+
+ return generator.prepare();
+ })
+
+ // 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.info.ln("processing", file, p);
+ return Q(generator.convertFile(file));
+ });
+ }, Q());
+ });
+ }
+ })
+
+ // Finish generation
+ .then(function() {
+ return generator.callHook("finish:before");
+ })
+ .then(function() {
+ return generator.finish();
+ })
+ .then(function() {
+ return generator.callHook("finish");
+ })
+ .then(function() {
+ that.log.info.ln("generation is finished");
+ });
+};
+
+// Generate the output for a multilingual book
+Book.prototype.generateMultiLingual = function(generator) {
+ var that = this;
+
+ return Q()
+ .then(function() {
+ // Generate sub-books
+ return _.reduce(that.books, function(prev, book) {
+ return prev.then(function() {
+ return book.generate(that.options.generator);
+ });
+ }, Q());
+ });
+};
+
+// Extract files from ebook generated
+Book.prototype.generateFile = function(output, options) {
+ var book = this;
+
+ options = _.defaults(options || {}, {
+ ebookFormat: path.extname(output).slice(1)
+ });
+ output = output || path.resolve(book.root, "book."+options.ebookFormat);
+
+ return fs.tmp.dir()
+ .then(function(tmpDir) {
+ book.setOutput(tmpDir);
+
+ return book.generate(options.ebookFormat)
+ .then(function(_options) {
+ 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);
+ });
+ });
+ });
+};
+
+// 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;
+ var failed = [];
+
+ // 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 structure = that.config.getStructure("readme");
+ that.log.debug.ln("start parsing readme:", structure);
+
+ return that.findFile(structure)
+ .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);
+ });
+ })
+ .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 structure = that.config.getStructure("langs");
+ that.log.debug.ln("start parsing languages index:", structure);
+
+ return that.findFile(structure)
+ .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);
+ });
+ })
+ .then(function(langs) {
+ that.langs = langs;
+ });
+};
+
+// Parse summary to extract list of chapters
+Book.prototype.parseSummary = function() {
+ var that = this;
+
+ var structure = that.config.getStructure("summary");
+ that.log.debug.ln("start parsing summary:", structure);
+
+ return that.findFile(structure)
+ .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,
+ files: that.files
+ });
+ });
+ })
+ .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 structure = that.config.getStructure("glossary");
+ that.log.debug.ln("start parsing glossary: ", structure);
+
+ return that.findFile(structure)
+ .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);
+ });
+ })
+ .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;
+ });
+ };
+
+ 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() {
+ that.indexPage(page);
+ return page;
+ });
+};
+
+// Find file that can be parsed with a specific filename
+Book.prototype.findFile = function(filename) {
+ var that = this;
+
+ return _.reduce(parsers.extensions, function(prev, ext) {
+ return prev.then(function(output) {
+ // Stop if already find a parser
+ if (output) return output;
+
+ var filepath = filename+ext;
+
+ return that.fileExists(filepath)
+ .then(function(exists) {
+ if (!exists) return null;
+ return {
+ parser: parsers.get(ext),
+ path: filepath
+ };
+ })
+ });
+ }, Q(null));
+};
+
+// Check if a file exists in the book
+Book.prototype.fileExists = function(filename) {
+ return fs.exists(
+ path.join(this.root, filename)
+ );
+};
+
+// Read a file
+Book.prototype.readFile = function(filename) {
+ return fs.readFile(
+ path.join(this.root, filename),
+ { encoding: "utf8" }
+ );
+};
+
+// Return stat for a file
+Book.prototype.statFile = function(filename) {
+ return fs.stat(path.join(this.root, 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
+Book.prototype.isMultilingual = function(filename) {
+ return this.books.length > 0;
+};
+
+// Return root of the parent
+Book.prototype.parentRoot = function() {
+ if (this.parent) return this.parent.parentRoot();
+ return this.root;
+};
+
+// Return true if it's a sub-book
+Book.prototype.isSubBook = function() {
+ return !!this.parent;
+};
+
+// Test if the file is the entry point
+Book.prototype.isEntryPoint = function(fp) {
+ return fp == this.readmeFile;
+};
+
+// Resolve a path in book
+Book.prototype.resolve = function(p) {
+ return path.resolve(this.root, p);
+};
+
+// Normalize a link to .html and convert README -> index
+Book.prototype.contentLink = 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;
+}
+
+// Index a page into the search index
+Book.prototype.indexPage = function(page) {
+ var nav = this.navigation[page.path];
+ if (!nav) return;
+
+ this.log.debug.ln("index page", page.path);
+ this.searchIndex.add({
+ url: this.contentLink(page.path),
+ title: nav.title,
+ body: pageUtil.extractText(page.sections),
+ });
+};
+
+// 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));
+ });
+};
+
+
+// Init and return a book
+Book.init = function(root) {
+ var book = new Book(root);
+ var extensionToUse = ".md";
+
+ var chaptersPaths = function(chapters) {
+ return _.reduce(chapters || [], function(accu, chapter) {
+ if (!chapter.path) return accu;
+ return accu.concat(
+ _.filter([
+ {
+ title: chapter.title,
+ path: chapter.path
+ }
+ ].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) {
+ var absolutePath = path.resolve(book.root, chapter.path);
+
+ return fs.exists(absolutePath)
+ .then(function(exists) {
+ book.log.info.ln("create", chapter.path);
+ if(exists) return;
+
+ 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= Book;
diff --git a/lib/configuration.js b/lib/configuration.js
new file mode 100644
index 0000000..578f4e2
--- /dev/null
+++ b/lib/configuration.js
@@ -0,0 +1,242 @@
+var _ = require("lodash");
+var Q = require("q");
+var path = require("path");
+var semver = require('semver');
+
+var pkg = require('../package.json');
+var fs = require("./utils/fs");
+var i18n = require("./utils/i18n");
+
+// Default plugins added to each books
+var defaultsPlugins = [];
+
+// Normalize a list of plugins to use
+function normalizePluginsList(plugins) {
+ // 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("@");
+ return {
+ 'name': parts[0],
+ 'version': parts[1] // optional
+ }
+ });
+
+ // 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
+ plugins = _.chain(plugins)
+ .concat(_.map(defaultsPlugins, function(plugin) {
+ return { 'name': plugin }
+ }))
+ .uniq()
+ .value();
+
+ // Build final list
+ plugins = _.filter(plugins, function(plugin) {
+ return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-");
+ });
+
+ return plugins;
+}
+
+// Normalize a list of plugin name to use
+function normalizePluginsNames(plugins) {
+ return _.pluck(normalizePluginsList(plugins), "name");
+};
+
+
+var Configuration = function(book, options) {
+ var that = this;
+
+ this.book = book;
+
+ this.options = _.cloneDeep(Configuration.DEFAULT);
+ this.options = _.merge(this.options, options || {});
+
+ // options.input == book.root
+ Object.defineProperty(this.options, "input", {
+ get: function () {
+ return that.book.root;
+ }
+ });
+};
+
+// Read and parse the configuration
+Configuration.prototype.load = function() {
+ var that = this;
+
+ return Q()
+ .then(function() {
+ try {
+ var _config = require(path.resolve(that.book.root, that.options.configFile));
+ 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 (!semver.satisfies(pkg.version, that.options.gitbook)) {
+ throw "GitBook version doesn't satisfy version required by the book: "+that.options.gitbook;
+ }
+ if (that.options.gitbook == "*") {
+ that.book.log.warn.ln("you should specify a gitbook version to use in your book.json, for example: "+(_.first(pkg.version.split("."))+".x.x"));
+ }
+
+ that.options.output = path.resolve(that.options.output || path.join(that.book.root, "_book"));
+ that.options.plugins = normalizePluginsList(that.options.plugins);
+ that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || "");
+ 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.getByLanguage(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);
+};
+
+// Get structure file
+Configuration.prototype.getStructure = function(name) {
+ return this.options.structure[name].split(".").slice(0, -1).join(".");
+};
+
+// Return normalized language
+Configuration.prototype.normalizeLanguage = function() {
+ return i18n.normalizeLanguage(this.options.language);
+};
+
+
+// Default configuration
+Configuration.DEFAULT = {
+ // Options that can't be extend
+ "configFile": "book",
+ "generator": "site",
+ "extension": null,
+
+ // Book metadats (somes are extracted from the README by default)
+ "title": null,
+ "description": null,
+ "isbn": null,
+ "language": "en",
+ "direction": null,
+
+ // version of gitbook to use
+ "gitbook": "*",
+
+ // Structure
+ "structure": {
+ "langs": "LANGS.md",
+ "readme": "README.md",
+ "glossary": "GLOSSARY.md",
+ "summary": "SUMMARY.md"
+ },
+
+ // CSS Styles
+ "styles": {
+ "website": "styles/website.css",
+ "ebook": "styles/ebook.css",
+ "pdf": "styles/pdf.css",
+ "mobi": "styles/mobi.css",
+ "epub": "styles/epub.css"
+ },
+
+ // Plugins list, can contain "-name" for removing default plugins
+ "plugins": [],
+
+ // Global configuration for plugins
+ "pluginsConfig": {
+ "fontSettings": {
+ "theme": null, //"sepia", "night" or "white",
+ "family": "sans",// "serif" or "sans",
+ "size": 2 // 1 - 4
+ }
+ },
+
+ // Variables for templating
+ "variables": {},
+
+ // Set another theme with your own layout
+ // It's recommended to use plugins or add more options for default theme, though
+ // See https://github.com/GitbookIO/gitbook/issues/209
+ "theme": path.resolve(__dirname, '../theme'),
+
+ // Links in template (null: default, false: remove, string: new value)
+ "links": {
+ // Custom links at top of sidebar
+ "sidebar": {
+ //"Custom link name": "https://customlink.com"
+ },
+
+ // Sharing links
+ "sharing": {
+ "google": null,
+ "facebook": null,
+ "twitter": null,
+ "weibo": null,
+ "all": null
+ }
+ },
+
+
+ // Options for PDF generation
+ "pdf": {
+ // Add toc at the end of the file
+ "toc": true,
+
+ // Add page numbers to the bottom of every page
+ "pageNumbers": false,
+
+ // Font size for the file content
+ "fontSize": 12,
+
+ // Paper size for the pdf
+ // Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’]
+ "paperSize": "a4",
+
+ // Margin (in pts)
+ // Note: 72 pts equals 1 inch
+ "margin": {
+ "right": 62,
+ "left": 62,
+ "top": 56,
+ "bottom": 56
+ },
+
+ //Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
+ "headerTemplate": "",
+
+ //Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
+ "footerTemplate": ""
+ }
+};
+
+module.exports= Configuration;
diff --git a/lib/generate/config.js b/lib/generate/config.js
deleted file mode 100644
index e198c91..0000000
--- a/lib/generate/config.js
+++ /dev/null
@@ -1,137 +0,0 @@
-var Q = require('q');
-var _ = require('lodash');
-var path = require('path');
-
-// Default configuration for gitbook
-var CONFIG = {
- // Folders to use for output
- // Caution: it overrides the value from the command line
- // It's not advised this option in the book.json
- "output": null,
-
- // Generator to use for building
- // Caution: it overrides the value from the command line
- // It's not advised this option in the book.json
- "generator": "site",
-
- // Configuration file to use
- "configFile": "book",
-
- // Book metadats (somes are extracted from the README by default)
- "title": null,
- "description": null,
- "isbn": null,
-
- // For ebook format, the extension to use for generation (default is detected from output extension)
- // "epub", "pdf", "mobi"
- // Caution: it overrides the value from the command line
- // It's not advised this option in the book.json
- "extension": null,
-
- // Plugins list, can contain "-name" for removing default plugins
- "plugins": [],
-
- // Global configuration for plugins
- "pluginsConfig": {
- "fontSettings": {
- "theme": null, //"sepia", "night" or "white",
- "family": "sans",// "serif" or "sans",
- "size": 2 // 1 - 4
- }
- },
-
- // Variables for templating
- "variables": {},
-
- // Set another theme with your own layout
- // It's recommended to use plugins or add more options for default theme, though
- // See https://github.com/GitbookIO/gitbook/issues/209
- "theme": path.resolve(__dirname, '../../theme'),
-
- // Links in template (null: default, false: remove, string: new value)
- "links": {
- // Custom links at top of sidebar
- "sidebar": {
- //"Custom link name": "https://customlink.com"
- },
-
- // Sharing links
- "sharing": {
- "google": null,
- "facebook": null,
- "twitter": null,
- "weibo": null,
- "all": null
- }
- },
-
- // CSS Styles
- "styles": {
- "website": "styles/website.css",
- "ebook": "styles/ebook.css",
- "pdf": "styles/pdf.css",
- "mobi": "styles/mobi.css",
- "epub": "styles/epub.css"
- },
-
- // Options for PDF generation
- "pdf": {
- // Add toc at the end of the file
- "toc": true,
-
- // Add page numbers to the bottom of every page
- "pageNumbers": false,
-
- // Font size for the file content
- "fontSize": 12,
-
- // Paper size for the pdf
- // Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’]
- "paperSize": "a4",
-
- // Margin (in pts)
- // Note: 72 pts equals 1 inch
- "margin": {
- "right": 62,
- "left": 62,
- "top": 36,
- "bottom": 36
- },
-
- //Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
- "headerTemplate": "",
-
- //Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
- "footerTemplate": ""
- }
-};
-
-// Return complete configuration
-var defaultsConfig = function(options) {
- return _.merge(options || {}, CONFIG, _.defaults);
-};
-
-// Read configuration from book.json
-var readConfig = function(options) {
- options = defaultsConfig(options);
-
- return Q()
- .then(function() {
- try {
- var _config = require(path.resolve(options.input, options.configFile));
- options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins', 'generator'));
- }
- catch(err) {
- // No config file: not a big deal
- return Q();
- }
- })
- .thenResolve(options);
-};
-
-module.exports = {
- CONFIG: CONFIG,
- defaults: defaultsConfig,
- read: readConfig
-}
-
diff --git a/lib/generate/fs.js b/lib/generate/fs.js
deleted file mode 100644
index 371051c..0000000
--- a/lib/generate/fs.js
+++ /dev/null
@@ -1,92 +0,0 @@
-var Q = require("q");
-var fs = require('graceful-fs');
-var fsExtra = require("fs-extra");
-var Ignore = require("fstream-ignore");
-
-var getFiles = function(path) {
- var d = Q.defer();
-
- // Our list of files
- var files = [];
-
- var ig = Ignore({
- path: path,
- ignoreFiles: ['.ignore', '.gitignore', '.bookignore']
- });
-
- // Add extra rules to ignore common folders
- ig.addIgnoreRules([
- // Skip Git stuff
- '.git/',
- '.gitignore',
-
- // Skip OS X meta data
- '.DS_Store',
-
- // Skip stuff installed by plugins
- 'node_modules',
-
- // Skip book outputs
- '*.pdf',
- '*.epub',
- '*.mobi',
-
- // Skip config files
- '.ignore',
- '.bookignore',
- 'book.json',
- ], '__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;
-};
-
-module.exports = {
- list: getFiles,
- readFile: Q.denodeify(fs.readFile),
- //writeFile: Q.denodeify(fs.writeFile),
- writeFile: function(filename, data, options) {
- var d = Q.defer();
-
- try {
- fs.writeFileSync(filename, data, options)
- } catch(err) {
- d.reject(err);
- }
- d.resolve();
-
-
- return d.promise;
- },
- 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;
- },
- existsSync: fs.existsSync,
- readFileSync: fs.readFileSync.bind(fs)
-};
diff --git a/lib/generate/generator.js b/lib/generate/generator.js
deleted file mode 100644
index 4791c98..0000000
--- a/lib/generate/generator.js
+++ /dev/null
@@ -1,87 +0,0 @@
-var _ = require("lodash");
-var path = require("path");
-var Q = require("q");
-var fs = require("./fs");
-
-var Plugin = require("./plugin");
-
-var BaseGenerator = function(options) {
- this.options = options;
-
- // Base for assets in plugins
- this.pluginAssetsBase = "book";
-
- this.options.plugins = Plugin.normalizeNames(this.options.plugins);
- this.options.plugins = _.union(this.options.plugins, this.options.defaultsPlugins);
- this.plugins = [];
-};
-
-BaseGenerator.prototype.callHook = function(name, data) {
- return this.plugins.hook(name, data);
-};
-
-// Sets up generator
-BaseGenerator.prototype.load = function() {
- return this.loadPlugins();
-};
-
-BaseGenerator.prototype.loadPlugins = function() {
- var that = this;
-
- return Plugin.fromList(this.options.plugins, this.options.input, this, {
- assetsBase: this.pluginAssetsBase
- })
- .then(function(_plugins) {
- that.plugins = _plugins;
-
- return that.callHook("init");
- });
-};
-
-BaseGenerator.prototype.convertFile = function(content, input) {
- return Q.reject(new Error("Could not convert "+input));
-};
-
-BaseGenerator.prototype.transferFile = function(input) {
- return fs.copy(
- path.join(this.options.input, input),
- path.join(this.options.output, input)
- );
-};
-
-BaseGenerator.prototype.transferFolder = function(input) {
- return fs.mkdirp(
- path.join(this.options.output, input)
- );
-};
-
-BaseGenerator.prototype.copyCover = function() {
- var that = this;
-
- return Q.all([
- fs.copy(path.join(this.options.input, "cover.jpg"), path.join(this.options.output, "cover.jpg")),
- fs.copy(path.join(this.options.input, "cover_small.jpg"), path.join(this.options.output, "cover_small.jpg"))
- ])
- .fail(function() {
- // If orignally from multi-lang, try copy from originalInput
- if (!that.options.originalInput) return;
-
- return Q.all([
- fs.copy(path.join(that.options.originalInput, "cover.jpg"), path.join(that.options.output, "cover.jpg")),
- fs.copy(path.join(that.options.originalInput, "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg"))
- ]);
- })
- .fail(function(err) {
- return Q();
- });
-};
-
-BaseGenerator.prototype.langsIndex = function(langs) {
- return Q.reject(new Error("Langs index is not supported in this generator"));
-};
-
-BaseGenerator.prototype.finish = function() {
- return Q.reject(new Error("Could not finish generation"));
-};
-
-module.exports = BaseGenerator;
diff --git a/lib/generate/index.js b/lib/generate/index.js
deleted file mode 100644
index de1fc0e..0000000
--- a/lib/generate/index.js
+++ /dev/null
@@ -1,374 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-var path = require("path");
-var tmp = require('tmp');
-
-var swig = require('./template');
-var fs = require("./fs");
-var parse = require("../parse");
-var Plugin = require("./plugin");
-var defaultConfig = require("./config");
-
-var generators = {
- "site": require("./site"),
- "page": require("./page"),
- "ebook": require("./ebook"),
- "json": require("./json")
-};
-
-var defaultDescription = "Book generated using GitBook";
-
-
-var containsFiles = function(dir, files) {
- return Q.all(_.map(files, function(file) {
- return fs.exists(path.join(dir, file));
- }))
- .then(_.all);
-};
-
-// Test if generator exists
-var checkGenerator = function(options) {
- if (!generators[options.generator]) {
- return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")+")"));
- }
- return Q();
-};
-
-// Create the generator and load plugins
-var loadGenerator = function(options) {
- return checkGenerator(options)
- .then(function() {
- var generator = new generators[options.generator](options);
-
- return generator.load()
- .then(_.constant(generator));
- });
-};
-
-
-
-var generate = function(options) {
- // Read config file
- return defaultConfig.read(options)
- .then(function(_options) {
- options = _options;
-
- // Validate options
- if (!options.input) {
- return Q.reject(new Error("Need option input (book input directory)"));
- }
-
- // Check files to get folder type (book, multilanguage book or neither)
- return checkGenerator(options);
- })
-
- // Read readme
- .then(function() {
- return fs.readFile(path.join(options.input, "README.md"), "utf-8")
- .then(function(_readme) {
- _readme = parse.readme(_readme);
-
- options.title = options.title || _readme.title;
- options.description = options.description || _readme.description || defaultDescription;
- });
- })
-
- // Detect multi-languages book
- .then(function() {
- return containsFiles(options.input, ['LANGS.md'])
- })
-
- .then(function(isMultiLang) {
- // Multi language book
- if(isMultiLang) {
- return generateMultiLang(options);
- }
-
- // Book
- return generateBook(options);
- });
-};
-
-/*
- * Generate a multilanguage book by generating a book for each folder.
- */
-var generateMultiLang = function(options) {
- var langsSummary;
- options.output = options.output || path.join(options.input, "_book");
-
- return checkGenerator(options)
-
- // Multi-languages book
- .then(function() {
- return fs.readFile(path.join(options.input, "LANGS.md"), "utf-8")
- })
-
- // Clean output folder
- .then(function(_langsSummary) {
- langsSummary = _langsSummary;
- return fs.remove(options.output);
- })
- .then(function() {
- return fs.mkdirp(options.output);
- })
-
- // Generate sub-books
- .then(function() {
- options.langsSummary = parse.langs(langsSummary);
-
- // Generated a book for each valid entry
- return _.reduce(options.langsSummary.list, function(prev, entry) {
- return prev.then(function() {
- return generate(_.extend({}, options, {
- input: path.join(options.input, entry.path),
- output: path.join(options.output, entry.path),
- originalInput: options.input,
- originalOutput: options.output
- }));
- })
- }, Q());
- })
-
- .then(function() {
- return loadGenerator(options);
- })
-
- // Generate languages index
- .then(function(generator) {
- return generator.langsIndex(options.langsSummary);
- })
-
- // Copy cover file
- .then(function() {
- return Q.all([
- fs.copy(path.join(options.input, "cover.jpg"), path.join(options.output, "cover.jpg")),
- fs.copy(path.join(options.input, "cover_small.jpg"), path.join(options.output, "cover_small.jpg"))
- ])
- .fail(function() {
- return Q();
- })
- })
-
- // Return options to caller
- .then(_.constant(options));
-};
-
-/*
- * Use a specific generator to convert a gitbook to a site/pdf/ebook/
- * output is always a folder
- */
-var generateBook = function(options) {
- var files;
-
- options.output = options.output || path.join(options.input, "_book");
-
- // Check if it's a book
- return containsFiles(options.input, ['SUMMARY.md', 'README.md'])
-
- // Fail if not a book
- .then(function(isBook) {
- if(!isBook) {
- return Q.reject(new Error("Invalid gitbook repository, need SUMMARY.md and README.md"));
- }
- })
-
- // Clean output folder
- .then(function() {
- return fs.remove(options.output);
- })
-
- .then(function() {
- return fs.mkdirp(options.output);
- })
-
- // List all files in the repository
- .then(function() {
- return fs.list(options.input)
- .then(function(_files) {
- files = _files;
- });
- })
-
- .then(function() {
- return loadGenerator(options);
- })
-
- // Convert files
- .then(function(generator) {
- // Generate the book
- return Q()
-
- // Get summary
- .then(function() {
- var summary = {
- path: path.join(options.input, "SUMMARY.md")
- };
-
- var _callHook = function(name) {
- return generator.callHook(name, summary)
- .then(function(_summary) {
- summary = _summary;
- return summary;
- });
- };
-
- return fs.readFile(summary.path, "utf-8")
- .then(function(_content) {
- summary.content = _content;
- return _callHook("summary:before");
- })
- .then(function() {
- summary.content = parse.summary(summary.content);
- return _callHook("summary:after");
- })
- .then(function() {
- options.summary = summary.content;
- options.navigation = parse.navigation(options.summary);
- });
- })
-
- // Read glossary
- .then(function() {
- var glossary = {};
-
- var _callHook = function(name) {
- return generator.callHook(name, glossary)
- .then(function(_glossary) {
- glossary = _glossary;
- return glossary;
- });
- };
-
- return fs.readFile(path.join(options.input, "GLOSSARY.md"), "utf-8")
- .fail(function() {
- return "";
- })
- .then(function(_content) {
- glossary.content = _content;
- return _callHook("glossary:before");
- })
- .then(function() {
- glossary.content = parse.glossary(glossary.content);
- return _callHook("glossary:after");
- })
- .then(function() {
- options.glossary = glossary.content;
- });
- })
-
- // Skip processing some files
- .then(function() {
- files = _.filter(files, function (file) {
- return !(
- file === 'SUMMARY.md' ||
- file === 'GLOSSARY.md'
- );
- });
- })
-
- // Copy file and replace markdown file
- .then(function() {
- return Q.all(
- _.chain(files)
- .map(function(file) {
- if (!file) return;
-
- if (file[file.length -1] == "/") {
- return Q(generator.transferFolder(file));
- } else if (path.extname(file) == ".md" && options.navigation[file] != null) {
- return fs.readFile(path.join(options.input, file), "utf-8")
- .then(function(content) {
- return Q(generator.convertFile(content, file));
- });
- } else {
- return Q(generator.transferFile(file));
- }
- })
- .value()
- );
- })
-
- // Finish generation
- .then(function() {
- return generator.callHook("finish:before");
- })
- .then(function() {
- return generator.finish();
- })
- .then(function() {
- return generator.callHook("finish");
- });
- })
-
- // Return all options
- .then(function() {
- return options;
- });
-};
-
-/*
- * Extract files from generate output in a temporary folder
- */
-var generateFile = function(options) {
- options = _.defaults(options || {}, {
- input: null,
- output: null,
- extension: null
- });
-
- return Q.nfcall(tmp.dir)
- .then(function(tmpDir) {
- return generate(
- _.extend({},
- options,
- {
- output: tmpDir
- })
- )
- .then(function(_options) {
- var ext = options.extension;
- var outputFile = options.output || path.resolve(options.input, "book."+ext);
-
- var copyFile = function(lang) {
- var _outputFile = outputFile;
- var _tmpDir = tmpDir;
-
- if (lang) {
- _outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+"_"+lang+path.extname(_outputFile);
- _tmpDir = path.join(_tmpDir, lang);
- }
-
- return fs.copy(
- path.join(_tmpDir, "index."+ext),
- _outputFile
- );
- };
-
- // Multi-langs book
- return Q()
- .then(function() {
- if (_options.langsSummary) {
- return Q.all(
- _.map(_options.langsSummary.list, function(lang) {
- return copyFile(lang.lang);
- })
- );
- } else {
- return copyFile();
- }
- })
- .then(function() {
- return fs.remove(tmpDir);
- });
- });
- });
-};
-
-module.exports = {
- generators: generators,
- folder: generate,
- file: generateFile,
- book: generateBook,
- Plugin: Plugin,
- config: defaultConfig
-};
diff --git a/lib/generate/init.js b/lib/generate/init.js
deleted file mode 100644
index 705e6e7..0000000
--- a/lib/generate/init.js
+++ /dev/null
@@ -1,69 +0,0 @@
-var Q = require('q');
-var _ = require('lodash');
-
-var path = require('path');
-
-var fs = require('./fs');
-var parse = require('../parse');
-
-
-// Extract paths out of a summary
-function paths(summary) {
- return _.reduce(summary.chapters, function(accu, chapter) {
- return accu.concat(
- _.filter([chapter.path].concat(_.pluck(chapter.articles, 'path')))
- );
- }, []);
-}
-
-// Get the parent folders out of a group of files
-function folders(files) {
- return _.chain(files)
- .map(function(file) {
- return path.dirname(file);
- })
- .uniq()
- .value();
-}
-
-function initDir(dir) {
- return fs.readFile(path.join(dir, 'SUMMARY.md'), 'utf8')
- .then(function(src) {
- // Parse summary
- return parse.summary(src);
- })
- .then(function(summary) {
- // Extract paths from summary
- return paths(summary);
- })
- .then(function(paths) {
- // Convert to absolute paths
- return _.map(paths, function(file) {
- return path.resolve(file);
- });
- })
- .then(function(files) {
- // Create folders
- return Q.all(_.map(folders(files), function(folder) {
- return fs.mkdirp(folder);
- }))
- .then(_.constant(files));
- })
- .then(function(files) {
- // Create files that don't exist
- return Q.all(_.map(files, function(file) {
- return fs.exists(file)
- .then(function(exists) {
- if(exists) return;
- return fs.writeFile(file, '');
- });
- }));
- })
- .fail(function(err) {
- console.error(err.stack);
- });
-}
-
-
-// Exports
-module.exports = initDir;
diff --git a/lib/generate/json/index.js b/lib/generate/json/index.js
deleted file mode 100644
index a252ed3..0000000
--- a/lib/generate/json/index.js
+++ /dev/null
@@ -1,77 +0,0 @@
-var util = require("util");
-var path = require("path");
-var Q = require("q");
-var _ = require("lodash");
-
-var fs = require("../fs");
-var parse = require("../../parse");
-var BaseGenerator = require("../generator");
-
-
-var Generator = function() {
- BaseGenerator.apply(this, arguments);
-};
-util.inherits(Generator, BaseGenerator);
-
-Generator.prototype.transferFile = function(input) {
- // ignore
-};
-
-Generator.prototype.convertFile = function(content, input) {
- var that = this;
- var json = {
- progress: parse.progress(this.options.navigation, input)
- };
-
- return Q()
- .then(function() {
- return parse.page(content, {
- dir: path.dirname(input) || '/'
- });
- })
- .then(function(parsed) {
- json.lexed = parsed.lexed;
- json.sections = parsed.sections;
- })
- .then(function() {
- return fs.writeFile(
- path.join(that.options.output, input.replace(".md", ".json")),
- JSON.stringify(json, null, 4)
- );
- });
-};
-
-// Generate languages index
-// Contains the first languages readme and langs infos
-Generator.prototype.langsIndex = function(langs) {
- var that = this;
-
- if (langs.list.length == 0) return Q.reject("Need at least one language");
-
- var mainLang = _.first(langs.list).lang;
- console.log("Main language is", mainLang);
-
- return Q()
- .then(function() {
- return fs.readFile(
- path.join(that.options.output, mainLang, "README.json")
- );
- })
- .then(function(content) {
- var json = JSON.parse(content);
- _.extend(json, {
- langs: langs.list
- });
-
- return fs.writeFile(
- path.join(that.options.output, "README.json"),
- JSON.stringify(json, null, 4)
- );
- });
-};
-
-Generator.prototype.finish = function() {
- // ignore
-};
-
-module.exports = Generator;
diff --git a/lib/generate/page/index.js b/lib/generate/page/index.js
deleted file mode 100644
index 8054fe6..0000000
--- a/lib/generate/page/index.js
+++ /dev/null
@@ -1,84 +0,0 @@
-var _ = require("lodash");
-var util = require("util");
-var path = require("path");
-var Q = require("q");
-var swig = require("../template");
-
-var fs = require("../fs");
-var parse = require("../../parse");
-var BaseGenerator = require("../site");
-
-var Generator = function() {
- BaseGenerator.apply(this, arguments);
-
- // Styles to use
- this.styles = ["ebook"];
-
- // Base for assets in plugins
- this.pluginAssetsBase = "ebook";
-
- // List of pages content
- this.pages = {};
-};
-util.inherits(Generator, BaseGenerator);
-
-Generator.prototype.loadTemplates = function() {
- this.template = swig.compileFile(
- this.plugins.template("ebook:page") || path.resolve(this.options.theme, 'templates/ebook/page.html')
- );
- this.summaryTemplate = swig.compileFile(
- this.plugins.template("ebook:sumary") || path.resolve(this.options.theme, 'templates/ebook/summary.html')
- );
- this.glossaryTemplate = swig.compileFile(
- this.plugins.template("ebook:glossary") || path.resolve(this.options.theme, 'templates/ebook/glossary.html')
- );
-};
-
-// Generate table of contents
-Generator.prototype.writeToc = function() {
- var that = this;
- var basePath = ".";
-
- return this._writeTemplate(this.summaryTemplate, {
- toc: parse.progress(this.options.navigation, "README.md").chapters,
- basePath: basePath,
- staticBase: path.join(basePath, "gitbook"),
- }, path.join(this.options.output, "SUMMARY.html"));
-};
-
-Generator.prototype.finish = function() {
- var that = this;
- var basePath = ".";
- var output = path.join(this.options.output, "index.html");
-
- var progress = parse.progress(this.options.navigation, "README.md");
-
- return Q()
-
- // Write table of contents
- .then(function() {
- return that.writeToc();
- })
-
- // Write glossary
- .then(function() {
- return that.writeGlossary();
- })
-
- // Copy cover
- .then(function() {
- return that.copyCover();
- })
-
- // Copy assets
- .then(function() {
- return that.copyAssets();
- });
-};
-
-// Generate languages index
-Generator.prototype.langsIndex = function(langs) {
- return Q();
-};
-
-module.exports = Generator;
diff --git a/lib/generate/plugin.js b/lib/generate/plugin.js
deleted file mode 100644
index 5ca5e92..0000000
--- a/lib/generate/plugin.js
+++ /dev/null
@@ -1,325 +0,0 @@
-var _ = require("lodash");
-var Q = require("q");
-var semver = require("semver");
-var path = require("path");
-var url = require("url");
-var fs = require("./fs");
-var npmi = require('npmi');
-var resolve = require('resolve');
-
-var pkg = require("../../package.json");
-
-var RESOURCES = ["js", "css"];
-
-var Plugin = function(name, root, generator) {
- this.name = name;
- this.root = root;
- this.packageInfos = {};
- this.infos = {};
- this.generator = generator;
-
- // Bind methods
- _.bindAll(this);
-
- _.each([
- "gitbook-plugin-"+name,
- "gitbook-theme-"+name,
- "gitbook-"+name,
- name,
- ], function(_name) {
- if (this.load(_name, __dirname)) return false;
- if (this.load(_name, path.resolve(root))) return false;
- }, this);
-};
-
-// 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 = name;
-
- return true;
- } catch (e) {
- 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 || "book";
- var book = this.infos[base];
-
- // Nothing specified, fallback to default
- if (!book) {
- return Q({});
- }
-
- // Dynamic function
- if(typeof book === "function") {
- // Call giving it the context of our generator
- return Q().then(book.bind(this.generator));
- }
-
- // 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(RESOURCES, function(resourceType) {
- resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
- });
-
- return resources;
- });
-};
-
-// Test if it's a valid plugin
-Plugin.prototype.isValid = function() {
- return (
- this.packageInfos &&
- this.packageInfos.name &&
- this.packageInfos.engines &&
- this.packageInfos.engines.gitbook &&
- semver.satisfies(pkg.version, this.packageInfos.engines.gitbook)
- );
-};
-
-// Resolve file path
-Plugin.prototype.resolveFile = function(filename) {
- return path.resolve(this.baseDir, filename);
-};
-
-// Resolve file path
-Plugin.prototype.callHook = function(name, data) {
- // Our generator will be the context to apply
- var context = this.generator;
-
- var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
- data = data || {};
-
- if (!hookFunc) return Q(data);
-
- return Q()
- .then(function() {
- return hookFunc.apply(context, [data]);
- });
-};
-
-// Copy plugin assets fodler
-Plugin.prototype.copyAssets = function(out, options) {
- var that = this;
- options = _.defaults(options || {}, {
- base: "book"
- });
-
- return this.getResources(options.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));
-};
-
-
-// Install a list of plugin
-Plugin.install = function(options) {
- // Normalize list of plugins
- var plugins = Plugin.normalizeList(options.plugins);
-
- // Install plugins one by one
- return _.reduce(plugins, function(prev, plugin) {
- return prev.then(function() {
- var fullname = "gitbook-plugin-"+plugin.name;
- console.log("Install plugin", plugin.name, "from npm ("+fullname+") with version", (plugin.version || "*"));
- return Q.nfcall(npmi, {
- 'name': fullname,
- 'version': plugin.version,
- 'path': options.input,
- 'npmLoad': {
- 'loglevel': 'silent',
- 'loaded': false,
- 'prefix': options.input
- }
- });
- });
- }, Q());
-};
-
-// Normalize a list of plugins to use
-Plugin.normalizeList = function(plugins) {
- // Normalize list to an array
- plugins = _.isString(plugins) ? plugins.split(",") : (plugins || []);
-
- // Divide as {name, version} to handle format like "myplugin@1.0.0"
- plugins = _.map(plugins, function(plugin) {
- var parts = plugin.split("@");
- return {
- 'name': parts[0],
- 'version': parts[1] // optional
- }
- });
-
- // 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
- plugins = _.chain(plugins)
- .concat(_.map(Plugin.defaults, function(plugin) {
- return { 'name': plugin }
- }))
- .uniq()
- .value();
-
- // Build final list
- plugins = _.filter(plugins, function(plugin) {
- return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == "-");
- });
-
- return plugins;
-};
-
-// Normalize a list of plugin name to use
-Plugin.normalizeNames = function(plugins) {
- return _.pluck(Plugin.normalizeList(plugins), "name");
-};
-
-// Extract data from a list of plugin
-Plugin.fromList = function(names, root, generator, options) {
- options = _.defaults(options || {}, {
- assetsBase: "book"
- });
-
- var failed = [];
-
- // Load plugins
- var plugins = _.map(names, function(name) {
- var plugin = new Plugin(name, root, generator);
- if (!plugin.isValid()) failed.push(name);
- return plugin;
- });
-
- if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")+". Run 'gitbook install' to install plugins from NPM."));
-
- // The raw resources extracted from each plugin
- var pluginResources;
-
- // Get resources of plugins
- return Q.all(_.map(plugins, function(plugin) {
- return plugin.getResources(options.assetsBase);
- }))
- // Extract resources out
- // css, js, etc ...
- .then(function(resources) {
- pluginResources = resources;
- // Group by resource types
- return _.chain(RESOURCES)
- .map(function(resourceType) {
- // Get resources from all the plugins for this current type
- return [
- // Key
- resourceType,
- // Value
- _.chain(resources)
- .pluck(resourceType)
- .compact()
- .flatten()
- .value()
- ];
- })
- .object()
- .value();
- })
- // Extract html snippets
- .then(function(resources) {
- // Map of html resources by name added by each plugin
- resources.html = pluginResources.reduce(function(accu, resource) {
- var html = (resource && resource.html) || {};
- _.each(html, function(code, key) {
- // Turn into function if not one already
- if (!_.isFunction(code)) code = _.constant(code);
- // Append
- accu[key] = (accu[key] || []).concat([code]);
- });
-
- return accu;
- }, {});
-
- return resources;
- })
- // Return big multi-plugin object
- .then(function(resources) {
- return {
- 'list': plugins,
- 'resources': resources,
- 'hook': function(name, data) {
- return _.reduce(plugins, function(prev, plugin) {
- return prev.then(function(ret) {
- return plugin.callHook(name, ret);
- });
- }, Q(data));
- },
- 'template': function(name) {
- var withTpl = _.find(plugins, function(plugin) {
- return (
- plugin.infos.templates &&
- plugin.infos.templates[name]
- );
- });
-
- if (!withTpl) return null;
- return withTpl.resolveFile(withTpl.infos.templates[name]);
- },
- 'html': function(tag, context, options) {
- return _.map(resources.html[tag] || [], function(code) {
- return code.call(context, options);
- }).join("\n");
- }
- };
- });
-};
-
-// Default plugins added to each books
-Plugin.defaults = [
- "mathjax"
-];
-
-module.exports = Plugin;
diff --git a/lib/generate/site/glossary_indexer.js b/lib/generate/site/glossary_indexer.js
deleted file mode 100644
index 46ac9a4..0000000
--- a/lib/generate/site/glossary_indexer.js
+++ /dev/null
@@ -1,101 +0,0 @@
-var _ = require("lodash");
-var kramed = require('kramed');
-var textRenderer = require('kramed-text-renderer');
-
-var entryId = require('../../parse/glossary').entryId;
-
-
-function Indexer(glossary) {
- if(!(this instanceof Indexer)) {
- return new Indexer(glossary);
- }
-
- _.bindAll(this);
-
- this.glossary = glossary || [];
-
- this.glossaryTerms = _.pluck(this.glossary, "id");
-
- // Regex for searching for terms through body
- this.termsRegex = new RegExp(
- // Match any of the terms
- "("+
- this.glossaryTerms.map(regexEscape).join('|') +
- ")",
-
- // Flags
- "gi"
- );
-
- // page url => terms
- this.idx = {
- /*
- "a/b.html": ["one word", "second word"]
- */
- };
-
- // term => page urls
- this.invertedIdx = {
- /*
- "word1": ["page1.html", "page2.html"]
- */
- };
-
- // Use text renderer
- this.renderer = textRenderer();
-}
-
-Indexer.prototype.text = function(nodes) {
- // Copy section
- var section = _.toArray(nodes);
-
- // kramed's Render expects this, we don't use it yet
- section.links = {};
-
- var options = _.extend({}, kramed.defaults, {
- renderer: this.renderer
- });
-
- return kramed.parser(section, options);
-};
-
-// Add page to glossary index
-Indexer.prototype.add = function(sections, url) {
- if(!(this.glossary && this.glossary.length > 0)) {
- return;
- }
-
- var textblob =
- _.where(sections, { type: 'normal' })
- .map(this.text)
- .join('\n');
-
- var matches = _(textblob.match(this.termsRegex) || [])
- .map(entryId)
- .uniq()
- .value();
-
- // Add idx for book
- this.idx[url] = matches;
-
- // Add to inverted idx
- matches.forEach(function(match) {
- if(!this.invertedIdx[match]) {
- this.invertedIdx[match] = [];
- }
- this.invertedIdx[match].push(url);
- }.bind(this));
-};
-
-// Dump index as a string
-Indexer.prototype.dump = function() {
- return JSON.stringify(this.idx);
-};
-
-
-function regexEscape(s) {
- return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
-}
-
-// Exports
-module.exports = Indexer;
diff --git a/lib/generate/site/index.js b/lib/generate/site/index.js
deleted file mode 100644
index dfdbf68..0000000
--- a/lib/generate/site/index.js
+++ /dev/null
@@ -1,314 +0,0 @@
-var util = require("util");
-var path = require("path");
-var Q = require("q");
-var _ = require("lodash");
-var swig = require("../template");
-
-var fs = require("../fs");
-var parse = require("../../parse");
-var BaseGenerator = require("../generator");
-var links = require("../../utils/links");
-var indexer = require('./search_indexer');
-var glossaryIndexer = require('./glossary_indexer');
-
-
-var Generator = function() {
- BaseGenerator.apply(this, arguments);
-
- // Attach methods to instance
- _.bindAll(this);
-
- this.styles = ["website"];
- this.revision = Date.now();
- this.indexer = indexer();
-};
-util.inherits(Generator, BaseGenerator);
-
-// Add template loading to load
-Generator.prototype.load = function() {
- var that = this;
-
- return BaseGenerator.prototype.load.apply(this)
- .then(function() {
- return that.loadStyles();
- })
- .then(function() {
- return that.loadTemplates();
- });
-};
-
-// Load all styles
-Generator.prototype.loadStyles = function() {
- var that = this;
- this.styles = _.chain(this.styles)
- .map(function(style) {
- var stylePath = that.options.styles[style];
- if (fs.existsSync(path.resolve(that.options.input, stylePath))) {
- return stylePath;
- }
- return null;
- })
- .compact()
- .value();
-};
-
-// Load all templates
-Generator.prototype.loadTemplates = function() {
- this.template = swig.compileFile(
- this.plugins.template("site:page") || path.resolve(this.options.theme, 'templates/website/page.html')
- );
- this.langsTemplate = swig.compileFile(
- this.plugins.template("site:langs") || path.resolve(this.options.theme, 'templates/website/langs.html')
- );
- this.glossaryTemplate = swig.compileFile(
- this.plugins.template("site:glossary") || path.resolve(this.options.theme, 'templates/website/glossary.html')
- );
-};
-
-// Generate a template
-Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
- var that = this;
-
- interpolate = interpolate || _.identity;
- return Q()
- .then(function(sections) {
- return tpl(_.extend({
- styles: that.styles,
-
- revision: that.revision,
-
- title: that.options.title,
- description: that.options.description,
-
- glossary: that.options.glossary,
-
- summary: that.options.summary,
- allNavigation: that.options.navigation,
-
- plugins: that.plugins,
- pluginsConfig: JSON.stringify(that.options.pluginsConfig),
- htmlSnippet: _.partialRight(that.plugins.html, that, options),
-
- options: that.options
- }, options));
- })
- .then(interpolate)
- .then(function(html) {
- return fs.writeFile(
- output,
- html
- );
- });
-};
-
-Generator.prototype.indexPage = function(lexed, pagePath) {
- // Setup glossary indexer if not yet setup
- if(!this.glossaryIndexer) {
- this.glossaryIndexer = glossaryIndexer(this.options.glossary);
- }
-
- this.indexer.add(lexed, pagePath);
- this.glossaryIndexer.add(lexed, pagePath);
- return Q();
-};
-
-// Convert a markdown file into a normalized data set
-Generator.prototype.prepareFile = function(content, _input) {
- var that = this;
-
- var input = path.join(this.options.input, _input);
-
- var page = {
- path: _input,
- rawPath: input,
- content: content,
- progress: parse.progress(this.options.navigation, _input)
- };
-
- var _callHook = function(name) {
- return that.callHook(name, page)
- .then(function(_page) {
- page = _page;
- return page;
- });
- };
-
- return Q()
- .then(function() {
- // Send content to plugins
- return _callHook("page:before");
- })
- .then(function() {
- // Lex, parse includes and get
- // Get HTML generated sections
- return parse.page(page.content, {
- // Local files path
- dir: path.dirname(_input) || '/',
-
- // Output directory
- outdir: path.dirname(_input) || '/',
-
- // Includer for templating
- includer: parse.includer(that.options.variables, [
- path.dirname(_input) || '/',
- path.join(that.options.input, '_includes'),
- ], path.join, fs.readFileSync)
- });
- })
- .then(function(parsed) {
- page.lexed = parsed.lexed;
- page.sections = parsed.sections;
-
- // Use plugin hook
- return _callHook("page");
- })
- .then(function() {
- return page;
- });
-};
-
-// Convert a markdown file to html
-Generator.prototype.convertFile = function(content, _input) {
- var that = this;
-
- var _output = _input.replace(".md", ".html");
- if (_output == "README.html") _output = "index.html";
- var output = path.join(this.options.output, _output);
- var basePath = path.relative(path.dirname(output), this.options.output) || ".";
-
- // Bug fix for issue #493 which would occur when relative-links are 2-level or more deep in win32
- if (process.platform === 'win32') {
- basePath = basePath.replace(/\\/g, '/');
- }
-
- return this.prepareFile(content, _input)
- .then(function(page) {
- // Index page in search
- return that.indexPage(page.lexed, _output).thenResolve(page);
- })
- .then(function(page) {
- // Write file
- return that._writeTemplate(that.template, {
- progress: page.progress,
-
- _input: page.path,
- content: page.sections,
-
- basePath: basePath,
- staticBase: links.join(basePath, "gitbook"),
- }, output, function(html) {
- page.content = html;
-
- return that.callHook("page:after", page).get("content")
- });
- });
-};
-
-// Generate languages index
-Generator.prototype.langsIndex = function(langs) {
- var that = this;
- var basePath = ".";
-
- return this._writeTemplate(this.langsTemplate, {
- langs: langs.list,
-
- basePath: basePath,
- staticBase: path.join(basePath, "gitbook"),
- }, path.join(this.options.output, "index.html"))
- .then(function() {
- // Copy assets
- return that.copyAssets();
- });
-};
-
-// Generate glossary
-Generator.prototype.writeGlossary = function() {
- var that = this;
- var basePath = ".";
-
- // No glossary
- if (!this.glossaryIndexer) return Q();
-
- // Transform the glossary to get term, description, files
- var glossary = _.chain(this.glossaryIndexer.invertedIdx)
- .map(function(links, id) {
- var term = _.find(that.options.glossary, { 'id': id });
-
- return {
- id: id,
- name: term.name,
- description: term.description,
- files: _.chain(links)
- .map(function(link) {
- var name = link.slice(0, -5);
-
- if (name == "index") {
- name = "README";
- }
- return that.options.navigation[name+".md"];
- })
- .sortBy("percent")
- .value()
- }
- })
- .sortBy("name")
- .value();
-
- return this._writeTemplate(this.glossaryTemplate, {
- glossaryIndex: glossary,
- basePath: basePath,
- staticBase: path.join(basePath, "gitbook"),
- }, path.join(this.options.output, "GLOSSARY.html"));
-};
-
-// Copy assets
-Generator.prototype.copyAssets = function() {
- var that = this;
-
- // Copy gitbook assets
- return fs.copy(
- path.join(that.options.theme, "assets"),
- path.join(that.options.output, "gitbook")
- )
-
- // Copy plugins assets
- .then(function() {
- return Q.all(
- _.map(that.plugins.list, function(plugin) {
- var pluginAssets = path.join(that.options.output, "gitbook/plugins/", plugin.name);
- return plugin.copyAssets(pluginAssets, {
- base: that.pluginAssetsBase
- });
- })
- );
- });
-};
-
-// Dump search index to disk
-Generator.prototype.writeSearchIndex = function() {
- return fs.writeFile(
- path.join(this.options.output, 'search_index.json'),
- this.indexer.dump()
- );
-};
-
-// Dump glossary index to disk
-Generator.prototype.writeGlossaryIndex = function() {
- if (!this.glossaryIndexer) return Q();
-
- return fs.writeFile(
- path.join(this.options.output, 'glossary_index.json'),
- JSON.stringify(this.options.glossary)
- );
-};
-
-
-Generator.prototype.finish = function() {
- return this.copyAssets()
- .then(this.copyCover)
- .then(this.writeGlossary)
- .then(this.writeGlossaryIndex)
- .then(this.writeSearchIndex);
-};
-
-module.exports = Generator;
diff --git a/lib/generate/site/search_indexer.js b/lib/generate/site/search_indexer.js
deleted file mode 100644
index 7cfe29a..0000000
--- a/lib/generate/site/search_indexer.js
+++ /dev/null
@@ -1,71 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-
-var lunr = require('lunr');
-var kramed = require('kramed');
-var textRenderer = require('kramed-text-renderer');
-
-
-function Indexer() {
- if(!(this instanceof Indexer)) {
- return new Indexer();
- }
-
- _.bindAll(this);
-
- // Setup lunr index
- this.idx = lunr(function () {
- this.ref('url');
-
- this.field('title', { boost: 10 });
- this.field('body');
- });
-
- this.renderer = textRenderer();
-}
-
-Indexer.prototype.text = function(nodes) {
- // Copy section
- var section = _.toArray(nodes);
-
- // kramed's Render expects this, we don't use it yet
- section.links = {};
-
- var options = _.extend({}, kramed.defaults, {
- renderer: this.renderer
- });
-
- return kramed.parser(section, options);
-};
-
-Indexer.prototype.addSection = function(path, section) {
- var url = [path, section.id].join('#');
-
- var title = this.text(
- _.filter(section, {'type': 'heading'})
- );
-
- var body = this.text(
- _.omit(section, {'type': 'heading'})
- );
-
- // Add to lunr index
- this.idx.add({
- url: url,
- title: title,
- body: body,
- });
-};
-
-Indexer.prototype.add = function(lexedPage, url) {
- var sections = lexedPage;
-
- _.map(sections, _.partial(this.addSection, url));
-};
-
-Indexer.prototype.dump = function() {
- return JSON.stringify(this.idx);
-};
-
-// Exports
-module.exports = Indexer;
diff --git a/lib/generate/template.js b/lib/generate/template.js
deleted file mode 100644
index acfa580..0000000
--- a/lib/generate/template.js
+++ /dev/null
@@ -1,52 +0,0 @@
-var path = require("path");
-var swig = require('swig');
-var hljs = require('highlight.js');
-
-var links = require('../utils/').links;
-var pkg = require('../../package.json');
-
-swig.setDefaults({
- locals: {
- gitbook: {
- version: pkg.version
- }
- }
-});
-
-// Swig filter for returning the count of lines in a code section
-swig.setFilter('lines', function(content) {
- return content.split('\n').length;
-});
-
-// Swig filter for returning a link to the associated html file of a markdown file
-swig.setFilter('mdLink', function(link) {
- var link = link.replace(".md", ".html");
- if (link == "README.html") link = "index.html";
- return link;
-});
-
-// Swig filter: highlight coloration
-swig.setFilter('code', function(code, lang) {
- try {
- return hljs.highlight(lang, code).value;
- } catch(e) {
- return hljs.highlightAuto(code).value;
- }
-});
-
-// Convert a level into a deep level
-swig.setFilter('lvl', function(lvl) {
- return lvl.split(".").length;
-});
-
-// Join path
-swig.setFilter('pathJoin', function(base, _path) {
- return links.join(base, _path);
-});
-
-// Is a link an absolute link
-swig.setFilter('isExternalLink', function(link) {
- return links.isExternal(link);
-});
-
-module.exports = swig;
diff --git a/lib/generator.js b/lib/generator.js
new file mode 100644
index 0000000..c809de2
--- /dev/null
+++ b/lib/generator.js
@@ -0,0 +1,78 @@
+var _ = require("lodash");
+var path = require("path");
+var Q = require("q");
+var fs = require("./utils/fs");
+
+var Plugin = require("./plugin");
+
+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.plugins.hook(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(
+ path.join(this.book.root, 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(path.join(that.book.root, "cover.jpg"), path.join(that.options.output, "cover.jpg")),
+ fs.copy(path.join(that.book.root, "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(err) {
+ return Q();
+ });
+};
+
+// At teh end of the generation
+BaseGenerator.prototype.finish = function() {
+ return Q.reject(new Error("Could not finish generation"));
+};
+
+module.exports = BaseGenerator;
diff --git a/lib/generate/ebook/index.js b/lib/generators/ebook.js
index c74ffcd..29b1966 100644
--- a/lib/generate/ebook/index.js
+++ b/lib/generators/ebook.js
@@ -4,27 +4,54 @@ var Q = require("q");
var _ = require("lodash");
var exec = require('child_process').exec;
-var fs = require('graceful-fs');
-var parse = require("../../parse");
-var BaseGenerator = require("../page");
-var stringUtils = require("../../utils/string");
+var fs = require("../utils/fs");
+var stringUtils = require("../utils/string");
+var BaseGenerator = require("./website");
-var Generator = function() {
+var Generator = function(book, format) {
BaseGenerator.apply(this, arguments);
// eBook format
- this.ebookFormat = this.options.extension || path.extname(this.options.output).replace("\.", "") || "pdf";
+ this.ebookFormat = format;
+
+ // Resources namespace
+ this.namespace = "ebook";
// Styles to use
- this.styles = ["ebook", this.ebookFormat];
+ this.styles = _.compact(["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"));
+};
+
Generator.prototype.finish = function() {
var that = this;
- return BaseGenerator.prototype.finish.apply(this)
+ return Q()
+ .then(this.copyAssets)
+ .then(this.copyCover)
+ .then(this.writeGlossary)
+ .then(this.writeSummary)
.then(function() {
+ if (!that.ebookFormat) return Q();
+
var d = Q.defer();
if (!that.options.cover && fs.existsSync(path.join(that.options.output, "cover.jpg"))) {
@@ -37,6 +64,8 @@ Generator.prototype.finish = function() {
"--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 ')]",
"--chapter-mark": "pagebreak",
@@ -61,8 +90,12 @@ Generator.prototype.finish = function() {
"--pdf-mono-font-size": String(pdfOptions.fontSize),
"--paper-size": String(pdfOptions.paperSize),
"--pdf-page-numbers": Boolean(pdfOptions.pageNumbers),
- "--pdf-header-template": String(pdfOptions.headerTemplate),
- "--pdf-footer-template": String(pdfOptions.footerTemplate)
+ "--pdf-header-template": String(pdfOptions.headerTemplate) || "<p class='header'><span>"+that.options.title+"</span></p>",
+ "--pdf-footer-template": String(pdfOptions.footerTemplate) || "<p class='footer'><span>_SECTION_</span> <span style='float:right;'>_PAGENUM_</span></p>"
+ });
+ } else if (that.ebookFormat == "epub") {
+ _.extend(_options, {
+ "--dont-split-on-page-breaks": true
});
}
@@ -73,8 +106,12 @@ Generator.prototype.finish = function() {
stringUtils.optionsToShellArgs(_options)
].join(" ");
- exec(command, function (error, stdout, stderr) {
+ that.book.log.info("start conversion to", that.ebookFormat, "....");
+
+ var child = exec(command, function (error, stdout, stderr) {
if (error) {
+ that.book.log.info.fail();
+
if (error.code == 127) {
error.message = "Need to install ebook-convert from Calibre";
} else {
@@ -82,9 +119,19 @@ Generator.prototype.finish = function() {
}
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;
});
};
diff --git a/lib/generators/index.js b/lib/generators/index.js
new file mode 100644
index 0000000..e619188
--- /dev/null
+++ b/lib/generators/index.js
@@ -0,0 +1,11 @@
+var _ = require("lodash");
+var EbookGenerator = require("./ebook");
+
+module.exports = {
+ json: require("./json"),
+ website: require("./website"),
+ ebook: EbookGenerator,
+ pdf: _.partialRight(EbookGenerator, "pdf"),
+ mobi: _.partialRight(EbookGenerator, "mobi"),
+ epub: _.partialRight(EbookGenerator, "epub")
+};
diff --git a/lib/generators/json.js b/lib/generators/json.js
new file mode 100644
index 0000000..6c9439d
--- /dev/null
+++ b/lib/generators/json.js
@@ -0,0 +1,76 @@
+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(input) { };
+
+// 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;
+
+ return Q()
+ .then(function() {
+ langs = that.book.langs;
+ mainLang = langs.length > 0? _.first(langs).lang : null;
+
+ readme = links.changeExtension(that.book.readmeFile, ".json");
+
+ // Read readme from main language
+ return fs.readFile(
+ mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme)
+ );
+ })
+ .then(function(content) {
+ // Extend it with infos about the languages
+ var json = JSON.parse(content);
+ _.extend(json, {
+ langs: langs
+ });
+
+ // Write it as README.json
+ return fs.writeFile(
+ path.join(that.options.output, "README.json"),
+ JSON.stringify(json, null, 4)
+ );
+ });
+};
+
+module.exports = Generator;
diff --git a/lib/generators/website.js b/lib/generators/website.js
new file mode 100644
index 0000000..a58e3c4
--- /dev/null
+++ b/lib/generators/website.js
@@ -0,0 +1,279 @@
+var util = require("util");
+var path = require("path");
+var Q = require("q");
+var _ = require("lodash");
+
+var nunjucks = require("nunjucks");
+var AutoEscapeExtension = require("nunjucks-autoescape");
+var FilterExtension = require("nunjucks-filter");
+var I18nExtension = require("nunjucks-i18n");
+
+var fs = require("../utils/fs");
+var BaseGenerator = require("../generator");
+var links = require("../utils/links");
+var pageUtil = require("../utils/page");
+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];
+ if (fs.existsSync(path.resolve(that.book.root, stylePath))) {
+ return stylePath;
+ }
+ return null;
+ })
+ .compact()
+ .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.getByLanguage(language) && language != "en") {
+ that.book.log.warn.ln("Language '"+language+"' is not available as a layout locales (en, "+i18n.getLanguages().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));
+ that.env.addExtension('I18nExtension', new I18nExtension({
+ env: that.env,
+ translations: i18n.getLocales(),
+ locale: "language"
+ }));
+ });
+};
+
+// Finis generation
+Generator.prototype.finish = function() {
+ return this.copyAssets()
+ .then(this.copyCover)
+ .then(this.writeGlossary)
+ .then(this.writeSearchIndex)
+ .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.contentLink(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() {
+ var that = this;
+ 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() {
+ var that = this;
+
+ // No glossary
+ if (this.book.glossary.length == 0) return Q();
+
+ return this._writeTemplate(this.templates["glossary"], {}, path.join(this.options.output, "GLOSSARY.html"));
+};
+
+// Write the search index
+Generator.prototype.writeSearchIndex = function() {
+ var that = this;
+
+ return fs.writeFile(
+ path.join(this.options.output, "search_index.json"),
+ JSON.stringify(this.book.searchIndex)
+ );
+};
+
+// 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(sections) {
+ 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(),
+
+ 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"),
+ }, 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"),
+ path.join(that.options.output, "gitbook")
+ )
+
+ // Copy plugins assets
+ .then(function() {
+ return Q.all(
+ _.map(that.book.plugins.list, function(plugin) {
+ var pluginAssets = path.join(that.options.output, "gitbook/plugins/", plugin.name);
+ return plugin.copyAssets(pluginAssets, that.namespace);
+ })
+ );
+ });
+};
+
+module.exports = Generator;
diff --git a/lib/index.js b/lib/index.js
index ba240ce..95abd43 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,5 +1,210 @@
-module.exports = {
- parse: require('./parse/'),
- generate: require('./generate/')
+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 Plugin = require("./plugin");
+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"
+};
+
+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 = undefined;
+
+ 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 files");
+ 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, kwargs) {
+ 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, kwargs) {
+ return Book.init(args[0] || process.cwd())
+ .then(function(){
+ console.log("");
+ console.log(color.green("Done, without error"));
+ });
+ }
+ }
+ ])
+};
diff --git a/lib/parse/glossary.js b/lib/parse/glossary.js
deleted file mode 100644
index 549e9fd..0000000
--- a/lib/parse/glossary.js
+++ /dev/null
@@ -1,48 +0,0 @@
-var _ = require('lodash');
-var kramed = require('kramed');
-
-// Get all the pairs of header + paragraph in a list of nodes
-function groups(nodes) {
- // A list of next nodes
- var next = nodes.slice(1).concat(null);
-
- return _.reduce(nodes, function(accu, node, idx) {
- // Skip
- if(!(
- node.type === 'heading' &&
- (next[idx] && next[idx].type === 'paragraph')
- )) {
- return accu;
- }
-
- // Add group
- accu.push([
- node,
- next[idx]
- ]);
-
- return accu;
- }, []);
-}
-
-function parseGlossary(src) {
- var nodes = kramed.lexer(src);
-
- return groups(nodes)
- .map(function(pair) {
- // Simplify each group to a simple object with name/description
- return {
- name: pair[0].text,
- id: entryId(pair[0].text),
- description: pair[1].text,
- };
- });
-}
-
-// Normalizes a glossary entry's name to create an ID
-function entryId(name) {
- return name.toLowerCase();
-}
-
-module.exports = parseGlossary;
-module.exports.entryId = entryId;
diff --git a/lib/parse/include.js b/lib/parse/include.js
deleted file mode 100644
index 483b184..0000000
--- a/lib/parse/include.js
+++ /dev/null
@@ -1,12 +0,0 @@
-var _ = require('lodash');
-
-module.exports = function(markdown, includer) {
- // Memoized include function (to cache lookups)
- var _include = _.memoize(includer);
-
- return markdown.replace(/{{([\s\S]+?)}}/g, function(match, key) {
- // If fails leave content as is
- key = key.trim();
- return _include(key) || match;
- });
-};
diff --git a/lib/parse/includer.js b/lib/parse/includer.js
deleted file mode 100644
index f7f20e0..0000000
--- a/lib/parse/includer.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// Return a fs inclduer
-module.exports = function(ctx, folders, resolveFile, readFile) {
- return function(name) {
- return ctx[name] ||
- folders.map(function(folder) {
- // Try including snippet from FS
- try {
- var fname = resolveFile(folder, name);
- // Trim trailing newlines/space of imported snippets
- return readFile(fname, 'utf8').trimRight();
- } catch(err) {}
- })
- .filter(Boolean)[0];
- }
-};
diff --git a/lib/parse/index.js b/lib/parse/index.js
deleted file mode 100644
index 23471af..0000000
--- a/lib/parse/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-module.exports = {
- summary: require('./summary'),
- glossary: require('./glossary'),
- langs: require('./langs'),
- page: require('./page'),
- lex: require('./lex'),
- progress: require('./progress'),
- navigation: require('./navigation'),
- readme: require('./readme'),
- includer: require('./includer')
-};
diff --git a/lib/parse/is_exercise.js b/lib/parse/is_exercise.js
deleted file mode 100644
index 74ed753..0000000
--- a/lib/parse/is_exercise.js
+++ /dev/null
@@ -1,17 +0,0 @@
-var _ = require('lodash');
-
-function isExercise(nodes) {
- var codeType = { type: 'code' };
-
- // Number of code nodes in section
- var len = _.filter(nodes, codeType).length;
-
- return (
- // Got 3 or 4 code blocks
- (len === 3 || len === 4) &&
- // Ensure all nodes are at the end
- _.all(_.last(nodes, len), codeType)
- );
-}
-
-module.exports = isExercise;
diff --git a/lib/parse/is_quiz.js b/lib/parse/is_quiz.js
deleted file mode 100644
index 3322ff0..0000000
--- a/lib/parse/is_quiz.js
+++ /dev/null
@@ -1,87 +0,0 @@
-var _ = require('lodash');
-
-function isQuizNode(node) {
- return (/^[(\[][ x][)\]]/).test(node.text || node);
-}
-
-function isTableQuestion(nodes) {
- var block = questionBlock(nodes);
- return (
- block.length === 1 &&
- block[0].type === 'table' &&
- _.all(block[0].cells[0].slice(1), isQuizNode)
- );
-}
-
-function isListQuestion(nodes) {
- var block = questionBlock(nodes);
- // Counter of when we go in and out of lists
- var inlist = 0;
- // Number of lists we found
- var lists = 0;
- // Elements found outside a list
- var outsiders = 0;
- // Ensure that we have nothing except lists
- _.each(block, function(node) {
- if(node.type === 'list_start') {
- inlist++;
- } else if(node.type === 'list_end') {
- inlist--;
- lists++;
- } else if(inlist === 0) {
- // Found non list_start or list_end whilst outside a list
- outsiders++;
- }
- });
- return lists > 0 && outsiders === 0;
-}
-
-function isQuestion(nodes) {
- return isListQuestion(nodes) || isTableQuestion(nodes);
-}
-
-// Remove (optional) paragraph header node and blockquote
-function questionBlock(nodes) {
- return nodes.slice(
- nodes[0].type === 'paragraph' ? 1 : 0,
- _.findIndex(nodes, { type: 'blockquote_start' })
- );
-}
-
-function splitQuestions(nodes) {
- // Represents nodes in current question
- var buffer = [];
- return _.reduce(nodes, function(accu, node) {
- // Add node to buffer
- buffer.push(node);
-
- // Flush buffer once we hit the end of a question
- if(node.type === 'blockquote_end') {
- accu.push(buffer);
- // Clear buffer
- buffer = [];
- }
-
- return accu;
- }, []);
-}
-
-function isQuiz(nodes) {
- // Extract potential questions
- var questions = splitQuestions(
- // Skip quiz title if there
- nodes.slice(
- (nodes[0] && nodes[0].type) === 'paragraph' ? 1 : 0
- )
- );
-
- // Nothing that looks like questions
- if(questions.length === 0) {
- return false;
- }
-
- // Ensure all questions are correctly structured
- return _.all(questions, isQuestion);
-}
-
-module.exports = isQuiz;
diff --git a/lib/parse/langs.js b/lib/parse/langs.js
deleted file mode 100644
index 01b7c8c..0000000
--- a/lib/parse/langs.js
+++ /dev/null
@@ -1,25 +0,0 @@
-var _ = require("lodash");
-var parseEntries = require("./summary").entries;
-
-
-var parseLangs = function(content) {
- var entries = parseEntries(content);
-
- return {
- list: _.chain(entries)
- .filter(function(entry) {
- return Boolean(entry.path);
- })
- .map(function(entry) {
- return {
- title: entry.title,
- path: entry.path,
- lang: entry.path.replace("/", "")
- };
- })
- .value()
- };
-};
-
-
-module.exports = parseLangs; \ No newline at end of file
diff --git a/lib/parse/lex.js b/lib/parse/lex.js
deleted file mode 100644
index 3391acf..0000000
--- a/lib/parse/lex.js
+++ /dev/null
@@ -1,79 +0,0 @@
-var _ = require('lodash');
-var kramed = require('kramed');
-
-var isExercise = require('./is_exercise');
-var isQuiz = require('./is_quiz');
-
-// Split a page up into sections (lesson, exercises, ...)
-function splitSections(nodes) {
- var section = [];
-
- return _.reduce(nodes, function(sections, el) {
- if(el.type === 'hr') {
- sections.push(section);
- section = [];
- } else {
- section.push(el);
- }
-
- return sections;
- }, []).concat([section]); // Add remaining nodes
-}
-
-// What is the type of this section
-function sectionType(nodes, idx) {
- if(isExercise(nodes)) {
- return 'exercise';
- } else if(isQuiz(nodes)) {
- return 'quiz';
- }
-
- return 'normal';
-}
-
-// Generate a uniqueId to identify this section in our code
-function sectionId(section, idx) {
- return _.uniqueId('gitbook_');
-}
-
-function lexPage(src) {
- // Lex file
- var nodes = kramed.lexer(src);
-
- return _.chain(splitSections(nodes))
- .map(function(section, idx) {
- // Detect section type
- section.type = sectionType(section, idx);
- return section;
- })
- .map(function(section, idx) {
- // Give each section an ID
- section.id = sectionId(section, idx);
- return section;
-
- })
- .filter(function(section) {
- return !_.isEmpty(section);
- })
- .reduce(function(sections, section) {
- var last = _.last(sections);
-
- // Merge normal sections together
- if(last && last.type === section.type && last.type === 'normal') {
- last.push.apply(last, [{'type': 'hr'}].concat(section));
- } else {
- // Add to list of sections
- sections.push(section);
- }
-
- return sections;
- }, [])
- .map(function(section) {
- section.links = nodes.links;
- return section;
- })
- .value();
-}
-
-// Exports
-module.exports = lexPage;
diff --git a/lib/parse/page.js b/lib/parse/page.js
deleted file mode 100644
index 5fb2081..0000000
--- a/lib/parse/page.js
+++ /dev/null
@@ -1,160 +0,0 @@
-var _ = require('lodash');
-var kramed = require('kramed');
-var hljs = require('highlight.js');
-
-var lex = require('./lex');
-var renderer = require('./renderer');
-
-var include = require('./include');
-var lnormalize = require('../utils/lang').normalize;
-
-
-
-// Render a section using our custom renderer
-function render(section, _options) {
- // Copy section
- var links = section.links || {};
- section = _.toArray(section);
- section.links = links;
-
- // Build options using defaults and our custom renderer
- var options = _.extend({}, kramed.defaults, {
- renderer: renderer(null, _options),
-
- // Synchronous highlighting with highlight.js
- highlight: function (code, lang) {
- if(!lang) return code;
-
- // Normalize lang
- lang = lnormalize(lang);
-
- try {
- return hljs.highlight(lang, code).value;
- } catch(e) { }
-
- return code;
- }
- });
-
- return kramed.parser(section, options);
-}
-
-function quizQuestion(node) {
- if (node.text) {
- node.text = node.text.replace(/^([\[(])x([\])])/, "$1 $2");
- } else {
- return node.replace(/^([\[(])x([\])])/, "$1 $2");
- }
-}
-
-function parsePage(src, options) {
- options = options || {};
-
- // Lex if not already lexed
- var parsed = {
- lexed: (_.isArray(src) ? src : lex(include(src, options.includer || function() { return undefined; })))
- };
- parsed.sections = parsed.lexed.map(function(section) {
- // Transform given type
- if(section.type === 'exercise') {
- var nonCodeNodes = _.reject(section, {
- 'type': 'code'
- });
-
- var codeNodes = _.filter(section, {
- 'type': 'code'
- });
-
- // Languages in code blocks
- var langs = _.pluck(codeNodes, 'lang').map(lnormalize);
-
- // Check that they are all the same
- var validLangs = _.all(_.map(langs, function(lang) {
- return lang && lang === langs[0];
- }));
-
- // Main language
- var lang = validLangs ? langs[0] : null;
-
- return {
- id: section.id,
- type: section.type,
- content: render(nonCodeNodes, options),
- lang: lang,
- code: {
- base: codeNodes[0].text,
- solution: codeNodes[1].text,
- validation: codeNodes[2].text,
- // Context is optional
- context: codeNodes[3] ? codeNodes[3].text : null,
- }
- };
- } else if (section.type === 'quiz') {
- var quiz = [], question, foundFeedback = false;
- var nonQuizNodes = section[0].type === 'paragraph' && section[1].type !== 'list_start' ? [section[0]] : [];
- var quizNodes = section.slice(0);
- quizNodes.splice(0, nonQuizNodes.length);
-
- for (var i = 0; i < quizNodes.length; i++) {
- var node = quizNodes[i];
-
- if (question && (((node.type === 'list_end' || node.type === 'blockquote_end') && i === quizNodes.length - 1)
- || node.type === 'table' || (node.type === 'paragraph' && !foundFeedback))) {
- quiz.push({
- base: render(question.questionNodes, options),
- solution: render(question.solutionNodes, options),
- feedback: render(question.feedbackNodes, options)
- });
- }
-
- if (node.type === 'table' || (node.type === 'paragraph' && !foundFeedback)) {
- question = { questionNodes: [], solutionNodes: [], feedbackNodes: [] };
- }
-
- if (node.type === 'blockquote_start') {
- foundFeedback = true;
- } else if (node.type === 'blockquote_end') {
- foundFeedback = false;
- }
-
- if (node.type === 'table') {
- question.solutionNodes.push(_.cloneDeep(node));
- node.cells = node.cells.map(function(row) {
- return row.map(quizQuestion);
- });
- question.questionNodes.push(node);
- } else if (!/blockquote/.test(node.type)) {
- if (foundFeedback) {
- question.feedbackNodes.push(node);
- } else if (node.type === 'paragraph' || node.type === 'text'){
- question.solutionNodes.push(_.cloneDeep(node));
- quizQuestion(node);
- question.questionNodes.push(node);
- } else {
- question.solutionNodes.push(node);
- question.questionNodes.push(node);
- }
- }
- }
-
- return {
- id: section.id,
- type: section.type,
- content: render(nonQuizNodes, options),
- quiz: quiz
- };
- }
-
- // Render normal pages
- return {
- id: section.id,
- type: section.type,
- content: render(section, options)
- };
- });
-
- return parsed;
-}
-
-// Exports
-module.exports = parsePage;
diff --git a/lib/parse/readme.js b/lib/parse/readme.js
deleted file mode 100644
index 9d8f552..0000000
--- a/lib/parse/readme.js
+++ /dev/null
@@ -1,45 +0,0 @@
-var _ = require('lodash');
-var kramed = require('kramed');
-var textRenderer = require('kramed-text-renderer');
-
-function extractFirstNode(nodes, nType) {
- return _.chain(nodes)
- .filter(function(node) {
- return node.type == nType;
- })
- .pluck("text")
- .first()
- .value();
-}
-
-
-function parseReadme(src) {
- var nodes, title, description;
- var renderer = textRenderer();
-
- // Parse content
- nodes = kramed.lexer(src);
-
- title = extractFirstNode(nodes, "heading") || '';
- description = extractFirstNode(nodes, "paragraph") || '';
-
- var convert = _.compose(
- function(text) {
- return _.unescape(text.replace(/(\r\n|\n|\r)/gm, ""));
- },
- function(text) {
- return kramed.parse(text, _.extend({}, kramed.defaults, {
- renderer: renderer
- }));
- }
- );
-
- return {
- title: convert(title),
- description: convert(description)
- };
-}
-
-
-// Exports
-module.exports = parseReadme;
diff --git a/lib/parse/renderer.js b/lib/parse/renderer.js
deleted file mode 100644
index 5b6a79d..0000000
--- a/lib/parse/renderer.js
+++ /dev/null
@@ -1,141 +0,0 @@
-var url = require('url');
-var _ = require('lodash');
-var inherits = require('util').inherits;
-var links = require('../utils').links;
-var kramed = require('kramed');
-
-var rendererId = 0;
-
-function GitBookRenderer(options, extra_options) {
- if(!(this instanceof GitBookRenderer)) {
- return new GitBookRenderer(options, extra_options);
- }
- GitBookRenderer.super_.call(this, options);
-
- this._extra_options = extra_options;
- this.quizRowId = 0;
- this.id = rendererId++;
- this.quizIndex = 0;
-}
-inherits(GitBookRenderer, kramed.Renderer);
-
-GitBookRenderer.prototype._unsanitized = function(href) {
- var prot = '';
- try {
- prot = decodeURIComponent(unescape(href))
- .replace(/[^\w:]/g, '')
- .toLowerCase();
-
- } catch (e) {
- return true;
- }
-
- if(prot.indexOf('javascript:') === 0) {
- return true;
- }
-
- return false;
-};
-
-GitBookRenderer.prototype.link = function(href, title, text) {
- // Our "fixed" href
- var _href = href;
-
- // Don't build if it looks malicious
- if (this.options.sanitize && this._unsanitized(href)) {
- return text;
- }
-
- // Parsed version of the url
- var parsed = url.parse(href);
- var o = this._extra_options;
- var extname = parsed.path? _.last(parsed.path.split(".")) : "";
-
- // Relative link, rewrite it to point to github repo
- if(links.isRelative(_href) && extname == "md") {
- _href = links.toAbsolute(_href, o.dir || "./", o.outdir || "./");
- _href = _href.replace(".md", ".html");
- }
-
- // Generate HTML for link
- var out = '<a href="' + _href + '"';
- // Title if no null
- if (title) {
- out += ' title="' + title + '"';
- }
- // Target blank if external
- if(parsed.protocol) {
- out += ' target="_blank"';
- }
- out += '>' + text + '</a>';
- return out;
-};
-
-GitBookRenderer.prototype.image = function(href, title, text) {
- // Our "fixed" href
- var _href = href;
-
- // Parsed version of the url
- var parsed = url.parse(href);
-
- // Options
- var o = this._extra_options;
-
- // Relative image, rewrite it depending output
- if(links.isRelative(href) && o && o.dir && o.outdir) {
- // o.dir: directory parent of the file currently in rendering process
- // o.outdir: directory parent from the html output
-
- _href = links.toAbsolute(_href, o.dir, o.outdir);
- }
-
- return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);
-};
-
-GitBookRenderer.prototype.tablerow = function(content) {
- this.quizRowId += 1;
- return GitBookRenderer.super_.prototype.tablerow(content);
-};
-
-var fieldRegex = /^([(\[])([ x])[\])]/;
-GitBookRenderer.prototype._createCheckboxAndRadios = function(text) {
- var match = fieldRegex.exec(text);
- if (!match) {
- return text;
- }
- //fix radio input uncheck failed
- var quizFieldName='quiz-row-' + this.id + '-' + this.quizRowId ;
- var quizIdentifier = quizFieldName + '-' + this.quizIndex++;
- var field = "<input name='" + quizFieldName + "' id='" + quizIdentifier + "' type='";
- field += match[1] === '(' ? "radio" : "checkbox";
- field += match[2] === 'x' ? "' checked/>" : "'/>";
- var splittedText = text.split(fieldRegex);
- var length = splittedText.length;
- var label = '<label class="quiz-label" for="' + quizIdentifier + '">' + splittedText[length - 1] + '</label>';
- return text.replace(fieldRegex, field).replace(splittedText[length - 1], label);
-};
-
-GitBookRenderer.prototype.tablecell = function(content, flags) {
- return GitBookRenderer.super_.prototype.tablecell(this._createCheckboxAndRadios(content), flags);
-};
-
-GitBookRenderer.prototype.listitem = function(text) {
- return GitBookRenderer.super_.prototype.listitem(this._createCheckboxAndRadios(text));
-};
-
-GitBookRenderer.prototype.code = function(code, lang, escaped) {
- return GitBookRenderer.super_.prototype.code.call(
- this,
- code,
- lang,
- escaped
- );
-};
-
-GitBookRenderer.prototype.heading = function(text, level, raw) {
- var id = this.options.headerPrefix + raw.toLowerCase().replace(/[^\w -]+/g, '').replace(/ /g, '-');
- return '<h' + level + ' id="' + id + '">' + text + '</h' + level + '>\n';
-};
-
-// Exports
-module.exports = GitBookRenderer;
diff --git a/lib/parse/summary.js b/lib/parse/summary.js
deleted file mode 100644
index 2fdec8a..0000000
--- a/lib/parse/summary.js
+++ /dev/null
@@ -1,167 +0,0 @@
-var _ = require('lodash');
-var kramed = require('kramed');
-
-
-// Utility function for splitting a list into groups
-function splitBy(list, starter, ender) {
- var starts = 0;
- var ends = 0;
- var group = [];
-
- // Groups
- return _.reduce(list, function(groups, value) {
- // Ignore start and end delimiters in resulted groups
- if(starter(value)) {
- starts++;
- } else if(ender(value)) {
- ends++;
- }
-
- // Add current value to group
- group.push(value);
-
- // We've got a matching
- if(starts === ends && starts !== 0) {
- // Add group to end groups
- // (remove starter and ender token)
- groups.push(group.slice(1, -1));
-
- // Reset group
- group = [];
- }
-
- return groups;
- }, []);
-}
-
-function listSplit(nodes, start_type, end_type) {
- return splitBy(nodes, function(el) {
- return el.type === start_type;
- }, function(el) {
- return el.type === end_type;
- });
-}
-
-// Get the biggest list
-// out of a list of kramed nodes
-function filterList(nodes) {
- return _.chain(nodes)
- .toArray()
- .rest(function(el) {
- // Get everything after list_start
- return el.type !== 'list_start';
- })
- .reverse()
- .rest(function(el) {
- // Get everything after list_end (remember we're reversed)
- return el.type !== 'list_end';
- })
- .reverse()
- .value().slice(1, -1);
-}
-
-function skipSpace(nodes) {
- return _.filter(nodes, function(node) {
- return node && node.type != 'space';
- });
-}
-
-function correctLoose(nodes) {
- return _.map(nodes, function(node) {
- // Return normal nodes
- if(!node || node.type != 'loose_item_start') {
- return node
- }
-
- // Correct loose items
- node.type = 'list_item_start';
-
- return node;
- })
-}
-
-// Parses an Article or Chapter title
-// supports extracting links
-function parseTitle(src, nums) {
- // Check if it's a link
- var matches = kramed.InlineLexer.rules.link.exec(src);
-
- var level = nums.join('.');
-
- // Not a link, return plain text
- if(!matches) {
- return {
- title: src,
- level: level,
- path: null,
- };
- }
-
- return {
- title: matches[1],
- level: level,
-
- // Normalize path
- // 1. Convert Window's "\" to "/"
- // 2. Remove leading "/" if exists
- path: matches[2].replace(/\\/g, '/').replace(/^\/+/, ''),
- };
-}
-
-function parseChapter(nodes, nums) {
- // Convert single number to an array
- nums = _.isArray(nums) ? nums : [nums];
-
- return _.extend(parseTitle(_.first(nodes).text, nums), {
- articles: _.map(listSplit(filterList(nodes), 'list_item_start', 'list_item_end'), function(nodes, i) {
- return parseChapter(nodes, nums.concat(i + 1));
- })
- });
-}
-
-function defaultChapterList(chapterList) {
- var first = _.first(chapterList);
-
- // Check if introduction node was specified in SUMMARY.md
- if (first) {
- var chapter = parseChapter(first, [0]);
-
- // Already have README node, we're good to go
- if(chapter.path === 'README.md') {
- return chapterList;
- }
- }
-
- // It wasn't specified, so add in default
- return [
- [ { type: 'text', text: '[Introduction](README.md)' } ]
- ].concat(chapterList);
-}
-
-function listGroups(src) {
- var nodes = kramed.lexer(src);
-
- // Get out groups of lists
- return listSplit(
- filterList(correctLoose(skipSpace(nodes))),
- 'list_item_start', 'list_item_end'
- );
-}
-
-function parseSummary(src) {
- // Split out chapter sections
- var chapters = defaultChapterList(listGroups(src));
-
- return {
- chapters: chapters.map(parseChapter)
- };
-}
-
-function parseEntries (src) {
- return listGroups(src).map(parseChapter);
-}
-
-
-// Exports
-module.exports = parseSummary;
-module.exports.entries = parseEntries;
diff --git a/lib/plugin.js b/lib/plugin.js
new file mode 100644
index 0000000..11298fc
--- /dev/null
+++ b/lib/plugin.js
@@ -0,0 +1,183 @@
+var _ = require("lodash");
+var Q = require("q");
+var semver = require("semver");
+var path = require("path");
+var url = require("url");
+var fs = require("./utils/fs");
+var resolve = require('resolve');
+
+var pkg = require("../package.json");
+
+var Plugin = function(book, name) {
+ this.book = book;
+ this.name = name;
+ this.packageInfos = {};
+ this.infos = {};
+
+ // Bind methods
+ _.bindAll(this);
+
+ _.each([
+ "gitbook-plugin-"+name,
+ "gitbook-"+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"
+]
+
+// 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 = 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 &&
+ semver.satisfies(pkg.version, 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;
+};
+
+// 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 remove 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));
+};
+
+module.exports = Plugin;
diff --git a/lib/pluginslist.js b/lib/pluginslist.js
new file mode 100644
index 0000000..61d52a5
--- /dev/null
+++ b/lib/pluginslist.js
@@ -0,0 +1,225 @@
+var _ = require("lodash");
+var Q = require("q");
+var npmi = require('npmi');
+var npm = require('npm');
+var semver = require('semver');
+
+var Plugin = require("./plugin");
+var pkg = require("../package.json");
+
+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, options) {
+ 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);
+ }
+
+ // Extract filters
+ _.each(plugin.getFilters(), function(filterFunc, filterName) {
+ that.book.template.addFilter(filterName, filterFunc);
+ });
+
+ // Extract blocks
+ _.each(plugin.getBlocks(), function(block, blockName) {
+ that.book.template.addBlock(blockName, block);
+ });
+
+ 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;
+ var defaultsPlugins = _.pluck(that.book.options.defaultsPlugins)
+
+ // Remove defaults (no need to install)
+ var plugins = _.filter(that.book.options.plugins, function(plugin) {
+ return !_.contains(defaultsPlugins, plugin.name);
+ });
+
+ // 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 && semver.satisfies(pkg.version, v.gitbook);
+ })
+ .sort(function(v1, v2) {
+ return semver.lt(v1.version, v2.version)? 1 : -1;
+ })
+ .pluck("version")
+ .first()
+ .value();
+ });
+ })
+
+ // Install the plugin with the resolved version
+ .then(function(version) {
+ if (!version) {
+ throw "Found no satisfactory version for plugin "+plugin.name;
+ }
+
+ that.book.log.info.ln("install plugin", plugin.name, "from npm ("+fullname+") with version", version);
+ return Q.nfcall(npmi, {
+ 'name': fullname,
+ 'version': version,
+ 'path': that.book.root,
+ 'npmLoad': {
+ 'loglevel': 'silent',
+ 'loaded': true,
+ 'prefix': that.book.root
+ }
+ });
+ })
+ .then(function() {
+ that.book.log.info.ok("plugin", plugin.name, "installed with success");
+ });
+ });
+ }, Q());
+};
+
+
+
+module.exports = PluginsList;
diff --git a/lib/template.js b/lib/template.js
new file mode 100644
index 0000000..28b11c6
--- /dev/null
+++ b/lib/template.js
@@ -0,0 +1,394 @@
+var _ = require("lodash");
+var Q = require("q");
+var path = require("path");
+var nunjucks = require("nunjucks");
+
+var git = require("./utils/git");
+var stringUtils = require("./utils/string");
+var fs = require("./utils/fs");
+var batch = require("./utils/batch");
+var pkg = require("../package.json");
+
+
+// The loader should handle relative and git url
+var BookLoader = nunjucks.Loader.extend({
+ async: true,
+
+ init: function(book) {
+ this.book = book;
+ },
+
+ getSource: function(fileurl, callback) {
+ var that = this;
+
+ git.resolveFile(fileurl)
+ .then(function(filepath) {
+ // Is local file
+ if (!filepath) filepath = path.resolve(that.book.root, 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 {
+ src: source.toString(),
+ path: filepath
+ }
+ });
+ })
+ .nodeify(callback);
+ },
+
+ resolve: function(from, to) {
+ return path.resolve(path.dirname(from), to);
+ }
+});
+
+
+var TemplateEngine = function(book) {
+ this.book = book;
+ this.log = this.book.log;
+
+ // Nunjucks env
+ this.env = new nunjucks.Environment(
+ new BookLoader(book),
+ {
+ // 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
+ this.blocks = {};
+
+ // Bind methods
+ _.bindAll(this);
+
+ // Default block "html" that return html not parsed
+ this.addBlock("html", {
+ process: _.identity
+ });
+};
+
+// Process a block in a context
+TemplateEngine.prototype.processBlock = function(blk) {
+ if (_.isString(blk)) blk = { body: 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.blocks[blk.id] = blk;
+
+ //Parsable block, just return it
+ if (blk.parse) {
+ return blk.body;
+ }
+
+ // Return it as a macro
+ return "%+%"+blk.id+"%+%";
+};
+
+// Replace blocks by body after processing
+// This is done to avoid that markdown 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.blocks[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) {}
+
+ 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 a block
+TemplateEngine.prototype.addBlock = function(name, block) {
+ var that = this;
+
+ block = _.defaults(block || {}, {
+ shortcuts: [],
+ end: "end"+name,
+ process: _.identity,
+ blocks: []
+ });
+
+ var extName = 'Block'+name+'Extension';
+ if (this.env.getExtension(extName)) {
+ this.log.warn.ln("conflict in blocks, '"+name+"' is already defined");
+ return false;
+ }
+
+ this.log.debug.ln("add block '"+name+"'");
+
+ var Ext = function () {
+ this.tags = [name];
+
+ this.parse = function(parser, nodes, lexer) {
+ 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 = args.pop() || {};
+
+ // Extract blocks body
+ var _blocks = _.map(block.blocks, function(blockName, i){
+ return {
+ name: blockName,
+ body: blocks[i]()
+ };
+ });
+
+ var func = that.bindContext(block.process);
+
+ Q()
+ .then(function() {
+ return func.call(context, {
+ body: body(),
+ args: args,
+ kwargs: kwargs,
+ blocks: _blocks
+ });
+ })
+
+ // 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({
+ parsers: shortcut.parsers,
+ start: shortcut.start,
+ end: shortcut.end,
+ tag: {
+ start: name,
+ end: block.end
+ }
+ });
+ }, this);
+};
+
+// Apply a shortcut to a string
+TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
+ if (!_.contains(shortcut.parsers, parser)) return content;
+ var regex = new RegExp(
+ stringUtils.escapeRegex(shortcut.start) + "([\\s\\S]*?[^\\$])" + stringUtils.escapeRegex(shortcut.end),
+ 'g'
+ );
+ return content.replace(regex, function(all, match) {
+ return "{% "+shortcut.tag.start+" %}"+ match + "{% "+shortcut.tag.end+" %}";
+ });
+};
+
+// Render a string from the book
+TemplateEngine.prototype.renderString = function(content, context, options) {
+ var context = _.extend({}, context, {
+ // Variables from book.json
+ book: this.book.options.variables,
+
+ // 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);
+
+ // Replace shortcuts
+ content = _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), options.type), content);
+
+ return Q.nfcall(this.env.renderString.bind(this.env), content, context, options);
+};
+
+// Render a file from the book
+TemplateEngine.prototype.renderFile = function(filename, options) {
+ var that = this, context;
+
+ 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, context;
+
+ return that.book.statFile(page.path)
+ .then(function(stat) {
+ 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.blocks, {
+ max: 20,
+ fn: function(blk, blkId) {
+ return Q()
+ .then(function() {
+ if (!blk.post) return Q();
+ return blk.post();
+ })
+ .then(function() {
+ delete that.blocks[blkId];
+ });
+ }
+ })
+ .thenResolve(_content);
+ });
+};
+
+module.exports = TemplateEngine;
diff --git a/lib/utils/batch.js b/lib/utils/batch.js
new file mode 100644
index 0000000..bd3b80f
--- /dev/null
+++ b/lib/utils/batch.js
@@ -0,0 +1,52 @@
+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(item) {}
+ });
+
+
+ function startItem(item, i) {
+ if (concurrents >= options.max) {
+ pending.push([item, i]);
+ return;
+ }
+
+ concurrents++;
+ Q()
+ .then(function() {
+ return options.fn(item, i);
+ })
+ .then(function() {
+ concurrents--;
+
+ // Next pending
+ var next = pending.shift();
+
+ if (concurrents == 0 && !next) {
+ d.resolve();
+ } else if (next) {
+ startItem.apply(null, next);
+ }
+ })
+ .fail(function(err) {
+ pending = [];
+ d.reject(err);
+ })
+ }
+
+ _.each(items, startItem);
+
+ return d.promise;
+}
+
+module.exports = {
+ execEach: execEach
+};
+
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
new file mode 100644
index 0000000..98a3a87
--- /dev/null
+++ b/lib/utils/fs.js
@@ -0,0 +1,178 @@
+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;
+ },
+ 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.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 (1) {
+ if (!fs.existsSync(filename)) break;
+ _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;
+ });
+}
+
+module.exports = fsUtils;
diff --git a/lib/utils/git.js b/lib/utils/git.js
new file mode 100644
index 0000000..9a669db
--- /dev/null
+++ b/lib/utils/git.js
@@ -0,0 +1,112 @@
+var Q = require("q");
+var _ = require("lodash");
+var url = require("url");
+var tmp = require("tmp");
+var path = require("path");
+var crc = require("crc");
+var exec = Q.denodeify(require("child_process").exec);
+var URI = require("URIjs");
+
+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);
+ });
+};
+
+
+module.exports = {
+ checkUrl: checkGitUrl,
+ parseUrl: parseGitUrl,
+ resolveFile: resolveFileFromGit
+};
diff --git a/lib/utils/i18n.js b/lib/utils/i18n.js
new file mode 100644
index 0000000..d7560bd
--- /dev/null
+++ b/lib/utils/i18n.js
@@ -0,0 +1,72 @@
+var _ = require("lodash");
+var path = require("path");
+var fs = require("fs");
+
+var I18N_PATH = path.resolve(__dirname, "../../theme/i18n/")
+
+var getLocales = _.memoize(function() {
+ var locales = fs.readdirSync(I18N_PATH);
+ return _.chain(locales)
+ .map(function(local) {
+ local = path.basename(local, ".json");
+ return [local, _.extend({
+ direction: "ltr"
+ }, require(path.join(I18N_PATH, local)), {
+ id: local
+ })];
+ })
+ .object()
+ .value();
+});
+
+var getLanguages = function() {
+ return _.keys(getLocales());
+};
+
+var getByLanguage = function(lang) {
+ lang = normalizeLanguage(lang);
+ var locales = getLocales();
+ return locales[lang];
+};
+
+var compareLocales = function(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 locales = getLocales();
+ var language = _.chain(locales)
+ .values()
+ .map(function(locale) {
+ locale = locale.id;
+
+ return {
+ locale: locale,
+ score: compareLocales(lang, locale)
+ }
+ })
+ .filter(function(lang) {
+ return lang.score > 0;
+ })
+ .sortBy("score")
+ .pluck("locale")
+ .last()
+ .value();
+ return language || lang;
+});
+
+module.exports = {
+ getLocales: getLocales,
+ getLanguages: getLanguages,
+ getByLanguage: getByLanguage,
+ normalizeLanguage: normalizeLanguage
+};
diff --git a/lib/utils/images.js b/lib/utils/images.js
new file mode 100644
index 0000000..f1302c3
--- /dev/null
+++ b/lib/utils/images.js
@@ -0,0 +1,39 @@
+var _ = require("lodash");
+var Q = require("q");
+var fs = require("./fs");
+var shellescape = require('shell-escape');
+var exec = require('child_process').exec;
+
+var links = require("./links");
+
+// 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 = exec(command, function (error, stdout, stderr) {
+ if (error) {
+ if (error.code == 127) error = new Error("Need to install 'svgexport' using 'npm install svgexport -g'");
+ return d.reject(error);
+ }
+ if (fs.existsSync(dest)) {
+ d.resolve();
+ } else {
+ d.reject(new Error("Error converting "+source));
+ }
+ });
+
+ return d.promise;
+};
+
+module.exports = {
+ convertSVG: convertSVG,
+ INVALID: [".svg"]
+};
diff --git a/lib/utils/index.js b/lib/utils/index.js
deleted file mode 100644
index dbc4087..0000000
--- a/lib/utils/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- lang: require('./lang'),
- links: require('./links')
-};
diff --git a/lib/utils/lang.js b/lib/utils/lang.js
deleted file mode 100644
index 9da737b..0000000
--- a/lib/utils/lang.js
+++ /dev/null
@@ -1,19 +0,0 @@
-var MAP = {
- 'py': 'python',
- 'js': 'javascript',
- 'rb': 'ruby',
- 'csharp': 'cs',
-};
-
-function normalize(lang) {
- if(!lang) { return null; }
-
- var lower = lang.toLowerCase();
- return MAP[lower] || lower;
-}
-
-// Exports
-module.exports = {
- normalize: normalize,
- MAP: MAP
-};
diff --git a/lib/utils/links.js b/lib/utils/links.js
index b4d2fb7..aa7c241 100644
--- a/lib/utils/links.js
+++ b/lib/utils/links.js
@@ -15,32 +15,43 @@ var isRelative = function(href) {
try {
var parsed = url.parse(href);
- return !parsed.protocol && parsed.path && parsed.path[0] != '/';
+ return !!(!parsed.protocol && parsed.path);
} catch(err) {}
return true;
};
+// Return true if the link is an achor
+var isAnchor = function(href) {
+ try {
+ var parsed = url.parse(href);
+ return !!(!parsed.protocol && !parsed.path && parsed.hash);
+ } catch(err) {}
+
+ return false;
+};
+
// Relative to absolute path
// dir: directory parent of the file currently in rendering process
// outdir: directory parent from the html output
var toAbsolute = function(_href, dir, outdir) {
- // Absolute file in source
- _href = path.join(dir, _href);
+ if (isExternal(_href)) return _href;
- // make it relative to output
- _href = path.relative(outdir, _href);
+ // Path '_href' inside the base folder
+ var hrefInRoot = path.normalize(path.join(dir, _href));
+ if (_href[0] == "/") hrefInRoot = path.normalize(_href.slice(1));
- if (process.platform === 'win32') {
- _href = _href.replace(/\\/g, '/');
- }
+ // Make it relative to output
+ _href = path.relative(outdir, hrefInRoot);
+
+ // Normalize windows paths
+ _href = _href.replace(/\\/g, '/');
return _href;
};
// Join links
-
var join = function() {
var _href = path.join.apply(path, arguments);
@@ -51,10 +62,19 @@ var join = function() {
return _href;
};
+// Change extension
+var changeExtension = function(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
+ join: join,
+ changeExtension: changeExtension
};
diff --git a/lib/utils/logger.js b/lib/utils/logger.js
new file mode 100644
index 0000000..4c6af79
--- /dev/null
+++ b/lib/utils/logger.js
@@ -0,0 +1,102 @@
+var _ = require('lodash');
+var util = require('util');
+var color = require('bash-color');
+
+var LEVELS = {
+ DEBUG: 0,
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3,
+ DISABLED: 10
+};
+
+var COLORS = {
+ DEBUG: color.purple,
+ INFO: color.cyan,
+ WARN: color.yellow,
+ ERROR: color.red
+};
+
+module.exports = function(_write, logLevel) {
+ var logger = {};
+ var lastChar = '\n';
+ if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
+
+ // Write a simple message
+ logger.write = function(msg) {
+ msg = msg.toString();
+ lastChar = _.last(msg);
+ return _write(msg);
+ };
+
+ // Format a message
+ logger.format = function() {
+ return util.format.apply(util, arguments);
+ };
+
+ // Write a line
+ logger.writeLn = function(msg) {
+ return this.write((msg || "")+"\n");
+ };
+
+ // Write a message with a certain level
+ logger.log = function(level) {
+ if (level < logLevel) return;
+
+ var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = logger.format.apply(logger, args);
+
+ if (lastChar == '\n') {
+ msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg;
+ }
+
+ return logger.write(msg);
+ };
+ logger.logLn = function() {
+ if (lastChar != '\n') logger.write("\n");
+
+ var args = Array.prototype.slice.apply(arguments);
+ args.push("\n");
+ logger.log.apply(logger, args);
+ };
+
+ // Write a OK
+ logger.ok = function(level) {
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = logger.format.apply(logger, args);
+ if (arguments.length > 1) {
+ logger.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
+ } else {
+ logger.log(level, color.green("OK"), "\n");
+ }
+ };
+
+ // Write an "FAIL"
+ logger.fail = function(level) {
+ return logger.log(level, color.red("ERROR")+"\n");
+ };
+
+ _.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) {
+ levelKey = levelKey.toLowerCase();
+
+ logger[levelKey] = _.partial(logger.log, level);
+ logger[levelKey].ln = _.partial(logger.logLn, level);
+ logger[levelKey].ok = _.partial(logger.ok, level);
+ logger[levelKey].fail = _.partial(logger.fail, level);
+ logger[levelKey].promise = function(p) {
+ return p.
+ then(function(st) {
+ logger[levelKey].ok();
+ return st;
+ }, function(err) {
+ logger[levelKey].fail();
+ throw err;
+ });
+ }
+ });
+
+ return logger;
+};
+module.exports.LEVELS = LEVELS;
+module.exports.COLORS = COLORS;
diff --git a/lib/parse/navigation.js b/lib/utils/navigation.js
index ae4eb9d..21666ad 100644
--- a/lib/parse/navigation.js
+++ b/lib/utils/navigation.js
@@ -27,19 +27,35 @@ function navigation(summary, files) {
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
// List of all navNodes
- // Flatten chapters, then add in default README node if ndeeded etc ...
+ // Flatten chapters, then add in default README node if needed etc ...
var navNodes = flattenChapters(summary.chapters);
- var prevNodes = [null].concat(navNodes.slice(0, -1));
- var nextNodes = navNodes.slice(1).concat([null]);
// Mapping of prev/next for a give path
- var mapping = _.chain(_.zip(navNodes, prevNodes, nextNodes))
- .map(function(nodes) {
- var current = nodes[0], prev = nodes[1], next = nodes[2];
+ var mapping = _.chain(navNodes)
+ .map(function(current, i) {
+ var prev = null, next = null;
// Skip if no path
if(!current.path) 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, {
title: current.title,
prev: prev,
@@ -47,7 +63,7 @@ function navigation(summary, files) {
level: current.level,
}];
})
- .filter()
+ .compact()
.object()
.value();
diff --git a/lib/utils/page.js b/lib/utils/page.js
new file mode 100644
index 0000000..effa24f
--- /dev/null
+++ b/lib/utils/page.js
@@ -0,0 +1,343 @@
+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 links = require('./links');
+var imgUtils = require('./images');
+var fs = require('./fs');
+var batch = require('./batch');
+
+// Render a cheerio dom as html
+var renderDom = function($, dom, options) {
+ if (!dom && $._root && $._root.children) {
+ dom = $._root.children;
+ }
+
+ options = options|| dom.options || $._options;
+ return domSerializer(dom, options);
+};
+
+// Map of images that have been converted
+var imgConversionCache = {};
+
+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.
+ do {
+ // 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;
+ }
+ }
+ }
+
+ } while ( node = node.nextSibling );
+ }
+
+ // Time to remove those elements!
+ 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] = content;
+ $(this).replaceWith($("<img>").attr("src", dest));
+ });
+ }
+
+ // Find images to normalize
+ $("img").each(function() {
+ var origin = undefined;
+ 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(origin);
+ src = links.toAbsolute(src, options.base, options.output);
+ isExternal = false;
+ }
+
+ var ext = path.extname(src);
+ var srcAbs = path.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 = path.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 = path.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);
+ });
+
+ $("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(path.join(options.base, href));
+ var absolutePath = parts.pathname;
+ var anchor = parts.hash;
+
+ // If is in navigation relative: transform as content
+ if (options.navigation[absolutePath]) {
+ href = options.book.contentLink(href);
+ }
+
+ // Transform as absolute
+ href = links.toAbsolute(href, options.base, options.output)+anchor;
+ } else {
+ // External links
+ $(this).attr("target", "_blank");
+ }
+
+ // Transform extension
+ $(this).attr("href", href);
+ });
+
+ // Replace glossayr terms
+ _.each(options.glossary, function(term) {
+ var r = new RegExp( "\\b(" + pregQuote(term.name.toLowerCase()) + ")\\b" , 'gi' );
+ var includedInFiles = false;
+
+ $("*").each(function() {
+ 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)));
+ })
+
+ // 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);
+};
+
+// Extract text from sections
+function extractText(sections) {
+ return _.reduce(sections, function(prev, section) {
+ if (section.type != "normal") return prev;
+
+ var $ = cheerio.load(section.content);
+ $("*").each(function() {
+ prev = prev+" "+$(this).text();
+ });
+
+ return prev;
+ }, "");
+};
+
+module.exports = {
+ normalize: normalizePage,
+ extractText: extractText
+};
diff --git a/lib/parse/progress.js b/lib/utils/progress.js
index 10a06d2..b66aea9 100644
--- a/lib/parse/progress.js
+++ b/lib/utils/progress.js
@@ -44,4 +44,4 @@ var calculProgress = function(navigation, current) {
};
}
-module.exports = calculProgress; \ No newline at end of file
+module.exports = calculProgress;
diff --git a/lib/utils/server.js b/lib/utils/server.js
new file mode 100644
index 0000000..2b97fe8
--- /dev/null
+++ b/lib/utils/server.js
@@ -0,0 +1,96 @@
+var Q = require('q');
+var _ = require('lodash');
+
+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 != null;
+};
+
+// Stop the server
+Server.prototype.stop = function() {
+ var that = this;
+ if (!this.isRunning()) return Q();
+
+ var d = Q.defer();
+ this.running.close(function(err) {
+ that.running = null;
+ that.emit("state", false);
+
+ if (err) d.reject(err);
+ else d.resolve();
+ });
+
+ for (var i = 0; i < this.sockets.length; i++) {
+ this.sockets[i].destroy();
+ }
+
+ return d.promise;
+};
+
+Server.prototype.start = function(dir, port) {
+ var that = this, pre = Q();
+ port = port || 8004;
+
+ if (that.isRunning()) pre = this.stop();
+ return pre
+ .then(function() {
+ var d = Q.defer();
+
+ that.running = http.createServer(function(req, res){
+ // Render error
+ function error(err) {
+ res.statusCode = err.status || 500;
+ res.end(err.message);
+ }
+
+ // Redirect to directory's index.html
+ function redirect() {
+ res.statusCode = 301;
+ res.setHeader('Location', req.url + '/');
+ res.end('Redirecting to ' + req.url + '/');
+ }
+
+ // Send file
+ send(req, url.parse(req.url).pathname)
+ .root(dir)
+ .on('error', error)
+ .on('directory', redirect)
+ .pipe(res);
+ });
+
+ that.running.on('connection', function (socket) {
+ that.sockets.push(socket);
+ socket.setTimeout(4000);
+ socket.on('close', function () {
+ that.sockets.splice(that.sockets.indexOf(socket), 1);
+ });
+ });
+
+ that.running.listen(port, function(err) {
+ if (err) return d.reject(err);
+
+ that.port = port;
+ that.dir = dir;
+ that.emit("state", true);
+ d.resolve();
+ });
+
+ return d.promise;
+ });
+}
+
+module.exports = Server;
diff --git a/lib/utils/string.js b/lib/utils/string.js
index 54c4c66..588f4d9 100644
--- a/lib/utils/string.js
+++ b/lib/utils/string.js
@@ -20,7 +20,13 @@ function optionsToShellArgs(options) {
.join(" ");
}
+function escapeRegex(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+}
+
module.exports = {
+ escapeRegex: escapeRegex,
escapeShellArg: escapeShellArg,
- optionsToShellArgs: optionsToShellArgs
+ optionsToShellArgs: optionsToShellArgs,
+ toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
};
diff --git a/lib/utils/watch.js b/lib/utils/watch.js
new file mode 100644
index 0000000..795bbb7
--- /dev/null
+++ b/lib/utils/watch.js
@@ -0,0 +1,38 @@
+var Q = require('q');
+var _ = require('lodash');
+var path = require('path');
+var Gaze = require('gaze').Gaze;
+
+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 gaze = new Gaze(toWatch, {
+ cwd: dir
+ });
+
+ gaze.once("all", function(e, filepath) {
+ gaze.close();
+
+ d.resolve(filepath);
+ });
+ gaze.once("error", function(err) {
+ gaze.close();
+
+ d.reject(err);
+ });
+
+ return d.promise;
+}
+
+module.exports = watch;