summaryrefslogtreecommitdiffstats
path: root/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils')
-rw-r--r--lib/utils/batch.js52
-rw-r--r--lib/utils/command.js80
-rw-r--r--lib/utils/error.js105
-rw-r--r--lib/utils/fs.js225
-rw-r--r--lib/utils/git.js168
-rw-r--r--lib/utils/i18n.js80
-rw-r--r--lib/utils/images.js71
-rw-r--r--lib/utils/location.js (renamed from lib/utils/links.js)49
-rw-r--r--lib/utils/logger.js188
-rw-r--r--lib/utils/navigation.js79
-rw-r--r--lib/utils/page.js397
-rw-r--r--lib/utils/path.js36
-rw-r--r--lib/utils/progress.js55
-rw-r--r--lib/utils/promise.js62
-rw-r--r--lib/utils/server.js94
-rw-r--r--lib/utils/string.js27
-rw-r--r--lib/utils/watch.js40
17 files changed, 599 insertions, 1209 deletions
diff --git a/lib/utils/batch.js b/lib/utils/batch.js
deleted file mode 100644
index 9069766..0000000
--- a/lib/utils/batch.js
+++ /dev/null
@@ -1,52 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-
-// Execute a method for all element
-function execEach(items, options) {
- if (_.size(items) === 0) return Q();
- var concurrents = 0, d = Q.defer(), pending = [];
-
- options = _.defaults(options || {}, {
- max: 100,
- fn: function() {}
- });
-
-
- function startItem(item, i) {
- if (concurrents >= options.max) {
- pending.push([item, i]);
- return;
- }
-
- concurrents++;
- Q()
- .then(function() {
- return options.fn(item, i);
- })
- .then(function() {
- concurrents--;
-
- // Next pending
- var next = pending.shift();
-
- if (concurrents === 0 && !next) {
- d.resolve();
- } else if (next) {
- startItem.apply(null, next);
- }
- })
- .fail(function(err) {
- pending = [];
- d.reject(err);
- });
- }
-
- _.each(items, startItem);
-
- return d.promise;
-}
-
-module.exports = {
- execEach: execEach
-};
-
diff --git a/lib/utils/command.js b/lib/utils/command.js
new file mode 100644
index 0000000..de240df
--- /dev/null
+++ b/lib/utils/command.js
@@ -0,0 +1,80 @@
+var _ = require('lodash');
+var childProcess = require('child_process');
+var spawn = require("spawn-cmd").spawn;
+var Promise = require('./promise');
+
+// Execute a command
+function exec(command, options) {
+ var d = Promise.defer();
+
+ var child = childProcess.exec(command, options, function(err, stdout, stderr) {
+ if (!err) {
+ return d.resolve();
+ }
+
+ err.message = stdout.toString('utf8') + stderr.toString('utf8');
+ d.reject(err);
+ });
+
+ child.stdout.on('data', function (data) {
+ d.notify(data);
+ });
+
+ child.stderr.on('data', function (data) {
+ d.notify(data);
+ });
+
+ return d.promise;
+}
+
+// Spawn an executable
+function spawnCmd(command, args, options) {
+ var d = Promise.defer();
+ var child = spawn(command, args, options);
+
+ child.on('error', function(error) {
+ return d.reject(error);
+ });
+
+ child.stdout.on('data', function (data) {
+ d.notify(data);
+ });
+
+ child.stderr.on('data', function (data) {
+ d.notify(data);
+ });
+
+ child.on('close', function(code) {
+ if (code === 0) {
+ d.resolve();
+ } else {
+ d.reject(new Error('Error with command "'+command+'"'));
+ }
+ });
+
+ return d.promise;
+}
+
+// Transform an option object to a command line string
+function escapeShellArg(s) {
+ s = s.replace(/"/g, '\\"');
+ return '"' + s + '"';
+}
+
+function optionsToShellArgs(options) {
+ return _.chain(options)
+ .map(function(value, key) {
+ if (value === null || value === undefined || value === false) return null;
+ if (value === true) return key;
+ return key + '=' + escapeShellArg(value);
+ })
+ .compact()
+ .value()
+ .join(' ');
+}
+
+module.exports = {
+ exec: exec,
+ spawn: spawnCmd,
+ optionsToShellArgs: optionsToShellArgs
+};
diff --git a/lib/utils/error.js b/lib/utils/error.js
new file mode 100644
index 0000000..27fa59d
--- /dev/null
+++ b/lib/utils/error.js
@@ -0,0 +1,105 @@
+var _ = require('lodash');
+var TypedError = require('error/typed');
+var WrappedError = require('error/wrapped');
+var deprecated = require('deprecated');
+
+var Logger = require('./logger');
+
+var log = new Logger();
+
+// Enforce as an Error object, and cleanup message
+function enforce(err) {
+ if (_.isString(err)) err = new Error(err);
+ err.message = err.message.replace(/^Error: /, '');
+
+ return err;
+}
+
+// Random error wrappers during parsing/generation
+var ParsingError = WrappedError({
+ message: 'Parsing Error: {origMessage}',
+ type: 'parse'
+});
+var OutputError = WrappedError({
+ message: 'Output Error: {origMessage}',
+ type: 'generate'
+});
+
+// A file does not exists
+var FileNotFoundError = TypedError({
+ type: 'file.not-found',
+ message: 'No "{filename}" file (or is ignored)',
+ filename: null
+});
+
+// A file is outside the scope
+var FileOutOfScopeError = TypedError({
+ type: 'file.out-of-scope',
+ message: '"{filename}" not in "{root}"',
+ filename: null,
+ root: null,
+ code: 'EACCESS'
+});
+
+// A file is outside the scope
+var RequireInstallError = TypedError({
+ type: 'install.required',
+ message: '"{cmd}" is not installed.\n{install}',
+ cmd: null,
+ code: 'ENOENT',
+ install: ''
+});
+
+// Error for nunjucks templates
+var TemplateError = WrappedError({
+ message: 'Error compiling template "{filename}": {origMessage}',
+ type: 'template',
+ filename: null
+});
+
+// Error for nunjucks templates
+var PluginError = WrappedError({
+ message: 'Error with plugin "{plugin}": {origMessage}',
+ type: 'plugin',
+ plugin: null
+});
+
+// Error with the book's configuration
+var ConfigurationError = WrappedError({
+ message: 'Error with book\'s configuration: {origMessage}',
+ type: 'configuration'
+});
+
+// Error during ebook generation
+var EbookError = WrappedError({
+ message: 'Error during ebook generation: {origMessage}\n{stdout}',
+ type: 'ebook',
+ stdout: ''
+});
+
+// Deprecate methods/fields
+function deprecateMethod(fn, msg) {
+ return deprecated.method(msg, log.warn.ln, fn);
+}
+function deprecateField(obj, prop, value, msg) {
+ return deprecated.field(msg, log.warn.ln, obj, prop, value);
+}
+
+module.exports = {
+ enforce: enforce,
+
+ ParsingError: ParsingError,
+ OutputError: OutputError,
+ RequireInstallError: RequireInstallError,
+
+ FileNotFoundError: FileNotFoundError,
+ FileOutOfScopeError: FileOutOfScopeError,
+
+ TemplateError: TemplateError,
+ PluginError: PluginError,
+ ConfigurationError: ConfigurationError,
+ EbookError: EbookError,
+
+ deprecateMethod: deprecateMethod,
+ deprecateField: deprecateField
+};
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index b82701f..42fd3c6 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -1,71 +1,36 @@
-var _ = require('lodash');
-var Q = require('q');
+var fs = require('graceful-fs');
+var mkdirp = require('mkdirp');
+var destroy = require('destroy');
+var rmdir = require('rmdir');
var tmp = require('tmp');
+var request = require('request');
var path = require('path');
-var fs = require('graceful-fs');
-var fsExtra = require('fs-extra');
-var Ignore = require('fstream-ignore');
-
-var fsUtils = {
- tmp: {
- file: function(opt) {
- return Q.nfcall(tmp.file.bind(tmp), opt).get(0);
- },
- dir: function() {
- return Q.nfcall(tmp.dir.bind(tmp)).get(0);
- }
- },
- list: listFiles,
- stat: Q.denodeify(fs.stat),
- readdir: Q.denodeify(fs.readdir),
- readFile: Q.denodeify(fs.readFile),
- writeFile: writeFile,
- writeStream: writeStream,
- mkdirp: Q.denodeify(fsExtra.mkdirp),
- copy: Q.denodeify(fsExtra.copy),
- remove: Q.denodeify(fsExtra.remove),
- symlink: Q.denodeify(fsExtra.symlink),
- exists: function(path) {
- var d = Q.defer();
- fs.exists(path, d.resolve);
- return d.promise;
- },
- findFile: findFile,
- existsSync: fs.existsSync.bind(fs),
- readFileSync: fs.readFileSync.bind(fs),
- clean: cleanFolder,
- getUniqueFilename: getUniqueFilename
-};
-
-// Write a file
-function writeFile(filename, data, options) {
- var d = Q.defer();
-
- try {
- fs.writeFileSync(filename, data, options);
- } catch(err) {
- d.reject(err);
- }
- d.resolve();
-
+var cp = require('cp');
+var cpr = require('cpr');
- return d.promise;
-}
+var Promise = require('./promise');
// Write a stream to a file
function writeStream(filename, st) {
- var d = Q.defer();
+ var d = Promise.defer();
var wstream = fs.createWriteStream(filename);
+ var cleanup = function() {
+ destroy(wstream);
+ wstream.removeAllListeners();
+ };
wstream.on('finish', function () {
+ cleanup();
d.resolve();
});
wstream.on('error', function (err) {
+ cleanup();
d.reject(err);
});
st.on('error', function(err) {
+ cleanup();
d.reject(err);
});
@@ -74,120 +39,80 @@ function writeStream(filename, st) {
return d.promise;
}
-// Find a filename available
-function getUniqueFilename(base, filename) {
- if (!filename) {
- filename = base;
- base = '/';
- }
+// Return a promise resolved with a boolean
+function fileExists(filename) {
+ var d = Promise.defer();
- filename = path.resolve(base, filename);
+ fs.exists(filename, function(exists) {
+ d.resolve(exists);
+ });
+
+ return d.promise;
+}
+
+// Generate temporary file
+function genTmpFile(opts) {
+ return Promise.nfcall(tmp.file, opts)
+ .get(0);
+}
+
+// Generate temporary dir
+function genTmpDir(opts) {
+ return Promise.nfcall(tmp.dir, opts)
+ .get(0);
+}
+
+// Download an image
+function download(uri, dest) {
+ return writeStream(dest, request(uri));
+}
+
+// Find a filename available in a folder
+function uniqueFilename(base, filename) {
var ext = path.extname(filename);
+ filename = path.resolve(base, filename);
filename = path.join(path.dirname(filename), path.basename(filename, ext));
var _filename = filename+ext;
var i = 0;
while (fs.existsSync(filename)) {
- _filename = filename+'_'+i+ext;
+ _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;
+ return Promise(path.relative(base, _filename));
}
-// 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;
- });
+// Create all required folder to create a file
+function ensureFile(filename) {
+ var base = path.dirname(filename);
+ return Promise.nfcall(mkdirp, base);
}
-// Find a file in a folder (case incensitive)
-// Return the real filename
-function findFile(root, filename) {
- return Q.nfcall(fs.readdir, root)
- .then(function(files) {
- return _.find(files, function(file) {
- return (file.toLowerCase() == filename.toLowerCase());
- });
+// Remove a folder
+function rmDir(base) {
+ return Promise.nfcall(rmdir, base, {
+ fs: fs
});
}
-module.exports = fsUtils;
+module.exports = {
+ exists: fileExists,
+ existsSync: fs.existsSync,
+ mkdirp: Promise.nfbind(mkdirp),
+ readFile: Promise.nfbind(fs.readFile),
+ writeFile: Promise.nfbind(fs.writeFile),
+ stat: Promise.nfbind(fs.stat),
+ statSync: fs.statSync,
+ readdir: Promise.nfbind(fs.readdir),
+ writeStream: writeStream,
+ copy: Promise.nfbind(cp),
+ copyDir: Promise.nfbind(cpr),
+ tmpFile: genTmpFile,
+ tmpDir: genTmpDir,
+ download: download,
+ uniqueFilename: uniqueFilename,
+ ensure: ensureFile,
+ rmDir: rmDir
+};
diff --git a/lib/utils/git.js b/lib/utils/git.js
index 72c8818..52b1096 100644
--- a/lib/utils/git.js
+++ b/lib/utils/git.js
@@ -1,127 +1,129 @@
-var Q = require('q');
var _ = require('lodash');
var path = require('path');
var crc = require('crc');
-var exec = Q.denodeify(require('child_process').exec);
var URI = require('urijs');
-var pathUtil = require('./path');
+var pathUtil = require('./path');
+var Promise = require('./promise');
+var command = require('./command');
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);
+function Git() {
+ this.tmpDir;
+ this.cloned = {};
}
-// 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 an unique ID for a combinaison host/ref
+Git.prototype.repoID = function(host, ref) {
+ return crc.crc32(host+'#'+(ref || '')).toString(16);
+};
- return {
- host: uri.toString(),
- ref: ref || 'master',
- filepath: filepath
- };
-}
+// Allocate a temporary folder for cloning repos in it
+Git.prototype.allocateDir = function() {
+ var that = this;
-// Clone a git repo from a specific ref
-function cloneGitRepo(host, ref) {
- var isBranch = false;
+ if (this.tmpDir) return Promise();
- ref = ref || 'master';
- if (!validateSha(ref)) isBranch = true;
+ return fs.tmpDir()
+ .then(function(dir) {
+ that.tmpDir = dir;
+ });
+};
- return Q()
+// Clone a git repository if non existant
+Git.prototype.clone = function(host, ref) {
+ var that = this;
- // Create temporary folder to store git repos
- .then(function() {
- if (GIT_TMP) return;
- return fs.tmp.dir()
- .then(function(_tmp) {
- GIT_TMP = _tmp;
- });
- })
+ return this.allocateDir()
// Return or clone the git repo
.then(function() {
// Unique ID for repo/ref combinaison
- var repoId = crc.crc32(host+'#'+ref).toString(16);
+ var repoId = that.repoID(host, ref);
// 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);
+ var repoPath = path.join(that.tmpDir, repoId);
+
+ if (that.cloned[repoId]) return repoPath;
+
+ // Clone repo
+ return command.exec('git clone '+host+' '+repoPath)
+
+ // Checkout reference if specified
+ .then(function() {
+ that.cloned[repoId] = true;
+
+ if (!ref) return;
+ return command.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);
+Git.prototype.resolve = function(giturl) {
+ // Path to a file in a git repo?
+ if (!Git.isUrl(giturl)) {
+ if (this.resolveRoot(giturl)) return Promise(giturl);
+ return Promise(null);
+ }
+ if (_.isString(giturl)) giturl = Git.parseUrl(giturl);
+ if (!giturl) return Promise(null);
// Clone or get from cache
- return cloneGitRepo(giturl.host, giturl.ref)
+ return this.clone(giturl.host, giturl.ref)
.then(function(repo) {
-
- // Resolve relative path
return path.resolve(repo, giturl.filepath);
});
-}
+};
// Return root of git repo from a filepath
-function resolveGitRoot(filepath) {
+Git.prototype.resolveRoot = function(filepath) {
var relativeToGit, repoId;
// No git repo cloned, or file is not in a git repository
- if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
+ if (!this.tmpDir || !pathUtil.isInRoot(this.tmpDir, filepath)) return null;
// Extract first directory (is the repo id)
- relativeToGit = path.relative(GIT_TMP, filepath);
+ relativeToGit = path.relative(this.tmpDir, filepath);
repoId = _.first(relativeToGit.split(path.sep));
if (!repoId) return;
// Return an absolute file
- return path.resolve(GIT_TMP, repoId);
-}
+ return path.resolve(this.tmpDir, repoId);
+};
+
+// Check if an url is a git dependency url
+Git.isUrl = function(giturl) {
+ return (giturl.indexOf(GIT_PREFIX) === 0);
+};
+
+// Parse and extract infos
+Git.parseUrl = function(giturl) {
+ var ref, uri, fileParts, filepath;
+ if (!Git.isUrl(giturl)) return null;
+ giturl = giturl.slice(GIT_PREFIX.length);
+
+ uri = new URI(giturl);
+ ref = uri.fragment() || null;
+ 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);
-module.exports = {
- checkUrl: checkGitUrl,
- parseUrl: parseGitUrl,
- resolveFile: resolveFileFromGit,
- resolveRoot: resolveGitRoot
+ // Recreate pathname without the real filename
+ uri.path(_.first(fileParts)+'.git');
+
+ return {
+ host: uri.toString(),
+ ref: ref,
+ filepath: filepath
+ };
};
+
+module.exports = Git;
diff --git a/lib/utils/i18n.js b/lib/utils/i18n.js
deleted file mode 100644
index de64b49..0000000
--- a/lib/utils/i18n.js
+++ /dev/null
@@ -1,80 +0,0 @@
-var _ = require('lodash');
-var path = require('path');
-var fs = require('fs');
-
-var i18n = require('i18n');
-
-var I18N_PATH = path.resolve(__dirname, '../../theme/i18n/');
-var DEFAULT_LANGUAGE = 'en';
-var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) {
- return path.basename(lang, '.json');
-});
-
-i18n.configure({
- locales: LOCALES,
- directory: I18N_PATH,
- defaultLocale: DEFAULT_LANGUAGE,
- updateFiles: false
-});
-
-function compareLocales(lang, locale) {
- var langMain = _.first(lang.split('-'));
- var langSecond = _.last(lang.split('-'));
-
- var localeMain = _.first(locale.split('-'));
- var localeSecond = _.last(locale.split('-'));
-
- if (locale == lang) return 100;
- if (localeMain == langMain) return 50;
- if (localeSecond == langSecond) return 20;
- return 0;
-}
-
-var normalizeLanguage = _.memoize(function(lang) {
- var language = _.chain(LOCALES)
- .values()
- .map(function(locale) {
- return {
- locale: locale,
- score: compareLocales(lang, locale)
- };
- })
- .filter(function(lang) {
- return lang.score > 0;
- })
- .sortBy('score')
- .pluck('locale')
- .last()
- .value();
- return language || lang;
-});
-
-function translate(locale, phrase) {
- var args = Array.prototype.slice.call(arguments, 2);
-
- return i18n.__.apply({}, [{
- locale: locale,
- phrase: phrase
- }].concat(args));
-}
-
-function getCatalog(locale) {
- locale = normalizeLanguage(locale);
- return i18n.getCatalog(locale);
-}
-
-function getLocales() {
- return LOCALES;
-}
-
-function hasLocale(locale) {
- return _.contains(LOCALES, locale);
-}
-
-module.exports = {
- __: translate,
- normalizeLanguage: normalizeLanguage,
- getCatalog: getCatalog,
- getLocales: getLocales,
- hasLocale: hasLocale
-};
diff --git a/lib/utils/images.js b/lib/utils/images.js
index a82b0a1..e387d6b 100644
--- a/lib/utils/images.js
+++ b/lib/utils/images.js
@@ -1,37 +1,44 @@
-var _ = require("lodash");
-var Q = require("q");
-var fs = require("./fs");
-var spawn = require("spawn-cmd").spawn;
-
-// Convert a svg file
-var convertSVG = function(source, dest, options) {
- if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source));
- var d = Q.defer();
-
- options = _.defaults(options || {}, {
-
- });
-
- //var command = shellescape(["svgexport", source, dest]);
- var child = spawn("svgexport", [source, dest]);
+var Promise = require('./promise');
+var command = require('./command');
+var fs = require('./fs');
+var error = require('./error');
+
+// Convert a svg file to a pmg
+function convertSVGToPNG(source, dest, options) {
+ if (!fs.existsSync(source)) return Promise.reject(new error.FileNotFoundError({ filename: source }));
+
+ return command.spawn('svgexport', [source, dest])
+ .fail(function(err) {
+ if (err.code == 'ENOENT') {
+ err = error.RequireInstallError({
+ cmd: 'svgexport',
+ install: 'Install it using: "npm install svgexport -g"'
+ });
+ }
+ throw err;
+ })
+ .then(function() {
+ if (fs.existsSync(dest)) return;
- child.on("error", function(error) {
- if (error.code == "ENOENT") error = new Error("Need to install \"svgexport\" using \"npm install svgexport -g\"");
- return d.reject(error);
+ throw new Error('Error converting '+source+' into '+dest);
});
-
- child.on("close", function(code) {
- if (code === 0 && fs.existsSync(dest)) {
- d.resolve();
- } else {
- d.reject(new Error("Error converting "+source+" into "+dest));
- }
+}
+
+// Convert a svg buffer to a png file
+function convertSVGBufferToPNG(buf, dest) {
+ // Create a temporary SVG file to convert
+ return fs.tmpFile({
+ postfix: '.svg'
+ })
+ .then(function(tmpSvg) {
+ return fs.writeFile(tmpSvg, buf)
+ .then(function() {
+ return convertSVGToPNG(tmpSvg, dest);
+ });
});
-
- return d.promise;
-};
+}
module.exports = {
- convertSVG: convertSVG,
- INVALID: [".svg"]
-};
+ convertSVGToPNG: convertSVGToPNG,
+ convertSVGBufferToPNG: convertSVGBufferToPNG
+}; \ No newline at end of file
diff --git a/lib/utils/links.js b/lib/utils/location.js
index 5122396..ba0c57d 100644
--- a/lib/utils/links.js
+++ b/lib/utils/location.js
@@ -1,7 +1,7 @@
var url = require('url');
var path = require('path');
-// Is the link an external link
+// Is the url an external url
function isExternal(href) {
try {
return Boolean(url.parse(href).protocol);
@@ -10,15 +10,9 @@ function isExternal(href) {
}
}
-// Return true if the link is relative
+// Inverse of isExternal
function isRelative(href) {
- try {
- var parsed = url.parse(href);
-
- return !!(!parsed.protocol && parsed.path);
- } catch(err) {
- return true;
- }
+ return !isExternal(href);
}
// Return true if the link is an achor
@@ -32,15 +26,20 @@ function isAnchor(href) {
}
// Normalize a path to be a link
-function normalizeLink(s) {
+function normalize(s) {
return s.replace(/\\/g, '/');
}
-// Relative to absolute path
+// Convert relative to absolute path
// dir: directory parent of the file currently in rendering process
// outdir: directory parent from the html output
function toAbsolute(_href, dir, outdir) {
if (isExternal(_href)) return _href;
+ outdir = outdir == undefined? dir : outdir;
+
+ _href = normalize(_href);
+ dir = normalize(dir);
+ outdir = normalize(outdir);
// Path "_href" inside the base folder
var hrefInRoot = path.normalize(path.join(dir, _href));
@@ -50,32 +49,22 @@ function toAbsolute(_href, dir, outdir) {
_href = path.relative(outdir, hrefInRoot);
// Normalize windows paths
- _href = normalizeLink(_href);
+ _href = normalize(_href);
return _href;
}
-// Join links
-function join() {
- var _href = path.join.apply(path, arguments);
-
- return normalizeLink(_href);
-};
-
-// Change extension
-function changeExtension(filename, newext) {
- return path.join(
- path.dirname(filename),
- path.basename(filename, path.extname(filename))+newext
- );
+// Convert an absolute path to a relative path for a specific folder (dir)
+// ('test/', 'hello.md') -> '../hello.md'
+function relative(dir, file) {
+ return normalize(path.relative(dir, file));
}
module.exports = {
- isAnchor: isAnchor,
- isRelative: isRelative,
isExternal: isExternal,
+ isRelative: isRelative,
+ isAnchor: isAnchor,
+ normalize: normalize,
toAbsolute: toAbsolute,
- join: join,
- changeExtension: changeExtension,
- normalize: normalizeLink
+ relative: relative
};
diff --git a/lib/utils/logger.js b/lib/utils/logger.js
index db3d90e..60215af 100644
--- a/lib/utils/logger.js
+++ b/lib/utils/logger.js
@@ -1,6 +1,6 @@
-var _ = require("lodash");
-var util = require("util");
-var color = require("bash-color");
+var _ = require('lodash');
+var util = require('util');
+var color = require('bash-color');
var LEVELS = {
DEBUG: 0,
@@ -17,86 +17,112 @@ var COLORS = {
ERROR: color.red
};
-module.exports = function(_write, logLevel) {
- var logger = {};
- var lastChar = "\n";
- if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
+function Logger(write, logLevel, prefix) {
+ if (!(this instanceof Logger)) return new Logger(write, logLevel);
+
+ this._write = write || function(msg) { process.stdout.write(msg); };
+ this.lastChar = '\n';
+
+ // Define log level
+ this.setLevel(logLevel);
+
+ _.bindAll(this);
- // 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) {
+ // Create easy-to-use method like "logger.debug.ln('....')"
+ _.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;
- });
- };
- });
+ this[levelKey] = _.partial(this.log, level);
+ this[levelKey].ln = _.partial(this.logLn, level);
+ this[levelKey].ok = _.partial(this.ok, level);
+ this[levelKey].fail = _.partial(this.fail, level);
+ this[levelKey].promise = _.partial(this.promise, level);
+ }, this);
+}
+
+// Create a new logger prefixed from this logger
+Logger.prototype.prefix = function(prefix) {
+ return (new Logger(this._write, this.logLevel, prefix));
+};
+
+// Change minimum level
+Logger.prototype.setLevel = function(logLevel) {
+ if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
+ this.logLevel = logLevel;
+};
+
+// Print a simple string
+Logger.prototype.write = function(msg) {
+ msg = msg.toString();
+ this.lastChar = _.last(msg);
+ return this._write(msg);
+};
+
+// Format a string using the first argument as a printf-like format.
+Logger.prototype.format = function() {
+ return util.format.apply(util, arguments);
+};
+
+// Print a line
+Logger.prototype.writeLn = function(msg) {
+ return this.write((msg || '')+'\n');
+};
- return logger;
+// Log/Print a message if level is allowed
+Logger.prototype.log = function(level) {
+ if (level < this.logLevel) return;
+
+ var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = this.format.apply(this, args);
+
+ if (this.lastChar == '\n') {
+ msg = COLORS[levelKey](levelKey.toLowerCase()+':')+' '+msg;
+ }
+
+ return this.write(msg);
+};
+
+// Log/Print a line if level is allowed
+Logger.prototype.logLn = function() {
+ if (this.lastChar != '\n') this.write('\n');
+
+ var args = Array.prototype.slice.apply(arguments);
+ args.push('\n');
+ return this.log.apply(this, args);
+};
+
+// Log a confirmation [OK]
+Logger.prototype.ok = function(level) {
+ var args = Array.prototype.slice.apply(arguments, [1]);
+ var msg = this.format.apply(this, args);
+ if (arguments.length > 1) {
+ this.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
+ } else {
+ this.log(level, color.green('OK'), '\n');
+ }
};
-module.exports.LEVELS = LEVELS;
-module.exports.COLORS = COLORS;
+
+// Log a "FAIL"
+Logger.prototype.fail = function(level) {
+ return this.log(level, color.red('ERROR') + '\n');
+};
+
+// Log state of a promise
+Logger.prototype.promise = function(level, p) {
+ var that = this;
+
+ return p.
+ then(function(st) {
+ that.ok(level);
+ return st;
+ }, function(err) {
+ that.fail(level);
+ throw err;
+ });
+};
+
+Logger.LEVELS = LEVELS;
+Logger.COLORS = COLORS;
+
+module.exports = Logger;
diff --git a/lib/utils/navigation.js b/lib/utils/navigation.js
deleted file mode 100644
index d07eb35..0000000
--- a/lib/utils/navigation.js
+++ /dev/null
@@ -1,79 +0,0 @@
-var _ = require("lodash");
-
-// Cleans up an article/chapter object
-// remove "articles" attributes
-function clean(obj) {
- return obj && _.omit(obj, ["articles"]);
-}
-
-function flattenChapters(chapters) {
- return _.reduce(chapters, function(accu, chapter) {
- return accu.concat([clean(chapter)].concat(flattenChapters(chapter.articles)));
- }, []);
-}
-
-// Returns from a summary a map of
-/*
- {
- "file/path.md": {
- prev: ...,
- next: ...,
- },
- ...
- }
-*/
-function navigation(summary, files) {
- // Support single files as well as list
- files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
-
- // List of all navNodes
- // Flatten chapters
- var navNodes = flattenChapters(summary.chapters);
-
- // Mapping of prev/next for a give path
- var mapping = _.chain(navNodes)
- .map(function(current, i) {
- var prev = null, next = null;
-
- // Skip if no path
- if(!current.exists) return null;
-
- // Find prev
- prev = _.chain(navNodes.slice(0, i))
- .reverse()
- .find(function(node) {
- return node.exists && !node.external;
- })
- .value();
-
- // Find next
- next = _.chain(navNodes.slice(i+1))
- .find(function(node) {
- return node.exists && !node.external;
- })
- .value();
-
- return [current.path, {
- index: i,
- title: current.title,
- introduction: current.introduction,
- prev: prev,
- next: next,
- level: current.level,
- }];
- })
- .compact()
- .object()
- .value();
-
- // Filter for only files we want
- if(files) {
- return _.pick(mapping, files);
- }
-
- return mapping;
-}
-
-
-// Exports
-module.exports = navigation;
diff --git a/lib/utils/page.js b/lib/utils/page.js
deleted file mode 100644
index 010d703..0000000
--- a/lib/utils/page.js
+++ /dev/null
@@ -1,397 +0,0 @@
-var Q = require('q');
-var _ = require('lodash');
-var url = require('url');
-var path = require('path');
-var cheerio = require('cheerio');
-var domSerializer = require('dom-serializer');
-var request = require('request');
-var crc = require('crc');
-var slug = require('github-slugid');
-
-var links = require('./links');
-var imgUtils = require('./images');
-var fs = require('./fs');
-var batch = require('./batch');
-
-var parsableExtensions = require('gitbook-parsers').extensions;
-
-// Map of images that have been converted
-var imgConversionCache = {};
-
-// Render a cheerio dom as html
-function renderDom($, dom, options) {
- if (!dom && $._root && $._root.children) {
- dom = $._root.children;
- }
-
- options = options|| dom.options || $._options;
- return domSerializer(dom, options);
-}
-
-function replaceText($, el, search, replace, text_only ) {
- return $(el).each(function(){
- var node = this.firstChild,
- val,
- new_val,
-
- // Elements to be removed at the end.
- remove = [];
-
- // Only continue if firstChild exists.
- if ( node ) {
-
- // Loop over all childNodes.
- while (node) {
-
- // Only process text nodes.
- if ( node.nodeType === 3 ) {
-
- // The original node value.
- val = node.nodeValue;
-
- // The new value.
- new_val = val.replace( search, replace );
-
- // Only replace text if the new value is actually different!
- if ( new_val !== val ) {
-
- if ( !text_only && /</.test( new_val ) ) {
- // The new value contains HTML, set it in a slower but far more
- // robust way.
- $(node).before( new_val );
-
- // Don't remove the node yet, or the loop will lose its place.
- remove.push( node );
- } else {
- // The new value contains no HTML, so it can be set in this
- // very fast, simple way.
- node.nodeValue = new_val;
- }
- }
- }
-
- node = node.nextSibling;
- }
- }
-
- // Time to remove those elements!
- if (remove.length) $(remove).remove();
- });
-}
-
-function pregQuote( str ) {
- return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
-}
-
-
-// Adapt an html snippet to be relative to a base folder
-function normalizeHtml(src, options) {
- var $ = cheerio.load(src, {
- // We should parse html without trying to normalize too much
- xmlMode: false,
-
- // SVG need some attributes to use uppercases
- lowerCaseAttributeNames: false,
- lowerCaseTags: false
- });
- var toConvert = [];
- var svgContent = {};
- var outputRoot = options.book.options.output;
-
- imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {};
-
- // Find svg images to extract and process
- if (options.convertImages) {
- $('svg').each(function() {
- var content = renderDom($, $(this));
- var svgId = _.uniqueId('svg');
- var dest = svgId+'.svg';
-
- // Generate filename
- dest = '/'+fs.getUniqueFilename(outputRoot, dest);
-
- svgContent[dest] = '<?xml version="1.0" encoding="UTF-8"?>'+content;
- $(this).replaceWith($('<img>').attr('src', dest));
- });
- }
-
- // Generate ID for headings
- $('h1,h2,h3,h4,h5,h6').each(function() {
- if ($(this).attr('id')) return;
-
- $(this).attr('id', slug($(this).text()));
- });
-
- // Find images to normalize
- $('img').each(function() {
- var origin;
- var src = $(this).attr('src');
-
- if (!src) return;
- var isExternal = links.isExternal(src);
-
- // Transform as relative to the bases
- if (links.isRelative(src)) {
- src = links.toAbsolute(src, options.base, options.output);
- }
-
- // Convert if needed
- if (options.convertImages) {
- // If image is external and ebook, then downlaod the images
- if (isExternal) {
- origin = src;
- src = '/'+crc.crc32(origin).toString(16)+path.extname(url.parse(origin).pathname);
- src = links.toAbsolute(src, options.base, options.output);
- isExternal = false;
- }
-
- var ext = path.extname(src);
- var srcAbs = links.join('/', options.base, src);
-
- // Test image extension
- if (_.contains(imgUtils.INVALID, ext)) {
- if (imgConversionCache[outputRoot][srcAbs]) {
- // Already converted
- src = imgConversionCache[outputRoot][srcAbs];
- } else {
- // Not converted yet
- var dest = '';
-
- // Replace extension
- dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+'.png');
- dest = dest[0] == '/'? dest.slice(1) : dest;
-
- // Get a name that doesn't exists
- dest = fs.getUniqueFilename(outputRoot, dest);
-
- options.book.log.debug.ln('detect invalid image (will be converted to png):', srcAbs);
-
- // Add to cache
- imgConversionCache[outputRoot][srcAbs] = '/'+dest;
-
- // Push to convert
- toConvert.push({
- origin: origin,
- content: svgContent[srcAbs],
- source: isExternal? srcAbs : path.join('./', srcAbs),
- dest: path.join('./', dest)
- });
-
- src = links.join('/', dest);
- }
-
- // Reset as relative to output
- src = links.toAbsolute(src, options.base, options.output);
- }
-
- else if (origin) {
- // Need to downlaod image
- toConvert.push({
- origin: origin,
- source: path.join('./', srcAbs)
- });
- }
- }
-
- $(this).attr('src', src);
- });
-
- // Normalize links
- $('a').each(function() {
- var href = $(this).attr('href');
- if (!href) return;
-
- if (links.isAnchor(href)) {
- // Keep it as it is
- } else if (links.isRelative(href)) {
- var parts = url.parse(href);
-
- var pathName = decodeURIComponent(parts.pathname);
- var anchor = parts.hash || '';
-
- // Calcul absolute path for this file (without the anchor)
- var absolutePath = links.join(options.base, pathName);
-
- // If is in navigation relative: transform as content
- if (options.navigation[absolutePath]) {
- absolutePath = options.book.contentLink(absolutePath);
- }
-
- // If md/adoc/rst files is not in summary
- // or for ebook, signal all files that are outside the summary
- else if (_.contains(parsableExtensions, path.extname(absolutePath)) ||
- _.contains(['epub', 'pdf', 'mobi'], options.book.options.generator)) {
- options.book.log.warn.ln('page', options.input, 'contains an hyperlink to resource outside spine \''+href+'\'');
- }
-
- // Transform as absolute
- href = links.toAbsolute('/'+absolutePath, options.base, options.output)+anchor;
- } else {
- // External links
- $(this).attr('target', '_blank');
- }
-
- // Transform extension
- $(this).attr('href', href);
- });
-
- // Highlight code blocks
- $('code').each(function() {
- // Normalize language
- var lang = _.chain(
- ($(this).attr('class') || '').split(' ')
- )
- .map(function(cl) {
- // Markdown
- if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
-
- // Asciidoc
- if (cl.search('language-') === 0) return cl.slice('language-'.length);
-
- return null;
- })
- .compact()
- .first()
- .value();
-
- var source = $(this).text();
- var blk = options.book.template.applyBlock('code', {
- body: source,
- kwargs: {
- language: lang
- }
- });
-
- if (blk.html === false) $(this).text(blk.body);
- else $(this).html(blk.body);
- });
-
- // Replace glossary terms
- var glossary = _.sortBy(options.glossary, function(term) {
- return -term.name.length;
- });
-
- _.each(glossary, function(term) {
- var r = new RegExp( '\\b(' + pregQuote(term.name.toLowerCase()) + ')\\b' , 'gi' );
- var includedInFiles = false;
-
- $('*').each(function() {
- // Ignore codeblocks
- if (_.contains(['code', 'pre', 'a', 'script'], this.name.toLowerCase())) return;
-
- replaceText($, this, r, function(match) {
- // Add to files index in glossary
- if (!includedInFiles) {
- includedInFiles = true;
- term.files = term.files || [];
- term.files.push(options.navigation[options.input]);
- }
- return '<a href=\''+links.toAbsolute('/GLOSSARY.html', options.base, options.output) + '#' + term.id+'\' class=\'glossary-term\' title=\''+_.escape(term.description)+'\'>'+match+'</a>';
- });
- });
- });
-
- return {
- html: renderDom($),
- images: toConvert
- };
-}
-
-// Convert svg images to png
-function convertImages(images, options) {
- if (!options.convertImages) return Q();
-
- var downloaded = [];
- options.book.log.debug.ln('convert ', images.length, 'images to png');
-
- return batch.execEach(images, {
- max: 100,
- fn: function(image) {
- var imgin = path.resolve(options.book.options.output, image.source);
-
- return Q()
-
- // Write image if need to be download
- .then(function() {
- if (!image.origin && !_.contains(downloaded, image.origin)) return;
- options.book.log.debug('download image', image.origin, '...');
- downloaded.push(image.origin);
- return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin)))
- .fail(function(err) {
- if (!_.isError(err)) err = new Error(err);
-
- err.message = 'Fail downloading '+image.origin+': '+err.message;
- throw err;
- });
- })
-
- // Write svg if content
- .then(function() {
- if (!image.content) return;
- return fs.writeFile(imgin, image.content);
- })
-
- // Convert
- .then(function() {
- if (!image.dest) return;
- var imgout = path.resolve(options.book.options.output, image.dest);
- options.book.log.debug('convert image', image.source, 'to', image.dest, '...');
- return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout));
- });
- }
- })
- .then(function() {
- options.book.log.debug.ok(images.length+' images converted with success');
- });
-}
-
-// Adapt page content to be relative to a base folder
-function normalizePage(sections, options) {
- options = _.defaults(options || {}, {
- // Current book
- book: null,
-
- // Do we need to convert svg?
- convertImages: false,
-
- // Current file path
- input: '.',
-
- // Navigation to use to transform path
- navigation: {},
-
- // Directory parent of the file currently in rendering process
- base: './',
-
- // Directory parent from the html output
- output: './',
-
- // Glossary terms
- glossary: []
- });
-
- // List of images to convert
- var toConvert = [];
-
- sections = _.map(sections, function(section) {
- if (section.type != 'normal') return section;
-
- var out = normalizeHtml(section.content, options);
-
- toConvert = toConvert.concat(out.images);
- section.content = out.html;
- return section;
- });
-
- return Q()
- .then(function() {
- toConvert = _.uniq(toConvert, 'source');
- return convertImages(toConvert, options);
- })
- .thenResolve(sections);
-}
-
-
-module.exports = {
- normalize: normalizePage
-};
diff --git a/lib/utils/path.js b/lib/utils/path.js
index 5285896..c233c92 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -1,5 +1,12 @@
-var _ = require("lodash");
-var path = require("path");
+var _ = require('lodash');
+var path = require('path');
+
+var error = require('./error');
+
+// Normalize a filename
+function normalizePath(filename) {
+ return path.normalize(filename);
+}
// Return true if file path is inside a folder
function isInRoot(root, filename) {
@@ -10,31 +17,42 @@ function isInRoot(root, filename) {
// Resolve paths in a specific folder
// Throw error if file is outside this folder
function resolveInRoot(root) {
- var input, result, err;
+ var input, result;
input = _.chain(arguments)
.toArray()
.slice(1)
.reduce(function(current, p) {
// Handle path relative to book root ("/README.md")
- if (p[0] == "/" || p[0] == "\\") return p.slice(1);
+ if (p[0] == '/' || p[0] == '\\') return p.slice(1);
return current? path.join(current, p) : path.normalize(p);
- }, "")
+ }, '')
.value();
result = path.resolve(root, input);
if (!isInRoot(root, result)) {
- err = new Error("EACCESS: \"" + result + "\" not in \"" + root + "\"");
- err.code = "EACCESS";
- throw err;
+ throw new error.FileOutOfScopeError({
+ filename: result,
+ root: root
+ });
}
return result;
}
+// Chnage extension
+function setExtension(filename, ext) {
+ return path.join(
+ path.dirname(filename),
+ path.basename(filename, path.extname(filename)) + ext
+ );
+}
+
module.exports = {
isInRoot: isInRoot,
- resolveInRoot: resolveInRoot
+ resolveInRoot: resolveInRoot,
+ normalize: normalizePath,
+ setExtension: setExtension
};
diff --git a/lib/utils/progress.js b/lib/utils/progress.js
deleted file mode 100644
index 8dda892..0000000
--- a/lib/utils/progress.js
+++ /dev/null
@@ -1,55 +0,0 @@
-var _ = require('lodash');
-
-// Returns from a navigation and a current file, a snapshot of current detailed state
-function calculProgress(navigation, current) {
- var n = _.size(navigation);
- var percent = 0, prevPercent = 0, currentChapter = null;
- var done = true;
-
- var chapters = _.chain(navigation)
-
- // Transform as array
- .map(function(nav, path) {
- nav.path = path;
- return nav;
- })
-
- // Sort entries
- .sortBy(function(nav) {
- return nav.index;
- })
-
- .map(function(nav, i) {
- // Calcul percent
- nav.percent = (i * 100) / Math.max((n - 1), 1);
-
- // Is it done
- nav.done = done;
- if (nav.path == current) {
- currentChapter = nav;
- percent = nav.percent;
- done = false;
- } else if (done) {
- prevPercent = nav.percent;
- }
-
- return nav;
- })
- .value();
-
- return {
- // Previous percent
- prevPercent: prevPercent,
-
- // Current percent
- percent: percent,
-
- // List of chapter with progress
- chapters: chapters,
-
- // Current chapter
- current: currentChapter
- };
-}
-
-module.exports = calculProgress;
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
new file mode 100644
index 0000000..d49cf27
--- /dev/null
+++ b/lib/utils/promise.js
@@ -0,0 +1,62 @@
+var Q = require('q');
+var _ = require('lodash');
+
+// Reduce an array to a promise
+function reduce(arr, iter, base) {
+ return _.reduce(arr, function(prev, elem, i) {
+ return prev.then(function(val) {
+ return iter(val, elem, i);
+ });
+ }, Q(base));
+}
+
+// Transform an array
+function serie(arr, iter, base) {
+ return reduce(arr, function(before, item, i) {
+ return Q(iter(item, i))
+ .then(function(r) {
+ before.push(r);
+ return before;
+ });
+ }, []);
+}
+
+// Iter over an array and return first result (not null)
+function some(arr, iter) {
+ return _.reduce(arr, function(prev, elem, i) {
+ return prev.then(function(val) {
+ if (val) return val;
+
+ return iter(elem, i);
+ });
+ }, Q());
+}
+
+// Map an array using an async (promised) iterator
+function map(arr, iter) {
+ return reduce(arr, function(prev, entry, i) {
+ return Q(iter(entry, i))
+ .then(function(out) {
+ prev.push(out);
+ return prev;
+ });
+ }, []);
+}
+
+// Wrap a fucntion in a promise
+function wrap(func) {
+ return _.wrap(func, function(_func) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return Q()
+ .then(function() {
+ return _func.apply(null, args);
+ });
+ });
+}
+
+module.exports = Q;
+module.exports.reduce = reduce;
+module.exports.map = map;
+module.exports.serie = serie;
+module.exports.some = some;
+module.exports.wrapfn = wrap;
diff --git a/lib/utils/server.js b/lib/utils/server.js
deleted file mode 100644
index 1d6822f..0000000
--- a/lib/utils/server.js
+++ /dev/null
@@ -1,94 +0,0 @@
-var Q = require("q");
-var events = require("events");
-var http = require("http");
-var send = require("send");
-var util = require("util");
-var url = require("url");
-
-var Server = function() {
- this.running = null;
- this.dir = null;
- this.port = 0;
- this.sockets = [];
-};
-util.inherits(Server, events.EventEmitter);
-
-// Return true if the server is running
-Server.prototype.isRunning = function() {
- return !!this.running;
-};
-
-// Stop the server
-Server.prototype.stop = function() {
- var that = this;
- if (!this.isRunning()) return Q();
-
- var d = Q.defer();
- this.running.close(function(err) {
- that.running = null;
- that.emit("state", false);
-
- if (err) d.reject(err);
- else d.resolve();
- });
-
- for (var i = 0; i < this.sockets.length; i++) {
- this.sockets[i].destroy();
- }
-
- return d.promise;
-};
-
-Server.prototype.start = function(dir, port) {
- var that = this, pre = Q();
- port = port || 8004;
-
- if (that.isRunning()) pre = this.stop();
- return pre
- .then(function() {
- var d = Q.defer();
-
- that.running = http.createServer(function(req, res){
- // Render error
- function error(err) {
- res.statusCode = err.status || 500;
- res.end(err.message);
- }
-
- // Redirect to directory"s index.html
- function redirect() {
- res.statusCode = 301;
- res.setHeader("Location", req.url + "/");
- res.end("Redirecting to " + req.url + "/");
- }
-
- // Send file
- send(req, url.parse(req.url).pathname)
- .root(dir)
- .on("error", error)
- .on("directory", redirect)
- .pipe(res);
- });
-
- that.running.on("connection", function (socket) {
- that.sockets.push(socket);
- socket.setTimeout(4000);
- socket.on("close", function () {
- that.sockets.splice(that.sockets.indexOf(socket), 1);
- });
- });
-
- that.running.listen(port, function(err) {
- if (err) return d.reject(err);
-
- that.port = port;
- that.dir = dir;
- that.emit("state", true);
- d.resolve();
- });
-
- return d.promise;
- });
-};
-
-module.exports = Server;
diff --git a/lib/utils/string.js b/lib/utils/string.js
deleted file mode 100644
index caa2364..0000000
--- a/lib/utils/string.js
+++ /dev/null
@@ -1,27 +0,0 @@
-var _ = require("lodash");
-
-function escapeShellArg(arg) {
- var ret = "";
-
- ret = arg.replace(/"/g, '\\"');
-
- return "\"" + ret + "\"";
-}
-
-function optionsToShellArgs(options) {
- return _.chain(options)
- .map(function(value, key) {
- if (value === null || value === undefined || value === false) return null;
- if (value === true) return key;
- return key+"="+escapeShellArg(value);
- })
- .compact()
- .value()
- .join(" ");
-}
-
-module.exports = {
- escapeShellArg: escapeShellArg,
- optionsToShellArgs: optionsToShellArgs,
- toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
-};
diff --git a/lib/utils/watch.js b/lib/utils/watch.js
deleted file mode 100644
index 4d1a752..0000000
--- a/lib/utils/watch.js
+++ /dev/null
@@ -1,40 +0,0 @@
-var Q = require("q");
-var _ = require("lodash");
-var path = require("path");
-var chokidar = require("chokidar");
-
-var parsers = require("gitbook-parsers");
-
-function watch(dir) {
- var d = Q.defer();
- dir = path.resolve(dir);
-
- var toWatch = [
- "book.json", "book.js"
- ];
-
- _.each(parsers.extensions, function(ext) {
- toWatch.push("**/*"+ext);
- });
-
- var watcher = chokidar.watch(toWatch, {
- cwd: dir,
- ignored: "_book/**",
- ignoreInitial: true
- });
-
- watcher.once("all", function(e, filepath) {
- watcher.close();
-
- d.resolve(filepath);
- });
- watcher.once("error", function(err) {
- watcher.close();
-
- d.reject(err);
- });
-
- return d.promise;
-}
-
-module.exports = watch;