diff --git a/Gruntfile.js b/Gruntfile.js index 407dfee6c6b1..34f1e4c629ce 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -196,7 +196,7 @@ module.exports = function(grunt) { unused: true, global_defs: { - 'TEST': false + '__DEV__': false } } }, diff --git a/src/raven.js b/src/raven.js index e46e519d4122..18fefed96848 100644 --- a/src/raven.js +++ b/src/raven.js @@ -1,27 +1,10 @@ -/*global XDomainRequest:false*/ +/*global XDomainRequest:false, __DEV__:false*/ 'use strict'; var TraceKit = require('../vendor/TraceKit/tracekit'); var RavenConfigError = require('./configError'); -var utils = require('./utils'); var stringify = require('json-stringify-safe'); -var isFunction = utils.isFunction; -var isUndefined = utils.isUndefined; -var isError = utils.isError; -var isEmptyObject = utils.isEmptyObject; -var hasKey = utils.hasKey; -var joinRegExp = utils.joinRegExp; -var each = utils.each; -var objectMerge = utils.objectMerge; -var truncate = utils.truncate; -var urlencode = utils.urlencode; -var uuid4 = utils.uuid4; -var htmlTreeAsString = utils.htmlTreeAsString; -var parseUrl = utils.parseUrl; -var isString = utils.isString; -var fill = utils.fill; - var wrapConsoleMethod = require('./console').wrapMethod; var dsnKeys = 'source protocol user pass host port path'.split(' '), @@ -31,6 +14,8 @@ function now() { return +new Date(); } +var _window = typeof window !== 'undefined' ? window : undefined; +var _document = _window && _window.document; // First, check for JSON support // If there is no JSON, we no-op the core features of Raven @@ -38,7 +23,7 @@ function now() { function Raven() { this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify); // Raven can run in contexts where there's no document (react-native) - this._hasDocument = typeof document !== 'undefined'; + this._hasDocument = !isUndefined(_document); this._lastCapturedException = null; this._lastEventId = null; this._globalServer = null; @@ -62,7 +47,7 @@ function Raven() { this._originalErrorStackTraceLimit = Error.stackTraceLimit; // capture references to window.console *and* all its methods first // before the console plugin has a chance to monkey patch - this._originalConsole = window.console || {}; + this._originalConsole = _window.console || {}; this._originalConsoleMethods = {}; this._plugins = []; this._startTime = now(); @@ -70,7 +55,7 @@ function Raven() { this._breadcrumbs = []; this._lastCapturedEvent = null; this._keypressTimeout; - this._location = window.location; + this._location = _window.location; this._lastHref = this._location && this._location.href; for (var method in this._originalConsole) { // eslint-disable-line guard-for-in @@ -105,11 +90,13 @@ Raven.prototype = { config: function(dsn, options) { var self = this; - if (this._globalServer) { + if (self._globalServer) { this._logDebug('error', 'Error: Raven has already been configured'); - return this; + return self; } - if (!dsn) return this; + if (!dsn) return self; + + var globalOptions = self._globalOptions; // merge in options if (options) { @@ -118,24 +105,24 @@ Raven.prototype = { if (key === 'tags' || key === 'extra') { self._globalContext[key] = value; } else { - self._globalOptions[key] = value; + globalOptions[key] = value; } }); } - this.setDSN(dsn); + self.setDSN(dsn); // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. - this._globalOptions.ignoreErrors.push(/^Script error\.?$/); - this._globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); + globalOptions.ignoreErrors.push(/^Script error\.?$/); + globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); // join regexp rules into one big rule - this._globalOptions.ignoreErrors = joinRegExp(this._globalOptions.ignoreErrors); - this._globalOptions.ignoreUrls = this._globalOptions.ignoreUrls.length ? joinRegExp(this._globalOptions.ignoreUrls) : false; - this._globalOptions.whitelistUrls = this._globalOptions.whitelistUrls.length ? joinRegExp(this._globalOptions.whitelistUrls) : false; - this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths); - this._globalOptions.maxBreadcrumbs = Math.max(0, Math.min(this._globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 + globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); + globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; + globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; + globalOptions.includePaths = joinRegExp(globalOptions.includePaths); + globalOptions.maxBreadcrumbs = Math.max(0, Math.min(globalOptions.maxBreadcrumbs || 100, 100)); // default and hard limit is 100 var autoBreadcrumbDefaults = { xhr: true, @@ -144,18 +131,18 @@ Raven.prototype = { location: true }; - var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; + var autoBreadcrumbs = globalOptions.autoBreadcrumbs; if ({}.toString.call(autoBreadcrumbs) === '[object Object]') { autoBreadcrumbs = objectMerge(autoBreadcrumbDefaults, autoBreadcrumbs); } else if (autoBreadcrumbs !== false) { autoBreadcrumbs = autoBreadcrumbDefaults; } - this._globalOptions.autoBreadcrumbs = autoBreadcrumbs; + globalOptions.autoBreadcrumbs = autoBreadcrumbs; - TraceKit.collectWindowErrors = !!this._globalOptions.collectWindowErrors; + TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; // return for chaining - return this; + return self; }, /* @@ -168,21 +155,21 @@ Raven.prototype = { */ install: function() { var self = this; - if (this.isSetup() && !this._isRavenInstalled) { + if (self.isSetup() && !self._isRavenInstalled) { TraceKit.report.subscribe(function () { self._handleOnErrorStackInfo.apply(self, arguments); }); - this._instrumentTryCatch(); + self._instrumentTryCatch(); if (self._globalOptions.autoBreadcrumbs) - this._instrumentBreadcrumbs(); + self._instrumentBreadcrumbs(); // Install all of the plugins - this._drainPlugins(); + self._drainPlugins(); - this._isRavenInstalled = true; + self._isRavenInstalled = true; } - Error.stackTraceLimit = this._globalOptions.stackTraceLimit; + Error.stackTraceLimit = self._globalOptions.stackTraceLimit; return this; }, @@ -192,19 +179,20 @@ Raven.prototype = { * @param {string} dsn The public Sentry DSN */ setDSN: function(dsn) { - var uri = this._parseDSN(dsn), + var self = this, + uri = self._parseDSN(dsn), lastSlash = uri.path.lastIndexOf('/'), path = uri.path.substr(1, lastSlash); - this._dsn = dsn; - this._globalKey = uri.user; - this._globalSecret = uri.pass && uri.pass.substr(1); - this._globalProject = uri.path.substr(lastSlash + 1); + self._dsn = dsn; + self._globalKey = uri.user; + self._globalSecret = uri.pass && uri.pass.substr(1); + self._globalProject = uri.path.substr(lastSlash + 1); - this._globalServer = this._getGlobalServer(uri); + self._globalServer = self._getGlobalServer(uri); - this._globalEndpoint = this._globalServer + - '/' + path + 'api/' + this._globalProject + '/store/'; + self._globalEndpoint = self._globalServer + + '/' + path + 'api/' + self._globalProject + '/store/'; }, /* @@ -427,7 +415,7 @@ Raven.prototype = { }, addPlugin: function(plugin /*arg1, arg2, ... argN*/) { - var pluginArgs = Array.prototype.slice.call(arguments, 1); + var pluginArgs = [].slice.call(arguments, 1); this._plugins.push([plugin, pluginArgs]); if (this._isRavenInstalled) { @@ -606,14 +594,14 @@ Raven.prototype = { // TODO: remove window dependence? // Attempt to initialize Raven on load - var RavenConfig = window.RavenConfig; + var RavenConfig = _window.RavenConfig; if (RavenConfig) { this.config(RavenConfig.dsn, RavenConfig.config).install(); } }, showReportDialog: function (options) { - if (!window.document) // doesn't work without a document (React native) + if (!_document) // doesn't work without a document (React native) return; options = options || {}; @@ -641,10 +629,10 @@ Raven.prototype = { var globalServer = this._getGlobalServer(this._parseDSN(dsn)); - var script = document.createElement('script'); + var script = _document.createElement('script'); script.async = true; script.src = globalServer + '/api/embed/error-page/' + qs; - (document.head || document.body).appendChild(script); + (_document.head || _document.body).appendChild(script); }, /**** Private functions ****/ @@ -668,11 +656,11 @@ Raven.prototype = { eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); - if (document.createEvent) { - evt = document.createEvent('HTMLEvents'); + if (_document.createEvent) { + evt = _document.createEvent('HTMLEvents'); evt.initEvent(eventType, true, true); } else { - evt = document.createEventObject(); + evt = _document.createEventObject(); evt.eventType = eventType; } @@ -680,14 +668,14 @@ Raven.prototype = { evt[key] = options[key]; } - if (document.createEvent) { + if (_document.createEvent) { // IE9 if standards - document.dispatchEvent(evt); + _document.dispatchEvent(evt); } else { // IE8 regardless of Quirks or Standards // IE9 if quirks try { - document.fireEvent('on' + evt.eventType.toLowerCase(), evt); + _document.fireEvent('on' + evt.eventType.toLowerCase(), evt); } catch(e) { // Do nothing } @@ -837,7 +825,7 @@ Raven.prototype = { var autoBreadcrumbs = this._globalOptions.autoBreadcrumbs; function wrapEventTarget(global) { - var proto = window[global] && window[global].prototype; + var proto = _window[global] && _window[global].prototype; if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) { fill(proto, 'addEventListener', function(orig) { return function (evtName, fn, capture, secure) { // preserve arity @@ -875,10 +863,10 @@ Raven.prototype = { } } - fill(window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); - fill(window, 'setInterval', wrapTimeFn, wrappedBuiltIns); - if (window.requestAnimationFrame) { - fill(window, 'requestAnimationFrame', function (orig) { + fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns); + fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns); + if (_window.requestAnimationFrame) { + fill(_window, 'requestAnimationFrame', function (orig) { return function (cb) { return orig(self.wrap(cb)); }; @@ -892,7 +880,7 @@ Raven.prototype = { wrapEventTarget(eventTargets[i]); } - var $ = window.jQuery || window.$; + var $ = _window.jQuery || _window.$; if ($ && $.fn && $.fn.ready) { fill($.fn, 'ready', function (orig) { return function (fn) { @@ -926,7 +914,7 @@ Raven.prototype = { } } - if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in window) { + if (autoBreadcrumbs.xhr && 'XMLHttpRequest' in _window) { var xhrproto = XMLHttpRequest.prototype; fill(xhrproto, 'open', function(origOpen) { return function (method, url) { // preserve arity @@ -986,14 +974,14 @@ Raven.prototype = { // Capture breadcrumbs from any click that is unhandled / bubbled up all the way // to the document. Do this before we instrument addEventListener. if (autoBreadcrumbs.dom && this._hasDocument) { - if (document.addEventListener) { - document.addEventListener('click', self._breadcrumbEventHandler('click'), false); - document.addEventListener('keypress', self._keypressEventHandler(), false); + if (_document.addEventListener) { + _document.addEventListener('click', self._breadcrumbEventHandler('click'), false); + _document.addEventListener('keypress', self._keypressEventHandler(), false); } else { // IE8 Compatibility - document.attachEvent('onclick', self._breadcrumbEventHandler('click')); - document.attachEvent('onkeypress', self._keypressEventHandler()); + _document.attachEvent('onclick', self._breadcrumbEventHandler('click')); + _document.attachEvent('onkeypress', self._keypressEventHandler()); } } @@ -1001,13 +989,13 @@ Raven.prototype = { // NOTE: in Chrome App environment, touching history.pushState, *even inside // a try/catch block*, will cause Chrome to output an error to console.error // borrowed from: https://github.com/angular/angular.js/pull/13945/files - var chrome = window.chrome; + var chrome = _window.chrome; var isChromePackagedApp = chrome && chrome.app && chrome.app.runtime; - var hasPushState = !isChromePackagedApp && window.history && history.pushState; + var hasPushState = !isChromePackagedApp && _window.history && history.pushState; if (autoBreadcrumbs.location && hasPushState) { // TODO: remove onpopstate handler on uninstall() - var oldOnPopState = window.onpopstate; - window.onpopstate = function () { + var oldOnPopState = _window.onpopstate; + _window.onpopstate = function () { var currentHref = self._location.href; self._captureUrlChange(self._lastHref, currentHref); @@ -1033,7 +1021,7 @@ Raven.prototype = { }, wrappedBuiltIns); } - if (autoBreadcrumbs.console && 'console' in window && console.log) { + if (autoBreadcrumbs.console && 'console' in _window && console.log) { // console var consoleMethodCallback = function (msg, data) { self.captureBreadcrumb({ @@ -1232,7 +1220,7 @@ Raven.prototype = { }, _getHttpData: function() { - if (!this._hasDocument || !document.location || !document.location.href) { + if (!this._hasDocument || !_document.location || !_document.location.href) { return; } @@ -1242,10 +1230,10 @@ Raven.prototype = { } }; - httpData.url = document.location.href; + httpData.url = _document.location.href; - if (document.referrer) { - httpData.headers.Referer = document.referrer; + if (_document.referrer) { + httpData.headers.Referer = _document.referrer; } return httpData; @@ -1319,6 +1307,10 @@ Raven.prototype = { this._sendProcessedPayload(data); }, + _getUuid: function () { + return uuid4(); + }, + _sendProcessedPayload: function(data, callback) { var self = this; var globalOptions = this._globalOptions; @@ -1326,7 +1318,7 @@ Raven.prototype = { // Send along an event_id if not explicitly passed. // This event_id can be used to reference the error within Sentry itself. // Set lastEventId after we know the error should actually be sent - this._lastEventId = data.event_id || (data.event_id = uuid4()); + this._lastEventId = data.event_id || (data.event_id = this._getUuid()); // Try and clean up the packet before sending by truncating long values data = this._trimPacket(data); @@ -1442,6 +1434,285 @@ Raven.prototype = { } }; +/*------------------------------------------------ + * utils + * + * conditionally exported for test via Raven.utils + ================================================= + */ +var objectPrototype = Object.prototype; + +function isUndefined(what) { + return what === void 0; +} + +function isFunction(what) { + return typeof what === 'function'; +} + +function isString(what) { + return objectPrototype.toString.call(what) === '[object String]'; +} + +function isObject(what) { + return typeof what === 'object' && what !== null; +} + +function isEmptyObject(what) { + for (var _ in what) return false; // eslint-disable-line guard-for-in, no-unused-vars + return true; +} + +// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 +// with some tiny modifications +function isError(what) { + var toString = objectPrototype.toString.call(what); + return isObject(what) && + toString === '[object Error]' || + toString === '[object Exception]' || // Firefox NS_ERROR_FAILURE Exceptions + what instanceof Error; +} + +function each(obj, callback) { + var i, j; + + if (isUndefined(obj.length)) { + for (i in obj) { + if (hasKey(obj, i)) { + callback.call(null, i, obj[i]); + } + } + } else { + j = obj.length; + if (j) { + for (i = 0; i < j; i++) { + callback.call(null, i, obj[i]); + } + } + } +} + +function objectMerge(obj1, obj2) { + if (!obj2) { + return obj1; + } + each(obj2, function(key, value){ + obj1[key] = value; + }); + return obj1; +} + +function truncate(str, max) { + return !max || str.length <= max ? str : str.substr(0, max) + '\u2026'; +} + +/** + * hasKey, a better form of hasOwnProperty + * Example: hasKey(MainHostObject, property) === true/false + * + * @param {Object} host object to check property + * @param {string} key to check + */ +function hasKey(object, key) { + return objectPrototype.hasOwnProperty.call(object, key); +} + +function joinRegExp(patterns) { + // Combine an array of regular expressions and strings into one large regexp + // Be mad. + var sources = [], + i = 0, len = patterns.length, + pattern; + + for (; i < len; i++) { + pattern = patterns[i]; + if (isString(pattern)) { + // If it's a string, we need to escape it + // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')); + } else if (pattern && pattern.source) { + // If it's a regexp already, we want to extract the source + sources.push(pattern.source); + } + // Intentionally skip other cases + } + return new RegExp(sources.join('|'), 'i'); +} + +function urlencode(o) { + var pairs = []; + each(o, function(key, value) { + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return pairs.join('&'); +} + +// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B +// intentionally using regex and not href parsing trick because React Native and other +// environments where DOM might not be available +function parseUrl(url) { + var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); + if (!match) return {}; + + // coerce to undefined values to empty string so we don't get 'undefined' + var query = match[6] || ''; + var fragment = match[8] || ''; + return { + protocol: match[2], + host: match[4], + path: match[5], + relative: match[5] + query + fragment // everything minus origin + }; +} +function uuid4() { + var crypto = window.crypto || window.msCrypto; + + if (!isUndefined(crypto) && crypto.getRandomValues) { + // Use window.crypto API if available + var arr = new Uint16Array(8); + crypto.getRandomValues(arr); + + // set 4 in byte 7 + arr[3] = arr[3] & 0xFFF | 0x4000; + // set 2 most significant bits of byte 9 to '10' + arr[4] = arr[4] & 0x3FFF | 0x8000; + + var pad = function(num) { + var v = num.toString(16); + while (v.length < 4) { + v = '0' + v; + } + return v; + }; + + return pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + + pad(arr[5]) + pad(arr[6]) + pad(arr[7]); + } else { + // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, + v = c === 'x' ? r : r&0x3|0x8; + return v.toString(16); + }); + } +} + +/** + * Given a child DOM element, returns a query-selector statement describing that + * and its ancestors + * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] + * @param elem + * @returns {string} + */ +function htmlTreeAsString(elem) { + /* eslint no-extra-parens:0*/ + var MAX_TRAVERSE_HEIGHT = 5, + MAX_OUTPUT_LEN = 80, + out = [], + height = 0, + len = 0, + separator = ' > ', + sepLength = separator.length, + nextStr; + + while (elem && height++ < MAX_TRAVERSE_HEIGHT) { + + nextStr = htmlElementAsString(elem); + // bail out if + // - nextStr is the 'html' element + // - the length of the string that would be created exceeds MAX_OUTPUT_LEN + // (ignore this limit if we are on the first iteration) + if (nextStr === 'html' || height > 1 && len + (out.length * sepLength) + nextStr.length >= MAX_OUTPUT_LEN) { + break; + } + + out.push(nextStr); + + len += nextStr.length; + elem = elem.parentNode; + } + + return out.reverse().join(separator); +} + +/** + * Returns a simple, query-selector representation of a DOM element + * e.g. [HTMLElement] => input#foo.btn[name=baz] + * @param HTMLElement + * @returns {string} + */ +function htmlElementAsString(elem) { + var out = [], + className, + classes, + key, + attr, + i; + + if (!elem || !elem.tagName) { + return ''; + } + + out.push(elem.tagName.toLowerCase()); + if (elem.id) { + out.push('#' + elem.id); + } + + className = elem.className; + if (className && isString(className)) { + classes = className.split(' '); + for (i = 0; i < classes.length; i++) { + out.push('.' + classes[i]); + } + } + var attrWhitelist = ['type', 'name', 'title', 'alt']; + for (i = 0; i < attrWhitelist.length; i++) { + key = attrWhitelist[i]; + attr = elem.getAttribute(key); + if (attr) { + out.push('[' + key + '="' + attr + '"]'); + } + } + return out.join(''); +} + +/** + * Polyfill a method + * @param obj object e.g. `document` + * @param name method name present on object e.g. `addEventListener` + * @param replacement replacement function + * @param track {optional} record instrumentation to an array + */ +function fill(obj, name, replacement, track) { + var orig = obj[name]; + obj[name] = replacement(orig); + if (track) { + track.push([obj, name, orig]); + } +} + +if (typeof __DEV__ !== 'undefined' && __DEV__) { + Raven.utils = { + isUndefined: isUndefined, + isFunction: isFunction, + isString: isString, + isObject: isObject, + isEmptyObject: isEmptyObject, + isError: isError, + each: each, + objectMerge: objectMerge, + truncate: truncate, + hasKey: hasKey, + joinRegExp: joinRegExp, + urlencode: urlencode, + uuid4: uuid4, + htmlTreeAsString: htmlTreeAsString, + htmlElementAsString: htmlElementAsString, + parseUrl: parseUrl, + fill: fill + }; +}; + // Deprecations Raven.prototype.setUser = Raven.prototype.setUserContext; Raven.prototype.setReleaseContext = Raven.prototype.setRelease; diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index adbc62aafe7b..000000000000 --- a/src/utils.js +++ /dev/null @@ -1,272 +0,0 @@ -/*eslint no-extra-parens:0*/ -'use strict'; - -var objectPrototype = Object.prototype; - -function isUndefined(what) { - return what === void 0; -} - -function isFunction(what) { - return typeof what === 'function'; -} - -function isString(what) { - return objectPrototype.toString.call(what) === '[object String]'; -} - -function isObject(what) { - return typeof what === 'object' && what !== null; -} - -function isEmptyObject(what) { - for (var _ in what) return false; // eslint-disable-line guard-for-in, no-unused-vars - return true; -} - -// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 -// with some tiny modifications -function isError(what) { - var toString = objectPrototype.toString.call(what); - return isObject(what) && - toString === '[object Error]' || - toString === '[object Exception]' || // Firefox NS_ERROR_FAILURE Exceptions - what instanceof Error; -} - -function each(obj, callback) { - var i, j; - - if (isUndefined(obj.length)) { - for (i in obj) { - if (hasKey(obj, i)) { - callback.call(null, i, obj[i]); - } - } - } else { - j = obj.length; - if (j) { - for (i = 0; i < j; i++) { - callback.call(null, i, obj[i]); - } - } - } -} - -function objectMerge(obj1, obj2) { - if (!obj2) { - return obj1; - } - each(obj2, function(key, value){ - obj1[key] = value; - }); - return obj1; -} - -function truncate(str, max) { - return !max || str.length <= max ? str : str.substr(0, max) + '\u2026'; -} - -/** - * hasKey, a better form of hasOwnProperty - * Example: hasKey(MainHostObject, property) === true/false - * - * @param {Object} host object to check property - * @param {string} key to check - */ -function hasKey(object, key) { - return objectPrototype.hasOwnProperty.call(object, key); -} - -function joinRegExp(patterns) { - // Combine an array of regular expressions and strings into one large regexp - // Be mad. - var sources = [], - i = 0, len = patterns.length, - pattern; - - for (; i < len; i++) { - pattern = patterns[i]; - if (isString(pattern)) { - // If it's a string, we need to escape it - // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')); - } else if (pattern && pattern.source) { - // If it's a regexp already, we want to extract the source - sources.push(pattern.source); - } - // Intentionally skip other cases - } - return new RegExp(sources.join('|'), 'i'); -} - -function urlencode(o) { - var pairs = []; - each(o, function(key, value) { - pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }); - return pairs.join('&'); -} - -// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B -// intentionally using regex and not href parsing trick because React Native and other -// environments where DOM might not be available -function parseUrl(url) { - var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/); - if (!match) return {}; - - // coerce to undefined values to empty string so we don't get 'undefined' - var query = match[6] || ''; - var fragment = match[8] || ''; - return { - protocol: match[2], - host: match[4], - path: match[5], - relative: match[5] + query + fragment // everything minus origin - }; -} -function uuid4() { - var crypto = window.crypto || window.msCrypto; - - if (!isUndefined(crypto) && crypto.getRandomValues) { - // Use window.crypto API if available - var arr = new Uint16Array(8); - crypto.getRandomValues(arr); - - // set 4 in byte 7 - arr[3] = arr[3] & 0xFFF | 0x4000; - // set 2 most significant bits of byte 9 to '10' - arr[4] = arr[4] & 0x3FFF | 0x8000; - - var pad = function(num) { - var v = num.toString(16); - while (v.length < 4) { - v = '0' + v; - } - return v; - }; - - return pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + - pad(arr[5]) + pad(arr[6]) + pad(arr[7]); - } else { - // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 - return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, - v = c === 'x' ? r : r&0x3|0x8; - return v.toString(16); - }); - } -} - -/** - * Given a child DOM element, returns a query-selector statement describing that - * and its ancestors - * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] - * @param elem - * @returns {string} - */ -function htmlTreeAsString(elem) { - var MAX_TRAVERSE_HEIGHT = 5, - MAX_OUTPUT_LEN = 80, - out = [], - height = 0, - len = 0, - separator = ' > ', - sepLength = separator.length, - nextStr; - - while (elem && height++ < MAX_TRAVERSE_HEIGHT) { - - nextStr = htmlElementAsString(elem); - // bail out if - // - nextStr is the 'html' element - // - the length of the string that would be created exceeds MAX_OUTPUT_LEN - // (ignore this limit if we are on the first iteration) - if (nextStr === 'html' || height > 1 && len + (out.length * sepLength) + nextStr.length >= MAX_OUTPUT_LEN) { - break; - } - - out.push(nextStr); - - len += nextStr.length; - elem = elem.parentNode; - } - - return out.reverse().join(separator); -} - -/** - * Returns a simple, query-selector representation of a DOM element - * e.g. [HTMLElement] => input#foo.btn[name=baz] - * @param HTMLElement - * @returns {string} - */ -function htmlElementAsString(elem) { - var out = [], - className, - classes, - key, - attr, - i; - - if (!elem || !elem.tagName) { - return ''; - } - - out.push(elem.tagName.toLowerCase()); - if (elem.id) { - out.push('#' + elem.id); - } - - className = elem.className; - if (className && isString(className)) { - classes = className.split(' '); - for (i = 0; i < classes.length; i++) { - out.push('.' + classes[i]); - } - } - var attrWhitelist = ['type', 'name', 'title', 'alt']; - for (i = 0; i < attrWhitelist.length; i++) { - key = attrWhitelist[i]; - attr = elem.getAttribute(key); - if (attr) { - out.push('[' + key + '="' + attr + '"]'); - } - } - return out.join(''); -} - -/** - * Polyfill a method - * @param obj object e.g. `document` - * @param name method name present on object e.g. `addEventListener` - * @param replacement replacement function - * @param track {optional} record instrumentation to an array - */ -function fill(obj, name, replacement, track) { - var orig = obj[name]; - obj[name] = replacement(orig); - if (track) { - track.push([obj, name, orig]); - } -} - -module.exports = { - isUndefined: isUndefined, - isFunction: isFunction, - isString: isString, - isObject: isObject, - isEmptyObject: isEmptyObject, - isError: isError, - each: each, - objectMerge: objectMerge, - truncate: truncate, - hasKey: hasKey, - joinRegExp: joinRegExp, - urlencode: urlencode, - uuid4: uuid4, - htmlTreeAsString: htmlTreeAsString, - htmlElementAsString: htmlElementAsString, - parseUrl: parseUrl, - fill: fill -}; diff --git a/test/index.html b/test/index.html index 63028e17b21d..c273d684532b 100644 --- a/test/index.html +++ b/test/index.html @@ -19,7 +19,7 @@