<commit_before>/**
 * ------------------------------------------
 * Gulpfile
 * ------------------------------------------
 * - Starts a server on default port (8000)
 * - Loads all available sources from /client folder
 * - Loads all bower and vendor components
 * - JADE and STYLUS processing
 * - Allows different bundling for production vs development instances
 *     (see tasks "build:dev" and "build:prod")
 * - Uglifying/minifying js/css code
 * - Injects all file references to /public/index.html
 * ------------------------------------------
 */

var
    del = require('del'),
    gulp = require('gulp'),
    merge = require('merge-stream'),
    path = require('path'),
    pkg = require('./package.json'),
    plugins = require('gulp-load-plugins')(),
    runSequence = require('run-sequence'),
    stylish = require('jshint-stylish'),
    vinylPaths = require('vinyl-paths'),
    plumber = require('gulp-plumber'),
    source = require('vinyl-source-stream'),
    config = require('./lib/config'),
    iife = require('gulp-iife');

var sources = {
  jshint: {
    client: {
      src: './client/**/*.js',
      jshintrc: './client/.jshintrc'
    },
    tests: {
      src: './tests/**/*.js',
      jshintrc: './tests/.jshintrc'
    }
  },

  stylus: './client/css/main.styl',

  jade: ['./client/**/*.jade', '!./client/html/index.jade'],
  index: './client/html/index.jade',

  dest: './public/' + pkg.version
};

// Vendor & bower plugins to load in specified order
var bowerfiles = {
  css: [
    'public/' + pkg.version + '/vendor/bootstrap/css/bootstrap.min.css',
  ],

  js: [
    'public/' + pkg.version + '/bower_components/angular/angular.min.js',
    'public/' + pkg.version + '/bower_components/angular-animate/angular-animate.min.js',
    'public/' + pkg.version + '/bower_components/angular-sanitize/angular-sanitize.min.js',
    'public/' + pkg.version + '/bower_components/angular-ui-router/release/angular-ui-router.min.js',
    'public/' + pkg.version + '/vendor/d3/d3.v3.js',
    'public/' + pkg.version + '/vendor/d3-tip/d3.tip.v0.6.3.js',
  ]
};

var jadeOptions = {
  pretty: true,
  compileDebug: false,
  data: {
    pkg: pkg
  }
};

var stylusOptions = {
  paths: [
    './bower_components/normalize-css/'
  ],
  'include css': true
};

var injectOptions = { relative: true, addRootSlash: true };

// fonts etc. e.g. '//fonts.googleapis.com/css?family=Lato'
var cssCDN = ["http://fonts.googleapis.com/css?family=Gochi+Hand"];

var cssCode = '\n\n';
var spacing = '    ';

cssCDN.forEach(function(value) {
  cssCode += spacing + '<link rel="stylesheet" href="' + value + '" />\n';
});

gulp.task('lint:client', function() {
  return gulp.src(sources.jshint.client.src)
    .pipe(plumber())
    .pipe(plugins.jscs())
    .pipe(plugins.jshint(sources.jshint.client.jshintrc))
    .pipe(plugins.jshint.reporter(stylish));
});

gulp.task('lint:tests', function() {
  return gulp.src(sources.jshint.tests.src)
    .pipe(plumber())
    .pipe(plugins.jscs())
    .pipe(plugins.jshint(sources.jshint.tests.jshintrc))
    .pipe(plugins.jshint.reporter(stylish));
});

gulp.task('make:copy:config', function() {
  //get and publish Config module
  var stream = source('_config.js');
  stream.end(config.configModule(pkg));
  stream.pipe(gulp.dest(sources.dest + '/js/app'));
});

gulp.task('copy:css', function() {
  return gulp.src(sources.stylus, { base: './client/' })
    .pipe(plugins.stylus(stylusOptions))
    .pipe(plugins.autoprefixer({ browsers: ['last 2 versions'] }))
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:bower', function() {
  return gulp.src(['./bower_components/**/*', './vendor/**/*'], { base: './' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:js', function() {
  return gulp.src('./client/js/**/*.js', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:index', function() {
  return gulp.src(sources.index)
    .pipe(plugins.jade(jadeOptions))
    .pipe(gulp.dest('./public/'));
});

gulp.task('copy:html', function() {
  return gulp.src(['./client/html/templates/**/*.jade', './client/html/directives/**/*.jade'], { base: './client/html/' })
    .pipe(plugins.jade(jadeOptions))
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:fonts', function() {
  return gulp.src('./client/fonts/**/*.{ttf,woff,eot,svg}', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:img', function() {
  return gulp.src('./client/img/**/*', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:json', function() {
  return gulp.src('./client/json/*.json', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

// delete everything inside /public
gulp.task('clean', function() {
  return gulp.src('./public/')
    .pipe(vinylPaths(del));
});

gulp.task('lint', ['lint:client']);

gulp.task('build:dev', ['lint'], function() {
  runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'inject:dev', 'server', watch);
});

gulp.task('build:prod', ['lint'], function() {
  runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'optimize', 'inject:prod', 'clean:prod', 'server'); // proper production setup
  //runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'inject:dev');
});

// inject file references in /public/index.html at specified locations [uses gulp-inject]
gulp.task('inject:dev', function() {
  var cssFiles = bowerfiles.css.concat(['public/' + pkg.version + '/css/main.css']);
  var jsOtherFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/other/**/*.js']);
  var jsFilterFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/filters/*.js']);
  var jsServiceFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/services/*.js']);
  var jsDirectiveFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/directives/*.js']);
  var jsControllerFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/controllers/*.js']);
  var jsAppFile = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/*.js']);
  var allFiles = cssFiles.concat(jsOtherFiles).concat(jsFilterFiles).concat(jsServiceFiles).concat(jsDirectiveFiles).concat(jsControllerFiles).concat(jsAppFile);

  // Prioritise/order some of some files to be injected first
  var orderOfTheFiles = bowerfiles.css.concat(['main.css']);
  orderOfTheFiles = orderOfTheFiles.concat(bowerfiles.js);
  orderOfTheFiles = orderOfTheFiles.concat(['_config.js']);
  orderOfTheFiles = orderOfTheFiles.concat(['_app.js']);
  orderOfTheFiles = getOrderOfTheFiles(orderOfTheFiles);//strip paths...

  var allFilesStream = gulp.src(allFiles, { read: false })
    .pipe(plugins.order(orderOfTheFiles));

  return gulp.src('./public/index.html')
    .pipe(plugins.inject(allFilesStream, injectOptions))// will insert after <!-- inject:css --> and <!-- inject:js -->
    .pipe(plugins.injectString.after('<!-- inject:css -->', cssCode))//add cssCDN and other files
    .pipe(gulp.dest('./public'));
});

// minify css and uglify js code in production
gulp.task('optimize', function() {
  var cssFiles = bowerfiles.css.concat(['public/' + pkg.version + '/css/main.css']);
  var orderOfTheCssFiles = getOrderOfTheFiles(bowerfiles.css.concat(['main.css']));

  var processCSS = gulp.src(cssFiles)
    .pipe(plugins.order(orderOfTheCssFiles))
    .pipe(plugins.concat('app.min.css'))
    .pipe(plugins.minifyCss())
    .pipe(gulp.dest(sources.dest + '/css'));

  var jsVendor = bowerfiles.js.concat([]);
  var orderOfTheVendorFiles = getOrderOfTheFiles(bowerfiles.js.concat([]));

  var jsApp = ['public/' + pkg.version + '/js/**/*.js'];
  var orderOfTheAppFiles = ['_config.js', '_app.js'];

  var processJSVendor = gulp.src(jsVendor)
    .pipe(plugins.order(orderOfTheVendorFiles))
    .pipe(plugins.concat('vendor.min.js'))
    .pipe(plugins.uglify())
    .pipe(gulp.dest(sources.dest + '/js'));

  var processJSApp = gulp.src(jsApp)
    .pipe(plugins.order(orderOfTheAppFiles))
    .pipe(plugins.ngAnnotate())
    .pipe(plugins.concat('app.min.js'))
    .pipe(plugins.uglify())
    .pipe(iife())
    .pipe(gulp.dest(sources.dest + '/js'));

  return merge(processCSS, processJSVendor, processJSApp);
});

// inject minified/uglified file references in /public/index.html at specified locations [uses gulp-inject]
gulp.task('inject:prod', function() {
  var allFiles = gulp.src([sources.dest + '/css/app.min.css', sources.dest + '/js/vendor.min.js', sources.dest + '/js/app.min.js'], { read: false });

  return gulp.src('./public/index.html')
    .pipe(plugins.inject(allFiles, injectOptions))// will insert after <!-- inject:css --> and <!-- inject:js -->
    .pipe(plugins.injectString.after('<!-- inject:css -->', cssCode))//add cssCDN and other files
    .pipe(gulp.dest('./public/'));
});

// remove all dev code and keep only minified/uglified files in production
gulp.task('clean:prod', function() {
  return gulp.src([sources.dest + '/bower_components/', sources.dest + '/css/**/*', '!' + sources.dest + '/css/app.min.css', sources.dest + '/js/**/*', '!' + sources.dest + '/js/app.min.js', '!' + sources.dest + '/js/vendor.min.js'], { read: false })
    .pipe(vinylPaths(del));
});

gulp.task('default', ['build:dev']);

gulp.task('server', function() {
  return gulp.src('./public')
    .pipe(plugins.webserver({
      livereload: true,
      open: true,
      https: false,
      fallback: 'index.html'
    }));
});

function watch() {
  gulp.watch('client/**/*.js', fileChanged('js'));
  gulp.watch('client/**/*.styl', ['copy:css']);
  gulp.watch(['client/**/*.jade', '!client/html/layouts/**/*.jade'], fileChanged('jade'));
  gulp.watch(['client/html/layouts/**/*.jade'], function() {
    return runSequence(['copy:html', 'copy:index'], 'inject:dev');
  });
  gulp.watch('client/fonts/**/*', ['copy:fonts']);
  gulp.watch('client/img/**/*', ['copy:img']);
}

// Helper methods below

function fileChanged(type) {
  return function(event) {
    var loc = event.path;

    console.log('\n' + path.basename(loc) + ' was ' + event.type);

    switch (event.type) {
      case 'added':
      case 'changed':
        return fileAddedOrChanged(type, loc, event.type);
      case 'deleted':
        return fileDeleted(loc);
    }
  };
}

function fileAddedOrChanged(type, loc, eventType) {
  switch (type) {
    case 'js':
      var jsfile = gulp.src(loc, { base: './client/' })
        .pipe(plumber())
        .pipe(plugins.jshint(sources.jshint.client.jshintrc))
        .pipe(plugins.jscs())
        .pipe(plugins.jshint.reporter(stylish))
        .pipe(gulp.dest(sources.dest));

      if (eventType != 'changed') {
        setTimeout(function() {
          runSequence('inject:dev');
        }, 200);
      }

      return jsfile;

    case 'jade':
      if (loc == __dirname + '/client/html/index.jade') {
        var file = gulp.src(loc, { base: './client/html/' })
          .pipe(plugins.jade(jadeOptions))
          .pipe(gulp.dest('./public'));

        setTimeout(function() {
          runSequence('inject:dev');
        }, 200);

        return file;
      }
      else {
        return gulp.src(loc, { base: './client/html/' })
          .pipe(plugins.jade(jadeOptions))
          .pipe(gulp.dest(sources.dest));
      }
  }
}

function fileDeleted(loc) {
  var file = gulp.src(deletePath(loc), { read: false })
    .pipe(vinylPaths(del));

  setTimeout(function() {
    runSequence('inject:dev');
  }, 200);

  return file;
}

function deletePath(loc) {
  var newPath = path.relative(__dirname + '/client/', loc);
  newPath = path.join(sources.dest, newPath);

  var end;

  switch (path.extname(newPath)) {
    case '.jade': end = path.basename(newPath, '.jade') + '.html'; break;
    case '.js': end = path.basename(newPath); break;
  }

  return path.dirname(newPath) + '/' + end;
}

// Strip paths and keep only filenames
function getOrderOfTheFiles(files) {
  var orderOfTheFiles = files;

  orderOfTheFiles.forEach(function(value, key) {
    orderOfTheFiles[key] = path.basename(value);
  });

  return orderOfTheFiles;
}
<commit_msg>switched build option to production<commit_after>/**
 * ------------------------------------------
 * Gulpfile
 * ------------------------------------------
 * - Starts a server on default port (8000)
 * - Loads all available sources from /client folder
 * - Loads all bower and vendor components
 * - JADE and STYLUS processing
 * - Allows different bundling for production vs development instances
 *     (see tasks "build:dev" and "build:prod")
 * - Uglifying/minifying js/css code
 * - Injects all file references to /public/index.html
 * ------------------------------------------
 */

var
    del = require('del'),
    gulp = require('gulp'),
    merge = require('merge-stream'),
    path = require('path'),
    pkg = require('./package.json'),
    plugins = require('gulp-load-plugins')(),
    runSequence = require('run-sequence'),
    stylish = require('jshint-stylish'),
    vinylPaths = require('vinyl-paths'),
    plumber = require('gulp-plumber'),
    source = require('vinyl-source-stream'),
    config = require('./lib/config'),
    iife = require('gulp-iife');

var sources = {
  jshint: {
    client: {
      src: './client/**/*.js',
      jshintrc: './client/.jshintrc'
    },
    tests: {
      src: './tests/**/*.js',
      jshintrc: './tests/.jshintrc'
    }
  },

  stylus: './client/css/main.styl',

  jade: ['./client/**/*.jade', '!./client/html/index.jade'],
  index: './client/html/index.jade',

  dest: './public/' + pkg.version
};

// Vendor & bower plugins to load in specified order
var bowerfiles = {
  css: [
    'public/' + pkg.version + '/vendor/bootstrap/css/bootstrap.min.css',
  ],

  js: [
    'public/' + pkg.version + '/bower_components/angular/angular.min.js',
    'public/' + pkg.version + '/bower_components/angular-animate/angular-animate.min.js',
    'public/' + pkg.version + '/bower_components/angular-sanitize/angular-sanitize.min.js',
    'public/' + pkg.version + '/bower_components/angular-ui-router/release/angular-ui-router.min.js',
    'public/' + pkg.version + '/vendor/d3/d3.v3.js',
    'public/' + pkg.version + '/vendor/d3-tip/d3.tip.v0.6.3.js',
  ]
};

var jadeOptions = {
  pretty: true,
  compileDebug: false,
  data: {
    pkg: pkg
  }
};

var stylusOptions = {
  paths: [
    './bower_components/normalize-css/'
  ],
  'include css': true
};

var injectOptions = { relative: true, addRootSlash: true };

// fonts etc. e.g. '//fonts.googleapis.com/css?family=Lato'
var cssCDN = ["http://fonts.googleapis.com/css?family=Gochi+Hand"];

var cssCode = '\n\n';
var spacing = '    ';

cssCDN.forEach(function(value) {
  cssCode += spacing + '<link rel="stylesheet" href="' + value + '" />\n';
});

gulp.task('lint:client', function() {
  return gulp.src(sources.jshint.client.src)
    .pipe(plumber())
    .pipe(plugins.jscs())
    .pipe(plugins.jshint(sources.jshint.client.jshintrc))
    .pipe(plugins.jshint.reporter(stylish));
});

gulp.task('lint:tests', function() {
  return gulp.src(sources.jshint.tests.src)
    .pipe(plumber())
    .pipe(plugins.jscs())
    .pipe(plugins.jshint(sources.jshint.tests.jshintrc))
    .pipe(plugins.jshint.reporter(stylish));
});

gulp.task('make:copy:config', function() {
  //get and publish Config module
  var stream = source('_config.js');
  stream.end(config.configModule(pkg));
  stream.pipe(gulp.dest(sources.dest + '/js/app'));
});

gulp.task('copy:css', function() {
  return gulp.src(sources.stylus, { base: './client/' })
    .pipe(plugins.stylus(stylusOptions))
    .pipe(plugins.autoprefixer({ browsers: ['last 2 versions'] }))
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:bower', function() {
  return gulp.src(['./bower_components/**/*', './vendor/**/*'], { base: './' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:js', function() {
  return gulp.src('./client/js/**/*.js', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:index', function() {
  return gulp.src(sources.index)
    .pipe(plugins.jade(jadeOptions))
    .pipe(gulp.dest('./public/'));
});

gulp.task('copy:html', function() {
  return gulp.src(['./client/html/templates/**/*.jade', './client/html/directives/**/*.jade'], { base: './client/html/' })
    .pipe(plugins.jade(jadeOptions))
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:fonts', function() {
  return gulp.src('./client/fonts/**/*.{ttf,woff,eot,svg}', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:img', function() {
  return gulp.src('./client/img/**/*', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

gulp.task('copy:json', function() {
  return gulp.src('./client/json/*.json', { base: './client/' })
    .pipe(gulp.dest(sources.dest));
});

// delete everything inside /public
gulp.task('clean', function() {
  return gulp.src('./public/')
    .pipe(vinylPaths(del));
});

gulp.task('lint', ['lint:client']);

gulp.task('build:dev', ['lint'], function() {
  runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'inject:dev', 'server', watch);
});

gulp.task('build:prod', ['lint'], function() {
  runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'optimize', 'inject:prod', 'clean:prod', 'server'); // proper production setup
  //runSequence('clean', ['make:copy:config', 'copy:css', 'copy:html', 'copy:js', 'copy:index', 'copy:fonts', 'copy:img', 'copy:json', 'copy:bower'], 'inject:dev');
});

// inject file references in /public/index.html at specified locations [uses gulp-inject]
gulp.task('inject:dev', function() {
  var cssFiles = bowerfiles.css.concat(['public/' + pkg.version + '/css/main.css']);
  var jsOtherFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/other/**/*.js']);
  var jsFilterFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/filters/*.js']);
  var jsServiceFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/services/*.js']);
  var jsDirectiveFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/directives/*.js']);
  var jsControllerFiles = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/controllers/*.js']);
  var jsAppFile = bowerfiles.js.concat(['public/' + pkg.version + '/js/app/angular/*.js']);
  var allFiles = cssFiles.concat(jsOtherFiles).concat(jsFilterFiles).concat(jsServiceFiles).concat(jsDirectiveFiles).concat(jsControllerFiles).concat(jsAppFile);

  // Prioritise/order some of some files to be injected first
  var orderOfTheFiles = bowerfiles.css.concat(['main.css']);
  orderOfTheFiles = orderOfTheFiles.concat(bowerfiles.js);
  orderOfTheFiles = orderOfTheFiles.concat(['_config.js']);
  orderOfTheFiles = orderOfTheFiles.concat(['_app.js']);
  orderOfTheFiles = getOrderOfTheFiles(orderOfTheFiles);//strip paths...

  var allFilesStream = gulp.src(allFiles, { read: false })
    .pipe(plugins.order(orderOfTheFiles));

  return gulp.src('./public/index.html')
    .pipe(plugins.inject(allFilesStream, injectOptions))// will insert after <!-- inject:css --> and <!-- inject:js -->
    .pipe(plugins.injectString.after('<!-- inject:css -->', cssCode))//add cssCDN and other files
    .pipe(gulp.dest('./public'));
});

// minify css and uglify js code in production
gulp.task('optimize', function() {
  var cssFiles = bowerfiles.css.concat(['public/' + pkg.version + '/css/main.css']);
  var orderOfTheCssFiles = getOrderOfTheFiles(bowerfiles.css.concat(['main.css']));

  var processCSS = gulp.src(cssFiles)
    .pipe(plugins.order(orderOfTheCssFiles))
    .pipe(plugins.concat('app.min.css'))
    .pipe(plugins.minifyCss())
    .pipe(gulp.dest(sources.dest + '/css'));

  var jsVendor = bowerfiles.js.concat([]);
  var orderOfTheVendorFiles = getOrderOfTheFiles(bowerfiles.js.concat([]));

  var jsApp = ['public/' + pkg.version + '/js/**/*.js'];
  var orderOfTheAppFiles = ['_config.js', '_app.js'];

  var processJSVendor = gulp.src(jsVendor)
    .pipe(plugins.order(orderOfTheVendorFiles))
    .pipe(plugins.concat('vendor.min.js'))
    .pipe(plugins.uglify())
    .pipe(gulp.dest(sources.dest + '/js'));

  var processJSApp = gulp.src(jsApp)
    .pipe(plugins.order(orderOfTheAppFiles))
    .pipe(plugins.ngAnnotate())
    .pipe(plugins.concat('app.min.js'))
    .pipe(plugins.uglify())
    .pipe(iife())
    .pipe(gulp.dest(sources.dest + '/js'));

  return merge(processCSS, processJSVendor, processJSApp);
});

// inject minified/uglified file references in /public/index.html at specified locations [uses gulp-inject]
gulp.task('inject:prod', function() {
  var allFiles = gulp.src([sources.dest + '/css/app.min.css', sources.dest + '/js/vendor.min.js', sources.dest + '/js/app.min.js'], { read: false });

  return gulp.src('./public/index.html')
    .pipe(plugins.inject(allFiles, injectOptions))// will insert after <!-- inject:css --> and <!-- inject:js -->
    .pipe(plugins.injectString.after('<!-- inject:css -->', cssCode))//add cssCDN and other files
    .pipe(gulp.dest('./public/'));
});

// remove all dev code and keep only minified/uglified files in production
gulp.task('clean:prod', function() {
  return gulp.src([sources.dest + '/bower_components/', sources.dest + '/css/**/*', '!' + sources.dest + '/css/app.min.css', sources.dest + '/js/**/*', '!' + sources.dest + '/js/app.min.js', '!' + sources.dest + '/js/vendor.min.js'], { read: false })
    .pipe(vinylPaths(del));
});

gulp.task('default', ['build:prod']);

gulp.task('server', function() {
  return gulp.src('./public')
    .pipe(plugins.webserver({
      livereload: true,
      open: true,
      https: false,
      fallback: 'index.html'
    }));
});

function watch() {
  gulp.watch('client/**/*.js', fileChanged('js'));
  gulp.watch('client/**/*.styl', ['copy:css']);
  gulp.watch(['client/**/*.jade', '!client/html/layouts/**/*.jade'], fileChanged('jade'));
  gulp.watch(['client/html/layouts/**/*.jade'], function() {
    return runSequence(['copy:html', 'copy:index'], 'inject:dev');
  });
  gulp.watch('client/fonts/**/*', ['copy:fonts']);
  gulp.watch('client/img/**/*', ['copy:img']);
}

// Helper methods below

function fileChanged(type) {
  return function(event) {
    var loc = event.path;

    console.log('\n' + path.basename(loc) + ' was ' + event.type);

    switch (event.type) {
      case 'added':
      case 'changed':
        return fileAddedOrChanged(type, loc, event.type);
      case 'deleted':
        return fileDeleted(loc);
    }
  };
}

function fileAddedOrChanged(type, loc, eventType) {
  switch (type) {
    case 'js':
      var jsfile = gulp.src(loc, { base: './client/' })
        .pipe(plumber())
        .pipe(plugins.jshint(sources.jshint.client.jshintrc))
        .pipe(plugins.jscs())
        .pipe(plugins.jshint.reporter(stylish))
        .pipe(gulp.dest(sources.dest));

      if (eventType != 'changed') {
        setTimeout(function() {
          runSequence('inject:dev');
        }, 200);
      }

      return jsfile;

    case 'jade':
      if (loc == __dirname + '/client/html/index.jade') {
        var file = gulp.src(loc, { base: './client/html/' })
          .pipe(plugins.jade(jadeOptions))
          .pipe(gulp.dest('./public'));

        setTimeout(function() {
          runSequence('inject:dev');
        }, 200);

        return file;
      }
      else {
        return gulp.src(loc, { base: './client/html/' })
          .pipe(plugins.jade(jadeOptions))
          .pipe(gulp.dest(sources.dest));
      }
  }
}

function fileDeleted(loc) {
  var file = gulp.src(deletePath(loc), { read: false })
    .pipe(vinylPaths(del));

  setTimeout(function() {
    runSequence('inject:dev');
  }, 200);

  return file;
}

function deletePath(loc) {
  var newPath = path.relative(__dirname + '/client/', loc);
  newPath = path.join(sources.dest, newPath);

  var end;

  switch (path.extname(newPath)) {
    case '.jade': end = path.basename(newPath, '.jade') + '.html'; break;
    case '.js': end = path.basename(newPath); break;
  }

  return path.dirname(newPath) + '/' + end;
}

// Strip paths and keep only filenames
function getOrderOfTheFiles(files) {
  var orderOfTheFiles = files;

  orderOfTheFiles.forEach(function(value, key) {
    orderOfTheFiles[key] = path.basename(value);
  });

  return orderOfTheFiles;
}
<|endoftext|>


<commit_before>let executeCopy = () => {
    try {
        return document.queryCommandSupported('copy') &&
               document.queryCommandEnabled('copy') &&
               document.execCommand('copy');
    } catch (e) {
        console.error(e);
        return false;
    }
};

export default text => {
    var textArea = document.createElement("textarea");

    textArea.style.position = 'fixed';
    textArea.style.top = 0;
    textArea.style.left = 0;
    textArea.style.width = '2em';
    textArea.style.height = '2em';
    textArea.style.padding = 0;
    textArea.style.border = 'none';
    textArea.style.outline = 'none';
    textArea.style.boxShadow = 'none';
    textArea.style.background = 'transparent';

    textArea.value = text;
    document.body.appendChild(textArea);
    textArea.select();

    let success = executeCopy();

    document.body.removeChild(textArea);
    return success;
};
<commit_msg>Cleanup<commit_after>let executeCopy = () => {
    try {
        return document.queryCommandSupported('copy') &&
               document.queryCommandEnabled('copy') &&
               document.execCommand('copy');
    } catch (e) {
        console.error(e);
        return false;
    }
};

export default text => {
    var textArea = document.createElement("textarea");

    textArea.style.position = 'fixed';
    textArea.style.top = 0;
    textArea.style.left = 0;
    textArea.style.padding = 0;
    textArea.style.border = 'none';
    textArea.style.outline = 'none';
    textArea.style.boxShadow = 'none';
    textArea.style.background = 'transparent';

    textArea.value = text;
    document.body.appendChild(textArea);
    textArea.select();

    let success = executeCopy();

    document.body.removeChild(textArea);
    return success;
};
<|endoftext|>


<commit_before>util        = require('util');
yaml        = require('js-yaml');
writeYaml   = require('write-yaml');
fs          = require('fs');
fse         = require('fs-extra');
download    = require('download');
npmRun      = require('npm-run');

//var sourceComposeFile   = process.argv[2];  // the full path of the docker-compose.yml file that describes the current rancher stack
var sourceComposeFile   = "docker-compose.yml";
var serviceName         = process.argv[2];  // the name of the service to upgrade
var newServiceImage     = process.argv[3];  // the image of the new service, ex: ezephoenix/webspike:34

filter_keys = function(obj, filter) {
  var key, keys = [];
  for (key in obj) {
    if (obj.hasOwnProperty(key) && key.match(filter)) {
      keys.push(key);
    }
  }
  return keys;
}

try {
  console.log("loading %s", sourceComposeFile);
  var yamlDoc = yaml.safeLoad(fs.readFileSync(sourceComposeFile, "utf8"));
  console.log("searching for service definition: %s", serviceName);

  var expression = util.format("^%s*",serviceName);
  var matches = filter_keys(yamlDoc, expression);

  var currentServiceEntry = null;
  if( matches.length == 0 ){
    throw util.format("could not find any services matching name: %s", serviceName);
  }
  else if( matches.length == 1){
    currentServiceEntry = matches[0];
  }
  else{
    console.log("multiple service entries found that match: '%s': %s ", serviceName, matches );

    var maxVersion = 0;
    matches.forEach(function(entry){
        var entryVersion = entry.split('-').pop();
        if(entryVersion > maxVersion){
            maxVersion = entryVersion;
            currentServiceEntry = entry;
        }
    });
  }
  if(currentServiceEntry == null) {
    throw "could not find a matching service entry, giving up";
  }

  console.log("Using service entry: " + currentServiceEntry);

  //TODO: check the docker registry to see if the image actually exists
  var currentServiceElement = yamlDoc[currentServiceEntry];
  console.log(currentServiceElement);
  //clone the service element
  var newServiceElement = (JSON.parse(JSON.stringify(currentServiceElement)));
  newServiceElement.image = newServiceImage;

  //name the new service:
  var newServiceName = util.format( "%s-%s", serviceName, newServiceImage.split(':').pop() );

  console.log("inserting new YAML element with name: %s", newServiceName );
  yamlDoc[newServiceName] = newServiceElement;

  var targetFile = "./docker-compose.yml";
  console.log("writing modified YAML file out to %s", targetFile);
  writeYaml.sync(targetFile, yamlDoc);
  console.log("successfully wrote modified YAML file out to %s", targetFile);

  var args = util.format("--url %s --access-key %s --secret-key %s -p %s --file %s upgrade %s %s",
    process.env.RANCHER_URL,
    process.env.RANCHER_ACCESS_KEY,
    process.env.RANCHER_SECRET_KEY,
    process.env.RANCHER_STACK,
    targetFile,
    currentServiceEntry,
    newServiceName );

  var source = "https://releases.rancher.com/compose/beta/latest/rancher-compose-linux-amd64.tar.gz"
  new download({extract: true})
    .get(source)
    .dest(".")
    .run(function(){
      console.log("rancher-compose downloaded");
      var source = "./rancher-compose-v0.3.0/rancher-compose ";

      var cmd = "rancher-compose " + args;
      console.log(cmd);

      //windows:
      //npmRun.sync("c:/tools/rancher-compose.exe " + args, {cwd: __dirname});

      //linux
      npmRun.sync(source + args, {cwd: __dirname});
      console.log("DONE");
    });
} catch (e) {
    console.log("Deployment failed:")
    console.error(e);
}<commit_msg>returning error in catch handler<commit_after>util        = require('util');
yaml        = require('js-yaml');
writeYaml   = require('write-yaml');
fs          = require('fs');
fse         = require('fs-extra');
download    = require('download');
npmRun      = require('npm-run');

//var sourceComposeFile   = process.argv[2];  // the full path of the docker-compose.yml file that describes the current rancher stack
var sourceComposeFile   = "docker-compose.yml";
var serviceName         = process.argv[2];  // the name of the service to upgrade
var newServiceImage     = process.argv[3];  // the image of the new service, ex: ezephoenix/webspike:34

filter_keys = function(obj, filter) {
  var key, keys = [];
  for (key in obj) {
    if (obj.hasOwnProperty(key) && key.match(filter)) {
      keys.push(key);
    }
  }
  return keys;
}

try {
  console.log("loading %s", sourceComposeFile);
  var yamlDoc = yaml.safeLoad(fs.readFileSync(sourceComposeFile, "utf8"));
  console.log("searching for service definition: %s", serviceName);

  var expression = util.format("^%s*",serviceName);
  var matches = filter_keys(yamlDoc, expression);

  var currentServiceEntry = null;
  if( matches.length == 0 ){
    throw util.format("could not find any services matching name: %s", serviceName);
  }
  else if( matches.length == 1){
    currentServiceEntry = matches[0];
  }
  else{
    console.log("multiple service entries found that match: '%s': %s ", serviceName, matches );

    var maxVersion = 0;
    matches.forEach(function(entry){
        var entryVersion = entry.split('-').pop();
        if(entryVersion > maxVersion){
            maxVersion = entryVersion;
            currentServiceEntry = entry;
        }
    });
  }
  if(currentServiceEntry == null) {
    throw "could not find a matching service entry, giving up";
  }

  console.log("Using service entry: " + currentServiceEntry);

  //TODO: check the docker registry to see if the image actually exists
  var currentServiceElement = yamlDoc[currentServiceEntry];
  console.log(currentServiceElement);
  //clone the service element
  var newServiceElement = (JSON.parse(JSON.stringify(currentServiceElement)));
  newServiceElement.image = newServiceImage;

  //name the new service:
  var newServiceName = util.format( "%s-%s", serviceName, newServiceImage.split(':').pop() );

  console.log("inserting new YAML element with name: %s", newServiceName );
  yamlDoc[newServiceName] = newServiceElement;

  var targetFile = "./docker-compose.yml";
  console.log("writing modified YAML file out to %s", targetFile);
  writeYaml.sync(targetFile, yamlDoc);
  console.log("successfully wrote modified YAML file out to %s", targetFile);

  var args = util.format("--url %s --access-key %s --secret-key %s -p %s --file %s upgrade %s %s",
    process.env.RANCHER_URL,
    process.env.RANCHER_ACCESS_KEY,
    process.env.RANCHER_SECRET_KEY,
    process.env.RANCHER_STACK,
    targetFile,
    currentServiceEntry,
    newServiceName );

  var source = "https://releases.rancher.com/compose/beta/latest/rancher-compose-linux-amd64.tar.gz"
  new download({extract: true})
    .get(source)
    .dest(".")
    .run(function(){
      console.log("rancher-compose downloaded");
      var source = "./rancher-compose-v0.3.0/rancher-compose ";

      var cmd = "rancher-compose " + args;
      console.log(cmd);

      //windows:
      //npmRun.sync("c:/tools/rancher-compose.exe " + args, {cwd: __dirname});

      //linux
      npmRun.sync(source + args, {cwd: __dirname});
      console.log("DONE");
    });
} catch (e) {
    console.log("Deployment failed:")
    console.error(e);
    return 1;
}<|endoftext|>


<commit_before>import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger'
import { routerMiddleware } from 'react-router-redux';

import rootReducer from 'reducers';

export default function configureStore(initialState = {}, browserHistory) {
  const middlewares = [thunk, routerMiddleware(browserHistory)];

  let composeEnhancers = compose;
  if (window.localStorage.enableReduxLogging) {
    if (logger) middlewares.push(logger());
  }
  if (window.localStorage.enableReduxExtension) {
    if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
  }

  const store = createStore(rootReducer, initialState, composeEnhancers(
    applyMiddleware.apply(this, middlewares)
  ));

  return store;
}
<commit_msg>Fix redux logger<commit_after>import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { routerMiddleware } from 'react-router-redux';

import rootReducer from 'reducers';

export default function configureStore(initialState = {}, browserHistory) {
  const middlewares = [thunk, routerMiddleware(browserHistory)];

  let composeEnhancers = compose;
  if (window.localStorage.enableReduxLogging) {
    if (logger) middlewares.push(logger);
  }
  if (window.localStorage.enableReduxExtension) {
    if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
  }

  const store = createStore(rootReducer, initialState, composeEnhancers(
    applyMiddleware.apply(this, middlewares)
  ));

  return store;
}
<|endoftext|>


<commit_before>// Copyright (c) 2019 August Detlefsen. All rights reserved.
// Use of this source code is governed by an Apache-style license that can be
// found in the LICENSE file.

var urlsToGroup = [];
var alwaysGroup = true;

/**
 * Parses the domain name from the URL of the current tab.
 *
 * @param {function(string)} callback - called when the domain of the current tab
 *   is found.
 */
function getCurrentTabDomain(callback) {
  var queryInfo = {
    active: true,
    currentWindow: true
  };

  chrome.tabs.query(queryInfo, function(tabs) {
    // A window can only have one active tab at a time, so the array consists of
    // exactly one tab.
    var tab = tabs[0];

    // Get the tab URL
    var url = tab.url;

    //get the domain from the URL
    var domain = url.match(/^[\w-]+:\/{2,}\[?([\w\.:-]+)\]?(?::[0-9]*)?/)[1];

    callback(domain);
  });
}

/**
 * Groups all tabs with URLs matching a pattern into the same window
 */
function groupTabs(urlPattern) {
  console.log("groupTabs: " + urlPattern);

  //check if we already have a window for this pattern
  getTabWindow(urlPattern, function(tabWindow){
    console.log("groupTabs: tabWindow: " + JSON.stringify(tabWindow));

    //get the tabs that match the URL pattern
    chrome.tabs.query({url:urlPattern}, function(tabs) {
      if (tabWindow) {
        moveTabs(tabs, tabWindow);

        //focus the window
        chrome.windows.update(tabWindow.id,{focused:true});

      } else {
        //no existing window for this pattern so create a new window
        var tabId = (tabs.length > 0) ? tabs[0].id : null;
        chrome.windows.create({"tabId":tabId}, function(window){

          console.log("window: " + JSON.stringify(window));

          //remove the first element from the tabs array -it has already been added to the window
          //tabs.splice(0,1);
          console.log("tabs: " + JSON.stringify(tabs));

          moveTabs(tabs, window);
          console.log("moveTabs done");

          //focus the window
          chrome.windows.update(window.id,{focused:true});
          console.log("window focused");

          //remember the URL pattern and the new window it was grouped into
          chrome.storage.local.get({urlsToGroup: []}, function(items){
            items.urlsToGroup.push({"urlPattern":urlPattern,"window":window.id});
            console.log("NEW urlsToGroup: " + JSON.stringify(items.urlsToGroup));
            chrome.storage.local.set({"urlsToGroup":items.urlsToGroup});
          })
        });
      }

      console.log("urlsToGroup(2): " + JSON.stringify(urlsToGroup));  });
  });
}

function removeGroup(urlPattern) {
    chrome.storage.local.get({urlsToGroup: []}, function(items) {
      console.log("removeGroup(" + urlPattern + ")");
      var newUrls = items.urlsToGroup.filter(function(el) {
        console.log("el: " + JSON.stringify(el));
        return el.urlPattern != urlPattern;
      });

      chrome.storage.local.set({"urlsToGroup":newUrls});
      console.log("removeGroup(): " + JSON.stringify(newUrls));
    });
}

/**
 * Gets the window that a particular tab should be grouped into:
 *
 * 1. If a window exists for the passed match rule the callback is executed on that window
 * 2. If there is an existing match rule but the window no longer exists a new window will be created
 * 3. Otherwise return null
 */
function getTabWindow(tabUrl, callback) {
  console.log("getTabWindow: " + tabUrl);

  //are we dealing with a new regex?

  var match = false;
  for (var i = 0; i < urlsToGroup.length; i++) {
    var rule = urlsToGroup[i];
    console.log("rule: " + JSON.stringify(rule));
    if (matchRuleShort(tabUrl, rule.urlPattern)) {
      //the new tab URL matches an existing group.
      console.log("MATCH!");
      match = true;
      //check that the window still exists
      chrome.windows.get(rule.window, {populate:true}, function(foundWindow){
        if (foundWindow) {
          console.log("FOUND! " + foundWindow);
          callback(foundWindow);
        } else {
          //create a new window with the new tab
          chrome.windows.create({}, function(newWindow){ //"tabId":tab.id
            console.log("CREATED NEW! " + JSON.stringify(newWindow));

            //reassign the group pattern to the new window
            rule.window = newWindow.id
            chrome.storage.local.set({"urlsToGroup":urlsToGroup});

            callback(newWindow);
          });
        }
      });
    }
  }

  if (!match) {
    callback(null);
  }
}

/**
 * Move an array of tabs to a destination window
 */
function moveTabs(tabs, destination) {
  for (var i = 0; i < tabs.length; i++) {
    var tab = tabs[i];
    if (tab.pinned) {
      continue; //skip pinned tabs
    }
    chrome.tabs.move(tab.id, {windowId:destination.id,index:-1});
  }
}

/**
 * Shorthand function to match a wildcard (*) string
 */
function matchRuleShort(str, rule) {
  return new RegExp("^" + rule.split("*").join(".*") + "$").test(str);
}

/**
 * Add a listener for tab update events
 */
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
  console.log("chrome.tabs.onUpdated: status: " + changeInfo.status + " url: " + changeInfo.url);
  if (alwaysGroup && typeof changeInfo.url != 'undefined') {
    console.log("alwaysGroup");

    chrome.storage.local.get({urlsToGroup: []}, function(items) {

      let rules = items.urlsToGroup.filter(rule => matchRuleShort(changeInfo.url, rule.urlPattern));
      console.log("rules: " + JSON.stringify(rules));
      if (rules.length < 1) return; //no matching rule for this URL, nothing to do

      //TODO: How do we distinguish between multiple match rules on the same domain? find the longest match rule?
      var rule = rules[0];
      //the new tab URL matches an existing group.
      console.log("match!");

      //check that the window still exists
      chrome.windows.get(rule.window, {populate:true}, function(foundWindow){
        console.log("foundWindow: " + JSON.stringify(foundWindow));
        if (foundWindow) {
          //Check for whether the new URL matches an existing tab
          //separate fragment for proper search matching
          var searchUrl  = changeInfo.url.split('#')[0];
          var searchFrag = changeInfo.url.split('#')[1];
          console.log("searchFrag: " + searchFrag);

          //TODO: how do we handle GET query params on the same URL? For example, ?ts=78123768

          console.log("chrome.tabs.query() params: " + searchUrl);
          chrome.tabs.query({"url":searchUrl,"windowId":foundWindow.id},function(tabs){
            console.log("chrome.tabs.query() result: (" + tabs.length + ")" + JSON.stringify(tabs));
            tabs = tabs.filter(t => t.id != tab.id);

            if (tabs.length > 0 && tabs[0].status === "complete") {
              for (var t = 0; t < tabs.length; t++) {
                var foundTab = tabs[t];
                console.log("checking foundTab in tabs: " + JSON.stringify(foundTab));

                var tabFrag = foundTab.url.split('#')[1];
                console.log("tabFrag: " + tabFrag);

                if (searchFrag == tabFrag) {
                  console.log("searchFrag == tabFrag");

                  //focus the existing tab with the same URL
                  //TODO: Make this configurable
                  focusTab(foundWindow.id, foundTab.id); //tabs[0]);

                  //reload to pick up new changes
                  chrome.tabs.reload(foundTab.id); //tabs[0].id);

                  //no need for the new tab
                  chrome.tabs.remove(tab.id);
                }
              }
            } else {
              //open the new tab in the group window
              chrome.tabs.move(tab.id, {windowId:foundWindow.id,index:-1});

              //focus the newly created tab
              focusTab(foundWindow.id, tab.id);
            }
          });

        } else {
          console.log("NOT foundWindow");
          //create a new window with the new tab
          chrome.windows.create({"tabId":tab.id}, function(newWindow){
            console.log("New window created: " + newWindow.id + " rule: " + JSON.stringify(rule));

            //reassign the group pattern to the new window
            rule.window = newWindow.id

            console.log("NEW urlsToGroup: " + JSON.stringify(items.urlsToGroup));
            chrome.storage.local.set({"urlsToGroup":items.urlsToGroup});

            //focus the newly created tab
            focusTab(newWindow.id, tab.id);
          });
        }
      }); // END chrome.windows.get
    });
  }
});

/**
 * Give focus to a particular tab
 */
function focusTab(window, tab) {
  console.log("focusTab("+ JSON.stringify(window) +", " + JSON.stringify(tab) + ")");
  chrome.windows.update(window,{focused:true}, function(window) {
    chrome.tabs.update(tab, {selected:true});
  });
}

/**
 * Add context menus at startup:
 * - Allow selecting partial URLs with wildcard
 * - Checkbox to determine whether to always group new tabs that match
 */
chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({"title": "Specify Tab URLs to Group",
                              "contexts":["all"],
                              "id": "groupTabsContext"});
  chrome.contextMenus.create({"title": "Always Group New Tabs",
                              "contexts":["all"],
                              "id": "groupTabsAlways",
                              "type": "checkbox",
                              "checked": alwaysGroup});
});

/**
 * Click handler for context menu items.
 */
chrome.contextMenus.onClicked.addListener(function(info, tab) {
    if (info.menuItemId === "groupTabsContext") {
      groupTabsContextOnClick(info.pageUrl, tab)
    } else if (info.menuItemId === "groupTabsAlways") {
      groupTabsAlwaysOnClick(info,tab);
    }
});

/**
 * Callback function activated when the context menu item is clicked
 */
function groupTabsContextOnClick(pageUrl, tab) {
  var regex = window.prompt('Enter URL regex to group (Use * for wildcard)', pageUrl);
  if (regex) groupTabs(regex);
}

/**
 * Callback function activated when the context menu item is clicked
 */
function groupTabsAlwaysOnClick(info, tab) {
  alwaysGroup = info.checked;
}
<commit_msg>Don't auto-close pinned tabs<commit_after>// Copyright (c) 2019 August Detlefsen. All rights reserved.
// Use of this source code is governed by an Apache-style license that can be
// found in the LICENSE file.

var urlsToGroup = [];
var alwaysGroup = true;

/**
 * Parses the domain name from the URL of the current tab.
 *
 * @param {function(string)} callback - called when the domain of the current tab
 *   is found.
 */
function getCurrentTabDomain(callback) {
  var queryInfo = {
    active: true,
    currentWindow: true
  };

  chrome.tabs.query(queryInfo, function(tabs) {
    // A window can only have one active tab at a time, so the array consists of
    // exactly one tab.
    var tab = tabs[0];

    // Get the tab URL
    var url = tab.url;

    //get the domain from the URL
    var domain = url.match(/^[\w-]+:\/{2,}\[?([\w\.:-]+)\]?(?::[0-9]*)?/)[1];

    callback(domain);
  });
}

/**
 * Groups all tabs with URLs matching a pattern into the same window
 */
function groupTabs(urlPattern) {
  console.log("groupTabs: " + urlPattern);

  //check if we already have a window for this pattern
  getTabWindow(urlPattern, function(tabWindow){
    console.log("groupTabs: tabWindow: " + JSON.stringify(tabWindow));

    //get the tabs that match the URL pattern
    chrome.tabs.query({url:urlPattern}, function(tabs) {
      if (tabWindow) {
        moveTabs(tabs, tabWindow);

        //focus the window
        chrome.windows.update(tabWindow.id,{focused:true});

      } else {
        //no existing window for this pattern so create a new window
        var tabId = (tabs.length > 0) ? tabs[0].id : null;
        chrome.windows.create({"tabId":tabId}, function(window){

          console.log("window: " + JSON.stringify(window));

          //remove the first element from the tabs array -it has already been added to the window
          //tabs.splice(0,1);
          console.log("tabs: " + JSON.stringify(tabs));

          moveTabs(tabs, window);
          console.log("moveTabs done");

          //focus the window
          chrome.windows.update(window.id,{focused:true});
          console.log("window focused");

          //remember the URL pattern and the new window it was grouped into
          chrome.storage.local.get({urlsToGroup: []}, function(items){
            items.urlsToGroup.push({"urlPattern":urlPattern,"window":window.id});
            console.log("NEW urlsToGroup: " + JSON.stringify(items.urlsToGroup));
            chrome.storage.local.set({"urlsToGroup":items.urlsToGroup});
          })
        });
      }

      console.log("urlsToGroup(2): " + JSON.stringify(urlsToGroup));  });
  });
}

function removeGroup(urlPattern) {
    chrome.storage.local.get({urlsToGroup: []}, function(items) {
      console.log("removeGroup(" + urlPattern + ")");
      var newUrls = items.urlsToGroup.filter(function(el) {
        console.log("el: " + JSON.stringify(el));
        return el.urlPattern != urlPattern;
      });

      chrome.storage.local.set({"urlsToGroup":newUrls});
      console.log("removeGroup(): " + JSON.stringify(newUrls));
    });
}

/**
 * Gets the window that a particular tab should be grouped into:
 *
 * 1. If a window exists for the passed match rule the callback is executed on that window
 * 2. If there is an existing match rule but the window no longer exists a new window will be created
 * 3. Otherwise return null
 */
function getTabWindow(tabUrl, callback) {
  console.log("getTabWindow: " + tabUrl);

  //are we dealing with a new regex?

  var match = false;
  for (var i = 0; i < urlsToGroup.length; i++) {
    var rule = urlsToGroup[i];
    console.log("rule: " + JSON.stringify(rule));
    if (matchRuleShort(tabUrl, rule.urlPattern)) {
      //the new tab URL matches an existing group.
      console.log("MATCH!");
      match = true;
      //check that the window still exists
      chrome.windows.get(rule.window, {populate:true}, function(foundWindow){
        if (foundWindow) {
          console.log("FOUND! " + foundWindow);
          callback(foundWindow);
        } else {
          //create a new window with the new tab
          chrome.windows.create({}, function(newWindow){ //"tabId":tab.id
            console.log("CREATED NEW! " + JSON.stringify(newWindow));

            //reassign the group pattern to the new window
            rule.window = newWindow.id
            chrome.storage.local.set({"urlsToGroup":urlsToGroup});

            callback(newWindow);
          });
        }
      });
    }
  }

  if (!match) {
    callback(null);
  }
}

/**
 * Move an array of tabs to a destination window
 */
function moveTabs(tabs, destination) {
  for (var i = 0; i < tabs.length; i++) {
    var tab = tabs[i];
    if (tab.pinned) {
      continue; //skip pinned tabs
    }
    chrome.tabs.move(tab.id, {windowId:destination.id,index:-1});
  }
}

/**
 * Shorthand function to match a wildcard (*) string
 */
function matchRuleShort(str, rule) {
  return new RegExp("^" + rule.split("*").join(".*") + "$").test(str);
}

/**
 * Add a listener for tab update events
 */
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
  console.log("chrome.tabs.onUpdated: status: " + changeInfo.status + " url: " + changeInfo.url);
  if (alwaysGroup && typeof changeInfo.url != 'undefined') {
    console.log("alwaysGroup");

    chrome.storage.local.get({urlsToGroup: []}, function(items) {

      let rules = items.urlsToGroup.filter(rule => matchRuleShort(changeInfo.url, rule.urlPattern));
      console.log("rules: " + JSON.stringify(rules));
      if (rules.length < 1) return; //no matching rule for this URL, nothing to do

      //TODO: How do we distinguish between multiple match rules on the same domain? find the longest match rule?
      var rule = rules[0];
      //the new tab URL matches an existing group.
      console.log("match!");

      //check that the window still exists
      chrome.windows.get(rule.window, {populate:true}, function(foundWindow){
        console.log("foundWindow: " + JSON.stringify(foundWindow));
        if (foundWindow) {
          //Check for whether the new URL matches an existing tab
          //separate fragment for proper search matching
          var searchUrl  = changeInfo.url.split('#')[0];
          var searchFrag = changeInfo.url.split('#')[1];
          console.log("searchFrag: " + searchFrag);

          //TODO: how do we handle GET query params on the same URL? For example, ?ts=78123768

          console.log("chrome.tabs.query() params: " + searchUrl);
          chrome.tabs.query({"url":searchUrl,"windowId":foundWindow.id},function(tabs){
            console.log("chrome.tabs.query() result: (" + tabs.length + ")" + JSON.stringify(tabs));
            tabs = tabs.filter(t => t.id != tab.id);

            if (tabs.length > 0 && tabs[0].status === "complete") {
              for (var t = 0; t < tabs.length; t++) {
                var foundTab = tabs[t];
                console.log("checking foundTab in tabs: " + JSON.stringify(foundTab));

                var tabFrag = foundTab.url.split('#')[1];
                console.log("tabFrag: " + tabFrag);

                if (searchFrag == tabFrag) {
                  console.log("searchFrag == tabFrag");

                  //focus the existing tab with the same URL
                  //TODO: Make this configurable
                  focusTab(foundWindow.id, foundTab.id); //tabs[0]);

                  //reload to pick up new changes
                  chrome.tabs.reload(foundTab.id); //tabs[0].id);

                  //no need for the new tab. Close it if it is not pinned
                  if (!tab.pinned) {
                    chrome.tabs.remove(tab.id);
                  }
                }
              }
            } else {
              //open the new tab in the group window
              chrome.tabs.move(tab.id, {windowId:foundWindow.id,index:-1});

              //focus the newly created tab
              focusTab(foundWindow.id, tab.id);
            }
          });

        } else {
          console.log("NOT foundWindow");
          //create a new window with the new tab
          chrome.windows.create({"tabId":tab.id}, function(newWindow){
            console.log("New window created: " + newWindow.id + " rule: " + JSON.stringify(rule));

            //reassign the group pattern to the new window
            rule.window = newWindow.id

            console.log("NEW urlsToGroup: " + JSON.stringify(items.urlsToGroup));
            chrome.storage.local.set({"urlsToGroup":items.urlsToGroup});

            //focus the newly created tab
            focusTab(newWindow.id, tab.id);
          });
        }
      }); // END chrome.windows.get
    });
  }
});

/**
 * Give focus to a particular tab
 */
function focusTab(window, tab) {
  console.log("focusTab("+ JSON.stringify(window) +", " + JSON.stringify(tab) + ")");
  chrome.windows.update(window,{focused:true}, function(window) {
    chrome.tabs.update(tab, {selected:true});
  });
}

/**
 * Add context menus at startup:
 * - Allow selecting partial URLs with wildcard
 * - Checkbox to determine whether to always group new tabs that match
 */
chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({"title": "Specify Tab URLs to Group",
                              "contexts":["all"],
                              "id": "groupTabsContext"});
  chrome.contextMenus.create({"title": "Always Group New Tabs",
                              "contexts":["all"],
                              "id": "groupTabsAlways",
                              "type": "checkbox",
                              "checked": alwaysGroup});
});

/**
 * Click handler for context menu items.
 */
chrome.contextMenus.onClicked.addListener(function(info, tab) {
    if (info.menuItemId === "groupTabsContext") {
      groupTabsContextOnClick(info.pageUrl, tab)
    } else if (info.menuItemId === "groupTabsAlways") {
      groupTabsAlwaysOnClick(info,tab);
    }
});

/**
 * Callback function activated when the context menu item is clicked
 */
function groupTabsContextOnClick(pageUrl, tab) {
  var regex = window.prompt('Enter URL regex to group (Use * for wildcard)', pageUrl);
  if (regex) groupTabs(regex);
}

/**
 * Callback function activated when the context menu item is clicked
 */
function groupTabsAlwaysOnClick(info, tab) {
  alwaysGroup = info.checked;
}
<|endoftext|>


<commit_before>// http://stackoverflow.com/a/17606289
String.prototype.replaceAll = function (search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);
};

!function ($, VirtualKeyboard, params, xcontext, navigator, URI) {
    var m = {};
    m.ready = false;
    m.userLangs = navigator.languages;
    m.userLang = m.userLangs[0];
    m.autoCompleteDelay = 200;
    m.indexes = {};
    m.indexNames = [];
    m.href = new String;
	m.specialChars = [];

    /* get the relevant indexes for the dictionary and store them in an array */
    m.getIndexes = $.getJSON(params.switchURL+"?version=1.2&operation=explain&x-context=" +
            xcontext + "&x-format=json");

    function gotIndexesDone(data) {
        m.indexes = data.indexes;
        $.each(m.indexes, function (unused, value) {
            $("style").append('.ui-widget-content li.ui-menu-item a.ui-corner-all.' + value.name + '{ color: #F00; }');
            if (value.name === "lemma" || value.name === "inflected" || value.name.substring(0, 6) === "sense-") {
                m.indexNames.push(value.name);
                m.indexesString = +value.name + ",";
            }
            m.ready = true;
        });
    }

    m.getIndexes.done(gotIndexesDone);

	m.getSpecialChars = $.get(params.switchURL+'?version=1.2&operation=searchRetrieve&x-context='+xcontext+'&query=rfpid=1');

	function gotSpecialCharsDone(data)  {
		$(data).find('value').each(function(){
			m.specialChars.push($(this).text());
		});
        VirtualKeyboard.keys[xcontext] =  m.specialChars;
        VirtualKeyboard.attachKeyboards();
	}

	m.getSpecialChars.done(gotSpecialCharsDone);

    $('document').ready(function () {
        resultContainerLoaded();

        $('.navbar-collapse ul li a').click(function () {
            $('.navbar-toggle:visible').click();
        });

        $(".nav").on("click", "li", function () {
            var index = $(this).index() + 2;
            $("#main > div").hide();
            $("#main > div:nth-child(" + index + ")").show();
        });

        $(".jumbotron").click(function () {
            $("#main > div").hide();
            $("#front").show();
        });

        $("body").on("submit", "form", onSubmitForm);
        $("#front .search-caller").on("click", onSearchCall);
    });

    function getFilteredSuggestions(unused, callWhenDone) {
        if (!m.ready) {
            return;
        }
        // so now we have to do several requests and then present the result
        // when all of them are done.
        // Solution is similiar to this:
        // http://stackoverflow.com/a/9865124
        // and this
        // http://stackoverflow.com/a/16287125
        var requestsForAllIndexes = []; // array of promises that will be fullfilled
        // one after the other.
        $.each(m.indexNames, function (unused, value) {
            var filterText = $("#query-text-ui").val();
            var url = params.switchURL + "?version=1.2&operation=scan&scanClause=" +
                    value + "&x-filter=" + filterText + "&x-context=" + xcontext +
                    "&x-format=json&maximumTerms=10";
            requestsForAllIndexes.push($.getJSON(url));
        });
        // when used like this collects all the promises and then delevers all of
        // them to the done function
        $.when.apply($, requestsForAllIndexes).then(function () {
            var results = [];
            $.each(arguments, function (index, responseData) {
                for (var i = 0; i < responseData[2].responseJSON.terms.length; i++) {
                    results.push(responseData[2].responseJSON.terms[i]);
                }
            });
            gotFilteredSuggestions(results, callWhenDone);
        }, function(jqXHR, textStatus, errorThrown) {
            console.log(errorThrown);
            callWhenDone();
        });
    }

    function gotFilteredSuggestions(results, callWhenDone) {
        var sortedResults = [];
        var firstTenOfsortedResults = [];

        sortedResults = results.sort(function (a, b) {
            if (a.value > b.value) {
                return 1;
            }
            if (a.value < b.value) {
                return -1;
            }
            return 0;
        });

        for (var j = 0; j < 9; j++) {
            if (sortedResults[j] !== undefined) {
                firstTenOfsortedResults.push(sortedResults[j]);
            }
        }

        callWhenDone($.map(firstTenOfsortedResults, function (item) {
            return {
                label: item.value,
                value: item.key,
                href: item.nextHref
            };
        }));
    }

    function onSubmitForm(event) {
        event.preventDefault();
        $("#submit-query").hide();
        $("#loader").show();
        var searchTerm = $("#query-text-ui").val();
        if ((searchTerm[0] !== '"') &&
                (searchTerm.indexOf(' ') !== -1)) {
            searchTerm = '"' + searchTerm + '"';
        }
        console.log(params.switchURL);
        console.log(searchTerm);
        if (m.href === '') {
            m.href = params.switchURL + "?version=1.2&operation=searchRetrieve&query=" +
                    encodeURIComponent(searchTerm) + "&x-context=" + xcontext +
                    "&x-userlangs=" + m.userLangs.join() + "&startRecord=1&maximumRecords=50&x-format=htmlpagetable";
        }

        var encodedHref = m.href.replaceAll('"', '%22').replaceAll(' ', '%20');
        $("#searchcontainer").load(encodedHref + " #result-container", resultContainerLoaded);
    }

    function resultContainerLoaded() {
        /* turn input field into autocomplete field, and fill with data from server for all relevant indexes */

        $("#loader").hide();
        $("#submit-query").show();
        $("#query-text-ui").autocomplete({
            source: getFilteredSuggestions,
            minLength: 1,
            delay: m.autoCompleteDelay,
            select: function (event, ui) {
                if (ui.item) {

                    $('#query-text-ui').val(ui.item.value);
                    // quick fix; bad!
                    m.href = ui.item.href;

                }
                $('#searchretrieve').submit();
            }
        }).data("autocomplete")._renderItem = function (ul, item) {
            var listItem = $("

")
                    .data("item.autocomplete", item)
                    .append("" + item.label + "")
                    .appendTo(ul);
            var index = item.href.slice(item.href.search("query=") + 6, item.href.search("%3"));

            listItem.addClass(index);

            return listItem;
        };

        VirtualKeyboard.attachKeyboards();
        m.href = '';
    }

    function onSearchCall(event) {
        event.preventDefault();
        var searchConfig = (new URI(event.currentTarget.href)).query(true);
        if (searchConfig.query === 'metaText==Dictionaries') {
            $('#li-about a').click();
            return;
        }
        $('#query-text-ui').val(searchConfig.query.replace('serverChoice=', ''));
        $('#li-search a').click();
        $('form#searchretrieve').submit();
        console.log(searchConfig);
    }
}(jQuery, VirtualKeyboard, params, xcontext, navigator, URI)<commit_msg>Use server side autocomp index instead of doing multiple client side requests.<commit_after>// http://stackoverflow.com/a/17606289
String.prototype.replaceAll = function (search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);
};

!function ($, VirtualKeyboard, params, xcontext, navigator, URI) {
    var m = {};
    m.ready = false;
    m.userLangs = navigator.languages;
    m.userLang = m.userLangs[0];
    m.autoCompleteDelay = 200;
    m.indexes = {};
    m.indexNames = [];
    m.href = new String;
	m.specialChars = [];

    /* get the relevant indexes for the dictionary and store them in an array */
    m.getIndexes = $.getJSON(params.switchURL+"?version=1.2&operation=explain&x-context=" +
            xcontext + "&x-format=json");

    function gotIndexesDone(data) {
        m.indexes = data.indexes;
        $.each(m.indexes, function (unused, value) {
            $("style").append('.ui-widget-content li.ui-menu-item a.ui-corner-all.' + value.name + '{ color: #F00; }');
            if (value.name === "autocomp") {
                m.ready = true;
            }
        });
    }

    m.getIndexes.done(gotIndexesDone);

	m.getSpecialChars = $.get(params.switchURL+'?version=1.2&operation=searchRetrieve&x-context='+xcontext+'&query=rfpid=1');

	function gotSpecialCharsDone(data)  {
		$(data).find('value').each(function(){
			m.specialChars.push($(this).text());
		});
        VirtualKeyboard.keys[xcontext] =  m.specialChars;
        VirtualKeyboard.attachKeyboards();
	}

	m.getSpecialChars.done(gotSpecialCharsDone);

    $('document').ready(function () {
        resultContainerLoaded();

        $('.navbar-collapse ul li a').click(function () {
            $('.navbar-toggle:visible').click();
        });

        $(".nav").on("click", "li", function () {
            var index = $(this).index() + 2;
            $("#main > div").hide();
            $("#main > div:nth-child(" + index + ")").show();
        });

        $(".jumbotron").click(function () {
            $("#main > div").hide();
            $("#front").show();
        });

        $("body").on("submit", "form", onSubmitForm);
        $("#front .search-caller").on("click", onSearchCall);
    });

    function getFilteredSuggestions(unused, callWhenDone) {
        if (!m.ready) {
        	callWhenDone();
            return;
        }

        var filterText = $("#query-text-ui").val();
        var url = params.switchURL + "?version=1.2&operation=scan&scanClause=autocomp" +
                  "&x-filter=" + filterText + "&x-context=" + xcontext +
                  "&x-format=json&maximumTerms=10";
        var requestsForAllIndexes = $.getJSON(url);
        requestsForAllIndexes.then(function (responseJSON) {
            var results = [];
            for (var i = 0; i < responseJSON.terms.length; i++) {
                results.push(responseJSON.terms[i]);
            }
            gotFilteredSuggestions(results, callWhenDone);
        }, function(jqXHR, textStatus, errorThrown) {
            console.log(errorThrown);
            callWhenDone();
        });
    }

    function gotFilteredSuggestions(results, callWhenDone) {
        var sortedResults = [];
        var firstTenOfsortedResults = [];

        sortedResults = results.sort(function (a, b) {
            if (a.label > b.label) {
                return 1;
            }
            if (a.label < b.label) {
                return -1;
            }
            return 0;
        });

        for (var j = 0; j < 9; j++) {
            if (sortedResults[j] !== undefined) {
                firstTenOfsortedResults.push(sortedResults[j]);
            }
        }

        callWhenDone($.map(firstTenOfsortedResults, function (item) {
            return {
                label: item.label,
                value: item.key,
                href: item.nextHref
            };
        }));
    }

    function onSubmitForm(event) {
        event.preventDefault();
        $("#submit-query").hide();
        $("#loader").show();
        var searchTerm = $("#query-text-ui").val();
        if ((searchTerm[0] !== '"') &&
                (searchTerm.indexOf(' ') !== -1)) {
            searchTerm = '"' + searchTerm + '"';
        }
        console.log(params.switchURL);
        console.log(searchTerm);
        if (m.href === '') {
            m.href = params.switchURL + "?version=1.2&operation=searchRetrieve&query=" +
                    encodeURIComponent(searchTerm) + "&x-context=" + xcontext +
                    "&x-userlangs=" + m.userLangs.join() + "&startRecord=1&maximumRecords=50&x-format=htmlpagetable";
        }

        var encodedHref = m.href.replaceAll('"', '%22').replaceAll(' ', '%20');
        $("#searchcontainer").load(encodedHref + " #result-container", resultContainerLoaded);
    }

    function resultContainerLoaded() {
        /* turn input field into autocomplete field, and fill with data from server for all relevant indexes */

        $("#loader").hide();
        $("#submit-query").show();
        $("#query-text-ui").autocomplete({
            source: getFilteredSuggestions,
            minLength: 1,
            delay: m.autoCompleteDelay,
            select: function (event, ui) {
                if (ui.item) {

                    $('#query-text-ui').val(ui.item.value);
                    // quick fix; bad!
                    m.href = ui.item.href;

                }
                $('#searchretrieve').submit();
            }
        }).data("autocomplete")._renderItem = function (ul, item) {
            var listItem = $("

")
                    .data("item.autocomplete", item)
                    .append("" + item.label + "")
                    .appendTo(ul);
            var index = item.href.slice(item.href.search("query=") + 6, item.href.search("%3"));

            listItem.addClass(index);

            return listItem;
        };

        VirtualKeyboard.attachKeyboards();
        m.href = '';
    }

    function onSearchCall(event) {
        event.preventDefault();
        var searchConfig = (new URI(event.currentTarget.href)).query(true);
        if (searchConfig.query === 'metaText==Dictionaries') {
            $('#li-about a').click();
            return;
        }
        $('#query-text-ui').val(searchConfig.query.replace('serverChoice=', ''));
        $('#li-search a').click();
        $('form#searchretrieve').submit();
        console.log(searchConfig);
    }
}(jQuery, VirtualKeyboard, params, xcontext, navigator, URI)<|endoftext|>


<commit_before>// ==UserScript==
// @name         YT ratio
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Stores visited video likes / dislikes ratios and creates browsable rankings.
// @author       Flagoul
// @match        https://www.youtube.com/*
// @grant        GM_addStyle

// ==/UserScript==

(function() {
    'use strict';

    const LS_KEY = "us-yt-ratio";
    const RANKING_LOWEST_VIEW_COUNT = 100; // the smallest view count possible for a video to be the first ranking
    const RANKING_STEP = 10; // the step (as a factor) between each ranking
    const RANKING_MAX_VIDEOS = 50; // the step (as a factor) between each ranking

    function getCoords(e) {
        let box = e.getBoundingClientRect();

        let body = document.body;
        let de = document.documentElement;

        let scrollTop = window.pageYOffset || de.scrollTop || body.scrollTop;
        let scrollLeft = window.pageXOffset || de.scrollLeft || body.scrollLeft;

        let clientTop = de.clientTop || body.clientTop || 0;
        let clientLeft = de.clientLeft || body.clientLeft || 0;

        return {
            "top": Math.round(box.top +  scrollTop - clientTop),
            "left": Math.round(box.left + scrollLeft - clientLeft)
        };
    }

    /**
    * Retrieves values related to current video and stores them in an object, that contains:
    * - id: the video id
    * - title: the video title
    * - view count: how many views the video has
    * - likes: the number of likes on the video
    * - dislikes: the number of dislikes on the video
    * - ratio: the raio of likes / dislikes of the video
    *
    * @return {Object} An object as described above. null if the video contains no likes / dislikes.
    */
    function retrieveValues() {
        let values = document.querySelectorAll(".like-button-renderer > span:nth-of-type(odd) button span");

        // no likes / dislikes
        if (values.length === 0) {
            return null;
        }

        let id = document.location.href.match(".*?v=([^&]*)")[1];
        let title = document.querySelector("#eow-title").title;

        let viewCount = parseInt(document.querySelector(".watch-view-count").textContent.split(/[\s,.]+/).slice(0, -1).join(""));

        let likes = parseInt(values[0].textContent.replace(/[\s,.]+/g, ""));
        let dislikes = parseInt(values[1].textContent.replace(/[\s,.]+/g, ""));

        return {
            "id": id,
            "title": title,
            "viewCount": viewCount,
            "likes": likes,
            "dislikes": dislikes,
            "ratio": likes/dislikes
        };
    }

    /**
    * Displays the ratio of likes / dislikes of the video. Adds an icon that allows displaying rankings on click.
    *
    * @param {Number} ratio - The ratio of likes / dislikes.
    * @param {Object} rankings - The rankings computed (@see {@link computeRankings}).
    */
    function displayRatio(ratio, rankings) {
        GM_addStyle(`
            .ratio-icon:before {
                margin-right: 4px !important;
                opacity: 1 !important;
                margin-bottom: 3px !important;
            }
        `);

        let ratioValue = document.createElement("span");
        ratioValue.classList.add("yt-uix-button-content");
        ratioValue.appendChild(document.createTextNode(ratio.toFixed(2)));

        let button = document.createElement("button");
        button.classList.add("yt-uix-button", "yt-uix-button-opacity", "yt-ui-menu-item", "has-icon", "action-panel-trigger-stats", "ratio-icon");
        button.style.width = "inherit";
        button.onclick = function (e) {
            displayRankings(rankings, button);
        };
        button.appendChild(ratioValue);

        let ratioSpan = document.createElement("span");
        ratioSpan.appendChild(button);
        ratioSpan.setAttribute("title", "Ratio likes / dislikes");

        document.querySelector(".like-button-renderer").appendChild(ratioSpan);
    }

    /**
    * Saves the values of the current video in the local storage and computes the ranking of the videos.
    *
    * Fetches the records in the local storage, adds the current video to it, computes rankings and then saves the records. During the computing, if a ranking has too many
    * videos, removes videos that are the bottom of this ranking. The videos removed will not be saved in the local storage (it includes current video if it would be too low
    * in the ranking).
    *
    * @param {Object} values - The values retrieved from the current video.
    * @param {Number} maxPerRanking - The maximum number of videos per ranking.
    * @return {Object} The computed rankings.
    */
    function saveValuesAndComputeRankings(values, maxPerRanking) {
        let records = JSON.parse(localStorage.getItem(LS_KEY));
        if (records === null) {
            records = {};
        }

        delete records[values.id];
        records[values.id] = values;

        let rankings = computeRankings(records, RANKING_LOWEST_VIEW_COUNT, RANKING_STEP);

        for (let i = 0; i < rankings.length; ++i) {

            // if the ranking is too big, truncate it and delete all low-ranked videos records
            let ranking = rankings[i];
            if (ranking.values.length > maxPerRanking) {
                let toDelete = ranking.values.slice(maxPerRanking);
                for (let j = 0; j < toDelete.length; ++j) {
                    delete records[toDelete[j].id];
                }
                rankings[i].values = ranking.values.slice(0, maxPerRanking);
            }
        }

        localStorage.setItem(LS_KEY, JSON.stringify(records));

        return rankings;
    }

    function computeRankings(records, from, step) {
        if (from <= 0) {
            throw "Smallest view count in rankings can't be <= 0.";
        }

        if (step <= 0) {
            throw "Step between rankings can't be <= 0.";
        }

        let rankings = [];

        for (let key in records) {
            if (records.hasOwnProperty(key)) {
                let record = records[key];

                if (record.viewCount >= from) {

                    let rankingFound = false;
                    let lower = from;
                    let upper = from;

                    for (let rankingNumber = 0; !rankingFound; ++rankingNumber) {
                        lower = upper;
                        upper = lower * step;

                        if (rankingNumber >= rankings.length) {
                            rankings.push({"lower": lower, "upper": upper, "values": []});
                        }

                        if (record.viewCount >= lower && record.viewCount < upper) {
                            rankings[rankingNumber].values.push(record);
                            rankingFound = true;
                        }
                    }
                }
            }
        }

        for (let i = 0; i < rankings.length; ++i) {
            rankings[i].values.sort(function (a, b) {
                return b.ratio - a.ratio;
            });
        }

        return rankings;
    }

    function displayRankings(rankings, button) {
        let availableRect = document.querySelector("#watch-headline-title").getBoundingClientRect();
        let width = availableRect.right - availableRect.left;
        let height = 500;

        let coords = getCoords(button);
        let rect = button.getBoundingClientRect();
        let buttonWidth = rect.right - rect.left;

        let rankingsContainer = document.createElement("div");
        rankingsContainer.classList.add("yt-uix-menu-content", "yt-ui-menu-content", "yt-uix-kbd-nav");
        rankingsContainer.role = "menu";
        rankingsContainer.style.width = width + "px";
        rankingsContainer.style.height = height + "px";
        rankingsContainer.style.overflow = "scroll";

        rankingsContainer.style.left = (coords.left + buttonWidth - width) + "px";
        rankingsContainer.style.top = (coords.top + 30) + "px";
        rankingsContainer.ariaExpanded = "true";
        rankingsContainer.dataKbddNavMoveOut="action-panel-overflow-button";

        for (let i = 0; i < rankings.length; ++i) {
          buildList(rankingsContainer, rankings[i]);
        }

        document.addEventListener("click", function(e) {
            if (!rankingsContainer.contains(e.target) && !button.contains(e.target)) {
                rankingsContainer.remove();
            }
        });

        document.querySelector("body").append(rankingsContainer);
    }

    function buildList(container, ranking) {
        let title = document.createElement("h2");
        title.style.padding = "0 15px";
        title.appendChild(document.createTextNode(ranking.lower.toLocaleString() + " - " + ranking.upper.toLocaleString() + " views"));
        container.append(title);

        let ul = document.createElement("ul");
        ul.classList.add("yt-uix-kbd-nav", "yt-uix-kbd-nav-list");

        for (let i = 0; i < ranking.values.length; ++i) {
            let values = ranking.values[i];

            let li = document.createElement("li");
            let a = document.createElement("a");
            a.href = "https://www.youtube.com/watch?v=" + values.id;
            a.classList.add("yt-ui-menu-item");

            let place = document.createElement("span");
            place.style.width = "20px";
            place.style.marginRight = "10px";
            place.style.display = "inline-block";
            place.style.textAlign = "right";
            place.appendChild(document.createTextNode(i + 1));
            a.append(place);

            let ratio = document.createElement("span");
            ratio.style.width = "40px";
            ratio.style.marginRight = "10px";
            ratio.style.display = "inline-block";
            ratio.style.textAlign = "right";
            ratio.appendChild(document.createTextNode(values.ratio.toFixed(2)));
            a.append(ratio);

            let label = document.createElement("span");
            label.appendChild(document.createTextNode(values.title));
            label.classList.add("yt-ui-menu-item-label");
            a.append(label);

            li.append(a);
            ul.append(li);
        }
        container.append(ul);
    }

    function doVideoRelatedActions() {
        let values = retrieveValues();

        // the video is worth registering (has likes and dislikes)
        if (values !== null) {
            let rankings = saveValuesAndComputeRankings(values, RANKING_MAX_VIDEOS);
            displayRatio(values.ratio, rankings);
        }
    }

    doVideoRelatedActions();

    window.addEventListener("spfdone", function(e) {
        doVideoRelatedActions();
    });
})();<commit_msg>Better integration of rankings (border, paddings)<commit_after>// ==UserScript==
// @name         YT ratio
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Stores visited video likes / dislikes ratios and creates browsable rankings.
// @author       Flagoul
// @match        https://www.youtube.com/*
// @grant        GM_addStyle

// ==/UserScript==

(function() {
    'use strict';

    const LS_KEY = "us-yt-ratio";
    const RANKING_LOWEST_VIEW_COUNT = 100; // the smallest view count possible for a video to be the first ranking
    const RANKING_STEP = 10; // the step (as a factor) between each ranking
    const RANKING_MAX_VIDEOS = 50; // the step (as a factor) between each ranking

    function getCoords(e) {
        let box = e.getBoundingClientRect();

        let body = document.body;
        let de = document.documentElement;

        let scrollTop = window.pageYOffset || de.scrollTop || body.scrollTop;
        let scrollLeft = window.pageXOffset || de.scrollLeft || body.scrollLeft;

        let clientTop = de.clientTop || body.clientTop || 0;
        let clientLeft = de.clientLeft || body.clientLeft || 0;

        return {
            "top": Math.round(box.top +  scrollTop - clientTop),
            "left": Math.round(box.left + scrollLeft - clientLeft)
        };
    }

    /**
    * Retrieves values related to current video and stores them in an object, that contains:
    * - id: the video id
    * - title: the video title
    * - view count: how many views the video has
    * - likes: the number of likes on the video
    * - dislikes: the number of dislikes on the video
    * - ratio: the raio of likes / dislikes of the video
    *
    * @return {Object} An object as described above. null if the video contains no likes / dislikes.
    */
    function retrieveValues() {
        let values = document.querySelectorAll(".like-button-renderer > span:nth-of-type(odd) button span");

        // no likes / dislikes
        if (values.length === 0) {
            return null;
        }

        let id = document.location.href.match(".*?v=([^&]*)")[1];
        let title = document.querySelector("#eow-title").title;

        let viewCount = parseInt(document.querySelector(".watch-view-count").textContent.split(/[\s,.]+/).slice(0, -1).join(""));

        let likes = parseInt(values[0].textContent.replace(/[\s,.]+/g, ""));
        let dislikes = parseInt(values[1].textContent.replace(/[\s,.]+/g, ""));

        return {
            "id": id,
            "title": title,
            "viewCount": viewCount,
            "likes": likes,
            "dislikes": dislikes,
            "ratio": likes/dislikes
        };
    }

    /**
    * Displays the ratio of likes / dislikes of the video. Adds an icon that allows displaying rankings on click.
    *
    * @param {Number} ratio - The ratio of likes / dislikes.
    * @param {Object} rankings - The rankings computed (@see {@link computeRankings}).
    */
    function displayRatio(ratio, rankings) {
        GM_addStyle(`
            .ratio-icon:before {
                margin-right: 4px !important;
                opacity: 1 !important;
                margin-bottom: 3px !important;
            }
        `);

        let ratioValue = document.createElement("span");
        ratioValue.classList.add("yt-uix-button-content");
        ratioValue.appendChild(document.createTextNode(ratio.toFixed(2)));

        let button = document.createElement("button");
        button.classList.add("yt-uix-button", "yt-uix-button-opacity", "yt-ui-menu-item", "has-icon", "action-panel-trigger-stats", "ratio-icon");
        button.style.width = "inherit";
        button.onclick = function (e) {
            displayRankings(rankings, button);
        };
        button.appendChild(ratioValue);

        let ratioSpan = document.createElement("span");
        ratioSpan.appendChild(button);
        ratioSpan.setAttribute("title", "Ratio likes / dislikes");

        document.querySelector(".like-button-renderer").appendChild(ratioSpan);
    }

    /**
    * Saves the values of the current video in the local storage and computes the ranking of the videos.
    *
    * Fetches the records in the local storage, adds the current video to it, computes rankings and then saves the records. During the computing, if a ranking has too many
    * videos, removes videos that are the bottom of this ranking. The videos removed will not be saved in the local storage (it includes current video if it would be too low
    * in the ranking).
    *
    * @param {Object} values - The values retrieved from the current video.
    * @param {Number} maxPerRanking - The maximum number of videos per ranking.
    * @return {Object} The computed rankings.
    */
    function saveValuesAndComputeRankings(values, maxPerRanking) {
        let records = JSON.parse(localStorage.getItem(LS_KEY));
        if (records === null) {
            records = {};
        }

        delete records[values.id];
        records[values.id] = values;

        let rankings = computeRankings(records, RANKING_LOWEST_VIEW_COUNT, RANKING_STEP);

        for (let i = 0; i < rankings.length; ++i) {

            // if the ranking is too big, truncate it and delete all low-ranked videos records
            let ranking = rankings[i];
            if (ranking.values.length > maxPerRanking) {
                let toDelete = ranking.values.slice(maxPerRanking);
                for (let j = 0; j < toDelete.length; ++j) {
                    delete records[toDelete[j].id];
                }
                rankings[i].values = ranking.values.slice(0, maxPerRanking);
            }
        }

        localStorage.setItem(LS_KEY, JSON.stringify(records));

        return rankings;
    }

    function computeRankings(records, from, step) {
        if (from <= 0) {
            throw "Smallest view count in rankings can't be <= 0.";
        }

        if (step <= 0) {
            throw "Step between rankings can't be <= 0.";
        }

        let rankings = [];

        for (let key in records) {
            if (records.hasOwnProperty(key)) {
                let record = records[key];

                if (record.viewCount >= from) {

                    let rankingFound = false;
                    let lower = from;
                    let upper = from;

                    for (let rankingNumber = 0; !rankingFound; ++rankingNumber) {
                        lower = upper;
                        upper = lower * step;

                        if (rankingNumber >= rankings.length) {
                            rankings.push({"lower": lower, "upper": upper, "values": []});
                        }

                        if (record.viewCount >= lower && record.viewCount < upper) {
                            rankings[rankingNumber].values.push(record);
                            rankingFound = true;
                        }
                    }
                }
            }
        }

        for (let i = 0; i < rankings.length; ++i) {
            rankings[i].values.sort(function (a, b) {
                return b.ratio - a.ratio;
            });
        }

        return rankings;
    }

    function displayRankings(rankings, button) {
        let availableRect = document.querySelector("#watch-headline-title").getBoundingClientRect();
        let width = availableRect.right - availableRect.left;
        let height = 500;

        let coords = getCoords(button);
        let rect = button.getBoundingClientRect();
        let buttonWidth = rect.right - rect.left;

        let rankingsContainer = document.createElement("div");
        rankingsContainer.classList.add("yt-uix-menu-content", "yt-ui-menu-content", "yt-uix-kbd-nav");
        rankingsContainer.role = "menu";
        rankingsContainer.style.width = width + "px";
        rankingsContainer.style.height = height + "px";
        rankingsContainer.style.overflow = "scroll";

        rankingsContainer.style.left = (coords.left + buttonWidth - width) + "px";
        rankingsContainer.style.top = (coords.top + 30) + "px";
        rankingsContainer.ariaExpanded = "true";
        rankingsContainer.dataKbddNavMoveOut="action-panel-overflow-button";

        for (let i = 0; i < rankings.length; ++i) {
          buildList(rankingsContainer, rankings[i]);
        }

        document.addEventListener("click", function(e) {
            if (!rankingsContainer.contains(e.target) && !button.contains(e.target)) {
                rankingsContainer.remove();
            }
        });

        document.querySelector("body").append(rankingsContainer);
    }

    function buildList(container, ranking) {
        let title = document.createElement("h2");
        title.style.borderTop = "1px solid #e2e2e2";
        title.style.margin = "7px 15px 0px";
        title.style.padding = "7px 15px 5px";
        title.appendChild(document.createTextNode(ranking.lower.toLocaleString() + " - " + ranking.upper.toLocaleString() + " views"));
        container.append(title);

        let ul = document.createElement("ul");
        ul.classList.add("yt-uix-kbd-nav", "yt-uix-kbd-nav-list");

        for (let i = 0; i < ranking.values.length; ++i) {
            let values = ranking.values[i];

            let li = document.createElement("li");
            let a = document.createElement("a");
            a.href = "https://www.youtube.com/watch?v=" + values.id;
            a.classList.add("yt-ui-menu-item");

            let place = document.createElement("span");
            place.style.width = "20px";
            place.style.marginRight = "10px";
            place.style.display = "inline-block";
            place.style.textAlign = "right";
            place.appendChild(document.createTextNode(i + 1));
            a.append(place);

            let ratio = document.createElement("span");
            ratio.style.width = "40px";
            ratio.style.marginRight = "10px";
            ratio.style.display = "inline-block";
            ratio.style.textAlign = "right";
            ratio.appendChild(document.createTextNode(values.ratio.toFixed(2)));
            a.append(ratio);

            let label = document.createElement("span");
            label.appendChild(document.createTextNode(values.title));
            label.classList.add("yt-ui-menu-item-label");
            a.append(label);

            li.append(a);
            ul.append(li);
        }
        container.append(ul);
    }

    function doVideoRelatedActions() {
        let values = retrieveValues();

        // the video is worth registering (has likes and dislikes)
        if (values !== null) {
            let rankings = saveValuesAndComputeRankings(values, RANKING_MAX_VIDEOS);
            displayRatio(values.ratio, rankings);
        }
    }

    doVideoRelatedActions();

    window.addEventListener("spfdone", function(e) {
        doVideoRelatedActions();
    });
})();<|endoftext|>


<commit_before>/**
* Map-reduces a collection with a 2d geospatial index on a `location` field,
* summing and counting the points within squares having a length of,
* `grid_size` in degrees of latitude and longitude.
*/
var runGridReduce = function(collection, grid_size, location_field, value_field, query) {
	var map = function() {
		if (!this[location_field]) return;
		if (grid_size < 1) {
			r = function(v) {
				var mag = 100;
				return Math.round(v / grid_size * mag) / mag;
			};
		} else {
			var r = Math.floor;
		}
		var grid_lat = r(this[location_field][0] / grid_size);
		var grid_lng = r(this[location_field][1] / grid_size);
		var key = grid_lat + ',' + grid_lng;
		var e = {
			count: 1,
		};
		e[location_field] = [grid_lat * grid_size, grid_lng * grid_size];
		e[value_field] = this[value_field];
		emit(key, e);
	};
	var reduce = function(key, values) {
		print(key+' -- '+values.length);
		var sum = 0;
		var count = 0;
		values.forEach(function(doc) {
			count += doc.count;
			sum += doc[value_field];
		});
		var r = {
			count: count
		};
		r[location_field] = [values[0][location_field][0], values[0][location_field][1]];
		r[value_field] = sum;
		return r;
	};
	var reduced_collection = collection + '_grid_' + grid_size;
	db[reduced_collection].drop();
	if (!location_field) {
		location_field = 'location';
	}
	if (!value_field) {
		value_field = 'value';
	}
	var index = {};
	index[location_field] = '2d';
	db[reduced_collection].ensureIndex(index);
	return db.runCommand({
		mapreduce: collection
		,query: query
		,map: map
		,reduce: reduce
		,out: {reduce: reduced_collection}
		,scope: {
			grid_size: grid_size,
			location_field: location_field,
			value_field: value_field
		}
		,verbose: true
		,keeptemp: true
	});
};

// Run on safecast for grid size 1 and without conditions
use geo;
var op = runGridReduce('safecast', .5, 'location', 'reading_value');
var op = runGridReduce('safecast', 1, 'location', 'reading_value');
var op = runGridReduce('safecast', 2, 'location', 'reading_value');
var op = runGridReduce('safecast', 3, 'location', 'reading_value');

// Test
var grp = db[op.result].group({
    initial: {count: 0, total_value: 0},
	reduce: function(doc, out){ var v = doc.value; out.count += v.count; print(out.count); out.total_value+=v.reading_value },
    finalize: function(out){ out.avg_value = out.total_value / out.count }
}, function(err, datasets) {
	print(datasets);
});
op;
grp;

<commit_msg>Removed debug print<commit_after>/**
* Map-reduces a collection with a 2d geospatial index on a `location` field,
* summing and counting the points within squares having a length of,
* `grid_size` in degrees of latitude and longitude.
*/
var runGridReduce = function(collection, grid_size, location_field, value_field, query) {
	var map = function() {
		if (!this[location_field]) return;
		if (grid_size < 1) {
			r = function(v) {
				var mag = 100;
				return Math.round(v / grid_size * mag) / mag;
			};
		} else {
			var r = Math.floor;
		}
		var grid_lat = r(this[location_field][0] / grid_size);
		var grid_lng = r(this[location_field][1] / grid_size);
		var key = grid_lat + ',' + grid_lng;
		var e = {
			count: 1,
		};
		e[location_field] = [grid_lat * grid_size, grid_lng * grid_size];
		e[value_field] = this[value_field];
		emit(key, e);
	};
	var reduce = function(key, values) {
		var sum = 0;
		var count = 0;
		values.forEach(function(doc) {
			count += doc.count;
			sum += doc[value_field];
		});
		var r = {
			count: count
		};
		r[location_field] = [values[0][location_field][0], values[0][location_field][1]];
		r[value_field] = sum;
		return r;
	};
	var reduced_collection = collection + '_grid_' + grid_size;
	db[reduced_collection].drop();
	if (!location_field) {
		location_field = 'location';
	}
	if (!value_field) {
		value_field = 'value';
	}
	var index = {};
	index[location_field] = '2d';
	db[reduced_collection].ensureIndex(index);
	return db.runCommand({
		mapreduce: collection
		,query: query
		,map: map
		,reduce: reduce
		,out: {reduce: reduced_collection}
		,scope: {
			grid_size: grid_size,
			location_field: location_field,
			value_field: value_field
		}
		,verbose: true
		,keeptemp: true
	});
};

// Run on safecast for grid size 1 and without conditions
use geo;
var op = runGridReduce('safecast', .9, 'location', 'reading_value');
var op = runGridReduce('safecast', 1, 'location', 'reading_value');
var op = runGridReduce('safecast', 2, 'location', 'reading_value');
var op = runGridReduce('safecast', 3, 'location', 'reading_value');

// Test
var grp = db[op.result].group({
    initial: {count: 0, total_value: 0},
	reduce: function(doc, out){ var v = doc.value; out.count += v.count; print(out.count); out.total_value+=v.reading_value },
    finalize: function(out){ out.avg_value = out.total_value / out.count }
}, function(err, datasets) {
	print(datasets);
});
op;
grp;

<|endoftext|>