summaryrefslogtreecommitdiffstats
path: root/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils')
-rw-r--r--lib/utils/__tests__/git.js58
-rw-r--r--lib/utils/__tests__/location.js78
-rw-r--r--lib/utils/__tests__/path.js17
-rw-r--r--lib/utils/error.js30
-rw-r--r--lib/utils/fs.js36
-rw-r--r--lib/utils/genKey.js13
-rw-r--r--lib/utils/location.js49
-rw-r--r--lib/utils/logger.js62
-rw-r--r--lib/utils/path.js15
-rw-r--r--lib/utils/promise.js112
-rw-r--r--lib/utils/timing.js89
11 files changed, 499 insertions, 60 deletions
diff --git a/lib/utils/__tests__/git.js b/lib/utils/__tests__/git.js
new file mode 100644
index 0000000..6eed81e
--- /dev/null
+++ b/lib/utils/__tests__/git.js
@@ -0,0 +1,58 @@
+var should = require('should');
+var path = require('path');
+var os = require('os');
+
+var Git = require('../git');
+
+describe('Git', function() {
+
+ describe('URL parsing', function() {
+
+ it('should correctly validate git urls', function() {
+ // HTTPS
+ expect(Git.isUrl('git+https://github.com/Hello/world.git')).toBeTruthy();
+
+ // SSH
+ expect(Git.isUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1')).toBeTruthy();
+
+ // Non valid
+ expect(Git.isUrl('https://github.com/Hello/world.git')).not.toBeTruthy();
+ expect(Git.isUrl('README.md')).not.toBeTruthy();
+ });
+
+ it('should parse HTTPS urls', function() {
+ var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md');
+
+ expect(parts.host).toBe('https://gist.github.com/69ea4542e4c8967d2fa7.git');
+ expect(parts.ref).toBe(null);
+ expect(parts.filepath).toBe('test.md');
+ });
+
+ it('should parse HTTPS urls with a reference', function() {
+ var parts = Git.parseUrl('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md#1.0.0');
+
+ expect(parts.host).toBe('https://gist.github.com/69ea4542e4c8967d2fa7.git');
+ expect(parts.ref).toBe('1.0.0');
+ expect(parts.filepath).toBe('test.md');
+ });
+
+ it('should parse SSH urls', function() {
+ var parts = Git.parseUrl('git+git@github.com:GitbookIO/gitbook.git/directory/README.md#e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
+
+ expect(parts.host).toBe('git@github.com:GitbookIO/gitbook.git');
+ expect(parts.ref).toBe('e1594cde2c32e4ff48f6c4eff3d3d461743d74e1');
+ expect(parts.filepath).toBe('directory/README.md');
+ });
+ });
+
+ describe('Cloning and resolving', function() {
+ pit('should clone an HTTPS url', function() {
+ var git = new Git(path.join(os.tmpdir(), 'test-git-'+Date.now()));
+ return git.resolve('git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md')
+ .then(function(filename) {
+ expect(path.extname(filename)).toBe('.md');
+ });
+ });
+ });
+
+});
diff --git a/lib/utils/__tests__/location.js b/lib/utils/__tests__/location.js
new file mode 100644
index 0000000..f2037ff
--- /dev/null
+++ b/lib/utils/__tests__/location.js
@@ -0,0 +1,78 @@
+jest.autoMockOff();
+
+describe('LocationUtils', function() {
+ var LocationUtils = require('../location');
+
+ it('should correctly test external location', function() {
+ expect(LocationUtils.isExternal('http://google.fr')).toBe(true);
+ expect(LocationUtils.isExternal('https://google.fr')).toBe(true);
+ expect(LocationUtils.isExternal('test.md')).toBe(false);
+ expect(LocationUtils.isExternal('folder/test.md')).toBe(false);
+ expect(LocationUtils.isExternal('/folder/test.md')).toBe(false);
+ });
+
+ it('should correctly detect anchor location', function() {
+ expect(LocationUtils.isAnchor('#test')).toBe(true);
+ expect(LocationUtils.isAnchor(' #test')).toBe(true);
+ expect(LocationUtils.isAnchor('https://google.fr#test')).toBe(false);
+ expect(LocationUtils.isAnchor('test.md#test')).toBe(false);
+ });
+
+ describe('.relative', function() {
+ it('should resolve to a relative path (same folder)', function() {
+ expect(LocationUtils.relative('links/', 'links/test.md')).toBe('test.md');
+ });
+
+ it('should resolve to a relative path (parent folder)', function() {
+ expect(LocationUtils.relative('links/', 'test.md')).toBe('../test.md');
+ });
+
+ it('should resolve to a relative path (child folder)', function() {
+ expect(LocationUtils.relative('links/', 'links/hello/test.md')).toBe('hello/test.md');
+ });
+ });
+
+ describe('.toAbsolute', function() {
+ it('should correctly transform as absolute', function() {
+ expect(LocationUtils.toAbsolute('http://google.fr')).toBe('http://google.fr');
+ expect(LocationUtils.toAbsolute('test.md', './', './')).toBe('test.md');
+ expect(LocationUtils.toAbsolute('folder/test.md', './', './')).toBe('folder/test.md');
+ });
+
+ it('should correctly handle windows path', function() {
+ expect(LocationUtils.toAbsolute('folder\\test.md', './', './')).toBe('folder/test.md');
+ });
+
+ it('should correctly handle absolute path', function() {
+ expect(LocationUtils.toAbsolute('/test.md', './', './')).toBe('test.md');
+ expect(LocationUtils.toAbsolute('/test.md', 'test', 'test')).toBe('../test.md');
+ expect(LocationUtils.toAbsolute('/sub/test.md', 'test', 'test')).toBe('../sub/test.md');
+ expect(LocationUtils.toAbsolute('/test.png', 'folder', '')).toBe('test.png');
+ });
+
+ it('should correctly handle absolute path (windows)', function() {
+ expect(LocationUtils.toAbsolute('\\test.png', 'folder', '')).toBe('test.png');
+ });
+
+ it('should resolve path starting by "/" in root directory', function() {
+ expect(
+ LocationUtils.toAbsolute('/test/hello.md', './', './')
+ ).toBe('test/hello.md');
+ });
+
+ it('should resolve path starting by "/" in child directory', function() {
+ expect(
+ LocationUtils.toAbsolute('/test/hello.md', './hello', './')
+ ).toBe('test/hello.md');
+ });
+
+ it('should resolve path starting by "/" in child directory, with same output directory', function() {
+ expect(
+ LocationUtils.toAbsolute('/test/hello.md', './hello', './hello')
+ ).toBe('../test/hello.md');
+ });
+ });
+
+});
+
+
diff --git a/lib/utils/__tests__/path.js b/lib/utils/__tests__/path.js
new file mode 100644
index 0000000..22bb016
--- /dev/null
+++ b/lib/utils/__tests__/path.js
@@ -0,0 +1,17 @@
+var path = require('path');
+
+describe('Paths', function() {
+ var PathUtils = require('..//path');
+
+ describe('setExtension', function() {
+ it('should correctly change extension of filename', function() {
+ expect(PathUtils.setExtension('test.md', '.html')).toBe('test.html');
+ expect(PathUtils.setExtension('test.md', '.json')).toBe('test.json');
+ });
+
+ it('should correctly change extension of path', function() {
+ expect(PathUtils.setExtension('hello/test.md', '.html')).toBe(path.normalize('hello/test.html'));
+ expect(PathUtils.setExtension('hello/test.md', '.json')).toBe(path.normalize('hello/test.json'));
+ });
+ });
+});
diff --git a/lib/utils/error.js b/lib/utils/error.js
index 27fa59d..7686779 100644
--- a/lib/utils/error.js
+++ b/lib/utils/error.js
@@ -1,15 +1,12 @@
-var _ = require('lodash');
+var is = require('is');
+
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);
+ if (is.string(err)) err = new Error(err);
err.message = err.message.replace(/^Error: /, '');
return err;
@@ -32,6 +29,13 @@ var FileNotFoundError = TypedError({
filename: null
});
+// A file cannot be parsed
+var FileNotParsableError = TypedError({
+ type: 'file.not-parsable',
+ message: '"{filename}" file cannot be parsed',
+ filename: null
+});
+
// A file is outside the scope
var FileOutOfScopeError = TypedError({
type: 'file.out-of-scope',
@@ -77,14 +81,6 @@ var EbookError = WrappedError({
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,
@@ -92,14 +88,12 @@ module.exports = {
OutputError: OutputError,
RequireInstallError: RequireInstallError,
+ FileNotParsableError: FileNotParsableError,
FileNotFoundError: FileNotFoundError,
FileOutOfScopeError: FileOutOfScopeError,
TemplateError: TemplateError,
PluginError: PluginError,
ConfigurationError: ConfigurationError,
- EbookError: EbookError,
-
- deprecateMethod: deprecateMethod,
- deprecateField: deprecateField
+ EbookError: EbookError
};
diff --git a/lib/utils/fs.js b/lib/utils/fs.js
index 42fd3c6..3f97096 100644
--- a/lib/utils/fs.js
+++ b/lib/utils/fs.js
@@ -97,12 +97,46 @@ function rmDir(base) {
});
}
+/**
+ Assert a file, if it doesn't exist, call "generator"
+
+ @param {String} filePath
+ @param {Function} generator
+ @return {Promise}
+*/
+function assertFile(filePath, generator) {
+ return fileExists(filePath)
+ .then(function(exists) {
+ if (exists) return;
+
+ return generator();
+ });
+}
+
+/**
+ Pick a file, returns the absolute path if exists, undefined otherwise
+
+ @param {String} rootFolder
+ @param {String} fileName
+ @return {String}
+*/
+function pickFile(rootFolder, fileName) {
+ var result = path.join(rootFolder, fileName);
+ if (fs.existsSync(result)) {
+ return result;
+ }
+
+ return undefined;
+}
+
module.exports = {
exists: fileExists,
existsSync: fs.existsSync,
mkdirp: Promise.nfbind(mkdirp),
readFile: Promise.nfbind(fs.readFile),
writeFile: Promise.nfbind(fs.writeFile),
+ assertFile: assertFile,
+ pickFile: pickFile,
stat: Promise.nfbind(fs.stat),
statSync: fs.statSync,
readdir: Promise.nfbind(fs.readdir),
@@ -113,6 +147,6 @@ module.exports = {
tmpDir: genTmpDir,
download: download,
uniqueFilename: uniqueFilename,
- ensure: ensureFile,
+ ensureFile: ensureFile,
rmDir: rmDir
};
diff --git a/lib/utils/genKey.js b/lib/utils/genKey.js
new file mode 100644
index 0000000..0650011
--- /dev/null
+++ b/lib/utils/genKey.js
@@ -0,0 +1,13 @@
+var lastKey = 0;
+
+/*
+ Generate a random key
+ @return {String}
+*/
+function generateKey() {
+ lastKey += 1;
+ var str = lastKey.toString(16);
+ return '00000'.slice(str.length) + str;
+}
+
+module.exports = generateKey;
diff --git a/lib/utils/location.js b/lib/utils/location.js
index ba43644..84a71ad 100644
--- a/lib/utils/location.js
+++ b/lib/utils/location.js
@@ -30,9 +30,14 @@ function normalize(s) {
return path.normalize(s).replace(/\\/g, '/');
}
-// Convert relative to absolute path
-// dir: directory parent of the file currently in rendering process
-// outdir: directory parent from the html output
+/**
+ Convert relative to absolute path
+
+ @param {String} href
+ @param {String} dir: directory parent of the file currently in rendering process
+ @param {String} outdir: directory parent from the html output
+ @return {String}
+*/
function toAbsolute(_href, dir, outdir) {
if (isExternal(_href)) return _href;
outdir = outdir == undefined? dir : outdir;
@@ -54,17 +59,49 @@ function toAbsolute(_href, dir, outdir) {
return _href;
}
-// Convert an absolute path to a relative path for a specific folder (dir)
-// ('test/', 'hello.md') -> '../hello.md'
+/**
+ Convert an absolute path to a relative path for a specific folder (dir)
+ ('test/', 'hello.md') -> '../hello.md'
+
+ @param {String} dir: current directory
+ @param {String} file: absolute path of file
+ @return {String}
+*/
function relative(dir, file) {
return normalize(path.relative(dir, file));
}
+/**
+ Convert an absolute path to a relative path for a specific folder (dir)
+ ('test/test.md', 'hello.md') -> '../hello.md'
+
+ @param {String} baseFile: current file
+ @param {String} file: absolute path of file
+ @return {String}
+*/
+function relativeForFile(baseFile, file) {
+ return relative(path.dirname(baseFile), file);
+}
+
+/**
+ Compare two paths, return true if they are identical
+ ('README.md', './README.md') -> true
+
+ @param {String} p1: first path
+ @param {String} p2: second path
+ @return {Boolean}
+*/
+function areIdenticalPaths(p1, p2) {
+ return normalize(p1) === normalize(p2);
+}
+
module.exports = {
+ areIdenticalPaths: areIdenticalPaths,
isExternal: isExternal,
isRelative: isRelative,
isAnchor: isAnchor,
normalize: normalize,
toAbsolute: toAbsolute,
- relative: relative
+ relative: relative,
+ relativeForFile: relativeForFile
};
diff --git a/lib/utils/logger.js b/lib/utils/logger.js
index 60215af..fc9c394 100644
--- a/lib/utils/logger.js
+++ b/lib/utils/logger.js
@@ -17,14 +17,17 @@ var COLORS = {
ERROR: color.red
};
-function Logger(write, logLevel, prefix) {
+function Logger(write, logLevel) {
if (!(this instanceof Logger)) return new Logger(write, logLevel);
- this._write = write || function(msg) { process.stdout.write(msg); };
+ this._write = write || function(msg) {
+ if(process.stdout) {
+ process.stdout.write(msg);
+ }
+ };
this.lastChar = '\n';
- // Define log level
- this.setLevel(logLevel);
+ this.setLevel(logLevel || 'info');
_.bindAll(this);
@@ -40,35 +43,48 @@ function Logger(write, logLevel, prefix) {
}, 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
-// Change minimum level
+ @param {String} logLevel
+*/
Logger.prototype.setLevel = function(logLevel) {
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
this.logLevel = logLevel;
};
-// Print a simple string
+/**
+ Print a simple string
+
+ @param {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.
+/**
+ 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
+/**
+ Print a line
+
+ @param {String}
+*/
Logger.prototype.writeLn = function(msg) {
return this.write((msg || '')+'\n');
};
-// Log/Print a message if level is allowed
+/**
+ Log/Print a message if level is allowed
+
+ @param {Number} level
+*/
Logger.prototype.log = function(level) {
if (level < this.logLevel) return;
@@ -83,7 +99,9 @@ Logger.prototype.log = function(level) {
return this.write(msg);
};
-// Log/Print a line if level is allowed
+/**
+ Log/Print a line if level is allowed
+*/
Logger.prototype.logLn = function() {
if (this.lastChar != '\n') this.write('\n');
@@ -92,7 +110,9 @@ Logger.prototype.logLn = function() {
return this.log.apply(this, args);
};
-// Log a confirmation [OK]
+/**
+ Log a confirmation [OK]
+*/
Logger.prototype.ok = function(level) {
var args = Array.prototype.slice.apply(arguments, [1]);
var msg = this.format.apply(this, args);
@@ -103,12 +123,20 @@ Logger.prototype.ok = function(level) {
}
};
-// Log a "FAIL"
+/**
+ Log a "FAIL"
+*/
Logger.prototype.fail = function(level) {
return this.log(level, color.red('ERROR') + '\n');
};
-// Log state of a promise
+/**
+ Log state of a promise
+
+ @param {Number} level
+ @param {Promise}
+ @return {Promise}
+*/
Logger.prototype.promise = function(level, p) {
var that = this;
diff --git a/lib/utils/path.js b/lib/utils/path.js
index c233c92..a4968c8 100644
--- a/lib/utils/path.js
+++ b/lib/utils/path.js
@@ -42,7 +42,7 @@ function resolveInRoot(root) {
return result;
}
-// Chnage extension
+// Chnage extension of a file
function setExtension(filename, ext) {
return path.join(
path.dirname(filename),
@@ -50,9 +50,20 @@ function setExtension(filename, ext) {
);
}
+/*
+ Return true if a filename is relative.
+
+ @param {String}
+ @return {Boolean}
+*/
+function isPureRelative(filename) {
+ return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0);
+}
+
module.exports = {
isInRoot: isInRoot,
resolveInRoot: resolveInRoot,
normalize: normalizePath,
- setExtension: setExtension
+ setExtension: setExtension,
+ isPureRelative: isPureRelative
};
diff --git a/lib/utils/promise.js b/lib/utils/promise.js
index d49cf27..19d7554 100644
--- a/lib/utils/promise.js
+++ b/lib/utils/promise.js
@@ -1,19 +1,46 @@
var Q = require('q');
-var _ = require('lodash');
+var Immutable = require('immutable');
-// Reduce an array to a promise
+/**
+ Reduce an array to a promise
+
+ @param {Array|List} arr
+ @param {Function(value, element, index)}
+ @return {Promise<Mixed>}
+*/
function reduce(arr, iter, base) {
- return _.reduce(arr, function(prev, elem, i) {
+ arr = Immutable.Iterable.isIterable(arr)? arr : Immutable.List(arr);
+
+ return arr.reduce(function(prev, elem, key) {
return prev.then(function(val) {
- return iter(val, elem, i);
+ return iter(val, elem, key);
});
}, Q(base));
}
-// Transform an array
+/**
+ Iterate over an array using an async iter
+
+ @param {Array|List} arr
+ @param {Function(value, element, index)}
+ @return {Promise}
+*/
+function forEach(arr, iter) {
+ return reduce(arr, function(val, el, key) {
+ return iter(el, key);
+ });
+}
+
+/**
+ Transform an array
+
+ @param {Array|List} arr
+ @param {Function(value, element, index)}
+ @return {Promise}
+*/
function serie(arr, iter, base) {
- return reduce(arr, function(before, item, i) {
- return Q(iter(item, i))
+ return reduce(arr, function(before, item, key) {
+ return Q(iter(item, key))
.then(function(r) {
before.push(r);
return before;
@@ -21,9 +48,17 @@ function serie(arr, iter, base) {
}, []);
}
-// Iter over an array and return first result (not null)
+/**
+ Iter over an array and return first result (not null)
+
+ @param {Array|List} arr
+ @param {Function(element, index)}
+ @return {Promise<Mixed>}
+*/
function some(arr, iter) {
- return _.reduce(arr, function(prev, elem, i) {
+ arr = Immutable.List(arr);
+
+ return arr.reduce(function(prev, elem, i) {
return prev.then(function(val) {
if (val) return val;
@@ -32,8 +67,14 @@ function some(arr, iter) {
}, Q());
}
-// Map an array using an async (promised) iterator
-function map(arr, iter) {
+/**
+ Map an array using an async (promised) iterator
+
+ @param {Array|List} arr
+ @param {Function(element, index)}
+ @return {Promise<List>}
+*/
+function mapAsList(arr, iter) {
return reduce(arr, function(prev, entry, i) {
return Q(iter(entry, i))
.then(function(out) {
@@ -43,18 +84,57 @@ function map(arr, iter) {
}, []);
}
-// Wrap a fucntion in a promise
+/**
+ Map an array or map
+
+ @param {Array|List|Map|OrderedMap} arr
+ @param {Function(element, key)}
+ @return {Promise<List|Map|OrderedMap>}
+*/
+function map(arr, iter) {
+ if (Immutable.Map.isMap(arr)) {
+ var type = 'Map';
+ if (Immutable.OrderedMap.isOrderedMap(arr)) {
+ type = 'OrderedMap';
+ }
+
+ return mapAsList(arr, function(value, key) {
+ return Q(iter(value, key))
+ .then(function(result) {
+ return [key, result];
+ });
+ })
+ .then(function(result) {
+ return Immutable[type](result);
+ });
+ } else {
+ return mapAsList(arr, iter)
+ .then(function(result) {
+ return Immutable.List(result);
+ });
+ }
+}
+
+
+/**
+ Wrap a function in a promise
+
+ @param {Function} func
+ @return {Funciton}
+*/
function wrap(func) {
- return _.wrap(func, function(_func) {
- var args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ var args = Array.prototype.slice.call(arguments, 0);
+
return Q()
.then(function() {
- return _func.apply(null, args);
+ return func.apply(null, args);
});
- });
+ };
}
module.exports = Q;
+module.exports.forEach = forEach;
module.exports.reduce = reduce;
module.exports.map = map;
module.exports.serie = serie;
diff --git a/lib/utils/timing.js b/lib/utils/timing.js
new file mode 100644
index 0000000..21a4b91
--- /dev/null
+++ b/lib/utils/timing.js
@@ -0,0 +1,89 @@
+var Immutable = require('immutable');
+var is = require('is');
+
+var timers = {};
+var startDate = Date.now();
+
+/**
+ Mesure an operation
+
+ @parqm {String} type
+ @param {Promise} p
+ @return {Promise}
+*/
+function measure(type, p) {
+ timers[type] = timers[type] || {
+ type: type,
+ count: 0,
+ total: 0,
+ min: undefined,
+ max: 0
+ };
+
+ var start = Date.now();
+
+ return p
+ .fin(function() {
+ var end = Date.now();
+ var duration = (end - start);
+
+ timers[type].count ++;
+ timers[type].total += duration;
+
+ if (is.undefined(timers[type].min)) {
+ timers[type].min = duration;
+ } else {
+ timers[type].min = Math.min(timers[type].min, duration);
+ }
+
+ timers[type].max = Math.max(timers[type].max, duration);
+ });
+}
+
+/**
+ Return a milliseconds number as a second string
+
+ @param {Number} ms
+ @return {String}
+*/
+function time(ms) {
+ if (ms < 1000) {
+ return (ms.toFixed(0)) + 'ms';
+ }
+
+ return (ms/1000).toFixed(2) + 's';
+}
+
+/**
+ Dump all timers to a logger
+
+ @param {Logger} logger
+*/
+function dump(logger) {
+ var prefix = ' > ';
+ var measured = 0;
+ var totalDuration = Date.now() - startDate;
+
+ Immutable.Map(timers)
+ .valueSeq()
+ .sortBy(function(timer) {
+ measured += timer.total;
+ return timer.total;
+ })
+ .forEach(function(timer) {
+ logger.debug.ln('Timer "' + timer.type + '" (' + timer.count + ' times) :');
+ logger.debug.ln(prefix + 'Total: ' + time(timer.total));
+ logger.debug.ln(prefix + 'Average: ' + time(timer.total / timer.count));
+ logger.debug.ln(prefix + 'Min: ' + time(timer.min));
+ logger.debug.ln(prefix + 'Max: ' + time(timer.max));
+ logger.debug.ln('---------------------------');
+ });
+
+
+ logger.debug.ln(time(totalDuration - measured) + ' spent in non-mesured sections');
+}
+
+module.exports = {
+ measure: measure,
+ dump: dump
+};