<commit_before>/**
 * @class $JSKK
 * //Description
 *
 *
 *
 * @author Timothy Chandler tim@s3.net.au
 * @version 1.0.0
 */
var $JSKK=
{
	version:		'1.0.0',
	emptyFunction:	function(){},
	/**
	 * Use this function to create a namespace.
	 *
	 * This function will safely create a namespace and return it for use.
	 * Use this function before adding anything to a namespaced object to assure
	 * it exists and you won't suffer data loss.
	 *
	 * @member $JSKK
	 * @param {String} namespace A string representation of the namespace to be created.
	 * @return {Object} namespace
	 */
	namespace:(function()
	{
		var validIdentifier=/^(?:[\$a-zA-Z_]\w*[.])*[\$a-zA-Z_]\w*$/;
		function inOrderDescend(t,initialContext)
		{
			var i,N;
			if (Object.isString(t))
			{
				var context,parts;
				if (!validIdentifier.test(t))
				{
					throw new Error('"'+t+'" is not a valid name for a package.');
				}
				context	=initialContext;
				parts	=t.split('.');
				for (var i=0,N=parts.length;i<N;i++)
				{
					t			=parts[i];
					context[t]	=context[t] || {};
					context		=context[t];
				}
				return context;
			}
			else
			{
				throw new TypeError();
			}
		}
		return function(spec,context)
		{
			return inOrderDescend(spec, context||window);
		};
	})(),
	/**
	 * Imports packagesinto a localized scope.
	 *
	 * This function simulates packages by allowing you to import a packages
	 * into a localized scope. This can also be thought of as importing
	 * a set of namespaces which can then be aliased (maintaining reference)
	 * in the localized scope.
	 *
	 * @member $JSKK
	 * @param {Mixed} arguments Accepts unlimited arguments to be imported.
	 * @return {Function} Function which accepts another function to act as the locaized scope.
	 */
	usingPackage: function()
	{
		var args=arguments;
		return function(inner){return inner.apply(args[0],args);};
	},
	toArray: function(iterable)
	{
		if (!iterable)return [];
		if (!(typeof iterable=='function' && iterable=='[object NodeList]') && iterable.toArray)return iterable.toArray();
		var	length=iterable.length || 0,
			results=new Array(length);
		while (length--)results[length]=iterable[length];
		return results;
	}
}
if (Object.isUndefined(window.console))
{
	window.console=
	{
		log:	$JSKK.emptyFunction,
		debug:	$JSKK.emptyFunction,
		warn:	$JSKK.emptyFunction,
		info:	$JSKK.emptyFunction,
		trace:	$JSKK.emptyFunction
	}
}
else
{
	if (Object.isUndefined(window.console.debug))
	{
		window.console.debug=window.console.log;
	}
}<commit_msg>Updated version number.<commit_after>/**
 * @class $JSKK
 * //Description
 *
 *
 *
 * @author Timothy Chandler tim@s3.net.au
 * @version 1.0.2
 */
var $JSKK=
{
	version:		'1.0.2',
	emptyFunction:	function(){},
	/**
	 * Use this function to create a namespace.
	 *
	 * This function will safely create a namespace and return it for use.
	 * Use this function before adding anything to a namespaced object to assure
	 * it exists and you won't suffer data loss.
	 *
	 * @member $JSKK
	 * @param {String} namespace A string representation of the namespace to be created.
	 * @return {Object} namespace
	 */
	namespace:(function()
	{
		var validIdentifier=/^(?:[\$a-zA-Z_]\w*[.])*[\$a-zA-Z_]\w*$/;
		function inOrderDescend(t,initialContext)
		{
			var i,N;
			if (Object.isString(t))
			{
				var context,parts;
				if (!validIdentifier.test(t))
				{
					throw new Error('"'+t+'" is not a valid name for a package.');
				}
				context	=initialContext;
				parts	=t.split('.');
				for (var i=0,N=parts.length;i<N;i++)
				{
					t			=parts[i];
					context[t]	=context[t] || {};
					context		=context[t];
				}
				return context;
			}
			else
			{
				throw new TypeError();
			}
		}
		return function(spec,context)
		{
			return inOrderDescend(spec, context||window);
		};
	})(),
	/**
	 * Imports packagesinto a localized scope.
	 *
	 * This function simulates packages by allowing you to import a packages
	 * into a localized scope. This can also be thought of as importing
	 * a set of namespaces which can then be aliased (maintaining reference)
	 * in the localized scope.
	 *
	 * @member $JSKK
	 * @param {Mixed} arguments Accepts unlimited arguments to be imported.
	 * @return {Function} Function which accepts another function to act as the locaized scope.
	 */
	usingPackage: function()
	{
		var args=arguments;
		return function(inner){return inner.apply(args[0],args);};
	},
	toArray: function(iterable)
	{
		if (!iterable)return [];
		if (!(typeof iterable=='function' && iterable=='[object NodeList]') && iterable.toArray)return iterable.toArray();
		var	length=iterable.length || 0,
			results=new Array(length);
		while (length--)results[length]=iterable[length];
		return results;
	}
}
if (Object.isUndefined(window.console))
{
	window.console=
	{
		log:	$JSKK.emptyFunction,
		debug:	$JSKK.emptyFunction,
		warn:	$JSKK.emptyFunction,
		info:	$JSKK.emptyFunction,
		trace:	$JSKK.emptyFunction
	}
}
else
{
	if (Object.isUndefined(window.console.debug))
	{
		window.console.debug=window.console.log;
	}
}<|endoftext|>


<commit_before>var qMap = require('lie-quickmap');
var all = require('lie-all');
var promise = require('lie');
var cast = require('lie-cast');
function any(array,func){
    var hasfunc = typeof func === 'function';
    return promise(function(yes,no){
        all(qMap(array,function(value){
            var cvalue = cast(value);
            if(hasfunc){
                cvalue = cvalue.then(func);
            }
            return cvalue.then(function(pValue){
                if(pValue){
                    yes(true);
                }else{
                    return false;
                }
            });
        })).then(function(){
            yes(false);
        },no);
    });
}
module.exports = any;<commit_msg>should work with a promsie<commit_after>var qMap = require('lie-quickmap');
var all = require('lie-all');
var promise = require('lie');
var cast = require('lie-cast');
function any(array,func){
    var hasfunc = typeof func === 'function';
    return promise(function(yes,no){
        cast(array).then(function(arr){
            all(qMap(arr,function(value){
                var cvalue = cast(value);
                if(hasfunc){
                    cvalue = cvalue.then(func);
                }
                return cvalue.then(function(pValue){
                    if(pValue){
                        yes(true);
                    }else{
                        return false;
                    }
                });
            })).then(function(){
                yes(false);
            },no);
       });
    });
}
module.exports = any;
<|endoftext|>


<commit_before>var Controller = function() {
    this.firstCall = true;  // Whether we are requesting data for the first time.
    this.finished = false;  // Whether we already hit the bottom.
    this.from      = null;  // Last "from" parameter we received from the remote.
    this.waiting  = false;  // Whether we are already waiting for the remote.
    this.templates = {};
    this.$ = $(this);
    this.window = $(window);
    this.document = $(document);
    this.content = $('#page_content');
    var that = this;
    $('[data-template-partial]:not([data-registered])').each(function() {
        var item = $(this);
        Handlebars.registerPartial(item.attr('data-template-partial'), item.html());
        item.attr('data-registered', 'true');
    });
    $('[data-template]').each(function() {
        var item = $(this);
        item.attr('data-template').split(',').forEach(function(k) {
            that.templates[k] = Handlebars.compile(item.html());
        });
    });
};

Controller.prototype.supportsEmojiNatively = function() {
    if (typeof(navigator) !== 'undefined') {
        var ua = navigator.userAgent;
        if (ua.match(/(iPhone|iPod|iPad|iPhone\s+Simulator)/i)) {
            if (ua.match(/OS\s+[12345]/i)) {
                return true;
            }
            if (ua.match(/OS\s+[6789]/i)) {
                return true;
            }
        }
        if (ua.match(/Mac OS X 10[._ ](?:[789]|1\d)/i)) {
            if (!ua.match(/Chrome/i) && !ua.match(/Firefox/i)) {
                return true;
            }
        }
        if (ua.match(/Windows NT 6.[1-9]/i) || ua.match(/Windows NT 10.[0-9]/i)) {
            if (!ua.match(/Chrome/i) && !ua.match(/MSIE 8/i)) {
                return true;
            }
        }
    }
    return false;
}

Controller.prototype.render = function(item) {
    var result = '';
    if(!this.templates[item.type]) {
        console.warn(['Unregistered template for item with type ', item.type].join(''));
    } else {
        try {
            result = this.templates[item.type](item);
        } catch(ex) {
            console.error('Error rendering item with template ' + item.type, ex);
        }
    }
    return result;
}

Controller.prototype.fixEmbeds = function(items) {
    $.when(
        items.find('iframe').ready,
        items.find('img').load
    ).done(function() {
        $('.grid').isotope('layout');
    });
    $('[data-item-type="vimeo"],[data-item-type="youtube"]').find('iframe').attr({
        width: 285,
        height: 168
    });
    this.embedInterval = this.embedInterval || setInterval(function() {
        $('[data-item-type="tweet"]').each(function() {
            var $this = $(this);
            $this.find('iframe').css({
                marginTop: 0,
                marginBottom: 0
            });
        }).promise().done(function() {
            $('.grid').isotope('layout');
        });
    }, 1000);
}

Controller.prototype.fixEmojis = function() {
    if(!window.supportsEmojiNatively) {
        window.supportsEmojiNatively = this.supportsEmojiNatively();
    }
    if(!window.supportsEmojiNatively) {
        twemoji.parse(document.body);
    }
}

Controller.prototype.load = function() {
    if(this.finished || this.waiting) {
        return;
    }
    this.$.trigger('loading');
    this.waiting = true;
    $.ajax({
            url: this.firstCall ? '/api/latest' : ['/api/skip/', this.next].join(''),
            method: 'GET',
            type: 'json'
        })
        .done(function(data) {
            this.next = data.next;
            this.$.trigger('loaded');
            this.waiting = false;
            if(!data.items) {
                this.finished = true;
                this.$.trigger('finished');
                return;
            }
            var $grid = $('.grid'),
                result = $();

            data.items.items.forEach(function(item) {
                result = result.add($(this.render(item)));
            }.bind(this));

            if(this.firstCall) {
                this.firstCall = false;
                $grid
                    .append(result)
                    .isotope({
                        itemSelector: '.grid-item',
                        masonry: {
                            columnWidth: '.grid-item'
                        }
                    });
            } else {
                $grid.isotope('insert', result);
            }
            this.fixEmbeds(result);
            this.fixEmojis();
        }.bind(this))
        .fail(function() {
            this.waiting = false;
            this.$.trigger('failed');
        }.bind(this));
};

Controller.prototype.handleScroll = function() {
    if(this.window.scrollTop() + this.window.height() > this.document.height() - this.window.height()) {
        this.load();
    }
}

Controller.prototype.fixHeaderHeight = function() {
    this.content.css({
        paddingTop: this.window.outerHeight()
    });
    this.window.resize($.debounce(function() {
        this.fixHeaderHeight()
    }.bind(this), 300));
}

$(function() {
    Handlebars.registerHelper('truncate', function(options) {
        var value = options.data.root.summary;
        if(value.length > 255) {
            var parts = value.split(' '),
                part;
            value = '';
            while(parts) {
                part = parts.shift();
                if(value.length + part.length > 255) {
                    value += '...';
                    break;
                } else {
                    value += ' ' + part;
                }
            }
        }
        return value;
    });
    Handlebars.registerHelper('domain', function(options) {
        var value = options.fn(this),
            regex = /(?:https?:\/\/)?([^\/]+).*/;
        if(regex.test(value)) {
            var match = regex.exec(value);
            value = regex[1];
        }
        return value;
    });

    $.debounce = function(func, wait) {
        var timeout;
        return function() {
            var context = this, args = arguments,
                then = function() {
                    timeout = null;
                    func.apply(context, args);
                };
            if(timeout) {
                clearTimeout(timeout);
            }
            timeout = setTimeout(then, wait);
        }
    };

    var fadeStart  = 0,
        fadeUntil  = Math.max(1, screen.height / 2),
        fadeTarget = $('#page_cover header, .cover_pattern'),
        fadeFooter = $('footer'),
        $document  = $(document),
        controller = new Controller(),
        scrollHandler = $.debounce(controller.handleScroll.bind(controller), 300);

    controller.load();
    controller.fixHeaderHeight();

    $(window).scroll(function() {
        scrollHandler();
        var offset  = $document.scrollTop(),
            opacity = 0;
        if(offset <= fadeStart) {
            opacity = 1;
        } else if(offset <= fadeUntil) {
            opacity = 1 - offset / fadeUntil;
        }
        fadeTarget.css('opacity', opacity);
        fadeFooter.css('opacity', 1 - opacity);

        $("#scroll_helper").remove();
    });
});
<commit_msg>Improve handlebars helpers<commit_after>var Controller = function() {
    this.firstCall = true;  // Whether we are requesting data for the first time.
    this.finished = false;  // Whether we already hit the bottom.
    this.from      = null;  // Last "from" parameter we received from the remote.
    this.waiting  = false;  // Whether we are already waiting for the remote.
    this.templates = {};
    this.$ = $(this);
    this.window = $(window);
    this.document = $(document);
    this.content = $('#page_content');
    var that = this;
    $('[data-template-partial]:not([data-registered])').each(function() {
        var item = $(this);
        Handlebars.registerPartial(item.attr('data-template-partial'), item.html());
        item.attr('data-registered', 'true');
    });
    $('[data-template]').each(function() {
        var item = $(this);
        item.attr('data-template').split(',').forEach(function(k) {
            that.templates[k] = Handlebars.compile(item.html());
        });
    });
};

Controller.prototype.supportsEmojiNatively = function() {
    if (typeof(navigator) !== 'undefined') {
        var ua = navigator.userAgent;
        if (ua.match(/(iPhone|iPod|iPad|iPhone\s+Simulator)/i)) {
            if (ua.match(/OS\s+[12345]/i)) {
                return true;
            }
            if (ua.match(/OS\s+[6789]/i)) {
                return true;
            }
        }
        if (ua.match(/Mac OS X 10[._ ](?:[789]|1\d)/i)) {
            if (!ua.match(/Chrome/i) && !ua.match(/Firefox/i)) {
                return true;
            }
        }
        if (ua.match(/Windows NT 6.[1-9]/i) || ua.match(/Windows NT 10.[0-9]/i)) {
            if (!ua.match(/Chrome/i) && !ua.match(/MSIE 8/i)) {
                return true;
            }
        }
    }
    return false;
}

Controller.prototype.render = function(item) {
    var result = '';
    if(!this.templates[item.type]) {
        console.warn(['Unregistered template for item with type ', item.type].join(''));
    } else {
        try {
            result = this.templates[item.type](item);
        } catch(ex) {
            console.error('Error rendering item with template ' + item.type, ex);
        }
    }
    return result;
}

Controller.prototype.fixEmbeds = function(items) {
    $.when(
        items.find('iframe').ready,
        items.find('img').load
    ).done(function() {
        $('.grid').isotope('layout');
    });
    $('[data-item-type="vimeo"],[data-item-type="youtube"]').find('iframe').attr({
        width: 285,
        height: 168
    });
    this.embedInterval = this.embedInterval || setInterval(function() {
        $('[data-item-type="tweet"]').each(function() {
            var $this = $(this);
            $this.find('iframe').css({
                marginTop: 0,
                marginBottom: 0
            });
        }).promise().done(function() {
            $('.grid').isotope('layout');
        });
    }, 1000);
}

Controller.prototype.fixEmojis = function() {
    if(!window.supportsEmojiNatively) {
        window.supportsEmojiNatively = this.supportsEmojiNatively();
    }
    if(!window.supportsEmojiNatively) {
        twemoji.parse(document.body);
    }
}

Controller.prototype.load = function() {
    if(this.finished || this.waiting) {
        return;
    }
    this.$.trigger('loading');
    this.waiting = true;
    $.ajax({
            url: this.firstCall ? '/api/latest' : ['/api/skip/', this.next].join(''),
            method: 'GET',
            type: 'json'
        })
        .done(function(data) {
            this.next = data.next;
            this.$.trigger('loaded');
            this.waiting = false;
            if(!data.items) {
                this.finished = true;
                this.$.trigger('finished');
                return;
            }
            var $grid = $('.grid'),
                result = $();

            data.items.items.forEach(function(item) {
                result = result.add($(this.render(item)));
            }.bind(this));

            if(this.firstCall) {
                this.firstCall = false;
                $grid
                    .append(result)
                    .isotope({
                        itemSelector: '.grid-item',
                        masonry: {
                            columnWidth: '.grid-item'
                        }
                    });
            } else {
                $grid.isotope('insert', result);
            }
            this.fixEmbeds(result);
            this.fixEmojis();
        }.bind(this))
        .fail(function() {
            this.waiting = false;
            this.$.trigger('failed');
        }.bind(this));
};

Controller.prototype.handleScroll = function() {
    if(this.window.scrollTop() + this.window.height() > this.document.height() - this.window.height()) {
        this.load();
    }
}

Controller.prototype.fixHeaderHeight = function() {
    this.content.css({
        paddingTop: this.window.outerHeight()
    });
    this.window.resize($.debounce(function() {
        this.fixHeaderHeight()
    }.bind(this), 300));
}

$(function() {
    Handlebars.registerHelper('truncate', function(opts) {
        if(typeof opts === 'object') {
            opts = opts.fn(this);
        }

        if(opts.length > 255) {
            var parts = opts.split(' '),
                part;
            opts = [];
            while(parts) {
                part = parts.shift();
                if(opts.join(' ').length + part.length > 255) {
                    break;
                }
                opts.push(part);
            }
            opts = opts.join(' ') + '...';
        }

        return opts;
    });

    Handlebars.registerHelper('domain', function(opts) {
        if(typeof opts === 'object') {
            opts = opts.fn(this);
        }
        var regex = /(?:https?:\/\/)?([^\/]+).*/;
        if(regex.match(opts)) {
            opts = regex.exec(opts)[1];
        }
        return opts;
    });

    $.debounce = function(func, wait) {
        var timeout;
        return function() {
            var context = this, args = arguments,
                then = function() {
                    timeout = null;
                    func.apply(context, args);
                };
            if(timeout) {
                clearTimeout(timeout);
            }
            timeout = setTimeout(then, wait);
        }
    };

    var fadeStart  = 0,
        fadeUntil  = Math.max(1, screen.height / 2),
        fadeTarget = $('#page_cover header, .cover_pattern'),
        fadeFooter = $('footer'),
        $document  = $(document),
        controller = new Controller(),
        scrollHandler = $.debounce(controller.handleScroll.bind(controller), 300);

    controller.load();
    controller.fixHeaderHeight();

    $(window).scroll(function() {
        scrollHandler();
        var offset  = $document.scrollTop(),
            opacity = 0;
        if(offset <= fadeStart) {
            opacity = 1;
        } else if(offset <= fadeUntil) {
            opacity = 1 - offset / fadeUntil;
        }
        fadeTarget.css('opacity', opacity);
        fadeFooter.css('opacity', 1 - opacity);

        $("#scroll_helper").remove();
    });
});
<|endoftext|>


<commit_before>/*
 * Copyright 2014-2016 Hewlett-Packard Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'find/app/page/settings/aci-widget',
    'find/app/page/settings/enable-view',
    'text!find/templates/app/page/settings/query-manipulation-widget.html'
], function(AciWidget, EnableView, queryManipulationTemplate) {

    return AciWidget.extend({

        queryManipulationTemplate: _.template(queryManipulationTemplate),

        initialize: function() {
            AciWidget.prototype.initialize.apply(this, arguments);

            this.enableView = new EnableView({
                enableIcon: 'fa fa-file',
                strings: this.strings
            })
        },

        render: function() {
            AciWidget.prototype.render.apply(this, arguments);

            this.enableView.render();
            var $validateButtonParent = this.$('button[name=validate]').parent();
            $validateButtonParent.before(this.enableView.el);

            $validateButtonParent.before(this.queryManipulationTemplate({
                strings: this.strings
            }));

            this.$blacklist = this.$('.blacklist-input');
            this.$typeahead = this.$('.typeahead-input');
            this.$expandQuery = this.$('.expand-query-input');
        },

        getConfig: function() {
            return {
                blacklist: this.$blacklist.val(),
                expandQuery: this.$expandQuery.prop('checked'),
                enabled: this.enableView.getConfig(),
                server: AciWidget.prototype.getConfig.call(this),
                typeAheadMode: this.$typeahead.val()
            }
        },

        updateConfig: function(config) {
            AciWidget.prototype.updateConfig.call(this, config.server);

            this.enableView.updateConfig(config.enabled);

            this.$blacklist.val(config.blacklist);
            this.$typeahead.val(config.typeAheadMode);
            this.$expandQuery.prop('checked', config.expandQuery);
        },

        shouldValidate: function() {
            return this.enableView.getConfig();
        }
    });

});<commit_msg>[CCUK-3425] Fix required setting asterisk<commit_after>/*
 * Copyright 2014-2016 Hewlett-Packard Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

define([
    'find/app/page/settings/aci-widget',
    'find/app/page/settings/enable-view',
    'text!find/templates/app/page/settings/query-manipulation-widget.html'
], function(AciWidget, EnableView, queryManipulationTemplate) {

    return AciWidget.extend({

        queryManipulationTemplate: _.template(queryManipulationTemplate),

        initialize: function() {
            AciWidget.prototype.initialize.apply(this, arguments);

            this.enableView = new EnableView({
                enableIcon: 'fa fa-file',
                strings: this.strings
            })
        },

        render: function() {
            AciWidget.prototype.render.apply(this, arguments);

            this.enableView.render();
            var $validateButtonParent = this.$('button[name=validate]').parent();
            $validateButtonParent.before(this.enableView.el);

            $validateButtonParent.before(this.queryManipulationTemplate({
                strings: this.strings
            }));

            this.$blacklist = this.$('.blacklist-input');
            this.$typeahead = this.$('.typeahead-input');
            this.$expandQuery = this.$('.expand-query-input');

            this.listenTo(this.enableView, 'change', function() {
                this.$('.settings-required-flag').toggleClass('hide', !this.enableView.getConfig());
            })
        },

        getConfig: function() {
            return {
                blacklist: this.$blacklist.val(),
                expandQuery: this.$expandQuery.prop('checked'),
                enabled: this.enableView.getConfig(),
                server: AciWidget.prototype.getConfig.call(this),
                typeAheadMode: this.$typeahead.val()
            }
        },

        updateConfig: function(config) {
            AciWidget.prototype.updateConfig.call(this, config.server);

            this.enableView.updateConfig(config.enabled);

            this.$blacklist.val(config.blacklist);
            this.$typeahead.val(config.typeAheadMode);
            this.$expandQuery.prop('checked', config.expandQuery);
        },

        shouldValidate: function() {
            return this.enableView.getConfig();
        }
    });

});<|endoftext|>


<commit_before>/**@module api-requester*/
modules.define('api-requester', [
    'api-request', 'app-logger', 'objects'
], function (provide, ApiRequest, logger, objects, ApiRequester) {
    "use strict";

    var request = require('request'),
        Readable = require('stream').Readable;

    /**
     * @class ApiRequester
     * @extends BEM
     * @exports
     */
    provide(ApiRequester.decl(/**@lends ApiRequester.prototype*/{

        /**
         * @param {String} method
         * @param {String} [route]
         * @param {?Object} [routeParameters]
         * @param {String|Object|Readable} [body]
         * @returns {ApiRequest}
         */
        sendRequest: function (method, route, routeParameters, body) {
            var url = this.params.router.generate(route, routeParameters),
                apiRequest = new ApiRequest(),
                _this = this,
                jar = request.jar(this.params.cookieStorage);

            var requestCallback = function (err, res, body) {
                var parsedBody;

                try {
                    parsedBody = _this._processBody(res, body);
                } catch (e) {
                    logger.error('API response on ' + method.toUpperCase() + ' ' + url + ' can not be parsed');
                    logger.error('API response body:');
                    logger.error(body);
                    apiRequest.reject(e);
                    return;
                }

                var output = {
                    error: err ? err.message : '',
                    body: parsedBody
                };
                if (res) {
                    output.response = {
                        statusCode: res.statusCode,
                        statusText: res.statusText
                    };
                }
                var badStatus = res && (res.statusCode < 200 || res.statusCode > 299);
                if (badStatus) {
                    if (!output.error) {
                        output.error = output.body;
                    }
                }
                if (err) {
                    logger.error('API request ' + method.toUpperCase() + ' ' + url + ' error:');
                    logger.error(err);
                    apiRequest.reject(output);
                } else if (badStatus) {
                    logger.warn('API request ' + method.toUpperCase() + ' ' + url + ' has status ' + res.statusCode);
                    logger.warn(output.error);
                    apiRequest.reject(output);
                } else {
                    apiRequest.resolve(output);
                }
            };

            var req;
            if (body && body instanceof Readable) {
                req = body.pipe(request({
                    url: url,
                    method: method,
                    jar: jar
                }, requestCallback));
            } else {
                var preparedBody = typeof body === 'object' ? JSON.stringify(body) : body;
                req = request({
                    url: url,
                    method: method,
                    headers: this._getRequestHeaders(route),
                    body: preparedBody,
                    gzip: true,
                    jar: jar
                }, requestCallback);
            }
            apiRequest.setRequest(req);
            return apiRequest;
        },

        /**
         * @param {String} route
         * @returns {Object}
         * @protected
         */
        _getRequestHeaders: function (route) {
            return {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Connection': 'keep-alive'
            };
        },

        /**
         * @param {OutgoingMessage} res
         * @param {String} body
         * @returns {String|Object}
         * @throws {Error} if JSON is malformed
         * @private
         */
        _processBody: function (res, body) {
            if (res && /json/.test(res.headers['content-type'])) {
                return JSON.parse(body);
            }
            return body;
        }
    }));
});
<commit_msg>Add ability to redefine requests sending from node to api<commit_after>/**@module api-requester*/
modules.define('api-requester', [
    'api-request', 'app-logger', 'objects'
], function (provide, ApiRequest, logger, objects, ApiRequester) {
    "use strict";

    var request = require('request'),
        Readable = require('stream').Readable;

    /**
     * @class ApiRequester
     * @extends BEM
     * @exports
     */
    provide(ApiRequester.decl(/**@lends ApiRequester.prototype*/{

        /**
         * @type function
         * @protected
         */
        _request: request,

        /**
         * @param {String} method
         * @param {String} [route]
         * @param {?Object} [routeParameters]
         * @param {String|Object|Readable} [body]
         * @returns {ApiRequest}
         */
        sendRequest: function (method, route, routeParameters, body) {
            var url = this.params.router.generate(route, routeParameters),
                apiRequest = new ApiRequest(),
                _this = this;

            apiRequest.setRequest(this._sendRequest({
                route: route,
                url: url,
                method: method,
                body: body,
                jar: request.jar(this.params.cookieStorage)
            }, function (err, res, body) {
                var parsedBody;

                try {
                    parsedBody = _this._processBody(res, body);
                } catch (e) {
                    logger.error('API response on ' + method.toUpperCase() + ' ' + url + ' can not be parsed');
                    logger.error('API response body:');
                    logger.error(body);
                    apiRequest.reject(e);
                    return;
                }

                var output = {
                    error: err ? err.message : '',
                    body: parsedBody
                };
                if (res) {
                    output.response = {
                        statusCode: res.statusCode,
                        statusText: res.statusText
                    };
                }
                var badStatus = res && (res.statusCode < 200 || res.statusCode > 299);
                if (badStatus) {
                    if (!output.error) {
                        output.error = output.body;
                    }
                }
                if (err) {
                    logger.error('API request ' + method.toUpperCase() + ' ' + url + ' error:');
                    logger.error(err);
                    apiRequest.reject(output);
                } else if (badStatus) {
                    logger.warn('API request ' + method.toUpperCase() + ' ' + url + ' has status ' + res.statusCode);
                    logger.warn(output.error);
                    apiRequest.reject(output);
                } else {
                    apiRequest.resolve(output);
                }
            }));
            return apiRequest;
        },

        /**
         * @param {object} options
         * @param {string} options.route
         * @param {string} options.url
         * @param {string} options.method
         * @param {string|object} options.body
         * @param {CookieJar} options.jar
         * @param {function} requestCallback
         * @returns {object}
         * @protected
         */
        _sendRequest: function (options, requestCallback) {
            var body = options.body,
                requestParameters = {
                    url: options.url,
                    method: options.method,
                    jar: options.jar
                };
            if (body && body instanceof Readable) {
                return body.pipe(this._request(requestParameters, requestCallback));
            }

            var preparedBody = typeof body === 'object' ? JSON.stringify(body) : body;
            return this._request(objects.extend(requestParameters, {
                headers: this._getRequestHeaders(options.route),
                body: preparedBody,
                gzip: true
            }), requestCallback);
        },

        /**
         * @param {String} route
         * @returns {Object}
         * @protected
         */
        _getRequestHeaders: function (route) {
            return {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'Connection': 'keep-alive'
            };
        },

        /**
         * @param {OutgoingMessage} res
         * @param {String} body
         * @returns {String|Object}
         * @throws {Error} if JSON is malformed
         * @private
         */
        _processBody: function (res, body) {
            if (res && /json/.test(res.headers['content-type'])) {
                return JSON.parse(body);
            }
            return body;
        }
    }));
});
<|endoftext|>


<commit_before>'use strict';

const url = require('url');
const ldap = require('ldapjs');
const utils = require('./utilities');
const async = require('async');

let ad;
let log;
let RangeAttributeParser;

function isReferralAllowed(referralUri) {
  if (ad.defaultReferrals.enabled === false || !referralUri) {
    return false;
  }

  let result = true;
  for (let excludePattern of ad.defaultReferrals.exclude) {
    const regex = new RegExp(excludePattern, 'i');
    if (regex.test(referralUri)) {
      result = false;
      break;
    }
  }

  return result;
}

/**
 * @property {string} baseDN Where in the tree to start searches.
 * @property {function} callback A function to invoke when the search has
 * completed. This method must accept an error and a result, in that order.
 * @property {object} opts All of the options relevant to an
 * {@link ActiveDirectory} instance. It must include an `opts` property that
 * represents that is an {@link LDAPQueryParameters}.
 * @private
 * @typedef {object} SearcherOptions
 */

/**
 * An interface for performing searches against an Active Directory database.
 * It handles ranged results, finding deleted items, and following referrals.
 *
 * @private
 * @param {SearcherOptions} opts
 * @constructor
 */
function Searcher(opts) { // jshint -W071
  this.baseDN = opts.baseDN;
  this.callback = opts.callback;
  this.opts = opts;
  this.ldapOpts = utils.getLdapClientOpts(this.opts);
  this.query = this.opts.opts;

  this.results = new Map();
  this.pendingReferrals = new Set();
  this.searchComplete = false;
  this.rangeProcessing = false;

  this.client = utils.createClient(ad.url || ad.opts.url, this.ldapOpts, ad);
  this.client.on('error', (err) => {
    // to handle connection errors
    this.callback(err);
  });

  this.controls = this.opts.controls || [];

  if (this.opts.opts.paged===undefined) {
    this.opts.opts.paged=true;
  }
  /*
  // Add paging results control by default if not already added.
  const pagedControls = this.controls.filter(
    (control) => control instanceof ldap.PagedResultsControl
  );
  if (pagedControls.length === 0) {
    log.trace('Adding PagedResultControl to search (%s) with filter "%s" for %j',
              this.baseDN,
              this.query.filter,
              (this.query.attributes) ? this.opts.attributes : '[*]'
    );
    this.controls.push(new ldap.PagedResultsControl({
      value: { size: ad.pageSize }
    }));
  }
  */

  if (this.opts.includeDeleted) {
    const deletedControls = this.controls.filter(
      (control) => control.type === '1.2.840.113556.1.4.417'
    );
    if (deletedControls.length === 0) {
      log.trace('Adding ShowDeletedOidControl(1.2.840.113556.1.4.417) to search (%s) with filter "%s" for %j',
                this.baseDN,
                this.query.filter,
                (this.query.attributes) ? this.query.attributes : '[*]'
      );
      this.controls.push(new ldap.Control({
        type: '1.2.840.113556.1.4.417',
        criticality: true
      }));
    }
  }
}

/**
 * The only method you should need to invoke. It uses the information parsed
 * during construction to construct the query and submit it to the server. Once
 * the query has completed, or an error occurs, the callback you specified
 * during construction will be invoked.
 */
Searcher.prototype.search = function search() {
  log.trace('Querying active directory (%s) with filter "%s" for %j',
            this.baseDN,
            this.query.filter,
            (this.query.attributes) ? this.query.attributes : '[*]'
  );

  this.client.search(this.baseDN, this.query, this.controls, (err, res) => {
    if (err) {
      if (this.callback) {
        this.callback(err);
      }
      return;
    }

    const errCallback = (err) => {
      if (err.name === 'SizeLimitExceededError') {
        this.onSearchEnd(res);
        return;
      }

      this.client.unbind();
      log.trace(err,
                '[%s] An error occurred performing the requested LDAP search on %s (%j)',
                err.errno || 'UNKNOWN',
                this.baseDN,
                this.opts
      );
      if (this.callback) {
        this.callback(err);
      }
    };

    res.on('searchEntry', this.onSearchEntry.bind(this));
    res.on('searchReference', this.onReferralChase.bind(this));
    res.on('error', errCallback);
    res.on('end', (err) => {
      this.searchComplete = true;
      this.onSearchEnd(err);
    });
  });
};

Searcher.prototype.rangeSearch = function rangeSearch(query, rangeCB) {
  log.trace('Quering (%s) for range search with filter "%s" for: %j',
            this.baseDN, query.filter, query.attributes);
  this.client.search(this.baseDN, query, this.controls, (err, res) => {
    if (err) {
      return rangeCB(err);
    }
    res.on('searchEntry', (entry) => {
      const obj = entry.object;
      rangeCB(null, obj);
    });
    res.on('searchReference', this.onReferralChase.bind(this));
    res.on('end', () => {
      this.rangeProcessing = false;
    });
    res.on('error', rangeCB);
  });
};

/**
 * If set via the options of the query or the {@link ActiveDirectory}
 * instance, run the entry through the function. Otherwise, just feed it to
 * the parser callback.
 *
 * @param {object} entry The search entry object.
 * @param {object} raw The raw search entry object as returned from ldap.js.
 * @param {function} callback The callback to execute when complete.
 */
Searcher.prototype.entryParser = function entryParser(entry, raw, callback) {
  if (this.opts.entryParser) {
    return this.opts.entryParser(entry, raw, callback);
  } else if (ad.opts.entryParser) {
    return ad.opts.entryParser(entry, raw, callback);
  }

  return callback(entry);
};

/**
 * Invoked when the ldap.js client is returning a search entry result.
 *
 * @param {object} entry The search entry as returned by ldap.js.
 */
Searcher.prototype.onSearchEntry = function onSearchEntry(entry) {
  log.trace('onSearchEntry(entry)');
  const result = entry.object;
  delete result.controls;

  // Some attributes can have range attributes (paging). Execute the query
  // again to get additional items.
  this.rangeProcessing = true;

  const rangeProcessor = new RangeAttributeParser(this);
  rangeProcessor.on('error', this.callback);
  rangeProcessor.on('done', (results) => {
    async.each(
      results,
      (result, acb) => {
        this.entryParser(
          result, entry.raw, (r) => {
            this.results.set(result.dn, r);
            this.rangeProcessing = false;
            acb();
          }
        )
      },
      () => {
        if (this.searchComplete) {
          this.onSearchEnd();
        }
      }
    );
  });

  rangeProcessor.parseResult(result);
};

/**
 * Used to handle referrals, if they are enabled.
 *
 * @param {object} referral A referral object that has a `uris` property.
 */
Searcher.prototype.onReferralChase = function onReferralChase(referral) {
  referral.uris.forEach((uri) => {
    if (!isReferralAllowed(uri)) {
      return;
    }

    log.trace('Following LDAP referral chase at %s', uri);
    const referral = url.parse(uri);
    const referralBaseDn = (referral.pathname || '/').substring(1);
    const refSearcher = new Searcher({
      baseDN: referralBaseDn,
      opts: this.opts,
      callback: (err, res) => {
        if (err) {
          log.trace(
            err,
            '[%s] An error occurred chasing the LDAP referral on %s (%j)',
            err.errno,
            referralBaseDn,
            this.opts
          );
        }
        this.removeReferral(refSearcher);
      }
    });
    this.pendingReferrals.add(refSearcher);

    refSearcher.search();
  });
};

/**
 * Invoked when the main search has completed, including any referrals.
 */
Searcher.prototype.onSearchEnd = function onSearchEnd() {
  if (!this.rangeProcessing && this.pendingReferrals.size === 0) {
    this.client.unbind();
    log.trace('Active directory search (%s) for "%s" returned %d entries.',
              this.baseDN,
              this.query.filter,
              this.results.length
    );
    if (this.callback) {
      this.callback(null, Array.from(this.results.values()));
    }
  }
};

/**
 * Dequeues a referral chase client.
 *
 * @param {object} referral An instance of {@link Searcher} being used to chase
 * a referral.
 */
Searcher.prototype.removeReferral = function removeReferral(referral) {
  if (!referral) {
    return;
  }

  referral.client.unbind();
  this.pendingReferrals.delete(referral);
};

/**
 * @property {string} [scope] The type of search to perform: base, one, or sub.
 * Default: base.
 * @property {string} [filter] The LDAP filter to use.
 * Default: '(objectclass=*)'
 * @property {array} [attributes] A list of entry attributes to include in the
 * result. Defaults to all attributes.
 * @property {int} [sizeLimit] The maximum number of entries to return.
 * Default: 0 (infinite)
 * @property {int} [timeLimit] The maximum number of seconds to wait for a
 * server response. Default: 10.
 * @typedef {object} LDAPQueryParameters
 */

/**
 * Wraps an instance of {@link Searcher} so as to be backward compatible with
 * the original search function.
 *
 * @private
 * @param {string} [baseDN] The optional base directory where the LDAP query is
 * to originate from. If not specified, then starts at the root.
 * @param {LDAPQueryParameters} [opts] LDAP query string parameters to execute.
 * @param {function} callback The callback to execure when completed.
 * The call back is a standard Node style callback with an error parameter and
 * a result parameter.
 */
function search(baseDN, opts, callback) { // jshint -W071
  const options = Object.assign({}, ad.opts);

  if (typeof baseDN === 'function') {
    options.baseDN = ad.baseDN;
    options.callback = baseDN;
    options.opts = {};
  } else if (typeof baseDN === 'object') {
    options.baseDN = baseDN.baseDN || ad.baseDN;
    options.callback = opts;
    options.opts = baseDN;
  } else if (typeof baseDN === 'string') {
    options.baseDN = baseDN;
    if (typeof opts === 'function') {
      options.callback = opts;
      options.opts = {};
    } else {
      options.callback = callback;
      options.opts = opts;
    }
  }

  const searcher = new Searcher(options);
  return searcher.search();
}

module.exports = function($ad, $log) {
  ad = $ad;
  log = $log;
  RangeAttributeParser = require('./parseRangeAttributes')($ad, $log);
  return search;
};
<commit_msg>fetch more than 1000 users<commit_after>'use strict';

const url = require('url');
const ldap = require('ldapjs');
const utils = require('./utilities');
const async = require('async');

let ad;
let log;
let RangeAttributeParser;

function isReferralAllowed(referralUri) {
  if (ad.defaultReferrals.enabled === false || !referralUri) {
    return false;
  }

  let result = true;
  for (let excludePattern of ad.defaultReferrals.exclude) {
    const regex = new RegExp(excludePattern, 'i');
    if (regex.test(referralUri)) {
      result = false;
      break;
    }
  }

  return result;
}

/**
 * @property {string} baseDN Where in the tree to start searches.
 * @property {function} callback A function to invoke when the search has
 * completed. This method must accept an error and a result, in that order.
 * @property {object} opts All of the options relevant to an
 * {@link ActiveDirectory} instance. It must include an `opts` property that
 * represents that is an {@link LDAPQueryParameters}.
 * @private
 * @typedef {object} SearcherOptions
 */

/**
 * An interface for performing searches against an Active Directory database.
 * It handles ranged results, finding deleted items, and following referrals.
 *
 * @private
 * @param {SearcherOptions} opts
 * @constructor
 */
function Searcher(opts) { // jshint -W071
  this.baseDN = opts.baseDN;
  this.callback = opts.callback;
  this.opts = opts;
  this.ldapOpts = utils.getLdapClientOpts(this.opts);
  this.query = this.opts.opts;

  this.results = new Map();
  this.pendingReferrals = new Set();
  this.searchComplete = false;
  this.rangeProcessing = false;

  this.client = utils.createClient(ad.url || ad.opts.url, this.ldapOpts, ad);
  this.client.on('error', (err) => {
    // to handle connection errors
    this.callback(err);
  });

  this.controls = this.opts.controls || [];

  if (this.opts.opts.paged === undefined) {
    this.opts.opts.paged=true;
  }

  if (this.opts.includeDeleted) {
    const deletedControls = this.controls.filter(
      (control) => control.type === '1.2.840.113556.1.4.417'
    );
    if (deletedControls.length === 0) {
      log.trace('Adding ShowDeletedOidControl(1.2.840.113556.1.4.417) to search (%s) with filter "%s" for %j',
                this.baseDN,
                this.query.filter,
                (this.query.attributes) ? this.query.attributes : '[*]'
      );
      this.controls.push(new ldap.Control({
        type: '1.2.840.113556.1.4.417',
        criticality: true
      }));
    }
  }
}

/**
 * The only method you should need to invoke. It uses the information parsed
 * during construction to construct the query and submit it to the server. Once
 * the query has completed, or an error occurs, the callback you specified
 * during construction will be invoked.
 */
Searcher.prototype.search = function search() {
  log.trace('Querying active directory (%s) with filter "%s" for %j',
            this.baseDN,
            this.query.filter,
            (this.query.attributes) ? this.query.attributes : '[*]'
  );

  this.client.search(this.baseDN, this.query, this.controls, (err, res) => {
    if (err) {
      if (this.callback) {
        this.callback(err);
      }
      return;
    }

    const errCallback = (err) => {
      if (err.name === 'SizeLimitExceededError') {
        this.onSearchEnd(res);
        return;
      }

      this.client.unbind();
      log.trace(err,
                '[%s] An error occurred performing the requested LDAP search on %s (%j)',
                err.errno || 'UNKNOWN',
                this.baseDN,
                this.opts
      );
      if (this.callback) {
        this.callback(err);
      }
    };

    res.on('searchEntry', this.onSearchEntry.bind(this));
    res.on('searchReference', this.onReferralChase.bind(this));
    res.on('error', errCallback);
    res.on('end', (err) => {
      this.searchComplete = true;
      this.onSearchEnd(err);
    });
  });
};

Searcher.prototype.rangeSearch = function rangeSearch(query, rangeCB) {
  log.trace('Quering (%s) for range search with filter "%s" for: %j',
            this.baseDN, query.filter, query.attributes);
  this.client.search(this.baseDN, query, this.controls, (err, res) => {
    if (err) {
      return rangeCB(err);
    }
    res.on('searchEntry', (entry) => {
      const obj = entry.object;
      rangeCB(null, obj);
    });
    res.on('searchReference', this.onReferralChase.bind(this));
    res.on('end', () => {
      this.rangeProcessing = false;
    });
    res.on('error', rangeCB);
  });
};

/**
 * If set via the options of the query or the {@link ActiveDirectory}
 * instance, run the entry through the function. Otherwise, just feed it to
 * the parser callback.
 *
 * @param {object} entry The search entry object.
 * @param {object} raw The raw search entry object as returned from ldap.js.
 * @param {function} callback The callback to execute when complete.
 */
Searcher.prototype.entryParser = function entryParser(entry, raw, callback) {
  if (this.opts.entryParser) {
    return this.opts.entryParser(entry, raw, callback);
  } else if (ad.opts.entryParser) {
    return ad.opts.entryParser(entry, raw, callback);
  }

  return callback(entry);
};

/**
 * Invoked when the ldap.js client is returning a search entry result.
 *
 * @param {object} entry The search entry as returned by ldap.js.
 */
Searcher.prototype.onSearchEntry = function onSearchEntry(entry) {
  log.trace('onSearchEntry(entry)');
  const result = entry.object;
  delete result.controls;

  // Some attributes can have range attributes (paging). Execute the query
  // again to get additional items.
  this.rangeProcessing = true;

  const rangeProcessor = new RangeAttributeParser(this);
  rangeProcessor.on('error', this.callback);
  rangeProcessor.on('done', (results) => {
    async.each(
      results,
      (result, acb) => {
        this.entryParser(
          result, entry.raw, (r) => {
            this.results.set(result.dn, r);
            this.rangeProcessing = false;
            acb();
          }
        )
      },
      () => {
        if (this.searchComplete) {
          this.onSearchEnd();
        }
      }
    );
  });

  rangeProcessor.parseResult(result);
};

/**
 * Used to handle referrals, if they are enabled.
 *
 * @param {object} referral A referral object that has a `uris` property.
 */
Searcher.prototype.onReferralChase = function onReferralChase(referral) {
  referral.uris.forEach((uri) => {
    if (!isReferralAllowed(uri)) {
      return;
    }

    log.trace('Following LDAP referral chase at %s', uri);
    const referral = url.parse(uri);
    const referralBaseDn = (referral.pathname || '/').substring(1);
    const refSearcher = new Searcher({
      baseDN: referralBaseDn,
      opts: this.opts,
      callback: (err, res) => {
        if (err) {
          log.trace(
            err,
            '[%s] An error occurred chasing the LDAP referral on %s (%j)',
            err.errno,
            referralBaseDn,
            this.opts
          );
        }
        this.removeReferral(refSearcher);
      }
    });
    this.pendingReferrals.add(refSearcher);

    refSearcher.search();
  });
};

/**
 * Invoked when the main search has completed, including any referrals.
 */
Searcher.prototype.onSearchEnd = function onSearchEnd() {
  if (!this.rangeProcessing && this.pendingReferrals.size === 0) {
    this.client.unbind();
    log.trace('Active directory search (%s) for "%s" returned %d entries.',
              this.baseDN,
              this.query.filter,
              this.results.length
    );
    if (this.callback) {
      this.callback(null, Array.from(this.results.values()));
    }
  }
};

/**
 * Dequeues a referral chase client.
 *
 * @param {object} referral An instance of {@link Searcher} being used to chase
 * a referral.
 */
Searcher.prototype.removeReferral = function removeReferral(referral) {
  if (!referral) {
    return;
  }

  referral.client.unbind();
  this.pendingReferrals.delete(referral);
};

/**
 * @property {string} [scope] The type of search to perform: base, one, or sub.
 * Default: base.
 * @property {string} [filter] The LDAP filter to use.
 * Default: '(objectclass=*)'
 * @property {array} [attributes] A list of entry attributes to include in the
 * result. Defaults to all attributes.
 * @property {int} [sizeLimit] The maximum number of entries to return.
 * Default: 0 (infinite)
 * @property {int} [timeLimit] The maximum number of seconds to wait for a
 * server response. Default: 10.
 * @typedef {object} LDAPQueryParameters
 */

/**
 * Wraps an instance of {@link Searcher} so as to be backward compatible with
 * the original search function.
 *
 * @private
 * @param {string} [baseDN] The optional base directory where the LDAP query is
 * to originate from. If not specified, then starts at the root.
 * @param {LDAPQueryParameters} [opts] LDAP query string parameters to execute.
 * @param {function} callback The callback to execure when completed.
 * The call back is a standard Node style callback with an error parameter and
 * a result parameter.
 */
function search(baseDN, opts, callback) { // jshint -W071
  const options = Object.assign({}, ad.opts);

  if (typeof baseDN === 'function') {
    options.baseDN = ad.baseDN;
    options.callback = baseDN;
    options.opts = {};
  } else if (typeof baseDN === 'object') {
    options.baseDN = baseDN.baseDN || ad.baseDN;
    options.callback = opts;
    options.opts = baseDN;
  } else if (typeof baseDN === 'string') {
    options.baseDN = baseDN;
    if (typeof opts === 'function') {
      options.callback = opts;
      options.opts = {};
    } else {
      options.callback = callback;
      options.opts = opts;
    }
  }

  const searcher = new Searcher(options);
  return searcher.search();
}

module.exports = function($ad, $log) {
  ad = $ad;
  log = $log;
  RangeAttributeParser = require('./parseRangeAttributes')($ad, $log);
  return search;
};
<|endoftext|>


<commit_before>// Main code running in every mathdown document (all shown via index.html).

"use strict";

// Logging
// =======

// Prevent errors on IE (but do strive to log somehow if IE Dev Tools are open).
function log() {
  try {
    if(console.log.apply) {
      console.log.apply(console, arguments);
    } else {
      /* IE's console.log doesn't have .apply, .call, or bind. */
      console.log(Array.prototype.slice.call(arguments));
    }
  } catch (err) {}
}

// Send MathJax messages to log.
log("MJ:", MathJax.Message.Log());
var origFilterText = MathJax.Message.filterText;
MathJax.Message.filterText = function(text, n, msgType) {
  // Exclude non-informative "Processing/Typesetting math: 0% / 100%".
  if(msgType !== "ProcessMath" && msgType !== "TypesetMath") {
    log("MJ:", text, "[" + msgType + "]");
  }
  return origFilterText(text, n, msgType);
}

var statusElement = document.getElementById("status");
// Show "flash" message.  To hide use className="" but it's still
// useful to provide text (briefly visible).
function setStatus(className, text) {
  statusElement.innerHTML = "";
  statusElement.className = className;
  if(text) {
    statusElement.appendChild(document.createTextNode(text));
  }
}

// Random pad IDs
// ==============

// Return an int in [0, 62).
var random62 = (window.crypto && window.crypto.getRandomValues) ?
    function() {
      var buf = new Uint8Array(1);
      var n;
      do {
        window.crypto.getRandomValues(buf);
        n = (buf[0] % 64);
      } while(n >= 62);
      return n;
    }
    :
    function() {
      // Math.random is pathetic on most browsers, though some initialize
      // with secure seed: http://stackoverflow.com/a/18507748/239657
      return Math.floor(Math.random() * 62);
    };

function randomBase62(length) {
  var base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var text = "";
  for(var i=0; i < length; i++)
    text += base62.charAt(random62());
  return text;
}

function newPad() {
  var doc = randomBase62(11);  // 62**11 ~= 2**65.
  window.open("?doc=" + doc, "_blank");
}
window.newPad = newPad;

// Editor
// ======

// Apply class="leadingspace" to leading whitespace so we can make it monospace.
// Stolen from CodeMirror/addon/edit/trailingspace.js.
CodeMirror.defineOption("showLeadingSpace", false, function(cm, val, prev) {
  if (prev === CodeMirror.Init) prev = false;
  if (prev && !val)
    cm.removeOverlay("leadingspace");
  else if (!prev && val)
    cm.addOverlay({
      token: function(stream) {
        if (stream.sol() && stream.eatSpace()) {
          return "leadingspace";
        } else {
          stream.skipToEnd();
          return null;
        }
      },
      name: "leadingspace"
    });
});

// To add vertical margin to headers, the .cm-header[N] classes must apply to
// <pre> and not the <span>.  Wrap the mode to achieve this.
CodeMirror.defineMode("gfm_header_line_classes", function(cmConfig, modeCfg) {
  modeCfg.name = "gfm";
  modeCfg.highlightFormatting = true;
  var mode = CodeMirror.getMode(cmConfig, modeCfg);
  var origToken = mode.token;
  // We use GFM mostly for URLs but don't want GitHub-specific formatting that
  // makes `0123456789` a link.  TODO: add mode option of gfm.js.
  var shaOrIssueRE = /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40})|b^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+$/;
  mode.token = function(stream, state) {
    var classes = origToken(stream, state) || "";
    if (shaOrIssueRE.test(stream.current())) {
      classes = classes.replace(/(^| )link( |$)/, " ");
    }
    classes = classes.replace(/(^| )(header\S*)/g, "$1line-cm-$2");
    return /^\s*$/.test(classes) ? null : classes;
  }
  return mode;
});

// Defaults are insert \t / reindent.  Indent / dedent are more expected & useful.
CodeMirror.keyMap["default"]["Tab"] = "indentMore";
CodeMirror.keyMap["default"]["Shift-Tab"] = "indentLess";

function createEditor(docDirection) {
  return CodeMirror.fromTextArea(document.getElementById("code"),
                                 {foldGutter: true,
                                  gutters: ["CodeMirror-foldgutter"],
                                  indentUnit: 4,
                                  lineNumbers: false,
                                  lineWrapping: true,
                                  mode: "gfm_header_line_classes",
                                  showLeadingSpace: true,
                                  direction: docDirection});
}

function setupEditor(editor) {
  // Indent soft-wrapped lines.  Based on CodeMirror/demo/indentwrap.html.
  var leadingSpaceListBulletsQuotes = /^\s*([*+-]\s+|\d+\.\s+|>\s*)*/;
  editor.on("renderLine", function(cm, line, elt) {
    // Show continued list/qoute lines aligned to start of text rather
    // than first non-space char.  MINOR BUG: also does this inside
    // literal blocks.
    // Would like to measure real sizes of spans styled
    // /leadingspace|formatting-list|formatting-quote/, but can't do
    // that without inserting them into the DOM.  So count chars instead.
    var leading = (leadingSpaceListBulletsQuotes.exec(line.text) || [""])[0];
    var off = CodeMirror.countColumn(leading, leading.length, cm.getOption("tabSize"));
    // Using "ex" is a bit better than cm.defaultCharWidth() — it picks up
    // increased font if applied to whole line, i.e. in header lines
    // (not that it makes sense to indent headers).
    // However any resemblance of 1ex to the width of one monospace char
    // is purely coincidental (1em is way too wide in practice).
    elt.style.textIndent = "-" + off + "ex";

    // We need to know line direction to set paddingLeft or paddingRight appropriately.
    // TODO: CM should expose getDirection().
    var lineDirection = elt.style.direction || cm.getDoc().direction;
    // TODO: CM doesn't re-run this hook when line direction changed :-(
    // => KLUDGE: set both paddings!  In fact this is a good for mixed-direction docs anyway -
    //    long lines of opposite direction won't visually break indentation structrure.
    //    But I'm afraid it may be a nuisance on pure-LTR docs, so as a compromise
    //    I set only paddingLeft on LTR lines in LTR docs.
    if (docDirection === "rtl" || lineDirection === "rtl") {
      elt.style.paddingRight = off + "ex";
      elt.style.paddingLeft = off + "ex";
    } else {
      elt.style.paddingLeft = off + "ex";
    }
  });
  editor.refresh();

  // Keep title and url #hash part in sync with first line of document.
  function updateTitle() {
    var text = editor.getLine(0); // TODO: find first # Title line?
    text = text.replace(/^\s*#+\s*/, "");
    document.title = text + " | mathdown";
    window.location.hash = text.replace(/\W+/g, "-").replace(/^-/, "").replace(/-$/, "");
  }
  CodeMirror.on(editor.getDoc(), "change", function(doc, changeObj) {
    if (changeObj.from.line === 0) {
      updateTitle();
    }
  });

  CodeMirror.hookMath(editor, MathJax);
}

// Firepad
// =======

function setupFirepad(editor, firepad) {
  firepad.on("ready", function() {
    setStatus("", "done");

    if (firepad.isHistoryEmpty()) {
      firepad.setText(
        "# Untitled\n" +
          "Bookmark this page — the random-looking address is the only way to access this document!\n" +
          "Share the link to collaborate.\n" +
          "Edits are saved in real time.\n" +
          "\n" +
          "Use Markdown for document structure and $\\LaTeX$ math syntax (see **Help** link above).\n" +
          "To edit formulas just enter them with arrow keys.\n" +
          // HACK: Pad the editor with a few empty lines so it looks
          // more inviting to write in.
          "\n" +
          "\n"
      );
    } else {
      // Keeping cursor at start (hopefully) reduces CM redrawing as
      // syntax highlight and math rendering changes line heights.
      editor.setCursor({line: 0, ch: 0});
    }

    // CM's autofocus option doesn't work with Firepad - seems focused
    // but can't type.  Focusing here after Firepad init works.
    editor.focus();

    // Queuing this allows text to appear before math.
    MathJax.Hub.Queue(function() {
      editor.renderAllMath();
    });
  });

  //  firepadRef.child("
  firepad.on("synced", function(isSynced) {
    log("synced", isSynced);
    if(!isSynced) {
      setStatus("info", "Unsaved!");
    } else {
      setStatus("", "saved");
    }
  })
}

// URL parameters => load document
// ===============================

function locationQueryParams() {
  /* If more is needed, use https://github.com/medialize/URI.js */
  var queryParts = window.location.search.replace(/^\?/g, "").split("&");
  var params = {};
  for(var i = 0; i < queryParts.length; i++) {
    var keyval = queryParts[i].split("=");
    params[decodeURIComponent(keyval[0])] = decodeURIComponent(keyval[1] || "");
  }
  return params;
}

var queryParams = locationQueryParams();
var doc = queryParams["doc"];
// EXPERIMENTAL KLUDGE param: for now we support dir=rtl to make RTL docs (somewhat) practical
// (but don't expose it in the GUI).
// In the future it might be ignored - once we autodetect each line's base direction (#23).
// Also, document direction is semantic, it makes more sense to store it in firebase?
var docDirection = (queryParams["dir"] === "rtl" ? "rtl" : "ltr");

if(doc === undefined) {
    // TODO: this should be a server-side redirect (when we have a server).
    window.location.search = "?doc=about";
} else {
  var rootRef = new Firebase("https://mathdown.firebaseIO.com/");
  var firepadsRef = rootRef.child("firepads");
  var firepadRef = firepadsRef.child(doc);
  log("firebase ref:", firepadRef.toString());

  var editor = createEditor(docDirection);
  setupEditor(editor);

  var firepad = Firepad.fromCodeMirror(firepadRef, editor);
  setupFirepad(editor, firepad);
}
<commit_msg>Cleaner way to attach to newPad() the button.<commit_after>// Main code running in every mathdown document (all shown via index.html).

"use strict";

// Logging
// =======

// Prevent errors on IE (but do strive to log somehow if IE Dev Tools are open).
function log() {
  try {
    if(console.log.apply) {
      console.log.apply(console, arguments);
    } else {
      /* IE's console.log doesn't have .apply, .call, or bind. */
      console.log(Array.prototype.slice.call(arguments));
    }
  } catch (err) {}
}

// Send MathJax messages to log.
log("MJ:", MathJax.Message.Log());
var origFilterText = MathJax.Message.filterText;
MathJax.Message.filterText = function(text, n, msgType) {
  // Exclude non-informative "Processing/Typesetting math: 0% / 100%".
  if(msgType !== "ProcessMath" && msgType !== "TypesetMath") {
    log("MJ:", text, "[" + msgType + "]");
  }
  return origFilterText(text, n, msgType);
}

var statusElement = document.getElementById("status");
// Show "flash" message.  To hide use className="" but it's still
// useful to provide text (briefly visible).
function setStatus(className, text) {
  statusElement.innerHTML = "";
  statusElement.className = className;
  if(text) {
    statusElement.appendChild(document.createTextNode(text));
  }
}

// Random pad IDs
// ==============

// Return an int in [0, 62).
var random62 = (window.crypto && window.crypto.getRandomValues) ?
    function() {
      var buf = new Uint8Array(1);
      var n;
      do {
        window.crypto.getRandomValues(buf);
        n = (buf[0] % 64);
      } while(n >= 62);
      return n;
    }
    :
    function() {
      // Math.random is pathetic on most browsers, though some initialize
      // with secure seed: http://stackoverflow.com/a/18507748/239657
      return Math.floor(Math.random() * 62);
    };

function randomBase62(length) {
  var base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var text = "";
  for(var i=0; i < length; i++)
    text += base62.charAt(random62());
  return text;
}

function newPad() {
  var doc = randomBase62(11);  // 62**11 ~= 2**65.
  window.open("?doc=" + doc, "_blank");
}
document.getElementById("new").onclick = newPad;

// Editor
// ======

// Apply class="leadingspace" to leading whitespace so we can make it monospace.
// Stolen from CodeMirror/addon/edit/trailingspace.js.
CodeMirror.defineOption("showLeadingSpace", false, function(cm, val, prev) {
  if (prev === CodeMirror.Init) prev = false;
  if (prev && !val)
    cm.removeOverlay("leadingspace");
  else if (!prev && val)
    cm.addOverlay({
      token: function(stream) {
        if (stream.sol() && stream.eatSpace()) {
          return "leadingspace";
        } else {
          stream.skipToEnd();
          return null;
        }
      },
      name: "leadingspace"
    });
});

// To add vertical margin to headers, the .cm-header[N] classes must apply to
// <pre> and not the <span>.  Wrap the mode to achieve this.
CodeMirror.defineMode("gfm_header_line_classes", function(cmConfig, modeCfg) {
  modeCfg.name = "gfm";
  modeCfg.highlightFormatting = true;
  var mode = CodeMirror.getMode(cmConfig, modeCfg);
  var origToken = mode.token;
  // We use GFM mostly for URLs but don't want GitHub-specific formatting that
  // makes `0123456789` a link.  TODO: add mode option of gfm.js.
  var shaOrIssueRE = /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40})|b^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+$/;
  mode.token = function(stream, state) {
    var classes = origToken(stream, state) || "";
    if (shaOrIssueRE.test(stream.current())) {
      classes = classes.replace(/(^| )link( |$)/, " ");
    }
    classes = classes.replace(/(^| )(header\S*)/g, "$1line-cm-$2");
    return /^\s*$/.test(classes) ? null : classes;
  }
  return mode;
});

// Defaults are insert \t / reindent.  Indent / dedent are more expected & useful.
CodeMirror.keyMap["default"]["Tab"] = "indentMore";
CodeMirror.keyMap["default"]["Shift-Tab"] = "indentLess";

function createEditor(docDirection) {
  return CodeMirror.fromTextArea(document.getElementById("code"),
                                 {foldGutter: true,
                                  gutters: ["CodeMirror-foldgutter"],
                                  indentUnit: 4,
                                  lineNumbers: false,
                                  lineWrapping: true,
                                  mode: "gfm_header_line_classes",
                                  showLeadingSpace: true,
                                  direction: docDirection});
}

function setupEditor(editor) {
  // Indent soft-wrapped lines.  Based on CodeMirror/demo/indentwrap.html.
  var leadingSpaceListBulletsQuotes = /^\s*([*+-]\s+|\d+\.\s+|>\s*)*/;
  editor.on("renderLine", function(cm, line, elt) {
    // Show continued list/qoute lines aligned to start of text rather
    // than first non-space char.  MINOR BUG: also does this inside
    // literal blocks.
    // Would like to measure real sizes of spans styled
    // /leadingspace|formatting-list|formatting-quote/, but can't do
    // that without inserting them into the DOM.  So count chars instead.
    var leading = (leadingSpaceListBulletsQuotes.exec(line.text) || [""])[0];
    var off = CodeMirror.countColumn(leading, leading.length, cm.getOption("tabSize"));
    // Using "ex" is a bit better than cm.defaultCharWidth() — it picks up
    // increased font if applied to whole line, i.e. in header lines
    // (not that it makes sense to indent headers).
    // However any resemblance of 1ex to the width of one monospace char
    // is purely coincidental (1em is way too wide in practice).
    elt.style.textIndent = "-" + off + "ex";

    // We need to know line direction to set paddingLeft or paddingRight appropriately.
    // TODO: CM should expose getDirection().
    var lineDirection = elt.style.direction || cm.getDoc().direction;
    // TODO: CM doesn't re-run this hook when line direction changed :-(
    // => KLUDGE: set both paddings!  In fact this is a good for mixed-direction docs anyway -
    //    long lines of opposite direction won't visually break indentation structrure.
    //    But I'm afraid it may be a nuisance on pure-LTR docs, so as a compromise
    //    I set only paddingLeft on LTR lines in LTR docs.
    if (docDirection === "rtl" || lineDirection === "rtl") {
      elt.style.paddingRight = off + "ex";
      elt.style.paddingLeft = off + "ex";
    } else {
      elt.style.paddingLeft = off + "ex";
    }
  });
  editor.refresh();

  // Keep title and url #hash part in sync with first line of document.
  function updateTitle() {
    var text = editor.getLine(0); // TODO: find first # Title line?
    text = text.replace(/^\s*#+\s*/, "");
    document.title = text + " | mathdown";
    window.location.hash = text.replace(/\W+/g, "-").replace(/^-/, "").replace(/-$/, "");
  }
  CodeMirror.on(editor.getDoc(), "change", function(doc, changeObj) {
    if (changeObj.from.line === 0) {
      updateTitle();
    }
  });

  CodeMirror.hookMath(editor, MathJax);
}

// Firepad
// =======

function setupFirepad(editor, firepad) {
  firepad.on("ready", function() {
    setStatus("", "done");

    if (firepad.isHistoryEmpty()) {
      firepad.setText(
        "# Untitled\n" +
          "Bookmark this page — the random-looking address is the only way to access this document!\n" +
          "Share the link to collaborate.\n" +
          "Edits are saved in real time.\n" +
          "\n" +
          "Use Markdown for document structure and $\\LaTeX$ math syntax (see **Help** link above).\n" +
          "To edit formulas just enter them with arrow keys.\n" +
          // HACK: Pad the editor with a few empty lines so it looks
          // more inviting to write in.
          "\n" +
          "\n"
      );
    } else {
      // Keeping cursor at start (hopefully) reduces CM redrawing as
      // syntax highlight and math rendering changes line heights.
      editor.setCursor({line: 0, ch: 0});
    }

    // CM's autofocus option doesn't work with Firepad - seems focused
    // but can't type.  Focusing here after Firepad init works.
    editor.focus();

    // Queuing this allows text to appear before math.
    MathJax.Hub.Queue(function() {
      editor.renderAllMath();
    });
  });

  //  firepadRef.child("
  firepad.on("synced", function(isSynced) {
    log("synced", isSynced);
    if(!isSynced) {
      setStatus("info", "Unsaved!");
    } else {
      setStatus("", "saved");
    }
  })
}

// URL parameters => load document
// ===============================

function locationQueryParams() {
  /* If more is needed, use https://github.com/medialize/URI.js */
  var queryParts = window.location.search.replace(/^\?/g, "").split("&");
  var params = {};
  for(var i = 0; i < queryParts.length; i++) {
    var keyval = queryParts[i].split("=");
    params[decodeURIComponent(keyval[0])] = decodeURIComponent(keyval[1] || "");
  }
  return params;
}

var queryParams = locationQueryParams();
var doc = queryParams["doc"];
// EXPERIMENTAL KLUDGE param: for now we support dir=rtl to make RTL docs (somewhat) practical
// (but don't expose it in the GUI).
// In the future it might be ignored - once we autodetect each line's base direction (#23).
// Also, document direction is semantic, it makes more sense to store it in firebase?
var docDirection = (queryParams["dir"] === "rtl" ? "rtl" : "ltr");

if(doc === undefined) {
    // TODO: this should be a server-side redirect (when we have a server).
    window.location.search = "?doc=about";
} else {
  var rootRef = new Firebase("https://mathdown.firebaseIO.com/");
  var firepadsRef = rootRef.child("firepads");
  var firepadRef = firepadsRef.child(doc);
  log("firebase ref:", firepadRef.toString());

  var editor = createEditor(docDirection);
  setupEditor(editor);

  var firepad = Firepad.fromCodeMirror(firepadRef, editor);
  setupFirepad(editor, firepad);
}
<|endoftext|>


<commit_before>'use babel';

/* @flow */

import { Emitter, CompositeDisposable } from 'atom';
/* flow-include
import type { TextEditor, Disposable } from 'atom'
import type { Tester } from './types'
*/

export default class TesterRegistry {
    /* ::
    emitter: Emitter;
    testers: Set<Tester>;
    locks: Set<Tester>;
    testOnChange: boolean;
    subscriptions: CompositeDisposable;
    */

  constructor() {
    this.emitter = new Emitter();
    this.testers = new Set();
    this.locks = new Set();
    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.config.observe('tester.testOnChange', (testOnChange) => {
      this.testOnChange = testOnChange;
    }));
  }

  hasTester(tester /* : Tester*/) /* : boolean*/ {
    return this.testers.has(tester);
  }

  addTester(tester /* : Tester*/) {
    console.log('addTester', tester);
    this.testers.add(tester);
  }

  deleteTester(tester /* : Tester*/) {
    console.log('deleteTester', tester);
    this.testers.delete(tester);
  }

  onDidUpdateMessages(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-update-messages', callback);
  }
  onDidBeginTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-begin-testing', callback);
  }
  onDidFinishTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-finish-testing', callback);
  }
  dispose() {
    this.testers.clear();
    this.subscriptions.dispose();
  }
  test(onChange/* : Event*/, editorTester/* : TextEditor*/) {
    console.log('TEST', onChange, editorTester);
    const editor = editorTester.getEditor();

    if (!editor.getPath() ||
        editor !== atom.workspace.getActiveTextEditor() ||
        this.locks.has(editorTester)) {
      console.log('Nothing todo');
      return Promise.resolve();
    }

    this.locks.add(editorTester);

    const promises = [];
    this.testers.forEach((tester) => {
      promises.push(new Promise((resolve) => {
        console.log('resolve promise', resolve, tester);
        resolve(tester.test(editor));
      }).then((results) => {
        console.log('did-update-messages', results);
        if (results) {
          this.emitter.emit('did-update-messages', { tester, messages: results, editor });
        }
      }));
    });
    console.log('Promises all', promises);
    return Promise.all(promises)
      .then((results) => {
        console.log('All DONE', results);
        this.locks.delete(editorTester);
      })
      .catch(err => {
          console.log('All catch', err);
      });
  }
}
<commit_msg>maybe that<commit_after>'use babel';

/* @flow */

import { Emitter, CompositeDisposable } from 'atom';
/* flow-include
import type { TextEditor, Disposable } from 'atom'
import type { Tester } from './types'
*/

export default class TesterRegistry {
    /* ::
    emitter: Emitter;
    testers: Set<Tester>;
    locks: Set<Tester>;
    testOnChange: boolean;
    subscriptions: CompositeDisposable;
    */

  constructor() {
    this.emitter = new Emitter();
    this.testers = new Set();
    this.locks = new Set();
    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.config.observe('tester.testOnChange', (testOnChange) => {
      this.testOnChange = testOnChange;
    }));
  }

  hasTester(tester /* : Tester*/) /* : boolean*/ {
    return this.testers.has(tester);
  }

  addTester(tester /* : Tester*/) {
    console.log('addTester', tester);
    this.testers.add(tester);
  }

  deleteTester(tester /* : Tester*/) {
    console.log('deleteTester', tester);
    this.testers.delete(tester);
  }

  onDidUpdateMessages(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-update-messages', callback);
  }
  onDidBeginTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-begin-testing', callback);
  }
  onDidFinishTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-finish-testing', callback);
  }
  dispose() {
    this.testers.clear();
    this.subscriptions.dispose();
  }
  test(onChange/* : Event*/, editorTester/* : TextEditor*/) {
    console.log('TEST', onChange, editorTester);
    const editor = editorTester.getEditor();

    if (!editor.getPath() ||
        editor !== atom.workspace.getActiveTextEditor() ||
        this.locks.has(editorTester)) {
      console.log('Nothing todo');
      return Promise.resolve();
    }

    this.locks.add(editorTester);

    const promises = [];
    this.testers.forEach((tester) => {
      promises.push(new Promise((resolve) => {
        console.log('resolve promise', resolve, tester);
        resolve(tester.test(editor));
      }).then((results) => {
        console.log('did-update-messages', results);
        if (results) {
          this.emitter.emit('did-update-messages', { tester, messages: results, editor });
        }
      }));
    });
    console.log('Promises all', promises);
    Promise.all(promises)
      .then((results) => {
        console.log('All DONE', results);
        this.locks.delete(editorTester);
      })
      .catch(err => {
          console.log('All catch', err);
      });
    return true;
  }
}
<|endoftext|>


<commit_before>/*global QUnit */
sap.ui.define([
	"sap/ui/test/_LogCollector",
	"sap/ui/test/_OpaLogger",
	"sap/ui/test/autowaiter/_resourceWaiter",
	"sap/m/Image",
	"sap/ui/thirdparty/URI"
], function (_LogCollector, _OpaLogger, _resourceWaiter, Image, URI) {
	"use strict";

	// before running this test in IE11, clear cached images

	var oLogCollector = _LogCollector.getInstance();

	QUnit.module("ResourceWaiter", {
		beforeEach: function () {
			this.defaultLogLevel = _OpaLogger.getLevel();
			_OpaLogger.setLevel("trace");
			var oCurrentUri = new URI();
			var sPrefix = oCurrentUri.path().split('/')[1] === "testsuite" ? "/testsuite" : "";
			var sBase = oCurrentUri.origin() + sPrefix + "/test-resources/sap/ui/core/images/";
			this.sExistingImageSrc = sBase + "Mobile.png";
			this.sReplacerImageSrc = sBase + "Notebook.png";
			this.sNotFoundSrc = sBase + "noSuchImage.jpg";
		},
		afterEach: function () {
			_OpaLogger.setLevel(this.defaultLogLevel);
			oLogCollector.getAndClearLog(); // cleanup
		}
	});

	function assertImagePending(sSrc, assert) {
		var bHasPending = _resourceWaiter.hasPending();
		var sLogs = oLogCollector.getAndClearLog();
		if (bHasPending) {
			assert.ok(true, "Has pending resources");
			// image could be cached or loaded in previous steps
			assert.ok(sLogs.match("Pending resource: " + sSrc) ||
				sLogs.match("Image with src '" + sSrc + "' is updated and pending again"),
				"Image with src " + sSrc + " is pending");
		} else {
			// when CPU is very slow, hasPending may be called before load handler
			assert.ok(true, "Has no pending resources");
			assert.ok(sLogs.match("Image with src '" + sSrc + "' completed") ||
				sLogs.match("Image with src '" + sSrc + "' loaded successfully"),
				"Image already loaded");
		}
	}

	// workaround for image loading (async) - there can multiple images created for 1 test, but we only check one at a time
	function assertNoRelevantPending(sRelevantSrc, bPass, assert) {
		_resourceWaiter.hasPending();
		var sLogs = oLogCollector.getAndClearLog();
		var aRelevantPending = sLogs.match("Pending resource: " + sRelevantSrc);
		assert.ok(!aRelevantPending, "Should have no pending resources with src " + sRelevantSrc);
		assert.ok(sLogs.match("Image with src '" + sRelevantSrc + (bPass ? "' loaded successfully" : "' failed to load")),
			"Image with src " + sRelevantSrc + " is not pending");
	}

	QUnit.test("Should wait for image to load", function (assert) {
		var fnDone = assert.async();
		this.oImageExistingSrc = new Image({
			src: this.sExistingImageSrc
		});
		this.oImageExistingSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		// wait for the rendering to start
		setTimeout(function () {
			assertImagePending(this.sExistingImageSrc, assert);

			this.oImageExistingSrc.attachLoad(fnOnComplete(true).bind(this));
		}.bind(this), 0);

		function fnOnComplete(bPass) {
			return function () {
				// wait for other load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sExistingImageSrc, bPass, assert);

					// cleanup
					this.oImageExistingSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			};
		}
	});

	QUnit.test("Should wait for image to complete with error", function (assert) {
		var fnDone = assert.async();
		this.oImageWrongSrc = new Image({
			src: this.sNotFoundSrc
		});
		this.oImageWrongSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		// wait for the rendering to start
		setTimeout(function () {
			assertImagePending(this.sNotFoundSrc, assert);

			this.oImageWrongSrc.attachError(function () {
				// wait for other load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sNotFoundSrc, false, assert);

					// cleanup
					this.oImageWrongSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			}.bind(this));
		}.bind(this), 0);
	});

	QUnit.test("Should wait for image to load when src is changed", function (assert) {
		var fnDone = assert.async();
		this.oImageWrongSrc = new Image({
			src: this.sNotFoundSrc
		});
		this.oImageWrongSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		var bSrcChanged = false;

		this.oImageWrongSrc.attachError(function () {
			if (!bSrcChanged) {
				bSrcChanged = true;
				setTimeout(function () {
					assertNoRelevantPending(this.sNotFoundSrc, false, assert);

					// update image - from sNotFoundSrc to sReplacerImageSrc
					this.oImageWrongSrc.setSrc(this.sReplacerImageSrc);
					sap.ui.getCore().applyChanges();

					// wait for the rendering to start
					setTimeout(function () {
						assertImagePending(this.sReplacerImageSrc, assert);

						this.oImageWrongSrc.attachLoad(fnOnComplete(true).bind(this));
					}.bind(this), 0);
				}.bind(this), 0);
			}
		}.bind(this));

		function fnOnComplete(bPass) {
			return function () {
				// wait for load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sReplacerImageSrc, bPass, assert);

					// cleanup
					this.oImageWrongSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			};
		}
	});
});
<commit_msg>[INTERNAL][FIX] Opa: fix image path is resourceWaiter test<commit_after>/*global QUnit */
sap.ui.define([
	"sap/ui/test/_LogCollector",
	"sap/ui/test/_OpaLogger",
	"sap/ui/test/autowaiter/_resourceWaiter",
	"sap/m/Image"
], function (_LogCollector, _OpaLogger, _resourceWaiter, Image) {
	"use strict";

	// before running this test in IE11, clear cached images

	var oLogCollector = _LogCollector.getInstance();

	QUnit.module("ResourceWaiter", {
		beforeEach: function () {
			this.defaultLogLevel = _OpaLogger.getLevel();
			_OpaLogger.setLevel("trace");
			var sBase = "test-resources/sap/ui/core/images/";
			this.sExistingImageSrc = sBase + "Mobile.png";
			this.sReplacerImageSrc = sBase + "Notebook.png";
			this.sNotFoundSrc = sBase + "noSuchImage.jpg";
		},
		afterEach: function () {
			_OpaLogger.setLevel(this.defaultLogLevel);
			oLogCollector.getAndClearLog(); // cleanup
		}
	});

	function assertImagePending(sSrc, assert) {
		var bHasPending = _resourceWaiter.hasPending();
		var sLogs = oLogCollector.getAndClearLog();
		if (bHasPending) {
			assert.ok(true, "Has pending resources");
			// image could be cached or loaded in previous steps
			assert.ok(sLogs.match("Pending resource: .*" + sSrc) ||
				sLogs.match("Image with src '.*" + sSrc + "' is updated and pending again"),
				"Image with src " + sSrc + " is pending");
		} else {
			// when CPU is very slow, hasPending may be called before load handler
			assert.ok(true, "Has no pending resources");
			assert.ok(sLogs.match("Image with src '.*" + sSrc + "' completed") ||
				sLogs.match("Image with src '.*" + sSrc + "' loaded successfully"),
				"Image already loaded");
		}
	}

	// workaround for image loading (async) - there can multiple images created for 1 test, but we only check one at a time
	function assertNoRelevantPending(sRelevantSrc, bPass, assert) {
		_resourceWaiter.hasPending();
		var sLogs = oLogCollector.getAndClearLog();
		var aRelevantPending = sLogs.match("Pending resource: .*" + sRelevantSrc);
		assert.ok(!aRelevantPending, "Should have no pending resources with src " + sRelevantSrc);
		assert.ok(sLogs.match("Image with src '.*" + sRelevantSrc + (bPass ? "' loaded successfully" : "' failed to load")),
			"Image with src " + sRelevantSrc + " is not pending");
	}

	QUnit.test("Should wait for image to load", function (assert) {
		var fnDone = assert.async();
		this.oImageExistingSrc = new Image({
			src: this.sExistingImageSrc
		});
		this.oImageExistingSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		// wait for the rendering to start
		setTimeout(function () {
			assertImagePending(this.sExistingImageSrc, assert);

			this.oImageExistingSrc.attachLoad(fnOnComplete(true).bind(this));
		}.bind(this), 0);

		function fnOnComplete(bPass) {
			return function () {
				// wait for other load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sExistingImageSrc, bPass, assert);

					// cleanup
					this.oImageExistingSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			};
		}
	});

	QUnit.test("Should wait for image to complete with error", function (assert) {
		var fnDone = assert.async();
		this.oImageWrongSrc = new Image({
			src: this.sNotFoundSrc
		});
		this.oImageWrongSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		// wait for the rendering to start
		setTimeout(function () {
			assertImagePending(this.sNotFoundSrc, assert);

			this.oImageWrongSrc.attachError(function () {
				// wait for other load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sNotFoundSrc, false, assert);

					// cleanup
					this.oImageWrongSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			}.bind(this));
		}.bind(this), 0);
	});

	QUnit.test("Should wait for image to load when src is changed", function (assert) {
		var fnDone = assert.async();
		this.oImageWrongSrc = new Image({
			src: this.sNotFoundSrc
		});
		this.oImageWrongSrc.placeAt("qunit-fixture");
		sap.ui.getCore().applyChanges();

		var bSrcChanged = false;

		this.oImageWrongSrc.attachError(function () {
			if (!bSrcChanged) {
				bSrcChanged = true;
				setTimeout(function () {
					assertNoRelevantPending(this.sNotFoundSrc, false, assert);

					// update image - from sNotFoundSrc to sReplacerImageSrc
					this.oImageWrongSrc.setSrc(this.sReplacerImageSrc);
					sap.ui.getCore().applyChanges();

					// wait for the rendering to start
					setTimeout(function () {
						assertImagePending(this.sReplacerImageSrc, assert);

						this.oImageWrongSrc.attachLoad(fnOnComplete(true).bind(this));
					}.bind(this), 0);
				}.bind(this), 0);
			}
		}.bind(this));

		function fnOnComplete(bPass) {
			return function () {
				// wait for load handlers
				setTimeout(function () {
					assertNoRelevantPending(this.sReplacerImageSrc, bPass, assert);

					// cleanup
					this.oImageWrongSrc.destroy();
					sap.ui.getCore().applyChanges();
					fnDone();
				}.bind(this), 0);
			};
		}
	});
});
<|endoftext|>