diff options
Diffstat (limited to 'lib/utils')
-rw-r--r-- | lib/utils/__tests__/git.js | 58 | ||||
-rw-r--r-- | lib/utils/__tests__/location.js | 78 | ||||
-rw-r--r-- | lib/utils/__tests__/path.js | 17 | ||||
-rw-r--r-- | lib/utils/error.js | 30 | ||||
-rw-r--r-- | lib/utils/fs.js | 36 | ||||
-rw-r--r-- | lib/utils/genKey.js | 13 | ||||
-rw-r--r-- | lib/utils/location.js | 49 | ||||
-rw-r--r-- | lib/utils/logger.js | 62 | ||||
-rw-r--r-- | lib/utils/path.js | 15 | ||||
-rw-r--r-- | lib/utils/promise.js | 112 | ||||
-rw-r--r-- | lib/utils/timing.js | 89 |
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 +}; |