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/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.js80
-rw-r--r--lib/utils/page.js343
-rw-r--r--lib/utils/progress.js47
-rw-r--r--lib/utils/server.js96
-rw-r--r--lib/utils/string.js8
-rw-r--r--lib/utils/watch.js38
15 files changed, 1196 insertions, 34 deletions
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/utils/navigation.js b/lib/utils/navigation.js
new file mode 100644
index 0000000..21666ad
--- /dev/null
+++ b/lib/utils/navigation.js
@@ -0,0 +1,80 @@
+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, then add in default README node if needed etc ...
+ 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.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,
+ 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
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/utils/progress.js b/lib/utils/progress.js
new file mode 100644
index 0000000..b66aea9
--- /dev/null
+++ b/lib/utils/progress.js
@@ -0,0 +1,47 @@
+var _ = require("lodash");
+
+// Returns from a navigation and a current file, a snapshot of current detailed state
+var calculProgress = function(navigation, current) {
+ var n = _.size(navigation);
+ var percent = 0, prevPercent = 0, currentChapter = null;
+ var done = true;
+
+ var chapters = _.chain(navigation)
+ .map(function(nav, path) {
+ nav.path = path;
+ return nav;
+ })
+ .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/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;