<commit_before>import fs   from 'fs';
import glob from 'glob';
import url  from 'url';
import yml  from 'yamljs';
import chokidar from 'chokidar';

import express    from 'express';
import https      from 'https';
import bodyParser from 'body-parser';
import cors       from 'cors';

// for log
const red   = '\u001b[31m';
const green = '\u001b[32m';
const cyab  = '\u001b[36m';
const reset = '\u001b[0m';

//------------------------------------------------------------------------------
// utilities
//------------------------------------------------------------------------------
const privates = {
    //------------------------
    // related to express
    //------------------------

    /**
     * get request parameters
     *
     * @param {Request} req request object from express
     * @return {object}
     */
    getParams : (req) => {
        if (req.method === 'GET') {
            return req.query;
        }
        return req.body;
    },

    /**
     * normal output (200)
     *
     * @param {Response} res  response object from express
     * @param {object}   data output data object
     */
    outputJson : (res, data) => {
        res.writeHead(data.status, { 'Content-Type' : 'application/json; charset=utf-8' });
        res.end(JSON.stringify(data.body));
    },

    /**
     * not found output (404)
     *
     * @param {Response} res  response object from express
     */
    outputNotFound : (res) => {
        res.writeHead(404);
        res.end('Not Found');
    },

    /**
     * error output (500)
     *
     * @param {Response} res  response object from express
     */
    outputError : (res) => {
        res.writeHead(500);
        res.end('Server Error!');
    },

    //------------------------
    // for searching
    //------------------------

    /**
     * is subset
     * these values assumed to be string, since object is http request params.
     *
     * @param {object} whole expect whole set object
     * @param {object} part  expect subset object
     * @return {boolean}
     */
    isSubsetObject : (whole, part) => {
        // wild card
        if (part === '*' && typeof whole !== 'undefined') {
            return true;
        }
        if (typeof whole !== typeof part) {
            return false;
        }
        if (Array.isArray(part)) {
            return part.sort().every(
                (val, index) => privates.isSubsetObject(whole[index], val),
            );
        } else if (part instanceof Object) {
            return Object.keys(part).every(
                key => privates.isSubsetObject(whole[key], part[key]),
            );
        }
        // enum string
        if (privates.isPlaceholder(String(part))) {
            const enums = part.slice(1, -1).split('|');
            return enums.findIndex(v => (v === whole)) !== -1;
        }
        // another string
        return whole === part;
    },

    /**
     * is placeholder string
     *
     * @param {string} str
     * @return {boolean}
     */
    isPlaceholder : str => (str.substr(0, 1) === '{' && str.substr(-1) === '}'),

    /**
     * compare path and get requested routing params
     *
     * @param {string} stubPath path that was set to files
     * @param {string} reqPath  path that was requested
     * @return {array}
     *   0 : {boolean} is matched
     *   1 : {object}  requested routing params
     */
    isMatchingPathAndExtractParams : (stubPath, reqPath) => {
        const stubDirs  = stubPath.split('/');
        const reqDirs   = reqPath.split('/');
        let reqParams   = {};
        let isMatch;
        if (stubDirs.length === reqDirs.length) {
            isMatch = stubDirs.every((stubDir, j) => {
                const reqDir = reqDirs[j];
                if (privates.isPlaceholder(stubDir)) {
                    reqParams[stubDir.slice(1, -1)] = reqDir;
                    return true;
                } else if (stubDir === reqDir) {
                    return true;
                }
                // no match
                reqParams = {};
                return false;
            });
        } else {
            isMatch = false;
        }

        return [isMatch, reqParams];
    },

    //------------------------
    // others
    //------------------------

    parsers : [
        // YAML
        {
            extension : 'yml',
            parse     : file => yml.load(file),
        },
        // JSON
        {
            extension : 'json',
            parse     : file => JSON.parse(fs.readFileSync(file, 'utf-8')),
        },
    ],

    /**
     * Read files under a specified directory
     *
     * @param {string} directory string indicating the directory
     * @return {object}
     */
    loadFiles : (directory) => {
        const data = {};

        privates.parsers.forEach((typeInfo) => {
            const files = glob.sync(`${directory}/*.${typeInfo.extension}`);
            files.forEach((file) => {
                data[file] = {};
                const parsed = typeInfo.parse(file);
                Object.keys(parsed).forEach((key) => {
                    data[file][key] = parsed[key];
                });
            });
        });

        return data;
    },
};

//------------------------------------------------------------------------------
// main
//------------------------------------------------------------------------------
/**
 * Stubon
 *
 * @prop {object}  app   express object
 * @prop {object}  stubs stub setting object
 * @prop {boolean} debug detailed log flag
 * @prop {boolean} ssl   ssl flag
 */
class Stubon {

    /**
     * constructor
     *
     * @param {string}  directory             setting file directory
     * @param {object}  options
     * @param {boolean} [options.debug=false] detailed log flag
     * @param {boolean} [options.ssl=false]   https mode
     */
    constructor(directory, options = {}) {
        this.debug = options.debug || false;
        this.ssl   = options.ssl || false;
        this.stubs = privates.loadFiles(directory);
        this.app   = express();

        // watch files
        chokidar.watch(directory, { persistent : true })
            .on('change', () => {
                this.log('\n----- stub files changed -----');
                this.stubs = privates.loadFiles(directory);
            });
    }

    /**
     * start server
     */
    server() {
        // to be able to receive request params on POST method.
        this.app.use(bodyParser.urlencoded({ extended : true }));
        this.app.use(bodyParser.json());

        // allow cors
        this.app.use(cors());

        // set routing
        this.app.all('*', (req, res) => {
            this.log('\n----- recieve request -----');
            this.router(req, res);
        });

        // set ssl (optional)
        if (this.ssl) {
            this.app = https.createServer({
                key  : fs.readFileSync(`${__dirname}/../ssl/server.key`),
                cert : fs.readFileSync(`${__dirname}/../ssl/server.crt`),
            }, this.app);
        }

        return this.app;
    }

    /**
     * router
     *
     * @param {object} req request object
     * @param {object} res respons object
     */
    router(req, res) {
        const {
            getParams,
            isMatchingPathAndExtractParams,
            isSubsetObject,
            outputJson,
            outputNotFound,
            outputError,
        } = privates;
        try {
            const reqPath    = decodeURI(url.parse(req.url).pathname);
            const reqQueries = getParams(req);

            this.log([
                { path : reqPath },
                { method : req.method },
                { query : reqQueries },
                { header : JSON.stringify(req.headers) },
            ], true);

            const isFound = Object.keys(this.stubs).some((file) => {
                this.log(`file: ${file}`, true);
                return Object.keys(this.stubs[file]).some((stubPath) => {
                    // check path
                    this.log(`  path: ${stubPath}`, true);
                    const [isMatch, reqParams] = isMatchingPathAndExtractParams(stubPath, reqPath);
                    if (!isMatch) {
                        return false;
                    }

                    this.log('  matched.', true);
                    return this.stubs[file][stubPath].some((setting, i) => {
                        this.log(`    index: ${i}`, true);
                        const exp = setting.request;

                        // check params
                        if ((!exp.method || exp.method === req.method)
                            && (!exp.params || isSubsetObject(reqParams, exp.params))
                            && (!exp.queries || isSubsetObject(reqQueries, exp.queries))
                            && (!exp.headers || isSubsetObject(req.headers, exp.headers))
                        ) {
                            this.log(`>> ${green}match!${reset} ${file} ${stubPath} [${i}]`);
                            const response = this.stubs[file][stubPath][i].response;
                            const options  = this.stubs[file][stubPath][i].options || {};
                            const lagSec   = parseInt(options.lagSec, 10) || 0;
                            setTimeout(() => outputJson(res, response), lagSec * 1000);
                            return true;
                        }

                        return false;
                    });
                });
            });
            if (!isFound) {
                this.log(`>> ${cyab}not found${reset}`);
                outputNotFound(res);
            }
        } catch (e) {
            this.log(`>> ${red}error${reset}`);
            this.log(e);
            outputError(res);
        }
    }

    /**
     * logger
     *
     * @param {string}  msg                message
     * @param {boolean} [isDebugLog=false] detailed log flag
     */
    log(msg, isDebugLog = false) {
        if ((isDebugLog && this.debug) || !isDebugLog) {
            console.log(msg);
        }
    }
}

export { privates }; // export for test
export default Stubon;
<commit_msg>[clean] methodization of conditional statements<commit_after>import fs   from 'fs';
import glob from 'glob';
import url  from 'url';
import yml  from 'yamljs';
import chokidar from 'chokidar';

import express    from 'express';
import https      from 'https';
import bodyParser from 'body-parser';
import cors       from 'cors';

// for log
const red   = '\u001b[31m';
const green = '\u001b[32m';
const cyab  = '\u001b[36m';
const reset = '\u001b[0m';

//------------------------------------------------------------------------------
// utilities
//------------------------------------------------------------------------------
const privates = {
    //------------------------
    // related to express
    //------------------------

    /**
     * get request parameters
     *
     * @param {Request} req request object from express
     * @return {object}
     */
    getParams : (req) => {
        if (req.method === 'GET') {
            return req.query;
        }
        return req.body;
    },

    /**
     * normal output (200)
     *
     * @param {Response} res  response object from express
     * @param {object}   data output data object
     */
    outputJson : (res, data) => {
        res.writeHead(data.status, { 'Content-Type' : 'application/json; charset=utf-8' });
        res.end(JSON.stringify(data.body));
    },

    /**
     * not found output (404)
     *
     * @param {Response} res  response object from express
     */
    outputNotFound : (res) => {
        res.writeHead(404);
        res.end('Not Found');
    },

    /**
     * error output (500)
     *
     * @param {Response} res  response object from express
     */
    outputError : (res) => {
        res.writeHead(500);
        res.end('Server Error!');
    },

    //------------------------
    // for searching
    //------------------------

    /**
     * Check whether the setting matches the request
     *
     * @param {object} exp expected request object that configured in files
     * @param {string} reqMethod  requested method
     * @param {object} reqParams  requested routing parameters
     * @param {object} reqQueries requested queries
     * @param {object} reqHeaders requested headers
     * @return {boolean}
     */
    isMatchedRequest : (exp, reqMethod, reqParams, reqQueries, reqHeaders) => (
        (!exp.method || exp.method === reqMethod)
        && (!exp.params || privates.isSubsetObject(reqParams, exp.params))
        && (!exp.queries || privates.isSubsetObject(reqQueries, exp.queries))
        && (!exp.headers || privates.isSubsetObject(reqHeaders, exp.headers))
    ),

    /**
     * is subset
     * these values assumed to be string, since object is http request params.
     *
     * @param {object} whole expect whole set object
     * @param {object} part  expect subset object
     * @return {boolean}
     */
    isSubsetObject : (whole, part) => {
        // wild card
        if (part === '*' && typeof whole !== 'undefined') {
            return true;
        }
        if (typeof whole !== typeof part) {
            return false;
        }
        if (Array.isArray(part)) {
            return part.sort().every(
                (val, index) => privates.isSubsetObject(whole[index], val),
            );
        } else if (part instanceof Object) {
            return Object.keys(part).every(
                key => privates.isSubsetObject(whole[key], part[key]),
            );
        }
        // enum string
        if (privates.isPlaceholder(String(part))) {
            const enums = part.slice(1, -1).split('|');
            return enums.findIndex(v => (v === whole)) !== -1;
        }
        // another string
        return whole === part;
    },

    /**
     * is placeholder string
     *
     * @param {string} str
     * @return {boolean}
     */
    isPlaceholder : str => (str.substr(0, 1) === '{' && str.substr(-1) === '}'),

    /**
     * compare path and get requested routing params
     *
     * @param {string} stubPath path that was set to files
     * @param {string} reqPath  path that was requested
     * @return {array}
     *   0 : {boolean} is matched
     *   1 : {object}  requested routing params
     */
    isMatchingPathAndExtractParams : (stubPath, reqPath) => {
        const stubDirs  = stubPath.split('/');
        const reqDirs   = reqPath.split('/');
        let reqParams   = {};
        let isMatch;
        if (stubDirs.length === reqDirs.length) {
            isMatch = stubDirs.every((stubDir, j) => {
                const reqDir = reqDirs[j];
                if (privates.isPlaceholder(stubDir)) {
                    reqParams[stubDir.slice(1, -1)] = reqDir;
                    return true;
                } else if (stubDir === reqDir) {
                    return true;
                }
                // no match
                reqParams = {};
                return false;
            });
        } else {
            isMatch = false;
        }

        return [isMatch, reqParams];
    },

    //------------------------
    // others
    //------------------------

    parsers : [
        // YAML
        {
            extension : 'yml',
            parse     : file => yml.load(file),
        },
        // JSON
        {
            extension : 'json',
            parse     : file => JSON.parse(fs.readFileSync(file, 'utf-8')),
        },
    ],

    /**
     * Read files under a specified directory
     *
     * @param {string} directory string indicating the directory
     * @return {object}
     */
    loadFiles : (directory) => {
        const data = {};

        privates.parsers.forEach((typeInfo) => {
            const files = glob.sync(`${directory}/*.${typeInfo.extension}`);
            files.forEach((file) => {
                data[file] = {};
                const parsed = typeInfo.parse(file);
                Object.keys(parsed).forEach((key) => {
                    data[file][key] = parsed[key];
                });
            });
        });

        return data;
    },
};

//------------------------------------------------------------------------------
// main
//------------------------------------------------------------------------------
/**
 * Stubon
 *
 * @prop {object}  app   express object
 * @prop {object}  stubs stub setting object
 * @prop {boolean} debug detailed log flag
 * @prop {boolean} ssl   ssl flag
 */
class Stubon {

    /**
     * constructor
     *
     * @param {string}  directory             setting file directory
     * @param {object}  options
     * @param {boolean} [options.debug=false] detailed log flag
     * @param {boolean} [options.ssl=false]   https mode
     */
    constructor(directory, options = {}) {
        this.debug = options.debug || false;
        this.ssl   = options.ssl || false;
        this.stubs = privates.loadFiles(directory);
        this.app   = express();

        // watch files
        chokidar.watch(directory, { persistent : true })
            .on('change', () => {
                this.log('\n----- stub files changed -----');
                this.stubs = privates.loadFiles(directory);
            });
    }

    /**
     * start server
     */
    server() {
        // to be able to receive request params on POST method.
        this.app.use(bodyParser.urlencoded({ extended : true }));
        this.app.use(bodyParser.json());

        // allow cors
        this.app.use(cors());

        // set routing
        this.app.all('*', (req, res) => {
            this.log('\n----- recieve request -----');
            this.router(req, res);
        });

        // set ssl (optional)
        if (this.ssl) {
            this.app = https.createServer({
                key  : fs.readFileSync(`${__dirname}/../ssl/server.key`),
                cert : fs.readFileSync(`${__dirname}/../ssl/server.crt`),
            }, this.app);
        }

        return this.app;
    }

    /**
     * router
     *
     * @param {object} req request object
     * @param {object} res respons object
     */
    router(req, res) {
        const {
            getParams,
            isMatchedRequest,
            isMatchingPathAndExtractParams,
            outputJson,
            outputNotFound,
            outputError,
        } = privates;
        try {
            const reqPath    = decodeURI(url.parse(req.url).pathname);
            const reqQueries = getParams(req);

            this.log([
                { path : reqPath },
                { method : req.method },
                { query : reqQueries },
                { header : JSON.stringify(req.headers) },
            ], true);

            const isFound = Object.keys(this.stubs).some((file) => {
                this.log(`file: ${file}`, true);
                return Object.keys(this.stubs[file]).some((stubPath) => {
                    // check path
                    this.log(`  path: ${stubPath}`, true);
                    const [isMatch, reqParams] = isMatchingPathAndExtractParams(stubPath, reqPath);
                    if (!isMatch) {
                        return false;
                    }

                    this.log('  matched.', true);
                    return this.stubs[file][stubPath].some((setting, i) => {
                        this.log(`    index: ${i}`, true);
                        const exp = setting.request;
                        const valsToCheck = [req.method, reqParams, reqQueries, req.headers];

                        // check requests
                        if (isMatchedRequest(exp, ...valsToCheck)) {
                            this.log(`>> ${green}match!${reset} ${file} ${stubPath} [${i}]`);
                            const response = this.stubs[file][stubPath][i].response;
                            const options  = this.stubs[file][stubPath][i].options || {};
                            const lagSec   = parseInt(options.lagSec, 10) || 0;
                            setTimeout(() => outputJson(res, response), lagSec * 1000);
                            return true;
                        }

                        return false;
                    });
                });
            });
            if (!isFound) {
                this.log(`>> ${cyab}not found${reset}`);
                outputNotFound(res);
            }
        } catch (e) {
            this.log(`>> ${red}error${reset}`);
            this.log(e);
            outputError(res);
        }
    }

    /**
     * logger
     *
     * @param {string}  msg                message
     * @param {boolean} [isDebugLog=false] detailed log flag
     */
    log(msg, isDebugLog = false) {
        if ((isDebugLog && this.debug) || !isDebugLog) {
            console.log(msg);
        }
    }
}

export { privates }; // export for test
export default Stubon;
<|endoftext|>


<commit_before>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Colour input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldColour');

goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Field');
goog.require('Blockly.utils');

goog.require('goog.style');

/**
 * Class for a colour input field.
 * @param {string} colour The initial colour in '#rrggbb' format.
 * @param {Function=} opt_validator A function that is executed when a new
 *     colour is selected.  Its sole argument is the new colour value.  Its
 *     return value becomes the selected colour, unless it is undefined, in
 *     which case the new colour stands, or it is null, in which case the change
 *     is aborted.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldColour = function(colour, opt_validator) {
  Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator);
  this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP);
};
goog.inherits(Blockly.FieldColour, Blockly.Field);

/**
 * Construct a FieldColour from a JSON arg object.
 * @param {!Object} options A JSON object with options (colour).
 * @returns {!Blockly.FieldColour} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldColour.fromJson = function(options) {
  return new Blockly.FieldColour(options['colour']);
};

/**
 * Default width of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_WIDTH = 16;

/**
 * Default height of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_HEIGHT = 12;

/**
 * Array of colours used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.colours_ = null;

/**
 * Array of colour tooltips used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.titles_ = null;

/**
 * Number of colour columns used by this field.  If 0, use the global setting.
 * By default use the global constants for columns.
 * @type {number}
 * @private
 */
Blockly.FieldColour.prototype.columns_ = 0;

/**
 * Install this field on a block.
 */
Blockly.FieldColour.prototype.init = function() {
  Blockly.FieldColour.superClass_.init.call(this);
  this.borderRect_.style['fillOpacity'] = 1;
  this.size_ = new goog.math.Size(Blockly.FieldColour.DEFAULT_WIDTH,
      Blockly.FieldColour.DEFAULT_HEIGHT);
  this.setValue(this.getValue());
};

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldColour.prototype.CURSOR = 'default';

/**
 * Close the colour picker if this input is being deleted.
 */
Blockly.FieldColour.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldColour.superClass_.dispose.call(this);
};

/**
 * Return the current colour.
 * @return {string} Current colour in '#rrggbb' format.
 */
Blockly.FieldColour.prototype.getValue = function() {
  return this.colour_;
};

/**
 * Get the size, and rerender if necessary.
 * @return {!goog.math.Size} Height and width.
 */
Blockly.FieldColour.prototype.getSize = function() {
  if (!this.size_.width) {
    this.render_();
  }

  return this.size_;
};

/**
 * Updates the width of the field.  Colour fields have a constant width, but
 * the width is sometimes reset to force a rerender.
 */
Blockly.FieldColour.prototype.updateWidth = function() {
  var width = Blockly.FieldColour.DEFAULT_WIDTH;
  if (this.borderRect_) {
    this.borderRect_.setAttribute('width',
        width + Blockly.BlockSvg.SEP_SPACE_X);
  }
  this.size_.width = width;
};

/**
 * Set the colour.
 * @param {string} colour The new colour in '#rrggbb' format.
 */
Blockly.FieldColour.prototype.setValue = function(colour) {
  if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
      this.colour_ != colour) {
    Blockly.Events.fire(new Blockly.Events.BlockChange(
        this.sourceBlock_, 'field', this.name, this.colour_, colour));
  }
  this.colour_ = colour;
  if (this.borderRect_) {
    this.borderRect_.style.fill = colour;
  }
};

/**
 * Get the text from this field.  Used when the block is collapsed.
 * @return {string} Current text.
 */
Blockly.FieldColour.prototype.getText = function() {
  var colour = this.colour_;
  // Try to use #rgb format if possible, rather than #rrggbb.
  var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
  if (m) {
    colour = '#' + m[1] + m[2] + m[3];
  }
  return colour;
};

/**
 * An array of colour strings for the palette.
 * Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.COLOURS = [
  // grays
  '#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
  // reds
  '#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
  // oranges
  '#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
  // yellows
  '#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
  // olives
  '#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
  // greens
  '#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
  // turquoises
  '#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
  // blues
  '#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
  // purples
  '#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
  // violets
  '#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
];

/**
 * An array of tooltip strings for the palette.  If not the same length as
 * COLOURS, the colour's hex code will be used for any missing titles.
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.TITLES = [];

/**
 * Number of columns in the palette.
 * All colour pickers use this unless overridden with setColumns.
 */
Blockly.FieldColour.COLUMNS = 7;

/**
 * Set a custom colour grid for this field.
 * @param {Array.<string>} colours Array of colours for this block,
 *     or null to use default (Blockly.FieldColour.COLOURS).
 * @param {Array.<string>} opt_titles Optional array of colour tooltips,
 *     or null to use default (Blockly.FieldColour.TITLES).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColours = function(colours, opt_titles) {
  this.colours_ = colours;
  if (opt_titles !== undefined) {
    this.titles_ = opt_titles;
  }
  return this;
};

/**
 * Set a custom grid size for this field.
 * @param {number} columns Number of columns for this block,
 *     or 0 to use default (Blockly.FieldColour.COLUMNS).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColumns = function(columns) {
  this.columns_ = columns;
  return this;
};

/**
 * Create a palette under the colour field.
 * @private
 */
Blockly.FieldColour.prototype.showEditor_ = function() {

  Blockly.DropDownDiv.hideWithoutAnimation();
  Blockly.DropDownDiv.clearContent();

  var picker = this.createWidget_();
  Blockly.DropDownDiv.getContentDiv().appendChild(picker);
  // Dropdown div accepts CSS colours.
  // But maybe this should be done with a class instead?
  Blockly.DropDownDiv.setColour('white', 'silver');

  Blockly.DropDownDiv.showPositionedByField(this);

  // Configure event handler on the table to listen for any event in a cell.
  Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker,
      'mouseup', this, this.onClick_);
};

/**
 * Handle a click on a colour cell.
 * @param {!Event} e Mouse event.
 * @private
 */
Blockly.FieldColour.prototype.onClick_ = function(e) {
  var cell = e.target;
  if (cell && !cell.label) {
    // The target element is the 'div', back out to the 'td'.
    cell = cell.parentNode;
  }
  var colour = cell && cell.label;
  Blockly.WidgetDiv.hide();
  if (this.sourceBlock_) {
    // Call any validation function, and allow it to override.
    colour = this.callValidator(colour);
  }
  if (colour !== null) {
    this.setValue(colour);
  }
};

/**
 * Create a colour picker widget.
 * @return {!Element} The newly created colour picker.
 * @private
 */
Blockly.FieldColour.prototype.createWidget_ = function() {
  var columns = this.columns_ || Blockly.FieldColour.COLUMNS;
  var colours = this.colours_ || Blockly.FieldColour.COLOURS;
  var titles = this.titles_ || Blockly.FieldColour.TITLES;
  var selectedColour = this.getValue();
  // Create the palette.
  var table = document.createElement('table');
  table.className = 'blocklyColourTable';
  var row;
  for (var i = 0; i < colours.length; i++) {
    if (i % columns == 0) {
      row = document.createElement('tr');
      table.appendChild(row);
    }
    var cell = document.createElement('td');
    row.appendChild(cell);
    var div = document.createElement('div');
    cell.appendChild(div);
    cell.label = colours[i];  // This becomes the value, if clicked.
    cell.title = titles[i] || colours[i];
    div.style.backgroundColor = colours[i];
    if (colours[i] == selectedColour) {
      div.className = 'blocklyColourSelected';
    }
  }
  return table;
};

/**
 * Hide the colour picker widget.
 * @private
 */
Blockly.FieldColour.widgetDispose_ = function() {
  if (Blockly.FieldColour.onUpWrapper_) {
    Blockly.unbindEvent_(Blockly.FieldColour.onUpWrapper_);
  }
  Blockly.Events.setGroup(false);
};

Blockly.Field.register('field_colour', Blockly.FieldColour);
<commit_msg>Use constants for dropdown div colours in the colour field<commit_after>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Colour input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldColour');

goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Field');
goog.require('Blockly.utils');

goog.require('goog.style');

/**
 * Class for a colour input field.
 * @param {string} colour The initial colour in '#rrggbb' format.
 * @param {Function=} opt_validator A function that is executed when a new
 *     colour is selected.  Its sole argument is the new colour value.  Its
 *     return value becomes the selected colour, unless it is undefined, in
 *     which case the new colour stands, or it is null, in which case the change
 *     is aborted.
 * @extends {Blockly.Field}
 * @constructor
 */
Blockly.FieldColour = function(colour, opt_validator) {
  Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator);
  this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP);
};
goog.inherits(Blockly.FieldColour, Blockly.Field);

/**
 * Construct a FieldColour from a JSON arg object.
 * @param {!Object} options A JSON object with options (colour).
 * @returns {!Blockly.FieldColour} The new field instance.
 * @package
 * @nocollapse
 */
Blockly.FieldColour.fromJson = function(options) {
  return new Blockly.FieldColour(options['colour']);
};

/**
 * Default width of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_WIDTH = 16;

/**
 * Default height of a colour field.
 * @type {number}
 * @private
 * @const
 */
Blockly.FieldColour.DEFAULT_HEIGHT = 12;

/**
 * Array of colours used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.colours_ = null;

/**
 * Array of colour tooltips used by this field.  If null, use the global list.
 * @type {Array.<string>}
 * @private
 */
Blockly.FieldColour.prototype.titles_ = null;

/**
 * Number of colour columns used by this field.  If 0, use the global setting.
 * By default use the global constants for columns.
 * @type {number}
 * @private
 */
Blockly.FieldColour.prototype.columns_ = 0;

/**
 * Border colour for the dropdown div showing the colour picker.  Must be a CSS
 * string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BORDER_COLOUR = 'silver';

/**
 * Background colour for the dropdown div showing the colour picker.  Must be a
 * CSS string.
 * @type {string}
 * @private
 */
Blockly.FieldColour.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white';

/**
 * Install this field on a block.
 */
Blockly.FieldColour.prototype.init = function() {
  Blockly.FieldColour.superClass_.init.call(this);
  this.borderRect_.style['fillOpacity'] = 1;
  this.size_ = new goog.math.Size(Blockly.FieldColour.DEFAULT_WIDTH,
      Blockly.FieldColour.DEFAULT_HEIGHT);
  this.setValue(this.getValue());
};

/**
 * Mouse cursor style when over the hotspot that initiates the editor.
 */
Blockly.FieldColour.prototype.CURSOR = 'default';

/**
 * Close the colour picker if this input is being deleted.
 */
Blockly.FieldColour.prototype.dispose = function() {
  Blockly.WidgetDiv.hideIfOwner(this);
  Blockly.FieldColour.superClass_.dispose.call(this);
};

/**
 * Return the current colour.
 * @return {string} Current colour in '#rrggbb' format.
 */
Blockly.FieldColour.prototype.getValue = function() {
  return this.colour_;
};

/**
 * Get the size, and rerender if necessary.
 * @return {!goog.math.Size} Height and width.
 */
Blockly.FieldColour.prototype.getSize = function() {
  if (!this.size_.width) {
    this.render_();
  }

  return this.size_;
};

/**
 * Updates the width of the field.  Colour fields have a constant width, but
 * the width is sometimes reset to force a rerender.
 */
Blockly.FieldColour.prototype.updateWidth = function() {
  var width = Blockly.FieldColour.DEFAULT_WIDTH;
  if (this.borderRect_) {
    this.borderRect_.setAttribute('width',
        width + Blockly.BlockSvg.SEP_SPACE_X);
  }
  this.size_.width = width;
};

/**
 * Set the colour.
 * @param {string} colour The new colour in '#rrggbb' format.
 */
Blockly.FieldColour.prototype.setValue = function(colour) {
  if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
      this.colour_ != colour) {
    Blockly.Events.fire(new Blockly.Events.BlockChange(
        this.sourceBlock_, 'field', this.name, this.colour_, colour));
  }
  this.colour_ = colour;
  if (this.borderRect_) {
    this.borderRect_.style.fill = colour;
  }
};

/**
 * Get the text from this field.  Used when the block is collapsed.
 * @return {string} Current text.
 */
Blockly.FieldColour.prototype.getText = function() {
  var colour = this.colour_;
  // Try to use #rgb format if possible, rather than #rrggbb.
  var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
  if (m) {
    colour = '#' + m[1] + m[2] + m[3];
  }
  return colour;
};

/**
 * An array of colour strings for the palette.
 * Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.COLOURS = [
  // grays
  '#ffffff', '#cccccc', '#c0c0c0', '#999999', '#666666', '#333333', '#000000',
  // reds
  '#ffcccc', '#ff6666', '#ff0000', '#cc0000', '#990000', '#660000', '#330000',
  // oranges
  '#ffcc99', '#ff9966', '#ff9900', '#ff6600', '#cc6600', '#993300', '#663300',
  // yellows
  '#ffff99', '#ffff66', '#ffcc66', '#ffcc33', '#cc9933', '#996633', '#663333',
  // olives
  '#ffffcc', '#ffff33', '#ffff00', '#ffcc00', '#999900', '#666600', '#333300',
  // greens
  '#99ff99', '#66ff99', '#33ff33', '#33cc00', '#009900', '#006600', '#003300',
  // turquoises
  '#99ffff', '#33ffff', '#66cccc', '#00cccc', '#339999', '#336666', '#003333',
  // blues
  '#ccffff', '#66ffff', '#33ccff', '#3366ff', '#3333ff', '#000099', '#000066',
  // purples
  '#ccccff', '#9999ff', '#6666cc', '#6633ff', '#6600cc', '#333399', '#330099',
  // violets
  '#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033'
];

/**
 * An array of tooltip strings for the palette.  If not the same length as
 * COLOURS, the colour's hex code will be used for any missing titles.
 * All colour pickers use this unless overridden with setColours.
 * @type {!Array.<string>}
 */
Blockly.FieldColour.TITLES = [];

/**
 * Number of columns in the palette.
 * All colour pickers use this unless overridden with setColumns.
 */
Blockly.FieldColour.COLUMNS = 7;

/**
 * Set a custom colour grid for this field.
 * @param {Array.<string>} colours Array of colours for this block,
 *     or null to use default (Blockly.FieldColour.COLOURS).
 * @param {Array.<string>} opt_titles Optional array of colour tooltips,
 *     or null to use default (Blockly.FieldColour.TITLES).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColours = function(colours, opt_titles) {
  this.colours_ = colours;
  if (opt_titles !== undefined) {
    this.titles_ = opt_titles;
  }
  return this;
};

/**
 * Set a custom grid size for this field.
 * @param {number} columns Number of columns for this block,
 *     or 0 to use default (Blockly.FieldColour.COLUMNS).
 * @return {!Blockly.FieldColour} Returns itself (for method chaining).
 */
Blockly.FieldColour.prototype.setColumns = function(columns) {
  this.columns_ = columns;
  return this;
};

/**
 * Create a palette under the colour field.
 * @private
 */
Blockly.FieldColour.prototype.showEditor_ = function() {

  Blockly.DropDownDiv.hideWithoutAnimation();
  Blockly.DropDownDiv.clearContent();

  var picker = this.createWidget_();
  Blockly.DropDownDiv.getContentDiv().appendChild(picker);
  Blockly.DropDownDiv.setColour(
      this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR);

  Blockly.DropDownDiv.showPositionedByField(this);

  // Configure event handler on the table to listen for any event in a cell.
  Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker,
      'mouseup', this, this.onClick_);
};

/**
 * Handle a click on a colour cell.
 * @param {!Event} e Mouse event.
 * @private
 */
Blockly.FieldColour.prototype.onClick_ = function(e) {
  var cell = e.target;
  if (cell && !cell.label) {
    // The target element is the 'div', back out to the 'td'.
    cell = cell.parentNode;
  }
  var colour = cell && cell.label;
  Blockly.WidgetDiv.hide();
  if (this.sourceBlock_) {
    // Call any validation function, and allow it to override.
    colour = this.callValidator(colour);
  }
  if (colour !== null) {
    this.setValue(colour);
  }
};

/**
 * Create a colour picker widget.
 * @return {!Element} The newly created colour picker.
 * @private
 */
Blockly.FieldColour.prototype.createWidget_ = function() {
  var columns = this.columns_ || Blockly.FieldColour.COLUMNS;
  var colours = this.colours_ || Blockly.FieldColour.COLOURS;
  var titles = this.titles_ || Blockly.FieldColour.TITLES;
  var selectedColour = this.getValue();
  // Create the palette.
  var table = document.createElement('table');
  table.className = 'blocklyColourTable';
  var row;
  for (var i = 0; i < colours.length; i++) {
    if (i % columns == 0) {
      row = document.createElement('tr');
      table.appendChild(row);
    }
    var cell = document.createElement('td');
    row.appendChild(cell);
    var div = document.createElement('div');
    cell.appendChild(div);
    cell.label = colours[i];  // This becomes the value, if clicked.
    cell.title = titles[i] || colours[i];
    div.style.backgroundColor = colours[i];
    if (colours[i] == selectedColour) {
      div.className = 'blocklyColourSelected';
    }
  }
  return table;
};

/**
 * Hide the colour picker widget.
 * @private
 */
Blockly.FieldColour.widgetDispose_ = function() {
  if (Blockly.FieldColour.onUpWrapper_) {
    Blockly.unbindEvent_(Blockly.FieldColour.onUpWrapper_);
  }
  Blockly.Events.setGroup(false);
};

Blockly.Field.register('field_colour', Blockly.FieldColour);
<|endoftext|>


<commit_before>'use strict'

class Connection {
  constructor (db, client) {
    this.db = db
    this.client = client
  }

  close () {
    this.client.release()
  }

  query (...args) {
    this.db.log(args)
    return new Promise((resolve, reject) => {
      this.client.query(...args, (error, result) => {
        error ? reject(error) : resolve(result)
      })
    })
  }

  static create (db) {
    return db.pool.connect().then((client) => new Connection(db, client))
  }
}

module.exports = Connection
<commit_msg>async Connection#create<commit_after>'use strict'

class Connection {
  constructor (db, client) {
    this.db = db
    this.client = client
  }

  close () {
    this.client.release()
  }

  query (...args) {
    this.db.log(args)
    return new Promise((resolve, reject) => {
      this.client.query(...args, (error, result) => {
        error ? reject(error) : resolve(result)
      })
    })
  }

  static async create (db) {
    return new Connection(db, await db.pool.connect())
  }
}

module.exports = Connection
<|endoftext|>


<commit_before>/*
MIT License

Copyright (c) 2016-2017 Grall Arnaud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
'use strict';

const AbstractOverlay = require('./../abstract/abstract-overlay.js');
const TMan = require('tman-wrtc');
const lmerge = require('lodash.merge');

/**
 * A TManOverlay is an abstract network used to build overlay based on the TMan network over WebRTC.
 * @see https://github.com/RAN3D/tman-wrtc for more informations on TMan.
 * @abstract
 * @extends AbstractOverlay
 * @author Thomas Minier
 */
class TManOverlay extends AbstractOverlay {
  /**
   * The in-view ID of the peer in the network
   * @return {string} The in-view ID of the peer
   */
  get inviewId () {
    return this.rps.getInviewId();
  }

  /**
   * The out-view ID of the peer in the network
   * @return {string} The out-view ID of the peer
   */
  get outviewId () {
    return this.rps.getOutviewId();
  }

  /**
   * Build a TMan network
   * @param {Object} options - Options used to build the TMan
   * @return {TMan} The TMan network
   */
  _buildRPS (options) {
    const tmanOptions = lmerge({
      descriptor: this._descriptor(),
      descriptorTimeout: this._descriptorTimeout(),
      ranking: this._rankingFunction()
    }, options);
    return new TMan(tmanOptions);
  }

  /**
   * Gives the start descriptor used by the TMan overlay (can be an empty object).
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @return {Object} The start descriptor used by the TMan overlay
   */
  _descriptor () {
    throw new Error('A valid TMan based overlay must implement a _descriptor method to generate a base descriptor');
  }

  /**
   * Give the delay **in milliseconds** after which the descriptor must be recomputed.
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @return {number} The delay **in milliseconds** after which the descriptor must be recomputed
   */
  _descriptorTimeout () {
    throw new Error('A valid TMan based overlay must implement a _descriptorTimeout method to give the timeout on descriptors');
  }

  /**
   * Compare two peers and rank them according to a ranking function.
   * This function must return `0 if peerA == peerB`, `1 if peerA < peerB` and `-1 if peerA > peerB`.
   *
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @param {*} self - The current peer that perform the evaluation
   * @param {*} peerA - The first peer
   * @param {*} peerB - The second peer
   * @return {integer} `0 if peerA == peerB`, `1 if peerA < peerB` and `-1 if peerA > peerB` (according to the ranking algorithm)
   */
  _rankPeers (self, peerA, peerB) {
    throw new Error('A valid TMan based overlay must implement a _rankPeers method to rank two peers');
  }

  /**
   * Utility to rank two peers
   * @private
   */
  _rankingFunction () {
    return peer => (a, b) => this._rankPeers(peer, a, b);
  }

  /**
   * Get the IDs of all available neighbours
   * @param  {integer} limit - Max number of neighbours to look for
   * @return {string[]} Set of IDs for all available neighbours
   */
  getNeighbours (limit) {
    return this.rps.getPeers(limit);
  }
}

module.exports = TManOverlay;
<commit_msg>enhance abstract TMan overlay<commit_after>/*
MIT License

Copyright (c) 2016-2017 Grall Arnaud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
'use strict';

const AbstractNetwork = require('./abstract-network.js');
const TMan = require('tman-wrtc');
const lmerge = require('lodash.merge');

/**
 * A TManOverlay is an abstract network used to build overlay based on the TMan network over WebRTC.
 * @see https://github.com/RAN3D/tman-wrtc for more informations on TMan.
 * @abstract
 * @extends AbstractOverlay
 * @author Thomas Minier
 */
class TManOverlay extends AbstractNetwork {
  /**
   * Constructor
   * @param {Object} options - Additional options used to build the network
   * @return {NetworkManager} networkManager - Network manager used as root for the overlay
   */
  constructor (networkManager, options) {
    options.manager = networkManager;
    super(options);
    this._manager = networkManager;
    this._rps.start(this.options.delta);
  }

  /**
   * The in-view ID of the peer in the network
   * @return {string} The in-view ID of the peer
   */
  get inviewId () {
    return this.rps.getInviewId();
  }

  /**
   * The out-view ID of the peer in the network
   * @return {string} The out-view ID of the peer
   */
  get outviewId () {
    return this.rps.getOutviewId();
  }

  /**
   * Get our current descriptor
   * @return {Object} The peer current descriptor
   */
  get descriptor () {
    return this._rps.options.descriptor;
  }

  /**
   * Update the peer descriptor
   * @param  {Object} newDescriptor - The new descriptor
   * @return {void}
   */
  set descriptor (newDescriptor) {
    this._rps.options.descriptor = newDescriptor;
  }

  /**
   * Build a TMan network
   * @param {Object} options - Options used to build the TMan
   * @return {TMan} The TMan network
   */
  _buildRPS (options) {
    let globalOptions = options._options.overlay.options;

    // if webrtc options specified: create object config for Spray
    this.options = lmerge({config: globalOptions.webrtc}, this.options);
    // now merge of all options
    this.options = lmerge(globalOptions, this.options);

    const tmanOptions = lmerge({
      descriptor: this._startDescriptor(),
      descriptorTimeout: this._descriptorTimeout(),
      ranking: this._rankingFunction()
    }, this.options);

    return new TMan(tmanOptions, options.manager._rps._network._rps);
  }

  /**
   * Gives the start descriptor used by the TMan overlay (can be an empty object).
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @return {Object} The start descriptor used by the TMan overlay
   */
  _startDescriptor () {
    throw new Error('A valid TMan based overlay must implement a _descriptor method to generate a base descriptor');
  }

  /**
   * Give the delay **in milliseconds** after which the descriptor must be recomputed.
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @return {number} The delay **in milliseconds** after which the descriptor must be recomputed
   */
  _descriptorTimeout () {
    throw new Error('A valid TMan based overlay must implement a _descriptorTimeout method to give the timeout on descriptors');
  }

  /**
   * Compare two peers and rank them according to a ranking function.
   * This function must return `0 if peerA == peerB`, `1 if peerA < peerB` and `-1 if peerA > peerB`.
   *
   * Subclasses of {@link TManOverlay} **must** implement this method.
   * @param {*} neighbour - The neighbour to rank with
   * @param {Object} descriptorA - Descriptor of the first peer
   * @param {Object} descriptorB - Descriptor of the second peer
   * @param {TManOverlay} peerA - (optional) The overlay of the first peer
   * @param {TManOverlay} peerB - (optional) The overlay of the second peer
   * @return {integer} `0 if peerA == peerB`, `1 if peerA < peerB` and `-1 if peerA > peerB` (according to the ranking algorithm)
   */
  _rankPeers (neighbour, descriptorA, descriptorB, peerA, peerB) {
    throw new Error('A valid TMan based overlay must implement a _rankPeers method to rank two peers');
  }

  /**
   * Utility to rank two peers
   * @private
   */
  _rankingFunction () {
    return peer => (a, b) => this._rankPeers(peer, a.descriptor, b.descriptor, a, b);
  }

  /**
   * Get the IDs of all available neighbours
   * @param  {integer} limit - Max number of neighbours to look for
   * @return {string[]} Set of IDs for all available neighbours
   */
  getNeighbours (limit) {
    return this.rps.getPeers(limit);
  }
}

module.exports = TManOverlay;
<|endoftext|>


<commit_before>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2016 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Number input field
 * @author fenichel@google.com (Rachel Fenichel)
 */
'use strict';

goog.provide('Blockly.FieldNumber');

goog.require('Blockly.FieldTextInput');
goog.require('goog.math');

/**
 * Class for an editable number field.
 * @param {string} value The initial content of the field.
 * @param {number|string|undefined} opt_min Minimum value.
 * @param {number|string|undefined} opt_max Maximum value.
 * @param {number|string|undefined} opt_precision Precision for value.
 * @param {Function=} opt_validator An optional function that is called
 *     to validate any constraints on what the user entered.  Takes the new
 *     text as an argument and returns either the accepted text, a replacement
 *     text, or null to abort the change.
 * @extends {Blockly.FieldTextInput}
 * @constructor
 */
Blockly.FieldNumber =
    function(value, opt_min, opt_max, opt_precision, opt_validator) {
  value = String(value);
  Blockly.FieldNumber.superClass_.constructor.call(this, value, opt_validator);
  this.setConstraints(opt_min, opt_max, opt_precision);
};
goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput);

/**
 * Set the maximum, minimum and precision constraints on this field.
 * Any of these properties may be undefiend or NaN to be disabled.
 * Setting precision (usually a power of 10) enforces a minimum step between
 * values. That is, the user's value will rounded to the closest multiple of
 * precision. The least significant digit place is inferred from the precision.
 * Integers values can be enforces by choosing an integer precision.
 * @param {number|string|undefined} min Minimum value.
 * @param {number|string|undefined} max Maximum value.
 * @param {number|string|undefined} precision Precision for value.
 */
Blockly.FieldNumber.prototype.setConstraints = function(min, max, precision) {
  precision = parseFloat(precision);
  this.precision_ = isNaN(precision) ? 0 : precision;
  min = parseFloat(min);
  this.min_ = isNaN(min) ? -Infinity : min;
  max = parseFloat(max);
  this.max_ = isNaN(max) ? Infinity : max;
  this.setValue(this.callValidator(this.getValue));
};

/**
 * Ensure that only a number in the correct range may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid number, or null if invalid.
 * @this {!Blockly.FieldNumber}
 */
Blockly.FieldNumber.classValidator = function(text) {
  if (text === null) {
    return null;
  }
  text = String(text);
  // TODO: Handle cases like 'ten', '1.203,14', etc.
  // 'O' is sometimes mistaken for '0' by inexperienced users.
  text = text.replace(/O/ig, '0');
  // Strip out thousands separators.
  text = text.replace(/,/g, '');
  var n = parseFloat(text || 0);
  if (isNaN(n)) {
    // Invalid number.
    return null;
  }
  // Round to nearest multiple of precision.
  if (this.precision_ && Number.isFinite(n)) {
    n = Math.round(n / this.precision_) * this.precision_;
  }
  // Get the value in range.
  n = goog.math.clamp(n, this.min_, this.max_);
  return String(n);
};
<commit_msg>Fix validation on number field.<commit_after>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2016 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Number input field
 * @author fenichel@google.com (Rachel Fenichel)
 */
'use strict';

goog.provide('Blockly.FieldNumber');

goog.require('Blockly.FieldTextInput');
goog.require('goog.math');

/**
 * Class for an editable number field.
 * @param {number|string} value The initial content of the field.
 * @param {number|string|undefined} opt_min Minimum value.
 * @param {number|string|undefined} opt_max Maximum value.
 * @param {number|string|undefined} opt_precision Precision for value.
 * @param {Function=} opt_validator An optional function that is called
 *     to validate any constraints on what the user entered.  Takes the new
 *     text as an argument and returns either the accepted text, a replacement
 *     text, or null to abort the change.
 * @extends {Blockly.FieldTextInput}
 * @constructor
 */
Blockly.FieldNumber =
    function(value, opt_min, opt_max, opt_precision, opt_validator) {
  value = String(value);
  Blockly.FieldNumber.superClass_.constructor.call(this, value, opt_validator);
  this.setConstraints(opt_min, opt_max, opt_precision);
};
goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput);

/**
 * Set the maximum, minimum and precision constraints on this field.
 * Any of these properties may be undefiend or NaN to be disabled.
 * Setting precision (usually a power of 10) enforces a minimum step between
 * values. That is, the user's value will rounded to the closest multiple of
 * precision. The least significant digit place is inferred from the precision.
 * Integers values can be enforces by choosing an integer precision.
 * @param {number|string|undefined} min Minimum value.
 * @param {number|string|undefined} max Maximum value.
 * @param {number|string|undefined} precision Precision for value.
 */
Blockly.FieldNumber.prototype.setConstraints = function(min, max, precision) {
  precision = parseFloat(precision);
  this.precision_ = isNaN(precision) ? 0 : precision;
  min = parseFloat(min);
  this.min_ = isNaN(min) ? -Infinity : min;
  max = parseFloat(max);
  this.max_ = isNaN(max) ? Infinity : max;
  this.setValue(this.callValidator(this.getValue()));
};

/**
 * Ensure that only a number in the correct range may be entered.
 * @param {string} text The user's text.
 * @return {?string} A string representing a valid number, or null if invalid.
 * @this {!Blockly.FieldNumber}
 */
Blockly.FieldNumber.classValidator = function(text) {
  if (text === null) {
    return null;
  }
  text = String(text);
  // TODO: Handle cases like 'ten', '1.203,14', etc.
  // 'O' is sometimes mistaken for '0' by inexperienced users.
  text = text.replace(/O/ig, '0');
  // Strip out thousands separators.
  text = text.replace(/,/g, '');
  var n = parseFloat(text || 0);
  if (isNaN(n)) {
    // Invalid number.
    return null;
  }
  // Round to nearest multiple of precision.
  if (this.precision_ && Number.isFinite(n)) {
    n = Math.round(n / this.precision_) * this.precision_;
  }
  // Get the value in range.
  n = goog.math.clamp(n, this.min_, this.max_);
  return String(n);
};
<|endoftext|>


<commit_before>import TemplatePath from 'core/template/template.path';
import AppError from 'core/infrastructure/error';

function getKey(column) {
	return column.origin === 'custom' ? 'custom-cell' : `${column.type}-cell`;
}

function buildView(source, mode, column) {
	switch (mode) {
		case 'view': {
			const type = column.type;
			return {
				key: getKey(column),
				url: `qgrid.${source}.cell.${type}.tpl.html`,
				defaults: [`$default.${type}`, '$default']
			};
		}
		case 'edit': {
			const type = column.editor || column.type;

			return {
				key: `${getKey(column)}-${mode}`,
				url: `qgrid.${source}.cell.${type}.${mode}.tpl.html`,
				defaults: column.editor
					? [`$default.${column.editor}`, `$default.${type}`, '$default']
					: [`$default.${type}`, '$default']
			};
		}
		default:
			throw new AppError('cell.build', `Invalid mode "${mode}"`);
	}
}

export default function (template, mode = 'view') {
	return function (source, model, column) {
		const view = buildView(source, mode, column);
		const pathSource = {[TemplatePath.name(view.key)]: column, 'for': source};
		const path = TemplatePath.get(pathSource);
		const state = model[path.model]();

		const link = template.link(
			view.url,
			state.resource,
			[path.resource]
				.concat(view.defaults)
		);

		return (element, scope) => link(element, scope);
	};
}<commit_msg>Don't build pad column<commit_after>import TemplatePath from 'core/template/template.path';
import AppError from 'core/infrastructure/error';
import {noop} from 'core/services/utility';

function canBuild(column) {
	return column.type !== 'pad';
}

function getKey(column) {
	return column.origin === 'custom' ? 'custom-cell' : `${column.type}-cell`;
}

function buildView(source, mode, column) {
	switch (mode) {
		case 'view': {
			const type = column.type;
			return {
				key: getKey(column),
				url: `qgrid.${source}.cell.${type}.tpl.html`,
				defaults: [`$default.${type}`, '$default']
			};
		}
		case 'edit': {
			const type = column.editor || column.type;

			return {
				key: `${getKey(column)}-${mode}`,
				url: `qgrid.${source}.cell.${type}.${mode}.tpl.html`,
				defaults: column.editor
					? [`$default.${column.editor}`, `$default.${type}`, '$default']
					: [`$default.${type}`, '$default']
			};
		}
		default:
			throw new AppError('cell.build', `Invalid mode "${mode}"`);
	}
}

export default function (template, mode = 'view') {
	return function (source, model, column) {
		if (!canBuild(column)) {
			return noop;
		}

		const view = buildView(source, mode, column);
		const pathSource = {[TemplatePath.name(view.key)]: column, 'for': source};
		const path = TemplatePath.get(pathSource);
		const state = model[path.model]();

		const link = template.link(
			view.url,
			state.resource,
			[path.resource]
				.concat(view.defaults)
		);

		return (element, scope) => link(element, scope);
	};
}<|endoftext|>


<commit_before>var fullproof = (function(NAMESPACE) {
	NAMESPACE.english = NAMESPACE.english|| {};

	/*
	Copyright (c) 2011, Chris Umbel

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
	*/
	/*
	 * Borrowed from https://github.com/NaturalNode/natural/blob/master/lib/natural/phonetics/metaphone.js
	 */
	NAMESPACE.english.metaphone = (function(){
		"use strict";

		function dedup(token) {
		    return token.replace(/([^c])\1/g, '$1');
		}

		function dropInitialLetters(token) {
		    if(token.match(/^(kn|gn|pn|ae|wr)/))
		        return token.substr(1, token.length - 1);

		    return token;
		}

		function dropBafterMAtEnd(token) {
		    return token.replace(/mb$/, 'm');
		}

		function cTransform(token) {
		    token = token.replace(/([^s]|^)(c)(h)/g, '$1x$3').trim();
		    token = token.replace(/cia/g, 'xia');
		    token = token.replace(/c(i|e|y)/g, 's$1');
		    token = token.replace(/c/g, 'k');

		    return token;
		}

		function dTransform(token) {
		    token = token.replace(/d(ge|gy|gi)/g, 'j$1');
		    token = token.replace(/d/g, 't');

		    return token;
		}

		function dropG(token) {
		    token = token.replace(/gh(^$|[^aeiou])/g, 'h$1');
		    token = token.replace(/g(n|ned)$/g, '$1');

		    return token;
		}

		function transformG(token) {
		    token = token.replace(/([^g]|^)(g)(i|e|y)/g, '$1j$3');
		    token = token.replace(/gg/g, 'g');
		    token = token.replace(/g/g, 'k');

		    return token;
		}

		function dropH(token) {
		    return token.replace(/([aeiou])h([^aeiou])/g, '$1$2');
		}

		function transformCK(token) {
		    return token.replace(/ck/g, 'k');
		}
		function transformPH(token) {
		    return token.replace(/ph/g, 'f');
		}

		function transformQ(token) {
		    return token.replace(/q/g, 'k');
		}

		function transformS(token) {
		    return token.replace(/s(h|io|ia)/g, 'x$1');
		}

		function transformT(token) {
		    token = token.replace(/t(ia|io)/g, 'x$1');
		    token = token.replace(/th/, '0');

		    return token;
		}

		function dropT(token) {
		    return token.replace(/tch/g, 'ch');
		}

		function transformV(token) {
		    return token.replace(/v/g, 'f');
		}

		function transformWH(token) {
		    return token.replace(/^wh/, 'w');
		}

		function dropW(token) {
		    return token.replace(/w([^aeiou]|$)/g, '$1');
		}

		function transformX(token) {
		    token = token.replace(/^x/, 's');
		    token = token.replace(/x/g, 'ks');
		    return token;
		}

		function dropY(token) {
		    return token.replace(/y([^aeiou]|$)/g, '$1');
		}

		function transformZ(token) {
		    return token.replace(/z/, 's');
		}

		function dropVowels(token) {
		    return token.charAt(0) + token.substr(1, token.length).replace(/[aeiou]/g, '');
		}

		return function(token, maxLength) {
		    maxLength == maxLength || 32;
		    token = token.toLowerCase();
		    token = dedup(token);
		    token = dropInitialLetters(token);
		    token = dropBafterMAtEnd(token);
		    token = transformCK(token);
		    token = cTransform(token);
		    token = dTransform(token);
		    token = dropG(token);
		    token = transformG(token);
		    token = dropH(token);
		    token = transformPH(token);
		    token = transformQ(token);
		    token = transformS(token);
		    token = transformX(token);
		    token = transformT(token);
		    token = dropT(token);
		    token = transformV(token);
		    token = transformWH(token);
		    token = dropW(token);
		    token = dropY(token);
		    token = transformZ(token);
		    token = dropVowels(token);

		    token.toUpperCase();
		    if(token.length >= maxLength)
		        token = token.substring(0, maxLength);

		    return token.toUpperCase();
		};

	});
	return NAMESPACE;
})(fullproof||{});

<commit_msg>removed suspicious char<commit_after>var fullproof = (function(NAMESPACE) {
	NAMESPACE.english = NAMESPACE.english|| {};

	/*
	Copyright (c) 2011, Chris Umbel

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
	*/
	/*
	 * Borrowed from https://github.com/NaturalNode/natural/blob/master/lib/natural/phonetics/metaphone.js
	 */
	NAMESPACE.english.metaphone = (function(){
		"use strict";

		function dedup(token) {
		    return token.replace(/([^c])\1/g, '$1');
		}

		function dropInitialLetters(token) {
		    if(token.match(/^(kn|gn|pn|ae|wr)/))
		        return token.substr(1, token.length - 1);

		    return token;
		}

		function dropBafterMAtEnd(token) {
		    return token.replace(/mb$/, 'm');
		}

		function cTransform(token) {
		    token = token.replace(/([^s]|^)(c)(h)/g, '$1x$3').trim();
		    token = token.replace(/cia/g, 'xia');
		    token = token.replace(/c(i|e|y)/g, 's$1');
		    token = token.replace(/c/g, 'k');

		    return token;
		}

		function dTransform(token) {
		    token = token.replace(/d(ge|gy|gi)/g, 'j$1');
		    token = token.replace(/d/g, 't');

		    return token;
		}

		function dropG(token) {
		    token = token.replace(/gh(^$|[^aeiou])/g, 'h$1');
		    token = token.replace(/g(n|ned)$/g, '$1');

		    return token;
		}

		function transformG(token) {
		    token = token.replace(/([^g]|^)(g)(i|e|y)/g, '$1j$3');
		    token = token.replace(/gg/g, 'g');
		    token = token.replace(/g/g, 'k');

		    return token;
		}

		function dropH(token) {
		    return token.replace(/([aeiou])h([^aeiou])/g, '$1$2');
		}

		function transformCK(token) {
		    return token.replace(/ck/g, 'k');
		}
		function transformPH(token) {
		    return token.replace(/ph/g, 'f');
		}

		function transformQ(token) {
		    return token.replace(/q/g, 'k');
		}

		function transformS(token) {
		    return token.replace(/s(h|io|ia)/g, 'x$1');
		}

		function transformT(token) {
		    token = token.replace(/t(ia|io)/g, 'x$1');
		    token = token.replace(/th/, '0');

		    return token;
		}

		function dropT(token) {
		    return token.replace(/tch/g, 'ch');
		}

		function transformV(token) {
		    return token.replace(/v/g, 'f');
		}

		function transformWH(token) {
		    return token.replace(/^wh/, 'w');
		}

		function dropW(token) {
		    return token.replace(/w([^aeiou]|$)/g, '$1');
		}

		function transformX(token) {
		    token = token.replace(/^x/, 's');
		    token = token.replace(/x/g, 'ks');
		    return token;
		}

		function dropY(token) {
		    return token.replace(/y([^aeiou]|$)/g, '$1');
		}

		function transformZ(token) {
		    return token.replace(/z/, 's');
		}

		function dropVowels(token) {
		    return token.charAt(0) + token.substr(1, token.length).replace(/[aeiou]/g, '');
		}

		return function(token, maxLength) {
		    maxLength = maxLength || 32;
		    token = token.toLowerCase();
		    token = dedup(token);
		    token = dropInitialLetters(token);
		    token = dropBafterMAtEnd(token);
		    token = transformCK(token);
		    token = cTransform(token);
		    token = dTransform(token);
		    token = dropG(token);
		    token = transformG(token);
		    token = dropH(token);
		    token = transformPH(token);
		    token = transformQ(token);
		    token = transformS(token);
		    token = transformX(token);
		    token = transformT(token);
		    token = dropT(token);
		    token = transformV(token);
		    token = transformWH(token);
		    token = dropW(token);
		    token = dropY(token);
		    token = transformZ(token);
		    token = dropVowels(token);

		    token.toUpperCase();
		    if(token.length >= maxLength)
		        token = token.substring(0, maxLength);

		    return token.toUpperCase();
		};

	});
	return NAMESPACE;
})(fullproof||{});

<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { createFragmentContainer, graphql } from 'react-relay/compat';
import { browserHistory } from 'react-router';
import mergeWith from 'lodash.mergewith';
import xor from 'lodash.xor';
import memoize from 'memoize-one';
import styled from 'styled-components';
import Chip from '@material-ui/core/Chip';
import MediaLanguageChip from './MediaLanguageChip';
import { searchQueryFromUrl, urlFromSearchQuery } from '../search/Search';
import VideoAnnotationIcon from '../../../assets/images/video-annotation/video-annotation';
import { units } from '../../styles/js/shared';

const StyledMediaTagsContainer = styled.div`
  width: 100%;

  .media-tags {
    &:empty {
      display: none;
    }
  }

  .media-tags__language {
    white-space: nowrap;
  }

  .media-tags__list {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    list-style: none;
    padding: ${units(0.5)};
    margin: 0;

    li {
      margin: ${units(0.5)};
    }
  }
`;

// TODO Fix a11y issues
/* eslint jsx-a11y/click-events-have-key-events: 0 */
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
class MediaTags extends React.Component {
  filterTags = memoize((tags) => {
    const splitTags = {
      regularTags: [],
      videoTags: [],
    };
    const fragments = {};

    // Get regular tags and cluster video tags by tag_text
    if (Array.isArray(tags)) {
      tags.forEach((t) => {
        if (t.node.fragment) {
          if (!fragments[t.node.tag_text]) {
            fragments[t.node.tag_text] = [];
          }
          fragments[t.node.tag_text].push(t);
        } else {
          splitTags.regularTags.push(t);
        }
      });
    }

    // Get the video tags with earliest timestamp
    Object.values(fragments).forEach((tagFragments) => {
      tagFragments.sort((a, b) => {
        const aStart = parseFloat(a.node.fragment.match(/\d+(\.\d+)?/)[0]);
        const bStart = parseFloat(b.node.fragment.match(/\d+(\.\d+)?/)[0]);
        return aStart - bStart;
      });

      splitTags.videoTags.push(tagFragments[0]);
    });

    return splitTags;
  });

  searchTagUrl(tagString) {
    const { projectMedia } = this.props;
    const tagQuery = {
      tags: [tagString],
    };
    const searchQuery = searchQueryFromUrl();

    // Make a new query combining the current tag with whatever query is already in the URL.
    // This allows to support clicking tags on the search and project pages.
    const query = mergeWith({}, searchQuery, tagQuery, (objValue, srcValue) => {
      if (Array.isArray(objValue)) {
        return xor(objValue, srcValue);
      }
      return undefined;
    });
    if (!query.tags.length) {
      delete query.tags;
    }
    return urlFromSearchQuery(query, `/${projectMedia.team.slug}/all-items`);
  }

  handleTagViewClick(tagString) {
    const url = this.searchTagUrl(tagString);
    if (window !== window.parent) { // Browser extension
      window.open(`${window.location.origin}${url}`);
    } else {
      browserHistory.push(url);
    }
  }

  handleVideoAnnotationIconClick = (e, fragment) => {
    e.stopPropagation();
    if (this.props.onTimelineCommentOpen) {
      this.props.onTimelineCommentOpen(fragment);
    }
  };

  render() {
    const { projectMedia } = this.props;
    const { regularTags, videoTags } = this.filterTags(projectMedia.tags.edges);
    const tags = regularTags.concat(videoTags);

    return (

    );
  }
}

MediaTags.propTypes = {
  projectMedia: PropTypes.shape({
    tags: PropTypes.shape({
      edges: PropTypes.arrayOf(PropTypes.shape({
        node: PropTypes.shape({
          tag: PropTypes.string.isRequired,
          id: PropTypes.string.isRequired,
          tag_text: PropTypes.string.isRequired,
        }),
      }).isRequired).isRequired,
    }).isRequired,
  }).isRequired,
};

export { MediaTags };
export default createFragmentContainer(MediaTags, graphql`
  fragment MediaTags_projectMedia on ProjectMedia {
    id
    dbid
    team {
      slug
    }
    tags(first: 10000) {
      edges {
        node {
          id
          tag
          tag_text
          fragment
        }
      }
    }
    ...MediaLanguageChip_projectMedia
  }
`);
<commit_msg>#8724 Show language tag before regular tags (#714)<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { createFragmentContainer, graphql } from 'react-relay/compat';
import { browserHistory } from 'react-router';
import mergeWith from 'lodash.mergewith';
import xor from 'lodash.xor';
import memoize from 'memoize-one';
import styled from 'styled-components';
import Chip from '@material-ui/core/Chip';
import MediaLanguageChip from './MediaLanguageChip';
import { searchQueryFromUrl, urlFromSearchQuery } from '../search/Search';
import VideoAnnotationIcon from '../../../assets/images/video-annotation/video-annotation';
import { units } from '../../styles/js/shared';

const StyledMediaTagsContainer = styled.div`
  width: 100%;

  .media-tags {
    &:empty {
      display: none;
    }
  }

  .media-tags__language {
    white-space: nowrap;
  }

  .media-tags__list {
    display: flex;
    flex-wrap: wrap;
    list-style: none;
    padding: ${units(0.5)};
    margin: 0;

    li {
      margin: ${units(0.5)};
    }
  }
`;

// TODO Fix a11y issues
/* eslint jsx-a11y/click-events-have-key-events: 0 */
/* eslint jsx-a11y/no-noninteractive-element-interactions: 0 */
class MediaTags extends React.Component {
  filterTags = memoize((tags) => {
    const splitTags = {
      regularTags: [],
      videoTags: [],
    };
    const fragments = {};

    // Get regular tags and cluster video tags by tag_text
    if (Array.isArray(tags)) {
      tags.forEach((t) => {
        if (t.node.fragment) {
          if (!fragments[t.node.tag_text]) {
            fragments[t.node.tag_text] = [];
          }
          fragments[t.node.tag_text].push(t);
        } else {
          splitTags.regularTags.push(t);
        }
      });
    }

    // Get the video tags with earliest timestamp
    Object.values(fragments).forEach((tagFragments) => {
      tagFragments.sort((a, b) => {
        const aStart = parseFloat(a.node.fragment.match(/\d+(\.\d+)?/)[0]);
        const bStart = parseFloat(b.node.fragment.match(/\d+(\.\d+)?/)[0]);
        return aStart - bStart;
      });

      splitTags.videoTags.push(tagFragments[0]);
    });

    return splitTags;
  });

  searchTagUrl(tagString) {
    const { projectMedia } = this.props;
    const tagQuery = {
      tags: [tagString],
    };
    const searchQuery = searchQueryFromUrl();

    // Make a new query combining the current tag with whatever query is already in the URL.
    // This allows to support clicking tags on the search and project pages.
    const query = mergeWith({}, searchQuery, tagQuery, (objValue, srcValue) => {
      if (Array.isArray(objValue)) {
        return xor(objValue, srcValue);
      }
      return undefined;
    });
    if (!query.tags.length) {
      delete query.tags;
    }
    return urlFromSearchQuery(query, `/${projectMedia.team.slug}/all-items`);
  }

  handleTagViewClick(tagString) {
    const url = this.searchTagUrl(tagString);
    if (window !== window.parent) { // Browser extension
      window.open(`${window.location.origin}${url}`);
    } else {
      browserHistory.push(url);
    }
  }

  handleVideoAnnotationIconClick = (e, fragment) => {
    e.stopPropagation();
    if (this.props.onTimelineCommentOpen) {
      this.props.onTimelineCommentOpen(fragment);
    }
  };

  render() {
    const { projectMedia } = this.props;
    const { regularTags, videoTags } = this.filterTags(projectMedia.tags.edges);
    const tags = regularTags.concat(videoTags);

    return (

    );
  }
}

MediaTags.propTypes = {
  projectMedia: PropTypes.shape({
    tags: PropTypes.shape({
      edges: PropTypes.arrayOf(PropTypes.shape({
        node: PropTypes.shape({
          tag: PropTypes.string.isRequired,
          id: PropTypes.string.isRequired,
          tag_text: PropTypes.string.isRequired,
        }),
      }).isRequired).isRequired,
    }).isRequired,
  }).isRequired,
};

export { MediaTags };
export default createFragmentContainer(MediaTags, graphql`
  fragment MediaTags_projectMedia on ProjectMedia {
    id
    dbid
    team {
      slug
    }
    tags(first: 10000) {
      edges {
        node {
          id
          tag
          tag_text
          fragment
        }
      }
    }
    ...MediaLanguageChip_projectMedia
  }
`);
<|endoftext|>


<commit_before>/**
 * Handles native system notifications for new unread notifications in Pontoon.
 * @requires commons/js/Options.js, RemotePontoon.js
 */
class SystemNotifications {
    /**
     * Initialize instance and watch for storage updates.
     * @param options
     * @param remotePontoon
     */
    constructor(options, remotePontoon) {
        this._options = options;
        this._remotePontoon = remotePontoon;

        this._watchNotificationClicks();
        this._watchStorageChanges();
    }

    /**
     * Register a listener for notification clicks.
     * @private
     */
    _watchNotificationClicks() {
        browser.notifications.onClicked.addListener((notificationId) => this._notificationClick(notificationId));
    }

    /**
     * Handle click to a system notification.
     * @param notificationId corresponding to the Pontoon notification id, if the notification is not grouped
     * @private
     */
    async _notificationClick(notificationId) {
        const dataKey = 'notificationsData';
        const item = await browser.storage.local.get(dataKey);
        const notificationsData = item[dataKey];
        const notification = notificationsData[notificationId];
        if (notification && !notification.target) {
            browser.tabs.create({url: this._remotePontoon.getTeamProjectUrl(notification.actor.link)});
        } else {
            browser.tabs.create({url: this._remotePontoon.getTeamPageUrl()});
        }
        browser.notifications.clear(notificationId);
    }

    /**
     * Check for new unread notifications if the storage gets updated.
     * @private
     */
    _watchStorageChanges() {
        this._remotePontoon.subscribeToNotificationsChange(
            (change) => {
                const notificationsData = change.newValue;
                Promise.all([
                    this._getNewUnreadNotifications(notificationsData),
                    this._options.get('show_notifications').then((option) => option['show_notifications'])
                ]).then(([
                    newUnreadNotificationIds,
                    showNotifications
                ]) => {
                    if (showNotifications && newUnreadNotificationIds.length > 0) {
                        this._notifyAboutUnreadNotifications(newUnreadNotificationIds, notificationsData);
                    }
                });
            }
        );
    }

    /**
     * Get new unread notifications that the user hasn't been notified about.
     * @param notificationsData
     * @returns {Promise.<Array.<number>>} promise that will be fulfilled with the list of unread notification ids
     * @private
     * @async
     */
    async _getNewUnreadNotifications(notificationsData) {
        let unreadNotificationIds = [0];
        if (notificationsData !== undefined) {
            unreadNotificationIds = Object.keys(notificationsData)
                .map((key) => parseInt(key))
                .filter((id) => notificationsData[id].unread);
        }
        const dataKey = 'lastUnreadNotificationId';
        const lastKnownUnreadNotificationId = await browser.storage.local.get(dataKey).then((item) => {
            return item[dataKey] || 0;
        });
        return unreadNotificationIds.filter((id) => id > lastKnownUnreadNotificationId);
    }

    /**
     * Show a system notification about new unread notifications in Pontoon.
     * @param unreadNotificationIds
     * @param notificationsData
     * @private
     */
    _notifyAboutUnreadNotifications(unreadNotificationIds, notificationsData) {
        const notificationItems = unreadNotificationIds.sort().reverse()
            .map((id) => notificationsData[id])
            .map((notification) => {
                const item = {
                    title: '',
                    message: '',
                };
                if (notification.actor) {
                    item.title = `${item.title} ${notification.actor.text}`;
                }
                if (notification.verb) {
                    item.title = `${item.title} ${notification.verb}`;
                }
                if (notification.target) {
                    item.title = `${item.title} ${notification.target.text}`;
                }
                if (notification.message) {
                    item.message = notification.message;
                }
                return item;
            });
        const lastNotificationId = unreadNotificationIds.sort().reverse()[0];
        if (notificationItems.length === 1) {
            browser.notifications.create(
                `${lastNotificationId}`,
                {
                    type: 'basic',
                    iconUrl: browser.runtime.getURL('commons/img/pontoon-logo.svg'),
                    title: notificationItems[0].title,
                    message: notificationItems[0].message,
                }
            );
        } else {
            browser.notifications.create({
                type: 'list',
                iconUrl: browser.runtime.getURL('commons/img/pontoon-logo.svg'),
                title: 'You have new unread notifications',
                message: `There are ${notificationItems.length} new unread notifications in Pontoon for you.`,
                items: notificationItems,
            });
        }
        browser.storage.local.set({lastUnreadNotificationId: lastNotificationId});
    }
}
<commit_msg>Fix system notifications broken by #98<commit_after>/**
 * Handles native system notifications for new unread notifications in Pontoon.
 * @requires commons/js/Options.js, RemotePontoon.js
 */
class SystemNotifications {
    /**
     * Initialize instance and watch for storage updates.
     * @param options
     * @param remotePontoon
     */
    constructor(options, remotePontoon) {
        this._options = options;
        this._remotePontoon = remotePontoon;

        this._watchNotificationClicks();
        this._watchStorageChanges();
    }

    /**
     * Register a listener for notification clicks.
     * @private
     */
    _watchNotificationClicks() {
        browser.notifications.onClicked.addListener((notificationId) => this._notificationClick(notificationId));
    }

    /**
     * Handle click to a system notification.
     * @param notificationId corresponding to the Pontoon notification id, if the notification is not grouped
     * @private
     */
    async _notificationClick(notificationId) {
        const dataKey = 'notificationsData';
        const item = await browser.storage.local.get(dataKey);
        const notificationsData = item[dataKey];
        const notification = notificationsData[notificationId];
        if (notification && !notification.target) {
            browser.tabs.create({url: this._remotePontoon.getTeamProjectUrl(notification.actor.url)});
        } else {
            browser.tabs.create({url: this._remotePontoon.getTeamPageUrl()});
        }
        browser.notifications.clear(notificationId);
    }

    /**
     * Check for new unread notifications if the storage gets updated.
     * @private
     */
    _watchStorageChanges() {
        this._remotePontoon.subscribeToNotificationsChange(
            (change) => {
                const notificationsData = change.newValue;
                Promise.all([
                    this._getNewUnreadNotifications(notificationsData),
                    this._options.get('show_notifications').then((option) => option['show_notifications'])
                ]).then(([
                    newUnreadNotificationIds,
                    showNotifications
                ]) => {
                    if (showNotifications && newUnreadNotificationIds.length > 0) {
                        this._notifyAboutUnreadNotifications(newUnreadNotificationIds, notificationsData);
                    }
                });
            }
        );
    }

    /**
     * Get new unread notifications that the user hasn't been notified about.
     * @param notificationsData
     * @returns {Promise.<Array.<number>>} promise that will be fulfilled with the list of unread notification ids
     * @private
     * @async
     */
    async _getNewUnreadNotifications(notificationsData) {
        let unreadNotificationIds = [0];
        if (notificationsData !== undefined) {
            unreadNotificationIds = Object.keys(notificationsData)
                .map((key) => parseInt(key))
                .filter((id) => notificationsData[id].unread);
        }
        const dataKey = 'lastUnreadNotificationId';
        const lastKnownUnreadNotificationId = await browser.storage.local.get(dataKey).then((item) => {
            return item[dataKey] || 0;
        });
        return unreadNotificationIds.filter((id) => id > lastKnownUnreadNotificationId);
    }

    /**
     * Show a system notification about new unread notifications in Pontoon.
     * @param unreadNotificationIds
     * @param notificationsData
     * @private
     */
    _notifyAboutUnreadNotifications(unreadNotificationIds, notificationsData) {
        const notificationItems = unreadNotificationIds.sort().reverse()
            .map((id) => notificationsData[id])
            .map((notification) => {
                const item = {
                    title: '',
                    message: '',
                };
                if (notification.actor) {
                    item.title = `${item.title} ${notification.actor.anchor}`;
                }
                if (notification.verb) {
                    item.title = `${item.title} ${notification.verb}`;
                }
                if (notification.target) {
                    item.title = `${item.title} ${notification.target.anchor}`;
                }
                if (notification.message) {
                    item.message = notification.message;
                }
                return item;
            });
        const lastNotificationId = unreadNotificationIds.sort().reverse()[0];
        if (notificationItems.length === 1) {
            browser.notifications.create(
                `${lastNotificationId}`,
                {
                    type: 'basic',
                    iconUrl: browser.runtime.getURL('commons/img/pontoon-logo.svg'),
                    title: notificationItems[0].title,
                    message: notificationItems[0].message,
                }
            );
        } else {
            browser.notifications.create({
                type: 'list',
                iconUrl: browser.runtime.getURL('commons/img/pontoon-logo.svg'),
                title: 'You have new unread notifications',
                message: `There are ${notificationItems.length} new unread notifications in Pontoon for you.`,
                items: notificationItems,
            });
        }
        browser.storage.local.set({lastUnreadNotificationId: lastNotificationId});
    }
}
<|endoftext|>


<commit_before>angular.module('angular-themer', [])

	.provider('themer', function() {
		"use strict";

		var _styles = [], _selected = { label: '', href: '' };

		this.setStyles = function (styles) {
			_styles = styles;
		};

		this.setSelected = function (selected) {
			_selected = selected;
		};

		this.$get = [function () {
			return {
				styles: _styles,
				selected: _selected
			};
		}]

	})

	.directive('themerLink', function() {
		"use strict";

		return {
			restrict: 'A',
			template:  '<link rel="stylesheet" ng-href="{{ selected.href }}" />',
			replace: true,
			scope: true,
			controller: ['$scope', 'themer', function ($scope, themer) {
				$scope.selected = themer.selected;
			}]
		};

	})

	.directive('themerSwitcher', function () {
		"use strict";

		return {
			restrict: 'A',
			template: '<select ng-model="theme.selected.href" ng-options="style.href as style.label for style in theme.styles"></select>',
			scope: false,
			controller: ['$scope', 'themer', function ($scope, themer) {
				$scope.theme = {
					styles: themer.styles,
					selected: themer.selected
				};
			}]
		};
	})

;<commit_msg>style(themer): Add missing semicolon<commit_after>angular.module('angular-themer', [])

	.provider('themer', function() {
		"use strict";

		var _styles = [], _selected = { label: '', href: '' };

		this.setStyles = function (styles) {
			_styles = styles;
		};

		this.setSelected = function (selected) {
			_selected = selected;
		};

		this.$get = [function () {
			return {
				styles: _styles,
				selected: _selected
			};
		}];

	})

	.directive('themerLink', function() {
		"use strict";

		return {
			restrict: 'A',
			template:  '<link rel="stylesheet" ng-href="{{ selected.href }}" />',
			replace: true,
			scope: true,
			controller: ['$scope', 'themer', function ($scope, themer) {
				$scope.selected = themer.selected;
			}]
		};

	})

	.directive('themerSwitcher', function () {
		"use strict";

		return {
			restrict: 'A',
			template: '<select ng-model="theme.selected.href" ng-options="style.href as style.label for style in theme.styles"></select>',
			scope: false,
			controller: ['$scope', 'themer', function ($scope, themer) {
				$scope.theme = {
					styles: themer.styles,
					selected: themer.selected
				};
			}]
		};
	})

;<|endoftext|>