<commit_before>'use strict';

theToolController.controller('TopicsController', function ($scope, $location, $routeParams, TopicFactory) {

  //================================INITIALIZATION================================

  if($routeParams.id == "me") {
    $location.path('/topics/' + $scope.me.id);
    return;
  }

  $scope.loading = true;

  $scope.kinds = ["Info", "To do", "Decision", "Idea"];

  if ($location.path() == '/topics') {
    TopicFactory.Topics.getAll(gotTopics);
  }
  else {
    TopicFactory.TargetTopics.getAll({id: $scope.me.id}, gotTopics);
  }

  function gotTopics (topics) {
    setTimeout(function () {
      if ($scope.loading)
        gotTopics(topics);
    }, 1000);

    $scope.topics = topics;

    for (var i = 0, j = $scope.topics.length; i < j; i++) {
      $scope.topics[i].facebook = $scope.members.filter(function(o) {
        return $scope.topics[i].author == o.id;
      })[0].facebook;
    }

    $scope.loading = false;
  }

  //===================================FUNCTIONS===================================

  $scope.time = function(date) {
    return $scope.timeSince(new Date(date));
  };

  $scope.createTopic = function(kind) {
    var date = new Date();
    TopicFactory.Topics.create({
      //author: $scope.me.id,
      kind: kind
    }, function(response) {
      console.log(response);
      if (response.success) {
        $location.path("/topic/" + response.id + "/edit");
      }
    });
  };

});
<commit_msg>bugfix<commit_after>'use strict';

theToolController.controller('TopicsController', function ($scope, $location, $routeParams, TopicFactory) {

  //================================INITIALIZATION================================

  if($location.path() != '/topics' && $routeParams.id == "me") {
    $location.path('/topics/' + $scope.me.id);
    return;
  }

  $scope.loading = true;

  $scope.kinds = ["Info", "To do", "Decision", "Idea"];

  if ($location.path() == '/topics') {
    TopicFactory.Topics.getAll(gotTopics);
  }
  else {
    TopicFactory.TargetTopics.getAll({id: $routeParams.id}, gotTopics);
  }

  function gotTopics (topics) {
    setTimeout(function () {
      if ($scope.loading)
        gotTopics(topics);
    }, 1000);

    $scope.topics = topics;

    for (var i = 0, j = $scope.topics.length; i < j; i++) {
      $scope.topics[i].facebook = $scope.members.filter(function(o) {
        return $scope.topics[i].author == o.id;
      })[0].facebook;
    }

    $scope.loading = false;
  }

  //===================================FUNCTIONS===================================

  $scope.time = function(date) {
    return $scope.timeSince(new Date(date));
  };

  $scope.createTopic = function(kind) {
    var date = new Date();
    TopicFactory.Topics.create({
      //author: $scope.me.id,
      kind: kind
    }, function(response) {
      console.log(response);
      if (response.success) {
        $location.path("/topic/" + response.id + "/edit");
      }
    });
  };

});
<|endoftext|>


<commit_before>'use strict';

// MODULES //

var tape = require( 'tape' );
var lpad = require( './../lib' );

// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.equal( typeof lpad, 'function', 'main export is a function' );
	t.end();
});

tape( 'if the first argument is not a string primitive, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function(){}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( value, 10 );
		};
	}
});

tape( 'if the second argument is not a nonnegative integer, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		3.14,
		-5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function(){}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( 'beep', value );
		};
	}
});

tape( 'if the third argument is not a string primitive, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function(){}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( 'beep', 10, value );
		};
	}
});

tape( 'if the output string will exceed the maximum allowed string length, the function will throw an error', function test( t ) {
	t.throws( foo, RangeError, 'throws RangeError' );
	t.end();
	function foo() {
		lpad( 'beep', 1e300 );
	}
});

tape( 'if provided an empty pad string, the function throws an error', function test( t ) {
	t.throws( foo, RangeError, 'throws RangeError' );
	t.end();
	function foo() {
		lpad( 'beep', 10, '' );
	}
});

tape( 'by default, the function left pads a string with spaces', function test( t ) {
	var str = lpad( 'a', 5 );
	t.equal( str, '    a', 'left padded with spaces' );
	t.end();
});

tape( 'the function supports left padding a string with a custom pad string', function test( t ) {
	var str = lpad( 'beep', 10, 'b' );
	t.equal( str, 'bbbbbbbeep', 'left padded to desired length' );
	t.end();
});

tape( 'the function left pads a string such that an output string may exceed the specified length (minimum bound)', function test( t ) {
	var str = lpad( 'a', 5, 'beepboop' );
	t.equal( str, 'beepboopa', 'left padded and length exceeds minimum length' );
	t.end();
});

tape( 'if the specified string length is less than or equal to the input string length, the function returns the input string', function test( t ) {
	t.equal( lpad( 'boop', 2, 'beep' ), 'boop', 'returns input string (<)' );
	t.equal( lpad( 'boop', 4, 'beep' ), 'boop', 'returns input string (=)' );
	t.end();
});
<commit_msg>Fix lint errors<commit_after>'use strict';

// MODULES //

var tape = require( 'tape' );
var lpad = require( './../lib' );

// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.equal( typeof lpad, 'function', 'main export is a function' );
	t.end();
});

tape( 'if the first argument is not a string primitive, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( value, 10 );
		};
	}
});

tape( 'if the second argument is not a nonnegative integer, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		3.14,
		-5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( 'beep', value );
		};
	}
});

tape( 'if the third argument is not a string primitive, the function will throw an error', function test( t ) {
	var values;
	var i;

	values = [
		5,
		true,
		NaN,
		null,
		undefined,
		[],
		{},
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();
	function badValue( value ) {
		return function badValue() {
			lpad( 'beep', 10, value );
		};
	}
});

tape( 'if the output string will exceed the maximum allowed string length, the function will throw an error', function test( t ) {
	t.throws( foo, RangeError, 'throws RangeError' );
	t.end();
	function foo() {
		lpad( 'beep', 1e300 );
	}
});

tape( 'if provided an empty pad string, the function throws an error', function test( t ) {
	t.throws( foo, RangeError, 'throws RangeError' );
	t.end();
	function foo() {
		lpad( 'beep', 10, '' );
	}
});

tape( 'by default, the function left pads a string with spaces', function test( t ) {
	var str = lpad( 'a', 5 );
	t.equal( str, '    a', 'left padded with spaces' );
	t.end();
});

tape( 'the function supports left padding a string with a custom pad string', function test( t ) {
	var str = lpad( 'beep', 10, 'b' );
	t.equal( str, 'bbbbbbbeep', 'left padded to desired length' );
	t.end();
});

tape( 'the function left pads a string such that an output string may exceed the specified length (minimum bound)', function test( t ) {
	var str = lpad( 'a', 5, 'beepboop' );
	t.equal( str, 'beepboopa', 'left padded and length exceeds minimum length' );
	t.end();
});

tape( 'if the specified string length is less than or equal to the input string length, the function returns the input string', function test( t ) {
	t.equal( lpad( 'boop', 2, 'beep' ), 'boop', 'returns input string (<)' );
	t.equal( lpad( 'boop', 4, 'beep' ), 'boop', 'returns input string (=)' );
	t.end();
});
<|endoftext|>


<commit_before>/**
 * # http.js
 * Copyright(c) 2020 Stefano Balietti
 * MIT Licensed
 *
 * Configuration file for Express server in nodegame-server
 */
module.exports = configure;

// ## Global scope

const express = require('express');
const path = require('path');
const J = require('nodegame-client').JSUS;

const mime = require('express').static.mime;

const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const errorHandler = require('errorhandler');

// var session = require('cookie-session')({ secret: 'secret' });

/**
 * ### configure
 *
 * Defines standard routes for the HTTP server
 *
 * @param {object} options The object containing the custom settings
 */
function configure(app, servernode) {

    let rootDir = servernode.rootDir;
    let publicDir = path.join(rootDir, 'public');
    let basepath = servernode.basepath || '';
    let resourceManager = servernode.resourceManager;

    app.set('views', path.resolve(rootDir, 'views'));
    app.set('view engine', 'jade');
    app.set('view options', {layout: false});

    if (process.env.NODE_ENV === 'development') {
        app.use(express.static(publicDir));
        app.use(errorHandler());
    }
    else {
        app.use(express.static(publicDir, { maxAge: 3600000 }));
    }

    app.disable('x-powered-by');
    app.enable("jsonp callback");

    app.use(cookieParser());
    // app.use(bodyParser());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    function sendFromPublic(type, req, res, headers) {
        let file = req.params[0];
        if (!file) return;

        // Build path in `public/`.
        file = path.join(type, file);

        let mimeType, charset, filePath;

        // Build headers.
        if (!headers) {
            mimeType = mime.lookup(file);
            charset = mime.charsets.lookup(mimeType);
            headers = { 'Content-Type': mimeType };
            if (charset) headers.charset = charset;
        }

        // If it is not text, it was not cached.
        if (headers['Content-Type'].substring(0,4) !== 'text') {
            filePath = path.resolve(publicDir, file);
            res.sendFile(filePath);
            return;
        }

        // Already found in `public/` and cached.
        resourceManager.getFromPublic('/', file, function(cachedFile) {

            // File found in public (cached or loaded).
            if (cachedFile) {
                res.set(headers);
                res.status(200).send(cachedFile);
            }
            else {
                // Send 404.
                res.status(404).send('File not found.');
            }
        });
    }

    app.get(basepath + '/javascripts/*', function(req, res) {
        sendFromPublic('javascripts', req, res, {
            'Content-Type': 'text/javascript',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/sounds/*', function(req, res) {
        sendFromPublic('sounds', req, res, {
            'Content-Type': 'sound/ogg'
        });
    });

    app.get(basepath + '/stylesheets/*', function(req, res) {
        sendFromPublic('stylesheets', req, res, {
            'Content-Type': 'text/css',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/images/*', function(req, res) {
        sendFromPublic('images', req, res, {
            'Content-Type': 'image/png'
        });
    });

    app.get(basepath + '/pages/*', function(req, res) {
        sendFromPublic('pages', req, res, {
            'Content-Type': 'text/html',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/lib/*', function(req, res) {
        sendFromPublic('lib', req, res);
    });

    app.get(basepath + '/', function(req, res, next) {

        // Must be first.
        if (req.query) {
            let q = req.query.q;
            if (q) {
                if (!servernode.enableInfoQuery) {
                    res.status(403).end();
                    return;
                }

                let game = req.query.game;
                game = servernode.channels[game];

                switch(q) {
                case 'info':
                    res.status(200).send(servernode.info);
                    break;

                case 'channels':
                    // res.header("Access-Control-Allow-Origin", "*");
                    // res.header("Access-Control-Allow-Headers",
                    //            "X-Requested-With");
                    res.status(200).send(servernode.info.channels);
                    break;

                case 'games':
                    res.status(200).send(servernode.info.games);
                    break;

                case 'waitroom':
                    if (!game) {
                        // TODO: Return info for all games or
                        // take into account default channel.
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        let level = req.query.level;
                        if (level) {
                            game = game.gameLevels[level];
                            if (!game) {
                                res.status(400).send("Level not found: " +
                                                     level);
                                break;
                            }
                        }

                        let room = game.waitingRoom;

                        if (!room) {
                            res.status(400)
                               .send("Channel/level has no waitroom.");
                        }
                        else {
                            let nPlayers = room.clients.playerConnected.size();
                            res.status(200).send({
                                nPlayers: nPlayers,
                                open: room.isRoomOpen()
                            });
                        }
                    }
                    break;

                // @experimental
                case 'clients':
                    if (!game) {
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        let nClients = game.registry.clients.connectedPlayer;
                        nClients = nClients.size();
                        res.status(200).send({ nClients: nClients });
                    }
                    break;

                // @experimental
                case 'highscore':
                    if (!game) {
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        res.status(200).send({ highscore: game.highscore });
                    }
                    break;

                default:
                    res.status(400).send('Unknown query received.');
                }
                return;
            }
        }

        if (servernode.defaultChannel) {
            next();
            return;
        }

        if (servernode.homePage === false ||
            servernode.homePage.enabled === false) {

            res.render('index_simple', {
                title: 'Yay! nodeGame server is running.'
            });
        }
        else {
            let gamesObj = servernode.info.games;
            let listOfGames = J.keys(gamesObj);
            // Remove aliases.
            let filteredGames = listOfGames.filter(function(name) {
                return (!gamesObj[name].disabled && !gamesObj[name].errored &&
                        (!gamesObj[name].alias ||
                         gamesObj[name].alias.indexOf(name) === -1));
            });
            if (J.isArray(servernode.homePage.cardsOrder)) {
                filteredGames =
                    servernode.homePage.cardsOrder.filter(function(name) {
                        if (filteredGames.indexOf(name) !== -1) return true;
                        servernode.logger.error('homePage.cardsOrder ' +
                                                'game not found: ' + name);
                    });
            }
            else {
                filteredGames.sort();
            }

            let games = [];
            let colors = servernode.homePage.colors;

            let i = 0;
            for (let j = 0; j < filteredGames.length; j++) {
                let name = filteredGames[j];
                if (i >= colors.length) i = 0;
                let color = colors[i];
                // Mixout name and description from package.json
                // if not in card, or if no card is defined.
                let card = J.mixout(gamesObj[name].info.card || {}, {
                    name: name.charAt(0).toUpperCase() + name.slice(1),
                    description: gamesObj[name].info.description
                });
                games.push({
                    name: card.name,
                    color: color,
                    url: card.url,
                    description: card.description,
                    publication: card.publication,
                    wiki: card.wiki,
                    icon: card.icon
                });
                i++;
            }
            let externalCards = servernode.homePage.externalCards;
            if (externalCards && externalCards.length) {
                for (let j = 0; j < externalCards.length; j++) {
                    externalCards[j].color = colors[i++];
                    externalCards[j].external = true;
                    games.push(externalCards[j]);
                }
            }
            res.render('homepage', {
                title: servernode.homePage.title,
                games: games,
                nodeGameCard: servernode.homePage.nodeGameCard,
                footerContent: servernode.homePage.footerContent,
                logo: servernode.homePage.logo
            });
        }

    });

    return true;
}
<commit_msg>conf minor<commit_after>/**
 * # http.js
 * Copyright(c) 2020 Stefano Balietti
 * MIT Licensed
 *
 * Configuration file for Express server in nodegame-server
 */
module.exports = configure;

// ## Global scope

const express = require('express');
const path = require('path');
const J = require('nodegame-client').JSUS;

const mime = require('express').static.mime;

// const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const errorHandler = require('errorhandler');

// var session = require('cookie-session')({ secret: 'secret' });

/**
 * ### configure
 *
 * Defines standard routes for the HTTP server
 *
 * @param {object} options The object containing the custom settings
 */
function configure(app, servernode) {

    let rootDir = servernode.rootDir;
    let publicDir = path.join(rootDir, 'public');
    let basepath = servernode.basepath || '';
    let resourceManager = servernode.resourceManager;

    app.set('views', path.resolve(rootDir, 'views'));
    app.set('view engine', 'jade');
    app.set('view options', {layout: false});

    if (process.env.NODE_ENV === 'development') {
        app.use(express.static(publicDir));
        app.use(errorHandler());
    }
    else {
        app.use(express.static(publicDir, { maxAge: 3600000 }));
    }

    app.disable('x-powered-by');
    app.enable("jsonp callback");

    app.use(cookieParser());
    // app.use(bodyParser());
    // app.use(bodyParser.json());
    // app.use(bodyParser.urlencoded({ extended: true }));

    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    function sendFromPublic(type, req, res, headers) {
        let file = req.params[0];
        if (!file) return;

        // Build path in `public/`.
        file = path.join(type, file);

        let mimeType, charset, filePath;

        // Build headers.
        if (!headers) {
            mimeType = mime.lookup(file);
            charset = mime.charsets.lookup(mimeType);
            headers = { 'Content-Type': mimeType };
            if (charset) headers.charset = charset;
        }

        // If it is not text, it was not cached.
        if (headers['Content-Type'].substring(0,4) !== 'text') {
            filePath = path.resolve(publicDir, file);
            res.sendFile(filePath);
            return;
        }

        // Already found in `public/` and cached.
        resourceManager.getFromPublic('/', file, function(cachedFile) {

            // File found in public (cached or loaded).
            if (cachedFile) {
                res.set(headers);
                res.status(200).send(cachedFile);
            }
            else {
                // Send 404.
                res.status(404).send('File not found.');
            }
        });
    }

    app.get(basepath + '/javascripts/*', function(req, res) {
        sendFromPublic('javascripts', req, res, {
            'Content-Type': 'text/javascript',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/sounds/*', function(req, res) {
        sendFromPublic('sounds', req, res, {
            'Content-Type': 'sound/ogg'
        });
    });

    app.get(basepath + '/stylesheets/*', function(req, res) {
        sendFromPublic('stylesheets', req, res, {
            'Content-Type': 'text/css',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/images/*', function(req, res) {
        sendFromPublic('images', req, res, {
            'Content-Type': 'image/png'
        });
    });

    app.get(basepath + '/pages/*', function(req, res) {
        sendFromPublic('pages', req, res, {
            'Content-Type': 'text/html',
            'charset': 'utf-8'
        });
    });

    app.get(basepath + '/lib/*', function(req, res) {
        sendFromPublic('lib', req, res);
    });

    app.get(basepath + '/', function(req, res, next) {

        // Must be first.
        if (req.query) {
            let q = req.query.q;
            if (q) {
                if (!servernode.enableInfoQuery) {
                    res.status(403).end();
                    return;
                }

                let game = req.query.game;
                game = servernode.channels[game];

                switch(q) {
                case 'info':
                    res.status(200).send(servernode.info);
                    break;

                case 'channels':
                    // res.header("Access-Control-Allow-Origin", "*");
                    // res.header("Access-Control-Allow-Headers",
                    //            "X-Requested-With");
                    res.status(200).send(servernode.info.channels);
                    break;

                case 'games':
                    res.status(200).send(servernode.info.games);
                    break;

                case 'waitroom':
                    if (!game) {
                        // TODO: Return info for all games or
                        // take into account default channel.
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        let level = req.query.level;
                        if (level) {
                            game = game.gameLevels[level];
                            if (!game) {
                                res.status(400).send("Level not found: " +
                                                     level);
                                break;
                            }
                        }

                        let room = game.waitingRoom;

                        if (!room) {
                            res.status(400)
                               .send("Channel/level has no waitroom.");
                        }
                        else {
                            let nPlayers = room.clients.playerConnected.size();
                            res.status(200).send({
                                nPlayers: nPlayers,
                                open: room.isRoomOpen()
                            });
                        }
                    }
                    break;

                // @experimental
                case 'clients':
                    if (!game) {
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        let nClients = game.registry.clients.connectedPlayer;
                        nClients = nClients.size();
                        res.status(200).send({ nClients: nClients });
                    }
                    break;

                // @experimental
                case 'highscore':
                    if (!game) {
                        res.status(400).send("You must specify a valid game.");
                    }
                    else {
                        res.status(200).send({ highscore: game.highscore });
                    }
                    break;

                default:
                    res.status(400).send('Unknown query received.');
                }
                return;
            }
        }

        if (servernode.defaultChannel) {
            next();
            return;
        }

        if (servernode.homePage === false ||
            servernode.homePage.enabled === false) {

            res.render('index_simple', {
                title: 'Yay! nodeGame server is running.'
            });
        }
        else {
            let gamesObj = servernode.info.games;
            let listOfGames = J.keys(gamesObj);
            // Remove aliases.
            let filteredGames = listOfGames.filter(function(name) {
                return (!gamesObj[name].disabled && !gamesObj[name].errored &&
                        (!gamesObj[name].alias ||
                         gamesObj[name].alias.indexOf(name) === -1));
            });
            if (J.isArray(servernode.homePage.cardsOrder)) {
                filteredGames =
                    servernode.homePage.cardsOrder.filter(function(name) {
                        if (filteredGames.indexOf(name) !== -1) return true;
                        servernode.logger.error('homePage.cardsOrder ' +
                                                'game not found: ' + name);
                    });
            }
            else {
                filteredGames.sort();
            }

            let games = [];
            let colors = servernode.homePage.colors;

            let i = 0;
            for (let j = 0; j < filteredGames.length; j++) {
                let name = filteredGames[j];
                if (i >= colors.length) i = 0;
                let color = colors[i];
                // Mixout name and description from package.json
                // if not in card, or if no card is defined.
                let card = J.mixout(gamesObj[name].info.card || {}, {
                    name: name.charAt(0).toUpperCase() + name.slice(1),
                    description: gamesObj[name].info.description
                });
                games.push({
                    name: card.name,
                    color: color,
                    url: card.url,
                    description: card.description,
                    publication: card.publication,
                    wiki: card.wiki,
                    icon: card.icon
                });
                i++;
            }
            let externalCards = servernode.homePage.externalCards;
            if (externalCards && externalCards.length) {
                for (let j = 0; j < externalCards.length; j++) {
                    externalCards[j].color = colors[i++];
                    externalCards[j].external = true;
                    games.push(externalCards[j]);
                }
            }
            res.render('homepage', {
                title: servernode.homePage.title,
                games: games,
                nodeGameCard: servernode.homePage.nodeGameCard,
                footerContent: servernode.homePage.footerContent,
                logo: servernode.homePage.logo
            });
        }

    });

    return true;
}
<|endoftext|>


<commit_before>module.exports = validateParamTagDescription;
module.exports.tags = ['param', 'arg', 'argument'];
module.exports.scopes = ['function'];
module.exports.options = {
    requireParamDescription: {allowedValues: [true]}
};

/**
 * Validator for @param
 *
 * @param {(FunctionDeclaration|FunctionExpression)} node
 * @param {DocTag} tag
 * @param {Function} err
 */
function validateParamTagDescription(node, tag, err) {
    // checking existance
    if (!tag.description) {
        var offset = 0;
        if (tag.name && tag.name.value && tag.name.value.length) {
            offset = tag.name.value.length;
        }
        var loc = (tag.name ? tag.name.loc : null) || tag.loc;

        // we create a new one to prevent modifing given location
        var locWithOffset = {
            line: loc.line,
            column: loc.column + offset
        };
        return err('Missing param description', locWithOffset);
    }
}
<commit_msg>addressed the comments<commit_after>module.exports = validateParamTagDescription;
module.exports.tags = ['param', 'arg', 'argument'];
module.exports.scopes = ['function'];
module.exports.options = {
    requireParamDescription: {allowedValues: [true]}
};

/**
 * Validator for @param
 *
 * @param {(FunctionDeclaration|FunctionExpression)} node
 * @param {DocTag} tag
 * @param {Function} err
 */
function validateParamTagDescription(node, tag, err) {
    // checking existance
    if (tag.description) {
        return;
    }
    var offset = 0;
    if (tag.name && tag.name.value && tag.name.value.length) {
        offset = tag.name.value.length;
    }
    var loc = (tag.name ? tag.name.loc : null) || tag.loc;

    // we create a new one to prevent modifing given location
    var locWithOffset = {
        line: loc.line,
        column: loc.column + offset
    };
    return err('Missing param description', locWithOffset);
}
<|endoftext|>


<commit_before>/*! overthrow.js. An overflow:auto polyfill for responsive design. (c) 2012: Scott Jehl, Filament Group, Inc. Dual MIT/GPLv2 license */
(function( w, undefined ){

	var doc = w.document,
		docElem = doc.documentElement,
		classtext = "overthrow",

		// Touch events are used in the polyfill, and thus are a prerequisite
		canBeFilledWithPoly = "ontouchmove" in doc,

		// The following attempts to determine whether the browser has native overflow support
		// so we can enable it but not polyfill
		overflowProbablyAlreadyWorks =
			// Features-first. iOS5 overflow scrolling property check - no UA needed here. thanks Apple :)
			"WebkitOverflowScrolling" in docElem.style
			// Touch events aren't supported and screen width is greater than X
			// ...basically, this is a loose "desktop browser" check.
			// It may wrongly opt-in very large tablets with no touch support.
			|| ( !canBeFilledWithPoly && w.screen.width > 1200 )
			// Hang on to your hats.
			// Whitelist some popular, overflow-supporting mobile browsers for now and the future
			// These browsers are known to get overlow support right, but give us no way of detecting it.
			|| (function(){
				var ua = w.navigator.userAgent,
					// Webkit crosses platforms, and the browsers on our list run at least version 534
					webkit = ua.match( /AppleWebKit\/([0-9]+)/ ),
					wkversion = webkit && webkit[1],
					wkLte534 = webkit && wkversion >= 534;

				return (
					/* Android 3+ with webkit gte 534
					   Sample: Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 */
					ua.match( /Android ([0-9]+)/ ) && RegExp.$1 >= 3 && wkLte534
					/* Blackberry 7+ with webkit gte 534
					   ~: Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0 Mobile Safari/534.11+ */
					|| ua.match( / Version\/([0-9]+)/ ) && RegExp.$1 >= 0 && w.blackberry && wkLte534
					/* Blackberry Playbook with webkit gte 534
					   ~: Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/0.0.1 Safari/534.8+ */
					|| ua.indexOf( /PlayBook/ ) > -1 && RegExp.$1 >= 0 && wkLte534
					/* Firefox Mobile (Fennec) 4 and up
					   ~: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:2.1.1) Gecko/ Firefox/4.0.2pre Fennec/4.0. */
					|| ua.match( /Fennec\/([0-9]+)/ ) && RegExp.$1 >= 4
					/* WebOS 3 and up (TouchPad too)
					   ~: Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.48 Safari/534.6 TouchPad/1.0 */
					|| ua.match( /wOSBrowser\/([0-9]+)/ ) && RegExp.$1 >= 233 && wkLte534
					/* Nokia Browser N8
					   ~: Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/012.002; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.0 Mobile Safari/533.4 3gpp-gba
					   ~: Note: the N9 doesn't have native overflow with one-finger touch. wtf */
					|| ua.match( /NokiaBrowser\/([0-9\.]+)/ ) && parseFloat(RegExp.$1) === 7.3 && webkit && wkversion >= 533
				);
			})(),

		// Easing uses any of Robert Penner's equations. By default, overthrow uses ease-out-cubic
		// arguments: t = current iteration, b = initial value, c = end value, d = total iterations
		// use w.overthrow.easing to provide a custom function externally, or pass an easing function as a callback to the toss method
		defaultEasing = function (t, b, c, d) {
			return c*((t=t/d-1)*t*t + 1) + b;
		},

		enabled = false,

		// Keeper of intervals
		timeKeeper,

		/* toss scrolls and element with easing

		// elem is the element to scroll
		// options hash:
			* left is the desired horizontal scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
			* top is the desired vertical scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
			* duration is the number of milliseconds the throw will take. Default is 100.
			* easing is an optional custom easing function. Default is w.overthrow.easing. Must follow the easing function signature
		*/
		toss = function( elem, options ){
			var i = 0,
				o = options || {},
				top = o.top || "+0",
				left = o.left || "+0",
				duration = o.duration || 100,
				easing = o.easing || w.overthrow.easing,
				sLeft = elem.scrollLeft,
				sTop = elem.scrollTop;

			// top and left relative or positive distance
			left = typeof left === "string" ? parseFloat( left ) : left - sLeft,
			top = typeof top === "string" ? parseFloat( top ) : top - sTop;

			timeKeeper = setInterval(function(){
				if( i++ <= duration ){
					elem.scrollLeft = easing( i, sLeft, left, duration );
					elem.scrollTop = easing( i, sTop, top, duration );
				}
				else{
					intercept();
				}
			}, 1 );
		},

		// find closest overthrow (elem or a parent)
		closest = function( target, ascend ){
			return !ascend && target.className && target.className.indexOf( "overthrow" ) > -1 && target || closest( target.parentNode );
		},

		// Intercept any throw in progress
		intercept = function(){
			clearInterval( timeKeeper );
		},

		// Enable and potentially polyfill overflow
		enable = function(){

			// If it's on,
			if( enabled ){
				return;
			}
			// It's on.
			enabled = true;

			// If overflowProbablyAlreadyWorks or at least the element canBeFilledWithPoly, add a class to cue CSS that assumes overflow scrolling will work (setting height on elements and such)
			if( overflowProbablyAlreadyWorks || canBeFilledWithPoly ){
				docElem.className += " " + classtext;
			}

			// Destroy everything later. If you want to.
			w.overthrow.forget = function(){
				// Strip the class name from docElem
				docElem.className = docElem.className.replace( classtext, "" );
				// Remove touch binding (check for method support since this part isn't qualified by touch support like the rest)
				if( doc.removeEventListener ){
					doc.removeEventListener( "touchstart", start, false );
				}
				// reset easing to default
				w.overthrow.easing = defaultEasing;

				// Let 'em know
				enabled = false;
			};

			// If overflowProbablyAlreadyWorks or it doesn't look like the browser canBeFilledWithPoly, our job is done here. Exit viewport left.
			if( overflowProbablyAlreadyWorks || !canBeFilledWithPoly ){
				return;
			}

			// Fill 'er up!
			// From here down, all logic is associated with touch scroll handling
				// elem references the overthrow element in use
			var elem,

				// The last several Y values are kept here
				lastTops = [],

				// The last several X values are kept here
				lastLefts = [],

				// lastDown will be true if the last scroll direction was down, false if it was up
				lastDown,

				// lastRight will be true if the last scroll direction was right, false if it was left
				lastRight,

				// For a new gesture, or change in direction, reset the values from last scroll
				resetVertTracking = function(){
					lastTops = [];
					lastDown = null;
				},

				resetHorTracking = function(){
					lastLefts = [];
					lastRight = null;
				},

				// After releasing touchend, throw the overthrow element, depending on momentum
				finishScroll = function(){
					// Come up with a distance and duration based on how
					// Multipliers are tweaked to a comfortable balance across platforms
					var top = ( lastTops[ 0 ] - lastTops[ lastTops.length -1 ] ) * 8,
						left = ( lastLefts[ 0 ] - lastLefts[ lastLefts.length -1 ] ) * 8,
						duration = Math.max( Math.abs( left ), Math.abs( top ) ) / 8;

					// Make top and left relative-style strings (positive vals need "+" prefix)
					top = ( top > 0 ? "+" : "" ) + top;
					left = ( left > 0 ? "+" : "" ) + left;

					// Make sure there's a significant amount of throw involved, otherwise, just stay still
					if( !isNaN( duration ) && duration > 0 && ( Math.abs( left ) > 80 || Math.abs( top ) > 80 ) ){
						toss( elem, { left: left, top: top, duration: duration } );
					}
				},

				// On webkit, touch events hardly trickle through textareas and inputs
				// Disabling CSS pointer events makes sure they do, but it also makes the controls innaccessible
				// Toggling pointer events at the right moments seems to do the trick
				// Thanks Thomas Bachem http://stackoverflow.com/a/5798681 for the following
				inputs,
				setPointers = function( val ){
					inputs = elem.querySelectorAll( "textarea, input" );
				    for( var i = 0, il = inputs.length; i < il; i++) {
				        inputs[ i ].style.pointerEvents = val;
				    }
				},

				// For nested overthrows, changeScrollTarget restarts a touch event cycle on a parent or child overthrow
				changeScrollTarget = function( startEvent, ascend ){
					if( doc.createEvent ){
						var newTarget = ( !ascend || ascend === undefined ) && elem.parentNode || elem.touchchild || elem,
							tEnd;

						if( newTarget !== elem ){
							tEnd = doc.createEvent( "HTMLEvents" );
							tEnd.initEvent( "touchend", true, true );
							elem.dispatchEvent( tEnd );
							newTarget.touchchild = elem;
							elem = newTarget;
							newTarget.dispatchEvent( startEvent );
						}
					}
				},

				// Touchstart handler
				// On touchstart, touchmove and touchend are freshly bound, and all three share a bunch of vars set by touchstart
				// Touchend unbinds them again, until next time
				start = function( e ){

					// Stop any throw in progress
					intercept();

					// Reset the distance and direction tracking
					resetVertTracking();
					resetHorTracking();

					elem = closest( e.target );

					if( !elem || elem === docElem || e.touches.length > 1 ){
						return;
					}

					setPointers( "none" );
					var touchStartE = e,
						scrollT = elem.scrollTop,
						scrollL = elem.scrollLeft,
						height = elem.offsetHeight,
						width = elem.offsetWidth,
						startY = e.touches[ 0 ].pageY,
						startX = e.touches[ 0 ].pageX,
						scrollHeight = elem.scrollHeight,
						scrollWidth = elem.scrollWidth,

						// Touchmove handler
						move = function( e ){

							var ty = scrollT + startY - e.touches[ 0 ].pageY,
								tx = scrollL + startX - e.touches[ 0 ].pageX,
								down = ty >= ( lastTops.length ? lastTops[ 0 ] : 0 ),
								right = tx >= ( lastLefts.length ? lastLefts[ 0 ] : 0 );

							// If there's room to scroll the current container, prevent the default window scroll
							if( ( ty > 0 && ty < scrollHeight - height ) || ( roomToScrollX = tx > 0 && tx < scrollWidth - width ) ){
								e.preventDefault();
							}
							// This bubbling is dumb. Needs a rethink.
							else {
								changeScrollTarget( touchStartE );
							}

							// If down and lastDown are inequal, the y scroll has changed direction. Reset tracking.
							if( lastDown && down !== lastDown ){
								resetVertTracking();
							}

							// If right and lastRight are inequal, the x scroll has changed direction. Reset tracking.
							if( lastRight && right !== lastRight ){
								resetHorTracking();
							}

							// remember the last direction in which we were headed
							lastDown = down;
							lastRight = right;

							// set the container's scroll
							elem.scrollTop = ty;
							elem.scrollLeft = tx;

							lastTops.unshift( ty );
							lastLefts.unshift( tx );

							if( lastTops.length > 3 ){
								lastTops.pop();
							}
							if( lastLefts.length > 3 ){
								lastLefts.pop();
							}
						},

						// Touchend handler
						end = function( e ){
							// Apply momentum based easing for a graceful finish
							finishScroll();
							// Bring the pointers back
							setPointers( "auto" );
							setTimeout( function(){
								setPointers( "none" );
							}, 450 );
							elem.removeEventListener( "touchmove", move, false );
							elem.removeEventListener( "touchend", end, false );
						};

					elem.addEventListener( "touchmove", move, false );
					elem.addEventListener( "touchend", end, false );
				};

			// Bind to touch, handle move and end within
			doc.addEventListener( "touchstart", start, false );
		};

	// Expose overthrow API
	w.overthrow = {
		set: enable,
		forget: function(){},
		easing: defaultEasing,
		toss: toss,
		intercept: intercept,
		closest: closest,
		support: overflowProbablyAlreadyWorks ? "native" : canBeFilledWithPoly && "polyfilled" || "none"
	};

	// Auto-init
	enable();

})( this );<commit_msg>added version #<commit_after>/*! Overthrow v.0.1.0. An overflow:auto polyfill for responsive design. (c) 2012: Scott Jehl, Filament Group, Inc. Dual MIT/GPLv2 license */
(function( w, undefined ){

	var doc = w.document,
		docElem = doc.documentElement,
		classtext = "overthrow",

		// Touch events are used in the polyfill, and thus are a prerequisite
		canBeFilledWithPoly = "ontouchmove" in doc,

		// The following attempts to determine whether the browser has native overflow support
		// so we can enable it but not polyfill
		overflowProbablyAlreadyWorks =
			// Features-first. iOS5 overflow scrolling property check - no UA needed here. thanks Apple :)
			"WebkitOverflowScrolling" in docElem.style
			// Touch events aren't supported and screen width is greater than X
			// ...basically, this is a loose "desktop browser" check.
			// It may wrongly opt-in very large tablets with no touch support.
			|| ( !canBeFilledWithPoly && w.screen.width > 1200 )
			// Hang on to your hats.
			// Whitelist some popular, overflow-supporting mobile browsers for now and the future
			// These browsers are known to get overlow support right, but give us no way of detecting it.
			|| (function(){
				var ua = w.navigator.userAgent,
					// Webkit crosses platforms, and the browsers on our list run at least version 534
					webkit = ua.match( /AppleWebKit\/([0-9]+)/ ),
					wkversion = webkit && webkit[1],
					wkLte534 = webkit && wkversion >= 534;

				return (
					/* Android 3+ with webkit gte 534
					   Sample: Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 */
					ua.match( /Android ([0-9]+)/ ) && RegExp.$1 >= 3 && wkLte534
					/* Blackberry 7+ with webkit gte 534
					   ~: Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0 Mobile Safari/534.11+ */
					|| ua.match( / Version\/([0-9]+)/ ) && RegExp.$1 >= 0 && w.blackberry && wkLte534
					/* Blackberry Playbook with webkit gte 534
					   ~: Mozilla/5.0 (PlayBook; U; RIM Tablet OS 1.0.0; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/0.0.1 Safari/534.8+ */
					|| ua.indexOf( /PlayBook/ ) > -1 && RegExp.$1 >= 0 && wkLte534
					/* Firefox Mobile (Fennec) 4 and up
					   ~: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:2.1.1) Gecko/ Firefox/4.0.2pre Fennec/4.0. */
					|| ua.match( /Fennec\/([0-9]+)/ ) && RegExp.$1 >= 4
					/* WebOS 3 and up (TouchPad too)
					   ~: Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.48 Safari/534.6 TouchPad/1.0 */
					|| ua.match( /wOSBrowser\/([0-9]+)/ ) && RegExp.$1 >= 233 && wkLte534
					/* Nokia Browser N8
					   ~: Mozilla/5.0 (Symbian/3; Series60/5.2 NokiaN8-00/012.002; Profile/MIDP-2.1 Configuration/CLDC-1.1 ) AppleWebKit/533.4 (KHTML, like Gecko) NokiaBrowser/7.3.0 Mobile Safari/533.4 3gpp-gba
					   ~: Note: the N9 doesn't have native overflow with one-finger touch. wtf */
					|| ua.match( /NokiaBrowser\/([0-9\.]+)/ ) && parseFloat(RegExp.$1) === 7.3 && webkit && wkversion >= 533
				);
			})(),

		// Easing uses any of Robert Penner's equations. By default, overthrow uses ease-out-cubic
		// arguments: t = current iteration, b = initial value, c = end value, d = total iterations
		// use w.overthrow.easing to provide a custom function externally, or pass an easing function as a callback to the toss method
		defaultEasing = function (t, b, c, d) {
			return c*((t=t/d-1)*t*t + 1) + b;
		},

		enabled = false,

		// Keeper of intervals
		timeKeeper,

		/* toss scrolls and element with easing

		// elem is the element to scroll
		// options hash:
			* left is the desired horizontal scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
			* top is the desired vertical scroll. Default is "+0". For relative distances, pass a string with "+" or "-" in front.
			* duration is the number of milliseconds the throw will take. Default is 100.
			* easing is an optional custom easing function. Default is w.overthrow.easing. Must follow the easing function signature
		*/
		toss = function( elem, options ){
			var i = 0,
				o = options || {},
				top = o.top || "+0",
				left = o.left || "+0",
				duration = o.duration || 100,
				easing = o.easing || w.overthrow.easing,
				sLeft = elem.scrollLeft,
				sTop = elem.scrollTop;

			// top and left relative or positive distance
			left = typeof left === "string" ? parseFloat( left ) : left - sLeft,
			top = typeof top === "string" ? parseFloat( top ) : top - sTop;

			timeKeeper = setInterval(function(){
				if( i++ <= duration ){
					elem.scrollLeft = easing( i, sLeft, left, duration );
					elem.scrollTop = easing( i, sTop, top, duration );
				}
				else{
					intercept();
				}
			}, 1 );
		},

		// find closest overthrow (elem or a parent)
		closest = function( target, ascend ){
			return !ascend && target.className && target.className.indexOf( "overthrow" ) > -1 && target || closest( target.parentNode );
		},

		// Intercept any throw in progress
		intercept = function(){
			clearInterval( timeKeeper );
		},

		// Enable and potentially polyfill overflow
		enable = function(){

			// If it's on,
			if( enabled ){
				return;
			}
			// It's on.
			enabled = true;

			// If overflowProbablyAlreadyWorks or at least the element canBeFilledWithPoly, add a class to cue CSS that assumes overflow scrolling will work (setting height on elements and such)
			if( overflowProbablyAlreadyWorks || canBeFilledWithPoly ){
				docElem.className += " " + classtext;
			}

			// Destroy everything later. If you want to.
			w.overthrow.forget = function(){
				// Strip the class name from docElem
				docElem.className = docElem.className.replace( classtext, "" );
				// Remove touch binding (check for method support since this part isn't qualified by touch support like the rest)
				if( doc.removeEventListener ){
					doc.removeEventListener( "touchstart", start, false );
				}
				// reset easing to default
				w.overthrow.easing = defaultEasing;

				// Let 'em know
				enabled = false;
			};

			// If overflowProbablyAlreadyWorks or it doesn't look like the browser canBeFilledWithPoly, our job is done here. Exit viewport left.
			if( overflowProbablyAlreadyWorks || !canBeFilledWithPoly ){
				return;
			}

			// Fill 'er up!
			// From here down, all logic is associated with touch scroll handling
				// elem references the overthrow element in use
			var elem,

				// The last several Y values are kept here
				lastTops = [],

				// The last several X values are kept here
				lastLefts = [],

				// lastDown will be true if the last scroll direction was down, false if it was up
				lastDown,

				// lastRight will be true if the last scroll direction was right, false if it was left
				lastRight,

				// For a new gesture, or change in direction, reset the values from last scroll
				resetVertTracking = function(){
					lastTops = [];
					lastDown = null;
				},

				resetHorTracking = function(){
					lastLefts = [];
					lastRight = null;
				},

				// After releasing touchend, throw the overthrow element, depending on momentum
				finishScroll = function(){
					// Come up with a distance and duration based on how
					// Multipliers are tweaked to a comfortable balance across platforms
					var top = ( lastTops[ 0 ] - lastTops[ lastTops.length -1 ] ) * 8,
						left = ( lastLefts[ 0 ] - lastLefts[ lastLefts.length -1 ] ) * 8,
						duration = Math.max( Math.abs( left ), Math.abs( top ) ) / 8;

					// Make top and left relative-style strings (positive vals need "+" prefix)
					top = ( top > 0 ? "+" : "" ) + top;
					left = ( left > 0 ? "+" : "" ) + left;

					// Make sure there's a significant amount of throw involved, otherwise, just stay still
					if( !isNaN( duration ) && duration > 0 && ( Math.abs( left ) > 80 || Math.abs( top ) > 80 ) ){
						toss( elem, { left: left, top: top, duration: duration } );
					}
				},

				// On webkit, touch events hardly trickle through textareas and inputs
				// Disabling CSS pointer events makes sure they do, but it also makes the controls innaccessible
				// Toggling pointer events at the right moments seems to do the trick
				// Thanks Thomas Bachem http://stackoverflow.com/a/5798681 for the following
				inputs,
				setPointers = function( val ){
					inputs = elem.querySelectorAll( "textarea, input" );
				    for( var i = 0, il = inputs.length; i < il; i++) {
				        inputs[ i ].style.pointerEvents = val;
				    }
				},

				// For nested overthrows, changeScrollTarget restarts a touch event cycle on a parent or child overthrow
				changeScrollTarget = function( startEvent, ascend ){
					if( doc.createEvent ){
						var newTarget = ( !ascend || ascend === undefined ) && elem.parentNode || elem.touchchild || elem,
							tEnd;

						if( newTarget !== elem ){
							tEnd = doc.createEvent( "HTMLEvents" );
							tEnd.initEvent( "touchend", true, true );
							elem.dispatchEvent( tEnd );
							newTarget.touchchild = elem;
							elem = newTarget;
							newTarget.dispatchEvent( startEvent );
						}
					}
				},

				// Touchstart handler
				// On touchstart, touchmove and touchend are freshly bound, and all three share a bunch of vars set by touchstart
				// Touchend unbinds them again, until next time
				start = function( e ){

					// Stop any throw in progress
					intercept();

					// Reset the distance and direction tracking
					resetVertTracking();
					resetHorTracking();

					elem = closest( e.target );

					if( !elem || elem === docElem || e.touches.length > 1 ){
						return;
					}

					setPointers( "none" );
					var touchStartE = e,
						scrollT = elem.scrollTop,
						scrollL = elem.scrollLeft,
						height = elem.offsetHeight,
						width = elem.offsetWidth,
						startY = e.touches[ 0 ].pageY,
						startX = e.touches[ 0 ].pageX,
						scrollHeight = elem.scrollHeight,
						scrollWidth = elem.scrollWidth,

						// Touchmove handler
						move = function( e ){

							var ty = scrollT + startY - e.touches[ 0 ].pageY,
								tx = scrollL + startX - e.touches[ 0 ].pageX,
								down = ty >= ( lastTops.length ? lastTops[ 0 ] : 0 ),
								right = tx >= ( lastLefts.length ? lastLefts[ 0 ] : 0 );

							// If there's room to scroll the current container, prevent the default window scroll
							if( ( ty > 0 && ty < scrollHeight - height ) || ( roomToScrollX = tx > 0 && tx < scrollWidth - width ) ){
								e.preventDefault();
							}
							// This bubbling is dumb. Needs a rethink.
							else {
								changeScrollTarget( touchStartE );
							}

							// If down and lastDown are inequal, the y scroll has changed direction. Reset tracking.
							if( lastDown && down !== lastDown ){
								resetVertTracking();
							}

							// If right and lastRight are inequal, the x scroll has changed direction. Reset tracking.
							if( lastRight && right !== lastRight ){
								resetHorTracking();
							}

							// remember the last direction in which we were headed
							lastDown = down;
							lastRight = right;

							// set the container's scroll
							elem.scrollTop = ty;
							elem.scrollLeft = tx;

							lastTops.unshift( ty );
							lastLefts.unshift( tx );

							if( lastTops.length > 3 ){
								lastTops.pop();
							}
							if( lastLefts.length > 3 ){
								lastLefts.pop();
							}
						},

						// Touchend handler
						end = function( e ){
							// Apply momentum based easing for a graceful finish
							finishScroll();
							// Bring the pointers back
							setPointers( "auto" );
							setTimeout( function(){
								setPointers( "none" );
							}, 450 );
							elem.removeEventListener( "touchmove", move, false );
							elem.removeEventListener( "touchend", end, false );
						};

					elem.addEventListener( "touchmove", move, false );
					elem.addEventListener( "touchend", end, false );
				};

			// Bind to touch, handle move and end within
			doc.addEventListener( "touchstart", start, false );
		};

	// Expose overthrow API
	w.overthrow = {
		set: enable,
		forget: function(){},
		easing: defaultEasing,
		toss: toss,
		intercept: intercept,
		closest: closest,
		support: overflowProbablyAlreadyWorks ? "native" : canBeFilledWithPoly && "polyfilled" || "none"
	};

	// Auto-init
	enable();

})( this );<|endoftext|>


<commit_before>import path from 'path';
import fse from 'fs-extra';
import { DefinePlugin, HotModuleReplacementPlugin, ProgressPlugin } from 'webpack';
import Dotenv from 'dotenv-webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin';
import TerserWebpackPlugin from 'terser-webpack-plugin';
import VirtualModulePlugin from 'webpack-virtual-modules';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';

import resolveFrom from 'resolve-from';

import themingPaths from '@storybook/theming/paths';

import { createBabelLoader } from './babel-loader-preview';
import es6Transpiler from '../common/es6Transpiler';

import { nodeModulesPaths, loadEnv } from '../config/utils';
import { getPreviewHeadHtml, getPreviewBodyHtml } from '../utils/template';
import { toRequireContextString } from './to-require-context';
import { useBaseTsSupport } from '../config/useBaseTsSupport';

const reactPaths = {};

const storybookPaths = [
  'addons',
  'addons',
  'api',
  'channels',
  'channel-postmessage',
  'components',
  'core-events',
  'router',
  'theming',
  'semver',
  'client-api',
  'client-logger',
].reduce(
  (acc, sbPackage) => ({
    ...acc,
    [`@storybook/${sbPackage}`]: path.dirname(
      resolveFrom(__dirname, `@storybook/${sbPackage}/package.json`)
    ),
  }),
  {}
);

try {
  reactPaths.react = path.dirname(resolveFrom(process.cwd(), 'react/package.json'));
  reactPaths['react-dom'] = path.dirname(resolveFrom(process.cwd(), 'react-dom/package.json'));
} catch (e) {
  //
}

export default async ({
  configDir,
  babelOptions,
  entries,
  stories,
  outputDir = path.join('.', 'public'),
  quiet,
  packageJson,
  configType,
  framework,
  frameworkPath,
  presets,
  typescriptOptions,
}) => {
  const dlls = await presets.apply('webpackDlls', []);
  const logLevel = await presets.apply('logLevel', undefined);
  const frameworkOptions = await presets.apply(`${framework}Options`, {}, {});
  const { raw, stringified } = loadEnv({ production: true });
  const babelLoader = createBabelLoader(babelOptions, framework);
  const isProd = configType === 'PRODUCTION';
  const entryTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleEntry.template.js'), {
    encoding: 'utf8',
  });
  const storyTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleStory.template.js'), {
    encoding: 'utf8',
  });
  const frameworkInitEntry = path.resolve(
    path.join(configDir, 'storybook-init-framework-entry.js')
  );
  // Allows for custom frameworks that are not published under the @storybook namespace
  const frameworkImportPath = frameworkPath || `@storybook/${framework}`;
  const virtualModuleMapping = {
    // Ensure that the client API is initialized by the framework before any other iframe code
    // is loaded. That way our client-apis can assume the existence of the API+store
    [frameworkInitEntry]: `import '${frameworkImportPath}';`,
  };
  entries.forEach((entryFilename) => {
    const match = entryFilename.match(/(.*)-generated-(config|other)-entry.js$/);
    if (match) {
      const configFilename = match[1];
      const clientApi = storybookPaths['@storybook/client-api'];
      const clientLogger = storybookPaths['@storybook/client-logger'];

      virtualModuleMapping[entryFilename] = interpolate(entryTemplate, {
        configFilename,
        clientApi,
        clientLogger,
      });
    }
  });
  if (stories) {
    const storiesFilename = path.resolve(path.join(configDir, `generated-stories-entry.js`));
    virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, { frameworkImportPath })
      // Make sure we also replace quotes for this one
      .replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
  }

  const shouldCheckTs = useBaseTsSupport(framework) && typescriptOptions.check;
  const tsCheckOptions = typescriptOptions.checkOptions || {};

  return {
    mode: isProd ? 'production' : 'development',
    bail: isProd,
    devtool: '#cheap-module-source-map',
    entry: entries,
    output: {
      path: path.resolve(process.cwd(), outputDir),
      filename: '[name].[hash].bundle.js',
      publicPath: '',
    },
    plugins: [
      Object.keys(virtualModuleMapping).length > 0
        ? new VirtualModulePlugin(virtualModuleMapping)
        : null,
      new HtmlWebpackPlugin({
        filename: `iframe.html`,
        chunksSortMode: 'none',
        alwaysWriteToDisk: true,
        inject: false,
        templateParameters: (compilation, files, options) => ({
          compilation,
          files,
          options,
          version: packageJson.version,
          globals: {
            LOGLEVEL: logLevel,
          },
          headHtmlSnippet: getPreviewHeadHtml(configDir, process.env),
          dlls,
          bodyHtmlSnippet: getPreviewBodyHtml(configDir, process.env),
        }),
        minify: {
          collapseWhitespace: true,
          removeComments: true,
          removeRedundantAttributes: true,
          removeScriptTypeAttributes: false,
          removeStyleLinkTypeAttributes: true,
          useShortDoctype: true,
        },
        template: require.resolve(`../templates/index.ejs`),
      }),
      new DefinePlugin({
        'process.env': stringified,
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
        FRAMEWORK_OPTIONS: frameworkOptions,
      }),
      isProd ? null : new WatchMissingNodeModulesPlugin(nodeModulesPaths),
      isProd ? null : new HotModuleReplacementPlugin(),
      new CaseSensitivePathsPlugin(),
      quiet ? null : new ProgressPlugin(),
      new Dotenv({ silent: true }),
      shouldCheckTs ? new ForkTsCheckerWebpackPlugin(tsCheckOptions) : null,
    ].filter(Boolean),
    module: {
      rules: [
        babelLoader,
        es6Transpiler(),
        {
          test: /\.md$/,
          use: [
            {
              loader: require.resolve('raw-loader'),
            },
          ],
        },
      ],
    },
    resolve: {
      extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json', '.cjs'],
      modules: ['node_modules'].concat(raw.NODE_PATH || []),
      alias: {
        ...themingPaths,
        ...storybookPaths,
        ...reactPaths,
      },

      plugins: [
        // Transparently resolve packages via PnP when needed; noop otherwise
        PnpWebpackPlugin,
      ],
    },
    resolveLoader: {
      plugins: [PnpWebpackPlugin.moduleLoader(module)],
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
      },
      runtimeChunk: true,
      minimizer: isProd
        ? [
            new TerserWebpackPlugin({
              cache: true,
              parallel: true,
              sourceMap: true,
              terserOptions: {
                mangle: false,
                keep_fnames: true,
              },
            }),
          ]
        : [],
    },
    performance: {
      hints: isProd ? 'warning' : false,
    },
  };
};

/**
 * Return a string corresponding to template filled with bindings using following pattern:
 * For each (key, value) of `bindings` replace, in template, `{{key}}` by escaped version of `value`
 *
 * @param template {String} Template with `{{binding}}`
 * @param bindings {Object} key-value object use to fill the template, `{{key}}` will be replaced by `escaped(value)`
 * @returns {String} Filled template
 */
const interpolate = (template, bindings) => {
  return Object.entries(bindings).reduce((acc, [k, v]) => {
    const escapedString = v.replace(/\\/g, '/').replace(/\$/g, '$$$');
    return acc.replace(new RegExp(`{{${k}}}`, 'g'), escapedString);
  }, template);
};
<commit_msg>Core: pass framework options as global<commit_after>import path from 'path';
import fse from 'fs-extra';
import { DefinePlugin, HotModuleReplacementPlugin, ProgressPlugin } from 'webpack';
import Dotenv from 'dotenv-webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin';
import TerserWebpackPlugin from 'terser-webpack-plugin';
import VirtualModulePlugin from 'webpack-virtual-modules';
import PnpWebpackPlugin from 'pnp-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';

import resolveFrom from 'resolve-from';

import themingPaths from '@storybook/theming/paths';

import { createBabelLoader } from './babel-loader-preview';
import es6Transpiler from '../common/es6Transpiler';

import { nodeModulesPaths, loadEnv } from '../config/utils';
import { getPreviewHeadHtml, getPreviewBodyHtml } from '../utils/template';
import { toRequireContextString } from './to-require-context';
import { useBaseTsSupport } from '../config/useBaseTsSupport';

const reactPaths = {};

const storybookPaths = [
  'addons',
  'addons',
  'api',
  'channels',
  'channel-postmessage',
  'components',
  'core-events',
  'router',
  'theming',
  'semver',
  'client-api',
  'client-logger',
].reduce(
  (acc, sbPackage) => ({
    ...acc,
    [`@storybook/${sbPackage}`]: path.dirname(
      resolveFrom(__dirname, `@storybook/${sbPackage}/package.json`)
    ),
  }),
  {}
);

try {
  reactPaths.react = path.dirname(resolveFrom(process.cwd(), 'react/package.json'));
  reactPaths['react-dom'] = path.dirname(resolveFrom(process.cwd(), 'react-dom/package.json'));
} catch (e) {
  //
}

export default async ({
  configDir,
  babelOptions,
  entries,
  stories,
  outputDir = path.join('.', 'public'),
  quiet,
  packageJson,
  configType,
  framework,
  frameworkPath,
  presets,
  typescriptOptions,
}) => {
  const dlls = await presets.apply('webpackDlls', []);
  const logLevel = await presets.apply('logLevel', undefined);
  const frameworkOptions = await presets.apply(`${framework}Options`, {}, {});
  const { raw, stringified } = loadEnv({ production: true });
  const babelLoader = createBabelLoader(babelOptions, framework);
  const isProd = configType === 'PRODUCTION';
  const entryTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleEntry.template.js'), {
    encoding: 'utf8',
  });
  const storyTemplate = await fse.readFile(path.join(__dirname, 'virtualModuleStory.template.js'), {
    encoding: 'utf8',
  });
  const frameworkInitEntry = path.resolve(
    path.join(configDir, 'storybook-init-framework-entry.js')
  );
  // Allows for custom frameworks that are not published under the @storybook namespace
  const frameworkImportPath = frameworkPath || `@storybook/${framework}`;
  const virtualModuleMapping = {
    // Ensure that the client API is initialized by the framework before any other iframe code
    // is loaded. That way our client-apis can assume the existence of the API+store
    [frameworkInitEntry]: `import '${frameworkImportPath}';`,
  };
  entries.forEach((entryFilename) => {
    const match = entryFilename.match(/(.*)-generated-(config|other)-entry.js$/);
    if (match) {
      const configFilename = match[1];
      const clientApi = storybookPaths['@storybook/client-api'];
      const clientLogger = storybookPaths['@storybook/client-logger'];

      virtualModuleMapping[entryFilename] = interpolate(entryTemplate, {
        configFilename,
        clientApi,
        clientLogger,
      });
    }
  });
  if (stories) {
    const storiesFilename = path.resolve(path.join(configDir, `generated-stories-entry.js`));
    virtualModuleMapping[storiesFilename] = interpolate(storyTemplate, { frameworkImportPath })
      // Make sure we also replace quotes for this one
      .replace("'{{stories}}'", stories.map(toRequireContextString).join(','));
  }

  const shouldCheckTs = useBaseTsSupport(framework) && typescriptOptions.check;
  const tsCheckOptions = typescriptOptions.checkOptions || {};

  return {
    mode: isProd ? 'production' : 'development',
    bail: isProd,
    devtool: '#cheap-module-source-map',
    entry: entries,
    output: {
      path: path.resolve(process.cwd(), outputDir),
      filename: '[name].[hash].bundle.js',
      publicPath: '',
    },
    plugins: [
      Object.keys(virtualModuleMapping).length > 0
        ? new VirtualModulePlugin(virtualModuleMapping)
        : null,
      new HtmlWebpackPlugin({
        filename: `iframe.html`,
        chunksSortMode: 'none',
        alwaysWriteToDisk: true,
        inject: false,
        templateParameters: (compilation, files, options) => ({
          compilation,
          files,
          options,
          version: packageJson.version,
          globals: {
            LOGLEVEL: logLevel,
            FRAMEWORK_OPTIONS: frameworkOptions,
          },
          headHtmlSnippet: getPreviewHeadHtml(configDir, process.env),
          dlls,
          bodyHtmlSnippet: getPreviewBodyHtml(configDir, process.env),
        }),
        minify: {
          collapseWhitespace: true,
          removeComments: true,
          removeRedundantAttributes: true,
          removeScriptTypeAttributes: false,
          removeStyleLinkTypeAttributes: true,
          useShortDoctype: true,
        },
        template: require.resolve(`../templates/index.ejs`),
      }),
      new DefinePlugin({
        'process.env': stringified,
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      }),
      isProd ? null : new WatchMissingNodeModulesPlugin(nodeModulesPaths),
      isProd ? null : new HotModuleReplacementPlugin(),
      new CaseSensitivePathsPlugin(),
      quiet ? null : new ProgressPlugin(),
      new Dotenv({ silent: true }),
      shouldCheckTs ? new ForkTsCheckerWebpackPlugin(tsCheckOptions) : null,
    ].filter(Boolean),
    module: {
      rules: [
        babelLoader,
        es6Transpiler(),
        {
          test: /\.md$/,
          use: [
            {
              loader: require.resolve('raw-loader'),
            },
          ],
        },
      ],
    },
    resolve: {
      extensions: ['.mjs', '.js', '.jsx', '.ts', '.tsx', '.json', '.cjs'],
      modules: ['node_modules'].concat(raw.NODE_PATH || []),
      alias: {
        ...themingPaths,
        ...storybookPaths,
        ...reactPaths,
      },

      plugins: [
        // Transparently resolve packages via PnP when needed; noop otherwise
        PnpWebpackPlugin,
      ],
    },
    resolveLoader: {
      plugins: [PnpWebpackPlugin.moduleLoader(module)],
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
      },
      runtimeChunk: true,
      minimizer: isProd
        ? [
            new TerserWebpackPlugin({
              cache: true,
              parallel: true,
              sourceMap: true,
              terserOptions: {
                mangle: false,
                keep_fnames: true,
              },
            }),
          ]
        : [],
    },
    performance: {
      hints: isProd ? 'warning' : false,
    },
  };
};

/**
 * Return a string corresponding to template filled with bindings using following pattern:
 * For each (key, value) of `bindings` replace, in template, `{{key}}` by escaped version of `value`
 *
 * @param template {String} Template with `{{binding}}`
 * @param bindings {Object} key-value object use to fill the template, `{{key}}` will be replaced by `escaped(value)`
 * @returns {String} Filled template
 */
const interpolate = (template, bindings) => {
  return Object.entries(bindings).reduce((acc, [k, v]) => {
    const escapedString = v.replace(/\\/g, '/').replace(/\$/g, '$$$');
    return acc.replace(new RegExp(`{{${k}}}`, 'g'), escapedString);
  }, template);
};
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

'use strict';

/* global ga */

const FileUploader = require('./fileuploader');
const GithubAPI = require('./github');
const idb = require('idb-keyval');
const logger = require('./logger');
const ReportGenerator = require('../../../lighthouse-core/report/report-generator');

// TODO: We only need getFilenamePrefix from asset-saver. Tree shake!
const getFilenamePrefix = require('../../../lighthouse-core/lib/asset-saver').getFilenamePrefix;

const LH_CURRENT_VERSION = require('../../../package.json').version;
const APP_URL = `${location.origin}${location.pathname}`;

/**
 * Class to handle dynamic changes to the page when users view new reports.
 * @class
 */
class LighthouseViewerReport {

  constructor() {
    this.onShare = this.onShare.bind(this);
    this.onCopy = this.onCopy.bind(this);
    this.onCopyButtonClick = this.onCopyButtonClick.bind(this);
    this.onFileUpload = this.onFileUpload.bind(this);
    this.onPaste = this.onPaste.bind(this);
    this.onExportButtonClick = this.onExportButtonClick.bind(this);
    this.onExport = this.onExport.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);

    this._copyAttempt = false;

    this.json = null;
    this.fileUploader = new FileUploader(this.onFileUpload);
    this.github = new GithubAPI();

    this._isNewReport = true;

    this.initUI();
    this.loadFromURL();
  }

  initUI() {
    this.shareButton = document.querySelector('.js-share');
    if (this.shareButton) {
      this.shareButton.addEventListener('click', this.onShare);

      // Disable the share button after the user shares the gist or if we're loading
      // a gist from Github. In both cases, the gist is already shared :)
      if (this._isNewReport) {
        this.enableShareButton();
      } else {
        this.disableShareButton();
      }
    }

    const copyButton = document.querySelector('.js-copy');
    if (copyButton) {
      copyButton.addEventListener('click', this.onCopyButtonClick);
      document.addEventListener('copy', this.onCopy);
    }

    document.addEventListener('paste', this.onPaste);

    this.exportButton = document.querySelector('.js-export');
    if (this.exportButton) {
      this.exportButton.addEventListener('click', this.onExportButtonClick);
      const dropdown = document.querySelector('.export-dropdown');
      dropdown.addEventListener('click', this.onExport);
    }
  }

  enableShareButton() {
    this.shareButton.classList.remove('disable');
    this.shareButton.disabled = false;
  }

  disableShareButton() {
    this.shareButton.classList.add('disable');
    this.shareButton.disabled = true;
  }

  closeExportDropdown() {
    this.exportButton.classList.remove('active');
  }

  loadFromURL() {
    // Pull gist id from URL and render it.
    const params = new URLSearchParams(location.search);
    const gistId = params.get('gist');
    if (gistId) {
      logger.log('Fetching report from Github...', false);

      this.github.auth.ready.then(_ => {
        this.github.getGistFileContentAsJson(gistId).then(json => {
          logger.hide();

          this._isNewReport = false;
          this.replaceReportHTML(json.content);

          // Save fetched json and etag to IDB so we can use it later for 304
          // requests. This is done after replaceReportHTML, so we don't save
          // unrecognized JSON to IDB. replaceReportHTML will throw in that case.
          return idb.set(gistId, {etag: json.etag, content: json.content});
        }).catch(err => logger.error(err.message));
      });
    }
  }

  _validateReportJson(reportJson) {
    if (!reportJson.lighthouseVersion) {
      throw new Error('JSON file was not generated by Lighthouse');
    }

    // Leave off patch version in the comparison.
    const semverRe = new RegExp(/^(\d+)?\.(\d+)?\.(\d+)$/);
    const reportVersion = reportJson.lighthouseVersion.replace(semverRe, '$1.$2');
    const lhVersion = LH_CURRENT_VERSION.replace(semverRe, '$1.$2');

    if (reportVersion < lhVersion) {
      // TODO: figure out how to handler older reports. All permalinks to older
      // reports will start to throw this warning when the viewer rev's its
      // minor LH version.
      // See https://github.com/GoogleChrome/lighthouse/issues/1108
      logger.warn('Results may not display properly.\n' +
                  'Report was created with an earlier version of ' +
                  `Lighthouse (${reportJson.lighthouseVersion}). The latest ` +
                  `version is ${LH_CURRENT_VERSION}.`);
    }
  }

  replaceReportHTML(json) {
    this._validateReportJson(json);

    const reportGenerator = new ReportGenerator();

    let html;
    try {
      html = reportGenerator.generateHTML(json, 'viewer');
    } catch (err) {
      html = reportGenerator.renderException(err, json);
    }

    // Use only the results section of the full HTML page.
    const div = document.createElement('div');
    div.innerHTML = html;

    document.title = div.querySelector('title').textContent;

    html = div.querySelector('.js-report').outerHTML;

    this.json = json;

    // Remove the placeholder drop area UI once the user has interacted.
    this.fileUploader.removeDropzonePlaceholder();

    // Replace the HTML and hook up event listeners to the new DOM.
    document.querySelector('output').innerHTML = html;
    this.initUI();

    ga('send', 'event', 'report', 'view');
  }

  /**
   * Updates the page's HTML with contents of the JSON file passed in.
   * @param {!File} file
   * @return {!Promise<string>}
   * @throws file was not valid JSON generated by Lighthouse or an unknown file
   *     type of used.
   */
  onFileUpload(file) {
    return FileUploader.readFile(file).then(str => {
      if (!file.type.match('json')) {
        throw new Error('Unsupported report format. Expected JSON.');
      }
      this._isNewReport = true;

      this.replaceReportHTML(JSON.parse(str));
    }).catch(err => logger.error(err.message));
  }

  /**
   * Shares the current report by creating a gist on Github.
   * @return {!Promise<string>} id of the created gist.
   */
  onShare() {
    ga('send', 'event', 'report', 'share');

    // TODO: find and reuse existing json gist if one exists.
    return this.github.createGist(this.json).then(id => {
      ga('send', 'event', 'report', 'created');

      this.disableShareButton();
      history.pushState({}, null, `${APP_URL}?gist=${id}`);

      return id;
    }).catch(err => logger.log(err.message));
  }

  /**
   * Handler copy events.
   */
  onCopy(e) {
    // Only handle copy button presses (e.g. ignore the user copying page text).
    if (this._copyAttempt) {
      // We want to write our own data to the clipboard, not the user's text selection.
      e.preventDefault();
      e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
      logger.log('Report copied to clipboard');
    }

    this._copyAttempt = false;
  }

  /**
   * Copies the report JSON to the clipboard (if supported by the browser).
   */
  onCopyButtonClick() {
    ga('send', 'event', 'report', 'copy');

    try {
      if (document.queryCommandSupported('copy')) {
        this._copyAttempt = true;

        // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
        // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
        const successful = document.execCommand('copy');
        if (!successful) {
          this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
          logger.warn('Your browser does not support copy to clipboard.');
        }
      }
    } catch (err) {
      this._copyAttempt = false;
      logger.log(err.message);
    }
  }

  /**
   * Enables pasting a JSON report on the page.
   */
  onPaste(e) {
    e.preventDefault();

    try {
      const json = JSON.parse(e.clipboardData.getData('text'));
      this.replaceReportHTML(json);

      ga('send', 'event', 'report', 'paste');
    } catch (err) {
      // noop
    }
  }

  /**
   * Click handler for export button.
   */
  onExportButtonClick(e) {
    e.target.classList.toggle('active');
    document.addEventListener('keydown', this.onKeyDown);
  }

  /**
   * Downloads a file (blob) using a[download].
   * @param {Blob|File} The file to save.
   */
  _saveFile(blob) {
    const filename = getFilenamePrefix({
      url: this.json.url,
      date: new Date(this.json.generatedTime)
    });

    const ext = blob.type.match('json') ? '.json' : '.html';

    const a = document.createElement('a');
    a.download = `${filename}${ext}`;
    a.href = URL.createObjectURL(blob);
    a.click();

    setTimeout(_ => URL.revokeObjectURL(a.href), 500); // cleanup.
  }

  /**
   * Handler for "export as" button.
   */
  onExport(e) {
    if (!e.target.dataset.action) {
      return;
    }

    switch (e.target.dataset.action) {
      case 'print':
        window.print();
        break;
      case 'save-json':
        const jsonStr = JSON.stringify(this.json, null, 2);
        this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
        break;
      case 'save-html':
        const reportGenerator = new ReportGenerator();
        try {
          const htmlStr = reportGenerator.generateHTML(this.json, 'cli');
          this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
        } catch (err) {
          logger.error('Could not export as HTML.');
        }
        break;
    }

    this.closeExportDropdown();
    document.removeEventListener('keydown', this.onKeyDown);
  }

  /**
   * Keydown handler for the document.
   */
  onKeyDown(e) {
    if (e.keyCode === 27) { // ESC
      this.closeExportDropdown();
    }
  }
}

module.exports = LighthouseViewerReport;
<commit_msg>Viewer: dont check file type, try to parse json file (#1234)<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

'use strict';

/* global ga */

const FileUploader = require('./fileuploader');
const GithubAPI = require('./github');
const idb = require('idb-keyval');
const logger = require('./logger');
const ReportGenerator = require('../../../lighthouse-core/report/report-generator');

// TODO: We only need getFilenamePrefix from asset-saver. Tree shake!
const getFilenamePrefix = require('../../../lighthouse-core/lib/asset-saver').getFilenamePrefix;

const LH_CURRENT_VERSION = require('../../../package.json').version;
const APP_URL = `${location.origin}${location.pathname}`;

/**
 * Class to handle dynamic changes to the page when users view new reports.
 * @class
 */
class LighthouseViewerReport {

  constructor() {
    this.onShare = this.onShare.bind(this);
    this.onCopy = this.onCopy.bind(this);
    this.onCopyButtonClick = this.onCopyButtonClick.bind(this);
    this.onFileUpload = this.onFileUpload.bind(this);
    this.onPaste = this.onPaste.bind(this);
    this.onExportButtonClick = this.onExportButtonClick.bind(this);
    this.onExport = this.onExport.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);

    this._copyAttempt = false;

    this.json = null;
    this.fileUploader = new FileUploader(this.onFileUpload);
    this.github = new GithubAPI();

    this._isNewReport = true;

    this.initUI();
    this.loadFromURL();
  }

  initUI() {
    this.shareButton = document.querySelector('.js-share');
    if (this.shareButton) {
      this.shareButton.addEventListener('click', this.onShare);

      // Disable the share button after the user shares the gist or if we're loading
      // a gist from Github. In both cases, the gist is already shared :)
      if (this._isNewReport) {
        this.enableShareButton();
      } else {
        this.disableShareButton();
      }
    }

    const copyButton = document.querySelector('.js-copy');
    if (copyButton) {
      copyButton.addEventListener('click', this.onCopyButtonClick);
      document.addEventListener('copy', this.onCopy);
    }

    document.addEventListener('paste', this.onPaste);

    this.exportButton = document.querySelector('.js-export');
    if (this.exportButton) {
      this.exportButton.addEventListener('click', this.onExportButtonClick);
      const dropdown = document.querySelector('.export-dropdown');
      dropdown.addEventListener('click', this.onExport);
    }
  }

  enableShareButton() {
    this.shareButton.classList.remove('disable');
    this.shareButton.disabled = false;
  }

  disableShareButton() {
    this.shareButton.classList.add('disable');
    this.shareButton.disabled = true;
  }

  closeExportDropdown() {
    this.exportButton.classList.remove('active');
  }

  loadFromURL() {
    // Pull gist id from URL and render it.
    const params = new URLSearchParams(location.search);
    const gistId = params.get('gist');
    if (gistId) {
      logger.log('Fetching report from Github...', false);

      this.github.auth.ready.then(_ => {
        this.github.getGistFileContentAsJson(gistId).then(json => {
          logger.hide();

          this._isNewReport = false;
          this.replaceReportHTML(json.content);

          // Save fetched json and etag to IDB so we can use it later for 304
          // requests. This is done after replaceReportHTML, so we don't save
          // unrecognized JSON to IDB. replaceReportHTML will throw in that case.
          return idb.set(gistId, {etag: json.etag, content: json.content});
        }).catch(err => logger.error(err.message));
      });
    }
  }

  _validateReportJson(reportJson) {
    if (!reportJson.lighthouseVersion) {
      throw new Error('JSON file was not generated by Lighthouse');
    }

    // Leave off patch version in the comparison.
    const semverRe = new RegExp(/^(\d+)?\.(\d+)?\.(\d+)$/);
    const reportVersion = reportJson.lighthouseVersion.replace(semverRe, '$1.$2');
    const lhVersion = LH_CURRENT_VERSION.replace(semverRe, '$1.$2');

    if (reportVersion < lhVersion) {
      // TODO: figure out how to handler older reports. All permalinks to older
      // reports will start to throw this warning when the viewer rev's its
      // minor LH version.
      // See https://github.com/GoogleChrome/lighthouse/issues/1108
      logger.warn('Results may not display properly.\n' +
                  'Report was created with an earlier version of ' +
                  `Lighthouse (${reportJson.lighthouseVersion}). The latest ` +
                  `version is ${LH_CURRENT_VERSION}.`);
    }
  }

  replaceReportHTML(json) {
    this._validateReportJson(json);

    const reportGenerator = new ReportGenerator();

    let html;
    try {
      html = reportGenerator.generateHTML(json, 'viewer');
    } catch (err) {
      html = reportGenerator.renderException(err, json);
    }

    // Use only the results section of the full HTML page.
    const div = document.createElement('div');
    div.innerHTML = html;

    document.title = div.querySelector('title').textContent;

    html = div.querySelector('.js-report').outerHTML;

    this.json = json;

    // Remove the placeholder drop area UI once the user has interacted.
    this.fileUploader.removeDropzonePlaceholder();

    // Replace the HTML and hook up event listeners to the new DOM.
    document.querySelector('output').innerHTML = html;
    this.initUI();

    ga('send', 'event', 'report', 'view');
  }

  /**
   * Updates the page's HTML with contents of the JSON file passed in.
   * @param {!File} file
   * @return {!Promise<string>}
   * @throws file was not valid JSON generated by Lighthouse or an unknown file
   *     type of used.
   */
  onFileUpload(file) {
    return FileUploader.readFile(file).then(str => {
      let json;
      try {
        json = JSON.parse(str);
      } catch(e) {
        throw new Error('Could not parse JSON file.');
      }

      this._isNewReport = true;

      this.replaceReportHTML(json);
    }).catch(err => logger.error(err.message));
  }

  /**
   * Shares the current report by creating a gist on Github.
   * @return {!Promise<string>} id of the created gist.
   */
  onShare() {
    ga('send', 'event', 'report', 'share');

    // TODO: find and reuse existing json gist if one exists.
    return this.github.createGist(this.json).then(id => {
      ga('send', 'event', 'report', 'created');

      this.disableShareButton();
      history.pushState({}, null, `${APP_URL}?gist=${id}`);

      return id;
    }).catch(err => logger.log(err.message));
  }

  /**
   * Handler copy events.
   */
  onCopy(e) {
    // Only handle copy button presses (e.g. ignore the user copying page text).
    if (this._copyAttempt) {
      // We want to write our own data to the clipboard, not the user's text selection.
      e.preventDefault();
      e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
      logger.log('Report copied to clipboard');
    }

    this._copyAttempt = false;
  }

  /**
   * Copies the report JSON to the clipboard (if supported by the browser).
   */
  onCopyButtonClick() {
    ga('send', 'event', 'report', 'copy');

    try {
      if (document.queryCommandSupported('copy')) {
        this._copyAttempt = true;

        // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
        // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
        const successful = document.execCommand('copy');
        if (!successful) {
          this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
          logger.warn('Your browser does not support copy to clipboard.');
        }
      }
    } catch (err) {
      this._copyAttempt = false;
      logger.log(err.message);
    }
  }

  /**
   * Enables pasting a JSON report on the page.
   */
  onPaste(e) {
    e.preventDefault();

    try {
      const json = JSON.parse(e.clipboardData.getData('text'));
      this.replaceReportHTML(json);

      ga('send', 'event', 'report', 'paste');
    } catch (err) {
      // noop
    }
  }

  /**
   * Click handler for export button.
   */
  onExportButtonClick(e) {
    e.target.classList.toggle('active');
    document.addEventListener('keydown', this.onKeyDown);
  }

  /**
   * Downloads a file (blob) using a[download].
   * @param {Blob|File} The file to save.
   */
  _saveFile(blob) {
    const filename = getFilenamePrefix({
      url: this.json.url,
      date: new Date(this.json.generatedTime)
    });

    const ext = blob.type.match('json') ? '.json' : '.html';

    const a = document.createElement('a');
    a.download = `${filename}${ext}`;
    a.href = URL.createObjectURL(blob);
    a.click();

    setTimeout(_ => URL.revokeObjectURL(a.href), 500); // cleanup.
  }

  /**
   * Handler for "export as" button.
   */
  onExport(e) {
    if (!e.target.dataset.action) {
      return;
    }

    switch (e.target.dataset.action) {
      case 'print':
        window.print();
        break;
      case 'save-json':
        const jsonStr = JSON.stringify(this.json, null, 2);
        this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
        break;
      case 'save-html':
        const reportGenerator = new ReportGenerator();
        try {
          const htmlStr = reportGenerator.generateHTML(this.json, 'cli');
          this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
        } catch (err) {
          logger.error('Could not export as HTML.');
        }
        break;
    }

    this.closeExportDropdown();
    document.removeEventListener('keydown', this.onKeyDown);
  }

  /**
   * Keydown handler for the document.
   */
  onKeyDown(e) {
    if (e.keyCode === 27) { // ESC
      this.closeExportDropdown();
    }
  }
}

module.exports = LighthouseViewerReport;
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');

var Snap = require('snapsvg');

var GraphicsUtil = require('../../util/GraphicsUtil'),
    createLine = require('../../draw/Renderer').createLine;

/**
 * A plugin that provides interactivity in terms of events (mouse over and selection to a diagram).
 *
 * @class
 *
 * @param {EventBus} eventBus
 */
function InteractionEvents(eventBus, styles) {

  var HIT_STYLE = styles.cls('djs-hit', [ 'no-fill', 'no-border' ], {
    stroke: 'white',
    strokeWidth: 10
  });

  function fire(event, baseEvent, eventName) {
    var e = _.extend({}, baseEvent, event);
    eventBus.fire(eventName, e);
  }

  function registerEvents(eventBus) {

    eventBus.on('canvas.init', function(event) {
      var root = event.root;

      // implement direct canvas click
      root.click(function(event) {

        /**
         * An event indicating that the canvas has been directly clicked
         *
         * @memberOf InteractionEvents
         *
         * @event canvas.click
         *
         * @type {Object}
         */
        eventBus.fire('canvas.click', _.extend({}, event, { root: root }));
      });
    });

    eventBus.on([ 'shape.added', 'connection.added' ], function(event) {
      var element = event.element,
          gfx = event.gfx,
          visual = GraphicsUtil.getVisual(gfx),
          baseEvent = { element: element, gfx: gfx };

      var hit, type;

      if (element.waypoints) {
        hit = createLine(element.waypoints);
        type = 'connection';
      } else {
        hit = Snap.create('rect', { x: 0, y: 0, width: element.width, height: element.height });
        type = 'shape';
      }

      hit.attr(HIT_STYLE).appendTo(gfx.node);

      gfx.hover(function(e) {

        /**
         * An event indicating that shape|connection has been hovered
         *
         * shape.hover, connection.hover
         */
        fire(e, baseEvent, type + '.hover');
      }, function(e) {
        fire(e, baseEvent, type + '.out');
      });

      visual.click(function(e) {
        fire(e, baseEvent, type + '.click');
      });

      visual.dblclick(function(e) {
        fire(e, baseEvent, type + '.dblclick');
      });
    });
  }

  registerEvents(eventBus);
}

InteractionEvents.$inject = [ 'eventBus', 'styles' ];

module.exports = InteractionEvents;<commit_msg>fix(interaction-events): use djs-hit to register click<commit_after>'use strict';

var _ = require('lodash');

var Snap = require('snapsvg');

var GraphicsUtil = require('../../util/GraphicsUtil'),
    createLine = require('../../draw/Renderer').createLine;

/**
 * A plugin that provides interactivity in terms of events (mouse over and selection to a diagram).
 *
 * @class
 *
 * @param {EventBus} eventBus
 */
function InteractionEvents(eventBus, styles) {

  var HIT_STYLE = styles.cls('djs-hit', [ 'no-fill', 'no-border' ], {
    stroke: 'white',
    strokeWidth: 10
  });

  function fire(event, baseEvent, eventName) {
    var e = _.extend({}, baseEvent, event);
    eventBus.fire(eventName, e);
  }

  function registerEvents(eventBus) {

    eventBus.on('canvas.init', function(event) {
      var root = event.root;

      // implement direct canvas click
      event.paper.click(function(event) {

        /**
         * An event indicating that the canvas has been directly clicked
         *
         * @memberOf InteractionEvents
         *
         * @event canvas.click
         *
         * @type {Object}
         */
        eventBus.fire('canvas.click', _.extend({}, event, { root: root }));
      });
    });

    eventBus.on([ 'shape.added', 'connection.added' ], function(event) {
      var element = event.element,
          gfx = event.gfx,
          visual = GraphicsUtil.getVisual(gfx),
          baseEvent = { element: element, gfx: gfx };

      var hit, type;

      if (element.waypoints) {
        hit = createLine(element.waypoints);
        type = 'connection';
      } else {
        hit = Snap.create('rect', { x: 0, y: 0, width: element.width, height: element.height });
        type = 'shape';
      }

      hit.attr(HIT_STYLE).appendTo(gfx.node);

      gfx.hover(function(e) {

        /**
         * An event indicating that shape|connection has been hovered
         *
         * shape.hover, connection.hover
         */
        fire(e, baseEvent, type + '.hover');
      }, function(e) {
        fire(e, baseEvent, type + '.out');
      });

      hit.click(function(e) {
        fire(e, baseEvent, type + '.click');
      });

      hit.dblclick(function(e) {
        fire(e, baseEvent, type + '.dblclick');
      });
    });
  }

  registerEvents(eventBus);
}

InteractionEvents.$inject = [ 'eventBus', 'styles' ];

module.exports = InteractionEvents;<|endoftext|>


<commit_before>/**
 * @class Denkmal_App
 * @extends CM_App
 */
var Denkmal_App = CM_App.extend({

  ready: function() {
    CM_App.prototype.ready.call(this);

    this._registerServiceWorker();
  },

  _registerServiceWorker: function() {
    if ('serviceWorker' in navigator) {
      var path = cm.getUrlResource('layout', 'js/service-worker.js', {sameOrigin: true, root: true});

      navigator.serviceWorker.register(path).then(function(registration) {
        cm.debug.log('ServiceWorker registration succeeded.');
      }).catch(function(error) {
        cm.debug.log('ServiceWorker registration failed.', error);
      });
    } else {
      cm.debug.log('ServiceWorker not supported.');
    }
  }

});
<commit_msg>Explicitely specify service worker scope<commit_after>/**
 * @class Denkmal_App
 * @extends CM_App
 */
var Denkmal_App = CM_App.extend({

  ready: function() {
    CM_App.prototype.ready.call(this);

    this._registerServiceWorker();
  },

  _registerServiceWorker: function() {
    if ('serviceWorker' in navigator) {
      var path = cm.getUrlResource('layout', 'js/service-worker.js', {sameOrigin: true, root: true});

      navigator.serviceWorker.register(path, {scope: '/'}).then(function(registration) {
        cm.debug.log('ServiceWorker registration succeeded.');
      }).catch(function(error) {
        cm.debug.log('ServiceWorker registration failed.', error);
      });
    } else {
      cm.debug.log('ServiceWorker not supported.');
    }
  }

});
<|endoftext|>