diff options
Diffstat (limited to 'lib/utils')
-rw-r--r-- | lib/utils/batch.js | 52 | ||||
-rw-r--r-- | lib/utils/fs.js | 178 | ||||
-rw-r--r-- | lib/utils/git.js | 112 | ||||
-rw-r--r-- | lib/utils/i18n.js | 72 | ||||
-rw-r--r-- | lib/utils/images.js | 39 | ||||
-rw-r--r-- | lib/utils/index.js | 4 | ||||
-rw-r--r-- | lib/utils/lang.js | 19 | ||||
-rw-r--r-- | lib/utils/links.js | 40 | ||||
-rw-r--r-- | lib/utils/logger.js | 102 | ||||
-rw-r--r-- | lib/utils/navigation.js | 80 | ||||
-rw-r--r-- | lib/utils/page.js | 343 | ||||
-rw-r--r-- | lib/utils/progress.js | 47 | ||||
-rw-r--r-- | lib/utils/server.js | 96 | ||||
-rw-r--r-- | lib/utils/string.js | 8 | ||||
-rw-r--r-- | lib/utils/watch.js | 38 |
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; |