<commit_before>const dispatch = require('../dispatch/central-dispatch');
const log = require('../util/log');
const maybeFormatMessage = require('../util/maybe-format-message');

const BlockType = require('./block-type');

// These extensions are currently built into the VM repository but should not be loaded at startup.
// TODO: move these out into a separate repository?
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods

const builtinExtensions = {
    // This is an example that isn't loaded with the other core blocks,
    // but serves as a reference for loading core blocks as extensions.
    coreExample: () => require('../blocks/scratch3_core_example'),
    // These are the non-core built-in extensions.
    pen: () => require('../extensions/scratch3_pen'),
    wedo2: () => require('../extensions/scratch3_wedo2'),
    music: () => require('../extensions/scratch3_music'),
    microbit: () => require('../extensions/scratch3_microbit'),
    text2speech: () => require('../extensions/scratch3_text2speech'),
    translate: () => require('../extensions/scratch3_translate'),
    videoSensing: () => require('../extensions/scratch3_video_sensing'),
    ev3: () => require('../extensions/scratch3_ev3'),
    makeymakey: () => require('../extensions/scratch3_makeymakey'),
    boost: () => require('../extensions/scratch3_boost'),
    gdxfor: () => require('../extensions/scratch3_gdx_for'),
    synth: () => require('../extensions/scratch3_synth'),
    lightplay: () => require('../extensions/scratch3_lightplay'),
};

/**
 * @typedef {object} ArgumentInfo - Information about an extension block argument
 * @property {ArgumentType} type - the type of value this argument can take
 * @property {*|undefined} default - the default value of this argument (default: blank)
 */

/**
 * @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks
 * @property {ExtensionBlockMetadata} info - the raw block info
 * @property {object} json - the scratch-blocks JSON definition for this block
 * @property {string} xml - the scratch-blocks XML definition for this block
 */

/**
 * @typedef {object} CategoryInfo - Information about a block category
 * @property {string} id - the unique ID of this category
 * @property {string} name - the human-readable name of this category
 * @property {string|undefined} blockIconURI - optional URI for the block icon image
 * @property {string} color1 - the primary color for this category, in '#rrggbb' format
 * @property {string} color2 - the secondary color for this category, in '#rrggbb' format
 * @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
 * @property {Array.<ConvertedBlockInfo>} blocks - the blocks, separators, etc. in this category
 * @property {Array.<object>} menus - the menus provided by this category
 */

/**
 * @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
 * @property {string} extensionURL - the URL of the extension to be loaded by this worker
 * @property {Function} resolve - function to call on successful worker startup
 * @property {Function} reject - function to call on failed worker startup
 */

class ExtensionManager {
    constructor (runtime) {
        /**
         * The ID number to provide to the next extension worker.
         * @type {int}
         */
        this.nextExtensionWorker = 0;

        /**
         * FIFO queue of extensions which have been requested but not yet loaded in a worker,
         * along with promise resolution functions to call once the worker is ready or failed.
         *
         * @type {Array.<PendingExtensionWorker>}
         */
        this.pendingExtensions = [];

        /**
         * Map of worker ID to workers which have been allocated but have not yet finished initialization.
         * @type {Array.<PendingExtensionWorker>}
         */
        this.pendingWorkers = [];

        /**
         * Set of loaded extension URLs/IDs (equivalent for built-in extensions).
         * @type {Set.<string>}
         * @private
         */
        this._loadedExtensions = new Map();

        /**
         * Keep a reference to the runtime so we can construct internal extension objects.
         * TODO: remove this in favor of extensions accessing the runtime as a service.
         * @type {Runtime}
         */
        this.runtime = runtime;

        dispatch.setService('extensions', this).catch(e => {
            log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
        });
    }

    /**
     * Check whether an extension is registered or is in the process of loading. This is intended to control loading or
     * adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
     * `loadExtensionURL` if you need to wait until the extension is truly ready.
     * @param {string} extensionID - the ID of the extension.
     * @returns {boolean} - true if loaded, false otherwise.
     */
    isExtensionLoaded (extensionID) {
        return this._loadedExtensions.has(extensionID);
    }

    /**
     * Synchronously load an internal extension (core or non-core) by ID. This call will
     * fail if the provided id is not does not match an internal extension.
     * @param {string} extensionId - the ID of an internal extension
     */
    loadExtensionIdSync (extensionId) {
        if (!builtinExtensions.hasOwnProperty(extensionId)) {
            log.warn(`Could not find extension ${extensionId} in the built in extensions.`);
            return;
        }

        /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
        if (this.isExtensionLoaded(extensionId)) {
            const message = `Rejecting attempt to load a second extension with ID ${extensionId}`;
            log.warn(message);
            return;
        }

        const extension = builtinExtensions[extensionId]();
        const extensionInstance = new extension(this.runtime);
        const serviceName = this._registerInternalExtension(extensionInstance);
        this._loadedExtensions.set(extensionId, serviceName);
    }

    /**
     * Load an extension by URL or internal extension ID
     * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
     * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
     */
    loadExtensionURL (extensionURL) {
        if (builtinExtensions.hasOwnProperty(extensionURL)) {
            /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
            if (this.isExtensionLoaded(extensionURL)) {
                const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
                log.warn(message);
                return Promise.resolve();
            }

            const extension = builtinExtensions[extensionURL]();
            const extensionInstance = new extension(this.runtime);
            const serviceName = this._registerInternalExtension(extensionInstance);
            this._loadedExtensions.set(extensionURL, serviceName);
            return Promise.resolve();
        }

        return new Promise((resolve, reject) => {
            // If we `require` this at the global level it breaks non-webpack targets, including tests
            const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker');

            this.pendingExtensions.push({extensionURL, resolve, reject});
            dispatch.addWorker(new ExtensionWorker());
        });
    }

    /**
     * Regenerate blockinfo for any loaded extensions
     * @returns {Promise} resolved once all the extensions have been reinitialized
     */
    refreshBlocks () {
        const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName =>
            dispatch.call(serviceName, 'getInfo')
                .then(info => {
                    info = this._prepareExtensionInfo(serviceName, info);
                    dispatch.call('runtime', '_refreshExtensionPrimitives', info);
                })
                .catch(e => {
                    log.error(`Failed to refresh built-in extension primitives: ${JSON.stringify(e)}`);
                })
        );
        return Promise.all(allPromises);
    }

    allocateWorker () {
        const id = this.nextExtensionWorker++;
        const workerInfo = this.pendingExtensions.shift();
        this.pendingWorkers[id] = workerInfo;
        return [id, workerInfo.extensionURL];
    }

    /**
     * Synchronously collect extension metadata from the specified service and begin the extension registration process.
     * @param {string} serviceName - the name of the service hosting the extension.
     */
    registerExtensionServiceSync (serviceName) {
        const info = dispatch.callSync(serviceName, 'getInfo');
        this._registerExtensionInfo(serviceName, info);
    }

    /**
     * Collect extension metadata from the specified service and begin the extension registration process.
     * @param {string} serviceName - the name of the service hosting the extension.
     */
    registerExtensionService (serviceName) {
        dispatch.call(serviceName, 'getInfo').then(info => {
            this._registerExtensionInfo(serviceName, info);
        });
    }

    /**
     * Called by an extension worker to indicate that the worker has finished initialization.
     * @param {int} id - the worker ID.
     * @param {*?} e - the error encountered during initialization, if any.
     */
    onWorkerInit (id, e) {
        const workerInfo = this.pendingWorkers[id];
        delete this.pendingWorkers[id];
        if (e) {
            workerInfo.reject(e);
        } else {
            workerInfo.resolve(id);
        }
    }

    /**
     * Register an internal (non-Worker) extension object
     * @param {object} extensionObject - the extension object to register
     * @returns {string} The name of the registered extension service
     */
    _registerInternalExtension (extensionObject) {
        const extensionInfo = extensionObject.getInfo();
        const fakeWorkerId = this.nextExtensionWorker++;
        const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
        dispatch.setServiceSync(serviceName, extensionObject);
        dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName);
        return serviceName;
    }

    /**
     * Sanitize extension info then register its primitives with the VM.
     * @param {string} serviceName - the name of the service hosting the extension
     * @param {ExtensionInfo} extensionInfo - the extension's metadata
     * @private
     */
    _registerExtensionInfo (serviceName, extensionInfo) {
        extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
        dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
            log.error(`Failed to register primitives for extension on service ${serviceName}:`, e);
        });
    }

    /**
     * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
     * @param {string} text - the text to be sanitized
     * @returns {string} - the sanitized text
     * @private
     */
    _sanitizeID (text) {
        return text.toString().replace(/[<"&]/, '_');
    }

    /**
     * Apply minor cleanup and defaults for optional extension fields.
     * TODO: make the ID unique in cases where two copies of the same extension are loaded.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
     * @returns {ExtensionInfo} - a new extension info object with cleaned-up values
     * @private
     */
    _prepareExtensionInfo (serviceName, extensionInfo) {
        extensionInfo = Object.assign({}, extensionInfo);
        if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) {
            throw new Error('Invalid extension id');
        }
        extensionInfo.name = extensionInfo.name || extensionInfo.id;
        extensionInfo.blocks = extensionInfo.blocks || [];
        extensionInfo.targetTypes = extensionInfo.targetTypes || [];
        extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => {
            try {
                let result;
                switch (blockInfo) {
                case '---': // separator
                    result = '---';
                    break;
                default: // an ExtensionBlockMetadata object
                    result = this._prepareBlockInfo(serviceName, blockInfo);
                    break;
                }
                results.push(result);
            } catch (e) {
                // TODO: more meaningful error reporting
                log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
            }
            return results;
        }, []);
        extensionInfo.menus = extensionInfo.menus || [];
        extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
        return extensionInfo;
    }

    /**
     * Prepare extension menus. e.g. setup binding for dynamic menu functions.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {Array.<MenuInfo>} menus - the menu defined by the extension.
     * @returns {Array.<MenuInfo>} - a menuInfo object with all preprocessing done.
     * @private
     */
    _prepareMenuInfo (serviceName, menus) {
        const menuNames = Object.getOwnPropertyNames(menus);
        for (let i = 0; i < menuNames.length; i++) {
            const item = menuNames[i];
            // If the value is a string, it should be the name of a function in the
            // extension object to call to populate the menu whenever it is opened.
            // Set up the binding for the function object here so
            // we can use it later when converting the menu for Scratch Blocks.
            if (typeof menus[item] === 'string') {
                const serviceObject = dispatch.services[serviceName];
                const menuName = menus[item];
                menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName);
            }
        }
        return menus;
    }

    /**
     * Fetch the items for a particular extension menu, providing the target ID for context.
     * @param {object} extensionObject - the extension object providing the menu.
     * @param {string} menuName - the name of the menu function to call.
     * @returns {Array} menu items ready for scratch-blocks.
     * @private
     */
    _getExtensionMenuItems (extensionObject, menuName) {
        // Fetch the items appropriate for the target currently being edited. This assumes that menus only
        // collect items when opened by the user while editing a particular target.
        const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
        const editingTargetID = editingTarget ? editingTarget.id : null;
        const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);

        // TODO: Fix this to use dispatch.call when extensions are running in workers.
        const menuFunc = extensionObject[menuName];
        const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
            item => {
                item = maybeFormatMessage(item, extensionMessageContext);
                switch (typeof item) {
                case 'object':
                    return [
                        maybeFormatMessage(item.text, extensionMessageContext),
                        item.value
                    ];
                case 'string':
                    return [item, item];
                default:
                    return item;
                }
            });

        if (!menuItems || menuItems.length < 1) {
            throw new Error(`Extension menu returned no items: ${menuName}`);
        }
        return menuItems;
    }

    /**
     * Apply defaults for optional block fields.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension
     * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields.
     * @private
     */
    _prepareBlockInfo (serviceName, blockInfo) {
        blockInfo = Object.assign({}, {
            blockType: BlockType.COMMAND,
            terminal: false,
            blockAllThreads: false,
            arguments: {}
        }, blockInfo);
        blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode);
        blockInfo.text = blockInfo.text || blockInfo.opcode;

        switch (blockInfo.blockType) {
        case BlockType.EVENT:
            if (blockInfo.func) {
                log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
            }
            break;
        case BlockType.BUTTON:
            if (blockInfo.opcode) {
                log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
            }
            break;
        default:
            if (!blockInfo.opcode) {
                throw new Error('Missing opcode for block');
            }

            blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;

            // Avoid promise overhead if possible
            if (dispatch._isRemoteService(serviceName)) {
                blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
            } else {
                const serviceObject = dispatch.services[serviceName];
                const func = serviceObject[blockInfo.func];
                if (func) {
                    blockInfo.func = func.bind(serviceObject);
                } else {
                    throw new Error(`Could not find extension block function called ${blockInfo.func}`);
                }
            }
            break;
        }

        return blockInfo;
    }
}

module.exports = ExtensionManager;
<commit_msg>Revert "lightplay extension stub"<commit_after>const dispatch = require('../dispatch/central-dispatch');
const log = require('../util/log');
const maybeFormatMessage = require('../util/maybe-format-message');

const BlockType = require('./block-type');

// These extensions are currently built into the VM repository but should not be loaded at startup.
// TODO: move these out into a separate repository?
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods

const builtinExtensions = {
    // This is an example that isn't loaded with the other core blocks,
    // but serves as a reference for loading core blocks as extensions.
    coreExample: () => require('../blocks/scratch3_core_example'),
    // These are the non-core built-in extensions.
    pen: () => require('../extensions/scratch3_pen'),
    wedo2: () => require('../extensions/scratch3_wedo2'),
    music: () => require('../extensions/scratch3_music'),
    microbit: () => require('../extensions/scratch3_microbit'),
    text2speech: () => require('../extensions/scratch3_text2speech'),
    translate: () => require('../extensions/scratch3_translate'),
    videoSensing: () => require('../extensions/scratch3_video_sensing'),
    ev3: () => require('../extensions/scratch3_ev3'),
    makeymakey: () => require('../extensions/scratch3_makeymakey'),
    boost: () => require('../extensions/scratch3_boost'),
    gdxfor: () => require('../extensions/scratch3_gdx_for'),
    synth: () => require('../extensions/scratch3_synth'),
};

/**
 * @typedef {object} ArgumentInfo - Information about an extension block argument
 * @property {ArgumentType} type - the type of value this argument can take
 * @property {*|undefined} default - the default value of this argument (default: blank)
 */

/**
 * @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks
 * @property {ExtensionBlockMetadata} info - the raw block info
 * @property {object} json - the scratch-blocks JSON definition for this block
 * @property {string} xml - the scratch-blocks XML definition for this block
 */

/**
 * @typedef {object} CategoryInfo - Information about a block category
 * @property {string} id - the unique ID of this category
 * @property {string} name - the human-readable name of this category
 * @property {string|undefined} blockIconURI - optional URI for the block icon image
 * @property {string} color1 - the primary color for this category, in '#rrggbb' format
 * @property {string} color2 - the secondary color for this category, in '#rrggbb' format
 * @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
 * @property {Array.<ConvertedBlockInfo>} blocks - the blocks, separators, etc. in this category
 * @property {Array.<object>} menus - the menus provided by this category
 */

/**
 * @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
 * @property {string} extensionURL - the URL of the extension to be loaded by this worker
 * @property {Function} resolve - function to call on successful worker startup
 * @property {Function} reject - function to call on failed worker startup
 */

class ExtensionManager {
    constructor (runtime) {
        /**
         * The ID number to provide to the next extension worker.
         * @type {int}
         */
        this.nextExtensionWorker = 0;

        /**
         * FIFO queue of extensions which have been requested but not yet loaded in a worker,
         * along with promise resolution functions to call once the worker is ready or failed.
         *
         * @type {Array.<PendingExtensionWorker>}
         */
        this.pendingExtensions = [];

        /**
         * Map of worker ID to workers which have been allocated but have not yet finished initialization.
         * @type {Array.<PendingExtensionWorker>}
         */
        this.pendingWorkers = [];

        /**
         * Set of loaded extension URLs/IDs (equivalent for built-in extensions).
         * @type {Set.<string>}
         * @private
         */
        this._loadedExtensions = new Map();

        /**
         * Keep a reference to the runtime so we can construct internal extension objects.
         * TODO: remove this in favor of extensions accessing the runtime as a service.
         * @type {Runtime}
         */
        this.runtime = runtime;

        dispatch.setService('extensions', this).catch(e => {
            log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
        });
    }

    /**
     * Check whether an extension is registered or is in the process of loading. This is intended to control loading or
     * adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
     * `loadExtensionURL` if you need to wait until the extension is truly ready.
     * @param {string} extensionID - the ID of the extension.
     * @returns {boolean} - true if loaded, false otherwise.
     */
    isExtensionLoaded (extensionID) {
        return this._loadedExtensions.has(extensionID);
    }

    /**
     * Synchronously load an internal extension (core or non-core) by ID. This call will
     * fail if the provided id is not does not match an internal extension.
     * @param {string} extensionId - the ID of an internal extension
     */
    loadExtensionIdSync (extensionId) {
        if (!builtinExtensions.hasOwnProperty(extensionId)) {
            log.warn(`Could not find extension ${extensionId} in the built in extensions.`);
            return;
        }

        /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
        if (this.isExtensionLoaded(extensionId)) {
            const message = `Rejecting attempt to load a second extension with ID ${extensionId}`;
            log.warn(message);
            return;
        }

        const extension = builtinExtensions[extensionId]();
        const extensionInstance = new extension(this.runtime);
        const serviceName = this._registerInternalExtension(extensionInstance);
        this._loadedExtensions.set(extensionId, serviceName);
    }

    /**
     * Load an extension by URL or internal extension ID
     * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
     * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
     */
    loadExtensionURL (extensionURL) {
        if (builtinExtensions.hasOwnProperty(extensionURL)) {
            /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
            if (this.isExtensionLoaded(extensionURL)) {
                const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
                log.warn(message);
                return Promise.resolve();
            }

            const extension = builtinExtensions[extensionURL]();
            const extensionInstance = new extension(this.runtime);
            const serviceName = this._registerInternalExtension(extensionInstance);
            this._loadedExtensions.set(extensionURL, serviceName);
            return Promise.resolve();
        }

        return new Promise((resolve, reject) => {
            // If we `require` this at the global level it breaks non-webpack targets, including tests
            const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker');

            this.pendingExtensions.push({extensionURL, resolve, reject});
            dispatch.addWorker(new ExtensionWorker());
        });
    }

    /**
     * Regenerate blockinfo for any loaded extensions
     * @returns {Promise} resolved once all the extensions have been reinitialized
     */
    refreshBlocks () {
        const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName =>
            dispatch.call(serviceName, 'getInfo')
                .then(info => {
                    info = this._prepareExtensionInfo(serviceName, info);
                    dispatch.call('runtime', '_refreshExtensionPrimitives', info);
                })
                .catch(e => {
                    log.error(`Failed to refresh built-in extension primitives: ${JSON.stringify(e)}`);
                })
        );
        return Promise.all(allPromises);
    }

    allocateWorker () {
        const id = this.nextExtensionWorker++;
        const workerInfo = this.pendingExtensions.shift();
        this.pendingWorkers[id] = workerInfo;
        return [id, workerInfo.extensionURL];
    }

    /**
     * Synchronously collect extension metadata from the specified service and begin the extension registration process.
     * @param {string} serviceName - the name of the service hosting the extension.
     */
    registerExtensionServiceSync (serviceName) {
        const info = dispatch.callSync(serviceName, 'getInfo');
        this._registerExtensionInfo(serviceName, info);
    }

    /**
     * Collect extension metadata from the specified service and begin the extension registration process.
     * @param {string} serviceName - the name of the service hosting the extension.
     */
    registerExtensionService (serviceName) {
        dispatch.call(serviceName, 'getInfo').then(info => {
            this._registerExtensionInfo(serviceName, info);
        });
    }

    /**
     * Called by an extension worker to indicate that the worker has finished initialization.
     * @param {int} id - the worker ID.
     * @param {*?} e - the error encountered during initialization, if any.
     */
    onWorkerInit (id, e) {
        const workerInfo = this.pendingWorkers[id];
        delete this.pendingWorkers[id];
        if (e) {
            workerInfo.reject(e);
        } else {
            workerInfo.resolve(id);
        }
    }

    /**
     * Register an internal (non-Worker) extension object
     * @param {object} extensionObject - the extension object to register
     * @returns {string} The name of the registered extension service
     */
    _registerInternalExtension (extensionObject) {
        const extensionInfo = extensionObject.getInfo();
        const fakeWorkerId = this.nextExtensionWorker++;
        const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
        dispatch.setServiceSync(serviceName, extensionObject);
        dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName);
        return serviceName;
    }

    /**
     * Sanitize extension info then register its primitives with the VM.
     * @param {string} serviceName - the name of the service hosting the extension
     * @param {ExtensionInfo} extensionInfo - the extension's metadata
     * @private
     */
    _registerExtensionInfo (serviceName, extensionInfo) {
        extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
        dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
            log.error(`Failed to register primitives for extension on service ${serviceName}:`, e);
        });
    }

    /**
     * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
     * @param {string} text - the text to be sanitized
     * @returns {string} - the sanitized text
     * @private
     */
    _sanitizeID (text) {
        return text.toString().replace(/[<"&]/, '_');
    }

    /**
     * Apply minor cleanup and defaults for optional extension fields.
     * TODO: make the ID unique in cases where two copies of the same extension are loaded.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
     * @returns {ExtensionInfo} - a new extension info object with cleaned-up values
     * @private
     */
    _prepareExtensionInfo (serviceName, extensionInfo) {
        extensionInfo = Object.assign({}, extensionInfo);
        if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) {
            throw new Error('Invalid extension id');
        }
        extensionInfo.name = extensionInfo.name || extensionInfo.id;
        extensionInfo.blocks = extensionInfo.blocks || [];
        extensionInfo.targetTypes = extensionInfo.targetTypes || [];
        extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => {
            try {
                let result;
                switch (blockInfo) {
                case '---': // separator
                    result = '---';
                    break;
                default: // an ExtensionBlockMetadata object
                    result = this._prepareBlockInfo(serviceName, blockInfo);
                    break;
                }
                results.push(result);
            } catch (e) {
                // TODO: more meaningful error reporting
                log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
            }
            return results;
        }, []);
        extensionInfo.menus = extensionInfo.menus || [];
        extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
        return extensionInfo;
    }

    /**
     * Prepare extension menus. e.g. setup binding for dynamic menu functions.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {Array.<MenuInfo>} menus - the menu defined by the extension.
     * @returns {Array.<MenuInfo>} - a menuInfo object with all preprocessing done.
     * @private
     */
    _prepareMenuInfo (serviceName, menus) {
        const menuNames = Object.getOwnPropertyNames(menus);
        for (let i = 0; i < menuNames.length; i++) {
            const item = menuNames[i];
            // If the value is a string, it should be the name of a function in the
            // extension object to call to populate the menu whenever it is opened.
            // Set up the binding for the function object here so
            // we can use it later when converting the menu for Scratch Blocks.
            if (typeof menus[item] === 'string') {
                const serviceObject = dispatch.services[serviceName];
                const menuName = menus[item];
                menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName);
            }
        }
        return menus;
    }

    /**
     * Fetch the items for a particular extension menu, providing the target ID for context.
     * @param {object} extensionObject - the extension object providing the menu.
     * @param {string} menuName - the name of the menu function to call.
     * @returns {Array} menu items ready for scratch-blocks.
     * @private
     */
    _getExtensionMenuItems (extensionObject, menuName) {
        // Fetch the items appropriate for the target currently being edited. This assumes that menus only
        // collect items when opened by the user while editing a particular target.
        const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
        const editingTargetID = editingTarget ? editingTarget.id : null;
        const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);

        // TODO: Fix this to use dispatch.call when extensions are running in workers.
        const menuFunc = extensionObject[menuName];
        const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
            item => {
                item = maybeFormatMessage(item, extensionMessageContext);
                switch (typeof item) {
                case 'object':
                    return [
                        maybeFormatMessage(item.text, extensionMessageContext),
                        item.value
                    ];
                case 'string':
                    return [item, item];
                default:
                    return item;
                }
            });

        if (!menuItems || menuItems.length < 1) {
            throw new Error(`Extension menu returned no items: ${menuName}`);
        }
        return menuItems;
    }

    /**
     * Apply defaults for optional block fields.
     * @param {string} serviceName - the name of the service hosting this extension block
     * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension
     * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields.
     * @private
     */
    _prepareBlockInfo (serviceName, blockInfo) {
        blockInfo = Object.assign({}, {
            blockType: BlockType.COMMAND,
            terminal: false,
            blockAllThreads: false,
            arguments: {}
        }, blockInfo);
        blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode);
        blockInfo.text = blockInfo.text || blockInfo.opcode;

        switch (blockInfo.blockType) {
        case BlockType.EVENT:
            if (blockInfo.func) {
                log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
            }
            break;
        case BlockType.BUTTON:
            if (blockInfo.opcode) {
                log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
            }
            break;
        default:
            if (!blockInfo.opcode) {
                throw new Error('Missing opcode for block');
            }

            blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;

            // Avoid promise overhead if possible
            if (dispatch._isRemoteService(serviceName)) {
                blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
            } else {
                const serviceObject = dispatch.services[serviceName];
                const func = serviceObject[blockInfo.func];
                if (func) {
                    blockInfo.func = func.bind(serviceObject);
                } else {
                    throw new Error(`Could not find extension block function called ${blockInfo.func}`);
                }
            }
            break;
        }

        return blockInfo;
    }
}

module.exports = ExtensionManager;
<|endoftext|>


<commit_before>const { Telegram, Markup } = require('telegraf');
const crypto = require('crypto');
const querystring = require('querystring');
const fetch = require('node-fetch');
const songs = require('../songs');
const { findUserByIdAndUpdate, createSucceededMessage, createFailedMessage } = require('./dbmanager');
const { ADMIN_ID, SCROBBLERBOT_TOKEN } = require('../../config');

const telegram = new Telegram(SCROBBLERBOT_TOKEN);
const GLOBAL_KEYBOARD = Markup.keyboard([['🎵 Track', '💽 Album', '📃 Tracklist']]).resize().extra();

function sendToAdmin(text) {
  return telegram.sendMessage(ADMIN_ID, text);
}

function md5(text) {
  return crypto.createHash('md5').update(text, 'utf8').digest('hex');
}

function getRandomFavSong() {
  const index = Math.floor(Math.random() * songs.length);
  return songs[index];
}

async function error(ctx, e) {
  if (e.code === 400) { // message is not modified
    return;
  }

  console.log(e);
  const errText = '❗ Oops, something went wrong. Please try again later.';

  if (ctx.callbackQuery) {
    await ctx.editMessageText(errText);
  } else if (ctx.inlineQuery) {
    // pass
  } else {
    await ctx.reply(errText);
  }

  if (ctx.flow) await ctx.flow.leave();
  await sendToAdmin('❗ An error occured. Check the logs...');
}

function utf8(text) {
  return decodeURI(decodeURIComponent(text));
}

async function successfulScrobble(ctx, text = '✅ Success!', tracks = []) {
  await findUserByIdAndUpdate(ctx.from.id, {
    $inc: { scrobbles: 1 },
    username: ctx.from.username,
    last_scrobble: new Date(),
    album: {},
    track: {},
  });

  const extra = Markup.inlineKeyboard([
    Markup.callbackButton('Repeat', 'REPEAT'),
  ]).extra();

  let message;

  if (ctx.callbackQuery) {
    message = await ctx.editMessageText(text, extra);
  } else if (ctx.flow.state.messageIdToEdit) {
    message = await ctx.telegram
      .editMessageText(ctx.chat.id, ctx.flow.state.messageIdToEdit, null, text, extra);
  } else {
    message = await ctx.reply(text, extra);
  }

  await createSucceededMessage(message.message_id, tracks);
  await ctx.flow.leave();
}

function canScrobble(ctx) {
  if (!ctx.user || Date.now() - +ctx.user.last_scrobble <= 30000) {
    return false;
  }

  return true;
}

function multipleArray(array = [], multipleTimes = 1) {
  let multipliedArray = [];

  if (multipleTimes > 1) {
    for (let i = 0; i < multipleTimes; i += 1) {
      multipliedArray = multipliedArray.concat(array);
    }

    return multipliedArray;
  }

  return array;
}

async function requestError(ctx, e) {
  if (e.response) {
    let json;

    try {
      json = await e.response.json();
    } catch (err) {
      console.log(e.response);
      await sendToAdmin(`Got strange response. Check the console:\n\n${e.message}`);
      return;
    }

    const err = json.error;

    if (err === 14 || err === 4 || err === 9) {
      const text = '❌ Access has not been granted. Please re-authenticate';

      if (ctx.callbackQuery) {
        await ctx.editMessageText(text);
      } else {
        await ctx.reply(text);
      }

      await ctx.flow.enter('auth');
    }
  }
}

async function scrobbleError(ctx, e, tracks = [], msg = '❌ Failed') {
  const extra = Markup.inlineKeyboard([
    Markup.callbackButton('Retry', 'RETRY'),
  ]).extra();

  let messageId;

  if (ctx.flow.state.messageIdToEdit) {
    messageId = ctx.flow.state.messageIdToEdit;
    await ctx.telegram.editMessageText(ctx.chat.id, messageId, null, msg, extra);
  } else if (ctx.callbackQuery) {
    messageId = ctx.callbackQuery.message.message_id;
    await ctx.editMessageText(msg, extra);
  } else {
    const res = await ctx.reply(msg, extra);
    messageId = res.message_id;
  }

  if (tracks.length) {
    await createFailedMessage(messageId, tracks);
  }

  await requestError(ctx, e);
  await ctx.flow.leave();
}

function isUserAuthorized(ctx) {
  return ctx.user && ctx.user.key;
}

function validateTrackDurations(tracks = []) {
  const defDur = 300;
  return tracks.map((track) => {
    let duration = 0;
    const td = track.duration;

    if (tracks.length === 1) {
      return Object.assign(track, { duration });
    }

    duration = typeof td === 'undefined' ? defDur : +td || defDur;
    return Object.assign(track, { duration });
  });
}

async function httpRequest(url = '', opts = {}) {
  const res = await fetch(url, opts);

  if (res.status !== 200) {
    const err = { message: res.statusText, response: res };
    throw err;
  }

  return res;
}

async function httpPost(url = '', data = {}, opts = {}) {
  const res = await httpRequest(url, Object.assign({
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: typeof data === 'string' ? data : querystring.stringify(data),
  }, opts));

  return res.json();
}

async function httpGet(url = '', opts = {}) {
  const res = await httpRequest(url, Object.assign({
    method: 'GET',
  }, opts));

  return res.json();
}

function getIgnoredTracksFromLastfmRes(res = {}) {
  if (!res.scrobbles) return [];
  const ignored = [];
  const scrobbles = res.scrobbles.scrobble;

  if (Array.isArray(scrobbles)) {
    scrobbles.filter(scr => scr.ignoredMessage.code === '1')
      .forEach(scr => ignored.push(scr));
  } else if (scrobbles.ignoredMessage.code === '1') {
    ignored.push(scrobbles);
  }

  return ignored.map(track => ({
    artist: track.artist['#text'],
    name: track.track['#text'],
    album: track.album['#text'],
  }));
}

module.exports = {
  sendToAdmin,
  md5,
  getRandomFavSong,
  utf8,
  successfulScrobble,
  canScrobble,
  error,
  requestError,
  scrobbleError,
  isUserAuthorized,
  GLOBAL_KEYBOARD,
  multipleArray,
  validateTrackDurations,
  httpPost,
  httpGet,
  getIgnoredTracksFromLastfmRes,
};
<commit_msg>Add user agent header to all requests<commit_after>const { Telegram, Markup } = require('telegraf');
const crypto = require('crypto');
const querystring = require('querystring');
const fetch = require('node-fetch');
const songs = require('../songs');
const { findUserByIdAndUpdate, createSucceededMessage, createFailedMessage } = require('./dbmanager');
const { ADMIN_ID, SCROBBLERBOT_TOKEN } = require('../../config');

const telegram = new Telegram(SCROBBLERBOT_TOKEN);
const GLOBAL_KEYBOARD = Markup.keyboard([['🎵 Track', '💽 Album', '📃 Tracklist']]).resize().extra();

function sendToAdmin(text) {
  return telegram.sendMessage(ADMIN_ID, text);
}

function md5(text) {
  return crypto.createHash('md5').update(text, 'utf8').digest('hex');
}

function getRandomFavSong() {
  const index = Math.floor(Math.random() * songs.length);
  return songs[index];
}

async function error(ctx, e) {
  if (e.code === 400) { // message is not modified
    return;
  }

  console.log(e);
  const errText = '❗ Oops, something went wrong. Please try again later.';

  if (ctx.callbackQuery) {
    await ctx.editMessageText(errText);
  } else if (ctx.inlineQuery) {
    // pass
  } else {
    await ctx.reply(errText);
  }

  if (ctx.flow) await ctx.flow.leave();
  await sendToAdmin('❗ An error occured. Check the logs...');
}

function utf8(text) {
  return decodeURI(decodeURIComponent(text));
}

async function successfulScrobble(ctx, text = '✅ Success!', tracks = []) {
  await findUserByIdAndUpdate(ctx.from.id, {
    $inc: { scrobbles: 1 },
    username: ctx.from.username,
    last_scrobble: new Date(),
    album: {},
    track: {},
  });

  const extra = Markup.inlineKeyboard([
    Markup.callbackButton('Repeat', 'REPEAT'),
  ]).extra();

  let message;

  if (ctx.callbackQuery) {
    message = await ctx.editMessageText(text, extra);
  } else if (ctx.flow.state.messageIdToEdit) {
    message = await ctx.telegram
      .editMessageText(ctx.chat.id, ctx.flow.state.messageIdToEdit, null, text, extra);
  } else {
    message = await ctx.reply(text, extra);
  }

  await createSucceededMessage(message.message_id, tracks);
  await ctx.flow.leave();
}

function canScrobble(ctx) {
  if (!ctx.user || Date.now() - +ctx.user.last_scrobble <= 30000) {
    return false;
  }

  return true;
}

function multipleArray(array = [], multipleTimes = 1) {
  let multipliedArray = [];

  if (multipleTimes > 1) {
    for (let i = 0; i < multipleTimes; i += 1) {
      multipliedArray = multipliedArray.concat(array);
    }

    return multipliedArray;
  }

  return array;
}

async function requestError(ctx, e) {
  if (e.response) {
    let json;

    try {
      json = await e.response.json();
    } catch (err) {
      console.log(e.response);
      await sendToAdmin(`Got strange response. Check the console:\n\n${e.message}`);
      return;
    }

    const err = json.error;

    if (err === 14 || err === 4 || err === 9) {
      const text = '❌ Access has not been granted. Please re-authenticate';

      if (ctx.callbackQuery) {
        await ctx.editMessageText(text);
      } else {
        await ctx.reply(text);
      }

      await ctx.flow.enter('auth');
    }
  }
}

async function scrobbleError(ctx, e, tracks = [], msg = '❌ Failed') {
  const extra = Markup.inlineKeyboard([
    Markup.callbackButton('Retry', 'RETRY'),
  ]).extra();

  let messageId;

  if (ctx.flow.state.messageIdToEdit) {
    messageId = ctx.flow.state.messageIdToEdit;
    await ctx.telegram.editMessageText(ctx.chat.id, messageId, null, msg, extra);
  } else if (ctx.callbackQuery) {
    messageId = ctx.callbackQuery.message.message_id;
    await ctx.editMessageText(msg, extra);
  } else {
    const res = await ctx.reply(msg, extra);
    messageId = res.message_id;
  }

  if (tracks.length) {
    await createFailedMessage(messageId, tracks);
  }

  await requestError(ctx, e);
  await ctx.flow.leave();
}

function isUserAuthorized(ctx) {
  return ctx.user && ctx.user.key;
}

function validateTrackDurations(tracks = []) {
  const defDur = 300;
  return tracks.map((track) => {
    let duration = 0;
    const td = track.duration;

    if (tracks.length === 1) {
      return Object.assign(track, { duration });
    }

    duration = typeof td === 'undefined' ? defDur : +td || defDur;
    return Object.assign(track, { duration });
  });
}

async function httpRequest(url = '', options = {}) {
  const opts = Object.assign({}, options);
  opts.headers = Object.assign(opts.headers || {}, {
    'User-Agent': 'telegram@scrobblerBot/1.0.0 (+https://github.com/drvirtuozov/scrobblerBot)',
  });
  const res = await fetch(url, opts);

  if (res.status !== 200) {
    const err = { message: res.statusText, response: res };
    throw err;
  }

  return res;
}

async function httpPost(url = '', data = {}, opts = {}) {
  const res = await httpRequest(url, Object.assign({
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: typeof data === 'string' ? data : querystring.stringify(data),
  }, opts));

  return res.json();
}

async function httpGet(url = '', opts = {}) {
  const res = await httpRequest(url, Object.assign({
    method: 'GET',
  }, opts));

  return res.json();
}

function getIgnoredTracksFromLastfmRes(res = {}) {
  if (!res.scrobbles) return [];
  const ignored = [];
  const scrobbles = res.scrobbles.scrobble;

  if (Array.isArray(scrobbles)) {
    scrobbles.filter(scr => scr.ignoredMessage.code === '1')
      .forEach(scr => ignored.push(scr));
  } else if (scrobbles.ignoredMessage.code === '1') {
    ignored.push(scrobbles);
  }

  return ignored.map(track => ({
    artist: track.artist['#text'],
    name: track.track['#text'],
    album: track.album['#text'],
  }));
}

module.exports = {
  sendToAdmin,
  md5,
  getRandomFavSong,
  utf8,
  successfulScrobble,
  canScrobble,
  error,
  requestError,
  scrobbleError,
  isUserAuthorized,
  GLOBAL_KEYBOARD,
  multipleArray,
  validateTrackDurations,
  httpPost,
  httpGet,
  getIgnoredTracksFromLastfmRes,
};
<|endoftext|>


<commit_before>'use strict';
const path = require('path');
const yeoman = require('yeoman-generator');
const slug = require('mollusc');
const normalizeUrl = require('normalize-url');
const humanizeUrl = require('humanize-url');
const filter = require('gulp-filter');
const rename = require('gulp-rename');

let _props = {};

module.exports = yeoman.Base.extend({
  prompting() {
    const prompts = [{
      name: 'pluginName',
      message: 'What do you want to name your plugin?'
    }, {
      name: 'pluginDescription',
      message: 'What does your plugin do?'
    }, {
      name: 'pluginWebsite',
      message: 'What is the URL of the plugin\'s website?',
      validate: x => x.length > 0 ? true : 'You have to provide a website URL',
      filter: x => normalizeUrl(x)
    }, {
      name: 'authorName',
      message: 'What is your name?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide a name'
    }, {
      name: 'authorEmail',
      message: 'What is your email?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide an email'
    }, {
      name: 'authorWebsite',
      message: 'What is the URL of your website?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide a website URL',
      filter: x => normalizeUrl(x)
    }, {
      name: 'isSeparated',
      message: 'Separate admin and public functionality?',
      type: 'confirm',
      default: false
    }, {
      name: 'usesAutoloader',
      message: 'Would you like to use Composer\'s autoloader?',
      type: 'confirm',
      default: false
    }];

    return this.prompt(prompts)
      .then(props => {
        _props.plugin = {
          name: {
            human: props.pluginName,
            fileName: slug(props.pluginName, {
              lower: true
            }),
            className: slug(props.pluginName, {
              lower: false,
              replacement: '_'
            }),
            instanceName: slug(props.pluginName, {
              lower: true,
              replacement: '_'
            })
          },
          author: {
            name: props.authorName,
            email: props.authorEmail,
            website: humanizeUrl(props.authorWebsite)
          },
          description: props.pluginDescription,
          website: humanizeUrl(props.pluginWebsite),
          isSeparated: props.isSeparated,
          usesAutoloader: props.usesAutoloader
        };
      });
  },

  configuring() {
    const templateFilter = filter('**/*', {restore: true});

    this.registerTransformStream([
      templateFilter,
      rename(filePath => {
        filePath.basename = filePath.basename.replace('plugin-name', _props.plugin.name.fileName);
      }),
      templateFilter.restore
    ]);
  },

  writing() {
    this.fs.copyTpl(path.join(this.templatePath()), this.destinationPath(), _props);
  }
});
<commit_msg>allow admin and public functionality to be skipped<commit_after>'use strict';
const path = require('path');
const yeoman = require('yeoman-generator');
const slug = require('mollusc');
const normalizeUrl = require('normalize-url');
const humanizeUrl = require('humanize-url');
const filter = require('gulp-filter');
const rename = require('gulp-rename');

let _props = {};

module.exports = yeoman.Base.extend({
  prompting() {
    const prompts = [{
      name: 'pluginName',
      message: 'What do you want to name your plugin?'
    }, {
      name: 'pluginDescription',
      message: 'What does your plugin do?'
    }, {
      name: 'pluginWebsite',
      message: 'What is the URL of the plugin\'s website?',
      validate: x => x.length > 0 ? true : 'You have to provide a website URL',
      filter: x => normalizeUrl(x)
    }, {
      name: 'authorName',
      message: 'What is your name?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide a name'
    }, {
      name: 'authorEmail',
      message: 'What is your email?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide an email'
    }, {
      name: 'authorWebsite',
      message: 'What is the URL of your website?',
      store: true,
      validate: x => x.length > 0 ? true : 'You have to provide a website URL',
      filter: x => normalizeUrl(x)
    }, {
      name: 'isSeparated',
      message: 'Separate admin and public functionality?',
      type: 'confirm',
      default: false
    }, {
      name: 'usesAutoloader',
      message: 'Would you like to use Composer\'s autoloader?',
      type: 'confirm',
      default: false
    }];

    return this.prompt(prompts)
      .then(props => {
        _props.plugin = {
          name: {
            human: props.pluginName,
            fileName: slug(props.pluginName, {
              lower: true
            }),
            className: slug(props.pluginName, {
              lower: false,
              replacement: '_'
            }),
            instanceName: slug(props.pluginName, {
              lower: true,
              replacement: '_'
            })
          },
          author: {
            name: props.authorName,
            email: props.authorEmail,
            website: humanizeUrl(props.authorWebsite)
          },
          description: props.pluginDescription,
          website: humanizeUrl(props.pluginWebsite),
          isSeparated: props.isSeparated,
          usesAutoloader: props.usesAutoloader
        };
      });
  },

  writing() {
    const filters = [];

    const adminFiles = _props.plugin.isSeparated ? `${this.templatePath()}/admin/**/*` : `!${this.templatePath()}/admin/**/*`;
    const publicFiles = _props.plugin.isSeparated ? `${this.templatePath()}/public/**/*` : `!${this.templatePath()}/public/**/*`;

    const templateFilter = filter('**/*', {restore: true});

    filters.push(templateFilter);
    filters.push(rename(filePath => {
      filePath.basename = filePath.basename.replace('plugin-name', _props.plugin.name.fileName);
    }));
    filters.push(templateFilter.restore);

    this.registerTransformStream(filters);

    this.fs.copyTpl([`${this.templatePath()}/**/*`, adminFiles, publicFiles], this.destinationPath(), _props);
  }
});
<|endoftext|>


<commit_before>import * as PIXI from 'pixi.js';
import {  mapEvents, utils } from '../../core/';
const { resize } = utils;

/*-----------------------
-------- PUBLIC ---------
-----------------------*/
/**
 * Simple fog of war works with hexagon sized holes around objects
 *
 * @namespace flatworld.extensions.fogOfWars
 * @class pixelizedMiniMap
 **/
const simpleFogOfWar = (function() {
  const maskSprite = new PIXI.Sprite(PIXI.Texture.EMPTY);
  const renderTexture = new PIXI.RenderTexture(new PIXI.BaseRenderTexture(resize.getWindowSize().x, resize.getWindowSize().y));
  const FoWOverlay = new PIXI.Graphics();
  let zoomLayer;
  let mapRenderer;
  let maskMovableContainer;
  let maskStageContainer;
  let FoWCB;
  let objectsForFoW;
  let color;
  let mapInstance;

  return {
    // These two are required by all plugins
    init,
    pluginName: 'simpleFogOfWar',

    activateFogOfWar,
    refreshFoW,
    getFoWObjectArray,
    calculateCorrectCoordinates,
    FOR_TESTS: {
      setObjectsForFoW: (o) => objectsForFoW = o
    }
  };
  /**
   * Ínitialize as a plugin. Done by the Flatworld class.
   *
   * After plugin has been initialized by the flatworld, you must still call activateFogOfWar to
   * start showing it.
   *
   * @todo the offsets are really bad! For some reason they are needed, I don't know where the
   * issue lies :(. We probably need an offset for the renderer in the end anyway, but now it
   * doesn't even work properly without them.
   * can be used here and in getViewportArea-method etc.
   *
   * @method init
   * @param  {Object} parameters    This plugin requires cb and filter properties!
   */
  function init(params) {
    if (!(params.cb && params.filter)) {
      throw new Error('SimpleFogOfWar plugin requires cb and filter properties')
    }
    mapInstance = this.mapInstance;
    zoomLayer = this._properties.zoomLayer;
    mapRenderer = this.mapInstance.getRenderer();

    maskStageContainer = this.mapInstance.createSpecialLayer('FoWStageMaskLayer');
    /* This could use particleContainer. But particleContainer + drawRect graphics have a conflict. In our case, when we zoom far enough
     * and there is a lot of sprites on the screen, the particleContainer + drawRect gives a webGL error "glDrawElements: range out of
     * bounds for buffer". So we must use container for now.
     */
    maskMovableContainer = new PIXI.Container();
    maskMovableContainer.position = mapInstance.getMapCoordinates(undefined, true);

    activateFogOfWar(this.mapInstance, params.cb, params.filter, params.getOptions ? params.getOptions() : {});

    return Promise.resolve();
  }

  function activateFogOfWar(mapInstance, cb, filterCreator, options = {}) {
    color = options.color || 0x222222;
    FoWCB = cb;
    const filter = filterCreator();
    // Get layers with filters. So it filters layers and objects
    objectsForFoW = mapInstance.getPrimaryLayers({ filters: filter }).map(o => o.getObjects(filter));
    objectsForFoW = utils.general.flatten2Levels(objectsForFoW);

    createOverlay();

    setupFoW();
    setEvents();
  }

  function setupFoW() {
    const spriteArray = getFoWObjectArray(FoWCB);

    resetFoW(FoWOverlay);

    if (spriteArray.length > 0) {
      maskMovableContainer.addChild(...spriteArray);
    }

    resizeFoW();

    zoomLayer.mask = maskSprite;

    mapInstance.registerPreRenderer('renderFoW', moveFoW);

    resizeFoW();
  }

  function refreshFoW() {
    /*    var t0 = performance.now();*/
    mapRenderer.render(maskStageContainer, renderTexture, true, null, false);

    maskSprite.texture = renderTexture;
    /*    var t1 = performance.now();
    console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")*/
  }

  function moveFoW() {
    maskMovableContainer.position = mapInstance.getMapCoordinates(undefined, true);

    refreshFoW();
  }

  function zoomFoW() {
    maskStageContainer.scale.x = mapInstance.getZoom();
    maskStageContainer.scale.y = mapInstance.getZoom();

    createOverlay();
    refreshFoW();
  }

  function resizeFoW() {
    changeMaskSize();
    createOverlay();
    refreshFoW();
  }

  function changeMaskSize() {
    resize.resizePIXIRenderer(
      renderTexture,
      () => {}
    );

    //renderTexture.resize(resize.getWindowSize().x, resize.getWindowSize().y);
    maskSprite.width = resize.getWindowSize().x;
    maskSprite.height = resize.getWindowSize().y;
    /* MaskSprites bounds create a filterArea to the zoomLayer as the maskSprite is a mask for it. This causes resizing not to work
     * correctly, if the bounds are not changed and bounds are not updated without this call (for some reason just width and height change
     * is not enough).
     */
    maskSprite.getBounds();
  }

  function getFoWObjectArray(cb) {
    return objectsForFoW.map(object => cb(calculateCorrectCoordinates(object)));
  }

  function calculateCorrectCoordinates(object) {
    const coordinates = object.toGlobal(new PIXI.Point(0, 0));

    coordinates.x = Math.round(coordinates.x);
    coordinates.y = Math.round(coordinates.y);
    coordinates.anchor = object.anchor;
    coordinates.pivot = object.pivot;
    coordinates.scale = mapInstance.getZoom();

    return coordinates;
  }

  /** *************************************
  **************** PRIVATE ****************
  ****************************************/
  function resetFoW() {
    maskMovableContainer.children && maskMovableContainer.removeChildren();
    maskStageContainer.children && maskStageContainer.removeChildren();
    maskStageContainer.addChild(FoWOverlay);
    maskStageContainer.addChild(maskMovableContainer);
  }

  function createOverlay() {
    const coordinates = {
      x: -100,
      y: -100,
      width: mapRenderer.width + 200 + (mapRenderer.width / mapInstance.getZoom()),
      height: mapRenderer.height + 200 + (mapRenderer.height / mapInstance.getZoom())
    };

    FoWOverlay.clear();
    FoWOverlay.beginFill(color);
    FoWOverlay.drawRect(coordinates.x, coordinates.y, coordinates.width, coordinates.height);
    FoWOverlay.endFill();
  }
  function setEvents() {
    mapEvents.subscribe('mapResized', resizeFoW);
    mapEvents.subscribe('mapZoomed', zoomFoW);
    /* mapEvents.subscribe('mapMoved', moveFoW); */
  }
})();

/*-----------------------
---------- API ----------
-----------------------*/
export default simpleFogOfWar;
<commit_msg>Feat: Added object to fog of wars callback and additional data<commit_after>import * as PIXI from 'pixi.js';
import {  mapEvents, utils } from '../../core/';
const { resize } = utils;

/*-----------------------
-------- PUBLIC ---------
-----------------------*/
/**
 * Simple fog of war works with hexagon sized holes around objects
 *
 * @namespace flatworld.extensions.fogOfWars
 * @class pixelizedMiniMap
 **/
const simpleFogOfWar = (function() {
  const maskSprite = new PIXI.Sprite(PIXI.Texture.EMPTY);
  const renderTexture = new PIXI.RenderTexture(new PIXI.BaseRenderTexture(resize.getWindowSize().x, resize.getWindowSize().y));
  const FoWOverlay = new PIXI.Graphics();
  let zoomLayer;
  let mapRenderer;
  let maskMovableContainer;
  let maskStageContainer;
  let FoWCB;
  let objectsForFoW;
  let color;
  let mapInstance;

  return {
    // These two are required by all plugins
    init,
    pluginName: 'simpleFogOfWar',

    activateFogOfWar,
    refreshFoW,
    getFoWObjectArray,
    calculateCorrectCoordinates,
    FOR_TESTS: {
      setObjectsForFoW: (o) => objectsForFoW = o
    }
  };
  /**
   * Ínitialize as a plugin. Done by the Flatworld class.
   *
   * After plugin has been initialized by the flatworld, you must still call activateFogOfWar to
   * start showing it.
   *
   * @todo the offsets are really bad! For some reason they are needed, I don't know where the
   * issue lies :(. We probably need an offset for the renderer in the end anyway, but now it
   * doesn't even work properly without them.
   * can be used here and in getViewportArea-method etc.
   *
   * @method init
   * @param  {Object} parameters    This plugin requires cb and filter properties!
   */
  function init(params) {
    if (!(params.cb && params.filter)) {
      throw new Error('SimpleFogOfWar plugin requires cb and filter properties')
    }
    mapInstance = this.mapInstance;
    zoomLayer = this._properties.zoomLayer;
    mapRenderer = this.mapInstance.getRenderer();

    maskStageContainer = this.mapInstance.createSpecialLayer('FoWStageMaskLayer');
    /* This could use particleContainer. But particleContainer + drawRect graphics have a conflict. In our case, when we zoom far enough
     * and there is a lot of sprites on the screen, the particleContainer + drawRect gives a webGL error "glDrawElements: range out of
     * bounds for buffer". So we must use container for now.
     */
    maskMovableContainer = new PIXI.Container();
    maskMovableContainer.position = mapInstance.getMapCoordinates(undefined, true);

    activateFogOfWar(this.mapInstance, params.cb, params.filter, params.getOptions ? params.getOptions() : {});

    return Promise.resolve();
  }

  function activateFogOfWar(mapInstance, cb, filterCreator, options = {}) {
    color = options.color || 0x222222;
    FoWCB = cb;
    const filter = filterCreator();
    // Get layers with filters. So it filters layers and objects
    objectsForFoW = mapInstance.getPrimaryLayers({ filters: filter }).map(o => o.getObjects(filter));
    objectsForFoW = utils.general.flatten2Levels(objectsForFoW);

    createOverlay();

    setupFoW();
    setEvents();
  }

  function setupFoW() {
    const spriteArray = getFoWObjectArray(FoWCB);

    resetFoW(FoWOverlay);

    if (spriteArray.length > 0) {
      maskMovableContainer.addChild(...spriteArray);
    }

    resizeFoW();

    zoomLayer.mask = maskSprite;

    mapInstance.registerPreRenderer('renderFoW', moveFoW);

    resizeFoW();
  }

  function refreshFoW() {
    /*    var t0 = performance.now();*/
    mapRenderer.render(maskStageContainer, renderTexture, true, null, false);

    maskSprite.texture = renderTexture;
    /*    var t1 = performance.now();
    console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")*/
  }

  function moveFoW() {
    maskMovableContainer.position = mapInstance.getMapCoordinates(undefined, true);

    refreshFoW();
  }

  function zoomFoW() {
    maskStageContainer.scale.x = mapInstance.getZoom();
    maskStageContainer.scale.y = mapInstance.getZoom();

    createOverlay();
    refreshFoW();
  }

  function resizeFoW() {
    changeMaskSize();
    createOverlay();
    refreshFoW();
  }

  function changeMaskSize() {
    resize.resizePIXIRenderer(
      renderTexture,
      () => {}
    );

    //renderTexture.resize(resize.getWindowSize().x, resize.getWindowSize().y);
    maskSprite.width = resize.getWindowSize().x;
    maskSprite.height = resize.getWindowSize().y;
    /* MaskSprites bounds create a filterArea to the zoomLayer as the maskSprite is a mask for it. This causes resizing not to work
     * correctly, if the bounds are not changed and bounds are not updated without this call (for some reason just width and height change
     * is not enough).
     */
    maskSprite.getBounds();
  }

  function getFoWObjectArray(cb) {
    return objectsForFoW.map(object => cb(calculateCorrectCoordinates(object)));
  }

  function calculateCorrectCoordinates(object) {
    const coordinates = object.toGlobal(new PIXI.Point(0, 0));

    coordinates.x = Math.round(coordinates.x);
    coordinates.y = Math.round(coordinates.y);
    coordinates.anchor = object.anchor;
    coordinates.pivot = object.pivot;
    coordinates.scale = mapInstance.getZoom();
    coordinates.object = object;

    return coordinates;
  }

  /** *************************************
  **************** PRIVATE ****************
  ****************************************/
  function resetFoW() {
    maskMovableContainer.children && maskMovableContainer.removeChildren();
    maskStageContainer.children && maskStageContainer.removeChildren();
    maskStageContainer.addChild(FoWOverlay);
    maskStageContainer.addChild(maskMovableContainer);
  }

  function createOverlay() {
    const coordinates = {
      x: -100,
      y: -100,
      width: mapRenderer.width + 200 + (mapRenderer.width / mapInstance.getZoom()),
      height: mapRenderer.height + 200 + (mapRenderer.height / mapInstance.getZoom())
    };

    FoWOverlay.clear();
    FoWOverlay.beginFill(color);
    FoWOverlay.drawRect(coordinates.x, coordinates.y, coordinates.width, coordinates.height);
    FoWOverlay.endFill();
  }
  function setEvents() {
    mapEvents.subscribe('mapResized', resizeFoW);
    mapEvents.subscribe('mapZoomed', zoomFoW);
    /* mapEvents.subscribe('mapMoved', moveFoW); */
  }
})();

/*-----------------------
---------- API ----------
-----------------------*/
export default simpleFogOfWar;
<|endoftext|>


<commit_before>import { combineReducers } from 'redux';

function forSaleReducer (state =[], action) {
  return state
};

function forRentReducer (state = [], action) {

}

const rootReducer = combineReducers({
  forSale: forSaleReducer
});

export default rootReducer;
<commit_msg>create for rent and for sale reducers<commit_after>import { combineReducers } from 'redux';

function forSaleReducer (state =[], action) {
  return state
};

function forRentReducer (state = [], action) {
  return state
}

const rootReducer = combineReducers({
  forSale: forSaleReducer,
  forRent: forRentReducer
});

export default rootReducer;
<|endoftext|>


<commit_before>if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){
  module.exports = 'highcharts-ng';
}
(function () {
  'use strict';
  /*global angular: false, Highcharts: false */

  angular.module('highcharts-ng', [])
    .factory('highchartsNGUtils', highchartsNGUtils)
    .directive('highchart', ['highchartsNGUtils', highchart]);

  function highchartsNGUtils() {

    return {

      //IE8 support
      indexOf: function (arr, find, i /*opt*/) {
        if (i === undefined) i = 0;
        if (i < 0) i += arr.length;
        if (i < 0) i = 0;
        for (var n = arr.length; i < n; i++)
          if (i in arr && arr[i] === find)
            return i;
        return -1;
      },

      prependMethod: function (obj, method, func) {
        var original = obj[method];
        obj[method] = function () {
          var args = Array.prototype.slice.call(arguments);
          func.apply(this, args);
          if (original) {
            return original.apply(this, args);
          } else {
            return;
          }

        };
      },

      deepExtend: function deepExtend(destination, source) {
        //Slightly strange behaviour in edge cases (e.g. passing in non objects)
        //But does the job for current use cases.
        if (angular.isArray(source)) {
          destination = angular.isArray(destination) ? destination : [];
          for (var i = 0; i < source.length; i++) {
            destination[i] = deepExtend(destination[i] || {}, source[i]);
          }
        } else if (angular.isObject(source)) {
          for (var property in source) {
            destination[property] = deepExtend(destination[property] || {}, source[property]);
          }
        } else {
          destination = source;
        }
        return destination;
      }
    };
  }

  function highchart(highchartsNGUtils) {

    // acceptable shared state
    var seriesId = 0;
    var ensureIds = function (series) {
      var changed = false;
      angular.forEach(series, function(s) {
        if (!angular.isDefined(s.id)) {
          s.id = 'series-' + seriesId++;
          changed = true;
        }
      });
      return changed;
    };

    // immutable
    var axisNames = [ 'xAxis', 'yAxis' ];

    var getMergedOptions = function (scope, element, config) {
      var mergedOptions = {};

      var defaultOptions = {
        chart: {
          events: {}
        },
        title: {},
        subtitle: {},
        series: [],
        credits: {},
        plotOptions: {},
        navigator: {enabled: false}
      };

      if (config.options) {
        mergedOptions = highchartsNGUtils.deepExtend(defaultOptions, config.options);
      } else {
        mergedOptions = defaultOptions;
      }
      mergedOptions.chart.renderTo = element[0];

      angular.forEach(axisNames, function(axisName) {
        if(angular.isDefined(config[axisName])) {
          mergedOptions[axisName] = angular.copy(config[axisName]);

          if(angular.isDefined(config[axisName].currentMin) ||
              angular.isDefined(config[axisName].currentMax)) {

            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'selection', function(e){
              var thisChart = this;
              if (e[axisName]) {
                scope.$apply(function () {
                  scope.config[axisName].currentMin = e[axisName][0].min;
                  scope.config[axisName].currentMax = e[axisName][0].max;
                });
              } else {
                //handle reset button - zoom out to all
                scope.$apply(function () {
                  scope.config[axisName].currentMin = thisChart[axisName][0].dataMin;
                  scope.config[axisName].currentMax = thisChart[axisName][0].dataMax;
                });
              }
            });

            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'addSeries', function(e){
              scope.config[axisName].currentMin = this[axisName][0].min || scope.config[axisName].currentMin;
              scope.config[axisName].currentMax = this[axisName][0].max || scope.config[axisName].currentMax;
            });
          }
        }
      });

      if(config.title) {
        mergedOptions.title = config.title;
      }
      if (config.subtitle) {
        mergedOptions.subtitle = config.subtitle;
      }
      if (config.credits) {
        mergedOptions.credits = config.credits;
      }
      if(config.size) {
        if (config.size.width) {
          mergedOptions.chart.width = config.size.width;
        }
        if (config.size.height) {
          mergedOptions.chart.height = config.size.height;
        }
      }
      return mergedOptions;
    };

    var updateZoom = function (axis, modelAxis) {
      var extremes = axis.getExtremes();
      if(modelAxis.currentMin !== extremes.dataMin || modelAxis.currentMax !== extremes.dataMax) {
        axis.setExtremes(modelAxis.currentMin, modelAxis.currentMax, false);
      }
    };

    var processExtremes = function(chart, axis, axisName) {
      if(axis.currentMin || axis.currentMax) {
        chart[axisName][0].setExtremes(axis.currentMin, axis.currentMax, true);
      }
    };

    var chartOptionsWithoutEasyOptions = function (options) {
      return angular.extend({}, options, {data: null, visible: null});
    };

    return {
      restrict: 'EAC',
      replace: true,
      template: '

',
      scope: {
        config: '=',
        disableDataWatch: '='
      },
      link: function (scope, element, attrs) {
        // We keep some chart-specific variables here as a closure
        // instead of storing them on 'scope'.

        // prevSeriesOptions is maintained by processSeries
        var prevSeriesOptions = {};

        var processSeries = function(series) {
          var i;
          var ids = [];

          if(series) {
            var setIds = ensureIds(series);
            if(setIds) {
              //If we have set some ids this will trigger another digest cycle.
              //In this scenario just return early and let the next cycle take care of changes
              return false;
            }

            //Find series to add or update
            angular.forEach(series, function(s) {
              ids.push(s.id);
              var chartSeries = chart.get(s.id);
              if (chartSeries) {
                if (!angular.equals(prevSeriesOptions[s.id], chartOptionsWithoutEasyOptions(s))) {
                  chartSeries.update(angular.copy(s), false);
                } else {
                  if (s.visible !== undefined && chartSeries.visible !== s.visible) {
                    chartSeries.setVisible(s.visible, false);
                  }
                  chartSeries.setData(angular.copy(s.data), false);
                }
              } else {
                chart.addSeries(angular.copy(s), false);
              }
              prevSeriesOptions[s.id] = chartOptionsWithoutEasyOptions(s);
            });

            //  Shows no data text if all series are empty
            if(scope.config.noData) {
              var chartContainsData = false;

              for(i = 0; i < series.length; i++) {
                if (series[i].data && series[i].data.length > 0) {
                  chartContainsData = true;

                  break;
                }
              }

              if (!chartContainsData) {
                chart.showLoading(scope.config.noData);
              } else {
                chart.hideLoading();
              }
            }
          }

          //Now remove any missing series
          for(i = chart.series.length - 1; i >= 0; i--) {
            var s = chart.series[i];
            if (s.options.id !== 'highcharts-navigator-series' && highchartsNGUtils.indexOf(ids, s.options.id) < 0) {
              s.remove(false);
            }
          }

          return true;
        };

        // chart is maintained by initChart
        var chart = false;
        var initChart = function() {
          if (chart) chart.destroy();
          prevSeriesOptions = {};
          var config = scope.config || {};
          var mergedOptions = getMergedOptions(scope, element, config);
          var func = config.func || undefined;
          chart = config.useHighStocks ?
            new Highcharts.StockChart(mergedOptions, func) :
            new Highcharts.Chart(mergedOptions, func);
          for (var i = 0; i < axisNames.length; i++) {
            if (config[axisNames[i]]) {
              processExtremes(chart, config[axisNames[i]], axisNames[i]);
            }
          }
          if(config.loading) {
            chart.showLoading();
          }

        };
        initChart();

        if(scope.disableDataWatch){
          scope.$watchCollection('config.series', function (newSeries, oldSeries) {
            processSeries(newSeries);
            chart.redraw();
          });
        } else {
          scope.$watch('config.series', function (newSeries, oldSeries) {
            var needsRedraw = processSeries(newSeries);
            if(needsRedraw) {
              chart.redraw();
            }
          }, true);
        }

        scope.$watch('config.title', function (newTitle) {
          chart.setTitle(newTitle, true);
        }, true);

        scope.$watch('config.subtitle', function (newSubtitle) {
          chart.setTitle(true, newSubtitle);
        }, true);

        scope.$watch('config.loading', function (loading) {
          if(loading) {
            chart.showLoading();
          } else {
            chart.hideLoading();
          }
        });

        scope.$watch('config.credits.enabled', function (enabled) {
          if (enabled) {
            chart.credits.show();
          } else if (chart.credits) {
            chart.credits.hide();
          }
        });

        scope.$watch('config.useHighStocks', function (useHighStocks, oldUseHighStocks) {
          if(useHighStocks === oldUseHighStocks) return;
          initChart();
        });

        angular.forEach(axisNames, function(axisName) {
          scope.$watch('config.' + axisName, function (newAxes, oldAxes) {
            if (newAxes === oldAxes) return;
            if(newAxes) {
              chart[axisName][0].update(newAxes, false);
              updateZoom(chart[axisName][0], angular.copy(newAxes));
              chart.redraw();
            }
          }, true);
        });
        scope.$watch('config.options', function (newOptions, oldOptions, scope) {
          //do nothing when called on registration
          if (newOptions === oldOptions) return;
          initChart();
          processSeries(scope.config.series);
          chart.redraw();
        }, true);

        scope.$watch('config.size', function (newSize, oldSize) {
          if(newSize === oldSize) return;
          if(newSize) {
            chart.setSize(newSize.width || undefined, newSize.height || undefined);
          }
        }, true);

        scope.$on('highchartsng.reflow', function () {
          chart.reflow();
        });

        scope.$on('$destroy', function() {
          if (chart) {
            chart.destroy();
            setTimeout(function(){
              element.remove();
            }, 0);
          }
        });

      }
    };
  }

}());
<commit_msg>Remove double qoutes from strings<commit_after>if (typeof module !== 'undefined' && typeof exports !== 'undefined' && module.exports === exports){
  module.exports = 'highcharts-ng';
}
(function () {
  'use strict';
  /*global angular: false, Highcharts: false */

  angular.module('highcharts-ng', [])
    .factory('highchartsNGUtils', highchartsNGUtils)
    .directive('highchart', ['highchartsNGUtils', highchart]);

  function highchartsNGUtils() {

    return {

      //IE8 support
      indexOf: function (arr, find, i /*opt*/) {
        if (i === undefined) i = 0;
        if (i < 0) i += arr.length;
        if (i < 0) i = 0;
        for (var n = arr.length; i < n; i++)
          if (i in arr && arr[i] === find)
            return i;
        return -1;
      },

      prependMethod: function (obj, method, func) {
        var original = obj[method];
        obj[method] = function () {
          var args = Array.prototype.slice.call(arguments);
          func.apply(this, args);
          if (original) {
            return original.apply(this, args);
          } else {
            return;
          }

        };
      },

      deepExtend: function deepExtend(destination, source) {
        //Slightly strange behaviour in edge cases (e.g. passing in non objects)
        //But does the job for current use cases.
        if (angular.isArray(source)) {
          destination = angular.isArray(destination) ? destination : [];
          for (var i = 0; i < source.length; i++) {
            destination[i] = deepExtend(destination[i] || {}, source[i]);
          }
        } else if (angular.isObject(source)) {
          for (var property in source) {
            destination[property] = deepExtend(destination[property] || {}, source[property]);
          }
        } else {
          destination = source;
        }
        return destination;
      }
    };
  }

  function highchart(highchartsNGUtils) {

    // acceptable shared state
    var seriesId = 0;
    var ensureIds = function (series) {
      var changed = false;
      angular.forEach(series, function(s) {
        if (!angular.isDefined(s.id)) {
          s.id = 'series-' + seriesId++;
          changed = true;
        }
      });
      return changed;
    };

    // immutable
    var axisNames = [ 'xAxis', 'yAxis' ];

    var getMergedOptions = function (scope, element, config) {
      var mergedOptions = {};

      var defaultOptions = {
        chart: {
          events: {}
        },
        title: {},
        subtitle: {},
        series: [],
        credits: {},
        plotOptions: {},
        navigator: {enabled: false}
      };

      if (config.options) {
        mergedOptions = highchartsNGUtils.deepExtend(defaultOptions, config.options);
      } else {
        mergedOptions = defaultOptions;
      }
      mergedOptions.chart.renderTo = element[0];

      angular.forEach(axisNames, function(axisName) {
        if(angular.isDefined(config[axisName])) {
          mergedOptions[axisName] = angular.copy(config[axisName]);

          if(angular.isDefined(config[axisName].currentMin) ||
              angular.isDefined(config[axisName].currentMax)) {

            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'selection', function(e){
              var thisChart = this;
              if (e[axisName]) {
                scope.$apply(function () {
                  scope.config[axisName].currentMin = e[axisName][0].min;
                  scope.config[axisName].currentMax = e[axisName][0].max;
                });
              } else {
                //handle reset button - zoom out to all
                scope.$apply(function () {
                  scope.config[axisName].currentMin = thisChart[axisName][0].dataMin;
                  scope.config[axisName].currentMax = thisChart[axisName][0].dataMax;
                });
              }
            });

            highchartsNGUtils.prependMethod(mergedOptions.chart.events, 'addSeries', function(e){
              scope.config[axisName].currentMin = this[axisName][0].min || scope.config[axisName].currentMin;
              scope.config[axisName].currentMax = this[axisName][0].max || scope.config[axisName].currentMax;
            });
          }
        }
      });

      if(config.title) {
        mergedOptions.title = config.title;
      }
      if (config.subtitle) {
        mergedOptions.subtitle = config.subtitle;
      }
      if (config.credits) {
        mergedOptions.credits = config.credits;
      }
      if(config.size) {
        if (config.size.width) {
          mergedOptions.chart.width = config.size.width;
        }
        if (config.size.height) {
          mergedOptions.chart.height = config.size.height;
        }
      }
      return mergedOptions;
    };

    var updateZoom = function (axis, modelAxis) {
      var extremes = axis.getExtremes();
      if(modelAxis.currentMin !== extremes.dataMin || modelAxis.currentMax !== extremes.dataMax) {
        axis.setExtremes(modelAxis.currentMin, modelAxis.currentMax, false);
      }
    };

    var processExtremes = function(chart, axis, axisName) {
      if(axis.currentMin || axis.currentMax) {
        chart[axisName][0].setExtremes(axis.currentMin, axis.currentMax, true);
      }
    };

    var chartOptionsWithoutEasyOptions = function (options) {
      return angular.extend({}, options, {data: null, visible: null});
    };

    return {
      restrict: 'EAC',
      replace: true,
      template: '

',
      scope: {
        config: '=',
        disableDataWatch: '='
      },
      link: function (scope, element, attrs) {
        // We keep some chart-specific variables here as a closure
        // instead of storing them on 'scope'.

        // prevSeriesOptions is maintained by processSeries
        var prevSeriesOptions = {};

        var processSeries = function(series) {
          var i;
          var ids = [];

          if(series) {
            var setIds = ensureIds(series);
            if(setIds) {
              //If we have set some ids this will trigger another digest cycle.
              //In this scenario just return early and let the next cycle take care of changes
              return false;
            }

            //Find series to add or update
            angular.forEach(series, function(s) {
              ids.push(s.id);
              var chartSeries = chart.get(s.id);
              if (chartSeries) {
                if (!angular.equals(prevSeriesOptions[s.id], chartOptionsWithoutEasyOptions(s))) {
                  chartSeries.update(angular.copy(s), false);
                } else {
                  if (s.visible !== undefined && chartSeries.visible !== s.visible) {
                    chartSeries.setVisible(s.visible, false);
                  }
                  chartSeries.setData(angular.copy(s.data), false);
                }
              } else {
                chart.addSeries(angular.copy(s), false);
              }
              prevSeriesOptions[s.id] = chartOptionsWithoutEasyOptions(s);
            });

            //  Shows no data text if all series are empty
            if(scope.config.noData) {
              var chartContainsData = false;

              for(i = 0; i < series.length; i++) {
                if (series[i].data && series[i].data.length > 0) {
                  chartContainsData = true;

                  break;
                }
              }

              if (!chartContainsData) {
                chart.showLoading(scope.config.noData);
              } else {
                chart.hideLoading();
              }
            }
          }

          //Now remove any missing series
          for(i = chart.series.length - 1; i >= 0; i--) {
            var s = chart.series[i];
            if (s.options.id !== 'highcharts-navigator-series' && highchartsNGUtils.indexOf(ids, s.options.id) < 0) {
              s.remove(false);
            }
          }

          return true;
        };

        // chart is maintained by initChart
        var chart = false;
        var initChart = function() {
          if (chart) chart.destroy();
          prevSeriesOptions = {};
          var config = scope.config || {};
          var mergedOptions = getMergedOptions(scope, element, config);
          var func = config.func || undefined;
          chart = config.useHighStocks ?
            new Highcharts.StockChart(mergedOptions, func) :
            new Highcharts.Chart(mergedOptions, func);
          for (var i = 0; i < axisNames.length; i++) {
            if (config[axisNames[i]]) {
              processExtremes(chart, config[axisNames[i]], axisNames[i]);
            }
          }
          if(config.loading) {
            chart.showLoading();
          }

        };
        initChart();

        if(scope.disableDataWatch){
          scope.$watchCollection('config.series', function (newSeries, oldSeries) {
            processSeries(newSeries);
            chart.redraw();
          });
        } else {
          scope.$watch('config.series', function (newSeries, oldSeries) {
            var needsRedraw = processSeries(newSeries);
            if(needsRedraw) {
              chart.redraw();
            }
          }, true);
        }

        scope.$watch('config.title', function (newTitle) {
          chart.setTitle(newTitle, true);
        }, true);

        scope.$watch('config.subtitle', function (newSubtitle) {
          chart.setTitle(true, newSubtitle);
        }, true);

        scope.$watch('config.loading', function (loading) {
          if(loading) {
            chart.showLoading();
          } else {
            chart.hideLoading();
          }
        });

        scope.$watch('config.credits.enabled', function (enabled) {
          if (enabled) {
            chart.credits.show();
          } else if (chart.credits) {
            chart.credits.hide();
          }
        });

        scope.$watch('config.useHighStocks', function (useHighStocks, oldUseHighStocks) {
          if(useHighStocks === oldUseHighStocks) return;
          initChart();
        });

        angular.forEach(axisNames, function(axisName) {
          scope.$watch('config.' + axisName, function (newAxes, oldAxes) {
            if (newAxes === oldAxes) return;
            if(newAxes) {
              chart[axisName][0].update(newAxes, false);
              updateZoom(chart[axisName][0], angular.copy(newAxes));
              chart.redraw();
            }
          }, true);
        });
        scope.$watch('config.options', function (newOptions, oldOptions, scope) {
          //do nothing when called on registration
          if (newOptions === oldOptions) return;
          initChart();
          processSeries(scope.config.series);
          chart.redraw();
        }, true);

        scope.$watch('config.size', function (newSize, oldSize) {
          if(newSize === oldSize) return;
          if(newSize) {
            chart.setSize(newSize.width || undefined, newSize.height || undefined);
          }
        }, true);

        scope.$on('highchartsng.reflow', function () {
          chart.reflow();
        });

        scope.$on('$destroy', function() {
          if (chart) {
            chart.destroy();
            setTimeout(function(){
              element.remove();
            }, 0);
          }
        });

      }
    };
  }

}());
<|endoftext|>


<commit_before>/**
 * Created by julia on 07.11.2016.
 */
let Command = require('../../Objects/command');
let winston = require('winston');
let moment = require('moment');
class Say extends Command {
    constructor(t) {
        super();
        this.cmd = "say";
        this.cat = "fun";
        this.needGuild = false;
        this.t = t;
        this.accessLevel = 0;
    }

    run(msg) {
        let year = this.random(1990, 2016);
        let day = this.random(0, 366);
        let date = moment().year(year).dayOfYear(day);
        let dateFormat = date.format('YYYY-MM-DD');
        let dateYear = date.year();
        msg.channel.sendMessage(`https://d1ejxu6vysztl5.cloudfront.net/comics/garfield/${dateYear}/${dateFormat}.gif`).then(messageSent => {

        }).catch(winston.info);
    }

    random(min, max) {
        return Math.floor(Math.random() * (max - min) + min);
    }
}
module.exports = Say;<commit_msg>hotfix for the garfield cmd<commit_after>/**
 * Created by julia on 07.11.2016.
 */
let Command = require('../../Objects/command');
let winston = require('winston');
let moment = require('moment');
class Garfield extends Command {
    constructor(t) {
        super();
        this.cmd = "garfield";
        this.cat = "fun";
        this.needGuild = false;
        this.t = t;
        this.accessLevel = 0;
    }

    run(msg) {
        let year = this.random(1990, 2016);
        let day = this.random(0, 366);
        let date = moment().year(year).dayOfYear(day);
        let dateFormat = date.format('YYYY-MM-DD');
        let dateYear = date.year();
        msg.channel.sendMessage(`https://d1ejxu6vysztl5.cloudfront.net/comics/garfield/${dateYear}/${dateFormat}.gif`).then(messageSent => {

        }).catch(winston.info);
    }

    random(min, max) {
        return Math.floor(Math.random() * (max - min) + min);
    }
}
module.exports = Garfield;<|endoftext|>


<commit_before>// @flow
/* globals ReactClass */

import React from 'react'
import PropTypes from 'prop-types'
import hoistStatics from 'hoist-non-react-statics'
import { CHANNEL } from '../models/ThemeProvider'

const wrapWithTheme = (Component: ReactClass<any>) => {
  const componentName = (
    Component.displayName ||
    Component.name ||
    'Component'
  )

  class WithTheme extends React.Component {
    static displayName = `WithTheme(${componentName})`

    static contextTypes = {
      [CHANNEL]: PropTypes.func,
    };

    state: { theme?: ?Object } = {};
    unsubscribe: () => void;

    componentWillMount() {
      if (!this.context[CHANNEL]) {
        throw new Error('[withTheme] Please use ThemeProvider to be able to use withTheme')
      }

      const subscribe = this.context[CHANNEL]
      this.unsubscribe = subscribe(theme => {
        this.setState({ theme })
      })
    }

    componentWillUnmount() {
      if (typeof this.unsubscribe === 'function') this.unsubscribe()
    }

    render() {
      const { theme } = this.state

      return <Component theme={theme} {...this.props} />
    }
  }

  return hoistStatics(WithTheme, Component)
}

export default wrapWithTheme
<commit_msg>Add innerRef support to withTheme HOC<commit_after>// @flow
/* globals ReactClass */

import React from 'react'
import PropTypes from 'prop-types'
import hoistStatics from 'hoist-non-react-statics'
import { CHANNEL } from '../models/ThemeProvider'
import isStyledComponent from '../utils/isStyledComponent'

const wrapWithTheme = (Component: ReactClass<any>) => {
  const componentName = (
    Component.displayName ||
    Component.name ||
    'Component'
  )

  const _isStyledComponent = isStyledComponent(Component)

  class WithTheme extends React.Component {
    static displayName = `WithTheme(${componentName})`

    // NOTE: This is so that isStyledComponent passes for the innerRef unwrapping
    static styledComponentId = 'withTheme'

    static contextTypes = {
      [CHANNEL]: PropTypes.func,
    };

    state: { theme?: ?Object } = {};
    unsubscribe: () => void;

    componentWillMount() {
      if (!this.context[CHANNEL]) {
        throw new Error('[withTheme] Please use ThemeProvider to be able to use withTheme')
      }

      const subscribe = this.context[CHANNEL]
      this.unsubscribe = subscribe(theme => {
        this.setState({ theme })
      })
    }

    componentWillUnmount() {
      if (typeof this.unsubscribe === 'function') this.unsubscribe()
    }

    render() {
      const { innerRef } = this.props
      const { theme } = this.state

      return (
        <Component
          theme={theme}
          {...this.props}
          innerRef={_isStyledComponent ? innerRef : undefined}
          ref={_isStyledComponent ? undefined : innerRef}
        />
    }
  }

  return hoistStatics(WithTheme, Component)
}

export default wrapWithTheme
<|endoftext|>