<commit_before>// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ F R E E B O A R D                                                  │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Copyright © 2013 Jim Heising (https://github.com/jheising)         │ \\
// │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net)               │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license.                                    │ \\
// └────────────────────────────────────────────────────────────────────┘ \\

(function()
{
	var jsonDatasource = function(settings, updateCallback)
	{
		var self = this;
		var updateTimer = null;
		var currentSettings = settings;
		var errorStage = 0; 	// 0 = try standard request
								// 1 = try JSONP
								// 2 = try thingproxy.freeboard.io
		var lockErrorStage = false;

		function updateRefresh(refreshTime)
		{
			if(updateTimer)
			{
				clearInterval(updateTimer);
			}

			updateTimer = setInterval(function()
			{
				self.updateNow();
			}, refreshTime);
		}

		updateRefresh(currentSettings.refresh * 1000);

		this.updateNow = function()
		{
			if((errorStage > 1 && !currentSettings.use_thingproxy) || errorStage > 2) // We've tried everything, let's quit
			{
				return; // TODO: Report an error
			}

			var requestURL = currentSettings.url;

			if(errorStage == 2 && currentSettings.use_thingproxy)
			{
				requestURL = (location.protocol == "https:" ? "https:" : "http:") + "//thingproxy.freeboard.io/fetch/" + encodeURI(currentSettings.url);
			}

			var body = currentSettings.body;

			// Can the body be converted to JSON?
			if(body)
			{
				try
				{
					body = JSON.parse(body);
				}
				catch(e)
				{
				}
			}

			$.ajax({
				url       : requestURL,
				dataType  : (errorStage == 1) ? "JSONP" : "JSON",
				type : currentSettings.method || "GET",
				data : body,
				beforeSend: function(xhr)
				{
					try
					{
						_.each(currentSettings.headers, function(header)
						{
							var name = header.name;
							var value = header.value;

							if(!_.isUndefined(name) && !_.isUndefined(value))
							{
								xhr.setRequestHeader(name, value);
							}
						});
					}
					catch(e)
					{
					}
				},
				success   : function(data)
				{
					lockErrorStage = true;
					updateCallback(data);
				},
				error     : function(xhr, status, error)
				{
					if(!lockErrorStage)
					{
						// TODO: Figure out a way to intercept CORS errors only. The error message for CORS errors seems to be a standard 404.
						errorStage++;
						self.updateNow();
					}
				}
			});
		}

		this.onDispose = function()
		{
			clearInterval(updateTimer);
			updateTimer = null;
		}

		this.onSettingsChanged = function(newSettings)
		{
			currentSettings = newSettings;
			updateRefresh(currentSettings.refresh * 1000);
		}
	};

	freeboard.loadDatasourcePlugin({
		type_name  : "JSON",
		settings   : [
			{
				name        : "url",
				display_name: "URL",
				type        : "text"
			},
			{
				name         : "use_thingproxy",
				display_name : "Try thingproxy",
				description : 'A direct JSON connection will be tried first, if that fails, a JSONP connection will be tried. If that fails, you can use thingproxy, which can solve many connection problems to APIs. More information.',
				type         : "boolean",
				default_value: true
			},
			{
				name: "method",
				display_name: "Method",
				type: "option",
				options: [
					{
						name: "GET",
						value: "GET"
					},
					{
						name: "POST",
						value: "POST"
					},
					{
						name: "PUT",
						value: "PUT"
					},
					{
						name: "DELETE",
						value: "DELETE"
					}
				]
			},
			{
				name        : "body",
				display_name: "Body",
				type        : "text",
				description : "The body of the request. Normally only used if method is POST"
			},
			{
				name         : "refresh",
				display_name : "Refresh Every",
				type         : "number",
				suffix       : "seconds",
				default_value: 5
			},
			{
				name        : "headers",
				display_name: "Headers",
				type        : "array",
				settings    : [
					{
						name        : "name",
						display_name: "Name",
						type        : "text"
					},
					{
						name        : "value",
						display_name: "Value",
						type        : "text"
					}
				]
			}
		],
		newInstance: function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback( new jsonDatasource(settings, updateCallback));
		}
	});

	var openWeatherMapDatasource = function(settings, updateCallback)
	{
		var self = this;
		var updateTimer = null;
		var currentSettings = settings;

		function updateRefresh(refreshTime)
		{
			if(updateTimer)
			{
				clearInterval(updateTimer);
			}

			updateTimer = setInterval(function()
			{
				self.updateNow();
			}, refreshTime);
		}

		function toTitleCase(str)
		{
			return str.replace(/\w\S*/g, function(txt)
			{
				return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
			});
		}

		updateRefresh(currentSettings.refresh * 1000);

		this.updateNow = function()
		{
			$.ajax({
				url       : "http://api.openweathermap.org/data/2.5/weather?q=" + encodeURIComponent(currentSettings.location) + "&units=" + currentSettings.units,
				dataType  : "JSONP",
				success   : function(data)
				{
					// Rejigger our data into something easier to understand
					var newData = {
						place_name      : data.name,
						sunrise         : (new Date(data.sys.sunrise * 1000)).toLocaleTimeString(),
						sunset          : (new Date(data.sys.sunset * 1000)).toLocaleTimeString(),
						conditions      : toTitleCase(data.weather[0].description),
						current_temp    : data.main.temp,
						high_temp       : data.main.temp_max,
						low_temp        : data.main.temp_min,
						pressure        : data.main.pressure,
						humidity        : data.main.humidity,
						wind_speed      : data.wind.speed,
						wind_direction  : data.wind.deg
					};

					updateCallback(newData);
				},
				error     : function(xhr, status, error)
				{
				}
			});
		}

		this.onDispose = function()
		{
			clearInterval(updateTimer);
			updateTimer = null;
		}

		this.onSettingsChanged = function(newSettings)
		{
			currentSettings = newSettings;
            self.updateNow();
			updateRefresh(currentSettings.refresh * 1000);
		}
	};

	freeboard.loadDatasourcePlugin({
		type_name  : "openweathermap",
		display_name: "Open Weather Map API",
		settings   : [
			{
				name        : "location",
				display_name: "Location",
				type        : "text",
				description : "Example: London, UK"
			},
			{
				name        : "units",
				display_name: "Units",
				type   : "option",
				default : "imperial",
				options: [
					{
						name : "Imperial",
						value: "imperial"
					},
					{
						name : "Metric",
						value: "metric"
					}
				]
			},
			{
				name         : "refresh",
				display_name : "Refresh Every",
				type         : "number",
				suffix       : "seconds",
				default_value: 5
			}
		],
		newInstance: function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback( new openWeatherMapDatasource(settings, updateCallback));
		}
	});

	var dweetioDatasource = function(settings, updateCallback)
	{
		var self = this;
		var currentSettings = settings;

		function onNewDweet(dweet)
		{
			updateCallback(dweet);
		}

		this.updateNow = function()
		{
			dweetio.get_latest_dweet_for(currentSettings.thing_id, function(err, dweet){
				if(err)
				{
					//onNewDweet({});
				}
				else
				{
					onNewDweet(dweet[0].content);
				}
			});
		}

		this.onDispose = function()
		{

		}

		this.onSettingsChanged = function(newSettings)
		{
			dweetio.stop_listening();

			currentSettings = newSettings;

			dweetio.listen_for(currentSettings.thing_id, function(dweet)
			{
				onNewDweet(dweet.content);
			});
		}

		self.onSettingsChanged(settings);
	};

	freeboard.loadDatasourcePlugin({
		"type_name"   : "dweet_io",
		"display_name": "Dweet.io",
		"external_scripts" : [
			"http://dweet.io/client/dweet.io.min.js"
		],
		"settings"    : [
			{
				name        : "thing_id",
				display_name: "Thing Name",
				"description": "Example: salty-dog-1",
				type        : "text"
			}
		],
		newInstance : function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback(new dweetioDatasource(settings, updateCallback));
		}
	});

    var playbackDatasource = function(settings, updateCallback)
    {
        var self = this;
        var currentSettings = settings;
        var currentDataset = [];
        var currentIndex = 0;
        var currentTimeout;

        function moveNext()
        {
            if(currentDataset.length > 0)
            {
                if(currentIndex < currentDataset.length)
                {
                    updateCallback(currentDataset[currentIndex]);
                    currentIndex++;
                }

                if(currentIndex >= currentDataset.length && currentSettings.loop)
                {
                    currentIndex = 0;
                }

                if(currentIndex < currentDataset.length)
                {
                    currentTimeout = setTimeout(moveNext, currentSettings.refresh * 1000);
                }
            }
            else
            {
                updateCallback({});
            }
        }

        function stopTimeout()
        {
            currentDataset = [];
            currentIndex = 0;

            if(currentTimeout)
            {
                clearTimeout(currentTimeout);
                currentTimeout = null;
            }
        }

        this.updateNow = function()
        {
            stopTimeout();

            $.ajax({
                url       : currentSettings.datafile,
                dataType  : (currentSettings.is_jsonp) ? "JSONP" : "JSON",
                success   : function(data)
                {
                    if(_.isArray(data))
                    {
                        currentDataset = data;
                    }
                    else
                    {
                        currentDataset = [];
                    }

                    currentIndex = 0;

                    moveNext();
                },
                error     : function(xhr, status, error)
                {
                }
            });
        }

        this.onDispose = function()
        {
            stopTimeout();
        }

        this.onSettingsChanged = function(newSettings)
        {
            currentSettings = newSettings;
            self.updateNow();
        }
    };

    freeboard.loadDatasourcePlugin({
        "type_name": "playback",
        "display_name": "Playback",
        "settings": [
            {
                "name": "datafile",
                "display_name": "Data File URL",
                "type": "text",
                "description": "A link to a JSON array of data."
            },
            {
                name        : "is_jsonp",
                display_name: "Is JSONP",
                type        : "boolean"
            },
            {
                "name": "loop",
                "display_name": "Loop",
                "type": "boolean",
                "description": "Rewind and loop when finished"
            },
            {
                "name": "refresh",
                "display_name": "Refresh Every",
                "type": "number",
                "suffix": "seconds",
                "default_value": 5
            }
        ],
        newInstance: function(settings, newInstanceCallback, updateCallback)
        {
            newInstanceCallback( new playbackDatasource(settings, updateCallback));
        }
    });

    var clockDatasource = function(settings, updateCallback)
    {
        var self = this;
        var currentSettings = settings;
        var timer;

        function stopTimer()
        {
            if(timer)
            {
                clearTimeout(timer);
                timer = null;
            }
        }

        function updateTimer()
        {
            stopTimer();
            timer = setInterval(self.updateNow, currentSettings.refresh * 1000);
        }

        this.updateNow = function()
        {
            var date = new Date();

            var data = {
                numeric_value : date.getTime(),
                full_string_value : date.toLocaleString(),
                date_string_value : date.toLocaleDateString(),
                time_string_value : date.toLocaleTimeString(),
                date_object : date
            };

            updateCallback(data);
        }

        this.onDispose = function()
        {
            stopTimer();
        }

        this.onSettingsChanged = function(newSettings)
        {
            currentSettings = newSettings;
            updateTimer();
        }

        updateTimer();
    };

    freeboard.loadDatasourcePlugin({
        "type_name": "clock",
        "display_name": "Clock",
        "settings": [
            {
                "name": "refresh",
                "display_name": "Refresh Every",
                "type": "number",
                "suffix": "seconds",
                "default_value": 1
            }
        ],
        newInstance: function(settings, newInstanceCallback, updateCallback)
        {
            newInstanceCallback( new clockDatasource(settings, updateCallback));
        }
    });

}());<commit_msg>Fixed a bug<commit_after>// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ F R E E B O A R D                                                  │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Copyright © 2013 Jim Heising (https://github.com/jheising)         │ \\
// │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net)               │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license.                                    │ \\
// └────────────────────────────────────────────────────────────────────┘ \\

(function()
{
	var jsonDatasource = function(settings, updateCallback)
	{
		var self = this;
		var updateTimer = null;
		var currentSettings = settings;
		var errorStage = 0; 	// 0 = try standard request
								// 1 = try JSONP
								// 2 = try thingproxy.freeboard.io
		var lockErrorStage = false;

		function updateRefresh(refreshTime)
		{
			if(updateTimer)
			{
				clearInterval(updateTimer);
			}

			updateTimer = setInterval(function()
			{
				self.updateNow();
			}, refreshTime);
		}

		updateRefresh(currentSettings.refresh * 1000);

		this.updateNow = function()
		{
			if((errorStage > 1 && !currentSettings.use_thingproxy) || errorStage > 2) // We've tried everything, let's quit
			{
				return; // TODO: Report an error
			}

			var requestURL = currentSettings.url;

			if(errorStage == 2 && currentSettings.use_thingproxy)
			{
				requestURL = (location.protocol == "https:" ? "https:" : "http:") + "//thingproxy.freeboard.io/fetch/" + encodeURI(currentSettings.url);
			}

			var body = currentSettings.body;

			// Can the body be converted to JSON?
			if(body)
			{
				try
				{
					body = JSON.parse(body);
				}
				catch(e)
				{
				}
			}

			$.ajax({
				url       : requestURL,
				dataType  : (errorStage == 1) ? "JSONP" : "JSON",
				type : currentSettings.method || "GET",
				data : body,
				beforeSend: function(xhr)
				{
					try
					{
						_.each(currentSettings.headers, function(header)
						{
							var name = header.name;
							var value = header.value;

							if(!_.isUndefined(name) && !_.isUndefined(value))
							{
								xhr.setRequestHeader(name, value);
							}
						});
					}
					catch(e)
					{
					}
				},
				success   : function(data)
				{
					lockErrorStage = true;
					updateCallback(data);
				},
				error     : function(xhr, status, error)
				{
					if(!lockErrorStage)
					{
						// TODO: Figure out a way to intercept CORS errors only. The error message for CORS errors seems to be a standard 404.
						errorStage++;
						self.updateNow();
					}
				}
			});
		}

		this.onDispose = function()
		{
			clearInterval(updateTimer);
			updateTimer = null;
		}

		this.onSettingsChanged = function(newSettings)
		{
			lockErrorStage = false;
			errorStage = 0;

			currentSettings = newSettings;
			updateRefresh(currentSettings.refresh * 1000);
			self.updateNow();
		}
	};

	freeboard.loadDatasourcePlugin({
		type_name  : "JSON",
		settings   : [
			{
				name        : "url",
				display_name: "URL",
				type        : "text"
			},
			{
				name         : "use_thingproxy",
				display_name : "Try thingproxy",
				description : 'A direct JSON connection will be tried first, if that fails, a JSONP connection will be tried. If that fails, you can use thingproxy, which can solve many connection problems to APIs. More information.',
				type         : "boolean",
				default_value: true
			},
			{
				name         : "refresh",
				display_name : "Refresh Every",
				type         : "number",
				suffix       : "seconds",
				default_value: 5
			},
			{
				name: "method",
				display_name: "Method",
				type: "option",
				options: [
					{
						name: "GET",
						value: "GET"
					},
					{
						name: "POST",
						value: "POST"
					},
					{
						name: "PUT",
						value: "PUT"
					},
					{
						name: "DELETE",
						value: "DELETE"
					}
				]
			},
			{
				name        : "body",
				display_name: "Body",
				type        : "text",
				description : "The body of the request. Normally only used if method is POST"
			},
			{
				name        : "headers",
				display_name: "Headers",
				type        : "array",
				settings    : [
					{
						name        : "name",
						display_name: "Name",
						type        : "text"
					},
					{
						name        : "value",
						display_name: "Value",
						type        : "text"
					}
				]
			}
		],
		newInstance: function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback( new jsonDatasource(settings, updateCallback));
		}
	});

	var openWeatherMapDatasource = function(settings, updateCallback)
	{
		var self = this;
		var updateTimer = null;
		var currentSettings = settings;

		function updateRefresh(refreshTime)
		{
			if(updateTimer)
			{
				clearInterval(updateTimer);
			}

			updateTimer = setInterval(function()
			{
				self.updateNow();
			}, refreshTime);
		}

		function toTitleCase(str)
		{
			return str.replace(/\w\S*/g, function(txt)
			{
				return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
			});
		}

		updateRefresh(currentSettings.refresh * 1000);

		this.updateNow = function()
		{
			$.ajax({
				url       : "http://api.openweathermap.org/data/2.5/weather?q=" + encodeURIComponent(currentSettings.location) + "&units=" + currentSettings.units,
				dataType  : "JSONP",
				success   : function(data)
				{
					// Rejigger our data into something easier to understand
					var newData = {
						place_name      : data.name,
						sunrise         : (new Date(data.sys.sunrise * 1000)).toLocaleTimeString(),
						sunset          : (new Date(data.sys.sunset * 1000)).toLocaleTimeString(),
						conditions      : toTitleCase(data.weather[0].description),
						current_temp    : data.main.temp,
						high_temp       : data.main.temp_max,
						low_temp        : data.main.temp_min,
						pressure        : data.main.pressure,
						humidity        : data.main.humidity,
						wind_speed      : data.wind.speed,
						wind_direction  : data.wind.deg
					};

					updateCallback(newData);
				},
				error     : function(xhr, status, error)
				{
				}
			});
		}

		this.onDispose = function()
		{
			clearInterval(updateTimer);
			updateTimer = null;
		}

		this.onSettingsChanged = function(newSettings)
		{
			currentSettings = newSettings;
            self.updateNow();
			updateRefresh(currentSettings.refresh * 1000);
		}
	};

	freeboard.loadDatasourcePlugin({
		type_name  : "openweathermap",
		display_name: "Open Weather Map API",
		settings   : [
			{
				name        : "location",
				display_name: "Location",
				type        : "text",
				description : "Example: London, UK"
			},
			{
				name        : "units",
				display_name: "Units",
				type   : "option",
				default : "imperial",
				options: [
					{
						name : "Imperial",
						value: "imperial"
					},
					{
						name : "Metric",
						value: "metric"
					}
				]
			},
			{
				name         : "refresh",
				display_name : "Refresh Every",
				type         : "number",
				suffix       : "seconds",
				default_value: 5
			}
		],
		newInstance: function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback( new openWeatherMapDatasource(settings, updateCallback));
		}
	});

	var dweetioDatasource = function(settings, updateCallback)
	{
		var self = this;
		var currentSettings = settings;

		function onNewDweet(dweet)
		{
			updateCallback(dweet);
		}

		this.updateNow = function()
		{
			dweetio.get_latest_dweet_for(currentSettings.thing_id, function(err, dweet){
				if(err)
				{
					//onNewDweet({});
				}
				else
				{
					onNewDweet(dweet[0].content);
				}
			});
		}

		this.onDispose = function()
		{

		}

		this.onSettingsChanged = function(newSettings)
		{
			dweetio.stop_listening();

			currentSettings = newSettings;

			dweetio.listen_for(currentSettings.thing_id, function(dweet)
			{
				onNewDweet(dweet.content);
			});
		}

		self.onSettingsChanged(settings);
	};

	freeboard.loadDatasourcePlugin({
		"type_name"   : "dweet_io",
		"display_name": "Dweet.io",
		"external_scripts" : [
			"http://dweet.io/client/dweet.io.min.js"
		],
		"settings"    : [
			{
				name        : "thing_id",
				display_name: "Thing Name",
				"description": "Example: salty-dog-1",
				type        : "text"
			}
		],
		newInstance : function(settings, newInstanceCallback, updateCallback)
		{
			newInstanceCallback(new dweetioDatasource(settings, updateCallback));
		}
	});

    var playbackDatasource = function(settings, updateCallback)
    {
        var self = this;
        var currentSettings = settings;
        var currentDataset = [];
        var currentIndex = 0;
        var currentTimeout;

        function moveNext()
        {
            if(currentDataset.length > 0)
            {
                if(currentIndex < currentDataset.length)
                {
                    updateCallback(currentDataset[currentIndex]);
                    currentIndex++;
                }

                if(currentIndex >= currentDataset.length && currentSettings.loop)
                {
                    currentIndex = 0;
                }

                if(currentIndex < currentDataset.length)
                {
                    currentTimeout = setTimeout(moveNext, currentSettings.refresh * 1000);
                }
            }
            else
            {
                updateCallback({});
            }
        }

        function stopTimeout()
        {
            currentDataset = [];
            currentIndex = 0;

            if(currentTimeout)
            {
                clearTimeout(currentTimeout);
                currentTimeout = null;
            }
        }

        this.updateNow = function()
        {
            stopTimeout();

            $.ajax({
                url       : currentSettings.datafile,
                dataType  : (currentSettings.is_jsonp) ? "JSONP" : "JSON",
                success   : function(data)
                {
                    if(_.isArray(data))
                    {
                        currentDataset = data;
                    }
                    else
                    {
                        currentDataset = [];
                    }

                    currentIndex = 0;

                    moveNext();
                },
                error     : function(xhr, status, error)
                {
                }
            });
        }

        this.onDispose = function()
        {
            stopTimeout();
        }

        this.onSettingsChanged = function(newSettings)
        {
            currentSettings = newSettings;
            self.updateNow();
        }
    };

    freeboard.loadDatasourcePlugin({
        "type_name": "playback",
        "display_name": "Playback",
        "settings": [
            {
                "name": "datafile",
                "display_name": "Data File URL",
                "type": "text",
                "description": "A link to a JSON array of data."
            },
            {
                name        : "is_jsonp",
                display_name: "Is JSONP",
                type        : "boolean"
            },
            {
                "name": "loop",
                "display_name": "Loop",
                "type": "boolean",
                "description": "Rewind and loop when finished"
            },
            {
                "name": "refresh",
                "display_name": "Refresh Every",
                "type": "number",
                "suffix": "seconds",
                "default_value": 5
            }
        ],
        newInstance: function(settings, newInstanceCallback, updateCallback)
        {
            newInstanceCallback( new playbackDatasource(settings, updateCallback));
        }
    });

    var clockDatasource = function(settings, updateCallback)
    {
        var self = this;
        var currentSettings = settings;
        var timer;

        function stopTimer()
        {
            if(timer)
            {
                clearTimeout(timer);
                timer = null;
            }
        }

        function updateTimer()
        {
            stopTimer();
            timer = setInterval(self.updateNow, currentSettings.refresh * 1000);
        }

        this.updateNow = function()
        {
            var date = new Date();

            var data = {
                numeric_value : date.getTime(),
                full_string_value : date.toLocaleString(),
                date_string_value : date.toLocaleDateString(),
                time_string_value : date.toLocaleTimeString(),
                date_object : date
            };

            updateCallback(data);
        }

        this.onDispose = function()
        {
            stopTimer();
        }

        this.onSettingsChanged = function(newSettings)
        {
            currentSettings = newSettings;
            updateTimer();
        }

        updateTimer();
    };

    freeboard.loadDatasourcePlugin({
        "type_name": "clock",
        "display_name": "Clock",
        "settings": [
            {
                "name": "refresh",
                "display_name": "Refresh Every",
                "type": "number",
                "suffix": "seconds",
                "default_value": 1
            }
        ],
        newInstance: function(settings, newInstanceCallback, updateCallback)
        {
            newInstanceCallback( new clockDatasource(settings, updateCallback));
        }
    });

}());<|endoftext|>


<commit_before>var JSHint = require('jshint').JSHINT;
var CSSLint = require('csslint').CSSLint;
var css = require('css');
var acorn = require('acorn');
var domify = require('domify');
var JscsStringChecker = require('jscs/lib/string-checker.js');
var _ = require('lodash');

var ResponseTypes = {
	LINT : 'lint',
	STYLE : 'style',
	ERROR : 'error',
	FAILURE : 'failure',
	SUCCESS : 'success'
};

var extend = function(a, b){
	var surrogate = function(){};
	surrogate.prototype = a.prototype;
	b.prototype = new surrogate();
};

var Response = function(type, errors){
	this.type = type;
	this.errors = errors;
};

var RinzaiError = function(message, line, char){
	this.message = message;
	this.line = line;
	this.char = char;
};

var Question = function(config, options){
	this.options = options;
	this.test = config.test;
	this.envUrl = config.envUrl;
	this.messages = config.messages || {};
	this.type = config.type;
};

Question.prototype.createEnvironment = function(cb){
	var self = this;
	this.envFrame = document.createElement('iframe');
	this.envFrame.style.position = 'absolute';
	this.envFrame.style.top = '-1000px';
	this.envFrame.style.left = '-1000px';
	document.body.appendChild(this.envFrame);
	var env = this.envFrame.contentWindow;
	if(this.envUrl){
		this.envFrame.src = this.envUrl;
		var onLoad = function(){
			self.envFrame.removeEventListener('load', onLoad);
			cb(env);
		};
		this.envFrame.addEventListener('load', onLoad);
	} else {
		cb(env);
	}
};

Question.prototype.destroyEnvironment = function(){
	if(this.envFrame){
		this.envFrame.parentNode.removeChild(this.envFrame);
	 	delete this.envFrame;
	}
};

Question.prototype.runTest = function(content, parsed, cb){
	var self = this;
	var env = this.createEnvironment(function(env){
		try {
			self.test(content, parsed, env);
		} catch(e) {
			self.destroyEnvironment();
			return cb(e);
		}
		self.destroyEnvironment();
		return cb();
	});
};

var HTMLQuestion = function(){
	Question.apply(this, arguments);
};

extend(Question, HTMLQuestion);

HTMLQuestion.prototype.answer = function(content, cb){
	var self = this;
	var parser = new DOMParser();

	try {
		var parseErrors = this.validate(content);
		if (parseErrors.length){
			return cb(new Response(ResponseTypes.LINT, parseErrors));
		}
	} catch(e) {
		return cb(new Response(ResponseTypes.ERROR, [
			new RinzaiError(e.message, null, null)
		]));
	}

	var node = domify(content);
	var nodes = [];
	if (node instanceof DocumentFragment){
		nodes = _.toArray(node.querySelectorAll('*'));
	} else {
		nodes = [node];
	}
	this.runTest(content, nodes, function(testErr){
		if(testErr) {
			return cb(new Response(
				ResponseTypes.FAILURE,
				[
					new RinzaiError(testErr.message, null, null)
				]
			));
		}

		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

HTMLQuestion.prototype.validate = function(html){
	var parser = new DOMParser();
	var d = parser.parseFromString('<?xml version="1.0"?>\n' + html + '\n','text/xml');
	var errors = [];
	if (d.querySelector('parsererror')) {
		errors = _.map(d.querySelectorAll('parsererror > div'), function(node){
			var errorText = node.textContent;
			var matches = errorText.match(/error on line (\d+) at column (\d+)\:\s(.+)/);
			return new RinzaiError(matches[3], parseInt(matches[1], 10) - 1, parseInt(matches[2], 10));
		});
	} else {
			d = parser.parseFromString(html, 'text/html');
			allnodes = d.getElementsByTagName('*');
			for (var i=allnodes.length-1; i>=0; i--) {
					if (allnodes[i] instanceof HTMLUnknownElement){
						errors.push(new RinzaiError('Unknown HTML element: ' + allnodes[i].tagName, null, null));
					}
			}
	}
	return errors;
};

var JSQuestion = function(){
	Question.apply(this, arguments);
};

extend(Question, JSQuestion);

JSQuestion.prototype.answer = function(content, cb){
	var self = this;

	JSHint(content, this.options.jshint);
	if(JSHint.errors.length){
		return cb(new Response(
			ResponseTypes.LINT,
			_.map(JSHint.errors, function(err){
				return new RinzaiError(err.reason, err.line);
			})
		));
	}

	var checker = new JscsStringChecker();
	checker.registerDefaultRules();
	checker.configure(this.options.jscs || {});
	var styleErrors = checker.checkString(content);
	if(styleErrors.getErrorList().length){
		return cb(new Response(
			ResponseTypes.STYLE,
			_.map(styleErrors.getErrorList(), function(err){
				return new RinzaiError(err.message, err.line, err.column);
			})
		));
	}

	var ast = acorn.parse(content);
	this.runTest(content, ast, function(testErr){
		if(testErr){
			var firstStack = testErr.stack.split('\n')[1];
			if(firstStack && firstStack.indexOf('eval') > -1){
				var position = firstStack.match(/(\d+)\:(\d+)\)$/);
				return cb(new Response(
					ResponseTypes.ERROR,
					[
						new RinzaiError(testErr.message, parseInt(position[1], 10), parseInt(position[2], 10))
					]
				));
			} else {
				return cb(new Response(
					ResponseTypes.FAILURE,
					[
						new RinzaiError(testErr.message, null, null)
					]
				));
			}
		}
		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

var CSSQuestion = function(config){
	this.test = config.test;
};

extend(Question, CSSQuestion);

CSSQuestion.prototype.answer = function(content, cb){
	var results = CSSLint.verify(content);
	if(results.messages.length){
		return cb(new Response(
			ResponseTypes.LINT,
			_.map(results.messages, function(err){
				return new RinzaiError(err.message, err.line, err.col);
			})
		));
	}

	var ast = css.parse(content);
	this.runTest(content, ast, function(testErr){
		if(testErr){
			return cb(new Response(
				ResponseTypes.FAILURE,
				[
					new RinzaiError(testErr.message, null, null)
				]
			));
		}
		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

var Rinzai = function(config, options){
	this.options = options || {};
	this.questions = [];
	this.questionsById = {};
	_.forEach(config.questions, this.addQuestion, this);
};

Rinzai.prototype.addQuestion = function(q){
	var question;
	switch(q.type){
		case 'html':
			question = new HTMLQuestion(q, this.options);
			break;
		case 'javascript':
			question = new JSQuestion(q, this.options);
			break;
		case 'css':
			question = new CSSQuestion(q, this.options);
			break;
		default:
			throw new Error('Question type is unknown or undefined');
	}
	this.questions.push(question);
	this.questionsById[q.id] = question;
	return question;
};

module.exports = Rinzai;
<commit_msg>Async test running, env factory, js lint/parse/style error handling<commit_after>var JSHint = require('jshint').JSHINT;
var CSSLint = require('csslint').CSSLint;
var css = require('css');
var acorn = require('acorn');
var domify = require('domify');
var JscsStringChecker = require('jscs/lib/string-checker.js');
var _ = require('lodash');

var ResponseTypes = {
	LINT : 'lint',
	STYLE : 'style',
	ERROR : 'error',
	FAILURE : 'failure',
	SUCCESS : 'success'
};

var extend = function (a, b) {
	var surrogate = function(){};
	surrogate.prototype = a.prototype;
	b.prototype = new surrogate();
};

var Response = function (type, errors) {
	this.type = type;
	this.errors = errors;
};

var RinzaiError = function (message, line, char) {
	this.message = message;
	this.line = line;
	this.char = char;
};

var Run = function (fn, done, timeout) {
	this.fn = fn;
	this.done = done;
	this.timeout = timeout;
};

var Question = function (config, options, runner) {
	this.options = options;
	this.test = config.test;
	this.envUrl = config.envUrl;
	this.messages = config.messages || {};
	this.type = config.type;
	this.runner = runner;
	this.async = this.test.length === 4;
	this.timeout = options.timeout || 500;
};

Question.prototype.createEnvironment = function(cb){
	var self = this;
	if(!this.envFrames) this.envFrames = [];
	var envFrame = document.createElement('iframe');
	envFrame.style.position = 'absolute';
	envFrame.style.top = '-1000px';
	envFrame.style.left = '-1000px';
	document.body.appendChild(envFrame);
	this.envFrames.push(envFrame);
	var env = envFrame.contentWindow;
	if(this.envUrl){
		envFrame.src = this.envUrl;
		var onLoad = function(){
			envFrame.removeEventListener('load', onLoad);
			cb(env);
		};
		envFrame.addEventListener('load', onLoad);
	} else {
		cb(env);
	}
};

Question.prototype.destroyEnvironments = function(){
	if(this.envFrames){
		_.forEach(this.envFrames, function(envFrame){
			envFrame.parentNode.removeChild(envFrame);
		});
	 	delete this.envFrames;
	}
};

Question.prototype.runTest = function(content, parsed, cb){
	var self = this;
	var run;
	var envFactory = _.bindKey(this, 'createEnvironment');

	var done = function(err){
		self.destroyEnvironments();
		if (err) {
			cb(err);
		} else {
			cb();
		}
		self.runner.dequeue();
	};

	var fn = function(){
		try {
			if (self.async) {
				self.test(content, parsed, envFactory, done);
			} else {
				self.text(content, parsed, envFactory);
			}
		} catch(e) {
			done(e);
		}
		if (!self.async) {
			done();
		}
	};

	if (this.async) {
		run = new Run(fn, done, this.timeout);
	} else {
		run = new Run(fn);
	}

	this.runner.queue(run);
};

var HTMLQuestion = function(){
	Question.apply(this, arguments);
};

extend(Question, HTMLQuestion);

HTMLQuestion.prototype.answer = function(content, cb){
	var self = this;
	var parser = new DOMParser();

	try {
		var parseErrors = this.validate(content);
		if (parseErrors.length){
			return cb(new Response(ResponseTypes.LINT, parseErrors));
		}
	} catch(e) {
		return cb(new Response(ResponseTypes.ERROR, [
			new RinzaiError(e.message, null, null)
		]));
	}

	var node = domify(content);
	var nodes = [];
	if (node instanceof DocumentFragment){
		nodes = _.toArray(node.querySelectorAll('*'));
	} else {
		nodes = [node];
	}
	this.runTest(content, nodes, function(testErr){
		if(testErr) {
			return cb(new Response(
				ResponseTypes.FAILURE,
				[
					new RinzaiError(testErr.message, null, null)
				]
			));
		}

		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

HTMLQuestion.prototype.validate = function(html){
	var parser = new DOMParser();
	var d = parser.parseFromString('<?xml version="1.0"?>\n' + html + '\n','text/xml');
	var errors = [];
	if (d.querySelector('parsererror')) {
		errors = _.map(d.querySelectorAll('parsererror > div'), function(node){
			var errorText = node.textContent;
			var matches = errorText.match(/error on line (\d+) at column (\d+)\:\s(.+)/);
			return new RinzaiError(matches[3], parseInt(matches[1], 10) - 1, parseInt(matches[2], 10));
		});
	} else {
			d = parser.parseFromString(html, 'text/html');
			allnodes = d.getElementsByTagName('*');
			for (var i=allnodes.length-1; i>=0; i--) {
					if (allnodes[i] instanceof HTMLUnknownElement){
						errors.push(new RinzaiError('Unknown HTML element: ' + allnodes[i].tagName, null, null));
					}
			}
	}
	return errors;
};

var JSQuestion = function(){
	Question.apply(this, arguments);
};

extend(Question, JSQuestion);

JSQuestion.prototype.answer = function(content, cb){
	var self = this;
	var ast;
	try {
		JSHint(content, this.options.jshint);
		if(JSHint.errors.length){
			return cb(new Response(
				ResponseTypes.LINT,
				_.map(JSHint.errors, function(err){
					return new RinzaiError(err.reason, err.line);
				})
			));
		}

		var checker = new JscsStringChecker();
		checker.registerDefaultRules();
		checker.configure(this.options.jscs || {});
		var styleErrors = checker.checkString(content);
		if(styleErrors.getErrorList().length){
			return cb(new Response(
				ResponseTypes.STYLE,
				_.map(styleErrors.getErrorList(), function(err){
					return new RinzaiError(err.message, err.line, err.column);
				})
			));
		}

		ast = acorn.parse(content);
	} catch (e) {
		return cb(new Response(
			ResponseTypes.ERROR,
			[new RinzaiError(e.message, null, null)]
		));
	}

	this.runTest(content, ast, function(testErr){
		if(testErr){
			var firstStack = testErr.stack.split('\n')[1];
			if(firstStack && firstStack.indexOf('eval') > -1){
				var position = firstStack.match(/(\d+)\:(\d+)\)$/);
				return cb(new Response(
					ResponseTypes.ERROR,
					[new RinzaiError(testErr.message, parseInt(position[1], 10), parseInt(position[2], 10))]
				));
			} else {
				return cb(new Response(
					ResponseTypes.FAILURE,
					[new RinzaiError(testErr.message, null, null)]
				));
			}
		}
		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

var CSSQuestion = function(){
	Question.apply(this, arguments);
};

extend(Question, CSSQuestion);

CSSQuestion.prototype.answer = function (content, cb) {
	var results = CSSLint.verify(content);
	if(results.messages.length){
		return cb(new Response(
			ResponseTypes.LINT,
			_.map(results.messages, function (err) {
				return new RinzaiError(err.message, err.line, err.col);
			})
		));
	}

	var ast = css.parse(content);
	this.runTest(content, ast, function (testErr) {
		if(testErr){
			return cb(new Response(
				ResponseTypes.FAILURE,
				[
					new RinzaiError(testErr.message, null, null)
				]
			));
		}
		return cb(new Response(
			ResponseTypes.SUCCESS
		));
	});
};

var Runner = function () {
	this.runQueue = [];
	this.running = false;
};

Runner.prototype.queue = function (run) {
	if (this.running) {
		this.runQueue.push(run);
	} else {
		this.running = true;
		this.run(run);
	}
};

Runner.prototype.dequeue = function () {
	this.removeErrorHandler();
	this.clearTimeout();
	if (this.runQueue.length) {
		this.run(this.runQueue.shift());
	} else {
		this.running = false;
	}
};

Runner.prototype.run = function (run) {
	var self = this;
	if (run.done) {
		this.addErrorHandler(run.done);
		this.setTimeout(run.done, run.timeout);
	}
	run.fn();
};

Runner.prototype.addErrorHandler = function (handler) {
	var self = this;
	this.errorHandler = function(evt){
		self.removeErrorHandler();
		self.clearTimeout();
		handler(evt.error);
		evt.preventDefault();
	};
	window.addEventListener('error', this.errorHandler);
};

Runner.prototype.removeErrorHandler = function () {
	if(this.errorHandler) {
		window.removeEventListener('error', this.errorHandler);
		delete this.errorHandler;
	}
};

Runner.prototype.setTimeout = function (handler, timeout) {
	var self = this;
	this.runTimeout = setTimeout(function () {
		self.removeErrorHandler();
		delete self.runTimeout;
		handler(new Error('Test timed out.'));
	}, timeout);
};

Runner.prototype.clearTimeout = function () {
	if (this.runTimeout !== undefined) {
		clearTimeout(this.runTimeout);
		delete this.runTimeout;
	}
};

var Rinzai = function (config, options) {
	this.options = options || {};
	this.questions = [];
	this.questionsById = {};
	this.runner = new Runner();
	_.forEach(config.questions, this.addQuestion, this);
};

Rinzai.prototype.addQuestion = function (q) {
	var question;
	switch(q.type){
		case 'html':
			question = new HTMLQuestion(q, this.options, this.runner);
			break;
		case 'javascript':
			question = new JSQuestion(q, this.options, this.runner);
			break;
		case 'css':
			question = new CSSQuestion(q, this.options, this.runner);
			break;
		default:
			throw new Error('Question type is unknown or undefined');
	}
	this.questions.push(question);
	this.questionsById[q.id] = question;
	return question;
};

module.exports = Rinzai;
<|endoftext|>


<commit_before>/**
 * Clipboard.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This class contains logic for getting HTML contents out of the clipboard.
 *
 * We need to make a lot of ugly hacks to get the contents out of the clipboard since
 * the W3C Clipboard API is broken in all browsers: Gecko/WebKit/Blink. We might rewrite
 * this the way those API:s stabilize.
 *
 * Current implementation steps:
 *  1. On keydown with paste keys Ctrl+V or Shift+Insert create
 *     a paste bin element and move focus to that element.
 *  2. Wait for the browser to fire a "paste" event and get the contents out of the paste bin.
 *  3. Check if the paste was successful if true, process the HTML.
 *  (4). If the paste was unsuccessful use IE execCommand, Clipboard API, document.dataTransfer old WebKit API etc.
 *
 * @class tinymce.pasteplugin.Clipboard
 * @private
 */
define("tinymce/pasteplugin/Clipboard", [
	"tinymce/Env",
	"tinymce/util/VK",
	"tinymce/pasteplugin/Utils"
], function(Env, VK, Utils) {
	return function(editor) {
		var self = this, pasteBinElm, lastRng, keyboardPasteTimeStamp = 0;
		var pasteBinDefaultContent = '%MCEPASTEBIN%', keyboardPastePlainTextState;

		/**
		 * Pastes the specified HTML. This means that the HTML is filtered and then
		 * inserted at the current selection in the editor. It will also fire paste events
		 * for custom user filtering.
		 *
		 * @param {String} html HTML code to paste into the current selection.
		 */
		function pasteHtml(html) {
			var args, dom = editor.dom;

			args = editor.fire('BeforePastePreProcess', {content: html}); // Internal event used by Quirks
			args = editor.fire('PastePreProcess', args);
			html = args.content;

			if (!args.isDefaultPrevented()) {
				// User has bound PastePostProcess events then we need to pass it through a DOM node
				// This is not ideal but we don't want to let the browser mess up the HTML for example
				// some browsers add   to P tags etc
				if (editor.hasEventListeners('PastePostProcess') && !args.isDefaultPrevented()) {
					// We need to attach the element to the DOM so Sizzle selectors work on the contents
					var tempBody = dom.add(editor.getBody(), 'div', {style: 'display:none'}, html);
					args = editor.fire('PastePostProcess', {node: tempBody});
					dom.remove(tempBody);
					html = args.node.innerHTML;
				}

				if (!args.isDefaultPrevented()) {
					editor.insertContent(html);
				}
			}
		}

		/**
		 * Pastes the specified text. This means that the plain text is processed
		 * and converted into BR and P elements. It will fire paste events for custom filtering.
		 *
		 * @param {String} text Text to paste as the current selection location.
		 */
		function pasteText(text) {
			text = editor.dom.encode(text).replace(/\r\n/g, '\n');

			var startBlock = editor.dom.getParent(editor.selection.getStart(), editor.dom.isBlock);

			// Create start block html for example

			var forcedRootBlockName = editor.settings.forced_root_block;
			var forcedRootBlockStartHtml;
			if (forcedRootBlockName) {
				forcedRootBlockStartHtml = editor.dom.createHTML(forcedRootBlockName, editor.settings.forced_root_block_attrs);
				forcedRootBlockStartHtml = forcedRootBlockStartHtml.substr(0, forcedRootBlockStartHtml.length - 3) + '>';
			}

			if ((startBlock && /^(PRE|DIV)$/.test(startBlock.nodeName)) || !forcedRootBlockName) {
				text = Utils.filter(text, [
					[/\n/g, "
"]
				]);
			} else {
				text = Utils.filter(text, [
					[/\n\n/g, "
" + forcedRootBlockStartHtml],
					[/^(.*<\/p>)(
)$/, forcedRootBlockStartHtml + '$1'],
					[/\n/g, "
"]
				]);

				if (text.indexOf('
') != -1) {
					text = forcedRootBlockStartHtml + text;
				}
			}

			pasteHtml(text);
		}

		/**
		 * Creates a paste bin element and moves the selection into that element. It will also move the element offscreen
		 * so that resize handles doesn't get produced on IE or Drag handles or Firefox.
		 */
		function createPasteBin() {
			var dom = editor.dom, body = editor.getBody();
			var viewport = editor.dom.getViewPort(editor.getWin()), scrollTop, top = 20;

			lastRng = editor.selection.getRng();
			scrollTop = editor.inline ? editor.selection.getScrollContainer().scrollTop : viewport.y;

			// Calculate top cordinate this is needed to avoid scrolling to top of document
			// We want the paste bin to be as close to the caret as possible to avoid scrolling
			if (lastRng.getClientRects) {
				var rects = lastRng.getClientRects();
				if (rects.length) {
					top = scrollTop + (rects[0].top - dom.getPos(body).y);
				}
			}

			// Create a pastebin
			pasteBinElm = dom.add(editor.getBody(), 'div', {
				id: "mcepastebin",
				contentEditable: true,
				"data-mce-bogus": "1",
				style: 'position: absolute; top: ' + top + 'px;' +
					'width: 10px; height: 10px; overflow: hidden; opacity: 0'
			}, pasteBinDefaultContent);

			// Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
			if (Env.ie || Env.gecko) {
				dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) == 'rtl' ? 0xFFFF : -0xFFFF);
			}

			// Prevent focus events from bubbeling fixed FocusManager issues
			dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function(e) {
				e.stopPropagation();
			});

			pasteBinElm.focus();
			editor.selection.select(pasteBinElm, true);
		}

		/**
		 * Removes the paste bin if it exists.
		 */
		function removePasteBin() {
			if (pasteBinElm) {
				editor.dom.unbind(pasteBinElm);
				editor.dom.remove(pasteBinElm);

				if (lastRng) {
					editor.selection.setRng(lastRng);
				}
			}

			keyboardPastePlainTextState = false;
			pasteBinElm = lastRng = null;
		}

		/**
		 * Returns the contents of the paste bin as a HTML string.
		 *
		 * @return {String} Get the contents of the paste bin.
		 */
		function getPasteBinHtml() {
			return pasteBinElm ? pasteBinElm.innerHTML : pasteBinDefaultContent;
		}

		/**
		 * Gets various content types out of a datatransfer object.
		 *
		 * @param {DataTransfer} dataTransfer Event fired on paste.
		 * @return {Object} Object with mime types and data for those mime types.
		 */
		function getDataTransferItems(dataTransfer) {
			var data = {};

			if (dataTransfer && dataTransfer.types) {
				data['text/plain'] = dataTransfer.getData('Text');

				for (var i = 0; i < dataTransfer.types.length; i++) {
					var contentType = dataTransfer.types[i];
					data[contentType] = dataTransfer.getData(contentType);
				}
			}

			return data;
		}

		/**
		 * Gets various content types out of the Clipboard API. It will also get the
		 * plain text using older IE and WebKit API:s.
		 *
		 * @param {ClipboardEvent} clipboardEvent Event fired on paste.
		 * @return {Object} Object with mime types and data for those mime types.
		 */
		function getClipboardContent(clipboardEvent) {
			return getDataTransferItems(clipboardEvent.clipboardData || editor.getDoc().dataTransfer);
		}

		function getCaretRangeFromEvent(e) {
			var doc = editor.getDoc(), rng;

			if (doc.caretPositionFromPoint) {
				var point = doc.caretPositionFromPoint(e.pageX, e.pageY);
				rng = doc.createRange();
				rng.setStart(point.offsetNode, point.offset);
				rng.collapse(true);
			} else if (doc.caretRangeFromPoint) {
				rng = doc.caretRangeFromPoint(e.pageX, e.pageY);
			}

			return rng;
		}

		editor.on('keydown', function(e) {
			if (e.isDefaultPrevented()) {
				return;
			}

			// Ctrl+V or Shift+Insert
			if ((VK.metaKeyPressed(e) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) {
				keyboardPastePlainTextState = e.shiftKey && e.keyCode == 86;

				// Prevent undoManager keydown handler from making an undo level with the pastebin in it
				e.stopImmediatePropagation();

				keyboardPasteTimeStamp = new Date().getTime();

				// IE doesn't support Ctrl+Shift+V and it doesn't even produce a paste event
				// so lets fake a paste event and let IE use the execCommand/dataTransfer methods
				if (Env.ie && keyboardPastePlainTextState) {
					e.preventDefault();
					editor.fire('paste', {ieFake: true});
					return;
				}

				removePasteBin();
				createPasteBin();
			}
		});

		editor.on('paste', function(e) {
			var clipboardContent = getClipboardContent(e);
			var isKeyBoardPaste = new Date().getTime() - keyboardPasteTimeStamp < 1000;
			var plainTextMode = self.pasteFormat == "text" || keyboardPastePlainTextState;

			// Not a keyboard paste prevent default paste and try to grab the clipboard contents using different APIs
			if (!isKeyBoardPaste) {
				e.preventDefault();
			}

			// Try IE only method if paste isn't a keyboard paste
			if (Env.ie && (!isKeyBoardPaste || e.ieFake)) {
				createPasteBin();

				editor.dom.bind(pasteBinElm, 'paste', function(e) {
					e.stopPropagation();
				});

				editor.getDoc().execCommand('Paste', false, null);
				clipboardContent["text/html"] = getPasteBinHtml();
			}

			setTimeout(function() {
				var html = getPasteBinHtml();

				// WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
				if (pasteBinElm && pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
					plainTextMode = true;
				}

				removePasteBin();

				if (html == pasteBinDefaultContent || !isKeyBoardPaste) {
					html = clipboardContent['text/html'] || clipboardContent['text/plain'] || pasteBinDefaultContent;

					if (html == pasteBinDefaultContent) {
						if (!isKeyBoardPaste) {
							editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
						}

						return;
					}
				}

				if (plainTextMode) {
					pasteText(clipboardContent['text/plain'] || Utils.innerText(html));
				} else {
					pasteHtml(html);
				}
			}, 0);
		});

		editor.on('dragstart', function(e) {
			if (e.dataTransfer.types) {
				try {
					e.dataTransfer.setData('mce-internal', editor.selection.getContent());
				} catch (ex) {
					// IE 10 throws an error since it doesn't support custom data items
				}
			}
		});

		editor.on('drop', function(e) {
			var rng = getCaretRangeFromEvent(e);

			if (rng) {
				var dropContent = getDataTransferItems(e.dataTransfer);
				var content = dropContent['mce-internal'] || dropContent['text/html'] || dropContent['text/plain'];

				if (content) {
					e.preventDefault();

					editor.undoManager.transact(function() {
						if (dropContent['mce-internal']) {
							editor.execCommand('Delete');
						}

						editor.selection.setRng(rng);

						if (!dropContent['text/html']) {
							pasteText(content);
						} else {
							pasteHtml(content);
						}
					});
				}
			}
		});

		self.pasteHtml = pasteHtml;
		self.pasteText = pasteText;

		// Remove all data images from paste for example from Gecko
		// except internal images like video elements
		editor.on('preInit', function() {
			editor.parser.addNodeFilter('img', function(nodes) {
				if (!editor.settings.paste_data_images) {
					var i = nodes.length;

					while (i--) {
						var src = nodes[i].attributes.map.src;
						if (src && src.indexOf('data:image') === 0) {
							if (!nodes[i].attr('data-mce-object') && src !== Env.transparentSrc) {
								nodes[i].remove();
							}
						}
					}
				}
			});
		});

		// Fix for #6504 we need to remove the paste bin on IE if the user paste in a file
		editor.on('PreProcess', function() {
			editor.dom.remove(editor.dom.get('mcepastebin'));
		});
	};
});
<commit_msg>Fixed a but where WebKit/Blink clones elements when images are floated.<commit_after>/**
 * Clipboard.js
 *
 * Copyright, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * This class contains logic for getting HTML contents out of the clipboard.
 *
 * We need to make a lot of ugly hacks to get the contents out of the clipboard since
 * the W3C Clipboard API is broken in all browsers: Gecko/WebKit/Blink. We might rewrite
 * this the way those API:s stabilize.
 *
 * Current implementation steps:
 *  1. On keydown with paste keys Ctrl+V or Shift+Insert create
 *     a paste bin element and move focus to that element.
 *  2. Wait for the browser to fire a "paste" event and get the contents out of the paste bin.
 *  3. Check if the paste was successful if true, process the HTML.
 *  (4). If the paste was unsuccessful use IE execCommand, Clipboard API, document.dataTransfer old WebKit API etc.
 *
 * @class tinymce.pasteplugin.Clipboard
 * @private
 */
define("tinymce/pasteplugin/Clipboard", [
	"tinymce/Env",
	"tinymce/util/VK",
	"tinymce/pasteplugin/Utils"
], function(Env, VK, Utils) {
	return function(editor) {
		var self = this, pasteBinElm, lastRng, keyboardPasteTimeStamp = 0;
		var pasteBinDefaultContent = '%MCEPASTEBIN%', keyboardPastePlainTextState;

		/**
		 * Pastes the specified HTML. This means that the HTML is filtered and then
		 * inserted at the current selection in the editor. It will also fire paste events
		 * for custom user filtering.
		 *
		 * @param {String} html HTML code to paste into the current selection.
		 */
		function pasteHtml(html) {
			var args, dom = editor.dom;

			args = editor.fire('BeforePastePreProcess', {content: html}); // Internal event used by Quirks
			args = editor.fire('PastePreProcess', args);
			html = args.content;

			if (!args.isDefaultPrevented()) {
				// User has bound PastePostProcess events then we need to pass it through a DOM node
				// This is not ideal but we don't want to let the browser mess up the HTML for example
				// some browsers add   to P tags etc
				if (editor.hasEventListeners('PastePostProcess') && !args.isDefaultPrevented()) {
					// We need to attach the element to the DOM so Sizzle selectors work on the contents
					var tempBody = dom.add(editor.getBody(), 'div', {style: 'display:none'}, html);
					args = editor.fire('PastePostProcess', {node: tempBody});
					dom.remove(tempBody);
					html = args.node.innerHTML;
				}

				if (!args.isDefaultPrevented()) {
					editor.insertContent(html);
				}
			}
		}

		/**
		 * Pastes the specified text. This means that the plain text is processed
		 * and converted into BR and P elements. It will fire paste events for custom filtering.
		 *
		 * @param {String} text Text to paste as the current selection location.
		 */
		function pasteText(text) {
			text = editor.dom.encode(text).replace(/\r\n/g, '\n');

			var startBlock = editor.dom.getParent(editor.selection.getStart(), editor.dom.isBlock);

			// Create start block html for example

			var forcedRootBlockName = editor.settings.forced_root_block;
			var forcedRootBlockStartHtml;
			if (forcedRootBlockName) {
				forcedRootBlockStartHtml = editor.dom.createHTML(forcedRootBlockName, editor.settings.forced_root_block_attrs);
				forcedRootBlockStartHtml = forcedRootBlockStartHtml.substr(0, forcedRootBlockStartHtml.length - 3) + '>';
			}

			if ((startBlock && /^(PRE|DIV)$/.test(startBlock.nodeName)) || !forcedRootBlockName) {
				text = Utils.filter(text, [
					[/\n/g, "
"]
				]);
			} else {
				text = Utils.filter(text, [
					[/\n\n/g, "
" + forcedRootBlockStartHtml],
					[/^(.*<\/p>)(
)$/, forcedRootBlockStartHtml + '$1'],
					[/\n/g, "
"]
				]);

				if (text.indexOf('
') != -1) {
					text = forcedRootBlockStartHtml + text;
				}
			}

			pasteHtml(text);
		}

		/**
		 * Creates a paste bin element and moves the selection into that element. It will also move the element offscreen
		 * so that resize handles doesn't get produced on IE or Drag handles or Firefox.
		 */
		function createPasteBin() {
			var dom = editor.dom, body = editor.getBody();
			var viewport = editor.dom.getViewPort(editor.getWin()), scrollTop, top = 20;

			lastRng = editor.selection.getRng();
			scrollTop = editor.inline ? editor.selection.getScrollContainer().scrollTop : viewport.y;

			// Calculate top cordinate this is needed to avoid scrolling to top of document
			// We want the paste bin to be as close to the caret as possible to avoid scrolling
			if (lastRng.getClientRects) {
				var rects = lastRng.getClientRects();
				if (rects.length) {
					top = scrollTop + (rects[0].top - dom.getPos(body).y);
				}
			}

			// Create a pastebin
			pasteBinElm = dom.add(editor.getBody(), 'div', {
				id: "mcepastebin",
				contentEditable: true,
				"data-mce-bogus": "1",
				style: 'position: absolute; top: ' + top + 'px;' +
					'width: 10px; height: 10px; overflow: hidden; opacity: 0'
			}, pasteBinDefaultContent);

			// Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
			if (Env.ie || Env.gecko) {
				dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) == 'rtl' ? 0xFFFF : -0xFFFF);
			}

			// Prevent focus events from bubbeling fixed FocusManager issues
			dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function(e) {
				e.stopPropagation();
			});

			pasteBinElm.focus();
			editor.selection.select(pasteBinElm, true);
		}

		/**
		 * Removes the paste bin if it exists.
		 */
		function removePasteBin() {
			if (pasteBinElm) {
				var pasteBinClone;

				// WebKit/Blink might clone the div so
				// lets make sure we remove all clones
				// TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
				while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
					editor.dom.remove(pasteBinClone);
					editor.dom.unbind(pasteBinClone);
				}

				if (lastRng) {
					editor.selection.setRng(lastRng);
				}
			}

			keyboardPastePlainTextState = false;
			pasteBinElm = lastRng = null;
		}

		/**
		 * Returns the contents of the paste bin as a HTML string.
		 *
		 * @return {String} Get the contents of the paste bin.
		 */
		function getPasteBinHtml() {
			var html = pasteBinDefaultContent, pasteBinClones, i;

			// Since WebKit/Chrome might clone the paste bin when pasting
			// for example:  we need to check if any of them contains some useful html.
			// TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
			pasteBinClones = editor.dom.select('div[id=mcepastebin]');
			i = pasteBinClones.length;
			while (i--) {
				var cloneHtml = pasteBinClones[i].innerHTML;

				if (html == pasteBinDefaultContent) {
					html = '';
				}

				if (cloneHtml.length > html.length) {
					html = cloneHtml;
				}
			}

			return html;
		}

		/**
		 * Gets various content types out of a datatransfer object.
		 *
		 * @param {DataTransfer} dataTransfer Event fired on paste.
		 * @return {Object} Object with mime types and data for those mime types.
		 */
		function getDataTransferItems(dataTransfer) {
			var data = {};

			if (dataTransfer && dataTransfer.types) {
				data['text/plain'] = dataTransfer.getData('Text');

				for (var i = 0; i < dataTransfer.types.length; i++) {
					var contentType = dataTransfer.types[i];
					data[contentType] = dataTransfer.getData(contentType);
				}
			}

			return data;
		}

		/**
		 * Gets various content types out of the Clipboard API. It will also get the
		 * plain text using older IE and WebKit API:s.
		 *
		 * @param {ClipboardEvent} clipboardEvent Event fired on paste.
		 * @return {Object} Object with mime types and data for those mime types.
		 */
		function getClipboardContent(clipboardEvent) {
			return getDataTransferItems(clipboardEvent.clipboardData || editor.getDoc().dataTransfer);
		}

		function getCaretRangeFromEvent(e) {
			var doc = editor.getDoc(), rng;

			if (doc.caretPositionFromPoint) {
				var point = doc.caretPositionFromPoint(e.pageX, e.pageY);
				rng = doc.createRange();
				rng.setStart(point.offsetNode, point.offset);
				rng.collapse(true);
			} else if (doc.caretRangeFromPoint) {
				rng = doc.caretRangeFromPoint(e.pageX, e.pageY);
			}

			return rng;
		}

		editor.on('keydown', function(e) {
			if (e.isDefaultPrevented()) {
				return;
			}

			// Ctrl+V or Shift+Insert
			if ((VK.metaKeyPressed(e) && e.keyCode == 86) || (e.shiftKey && e.keyCode == 45)) {
				keyboardPastePlainTextState = e.shiftKey && e.keyCode == 86;

				// Prevent undoManager keydown handler from making an undo level with the pastebin in it
				e.stopImmediatePropagation();

				keyboardPasteTimeStamp = new Date().getTime();

				// IE doesn't support Ctrl+Shift+V and it doesn't even produce a paste event
				// so lets fake a paste event and let IE use the execCommand/dataTransfer methods
				if (Env.ie && keyboardPastePlainTextState) {
					e.preventDefault();
					editor.fire('paste', {ieFake: true});
					return;
				}

				removePasteBin();
				createPasteBin();
			}
		});

		editor.on('paste', function(e) {
			var clipboardContent = getClipboardContent(e);
			var isKeyBoardPaste = new Date().getTime() - keyboardPasteTimeStamp < 1000;
			var plainTextMode = self.pasteFormat == "text" || keyboardPastePlainTextState;

			// Not a keyboard paste prevent default paste and try to grab the clipboard contents using different APIs
			if (!isKeyBoardPaste) {
				e.preventDefault();
			}

			// Try IE only method if paste isn't a keyboard paste
			if (Env.ie && (!isKeyBoardPaste || e.ieFake)) {
				createPasteBin();

				editor.dom.bind(pasteBinElm, 'paste', function(e) {
					e.stopPropagation();
				});

				editor.getDoc().execCommand('Paste', false, null);
				clipboardContent["text/html"] = getPasteBinHtml();
			}

			setTimeout(function() {
				var html = getPasteBinHtml();

				// WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
				if (pasteBinElm && pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
					plainTextMode = true;
				}

				removePasteBin();

				if (html == pasteBinDefaultContent || !isKeyBoardPaste) {
					html = clipboardContent['text/html'] || clipboardContent['text/plain'] || pasteBinDefaultContent;

					if (html == pasteBinDefaultContent) {
						if (!isKeyBoardPaste) {
							editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
						}

						return;
					}
				}

				if (plainTextMode) {
					pasteText(clipboardContent['text/plain'] || Utils.innerText(html));
				} else {
					pasteHtml(html);
				}
			}, 0);
		});

		editor.on('dragstart', function(e) {
			if (e.dataTransfer.types) {
				try {
					e.dataTransfer.setData('mce-internal', editor.selection.getContent());
				} catch (ex) {
					// IE 10 throws an error since it doesn't support custom data items
				}
			}
		});

		editor.on('drop', function(e) {
			var rng = getCaretRangeFromEvent(e);

			if (rng) {
				var dropContent = getDataTransferItems(e.dataTransfer);
				var content = dropContent['mce-internal'] || dropContent['text/html'] || dropContent['text/plain'];

				if (content) {
					e.preventDefault();

					editor.undoManager.transact(function() {
						if (dropContent['mce-internal']) {
							editor.execCommand('Delete');
						}

						editor.selection.setRng(rng);

						if (!dropContent['text/html']) {
							pasteText(content);
						} else {
							pasteHtml(content);
						}
					});
				}
			}
		});

		self.pasteHtml = pasteHtml;
		self.pasteText = pasteText;

		// Remove all data images from paste for example from Gecko
		// except internal images like video elements
		editor.on('preInit', function() {
			editor.parser.addNodeFilter('img', function(nodes) {
				if (!editor.settings.paste_data_images) {
					var i = nodes.length;

					while (i--) {
						var src = nodes[i].attributes.map.src;
						if (src && src.indexOf('data:image') === 0) {
							if (!nodes[i].attr('data-mce-object') && src !== Env.transparentSrc) {
								nodes[i].remove();
							}
						}
					}
				}
			});
		});

		// Fix for #6504 we need to remove the paste bin on IE if the user paste in a file
		editor.on('PreProcess', function() {
			editor.dom.remove(editor.dom.get('mcepastebin'));
		});
	};
});
<|endoftext|>


<commit_before><commit_msg>Update reveal.js submodule<commit_after><|endoftext|>


<commit_before>function renderReviews() {
    var container = $('.reconciliationsToReview');
    container.empty();
    container.hide();
    $.each(entities, function(idx,entity){
        var template;
        if (entity.id === null || entity.id === undefined || entity.id === "")
            return
        if (entity.id === "None")
            template = $(".templates .reviewNewTemplate").clone();
        else
            template = $(".templates .reviewReconciledTemplate").clone();

        $(".candidateName",template).html("" + textValue(entity) + "");
        var freebaseName = null;
        if (entity['/rec_ui/freebase_name']){
            $.each(entity['/rec_ui/freebase_name'],function(idx,name){
                if (name.toLowerCase() === textValue(entity).toLowerCase())
                    freebaseName = name;
            })
            freebaseName = freebaseName || entity['/rec_ui/freebase_name'][0];
        }
        freebaseName = freebaseName || entity.id;

        $(".freebaseName", template).html(""+freebaseName+"");
        if (textValue(entity).toLowerCase() === freebaseName.toLowerCase())
            $(".freebaseName", template).addClass("identicalMatch");

        $(".internalLink", template).click(function(val) {
            $.historyLoad(entity["/rec_ui/id"]);
            $("#tabs > ul").tabs("select",0);})
        container.append(template);
    });
    container.show();
}<commit_msg>Minor performance improvement to review screen<commit_after>function renderReviews() {
    var container = $('.reconciliationsToReview').hide().empty();
    var newTemplate = $(".templates .reviewNewTemplate");
    var reconciledTemplate = $(".templates .reviewReconciledTemplate");
    $.each(entities, function(idx,entity){
        var template;
        if (entity.id === null || entity.id === undefined || entity.id === "")
            return
        if (entity.id === "None")
            template = newTemplate.clone();
        else
            template = reconciledTemplate.clone();

        $(".candidateName",template).html("" + textValue(entity) + "");
        var freebaseName = null;
        if (entity['/rec_ui/freebase_name']){
            $.each(entity['/rec_ui/freebase_name'],function(idx,name){
                if (name.toLowerCase() === textValue(entity).toLowerCase())
                    freebaseName = name;
            })
            freebaseName = freebaseName || entity['/rec_ui/freebase_name'][0];
        }
        freebaseName = freebaseName || entity.id;

        $(".freebaseName", template).html(""+freebaseName+"");
        if (textValue(entity).toLowerCase() === freebaseName.toLowerCase())
            $(".freebaseName", template).addClass("identicalMatch");

        $(".internalLink", template).click(function(val) {
            $.historyLoad(entity["/rec_ui/id"]);
            $("#tabs > ul").tabs("select",0);})
        container.append(template);
    });
    container.show();
}<|endoftext|>


<commit_before>var createKey = require('./createKey'),
    keyKey = createKey(-1);

function revive(input){
        objects = {};
        scannedObjects = [];

    function scan(input){
        if(input[keyKey]){
            objects[input[keyKey]] = input;
            delete input[keyKey];
        }

        for(var key in input){
            var value = input[key];

            if(value != null && typeof value === 'object'){
                if(scannedObjects.indexOf(value)<0){
                    scannedObjects.push(value);
                    scan(value);
                }
            }

            if(typeof value === 'string' && value.length === 1 && value.charCodeAt(0) > keyKey.charCodeAt(0)){
                input[key] = objects[value];
            }
        }
        return input;
    }

    return scan(input);
}

module.exports = revive;<commit_msg>reviver is non-destuctive<commit_after>var createKey = require('./createKey'),
    keyKey = createKey(-1);

function revive(input){
    var objects = {},
        scannedObjects = [];

    function scan(input){
        var output = input;

        if(typeof output !== 'object'){
            return output;
        }

        output = input instanceof Array ? [] : {};

        if(input[keyKey]){
            objects[input[keyKey]] = output;
        }

        for(var key in input){
            var value = input[key];

            if(key === keyKey){
                continue;
            }

            if(value != null && typeof value === 'object'){
                if(scannedObjects.indexOf(value)<0){
                    scannedObjects.push(value);
                    output[key] = scan(value);
                }
            }else if(typeof value === 'string' && value.length === 1 && value.charCodeAt(0) > keyKey.charCodeAt(0)){
                output[key] = objects[value];
            }else{
                output[key] = input[key];
            }
        }
        return output;
    }

    return scan(input);
}

module.exports = revive;<|endoftext|>


<commit_before>import { documentHelpers } from '../model'
import { isString } from '../util'

export default class BasicEditorApi {
  constructor (archive, editorSession) {
    this.archive = archive
    this.editorSession = editorSession
  }

  extendWith (apiExtension) {
    apiExtension._register(this)
  }

  getEditorSession () {
    return this.editorSession
  }

  getDocument () {
    return this.editorSession.getDocument()
  }

  getRoot () {
    return this.getDocument().root
  }

  deleteNode (nodeId) {
    this.editorSession.transaction(tx => {
      documentHelpers.deepDeleteNode(tx, nodeId)
    })
  }

  removeAndDeleteNode (nodeId) {
    const node = this.editorSession.getDocument().get(nodeId, true)
    const parent = node.getParent()
    if (parent) {
      const { property: propertyName, pos } = node.getXpath()
      const property = parent.schema.getProperty(propertyName)
      this.editorSession.transaction(tx => {
        // remove the item from its parent
        if (property.isArray()) {
          documentHelpers.removeAt(tx, [parent.id, propertyName], pos)
        } else {
          tx.set([parent.id, propertyName], null)
        }
        documentHelpers.deepDeleteNode(tx, nodeId)
        tx.setSelection(null)
      })
    }
  }

  removeItem (collectionPath, itemId) {
    this.editorSession.transaction(tx => {
      documentHelpers.removeFromCollection(tx, collectionPath, itemId)
      tx.setSelection(null)
    })
  }

  updateNode (id, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.get(id)
      node.assign(nodeData)
    })
  }

  insertAnnotation (type, nodeData) {
    this.editorSession.transaction(tx => {
      tx.annotate(Object.assign({ type }, nodeData))
    })
  }

  moveNode (nodeId, direction) {
    const doc = this.getDocument()
    const node = doc.get(nodeId)
    const parent = node.getParent()
    if (!parent) throw new Error('Node does not have parent')
    const propertyName = node.getXpath().property
    this.moveItem([parent.id, propertyName], nodeId, direction)
  }

  moveItem (collectionPath, itemId, direction) {
    const doc = this.getDocument()
    const collection = doc.get(collectionPath)
    if (!collection) throw new Error('Collection does not exist')
    const pos = collection.indexOf(itemId)
    const diff = direction === 'up' ? -1 : +1
    const insertPos = Math.min(collection.length - 1, Math.max(0, pos + diff))
    if (insertPos !== pos) {
      this.editorSession.transaction(tx => {
        documentHelpers.removeAt(tx, collectionPath, pos)
        documentHelpers.insertAt(tx, collectionPath, insertPos, itemId)
      })
    }
  }

  addNode (collectionPath, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.create(nodeData)
      documentHelpers.append(tx, collectionPath, node.id)
      this._selectItem(tx, node)
    })
  }

  insertNode (collectionPath, pos, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.create(nodeData)
      documentHelpers.insertAt(tx, collectionPath, pos, node.id)
      this._selectItem(tx, node)
    })
  }

  selectItem (item) {
    if (isString(item)) {
      item = this.getDocument().get(item)
    }
    this._selectItem(this.editorSession, item)
  }

  _selectItem (tx, node) {
    tx.setSelection({
      type: 'custom',
      customType: 'node',
      nodeId: node.id,
      data: {
        nodeType: node.type
      }
    })
  }
}
<commit_msg>Introduce api.renameAsset()<commit_after>import { documentHelpers } from '../model'
import { isString } from '../util'

export default class BasicEditorApi {
  constructor (archive, editorSession) {
    this.archive = archive
    this.editorSession = editorSession
  }

  extendWith (apiExtension) {
    apiExtension._register(this)
  }

  getEditorSession () {
    return this.editorSession
  }

  getDocument () {
    return this.editorSession.getDocument()
  }

  getRoot () {
    return this.getDocument().root
  }

  deleteNode (nodeId) {
    this.editorSession.transaction(tx => {
      documentHelpers.deepDeleteNode(tx, nodeId)
    })
  }

  removeAndDeleteNode (nodeId) {
    const node = this.editorSession.getDocument().get(nodeId, true)
    const parent = node.getParent()
    if (parent) {
      const { property: propertyName, pos } = node.getXpath()
      const property = parent.schema.getProperty(propertyName)
      this.editorSession.transaction(tx => {
        // remove the item from its parent
        if (property.isArray()) {
          documentHelpers.removeAt(tx, [parent.id, propertyName], pos)
        } else {
          tx.set([parent.id, propertyName], null)
        }
        documentHelpers.deepDeleteNode(tx, nodeId)
        tx.setSelection(null)
      })
    }
  }

  removeItem (collectionPath, itemId) {
    this.editorSession.transaction(tx => {
      documentHelpers.removeFromCollection(tx, collectionPath, itemId)
      tx.setSelection(null)
    })
  }

  updateNode (id, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.get(id)
      node.assign(nodeData)
    })
  }

  insertAnnotation (type, nodeData) {
    this.editorSession.transaction(tx => {
      tx.annotate(Object.assign({ type }, nodeData))
    })
  }

  moveNode (nodeId, direction) {
    const doc = this.getDocument()
    const node = doc.get(nodeId)
    const parent = node.getParent()
    if (!parent) throw new Error('Node does not have parent')
    const propertyName = node.getXpath().property
    this.moveItem([parent.id, propertyName], nodeId, direction)
  }

  moveItem (collectionPath, itemId, direction) {
    const doc = this.getDocument()
    const collection = doc.get(collectionPath)
    if (!collection) throw new Error('Collection does not exist')
    const pos = collection.indexOf(itemId)
    const diff = direction === 'up' ? -1 : +1
    const insertPos = Math.min(collection.length - 1, Math.max(0, pos + diff))
    if (insertPos !== pos) {
      this.editorSession.transaction(tx => {
        documentHelpers.removeAt(tx, collectionPath, pos)
        documentHelpers.insertAt(tx, collectionPath, insertPos, itemId)
      })
    }
  }

  addNode (collectionPath, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.create(nodeData)
      documentHelpers.append(tx, collectionPath, node.id)
      this._selectItem(tx, node)
    })
  }

  insertNode (collectionPath, pos, nodeData) {
    this.editorSession.transaction(tx => {
      const node = tx.create(nodeData)
      documentHelpers.insertAt(tx, collectionPath, pos, node.id)
      this._selectItem(tx, node)
    })
  }

  selectItem (item) {
    if (isString(item)) {
      item = this.getDocument().get(item)
    }
    this._selectItem(this.editorSession, item)
  }

  renameAsset (assetId, newFilename) {
    const archive = this.archive
    const asset = archive.getAssetById(assetId)
    if (asset.filename !== newFilename) {
      this.archive.renameAsset(assetId, newFilename)
    }
  }

  _selectItem (tx, node) {
    tx.setSelection({
      type: 'custom',
      customType: 'node',
      nodeId: node.id,
      data: {
        nodeType: node.type
      }
    })
  }
}
<|endoftext|>


<commit_before>// @flow
import md5 from 'js-md5';

import { toState } from '../redux';

import {
    DEFAULT_AVATAR_RELATIVE_PATH,
    LOCAL_PARTICIPANT_DEFAULT_ID
} from './constants';

declare var config: Object;
declare var interfaceConfig: Object;

/**
 * Returns the URL of the image for the avatar of a specific participant.
 *
 * @param {Participant} [participant] - The participant to return the avatar URL
 * of.
 * @param {string} [participant.avatarID] - Participant's avatar ID.
 * @param {string} [participant.avatarURL] - Participant's avatar URL.
 * @param {string} [participant.email] - Participant's e-mail address.
 * @param {string} [participant.id] - Participant's ID.
 * @public
 * @returns {string} The URL of the image for the avatar of the specified
 * participant.
 */
export function getAvatarURL({ avatarID, avatarURL, email, id, name }: {
        avatarID: string,
        avatarURL: string,
        email: string,
        id: string,
        name: string
}) {
    // If disableThirdPartyRequests disables third-party avatar services, we are
    // restricted to a stock image of ours.
    if (typeof config === 'object' && config.disableThirdPartyRequests) {
        return DEFAULT_AVATAR_RELATIVE_PATH;
    }

    // If an avatarURL is specified, then obviously there's nothing to generate.
    if (avatarURL) {
        return avatarURL;
    }

    let key = email || avatarID;
    let urlPrefix;
    let urlSuffix;

    // If the ID looks like an e-mail address, we'll use Gravatar because it
    // supports e-mail addresses.
    if (key && key.indexOf('@') > 0) {
        urlPrefix = 'https://www.gravatar.com/avatar/';
        urlSuffix = '?d=wavatar&size=200';
    } else {
        // Otherwise, we do not have much a choice but a random avatar (fetched
        // from a configured avatar service).
        if (!key) {
            key = id;
            if (!key) {
                return undefined;
            }
        }

        // The deployment is allowed to choose the avatar service which is to
        // generate the random avatars.
        urlPrefix
            = typeof interfaceConfig === 'object'
                && interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
        if (urlPrefix) {
            urlSuffix = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
        } else {
            urlPrefix = 'https://avatar-cdn.jitsi.net/';
            urlSuffix = `/${_getInitials(name) || ' '}/200/avatar.png`;
        }
    }

    return urlPrefix + md5.hex(key.trim().toLowerCase()) + urlSuffix;
}

/**
 * Returns the avatarURL for the participant associated with the passed in
 * participant ID.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @param {string} id - The ID of the participant to retrieve.
 * @param {boolean} isLocal - An optional parameter indicating whether or not
 * the partcipant id is for the local user. If true, a different logic flow is
 * used find the local user, ignoring the id value as it can change through the
 * beginning and end of a call.
 * @returns {(string|undefined)}
 */
export function getAvatarURLByParticipantId(
        stateful: Object | Function,
        id: string = LOCAL_PARTICIPANT_DEFAULT_ID) {
    const participant = getParticipantById(stateful, id);

    return participant && getAvatarURL(participant);
}

/**
 * Returns local participant from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {(Participant|undefined)}
 */
export function getLocalParticipant(stateful: Object | Function) {
    const participants = _getAllParticipants(stateful);

    return participants.find(p => p.local);
}

/**
 * Returns participant by ID from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @param {string} id - The ID of the participant to retrieve.
 * @private
 * @returns {(Participant|undefined)}
 */
export function getParticipantById(
        stateful: Object | Function,
        id: string) {
    const participants = _getAllParticipants(stateful);

    return participants.find(p => p.id === id);
}

/**
 * Returns a count of the known participants in the passed in redux state,
 * excluding any fake participants.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {number}
 */
export function getParticipantCount(stateful: Object | Function) {
    return getParticipants(stateful).length;
}

/**
 * Returns participant's display name.
 * FIXME: remove the hardcoded strings once interfaceConfig is stored in redux
 * and merge with a similarly named method in conference.js.
 *
 * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state.
 * @param {string} id - The ID of the participant's display name to retrieve.
 * @private
 * @returns {string}
 */
export function getParticipantDisplayName(
        stateful: Object | Function, id: string) {
    const participant = getParticipantById(stateful, id);

    if (participant) {
        if (participant.name) {
            return participant.name;
        }

        if (participant.local) {
            return typeof interfaceConfig === 'object'
                ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME
                : 'me';
        }
    }

    return typeof interfaceConfig === 'object'
        ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
        : 'Fellow Jitster';
}

/**
 * Selectors for getting all known participants with fake participants filtered
 * out.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {Participant[]}
 */
export function getParticipants(stateful: Object | Function) {
    return _getAllParticipants(stateful).filter(p => !p.isBot);
}

/**
 * Returns the participant which has its pinned state set to truthy.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {(Participant|undefined)}
 */
export function getPinnedParticipant(stateful: Object | Function) {
    return _getAllParticipants(stateful).find(p => p.pinned);
}

/**
 * Returns array of participants from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @private
 * @returns {Participant[]}
 */
function _getAllParticipants(stateful) {
    return (
        Array.isArray(stateful)
            ? stateful
            : toState(stateful)['features/base/participants'] || []);
}

/**
 * Gets the initials from a name, assuming a westernized name.
 *
 * @param {string} name - The name from which to parse initials.
 * @private
 * @returns {string}
 */
function _getInitials(name) {
    if (!name) {
        return '';
    }

    const nameParts = name.toUpperCase().split(' ');
    const firstName = nameParts[0];
    let initials = firstName[0];

    if (nameParts.length > 1) {
        initials += nameParts[nameParts.length - 1];
    }

    return initials;
}
<commit_msg>Revert "feat(avatars): use initials service for getting images (#2312)"<commit_after>// @flow
import md5 from 'js-md5';

import { toState } from '../redux';

import {
    DEFAULT_AVATAR_RELATIVE_PATH,
    LOCAL_PARTICIPANT_DEFAULT_ID
} from './constants';

declare var config: Object;
declare var interfaceConfig: Object;

/**
 * Returns the URL of the image for the avatar of a specific participant.
 *
 * @param {Participant} [participant] - The participant to return the avatar URL
 * of.
 * @param {string} [participant.avatarID] - Participant's avatar ID.
 * @param {string} [participant.avatarURL] - Participant's avatar URL.
 * @param {string} [participant.email] - Participant's e-mail address.
 * @param {string} [participant.id] - Participant's ID.
 * @public
 * @returns {string} The URL of the image for the avatar of the specified
 * participant.
 */
export function getAvatarURL({ avatarID, avatarURL, email, id }: {
        avatarID: string,
        avatarURL: string,
        email: string,
        id: string
}) {
    // If disableThirdPartyRequests disables third-party avatar services, we are
    // restricted to a stock image of ours.
    if (typeof config === 'object' && config.disableThirdPartyRequests) {
        return DEFAULT_AVATAR_RELATIVE_PATH;
    }

    // If an avatarURL is specified, then obviously there's nothing to generate.
    if (avatarURL) {
        return avatarURL;
    }

    let key = email || avatarID;
    let urlPrefix;
    let urlSuffix;

    // If the ID looks like an e-mail address, we'll use Gravatar because it
    // supports e-mail addresses.
    if (key && key.indexOf('@') > 0) {
        urlPrefix = 'https://www.gravatar.com/avatar/';
        urlSuffix = '?d=wavatar&size=200';
    } else {
        // Otherwise, we do not have much a choice but a random avatar (fetched
        // from a configured avatar service).
        if (!key) {
            key = id;
            if (!key) {
                return undefined;
            }
        }

        // The deployment is allowed to choose the avatar service which is to
        // generate the random avatars.
        urlPrefix
            = typeof interfaceConfig === 'object'
                && interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
        if (urlPrefix) {
            urlSuffix = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
        } else {
            // Otherwise, use a default (meeples, of course).
            urlPrefix = 'https://abotars.jitsi.net/meeple/';
            urlSuffix = '';
        }
    }

    return urlPrefix + md5.hex(key.trim().toLowerCase()) + urlSuffix;
}

/**
 * Returns the avatarURL for the participant associated with the passed in
 * participant ID.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @param {string} id - The ID of the participant to retrieve.
 * @param {boolean} isLocal - An optional parameter indicating whether or not
 * the partcipant id is for the local user. If true, a different logic flow is
 * used find the local user, ignoring the id value as it can change through the
 * beginning and end of a call.
 * @returns {(string|undefined)}
 */
export function getAvatarURLByParticipantId(
        stateful: Object | Function,
        id: string = LOCAL_PARTICIPANT_DEFAULT_ID) {
    const participant = getParticipantById(stateful, id);

    return participant && getAvatarURL(participant);
}

/**
 * Returns local participant from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {(Participant|undefined)}
 */
export function getLocalParticipant(stateful: Object | Function) {
    const participants = _getAllParticipants(stateful);

    return participants.find(p => p.local);
}

/**
 * Returns participant by ID from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @param {string} id - The ID of the participant to retrieve.
 * @private
 * @returns {(Participant|undefined)}
 */
export function getParticipantById(
        stateful: Object | Function,
        id: string) {
    const participants = _getAllParticipants(stateful);

    return participants.find(p => p.id === id);
}

/**
 * Returns a count of the known participants in the passed in redux state,
 * excluding any fake participants.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {number}
 */
export function getParticipantCount(stateful: Object | Function) {
    return getParticipants(stateful).length;
}

/**
 * Returns participant's display name.
 * FIXME: remove the hardcoded strings once interfaceConfig is stored in redux
 * and merge with a similarly named method in conference.js.
 *
 * @param {(Function|Object)} stateful - The (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state.
 * @param {string} id - The ID of the participant's display name to retrieve.
 * @private
 * @returns {string}
 */
export function getParticipantDisplayName(
        stateful: Object | Function, id: string) {
    const participant = getParticipantById(stateful, id);

    if (participant) {
        if (participant.name) {
            return participant.name;
        }

        if (participant.local) {
            return typeof interfaceConfig === 'object'
                ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME
                : 'me';
        }
    }

    return typeof interfaceConfig === 'object'
        ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
        : 'Fellow Jitster';
}

/**
 * Selectors for getting all known participants with fake participants filtered
 * out.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {Participant[]}
 */
export function getParticipants(stateful: Object | Function) {
    return _getAllParticipants(stateful).filter(p => !p.isBot);
}

/**
 * Returns the participant which has its pinned state set to truthy.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @returns {(Participant|undefined)}
 */
export function getPinnedParticipant(stateful: Object | Function) {
    return _getAllParticipants(stateful).find(p => p.pinned);
}

/**
 * Returns array of participants from Redux state.
 *
 * @param {(Function|Object|Participant[])} stateful - The redux state
 * features/base/participants, the (whole) redux state, or redux's
 * {@code getState} function to be used to retrieve the state
 * features/base/participants.
 * @private
 * @returns {Participant[]}
 */
function _getAllParticipants(stateful) {
    return (
        Array.isArray(stateful)
            ? stateful
            : toState(stateful)['features/base/participants'] || []);
}
<|endoftext|>


<commit_before>/* eslint-env node */
'use strict';

module.exports = {
  allowedVersions: {
    'ember-getowner-polyfill': '^1.0.0 || ^2.0.0',
    'ember-inflector': '^1.0.0 || ^2.0.0',
    'ember-weakmap': '^2.0.0 || ^3.1.1', // workaround for dependency conflict between ember-moment and liquid-tether
    'ember-hash-helper-polyfill': '^0.1.2 || ^0.2.0' // workaround for dep. conflict ember-tooltips/liquid-fire
  }
};
<commit_msg>rm exception for ember-weakmap from dependency linter config.<commit_after>/* eslint-env node */
'use strict';

module.exports = {
  allowedVersions: {
    'ember-getowner-polyfill': '^1.0.0 || ^2.0.0',
    'ember-inflector': '^1.0.0 || ^2.0.0',
    'ember-hash-helper-polyfill': '^0.1.2 || ^0.2.0' // workaround for dep. conflict ember-tooltips/liquid-fire
  }
};
<|endoftext|>