<commit_before>// Copyright © 2014 Intel Corporation. All rights reserved.
// Use  of this  source  code is  governed by  an Apache v2
// license that can be found in the LICENSE-APACHE-V2 file.

var FS = require("fs");
var Path = require("path");

var MemoryStream = require("memorystream");
var ShellJS = require("shelljs");

var IndexParser = require("./IndexParser");

/**
 * Callback signature for {@link Download01Org.fetchVersions}
 * @param {String[]} versions Array of available Crosswalk versions sorted oldest - newest
 * @param {String} errormsg Message in case of Error
 * @inner
 * @memberOf Download01Org
 */
function fetchVersionsFinishedCb(versions, errormsg) {}

/**
 * Callback signature for {@link Download01Org.download}.
 * @param {String} path Path to downloaded file, or null on error
 * @param {String} errormsg null if success, otherwise error message
 * @inner
 * @memberOf Download01Org
 */
function downloadFinishedCb(path, errormsg) {}

/**
 * Crosswalk release lookup and download.
 * @constructor
 * @param {Application} application application instance
 * @param {String} platform Crosswalk release platform android/windows
 * @param {String} channel Crosswalk channel beta/canary/stable
 * @throws {Error} If no valid channel was specified.
 */
function Download01Org(application, platform, channel) {

    this._application = application;

    if (Download01Org.PLATFORMS.indexOf(platform) == -1) {
        throw new Error("Unknown platform " + platform);
    }
    this._platform = platform;

    if (Download01Org.CHANNELS.indexOf(channel) == -1) {
        throw new Error("Unknown channel " + channel);
    }
    this._channel = channel;

    this._flavor = "crosswalk";

    this._androidWordSize = "32";
}

/**
 * Base download URL on https://download.01.org .
 * @member {String[]} BASE_URL
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "BASE_URL", {
                      get: function() {
                                return "https://download.01.org/crosswalk/releases/";
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Read-only array of valid release channels (stable, beta, canary).
 * @member {String[]} CHANNELS
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "CHANNELS", {
                      get: function() {
                                return ["stable", "beta", "canary"];
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Read-only array of valid release platforms (android, windows).
 * @member {String[]} PLATFORMS
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "PLATFORMS", {
                      get: function() {
                                return ["android", "windows"];
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Android type crosswalk or crosswalk-lite.
 * @member {String[]} androidFlavor
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org.prototype, "androidFlavor", {
                      get: function() {
                                return this._flavor;
                           },
                      set: function(flavor) {
                                if (this._platform === "android" &&
                                    ["crosswalk", "crosswalk-lite"].indexOf(flavor) > -1) {
                                    this._flavor = flavor;
                                } else {
                                    this._application.output.error("Invalid flavor '" + flavor + "' " +
                                                                   "for platform " + this._platform);
                                }
                           }
                      });

/**
 * Android 32bit or 64bit.
 * @member {String[]} androidWordSize
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org.prototype, "androidWordSize", {
                      get: function() {
                                return this._androidWordSize;
                           },
                      set: function(androidWordSize) {
                                if (androidWordSize == 32 ||
                                    androidWordSize == 64) {
                                    this._androidWordSize = androidWordSize;
                                } else {
                                    this._application.output.error("Invalid word size (32/64) " + androidWordSize);
                                }
                           }
                      });

/**
 * Fetch available Crosswalk versions index.
 * @param {Download01Org~fetchVersionsFinishedCb} callback callback function
 */
Download01Org.prototype.fetchVersions =
function(callback) {

    // Namespace util
    var util = this._application.util;
    var output = this._application.output;
    var url = Download01Org.BASE_URL +
              this._flavor + "/" +
              this._platform + "/" +
              this._channel + "/";

    // Download
    var stream = new MemoryStream();
    var buffer = "";
    stream.on("data", function(data) {
        buffer += data.toString();
    });

    // Namespace util
    var downloader = new util.Downloader(url, stream);

    var label = "Fetching '" + this._channel + "' versions index";
    var indicator = output.createFiniteProgress(label);
    downloader.progress = function(progress) {
        indicator.update(progress);
    };

    downloader.get(function(errormsg) {

        indicator.done("");

        if (errormsg) {

            callback(null, errormsg);

        } else {

            // Parse
            var parser = new IndexParser(buffer);
            var versions = parser.parse();
            callback(versions);
        }
    });
};

/**
 * Build release filename, depending on 32 or 64bit release.
 * @param {String} version Version string a.b.c.d
 * @returns {String} Crosswalk zip filename.
 */
Download01Org.prototype.buildFilename =
function(version) {

    var filename;

    if (this._androidWordSize == 64) {
        filename = "crosswalk-" + version + "-64bit.zip";
    } else {
        filename = "crosswalk-" + version + ".zip";
    }

    return filename;
};

/**
 * Locate Crosswalk distribution zip.
 * @param {String} version Crosswalk version to look for
 * @returns {String} Relative path to zip file.
 */
Download01Org.prototype.findLocally =
function(version) {

    var filename = this.buildFilename(version);
    if (ShellJS.test("-f", filename))  {
        return filename;
    } else if (ShellJS.test("-f", "../" + filename)) {
        // Also try parent dir.
        // This is especially useful for tests that run in a temporary dir.
        return "../" + filename;
    }

    return null;
};

/**
 * Download crosswalk zip, checks for already existing file, and returns it in case.
 * @param {String} version Crosswalk version string
 * @param {String} defaultPath Directory to download to if not already exists
 * @param {Download01Org~downloadFinishedCb} callback callback function
 * @throws {FileCreationFailed} If download file could not be written.
 */
Download01Org.prototype.download =
function(version, defaultPath, callback) {

    // Namespaces
    var exceptions = this._application.exceptions;
    var util = this._application.util;

    var output = this._application.output;
    var filename = this.buildFilename(version);
    var url = Download01Org.BASE_URL +
              this._flavor + "/" +
              this._platform + "/" +
              this._channel + "/" +
              version + "/" +
              filename;

    // Check for existing download in defaultPath, parent dir, and cache dir if set
    var handler = new util.DownloadHandler(defaultPath, filename);
    var localDirs = [defaultPath, ""];
    if (process.env.CROSSWALK_APP_TOOLS_CACHE_DIR)
        localDirs.push(process.env.CROSSWALK_APP_TOOLS_CACHE_DIR);
    var localPath = handler.findLocally(localDirs);
    if (localPath) {
        output.info("Using cached", localPath);
        callback(localPath);
        return;
    }

    // Download
    var label = "Downloading '" + this._channel + "' " + version;
    var indicator = output.createFiniteProgress(label);

    var stream = handler.createStream();
    var downloader = new util.Downloader(url, stream);
    downloader.progress = function(progress) {
        indicator.update(progress);
    };
    downloader.get(function(errormsg) {

        indicator.done("");

        if (errormsg) {

            callback(null, errormsg);

        } else {

            var finishedPath = handler.finish(process.env.CROSSWALK_APP_TOOLS_CACHE_DIR);
            callback(finishedPath);
        }
    });
};

/**
 * Find a specific version in a specific channel.
 * @param {String} version Version to look for, pick lastest if null is given
 * @param {String} channel Release channel to seach in, null for all channels
 * @param {Function} callback Callback (version, channel, errormsg)
 */
Download01Org.prototype.findCrosswalkVersion =
function(version, channel, callback) {

    var output = this._application.output;

    var versionName = version ?
                        version :
                        "latest version";

    // Start with first channel if not given.
    if (!channel) {
        channel = Download01Org.CHANNELS[0];
    }

    output.info("Looking for " + versionName + " in " + this._flavor + "/" + channel);

    var deps = new Download01Org(this._application, this._platform, channel);
    if (this.androidFlavor === "crosswalk-lite")
        deps.androidFlavor = "crosswalk-lite";
    deps.androidWordSize = this.androidWordSize;
    deps.fetchVersions(function(versions, errormsg) {

        if (errormsg) {
            // This is just an internal error.
            // Try all channels as per below, if an appropriate version
            // is not found, an error message will be created accordingly.
            output.verbose(errormsg);
        }

        // Look for specific version?
        if (version &&
            versions &&
            versions.indexOf(version) > -1) {

            callback(version, channel, null);
            return;

        } else if (version || !versions) {

            // Try next channel.
            var channelIndex = Download01Org.CHANNELS.indexOf(channel);
            if (channelIndex < Download01Org.CHANNELS.length - 1) {
                output.info("Version " + version + " not found in '" + channel + "', trying next channel");
                channelIndex++;
                channel = Download01Org.CHANNELS[channelIndex];
                this.findCrosswalkVersion(version, channel, callback);
            } else {
                // Already at last channel, version not found
                output.info("Version " + version + " not found in '" + channel + "', search failed");
                callback(null, null, "Version " + version + " seems not to be available on the server");
                return;
            }
        } else {
            // Use latest from current channel.
            version = IndexParser.pickLatest(versions, function (errmsg) {
                errormsg = errmsg;
            });
            callback(version, channel, errormsg);
            return;
        }
    }.bind(this));
};

/**
 * Look up and download correct crosswalk release.
 * @param {String} versionSpec Crosswalk version or channel (stable, beta, canary)
 * @param {Function} importCrosswalkFromDisk(path) function to extract downloaded release
 * @param {Function} callback Callback(version, errormsg)
 */
Download01Org.prototype.importCrosswalk =
function(versionSpec, importCrosswalkFromDisk, callback) {

    var output = this._application.output;

    var channel = "stable";
    var version = null;

    if (ShellJS.test("-e", versionSpec)) {

        // versionSpec is a filename, import directly
        var filename = Path.normalize(Path.resolve(versionSpec));
        output.info("Using", versionSpec);
        errormsg = null;
        var importedVersion = importCrosswalkFromDisk(filename);
        if (!importedVersion) {
            errormsg = "Failed to import from " + filename;
        }
        callback(importedVersion, errormsg);
        return;

    } else if (Download01Org.CHANNELS.indexOf(versionSpec) > -1) {
        // versionSpec is a channel name
        channel = versionSpec;
    } else {
        version = versionSpec;
    }

    // Download
    var deps = new Download01Org(this._application, this._platform, channel);
    if (this.androidFlavor === "crosswalk-lite")
        deps.androidFlavor = "crosswalk-lite";
    deps.androidWordSize = this.androidWordSize;
    deps.findCrosswalkVersion(version, channel,
                              function(version, channel, errormsg) {

        if (errormsg) {
            callback(null, errormsg);
            return;
        }

        output.info("Found version '" + version + "' in channel '" + channel + "'");

        // Download latest Crosswalk
        var deps = new Download01Org(this._application, this._platform, channel);
        if (this.androidFlavor === "crosswalk-lite")
            deps.androidFlavor = "crosswalk-lite";
        deps.androidWordSize = this.androidWordSize;
        deps.download(version, ".",
                      function(filename, errormsg) {

            if (errormsg) {
                callback(null, errormsg);
                return;
            }

            if (!filename) {
                callback(null, "Failed to download Crosswalk");
                return;
            }

            errormsg = null;
            var importedVersion = importCrosswalkFromDisk(filename);
            if (!importedVersion) {
                errormsg = "Failed to extract " + filename;
            }
            callback(importedVersion, errormsg);

        }.bind(this));
    }.bind(this));
};

module.exports = Download01Org;
<commit_msg>Windows: Fix download after release filename change<commit_after>// Copyright © 2014 Intel Corporation. All rights reserved.
// Use  of this  source  code is  governed by  an Apache v2
// license that can be found in the LICENSE-APACHE-V2 file.

var FS = require("fs");
var Path = require("path");

var MemoryStream = require("memorystream");
var ShellJS = require("shelljs");

var IndexParser = require("./IndexParser");

/**
 * Callback signature for {@link Download01Org.fetchVersions}
 * @param {String[]} versions Array of available Crosswalk versions sorted oldest - newest
 * @param {String} errormsg Message in case of Error
 * @inner
 * @memberOf Download01Org
 */
function fetchVersionsFinishedCb(versions, errormsg) {}

/**
 * Callback signature for {@link Download01Org.download}.
 * @param {String} path Path to downloaded file, or null on error
 * @param {String} errormsg null if success, otherwise error message
 * @inner
 * @memberOf Download01Org
 */
function downloadFinishedCb(path, errormsg) {}

/**
 * Crosswalk release lookup and download.
 * @constructor
 * @param {Application} application application instance
 * @param {String} platform Crosswalk release platform android/windows
 * @param {String} channel Crosswalk channel beta/canary/stable
 * @throws {Error} If no valid channel was specified.
 */
function Download01Org(application, platform, channel) {

    this._application = application;

    if (Download01Org.PLATFORMS.indexOf(platform) == -1) {
        throw new Error("Unknown platform " + platform);
    }
    this._platform = platform;

    if (Download01Org.CHANNELS.indexOf(channel) == -1) {
        throw new Error("Unknown channel " + channel);
    }
    this._channel = channel;

    this._flavor = "crosswalk";

    this._androidWordSize = "32";
}

/**
 * Base download URL on https://download.01.org .
 * @member {String[]} BASE_URL
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "BASE_URL", {
                      get: function() {
                                return "https://download.01.org/crosswalk/releases/";
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Read-only array of valid release channels (stable, beta, canary).
 * @member {String[]} CHANNELS
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "CHANNELS", {
                      get: function() {
                                return ["stable", "beta", "canary"];
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Read-only array of valid release platforms (android, windows).
 * @member {String[]} PLATFORMS
 * @static
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org, "PLATFORMS", {
                      get: function() {
                                return ["android", "windows"];
                           },
                      set: function(config) {
                                // Empty because read-only
                           }
                      });

/**
 * Android type crosswalk or crosswalk-lite.
 * @member {String[]} androidFlavor
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org.prototype, "androidFlavor", {
                      get: function() {
                                return this._flavor;
                           },
                      set: function(flavor) {
                                if (this._platform === "android" &&
                                    ["crosswalk", "crosswalk-lite"].indexOf(flavor) > -1) {
                                    this._flavor = flavor;
                                } else {
                                    this._application.output.error("Invalid flavor '" + flavor + "' " +
                                                                   "for platform " + this._platform);
                                }
                           }
                      });

/**
 * Android 32bit or 64bit.
 * @member {String[]} androidWordSize
 * @memberOf Download01Org
 */
Object.defineProperty(Download01Org.prototype, "androidWordSize", {
                      get: function() {
                                return this._androidWordSize;
                           },
                      set: function(androidWordSize) {
                                if (androidWordSize == 32 ||
                                    androidWordSize == 64) {
                                    this._androidWordSize = androidWordSize;
                                } else {
                                    this._application.output.error("Invalid word size (32/64) " + androidWordSize);
                                }
                           }
                      });

/**
 * Fetch available Crosswalk versions index.
 * @param {Download01Org~fetchVersionsFinishedCb} callback callback function
 */
Download01Org.prototype.fetchVersions =
function(callback) {

    // Namespace util
    var util = this._application.util;
    var output = this._application.output;
    var url = Download01Org.BASE_URL +
              this._flavor + "/" +
              this._platform + "/" +
              this._channel + "/";

    // Download
    var stream = new MemoryStream();
    var buffer = "";
    stream.on("data", function(data) {
        buffer += data.toString();
    });

    // Namespace util
    var downloader = new util.Downloader(url, stream);

    var label = "Fetching '" + this._channel + "' versions index";
    var indicator = output.createFiniteProgress(label);
    downloader.progress = function(progress) {
        indicator.update(progress);
    };

    downloader.get(function(errormsg) {

        indicator.done("");

        if (errormsg) {

            callback(null, errormsg);

        } else {

            // Parse
            var parser = new IndexParser(buffer);
            var versions = parser.parse();
            callback(versions);
        }
    });
};

/**
 * Build release filename, depending on 32 or 64bit release.
 * @param {String} version Version string a.b.c.d
 * @returns {String} Crosswalk zip filename.
 */
Download01Org.prototype.buildFilename =
function(version) {

    var filename;

    if (this._platform === "windows") {
        filename = "crosswalk64-" + version + ".zip";
    } else if (this._androidWordSize == 64) {
        filename = "crosswalk-" + version + "-64bit.zip";
    } else {
        filename = "crosswalk-" + version + ".zip";
    }

    return filename;
};

/**
 * Locate Crosswalk distribution zip.
 * @param {String} version Crosswalk version to look for
 * @returns {String} Relative path to zip file.
 */
Download01Org.prototype.findLocally =
function(version) {

    var filename = this.buildFilename(version);
    if (ShellJS.test("-f", filename))  {
        return filename;
    } else if (ShellJS.test("-f", "../" + filename)) {
        // Also try parent dir.
        // This is especially useful for tests that run in a temporary dir.
        return "../" + filename;
    }

    return null;
};

/**
 * Download crosswalk zip, checks for already existing file, and returns it in case.
 * @param {String} version Crosswalk version string
 * @param {String} defaultPath Directory to download to if not already exists
 * @param {Download01Org~downloadFinishedCb} callback callback function
 * @throws {FileCreationFailed} If download file could not be written.
 */
Download01Org.prototype.download =
function(version, defaultPath, callback) {

    // Namespaces
    var exceptions = this._application.exceptions;
    var util = this._application.util;

    var output = this._application.output;
    var filename = this.buildFilename(version);
    var url = Download01Org.BASE_URL +
              this._flavor + "/" +
              this._platform + "/" +
              this._channel + "/" +
              version + "/" +
              filename;

    // Check for existing download in defaultPath, parent dir, and cache dir if set
    var handler = new util.DownloadHandler(defaultPath, filename);
    var localDirs = [defaultPath, ""];
    if (process.env.CROSSWALK_APP_TOOLS_CACHE_DIR)
        localDirs.push(process.env.CROSSWALK_APP_TOOLS_CACHE_DIR);
    var localPath = handler.findLocally(localDirs);
    if (localPath) {
        output.info("Using cached", localPath);
        callback(localPath);
        return;
    }

    // Download
    var label = "Downloading '" + this._channel + "' " + version;
    var indicator = output.createFiniteProgress(label);

    var stream = handler.createStream();
    var downloader = new util.Downloader(url, stream);
    downloader.progress = function(progress) {
        indicator.update(progress);
    };
    downloader.get(function(errormsg) {

        indicator.done("");

        if (errormsg) {

            callback(null, errormsg);

        } else {

            var finishedPath = handler.finish(process.env.CROSSWALK_APP_TOOLS_CACHE_DIR);
            callback(finishedPath);
        }
    });
};

/**
 * Find a specific version in a specific channel.
 * @param {String} version Version to look for, pick lastest if null is given
 * @param {String} channel Release channel to seach in, null for all channels
 * @param {Function} callback Callback (version, channel, errormsg)
 */
Download01Org.prototype.findCrosswalkVersion =
function(version, channel, callback) {

    var output = this._application.output;

    var versionName = version ?
                        version :
                        "latest version";

    // Start with first channel if not given.
    if (!channel) {
        channel = Download01Org.CHANNELS[0];
    }

    output.info("Looking for " + versionName + " in " + this._flavor + "/" + channel);

    var deps = new Download01Org(this._application, this._platform, channel);
    if (this.androidFlavor === "crosswalk-lite")
        deps.androidFlavor = "crosswalk-lite";
    deps.androidWordSize = this.androidWordSize;
    deps.fetchVersions(function(versions, errormsg) {

        if (errormsg) {
            // This is just an internal error.
            // Try all channels as per below, if an appropriate version
            // is not found, an error message will be created accordingly.
            output.verbose(errormsg);
        }

        // Look for specific version?
        if (version &&
            versions &&
            versions.indexOf(version) > -1) {

            callback(version, channel, null);
            return;

        } else if (version || !versions) {

            // Try next channel.
            var channelIndex = Download01Org.CHANNELS.indexOf(channel);
            if (channelIndex < Download01Org.CHANNELS.length - 1) {
                output.info("Version " + version + " not found in '" + channel + "', trying next channel");
                channelIndex++;
                channel = Download01Org.CHANNELS[channelIndex];
                this.findCrosswalkVersion(version, channel, callback);
            } else {
                // Already at last channel, version not found
                output.info("Version " + version + " not found in '" + channel + "', search failed");
                callback(null, null, "Version " + version + " seems not to be available on the server");
                return;
            }
        } else {
            // Use latest from current channel.
            version = IndexParser.pickLatest(versions, function (errmsg) {
                errormsg = errmsg;
            });
            callback(version, channel, errormsg);
            return;
        }
    }.bind(this));
};

/**
 * Look up and download correct crosswalk release.
 * @param {String} versionSpec Crosswalk version or channel (stable, beta, canary)
 * @param {Function} importCrosswalkFromDisk(path) function to extract downloaded release
 * @param {Function} callback Callback(version, errormsg)
 */
Download01Org.prototype.importCrosswalk =
function(versionSpec, importCrosswalkFromDisk, callback) {

    var output = this._application.output;

    var channel = "stable";
    var version = null;

    if (ShellJS.test("-e", versionSpec)) {

        // versionSpec is a filename, import directly
        var filename = Path.normalize(Path.resolve(versionSpec));
        output.info("Using", versionSpec);
        errormsg = null;
        var importedVersion = importCrosswalkFromDisk(filename);
        if (!importedVersion) {
            errormsg = "Failed to import from " + filename;
        }
        callback(importedVersion, errormsg);
        return;

    } else if (Download01Org.CHANNELS.indexOf(versionSpec) > -1) {
        // versionSpec is a channel name
        channel = versionSpec;
    } else {
        version = versionSpec;
    }

    // Download
    var deps = new Download01Org(this._application, this._platform, channel);
    if (this.androidFlavor === "crosswalk-lite")
        deps.androidFlavor = "crosswalk-lite";
    deps.androidWordSize = this.androidWordSize;
    deps.findCrosswalkVersion(version, channel,
                              function(version, channel, errormsg) {

        if (errormsg) {
            callback(null, errormsg);
            return;
        }

        output.info("Found version '" + version + "' in channel '" + channel + "'");

        // Download latest Crosswalk
        var deps = new Download01Org(this._application, this._platform, channel);
        if (this.androidFlavor === "crosswalk-lite")
            deps.androidFlavor = "crosswalk-lite";
        deps.androidWordSize = this.androidWordSize;
        deps.download(version, ".",
                      function(filename, errormsg) {

            if (errormsg) {
                callback(null, errormsg);
                return;
            }

            if (!filename) {
                callback(null, "Failed to download Crosswalk");
                return;
            }

            errormsg = null;
            var importedVersion = importCrosswalkFromDisk(filename);
            if (!importedVersion) {
                errormsg = "Failed to extract " + filename;
            }
            callback(importedVersion, errormsg);

        }.bind(this));
    }.bind(this));
};

module.exports = Download01Org;
<|endoftext|>


<commit_before>
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const embed = require('./embed.js');
const Derpibooru = require('node-derpi');

/**
 * Handles an incoming Derpibooru-related command.
 *
 * @param {object} options - The options to pass to Derpibooru
 * @param {string} [options.sortFormat] - The sort format to pull a result from
 * @param {string} [options.sortOrder] - The order to sort the results in before retrieving one
 * @param {object} client - The Discord.js client object (this.client in command context)
 * @param {object} msg - The Discord.js message object (first param in async run)
 * @param {object} args - The arguments to the command (second param in async run)
 */
exports.handleDerpiCommand = async (options, client, msg, args) => {
	const requestId = `${msg.channel.id}:${msg.id} -`;

	console.info(`${requestId} Received.`);

	if (msg.channel.type === 'text' && msg.guild.settings.get(`blockedUsers.${msg.author.id}`)) {
		console.info(`${requestId} Blocked by adminstrator.`);
		return;
	}

	console.debug(`${requestId} Sending typing notification...`);
	msg.channel.startTyping();

	options.query = `${args.query}`;

	if (client.config.derpibooru.blockedTags) {
		if (options.query) options.query += ', ';
		options.query += `-(${client.config.derpibooru.blockedTags.join(' || ')})`;
	}

	let nsfw = false;

	// Only NSFW channels can have explicit content
	// (Assumes DMs are fine)
	if (msg.channel.type === 'dm') {
		options.filterID = client.settings.get(`filter.${msg.author.id}`, client.config.derpibooru.filters.nsfw);
		console.debug(`${requestId} Request is in a DM; NSFW filter enabled.`);
		nsfw = true;
	} else if (msg.channel.nsfw) {
		options.filterID = msg.guild.settings.get('filter.nsfw', client.config.derpibooru.filters.nsfw);
		console.debug(`${requestId} Request was sent in a channel marked NSFW; NSFW filter enabled.`);
		nsfw = true;
	} else {
		options.filterID = msg.guild.settings.get('filter.sfw', client.config.derpibooru.filters.sfw);
		console.debug(`${requestId} Request was not sent in an NSFW channel; using SFW filter.`);
	}

	console.debug(`${requestId} Options: ${JSON.stringify(options)}`);

	const maxQueryPreviewLength = 100;
	let messagePrefix = args.query !== '' ? `query: \`${args.query.substr(0, maxQueryPreviewLength) + (args.query.length > maxQueryPreviewLength ? '...' : '')}\`: ` : '';

	let searchResults;

	try {
		searchResults = await Derpibooru.Fetch.search(options);
	} catch (err) {
		console.error(`${requestId} ERROR: ${err.message}`);
		if (err.message === 'Received status code 400') {
			return msg.reply(`${messagePrefix}Your query syntax is invalid. Please check your syntax and try again.`);
		}
		return msg.reply(`${messagePrefix}An error occurred: ${err.message}`);
	} finally {
		await msg.channel.stopTyping();
	}

	let results = searchResults.images;
	let result;

	if (results.length > 0) {
		result = results[options.sortFormat === 'random' ? Math.floor(Math.random() * results.length) : 0];
	}

	if (result === undefined) {
		console.info(`${requestId} No results found.`);
		return msg.reply(`${messagePrefix}No ${!nsfw ? 'safe-for-work ' : ''}images found with the current filter.`);
	}

	console.info(`${requestId} Result found - https://derpibooru.org/${result.id}`);

	console.debug(`${requestId} Creating embed from result...`);
	let replyEmbed = await embed.derpibooruResultToEmbed(result);

	let resolution;
	if (msg.channel.type === 'dm') {
		resolution = client.settings.get(`embedResolution.${msg.author.id}`, 'medium');
	} else {
		resolution = msg.guild.settings.get('embedResolution', 'medium');
	}

	switch (resolution) {
	case 'full':
		replyEmbed.image.url = result.representations.full;
		break;
	case 'high':
		replyEmbed.image.url = result.representations.large;
		break;
	case 'medium':
		replyEmbed.image.url = result.representations.medium;
		break;
	case 'low':
		replyEmbed.image.url = result.representations.small;
		break;
	}

	await msg.reply(messagePrefix, {
		embed: replyEmbed
	});

	// Discord doesn't let us use the video property on rich embeds. So instead, I have to send a second message with the video URL to get an in-client video.
	// Thanks, Discord. Very cool.
	if (result.mimeType.startsWith('video/')) {
		console.log(`${requestId} Result is a video. Sending second message with video link...`);
		return msg.channel.send(replyEmbed.image.url);
	}
};
<commit_msg>Remove options output<commit_after>
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const embed = require('./embed.js');
const Derpibooru = require('node-derpi');

/**
 * Handles an incoming Derpibooru-related command.
 *
 * @param {object} options - The options to pass to Derpibooru
 * @param {string} [options.sortFormat] - The sort format to pull a result from
 * @param {string} [options.sortOrder] - The order to sort the results in before retrieving one
 * @param {object} client - The Discord.js client object (this.client in command context)
 * @param {object} msg - The Discord.js message object (first param in async run)
 * @param {object} args - The arguments to the command (second param in async run)
 */
exports.handleDerpiCommand = async (options, client, msg, args) => {
	const requestId = `${msg.channel.id}:${msg.id} -`;

	console.info(`${requestId} Received.`);

	if (msg.channel.type === 'text' && msg.guild.settings.get(`blockedUsers.${msg.author.id}`)) {
		console.info(`${requestId} Blocked by adminstrator.`);
		return;
	}

	console.debug(`${requestId} Sending typing notification...`);
	msg.channel.startTyping();

	options.query = `${args.query}`;

	if (client.config.derpibooru.blockedTags) {
		if (options.query) options.query += ', ';
		options.query += `-(${client.config.derpibooru.blockedTags.join(' || ')})`;
	}

	let nsfw = false;

	// Only NSFW channels can have explicit content
	// (Assumes DMs are fine)
	if (msg.channel.type === 'dm') {
		options.filterID = client.settings.get(`filter.${msg.author.id}`, client.config.derpibooru.filters.nsfw);
		console.debug(`${requestId} Request is in a DM; NSFW filter enabled.`);
		nsfw = true;
	} else if (msg.channel.nsfw) {
		options.filterID = msg.guild.settings.get('filter.nsfw', client.config.derpibooru.filters.nsfw);
		console.debug(`${requestId} Request was sent in a channel marked NSFW; NSFW filter enabled.`);
		nsfw = true;
	} else {
		options.filterID = msg.guild.settings.get('filter.sfw', client.config.derpibooru.filters.sfw);
		console.debug(`${requestId} Request was not sent in an NSFW channel; using SFW filter.`);
	}

	// console.debug(`${requestId} Options: ${JSON.stringify(options)}`);

	const maxQueryPreviewLength = 100;
	let messagePrefix = args.query !== '' ? `query: \`${args.query.substr(0, maxQueryPreviewLength) + (args.query.length > maxQueryPreviewLength ? '...' : '')}\`: ` : '';

	let searchResults;

	try {
		searchResults = await Derpibooru.Fetch.search(options);
	} catch (err) {
		console.error(`${requestId} ERROR: ${err.message}`);
		if (err.message === 'Received status code 400') {
			return msg.reply(`${messagePrefix}Your query syntax is invalid. Please check your syntax and try again.`);
		}
		return msg.reply(`${messagePrefix}An error occurred: ${err.message}`);
	} finally {
		await msg.channel.stopTyping();
	}

	let results = searchResults.images;
	let result;

	if (results.length > 0) {
		result = results[options.sortFormat === 'random' ? Math.floor(Math.random() * results.length) : 0];
	}

	if (result === undefined) {
		console.info(`${requestId} No results found.`);
		return msg.reply(`${messagePrefix}No ${!nsfw ? 'safe-for-work ' : ''}images found with the current filter.`);
	}

	console.info(`${requestId} Result found - https://derpibooru.org/${result.id}`);

	console.debug(`${requestId} Creating embed from result...`);
	let replyEmbed = await embed.derpibooruResultToEmbed(result);

	let resolution;
	if (msg.channel.type === 'dm') {
		resolution = client.settings.get(`embedResolution.${msg.author.id}`, 'medium');
	} else {
		resolution = msg.guild.settings.get('embedResolution', 'medium');
	}

	switch (resolution) {
	case 'full':
		replyEmbed.image.url = result.representations.full;
		break;
	case 'high':
		replyEmbed.image.url = result.representations.large;
		break;
	case 'medium':
		replyEmbed.image.url = result.representations.medium;
		break;
	case 'low':
		replyEmbed.image.url = result.representations.small;
		break;
	}

	await msg.reply(messagePrefix, {
		embed: replyEmbed
	});

	// Discord doesn't let us use the video property on rich embeds. So instead, I have to send a second message with the video URL to get an in-client video.
	// Thanks, Discord. Very cool.
	if (result.mimeType.startsWith('video/')) {
		console.log(`${requestId} Result is a video. Sending second message with video link...`);
		return msg.channel.send(replyEmbed.image.url);
	}
};
<|endoftext|>


<commit_before>
let now = Date.now

export default function (fn, wait = 250, immediate) {
  let
    timeout, args, context, timestamp, result,
    later = () => {
      let last = now() - timestamp

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last)
      }
      else {
        timeout = null
        if (!immediate) {
          result = fn.apply(context, args)
          if (!timeout) {
            context = args = null
          }
        }
      }
    }

  return function (...args) {
    var callNow = immediate && !timeout

    context = this
    timestamp = now()

    if (!timeout) {
      timeout = setTimeout(later, wait)
    }
    if (callNow) {
      result = fn.apply(context, args)
      context = args = null
    }

    return result
  }
}
<commit_msg>fix: Utils.debounce on function with parameters<commit_after>
let now = Date.now

export default function (fn, wait = 250, immediate) {
  let
    timeout, params, context, timestamp, result,
    later = () => {
      let last = now() - timestamp

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last)
      }
      else {
        timeout = null
        if (!immediate) {
          result = fn.apply(context, params)
          if (!timeout) {
            context = params = null
          }
        }
      }
    }

  return function (...args) {
    var callNow = immediate && !timeout

    context = this
    timestamp = now()
    params = args

    if (!timeout) {
      timeout = setTimeout(later, wait)
    }
    if (callNow) {
      result = fn.apply(context, args)
      context = params = null
    }

    return result
  }
}
<|endoftext|>


<commit_before>global.getInterface = function (superclass) {
    let mixin = () => class extends superclass {};
    Object.setPrototypeOf(mixin, superclass.prototype);
    mixin[Symbol.hasInstance] = (o) => {
        let mixins = o.constructor['_applied_mixins'];
        return mixins.indexOf(mixin) != -1;
    };

    return mixin;
};

global.mix = function (superclass, ...interfaces) {
    superclass = superclass || class {};
    superclass = interfaces.reduce((a, b) => b(a), superclass);

    let mixed = (s => class extends s {})(superclass);
    Object.defineProperty(mixed, '_applied_mixins', {
        value: [superclass, ...interfaces],
        enumerable: false
    });
    return mixed;
};
<commit_msg>fixed instanceof check for undefined values<commit_after>global.getInterface = function (superclass) {
    let mixin = () => class extends superclass {};
    Object.setPrototypeOf(mixin, superclass.prototype);
    mixin[Symbol.hasInstance] = (o) => {
        let mixins = o.constructor['_applied_mixins'];
        if (! mixins) {
            return false;
        }

        return mixins.indexOf(mixin) != -1;
    };

    return mixin;
};

global.mix = function (superclass, ...interfaces) {
    superclass = superclass || class {};
    superclass = interfaces.reduce((a, b) => b(a), superclass);

    let mixed = (s => class extends s {})(superclass);
    Object.defineProperty(mixed, '_applied_mixins', {
        value: [superclass, ...interfaces],
        enumerable: false
    });
    return mixed;
};
<|endoftext|>


<commit_before>/* HPK Namespace. */
var HPK = {};

/* ===== Utilities ===== */

jQuery.fn.boxify = function() {
  return this.each(function(index) {
    return jQuery(this)
      .addClass("box")
      .wrapInner("
")
      .prepend("
"
        + "
"
        + "
"
        + "
"
        + "

"
      )
      .append("
"
        + "
"
        + "
"
        + "
"
        + "

"
      );
  });
}

/* ===== GotoBox ===== */

/* Creates a new GotoBox object. */
HPK.GotoBox = function(presentation) {
  var that = this;
  this._element = $("<input type='text' id='goto-box'>")
    .keydown(function(event) {
      switch (event.which) {
        case 13: // Enter
          presentation.gotoSlide($(this).val() - 1);
          that.hide();
          return false;
        case 27: // ESC
          that.hide();
          return false;
        default:
          event.stopPropagation();
      }
    });
  $("body").append(this._element);
}

HPK.GotoBox.prototype = {
  /* Shows, clears and focuses the goto box. */
  show: function() {
    this._element.val("").show().focus();
  },

  /* Hides the goto box. */
  hide: function() {
    this._element.hide();
  }
}

/* ===== CurrentSlideCounter ===== */

/* Creates a new CurrentSlideCounter object. */
HPK.CurrentSlideCounter = function() {
  this._element = $("
");
  $("body").append(this._element);
}

HPK.CurrentSlideCounter.prototype = {
  /* Updates the current slide counter and the location hash. */
  update: function(currentSlideIndex) {
    if (currentSlideIndex !== null) {
      this._element.text(currentSlideIndex + 1);
      location.hash = "slide-" + (currentSlideIndex + 1);
    } else {
      /* Following statement is somewhat imperfect - it causes reload in Firefox
         and a trailing "#" character remians in the URL in IE and Chrome. But I
         don't think it's worth to investigate this more or work around the
         Firefox bug. At least until someone complains :-) */
      location.hash = "";
    }
  }
}

/* ===== SlideList ===== */

/* Creates a new SlideList object. */
HPK.SlideList = function(presentation, slides) {
  var that = this;

  var list = $("
")
  slides.each(function(i) {
    list.append($("
").append($("")
      .text($(this).find("h1").text())
      .click(function(event) {
        presentation.gotoSlide(i);
        that.hide();
        return false;
      })
    ));
  });

  this._focusTrap = $("<input type='text' class='focus-trap'>")
    .keydown(function(event) {
      if (event.which == 27) { // ESC
        that.hide();
        return false;
      } else {
        event.stopPropagation();
      }
    });
  this._element = $("
")
    .append(list)
    .append($("
").append($("")
      .text(HPK.localizationStrings["closeSlideListLinkText"])
      .click(function(event) {
        that.hide();
        return false;
      })
    ))
    .append(this._focusTrap)
    .boxify();

  $("body").append(this._element);
}

HPK.SlideList.prototype = {
  /* Shows the slide list. */
  show: function() {
    this._element
      .css(
        "top",
        ($("body").outerHeight() - this._element.outerHeight()) / 2 + "px"
      )
      .css("opacity", "0")
      .show()
      .fadeTo("fast", 0.8);
    this._focusTrap.focus();
  },

  /* Hides the slide list. */
  hide: function() {
    this._element.fadeOut("fast");
  },

  /* Hides the slide list immediately (without the fade effect). */
  hideWithoutFade: function() {
    this._element.hide();
  }
}

/* ===== Navigation ===== */

/* Creates a new Navigation object. */
HPK.Navigation = function(presentation, slideList) {
  this._visible = false;
  this._pinned = false;
  var that = this;
  this._element = $("
")
    .append($("")
      .attr("title", HPK.localizationStrings["prevSlideLinkTitle"])
      .click(function(event) {
        presentation.gotoPrevSlide();
        return false;
      })
    )
    .append($("")
      .attr("title", HPK.localizationStrings["slideListLinkTitle"])
      .click(function(event) {
        slideList.show();
        return false;
      })
    )
    .append($("")
      .attr("title", HPK.localizationStrings["nextSlideLinkTitle"])
      .click(function(event) {
        presentation.gotoNextSlide();
        return false;
      })
    )
    .mouseover(function() {
      that._clearHideTimer();
    })
    .mouseout(function() {
      that.touch();
    })
    .mousemove(function(event) {
      return false;
    })
    .boxify();
  $("body").append(this._element);
  this._hideTimer = null;
  this._unpinTimer = null;
}

HPK.Navigation.prototype = {
  /* Is the navigation visible? */
  isVisible: function() {
    return this._visible;
  },

  /* Is the navigation pinned? */
  isPinned: function() {
    return this._pinned;
  },

  /* Clears the current hiding timer. */
  _clearHideTimer: function() {
    if (this._hideTimer) {
      clearTimeout(this._hideTimer);
      this._hideTimer = null;
    }
  },

  /* Clears the current unpin timer. */
  _clearUnpinTimer: function() {
    if (this._unpinTimer) {
      clearTimeout(this._unpinTimer);
      this._unpinTimer = null;
    }
  },

  /* Clears the current hiding timer and creates a new one. */
  touch: function() {
    this._clearHideTimer();
    var that = this;
    this._hideTimer = setTimeout(function() {
      that.hide();
    }, 3000);
  },

  /* Marks the navigation as pinned for a while. */
  pin: function(ms) {
    this._pinned = true;
    var that = this;
    this._unpinTimer = setTimeout(function() {
      that._pinned = false;
    }, 3000);
  },

  /* Shows the navigation. */
  show: function() {
    this._visible = true;
    this._element.css("opacity", "0").show().fadeTo("normal", 0.8);
    this.touch();
  },

  /* Shows the navigation immediately (without the fade effect). */
  showWithoutFade: function() {
    this._visible = true;
    this._element.css("opacity", "0.8").show();
    this.touch();
  },

  /* Hides the navigation. */
  hide: function() {
    this._clearHideTimer();
    this._clearUnpinTimer();
    this._element.fadeOut("normal");
    this._visible = false;
    this._pinned = false;
  },

  /* Hides the navigation immediately (without the fade effect). */
  hideWithoutFade: function() {
    this._clearHideTimer();
    this._clearUnpinTimer();
    this._element.hide();
    this._visible = false;
    this._pinned = false;
  }
}

/* ===== Presentation ===== */

/* Creates a new Presentation object and injects the presentation mode toggle
   element into the HTML. */
HPK.Presentation = function() {
  this._slides = $("div.presentation div.slide");
  this._currentSlideIndex = null;
  this._presenting = false;
  this._screenStyleLinks = $("link[rel=stylesheet][media=screen]");
  this._projectionStyleLinks = $("link[rel=stylesheet][media=projection]");

  this._createRunPresentationLink();

  this._currentSlideCounter = new HPK.CurrentSlideCounter;
  this._gotoBox = new HPK.GotoBox(this);
  this._slideList = new HPK.SlideList(this, this._slides);
  this._navigation = new HPK.Navigation(this, this._slideList);
}

HPK.Presentation.prototype = {
  /* Sets the base document font size. The size is specified in CSS format (as
     passed to the font-size rule). */
  _setBaseFontSize: function(size) {
    document.body.style.fontSize = size;
  },

  /* Creates the link that enables user to run the presentation. */
  _createRunPresentationLink: function() {
    var that = this;
    var link = $("")
      .text(HPK.localizationStrings["runPresentationLinkText"])
      .click(function(event) {
        that.beginPresentation();

        /* The following line makes sure that the $(document.click(...) handler
           we setup in the "beginPresentation" will not catch the event. */
        return false;
      })
    $("body").append($("
").append(link).boxify());
  },

  /* Returns current slide as jQuery object. */
  _currentSlide: function() {
    return this._slides.slice(this._currentSlideIndex, this._currentSlideIndex + 1);
  },

  /* Are we presenting now? */
  isPresenting: function() {
    return this._presenting;
  },

  /* Begins the presentation. */
  beginPresentation: function() {
    if (this._presenting) { return; }
    if (this._slides.length == 0) { return; }

    this._setBaseFontSize(0.04 * $(window).height() + "px");
    this._slides.slice(1).hide();
    this._currentSlideIndex = 0;
    this._currentSlideCounter.update(this._currentSlideIndex);
    this._navigation.showWithoutFade();
    this._navigation.pin();

    this._screenStyleLinks.attr("media", "projection")
    this._projectionStyleLinks.attr("media", "screen");
    /* WebKit does not reflect all changes and needs a little help. */
    if ($.browser.safari) { // Detects Google Chrome too.
      this._screenStyleLinks.attr("disabled", "disabled");
      this._projectionStyleLinks.removeAttr("disabled");
    }

    var that = this;

    $(document).mousemove(function(event) {
      if (event.pageY >= 0.8 * $(document).height()) {
        if (!that._navigation.isVisible()) {
          that._navigation.show();
        } else {
          that._navigation.touch();
        }
      } else {
        if (that._navigation.isVisible() && !that._navigation.isPinned()) {
          that._navigation.hide();
        }
      }
    });

    $(document).click(function(event) {
      switch (event.button) {
        case 0: // Left button
          that.gotoNextSlide();
          return true;
        case 2: // Right button
          that.gotoPrevSlide();
          return true;
      }
    });

    $(document).keypress(function(event) {
      switch (event.which) {
        case 32:  // Space
        case 110: // "n"
          that.gotoNextSlide();
          return false;

        case 112: // "p"
          that.gotoPrevSlide();
          return false;

        case 103: // "g"
          that._gotoBox.show();
          return false;
      }
    });

    $(document).keydown(function(event) {
      switch (event.which) {
        case 13: // Enter
        case 34: // Page Down
        case 39: // Right Arrow
        case 40: // Down Arrow
          that.gotoNextSlide();
          return false;

        case 8:  // Backspace
        case 33: // Page Up
        case 37: // Left Arrow
        case 38: // Up Arrow
          that.gotoPrevSlide();
          return false;

        case 27: // ESC
          that.endPresentation();
          return false;
      }
    });

    this._presenting = true;
  },

  /* Ends the presentation. */
  endPresentation: function() {
    if (!this._presenting) { return; }

    this._setBaseFontSize("100%");
    this._slides.show();
    this._currentSlideIndex = null;
    this._currentSlideCounter.update(this._currentSlideIndex);
    this._gotoBox.hide();
    this._slideList.hideWithoutFade();
    this._navigation.hideWithoutFade();

    this._screenStyleLinks.attr("media", "screen");
    this._projectionStyleLinks.attr("media", "projection");
    /* WebKit does not reflect all changes and needs a little help. */
    if ($.browser.safari) { // Detects Google Chrome too.
      this._screenStyleLinks.removeAttr("disabled");
      this._projectionStyleLinks.attr("disabled", "disabled");
    }

    $(document).unbind("mousemove");
    $(document).unbind("click");
    $(document).unbind("keypress");
    $(document).unbind("keydown");

    this._presenting = false;
  },

  /* Are we on the last slide? */
  isOnFirstSlide: function() {
    return this._presenting
      && this._currentSlideIndex == 0;
  },

  /* Are we on the last slide? */
  isOnLastSlide: function() {
    return this._presenting
      && this._currentSlideIndex == this._slides.length - 1;
  },

  /* If presenting, moves to the slide with specified index (if there is any). */
  gotoSlide: function(slideIndex) {
    if (this._presenting && slideIndex >= 0 && slideIndex < this._slides.length) {
      this._currentSlide().hide();
      this._currentSlideIndex = slideIndex;
      this._currentSlide().show();
      this._currentSlideCounter.update(this._currentSlideIndex);
    }
  },

  /* If presenting, moves to the next slide (if there is any). */
  gotoNextSlide: function() {
    this.gotoSlide(this._currentSlideIndex + 1);
  },

  /* If presenting, moves to the previous slide (if there is any). */
  gotoPrevSlide: function() {
    this.gotoSlide(this._currentSlideIndex - 1);
  }
}

/* ===== Let's roll... ===== */

$(document).ready(function() {
  /* Opera has its own presentation capabilities, we do not have to help it.
     See http://www.opera.com/browser/tutorials/operashow/. */
  if ($.browser.opera) { return; }

  HPK.presentation = new HPK.Presentation();

  /* These variables will be closed over by the resize handler bellow, so they
     will behave like static variables from its point of view. */
  var oldWindowWidth = $(window).width();
  var oldWindowHeight = $(window).height();

  $(window).bind("resize", function() {
    var windowWidth = $(window).width();
    var windowHeight = $(window).height();

    /* Optimization, the event is sometimes triggered unnecessairly by IE. */
    if (windowWidth == oldWindowWidth && windowHeight == oldWindowHeight) {
      return;
    }

    /* In IE, the window height is actually larger than the screen height
       in the fullscreen mode, but it does not count scrollbar width into the
       window width. As a result, fullscreen mode detection must be a bit
       tolerant. Firefox 3+ can detect fullscreen mode by itself. */
    var isFullscreen = $.browser.mozilla
      ? window.fullScreen
      : screen.width - windowWidth <= 20
        && screen.height - windowHeight <= 20;

    /* Automatically begin the presentation when the browser is switched to the
       full-screen mode. The presentation does not automatically end when
       switching out of the full-screen mode because it would be disturbing. */
    if (!HPK.presentation.isPresenting() && isFullscreen) {
      HPK.presentation.beginPresentation();
    }

    oldWindowWidth = windowWidth;
    oldWindowHeight = windowHeight;
  });

  var matches = document.location.hash.match(/^#slide-(\d+)$/);
  if (matches) {
    HPK.presentation.beginPresentation();
    HPK.presentation.gotoSlide(parseInt(matches[1]) - 1);
  }
});
<commit_msg>Changed hashes from #slide-N to #presentation[+slide-N] to allow linking both into into the presentation and non-presentation mode.<commit_after>/* HPK Namespace. */
var HPK = {};

/* ===== Utilities ===== */

jQuery.fn.boxify = function() {
  return this.each(function(index) {
    return jQuery(this)
      .addClass("box")
      .wrapInner("
")
      .prepend("
"
        + "
"
        + "
"
        + "
"
        + "

"
      )
      .append("
"
        + "
"
        + "
"
        + "
"
        + "

"
      );
  });
}

/* ===== GotoBox ===== */

/* Creates a new GotoBox object. */
HPK.GotoBox = function(presentation) {
  var that = this;
  this._element = $("<input type='text' id='goto-box'>")
    .keydown(function(event) {
      switch (event.which) {
        case 13: // Enter
          presentation.gotoSlide($(this).val() - 1);
          that.hide();
          return false;
        case 27: // ESC
          that.hide();
          return false;
        default:
          event.stopPropagation();
      }
    });
  $("body").append(this._element);
}

HPK.GotoBox.prototype = {
  /* Shows, clears and focuses the goto box. */
  show: function() {
    this._element.val("").show().focus();
  },

  /* Hides the goto box. */
  hide: function() {
    this._element.hide();
  }
}

/* ===== CurrentSlideCounter ===== */

/* Creates a new CurrentSlideCounter object. */
HPK.CurrentSlideCounter = function() {
  this._element = $("
");
  $("body").append(this._element);
}

HPK.CurrentSlideCounter.prototype = {
  /* Updates the current slide counter and the location hash. */
  update: function(currentSlideIndex) {
    if (currentSlideIndex !== null) {
      this._element.text(currentSlideIndex + 1);
      location.hash = "presentation+slide-" + (currentSlideIndex + 1);
    } else {
      /* Following statement is somewhat imperfect - it causes reload in Firefox
         and a trailing "#" character remians in the URL in IE and Chrome. But I
         don't think it's worth to investigate this more or work around the
         Firefox bug. At least until someone complains :-) */
      location.hash = "";
    }
  }
}

/* ===== SlideList ===== */

/* Creates a new SlideList object. */
HPK.SlideList = function(presentation, slides) {
  var that = this;

  var list = $("
")
  slides.each(function(i) {
    list.append($("
").append($("")
      .text($(this).find("h1").text())
      .click(function(event) {
        presentation.gotoSlide(i);
        that.hide();
        return false;
      })
    ));
  });

  this._focusTrap = $("<input type='text' class='focus-trap'>")
    .keydown(function(event) {
      if (event.which == 27) { // ESC
        that.hide();
        return false;
      } else {
        event.stopPropagation();
      }
    });
  this._element = $("
")
    .append(list)
    .append($("
").append($("")
      .text(HPK.localizationStrings["closeSlideListLinkText"])
      .click(function(event) {
        that.hide();
        return false;
      })
    ))
    .append(this._focusTrap)
    .boxify();

  $("body").append(this._element);
}

HPK.SlideList.prototype = {
  /* Shows the slide list. */
  show: function() {
    this._element
      .css(
        "top",
        ($("body").outerHeight() - this._element.outerHeight()) / 2 + "px"
      )
      .css("opacity", "0")
      .show()
      .fadeTo("fast", 0.8);
    this._focusTrap.focus();
  },

  /* Hides the slide list. */
  hide: function() {
    this._element.fadeOut("fast");
  },

  /* Hides the slide list immediately (without the fade effect). */
  hideWithoutFade: function() {
    this._element.hide();
  }
}

/* ===== Navigation ===== */

/* Creates a new Navigation object. */
HPK.Navigation = function(presentation, slideList) {
  this._visible = false;
  this._pinned = false;
  var that = this;
  this._element = $("
")
    .append($("")
      .attr("title", HPK.localizationStrings["prevSlideLinkTitle"])
      .click(function(event) {
        presentation.gotoPrevSlide();
        return false;
      })
    )
    .append($("")
      .attr("title", HPK.localizationStrings["slideListLinkTitle"])
      .click(function(event) {
        slideList.show();
        return false;
      })
    )
    .append($("")
      .attr("title", HPK.localizationStrings["nextSlideLinkTitle"])
      .click(function(event) {
        presentation.gotoNextSlide();
        return false;
      })
    )
    .mouseover(function() {
      that._clearHideTimer();
    })
    .mouseout(function() {
      that.touch();
    })
    .mousemove(function(event) {
      return false;
    })
    .boxify();
  $("body").append(this._element);
  this._hideTimer = null;
  this._unpinTimer = null;
}

HPK.Navigation.prototype = {
  /* Is the navigation visible? */
  isVisible: function() {
    return this._visible;
  },

  /* Is the navigation pinned? */
  isPinned: function() {
    return this._pinned;
  },

  /* Clears the current hiding timer. */
  _clearHideTimer: function() {
    if (this._hideTimer) {
      clearTimeout(this._hideTimer);
      this._hideTimer = null;
    }
  },

  /* Clears the current unpin timer. */
  _clearUnpinTimer: function() {
    if (this._unpinTimer) {
      clearTimeout(this._unpinTimer);
      this._unpinTimer = null;
    }
  },

  /* Clears the current hiding timer and creates a new one. */
  touch: function() {
    this._clearHideTimer();
    var that = this;
    this._hideTimer = setTimeout(function() {
      that.hide();
    }, 3000);
  },

  /* Marks the navigation as pinned for a while. */
  pin: function(ms) {
    this._pinned = true;
    var that = this;
    this._unpinTimer = setTimeout(function() {
      that._pinned = false;
    }, 3000);
  },

  /* Shows the navigation. */
  show: function() {
    this._visible = true;
    this._element.css("opacity", "0").show().fadeTo("normal", 0.8);
    this.touch();
  },

  /* Shows the navigation immediately (without the fade effect). */
  showWithoutFade: function() {
    this._visible = true;
    this._element.css("opacity", "0.8").show();
    this.touch();
  },

  /* Hides the navigation. */
  hide: function() {
    this._clearHideTimer();
    this._clearUnpinTimer();
    this._element.fadeOut("normal");
    this._visible = false;
    this._pinned = false;
  },

  /* Hides the navigation immediately (without the fade effect). */
  hideWithoutFade: function() {
    this._clearHideTimer();
    this._clearUnpinTimer();
    this._element.hide();
    this._visible = false;
    this._pinned = false;
  }
}

/* ===== Presentation ===== */

/* Creates a new Presentation object and injects the presentation mode toggle
   element into the HTML. */
HPK.Presentation = function() {
  this._slides = $("div.presentation div.slide");
  this._currentSlideIndex = null;
  this._presenting = false;
  this._screenStyleLinks = $("link[rel=stylesheet][media=screen]");
  this._projectionStyleLinks = $("link[rel=stylesheet][media=projection]");

  this._createRunPresentationLink();

  this._currentSlideCounter = new HPK.CurrentSlideCounter;
  this._gotoBox = new HPK.GotoBox(this);
  this._slideList = new HPK.SlideList(this, this._slides);
  this._navigation = new HPK.Navigation(this, this._slideList);
}

HPK.Presentation.prototype = {
  /* Sets the base document font size. The size is specified in CSS format (as
     passed to the font-size rule). */
  _setBaseFontSize: function(size) {
    document.body.style.fontSize = size;
  },

  /* Creates the link that enables user to run the presentation. */
  _createRunPresentationLink: function() {
    var that = this;
    var link = $("")
      .text(HPK.localizationStrings["runPresentationLinkText"])
      .click(function(event) {
        that.beginPresentation();

        /* The following line makes sure that the $(document.click(...) handler
           we setup in the "beginPresentation" will not catch the event. */
        return false;
      })
    $("body").append($("
").append(link).boxify());
  },

  /* Returns current slide as jQuery object. */
  _currentSlide: function() {
    return this._slides.slice(this._currentSlideIndex, this._currentSlideIndex + 1);
  },

  /* Are we presenting now? */
  isPresenting: function() {
    return this._presenting;
  },

  /* Begins the presentation. */
  beginPresentation: function() {
    if (this._presenting) { return; }
    if (this._slides.length == 0) { return; }

    this._setBaseFontSize(0.04 * $(window).height() + "px");
    this._slides.slice(1).hide();
    this._currentSlideIndex = 0;
    this._currentSlideCounter.update(this._currentSlideIndex);
    this._navigation.showWithoutFade();
    this._navigation.pin();

    this._screenStyleLinks.attr("media", "projection")
    this._projectionStyleLinks.attr("media", "screen");
    /* WebKit does not reflect all changes and needs a little help. */
    if ($.browser.safari) { // Detects Google Chrome too.
      this._screenStyleLinks.attr("disabled", "disabled");
      this._projectionStyleLinks.removeAttr("disabled");
    }

    var that = this;

    $(document).mousemove(function(event) {
      if (event.pageY >= 0.8 * $(document).height()) {
        if (!that._navigation.isVisible()) {
          that._navigation.show();
        } else {
          that._navigation.touch();
        }
      } else {
        if (that._navigation.isVisible() && !that._navigation.isPinned()) {
          that._navigation.hide();
        }
      }
    });

    $(document).click(function(event) {
      switch (event.button) {
        case 0: // Left button
          that.gotoNextSlide();
          return true;
        case 2: // Right button
          that.gotoPrevSlide();
          return true;
      }
    });

    $(document).keypress(function(event) {
      switch (event.which) {
        case 32:  // Space
        case 110: // "n"
          that.gotoNextSlide();
          return false;

        case 112: // "p"
          that.gotoPrevSlide();
          return false;

        case 103: // "g"
          that._gotoBox.show();
          return false;
      }
    });

    $(document).keydown(function(event) {
      switch (event.which) {
        case 13: // Enter
        case 34: // Page Down
        case 39: // Right Arrow
        case 40: // Down Arrow
          that.gotoNextSlide();
          return false;

        case 8:  // Backspace
        case 33: // Page Up
        case 37: // Left Arrow
        case 38: // Up Arrow
          that.gotoPrevSlide();
          return false;

        case 27: // ESC
          that.endPresentation();
          return false;
      }
    });

    this._presenting = true;
  },

  /* Ends the presentation. */
  endPresentation: function() {
    if (!this._presenting) { return; }

    this._setBaseFontSize("100%");
    this._slides.show();
    this._currentSlideIndex = null;
    this._currentSlideCounter.update(this._currentSlideIndex);
    this._gotoBox.hide();
    this._slideList.hideWithoutFade();
    this._navigation.hideWithoutFade();

    this._screenStyleLinks.attr("media", "screen");
    this._projectionStyleLinks.attr("media", "projection");
    /* WebKit does not reflect all changes and needs a little help. */
    if ($.browser.safari) { // Detects Google Chrome too.
      this._screenStyleLinks.removeAttr("disabled");
      this._projectionStyleLinks.attr("disabled", "disabled");
    }

    $(document).unbind("mousemove");
    $(document).unbind("click");
    $(document).unbind("keypress");
    $(document).unbind("keydown");

    this._presenting = false;
  },

  /* Are we on the last slide? */
  isOnFirstSlide: function() {
    return this._presenting
      && this._currentSlideIndex == 0;
  },

  /* Are we on the last slide? */
  isOnLastSlide: function() {
    return this._presenting
      && this._currentSlideIndex == this._slides.length - 1;
  },

  /* If presenting, moves to the slide with specified index (if there is any). */
  gotoSlide: function(slideIndex) {
    if (this._presenting && slideIndex >= 0 && slideIndex < this._slides.length) {
      this._currentSlide().hide();
      this._currentSlideIndex = slideIndex;
      this._currentSlide().show();
      this._currentSlideCounter.update(this._currentSlideIndex);
    }
  },

  /* If presenting, moves to the next slide (if there is any). */
  gotoNextSlide: function() {
    this.gotoSlide(this._currentSlideIndex + 1);
  },

  /* If presenting, moves to the previous slide (if there is any). */
  gotoPrevSlide: function() {
    this.gotoSlide(this._currentSlideIndex - 1);
  }
}

/* ===== Let's roll... ===== */

$(document).ready(function() {
  /* Opera has its own presentation capabilities, we do not have to help it.
     See http://www.opera.com/browser/tutorials/operashow/. */
  if ($.browser.opera) { return; }

  HPK.presentation = new HPK.Presentation();

  /* These variables will be closed over by the resize handler bellow, so they
     will behave like static variables from its point of view. */
  var oldWindowWidth = $(window).width();
  var oldWindowHeight = $(window).height();

  $(window).bind("resize", function() {
    var windowWidth = $(window).width();
    var windowHeight = $(window).height();

    /* Optimization, the event is sometimes triggered unnecessairly by IE. */
    if (windowWidth == oldWindowWidth && windowHeight == oldWindowHeight) {
      return;
    }

    /* In IE, the window height is actually larger than the screen height
       in the fullscreen mode, but it does not count scrollbar width into the
       window width. As a result, fullscreen mode detection must be a bit
       tolerant. Firefox 3+ can detect fullscreen mode by itself. */
    var isFullscreen = $.browser.mozilla
      ? window.fullScreen
      : screen.width - windowWidth <= 20
        && screen.height - windowHeight <= 20;

    /* Automatically begin the presentation when the browser is switched to the
       full-screen mode. The presentation does not automatically end when
       switching out of the full-screen mode because it would be disturbing. */
    if (!HPK.presentation.isPresenting() && isFullscreen) {
      HPK.presentation.beginPresentation();
    }

    oldWindowWidth = windowWidth;
    oldWindowHeight = windowHeight;
  });

  var matches = document.location.hash.match(/^#presentation(\+slide-(\d+))?$/);
  if (matches) {
    HPK.presentation.beginPresentation();
    if (matches[1]) {
      HPK.presentation.gotoSlide(parseInt(matches[2]) - 1);
    }
  }
});
<|endoftext|>


<commit_before>export function distanceFromTwoPoints(x0, y0, x1, y1) {
  return Math.sqrt(Math.pow((x1 - x0), 2) + Math.pow((y1 - y0), 2));
}

export function horizontalLine(y) {
  return {a: 0, b: 1, c: -y}
}

export function verticalLine(x) {
  return {a: 1, b: 0, c: -x}
}

export function linePassingThroughTwoPoints(x1, y1, x2, y2) {
  // (x2 - x1)(y - y1) = (y2 - y1)(x - x1)
  // (y1 - y2)x + (x2 - x1)y + (y2x1 - x2y1) = 0

  if (x1 === x2 && y1 == y2) throw new Error("Geometry error");
  if (x1 === x2) return verticalLine(x);
  if (y1 === y2) return horizontalLine(y1);

  return {
    a: y1 - y2,
    b: x2 - x1,
    c: y2 * x1 - x2 * y1
  };
}

export function distancePointFromLine(a, b, c, x, y) {
  //https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
  return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b);
}

export function closestPointFromLine(a, b, c, x, y) {
  //https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
  let denom = a * a + b * b;
  return {
    x: (b * (b * x - a * y) - a * c) / denom,
    y: ((a * -b * x + a * y ) - b * c) / denom,
  }
}

export function intersectionFromTwoLines(a, b, c, j, k, l) {
  let det = (b * j - a * k);

  if (det === 0) return undefined; //no intersection

  let y = (a * l - c * j) / det;
  let x = (c * k - b * l) / det;
  return {x, y};
}

export function intersectionFromTwoLineSegment(p1, p2, p3, p4) {
  //https://github.com/psalaets/line-intersect/blob/master/lib/check-intersection.js

  let {x: x1, y: y1} = p1;
  let {x: x2, y: y2} = p2;
  let {x: x3, y: y3} = p3;
  let {x: x4, y:y4} = p4;

  let EPSILON = 10e-6;

  let denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1));
  let numA = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3));
  let numB = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3));

  if (Math.abs(denom) <= EPSILON) {
    if (Math.abs(numA) <= EPSILON && Math.abs(numB) <= EPSILON) {

      let comparator = (pa, pb) => pa.x === pb.x ? pa.y - pb.y : pa.x - pb.x;
      let line0 = [p1, p2].sort(comparator);
      let line1 = [p3.toJS(), p4.toJS()].sort(comparator);

      let [lineSX, lineDX] = [line0, line1].sort((lineA, lineB) => comparator(lineA[0], lineB[0]));

      if (lineSX[1].x === lineDX[0].x) {
        return {type: (lineDX[0].y <= lineSX[1].y) ? "colinear" : "none"};
      } else {
        return {type: (lineDX[0].x <= lineSX[1].x) ? "colinear" : "none"};
      }
    }
    return {type: "parallel"};
  }

  var uA = numA / denom;
  var uB = numB / denom;

  if (uA >= (0 - EPSILON) && uA <= (1 + EPSILON) && uB >= (0 - EPSILON) && uB <= (1 + EPSILON)) {
    let point = {
      x: x1 + (uA * (x2 - x1)),
      y: y1 + (uA * (y2 - y1))
    };
    return {type: "intersecting", point};
  }

  return {type: "none"};
}

export function distancePointFromLineSegment(x1, y1, x2, y2, xp, yp) {
  //http://stackoverflow.com/a/6853926/1398836

  let A = xp - x1;
  let B = yp - y1;
  let C = x2 - x1;
  let D = y2 - y1;

  let dot = A * C + B * D;
  let len_sq = C * C + D * D;
  let param = -1;
  if (len_sq != 0) //in case of 0 length line
    param = dot / len_sq;

  let xx, yy;

  if (param < 0) {
    xx = x1;
    yy = y1;
  }
  else if (param > 1) {
    xx = x2;
    yy = y2;
  }
  else {
    xx = x1 + param * C;
    yy = y1 + param * D;
  }

  let dx = xp - xx;
  let dy = yp - yy;
  return Math.sqrt(dx * dx + dy * dy);
}

export function closestPointFromLineSegment(x1, y1, x2, y2, xp, yp) {
  if (x1 === x2) return {x: x1, y: yp};
  if (y1 === y2) return {x: xp, y: y1};

  let m = (y2 - y1) / (x2 - x1);
  let q = y1 - m * x1;

  let mi = -1 / m;
  let qi = yp - mi * xp;

  let x = (qi - q) / (m - mi);
  let y = (m * x + q);

  return {x, y};
}

export function pointPositionOnLineSegment(x1, y1, x2, y2, xp, yp) {
  let length = distanceFromTwoPoints(x1, y1, x2, y2);
  let distance = distanceFromTwoPoints(x1, y1, xp, yp);

  let offset = distance / length;
  if (x1 > x2) offset = mapRange(offset, 0, 1, 1, 0);

  return offset;
}

export function mapRange(value, low1, high1, low2, high2) {
  return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}

export function angleBetweenTwoPointsAndOrigin(x1, y1, x2, y2) {
  let length = distanceFromTwoPoints(x1, y1, x2, y2);
  return (-Math.asin((y1 - y2) / length)) * 180 / Math.PI;
}

export function samePoints({x:x1, y:y1}, {x:x2, y:y2}) {
  let EPSILON = 10e-6;
  return Math.abs(x1 - x2) <= EPSILON && Math.abs(y1 - y2) <= EPSILON;
}
<commit_msg>Added new utilities to geometry<commit_after>export function compareVertices(v0, v1) {
  return v0.x === v1.x ? v0.y - v1.y : v0.x - v1.x
}

export function minVertex(v0,v1) {
  return compareVertices(v0,v1) > 0 ? v1: v0;
}

export function maxVertex(v0,v1) {
  return compareVertices(v0,v1) > 0 ? v0: v1;
}

export function distanceFromTwoPoints(x0, y0, x1, y1) {
  return Math.sqrt(Math.pow((x1 - x0), 2) + Math.pow((y1 - y0), 2));
}

export function horizontalLine(y) {
  return {a: 0, b: 1, c: -y}
}

export function verticalLine(x) {
  return {a: 1, b: 0, c: -x}
}

export function linePassingThroughTwoPoints(x1, y1, x2, y2) {
  // (x2 - x1)(y - y1) = (y2 - y1)(x - x1)
  // (y1 - y2)x + (x2 - x1)y + (y2x1 - x2y1) = 0

  if (x1 === x2 && y1 == y2) throw new Error("Geometry error");
  if (x1 === x2) return verticalLine(x);
  if (y1 === y2) return horizontalLine(y1);

  return {
    a: y1 - y2,
    b: x2 - x1,
    c: y2 * x1 - x2 * y1
  };
}

export function distancePointFromLine(a, b, c, x, y) {
  //https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
  return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b);
}

export function closestPointFromLine(a, b, c, x, y) {
  //https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
  let denom = a * a + b * b;
  return {
    x: (b * (b * x - a * y) - a * c) / denom,
    y: ((a * -b * x + a * y ) - b * c) / denom,
  }
}

export function intersectionFromTwoLines(a, b, c, j, k, l) {
  let det = (b * j - a * k);

  if (det === 0) return undefined; //no intersection

  let y = (a * l - c * j) / det;
  let x = (c * k - b * l) / det;
  return {x, y};
}

export function intersectionFromTwoLineSegment(p1, p2, p3, p4) {
  //https://github.com/psalaets/line-intersect/blob/master/lib/check-intersection.js

  let {x: x1, y: y1} = p1;
  let {x: x2, y: y2} = p2;
  let {x: x3, y: y3} = p3;
  let {x: x4, y:y4} = p4;

  let EPSILON = 10e-6;

  let denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1));
  let numA = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3));
  let numB = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3));

  if (Math.abs(denom) <= EPSILON) {
    if (Math.abs(numA) <= EPSILON && Math.abs(numB) <= EPSILON) {

      let comparator = (pa, pb) => pa.x === pb.x ? pa.y - pb.y : pa.x - pb.x;
      let line0 = [p1, p2].sort(comparator);
      let line1 = [p3.toJS(), p4.toJS()].sort(comparator);

      let [lineSX, lineDX] = [line0, line1].sort((lineA, lineB) => comparator(lineA[0], lineB[0]));

      if (lineSX[1].x === lineDX[0].x) {
        return {type: (lineDX[0].y <= lineSX[1].y) ? "colinear" : "none"};
      } else {
        return {type: (lineDX[0].x <= lineSX[1].x) ? "colinear" : "none"};
      }
    }
    return {type: "parallel"};
  }

  var uA = numA / denom;
  var uB = numB / denom;

  if (uA >= (0 - EPSILON) && uA <= (1 + EPSILON) && uB >= (0 - EPSILON) && uB <= (1 + EPSILON)) {
    let point = {
      x: x1 + (uA * (x2 - x1)),
      y: y1 + (uA * (y2 - y1))
    };
    return {type: "intersecting", point};
  }

  return {type: "none"};
}

export function distancePointFromLineSegment(x1, y1, x2, y2, xp, yp) {
  //http://stackoverflow.com/a/6853926/1398836

  let A = xp - x1;
  let B = yp - y1;
  let C = x2 - x1;
  let D = y2 - y1;

  let dot = A * C + B * D;
  let len_sq = C * C + D * D;
  let param = -1;
  if (len_sq != 0) //in case of 0 length line
    param = dot / len_sq;

  let xx, yy;

  if (param < 0) {
    xx = x1;
    yy = y1;
  }
  else if (param > 1) {
    xx = x2;
    yy = y2;
  }
  else {
    xx = x1 + param * C;
    yy = y1 + param * D;
  }

  let dx = xp - xx;
  let dy = yp - yy;
  return Math.sqrt(dx * dx + dy * dy);
}

export function closestPointFromLineSegment(x1, y1, x2, y2, xp, yp) {
  if (x1 === x2) return {x: x1, y: yp};
  if (y1 === y2) return {x: xp, y: y1};

  let m = (y2 - y1) / (x2 - x1);
  let q = y1 - m * x1;

  let mi = -1 / m;
  let qi = yp - mi * xp;

  let x = (qi - q) / (m - mi);
  let y = (m * x + q);

  return {x, y};
}

export function pointPositionOnLineSegment(x1, y1, x2, y2, xp, yp) {
  let length = distanceFromTwoPoints(x1, y1, x2, y2);
  let distance = distanceFromTwoPoints(x1, y1, xp, yp);

  let offset = distance / length;
  if (x1 > x2) offset = mapRange(offset, 0, 1, 1, 0);

  return offset;
}

export function mapRange(value, low1, high1, low2, high2) {
  return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}

export function angleBetweenTwoPointsAndOrigin(x1, y1, x2, y2) {
  let length = distanceFromTwoPoints(x1, y1, x2, y2);
  return (-Math.asin((y1 - y2) / length)) * 180 / Math.PI;
}

export function samePoints({x:x1, y:y1}, {x:x2, y:y2}) {
  let EPSILON = 10e-6;
  return Math.abs(x1 - x2) <= EPSILON && Math.abs(y1 - y2) <= EPSILON;
}
<|endoftext|>


<commit_before>'use strict';

var _    = require('lodash');
var glob = require('glob');
var path = require('path');

var REPLACES,
    regex       = {},
    headerRegex = /^\s*\/\*((.|\r?\n)*?)\*/;

REPLACES = {
  'case_insensitive': 'cI',
  'lexemes': 'l',
  'contains': 'c',
  'keywords': 'k',
  'subLanguage': 'sL',
  'className': 'cN',
  'begin': 'b',
  'beginKeywords': 'bK',
  'end': 'e',
  'endsWithParent': 'eW',
  'illegal': 'i',
  'excludeBegin': 'eB',
  'excludeEnd': 'eE',
  'returnBegin': 'rB',
  'returnEnd': 'rE',
  'relevance': 'r',
  'variants': 'v',

  'IDENT_RE': 'IR',
  'UNDERSCORE_IDENT_RE': 'UIR',
  'NUMBER_RE': 'NR',
  'C_NUMBER_RE': 'CNR',
  'BINARY_NUMBER_RE': 'BNR',
  'RE_STARTERS_RE': 'RSR',
  'BACKSLASH_ESCAPE': 'BE',
  'APOS_STRING_MODE': 'ASM',
  'QUOTE_STRING_MODE': 'QSM',
  'PHRASAL_WORDS_MODE': 'PWM',
  'C_LINE_COMMENT_MODE': 'CLCM',
  'C_BLOCK_COMMENT_MODE': 'CBCM',
  'HASH_COMMENT_MODE': 'HCM',
  'NUMBER_MODE': 'NM',
  'C_NUMBER_MODE': 'CNM',
  'BINARY_NUMBER_MODE': 'BNM',
  'CSS_NUMBER_MODE': 'CSSNM',
  'REGEXP_MODE': 'RM',
  'TITLE_MODE': 'TM',
  'UNDERSCORE_TITLE_MODE': 'UTM',
  'COMMENT': 'C',

  'beginRe': 'bR',
  'endRe': 'eR',
  'illegalRe': 'iR',
  'lexemesRe': 'lR',
  'terminators': 't',
  'terminator_end': 'tE',
};

regex.replaces = new RegExp(
  '\\b(' + Object.keys(REPLACES).join('|') + ')\\b', 'g');

regex.classname = /(block|parentNode)\.cN/g;

regex.header = /^\s*(\/\*((.|\r?\n)*?)\*\/)?\s*/;

function replace(from, to) {
  return { regex: from, replace: to };
}

function replaceClassNames(match) {
  return REPLACES[match];
}

// All meta data, for each language definition, it store within the headers
// of each file in `src/languages`. `parseHeader` extracts that data and
// turns it into a useful object -- mainly for categories and what language
// this definition requires.
function parseHeader(content) {
  var headers,
      match = content.match(headerRegex);

  if (!match) {
    return null;
  }

  headers = _.compact(match[1].split('\n'));

  return _.foldl(headers, function(result, header) {
    var keyVal = header.trim().split(': '),
        key    = keyVal[0],
        value  = keyVal[1] || '';

    if(key !== 'Description' && key !== 'Language') {
      value = value.split(/\s*,\s*/);
    }

    result[key] = value;

    return result;
  }, {});
}

function filterByQualifiers(blob, languages, categories) {
  if(_.isEmpty(languages) && _.isEmpty(categories)) return true;

  var language         = path.basename(blob.name, '.js'),
      fileInfo         = parseHeader(blob.result),
      fileCategories   = fileInfo.Category || [],
      containsCategory = _.partial(_.contains, categories);

  if(!fileInfo) return false;

  return _.contains(languages, language) ||
         _.any(fileCategories, containsCategory);
}

// For the filter task in `tools/tasks.js`, this function will look for
// categories and languages specificed from the CLI.
function buildFilterCallback(qualifiers) {
  var isCategory = _.matchesProperty(0, ':'),
      languages  = _.reject(qualifiers, isCategory),
      categories = _(qualifiers).filter(isCategory)
                                .map(function(c) {return c.slice(1);})
                                .value();

  return _.partial(filterByQualifiers, _, languages, categories);
}

function globDefaults(pattern, encoding) {
  encoding = encoding || 'utf8';

  // The limit option is a fix for issue #636 when the build script would
  // EMFILE error for those systems who had a limit of open files per
  // process.
  //
  // <https://github.com/isagalaev/highlight.js/issues/636>
  return { pattern: pattern, limit: 50, encoding: encoding };
}

function getStyleNames(callback) {
  var stylesDir = 'src/styles/',
      options   = { ignore: stylesDir + 'default.css' };

  glob(stylesDir + '*.css', options, function(err, styles) {
    callback(err, _.map(styles, function(style) {
      var basename = path.basename(style, '.css'),
          name     = _.startCase(basename),
          pathName = path.relative('src', style);

      return { path: pathName, name: name };
    }));
  });
}

module.exports = {
  buildFilterCallback: buildFilterCallback,
  getStyleNames: getStyleNames,
  glob: globDefaults,
  parseHeader: parseHeader,
  regex: regex,
  replace: replace,
  replaceClassNames: replaceClassNames
};
<commit_msg>Change getStyleNames to return a promise<commit_after>'use strict';

var _        = require('lodash');
var bluebird = require('bluebird');
var glob     = bluebird.promisifyAll(require('glob')).globAsync;
var path     = require('path');

var REPLACES,
    regex       = {},
    headerRegex = /^\s*\/\*((.|\r?\n)*?)\*/;

REPLACES = {
  'case_insensitive': 'cI',
  'lexemes': 'l',
  'contains': 'c',
  'keywords': 'k',
  'subLanguage': 'sL',
  'className': 'cN',
  'begin': 'b',
  'beginKeywords': 'bK',
  'end': 'e',
  'endsWithParent': 'eW',
  'illegal': 'i',
  'excludeBegin': 'eB',
  'excludeEnd': 'eE',
  'returnBegin': 'rB',
  'returnEnd': 'rE',
  'relevance': 'r',
  'variants': 'v',

  'IDENT_RE': 'IR',
  'UNDERSCORE_IDENT_RE': 'UIR',
  'NUMBER_RE': 'NR',
  'C_NUMBER_RE': 'CNR',
  'BINARY_NUMBER_RE': 'BNR',
  'RE_STARTERS_RE': 'RSR',
  'BACKSLASH_ESCAPE': 'BE',
  'APOS_STRING_MODE': 'ASM',
  'QUOTE_STRING_MODE': 'QSM',
  'PHRASAL_WORDS_MODE': 'PWM',
  'C_LINE_COMMENT_MODE': 'CLCM',
  'C_BLOCK_COMMENT_MODE': 'CBCM',
  'HASH_COMMENT_MODE': 'HCM',
  'NUMBER_MODE': 'NM',
  'C_NUMBER_MODE': 'CNM',
  'BINARY_NUMBER_MODE': 'BNM',
  'CSS_NUMBER_MODE': 'CSSNM',
  'REGEXP_MODE': 'RM',
  'TITLE_MODE': 'TM',
  'UNDERSCORE_TITLE_MODE': 'UTM',
  'COMMENT': 'C',

  'beginRe': 'bR',
  'endRe': 'eR',
  'illegalRe': 'iR',
  'lexemesRe': 'lR',
  'terminators': 't',
  'terminator_end': 'tE',
};

regex.replaces = new RegExp(
  '\\b(' + Object.keys(REPLACES).join('|') + ')\\b', 'g');

regex.classname = /(block|parentNode)\.cN/g;

regex.header = /^\s*(\/\*((.|\r?\n)*?)\*\/)?\s*/;

function replace(from, to) {
  return { regex: from, replace: to };
}

function replaceClassNames(match) {
  return REPLACES[match];
}

// All meta data, for each language definition, it store within the headers
// of each file in `src/languages`. `parseHeader` extracts that data and
// turns it into a useful object -- mainly for categories and what language
// this definition requires.
function parseHeader(content) {
  var headers,
      match = content.match(headerRegex);

  if (!match) {
    return null;
  }

  headers = _.compact(match[1].split('\n'));

  return _.foldl(headers, function(result, header) {
    var keyVal = header.trim().split(': '),
        key    = keyVal[0],
        value  = keyVal[1] || '';

    if(key !== 'Description' && key !== 'Language') {
      value = value.split(/\s*,\s*/);
    }

    result[key] = value;

    return result;
  }, {});
}

function filterByQualifiers(blob, languages, categories) {
  if(_.isEmpty(languages) && _.isEmpty(categories)) return true;

  var language         = path.basename(blob.name, '.js'),
      fileInfo         = parseHeader(blob.result),
      fileCategories   = fileInfo.Category || [],
      containsCategory = _.partial(_.contains, categories);

  if(!fileInfo) return false;

  return _.contains(languages, language) ||
         _.any(fileCategories, containsCategory);
}

// For the filter task in `tools/tasks.js`, this function will look for
// categories and languages specificed from the CLI.
function buildFilterCallback(qualifiers) {
  var isCategory = _.matchesProperty(0, ':'),
      languages  = _.reject(qualifiers, isCategory),
      categories = _(qualifiers).filter(isCategory)
                                .map(function(c) {return c.slice(1);})
                                .value();

  return _.partial(filterByQualifiers, _, languages, categories);
}

function globDefaults(pattern, encoding) {
  encoding = encoding || 'utf8';

  // The limit option is a fix for issue #636 when the build script would
  // EMFILE error for those systems who had a limit of open files per
  // process.
  //
  // <https://github.com/isagalaev/highlight.js/issues/636>
  return { pattern: pattern, limit: 50, encoding: encoding };
}

function getStyleNames() {
  var stylesDir = 'src/styles/',
      options   = { ignore: stylesDir + 'default.css' };

  return glob(stylesDir + '*.css', options)
    .map(function(style) {
      var basename = path.basename(style, '.css'),
          name     = _.startCase(basename),
          pathName = path.relative('src', style);

      return { path: pathName, name: name };
    });
}

module.exports = {
  buildFilterCallback: buildFilterCallback,
  getStyleNames: getStyleNames,
  glob: globDefaults,
  parseHeader: parseHeader,
  regex: regex,
  replace: replace,
  replaceClassNames: replaceClassNames
};
<|endoftext|>


<commit_before>export const serialize = obj => {

    const keys = Object.keys(obj);

    if (keys.length === 0) return '';

    const pairs = keys.sort((a, b) => a.localeCompare(b)).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`);

    return '?' + pairs.join('&');

};

export const deserialize = str => {

    const pairs = str.slice(1).split('&');

    const obj = pairs.reduce((map, pair) => {

        const [key, value] = pair.split('=').map(decodeURIComponent);

        map[key] = value;

        return map;

    }, {});

    return obj;

}
<commit_msg>split chained function calls in query-string utils<commit_after>export const serialize = obj => {

    const keys = Object.keys(obj);

    if (keys.length === 0) return '';

    const pairs = keys
        .sort((a, b) => a.localeCompare(b))
        .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`);

    return '?' + pairs.join('&');

};

export const deserialize = str => {

    const pairs = str.slice(1).split('&');

    const obj = pairs.reduce((map, pair) => {

        const [key, value] = pair.split('=').map(decodeURIComponent);

        map[key] = value;

        return map;

    }, {});

    return obj;

}
<|endoftext|>


<commit_before>/* eslint-disable no-param-reassign */
const sanitizeHtml = require('sanitize-html');

const maxDeep = 12;
// enable html for all current editors
const keys = ['content', 'text', 'comment', 'gradeComment', 'description'];
const paths = ['lessons', 'news', 'newsModel', 'homework', 'submissions'];
const saveKeys = ['password', 'secret'];
const allowedTags = ['h1', 'h2', 'h3', 'blockquote', 'p', 'a', 'ul', 'ol', 's', 'u', 'span', 'del',
	'li', 'b', 'i', 'img', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
	'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'audio', 'video', 'sub', 'sup'];
const allowedSchemes = ['http', 'https', 'ftp', 'mailto'];

// const allowedSchemesByTag = {
// 	// allow base64 image data
// 	img: ['data'],
// };

const allowedAttributes = {
	a: ['href', 'name', 'target'],
	img: ['src', 'width', 'height', 'alt'],
};

const htmlTrueOptions = {
	allowedTags,
	allowedAttributes, // allow all attributes of allowed tags
	allowedSchemes,
	// allowedSchemesByTag, // TODO enable this?
	parser: {
		decodeEntities: true,
	},
	allowedStyles: {
		'*': {
			// Match HEX and RGB
			color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/],
			'text-align': [/^left$/, /^right$/, /^center$/],
			// Match any number with px, em, or %
			'font-size': [/^\d+(?:px|em|%)$/],
			'font-style': [/^\w+$/],
		},
	},
};

const htmlFalseOptions = {
	allowedTags: [], // disallow all tags
	allowedAttributes: [], // disallow all attributes
	allowedSchemes: [], // disallow url schemes
	parser: {
		decodeEntities: true,
	},
};

/**
 * sanitizes data
 * @param {*} data
 * @param {*} param
 */
const sanitize = (data, { html = false }) => {
	// https://www.npmjs.com/package/sanitize-html
	if (html === true) {
		// editor-content data
		data = sanitizeHtml(data, htmlTrueOptions);
		data = data.replace(/(<script>).*?(<\/script>)/gim, ''); // force remove script tags
		data = data.replace(/(<script>).*?(<\/script>)/gim, ''); // force remove script tags
	} else {
		// non editor-content data
		data = sanitizeHtml(data, htmlFalseOptions);
	}
	return data;
};

/**
 * disables sanitization for defined keys if a path is matching
 * @param {*} path
 * @param {*} key
 */
const allowedHtmlByPathAndKeys = (path, key) => paths.includes(path) && keys.includes(key);

/**
 * Strips JS/HTML Code from data and returns clean version of it
 * @param data {object/array/string}
 * @param path {string}
 * @param depth {number} -
 * @param safeAttributes {array} - attributes over which sanitization won't be performed
 * @returns data - clean without JS
 */
const sanitizeDeep = (data, path, depth = 0, safeAttributes = []) => {
	if (depth >= maxDeep) {
		throw new Error('Data level is to deep. (sanitizeDeep)', { path, data });
	}
	if (typeof data === 'object' && data !== null) {
		// we have an object, can match strings or recurse child objects
		// eslint-disable-next-line consistent-return
		Object.entries(data).forEach(([key, value]) => {
			if (typeof value === 'string') {
				// ignore values completely
				if (saveKeys.includes(key) || safeAttributes.includes(key)) {
					return data; // TODO:  why not over keys in allowedHtmlByPathAndKeys
				}
				data[key] = sanitize(value, { html: allowedHtmlByPathAndKeys(path, key) });
			} else {
				sanitizeDeep(value, path, depth + 1, safeAttributes);
			}
		});
	} else if (typeof data === 'string') {
		// here we can sanitize the input
		data = sanitize(data, { html: false });
	} else if (Array.isArray(data)) {
		// here we have to check all array elements and sanitize strings or do recursion
		for (let i = 0; i < data.length; i += 1) {
			if (typeof data[i] === 'string') {
				data[i] = sanitize(data[i], { html: false });
			} else {
				sanitizeDeep(data[i], path, depth + 1, safeAttributes);
			}
		}
	}
	return data;
};

module.exports = {
	sanitizeDeep,
	sanitizeConsts: {
		keys,
		paths,
		saveKeys,
		allowedTags,
		allowedSchemes,
		maxDeep,
	},
};
<commit_msg>do not modify parameters<commit_after>/* eslint-disable no-param-reassign */
const sanitizeHtml = require('sanitize-html');

const maxDeep = 12;
// enable html for all current editors
const keys = ['content', 'text', 'comment', 'gradeComment', 'description'];
const paths = ['lessons', 'news', 'newsModel', 'homework', 'submissions'];
const saveKeys = ['password', 'secret'];
const allowedTags = ['h1', 'h2', 'h3', 'blockquote', 'p', 'a', 'ul', 'ol', 's', 'u', 'span', 'del',
	'li', 'b', 'i', 'img', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
	'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'audio', 'video', 'sub', 'sup'];
const allowedSchemes = ['http', 'https', 'ftp', 'mailto'];

// const allowedSchemesByTag = {
// 	// allow base64 image data
// 	img: ['data'],
// };

const allowedAttributes = {
	a: ['href', 'name', 'target'],
	img: ['src', 'width', 'height', 'alt'],
};

const htmlTrueOptions = {
	allowedTags,
	allowedAttributes, // allow all attributes of allowed tags
	allowedSchemes,
	// allowedSchemesByTag, // TODO enable this?
	parser: {
		decodeEntities: true,
	},
	allowedStyles: {
		'*': {
			// Match HEX and RGB
			color: [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/],
			'text-align': [/^left$/, /^right$/, /^center$/],
			// Match any number with px, em, or %
			'font-size': [/^\d+(?:px|em|%)$/],
			'font-style': [/^\w+$/],
		},
	},
};

const htmlFalseOptions = {
	allowedTags: [], // disallow all tags
	allowedAttributes: [], // disallow all attributes
	allowedSchemes: [], // disallow url schemes
	parser: {
		decodeEntities: true,
	},
};

/**
 * sanitizes data
 * @param {*} data
 * @param {*} param
 */
const sanitize = (data, { html = false }) => {
	let retValue = null;
	// https://www.npmjs.com/package/sanitize-html
	if (html === true) {
		// editor-content data
		retValue = sanitizeHtml(data, htmlTrueOptions);
		// TODO handle following lines in sanitizeHtml
		retValue = retValue.replace(/(<script>).*?(<\/script>)/gim, ''); // force remove script tags
		retValue = retValue.replace(/(<script>).*?(<\/script>)/gim, ''); // force remove script tags
	} else {
		// non editor-content data
		retValue = sanitizeHtml(data, htmlFalseOptions);
	}
	return retValue;
};

/**
 * disables sanitization for defined keys if a path is matching
 * @param {*} path
 * @param {*} key
 */
const allowedHtmlByPathAndKeys = (path, key) => paths.includes(path) && keys.includes(key);

/**
 * Strips JS/HTML Code from data and returns clean version of it
 * @param data {object/array/string}
 * @param path {string}
 * @param depth {number} -
 * @param safeAttributes {array} - attributes over which sanitization won't be performed
 * @returns data - clean without JS
 */
const sanitizeDeep = (data, path, depth = 0, safeAttributes = []) => {
	if (depth >= maxDeep) {
		throw new Error('Data level is to deep. (sanitizeDeep)', { path, data });
	}
	if (typeof data === 'object' && data !== null) {
		// we have an object, can match strings or recurse child objects
		// eslint-disable-next-line consistent-return
		Object.entries(data).forEach(([key, value]) => {
			if (typeof value === 'string') {
				// ignore values completely
				if (saveKeys.includes(key) || safeAttributes.includes(key)) {
					return data; // TODO:  why not over keys in allowedHtmlByPathAndKeys
				}
				data[key] = sanitize(value, { html: allowedHtmlByPathAndKeys(path, key) });
			} else {
				sanitizeDeep(value, path, depth + 1, safeAttributes);
			}
		});
	} else if (typeof data === 'string') {
		// here we can sanitize the input
		data = sanitize(data, { html: false });
	} else if (Array.isArray(data)) {
		// here we have to check all array elements and sanitize strings or do recursion
		for (let i = 0; i < data.length; i += 1) {
			if (typeof data[i] === 'string') {
				data[i] = sanitize(data[i], { html: false });
			} else {
				sanitizeDeep(data[i], path, depth + 1, safeAttributes);
			}
		}
	}
	return data;
};

module.exports = {
	sanitizeDeep,
	sanitizeConsts: {
		keys,
		paths,
		saveKeys,
		allowedTags,
		allowedSchemes,
		maxDeep,
	},
};
<|endoftext|>