<commit_before>#!/usr/bin/env node

'use strict';

const fs            = require('fs');
const jsonfile      = require('jsonfile');
const gulp          = require('gulp');
const logger        = require('../modules/logger')(gulp);
const $             = require('gulp-load-plugins')();
const runSequence   = require('run-sequence');
const pipeErrorStop = require('pipe-error-stop');
const del           = require('del');
const hbsLayouts    = require('handlebars-layouts');
const notifier      = require('node-notifier');
const svgMix        = require('../modules/gulp-svg-mix');
const browserSync   = require('browser-sync').create();

/**
 * конфиг по умолчанию
 */
let config = require('../modules/config');

config.app.browserSync.server.baseDir = config.base.dist;

/**
 * данные для шаблонов
 */
let db = {
    pageTitle : 'Marmelad project'
};

/**
 * обработчик ошибок для plumber
 */
let onError = function(err) {

    $.util.log(err.message);

    notifier.notify({
        'title': 'Error',
        'message': err.message
    });

    this.emit('end');
};

/**
 * регистрация хелперов расширения шаблонов handlebars-layouts
 * https://www.npmjs.com/package/handlebars-layouts
 */
$.compileHandlebars.Handlebars.registerHelper(hbsLayouts($.compileHandlebars.Handlebars));
/**
 * хелпер сравнения, аля if
 */
$.compileHandlebars.Handlebars.registerHelper('compare', (lvalue, rvalue, options) => {

    if (arguments.length < 3)
        throw new Error("Handlerbars Helper 'compare' needs 2 parameters");

    let operator = options.hash.operator || "==";

    let operators = {
        '=='     : function(l,r) { return l == r; },
        '==='    : function(l,r) { return l === r; },
        '!='     : function(l,r) { return l != r; },
        '<'      : function(l,r) { return l < r; },
        '>'      : function(l,r) { return l > r; },
        '<='     : function(l,r) { return l <= r; },
        '>='     : function(l,r) { return l >= r; },
        'typeof' : function(l,r) { return typeof l == r; }
    }

    if (!operators[operator])
        throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);

    let result = operators[operator](lvalue,rvalue);

    if (result) {
        return options.fn(this);
    } else {
        return options.inverse(this);
    }

});

/**
 * Получение списка директорий Блоков для их подключения в шаблоны
 *
 * @author Yunus Gaziev <yunus.gaziev@gmail.com>
 * @param dest {String} путь до папки в которой лежат блоки
 * @returns {Array} массив путей до шаблонов handlebars
 */
let getPartialsPaths = (dest) => {

    let folders = fs.readdirSync(dest); // массив папок-блоков
    let partials = []; // массив путей до шаблонов handlebars

    folders.forEach(function (el) {
        partials.push(dest + '/' + el);
    });

    return partials;
}

/**
 * сборка шаблонов handlebars
 * https://www.npmjs.com/package/gulp-compile-handlebars
 */
gulp.task('handlebars', function(done) {

    let stream = gulp.src([
            config.paths.pages + '/**/*.{hbs,handlebars}',
            '!' + config.paths.pages + '/**/_*.{hbs,handlebars}'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(
            $.compileHandlebars(db, {
                ignorePartials: false,
                batch: getPartialsPaths(config.paths.blocks)
            })
        )
        .on('error', $.notify.onError({title: 'Handlebars'}))
        .pipe(pipeErrorStop()) // на случай если handlebars сломается, иначе таск останавливается
        .pipe($.beml(config.app.beml))
        .pipe(svgMix({path: config.paths.svg + '/sprite.svg'}))
        .pipe($.prettify({indent_size: 4}))
        .pipe($.rename({extname: '.html'}))
        .pipe(gulp.dest(config.paths.dist));

    stream.on('end', function() {
        browserSync.reload();
        done();
    });

    stream.on('error', function(err) {
        done(err);
    });
});

/**
 * генерация svg спрайта
 */
gulp.task('svg-sprite', () => {

    return gulp.src(config.paths.svg + '/icons/*.svg')
        .pipe(
            $.svgSprite({
                mode: {
                    symbol: { // symbol mode to build the SVG
                        dest: config.paths.svg, // destination folder
                        sprite: 'sprite.svg', //sprite name
                        example: true // Build sample page
                    }
                },
                svg: {
                    xmlDeclaration: false, // strip out the XML attribute
                    doctypeDeclaration: false // don't include the !DOCTYPE declaration
                }
            })
        )
        .pipe(gulp.dest('.'));
});

/**
 * обновляет svg спрайт
 * пересборка шаблонов
 */
gulp.task('refresh-svg', (cb) => {
    runSequence('svg-sprite', 'handlebars', cb);
});

/**
 * обновление данных для шаблонов
 */
gulp.task('get-data', (done) => {

    jsonfile.readFile(config.paths.assets + '/data.json', (error, data) => {

        if (error) {
            data = {
                pageTitle : 'Error in data.json'
            };
        }

        db = data;

        done();
    });
});

/**
 * обновление данных для шаблонов
 * пересборка шаблонов с новыми данными
 */
gulp.task('refresh-data', (cb) => {
    runSequence('get-data', 'handlebars', cb);
});

/**
 * СТИЛИ ПАГИНОВ
 */
gulp.task('styles:plugins', () => {
    return gulp.src([
            config.paths.styles + '/libs/_variables.styl',
            config.paths.plugins + '/**/*.css',
            config.paths.plugins + '/**/*.styl'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(
            $.stylus({
                'include css': true
            })
        )
        .pipe($.concat('plugins.min.css'))
        .pipe($.autoprefixer())
        .pipe($.csso())
        .pipe(gulp.dest(config.paths.storage + config.base.styles))
        .pipe(browserSync.stream());
});

/**
 * СТИЛИ ОСНОВНЫЕ
 */
gulp.task('styles:main', () => {
    return gulp.src([
            config.paths.styles + '/libs/_variables.styl',
            config.paths.styles + '/libs/_mixins.styl',
            config.paths.styles + '/libs/_normalize.styl',
            config.paths.styles + '/*.styl',
            config.paths.blocks + '/**/*.styl'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('main.styl'))
        .pipe(
            $.stylus({
                'include css': true
            })
        )
        .pipe($.autoprefixer(config.app.autoprefixer))
        .pipe(gulp.dest(config.paths.storage + config.base.styles))
        .pipe(browserSync.stream());
});

/**
 * СКРИПТЫ ВЕНДОРНЫЕ
 */
gulp.task('scripts:vendor', () => {

    return gulp.src([config.paths.scripts.vendor + '/**/*.js'])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.storage + config.base.scripts + '/vendor'));
});

/**
 * СКРИПТЫ ПЛАГИНОВ
 */
gulp.task('scripts:plugins', () => {
    return gulp.src([config.paths.plugins + '/**/*.js'])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('plugins.min.js'))
        .pipe($.uglify())
        .pipe(gulp.dest(config.paths.storage + config.base.scripts));
});

/**
 * СКРИПТЫ ОСНОВНЫЕ
 */
gulp.task('scripts:main', (done) => {

    let stream = gulp.src([
            config.paths.scripts.main + '/*.js',
            config.paths.blocks + '/**/*.js',
            '!' + config.paths.scripts + '/**/_*.*'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('main.js'))
        .pipe($.wrap('$(document).ready(function(){\n\n"use strict";\n\n<%= contents %>\n});'))
        .pipe(
            $.jsPrettify({
                indent_size: 4,
                indent_char: " ",
                eol: "\n",
                collapseWhitespace: true
            })
        )
        .pipe(gulp.dest(config.paths.storage + config.base.scripts));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем картинки
 */
gulp.task('images', (done) => {

    let stream = gulp.src(config.paths.images.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.images.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем шрифты
 */
gulp.task('font', (done) => {

    let stream = gulp.src(config.paths.font.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.font.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем прочие файлы
 */
gulp.task('files', (done) => {

    let stream = gulp.src(config.paths.files.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.files.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * очищаем папку сборки перед сборкой Ж)
 */
gulp.task('clean', () => {
    del('dist');
});

gulp.task('watch', () => {

    /* СТИЛИ */
    $.watch(config.paths.plugins + '/**/*.css', $.batch((events, done) => {
        gulp.start('styles:plugins', done);
    }));
    $.watch(config.paths.styles + '/**/*.{styl,css}', $.batch((events, done) => {
        gulp.start('styles:main', done);
    }));
    $.watch(config.paths.blocks + '/**/*.{styl,css}', $.batch((events, done) => {
        gulp.start('styles:main', done);
    }));

    /* СКРИПТЫ */
    $.watch(config.paths.scripts.vendor + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:vendor', done);
    }));
    $.watch(config.paths.plugins + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:plugins', done);
    }));
    $.watch(config.paths.scripts.main + '/*.js', $.batch((events, done) => {
        gulp.start('scripts:main', done);
    }));
    $.watch(config.paths.blocks + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:main', done);
    }));

    /* ШАБЛОНЫ */
    $.watch(config.paths.pages + '/**/*.{hbs,handlebars}', $.batch((events, done) => {
        gulp.start('handlebars', done);
    }));
    $.watch(config.paths.blocks + '/**/*.{hbs,handlebars}', $.batch((events, done) => {
        gulp.start('handlebars', done);
    }));

    /* SVG-ИКОНКИ */
    $.watch(config.paths.svg + '/icons/*.svg', $.batch((events, done) => {
        gulp.start('refresh-svg', done);
    }));

    /* ДАННЫЕ ДЛЯ ШАБЛОНОВ */
    $.watch(config.paths.assets + '/data.json', $.batch((events, done) => {
        gulp.start('refresh-data', done);
    }));

    /* КАРТИНКИ ДЛЯ САЙТА*/
    $.watch(config.paths.images.src + '/**', $.batch((events, done) => {
        gulp.start('images', done);
    }));

    /* ШРИФТЫ ДЛЯ САЙТА*/
    $.watch(config.paths.font.src + '/**', $.batch((events, done) => {
        gulp.start('font', done);
    }));

    /* ФАЙЛЫ ДЛЯ САЙТА*/
    $.watch(config.paths.files.src + '/**', $.batch((events, done) => {
        gulp.start('files', done);
    }));

});

gulp.task('server', () => {

    browserSync.init(config.app.browserSync);

});

gulp.task('startup', (cb) => {
    runSequence(
        'clean',
        'server',
        'get-data',
        'svg-sprite',
        'handlebars',
        'styles:plugins',
        'styles:main',
        'scripts:vendor',
        'scripts:plugins',
        'scripts:main',
        'images',
        'font',
        'files',
        'watch',
        cb);
});

/**
 * создание файла конфигурации проекта
 */
gulp.task('writeConfiguration', (done) => {

    jsonfile.writeFile('marmelad.json', config, {spaces: 4}, (err) => {
        if (err) {
            $.util.log(err);
            done();
        } else {
            $.util.log('Configuration create success');
            done();
        }
    });
});

/**
 * копирование заготовки проекта
 */
gulp.task('copyAssets', () => {
    return gulp.src(__dirname.replace('bin', '') + 'assets/**/*.*')
        .pipe(gulp.dest(process.cwd() + '/assets'));
});

/**
 * копирование .editorconfig
 */
gulp.task('copyEditorconfig', () => {
    return gulp.src(__dirname.replace('bin', '') + '.editorconfig')
        .pipe(gulp.dest(process.cwd()));
});

/**
 * инициализация нового проекта
 */
gulp.task('initialize', (done) => {

    runSequence(
        'writeConfiguration',
        'copyAssets',
        'copyEditorconfig',
        'startup',
        done
    );
});

let init = () => {

    fs.exists('marmelad.json', (exist) => {

        if (exist) {
            config = require(process.cwd() + '/marmelad.json');
            gulp.start('startup');
        } else {
            gulp.start('initialize');
        }

    });
}

init();
<commit_msg>Расширение для данных шаблонов, конфигом<commit_after>#!/usr/bin/env node

'use strict';

const fs            = require('fs');
const jsonfile      = require('jsonfile');
const gulp          = require('gulp');
const logger        = require('../modules/logger')(gulp);
const $             = require('gulp-load-plugins')();
const runSequence   = require('run-sequence');
const pipeErrorStop = require('pipe-error-stop');
const del           = require('del');
const hbsLayouts    = require('handlebars-layouts');
const notifier      = require('node-notifier');
const svgMix        = require('../modules/gulp-svg-mix');
const browserSync   = require('browser-sync').create();
const pkg           = require('../package.json');

/**
 * конфиг по умолчанию
 */
let config = require('../modules/config');

config.app.browserSync.server.baseDir = config.base.dist;
config.version = pkg.version;
config.name    = pkg.name;
config.description = pkg.description;

/**
 * данные для шаблонов
 */
let db = {
    pageTitle : 'Marmelad project'
};

/**
 * обработчик ошибок для plumber
 */
let onError = function(err) {

    $.util.log(err.message);

    notifier.notify({
        'title': 'Error',
        'message': err.message
    });

    this.emit('end');
};

/**
 * регистрация хелперов расширения шаблонов handlebars-layouts
 * https://www.npmjs.com/package/handlebars-layouts
 */
$.compileHandlebars.Handlebars.registerHelper(hbsLayouts($.compileHandlebars.Handlebars));
/**
 * хелпер сравнения, аля if
 */
$.compileHandlebars.Handlebars.registerHelper('compare', (lvalue, rvalue, options) => {

    if (arguments.length < 3)
        throw new Error("Handlerbars Helper 'compare' needs 2 parameters");

    let operator = options.hash.operator || "==";

    let operators = {
        '=='     : function(l,r) { return l == r; },
        '==='    : function(l,r) { return l === r; },
        '!='     : function(l,r) { return l != r; },
        '<'      : function(l,r) { return l < r; },
        '>'      : function(l,r) { return l > r; },
        '<='     : function(l,r) { return l <= r; },
        '>='     : function(l,r) { return l >= r; },
        'typeof' : function(l,r) { return typeof l == r; }
    }

    if (!operators[operator])
        throw new Error("Handlerbars Helper 'compare' doesn't know the operator "+operator);

    let result = operators[operator](lvalue,rvalue);

    if (result) {
        return options.fn(this);
    } else {
        return options.inverse(this);
    }

});

/**
 * Получение списка директорий Блоков для их подключения в шаблоны
 *
 * @author Yunus Gaziev <yunus.gaziev@gmail.com>
 * @param dest {String} путь до папки в которой лежат блоки
 * @returns {Array} массив путей до шаблонов handlebars
 */
let getPartialsPaths = (dest) => {

    let folders = fs.readdirSync(dest); // массив папок-блоков
    let partials = []; // массив путей до шаблонов handlebars

    folders.forEach(function (el) {
        partials.push(dest + '/' + el);
    });

    return partials;
}

/**
 * сборка шаблонов handlebars
 * https://www.npmjs.com/package/gulp-compile-handlebars
 */
gulp.task('handlebars', function(done) {

    let stream = gulp.src([
            config.paths.pages + '/**/*.{hbs,handlebars}',
            '!' + config.paths.pages + '/**/_*.{hbs,handlebars}'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(
            $.compileHandlebars({ app : config.app, db}, {
                ignorePartials: false,
                batch: getPartialsPaths(config.paths.blocks)
            })
        )
        .on('error', $.notify.onError({title: 'Handlebars'}))
        .pipe(pipeErrorStop()) // на случай если handlebars сломается, иначе таск останавливается
        .pipe($.beml(config.app.beml))
        .pipe(svgMix({path: config.paths.svg + '/sprite.svg'}))
        .pipe($.prettify({indent_size: 4}))
        .pipe($.rename({extname: '.html'}))
        .pipe(gulp.dest(config.paths.dist));

    stream.on('end', function() {
        browserSync.reload();
        done();
    });

    stream.on('error', function(err) {
        done(err);
    });
});

/**
 * генерация svg спрайта
 */
gulp.task('svg-sprite', () => {

    return gulp.src(config.paths.svg + '/icons/*.svg')
        .pipe(
            $.svgSprite({
                mode: {
                    symbol: { // symbol mode to build the SVG
                        dest: config.paths.svg, // destination folder
                        sprite: 'sprite.svg', //sprite name
                        example: true // Build sample page
                    }
                },
                svg: {
                    xmlDeclaration: false, // strip out the XML attribute
                    doctypeDeclaration: false // don't include the !DOCTYPE declaration
                }
            })
        )
        .pipe(gulp.dest('.'));
});

/**
 * обновляет svg спрайт
 * пересборка шаблонов
 */
gulp.task('refresh-svg', (cb) => {
    runSequence('svg-sprite', 'handlebars', cb);
});

/**
 * обновление данных для шаблонов
 */
gulp.task('get-data', (done) => {

    jsonfile.readFile(config.paths.assets + '/data.json', (error, data) => {

        if (error) {
            data = {
                pageTitle : 'Error in data.json'
            };
        }

        db = data;

        done();
    });
});

/**
 * обновление данных для шаблонов
 * пересборка шаблонов с новыми данными
 */
gulp.task('refresh-data', (cb) => {
    runSequence('get-data', 'handlebars', cb);
});

/**
 * СТИЛИ ПАГИНОВ
 */
gulp.task('styles:plugins', () => {
    return gulp.src([
            config.paths.styles + '/libs/_variables.styl',
            config.paths.plugins + '/**/*.css',
            config.paths.plugins + '/**/*.styl'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(
            $.stylus({
                'include css': true
            })
        )
        .pipe($.concat('plugins.min.css'))
        .pipe($.autoprefixer())
        .pipe($.csso())
        .pipe(gulp.dest(config.paths.storage + config.base.styles))
        .pipe(browserSync.stream());
});

/**
 * СТИЛИ ОСНОВНЫЕ
 */
gulp.task('styles:main', () => {
    return gulp.src([
            config.paths.styles + '/libs/_variables.styl',
            config.paths.styles + '/libs/_mixins.styl',
            config.paths.styles + '/libs/_normalize.styl',
            config.paths.styles + '/*.styl',
            config.paths.blocks + '/**/*.styl'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('main.styl'))
        .pipe(
            $.stylus({
                'include css': true
            })
        )
        .pipe($.autoprefixer(config.app.autoprefixer))
        .pipe(gulp.dest(config.paths.storage + config.base.styles))
        .pipe(browserSync.stream());
});

/**
 * СКРИПТЫ ВЕНДОРНЫЕ
 */
gulp.task('scripts:vendor', () => {

    return gulp.src([config.paths.scripts.vendor + '/**/*.js'])
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.storage + config.base.scripts + '/vendor'));
});

/**
 * СКРИПТЫ ПЛАГИНОВ
 */
gulp.task('scripts:plugins', () => {
    return gulp.src([config.paths.plugins + '/**/*.js'])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('plugins.min.js'))
        .pipe($.uglify())
        .pipe(gulp.dest(config.paths.storage + config.base.scripts));
});

/**
 * СКРИПТЫ ОСНОВНЫЕ
 */
gulp.task('scripts:main', (done) => {

    let stream = gulp.src([
            config.paths.scripts.main + '/*.js',
            config.paths.blocks + '/**/*.js',
            '!' + config.paths.scripts + '/**/_*.*'
        ])
        .pipe($.plumber({errorHandler: onError}))
        .pipe($.concat('main.js'))
        .pipe($.wrap('$(document).ready(function(){\n\n"use strict";\n\n<%= contents %>\n});'))
        .pipe(
            $.jsPrettify({
                indent_size: 4,
                indent_char: " ",
                eol: "\n",
                collapseWhitespace: true
            })
        )
        .pipe(gulp.dest(config.paths.storage + config.base.scripts));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем картинки
 */
gulp.task('images', (done) => {

    let stream = gulp.src(config.paths.images.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.images.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем шрифты
 */
gulp.task('font', (done) => {

    let stream = gulp.src(config.paths.font.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.font.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * перекладываем прочие файлы
 */
gulp.task('files', (done) => {

    let stream = gulp.src(config.paths.files.src + '/**/*')
        .pipe($.plumber({errorHandler: onError}))
        .pipe(gulp.dest(config.paths.files.dest));

    stream.on('end', () => {
        browserSync.reload();
        done();
    });

    stream.on('error', (err) => {
        done(err);
    });
});

/**
 * очищаем папку сборки перед сборкой Ж)
 */
gulp.task('clean', () => {
    del('dist');
});

gulp.task('watch', () => {

    /* СТИЛИ */
    $.watch(config.paths.plugins + '/**/*.css', $.batch((events, done) => {
        gulp.start('styles:plugins', done);
    }));
    $.watch(config.paths.styles + '/**/*.{styl,css}', $.batch((events, done) => {
        gulp.start('styles:main', done);
    }));
    $.watch(config.paths.blocks + '/**/*.{styl,css}', $.batch((events, done) => {
        gulp.start('styles:main', done);
    }));

    /* СКРИПТЫ */
    $.watch(config.paths.scripts.vendor + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:vendor', done);
    }));
    $.watch(config.paths.plugins + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:plugins', done);
    }));
    $.watch(config.paths.scripts.main + '/*.js', $.batch((events, done) => {
        gulp.start('scripts:main', done);
    }));
    $.watch(config.paths.blocks + '/**/*.js', $.batch((events, done) => {
        gulp.start('scripts:main', done);
    }));

    /* ШАБЛОНЫ */
    $.watch(config.paths.pages + '/**/*.{hbs,handlebars}', $.batch((events, done) => {
        gulp.start('handlebars', done);
    }));
    $.watch(config.paths.blocks + '/**/*.{hbs,handlebars}', $.batch((events, done) => {
        gulp.start('handlebars', done);
    }));

    /* SVG-ИКОНКИ */
    $.watch(config.paths.svg + '/icons/*.svg', $.batch((events, done) => {
        gulp.start('refresh-svg', done);
    }));

    /* ДАННЫЕ ДЛЯ ШАБЛОНОВ */
    $.watch(config.paths.assets + '/data.json', $.batch((events, done) => {
        gulp.start('refresh-data', done);
    }));

    /* КАРТИНКИ ДЛЯ САЙТА*/
    $.watch(config.paths.images.src + '/**', $.batch((events, done) => {
        gulp.start('images', done);
    }));

    /* ШРИФТЫ ДЛЯ САЙТА*/
    $.watch(config.paths.font.src + '/**', $.batch((events, done) => {
        gulp.start('font', done);
    }));

    /* ФАЙЛЫ ДЛЯ САЙТА*/
    $.watch(config.paths.files.src + '/**', $.batch((events, done) => {
        gulp.start('files', done);
    }));

});

gulp.task('server', () => {

    browserSync.init(config.app.browserSync);

});

gulp.task('startup', (cb) => {
    runSequence(
        'clean',
        'server',
        'get-data',
        'svg-sprite',
        'handlebars',
        'styles:plugins',
        'styles:main',
        'scripts:vendor',
        'scripts:plugins',
        'scripts:main',
        'images',
        'font',
        'files',
        'watch',
        cb);
});

/**
 * создание файла конфигурации проекта
 */
gulp.task('writeConfiguration', (done) => {

    jsonfile.writeFile('marmelad.json', config, {spaces: 4}, (err) => {
        if (err) {
            $.util.log(err);
            done();
        } else {
            $.util.log('Configuration create success');
            done();
        }
    });
});

/**
 * копирование заготовки проекта
 */
gulp.task('copyAssets', () => {
    return gulp.src(__dirname.replace('bin', '') + 'assets/**/*.*')
        .pipe(gulp.dest(process.cwd() + '/assets'));
});

/**
 * копирование .editorconfig
 */
gulp.task('copyEditorconfig', () => {
    return gulp.src(__dirname.replace('bin', '') + '.editorconfig')
        .pipe(gulp.dest(process.cwd()));
});

/**
 * инициализация нового проекта
 */
gulp.task('initialize', (done) => {

    runSequence(
        'writeConfiguration',
        'copyAssets',
        'copyEditorconfig',
        'startup',
        done
    );
});

let init = () => {

    fs.exists('marmelad.json', (exist) => {

        if (exist) {
            config = require(process.cwd() + '/marmelad.json');
            config.app.version     = pkg.version;
            config.app.name        = pkg.name;
            config.app.description = pkg.description;
            gulp.start('startup');
        } else {
            gulp.start('initialize');
        }

    });
}

init();
<|endoftext|>


<commit_before>//MIT License

//Copyright (c) 2017 Arc676/Alessandro Vinciguerra

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

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

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

const fs = require('fs')

const MTG = require('mtgsdk')
const Discord = require('discord.js')

const client = new Discord.Client()

var name = "titania"

client.on('ready', () => {
	console.log(`Logged in as ${client.user.username}`)
})

const properties = [
	[
		"name",
		"manaCost"
	],
	[
		"rarity",
		"types"
	],
	[
		"text"
	],
	[
		"power",
		"toughness"
	]
]
function printCard(message, card) {
	var text = ""
	for (var i = 0; i < properties.length; i++) {
		for (var j = 0; j < properties[i].length; j++) {
			if (properties[i][j] == undefined) {
				break
			}
			text += card[properties[i][j]]
			if (j + 1 < properties[i].length) {
				text += "/"
			}
		}
		text += "\n"
	}
	message.reply(text)
}

function getQueryProperties(query) {
	var properties = {}
	for (var i = 0; i < query.length; i++) {
		const prop = query[i].split("=")
		properties[prop[0]] = prop[1]
	}
	return properties
}

client.on('message', message => {
	if (message.content === name + " die") {
		client.destroy()
		process.exit(0)
	}
	const startIndex = message.content.indexOf("[[")
	const endIndex = message.content.indexOf("]]")
	if (startIndex !== -1 && endIndex !== -1) {
		const query = message.content.substring(startIndex + 2, endIndex).split(" ")
		if (query.length === 0) {
			return
		}
		if (query[0] === "set") {
			//const properties = getQueryProperties(query.splice(1))
			message.reply("Set searching coming soon!")
		} else {
			const properties = getQueryProperties(query)
			if (properties === undefined) {
				message.reply("Sorry, your query failed")
				return
			}
			MTG.card.where(properties).then(cards => {
				message.reply("Found " + cards.length + " cards (including duplicates)")
				alreadyPrinted = []
				for (var i = 0; i < cards.length; i++) {
					if (!alreadyPrinted.includes(cards[i].name)) {
						printCard(message, cards[i])
						alreadyPrinted.push(cards[i].name)
					}
				}
			})
		}
	}

})

client.login(fs.readFileSync('tokens/' + name + '.token', 'utf8'))
<commit_msg>Fixed Titania bugs<commit_after>//MIT License

//Copyright (c) 2017 Arc676/Alessandro Vinciguerra

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

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

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

const fs = require('fs')

const MTG = require('mtgsdk')
const Discord = require('discord.js')

const client = new Discord.Client()

var name = "titania"

client.on('ready', () => {
	console.log(`Logged in as ${client.user.username}`)
})

const properties = [
	[
		"name",
		"manaCost"
	],
	[
		"rarity",
		"types"
	],
	[
		"text"
	],
	[
		"power",
		"toughness"
	]
]
function printCard(message, card) {
	var text = ""
	for (var i = 0; i < properties.length; i++) {
		for (var j = 0; j < properties[i].length; j++) {
			if (properties[i][j] in card) {
				text += card[properties[i][j]]
				if (j + 1 < properties[i].length) {
					text += "/"
				}
			}
		}
		text += "\n"
	}
	message.reply(text)
}

function getQueryProperties(query) {
	var properties = {}
	for (var i = 0; i < query.length; i++) {
		const prop = query[i].split("=")
		properties[prop[0]] = prop[1].replace(/-/g, ' ')
	}
	return properties
}

client.on('message', message => {
	if (message.content === name + " die") {
		client.destroy()
		process.exit(0)
	}
	const startIndex = message.content.indexOf("[[")
	const endIndex = message.content.indexOf("]]")
	if (startIndex !== -1 && endIndex !== -1) {
		const query = message.content.substring(startIndex + 2, endIndex).split(" ")
		if (query.length === 0) {
			return
		}
		if (query[0] === "set") {
			//const properties = getQueryProperties(query.splice(1))
			message.reply("Set searching coming soon!")
		} else {
			const properties = getQueryProperties(query)
			if (properties === undefined) {
				message.reply("Sorry, your query failed")
				return
			}
			MTG.card.where(properties).then(cards => {
				message.reply("Found " + cards.length + " cards (including duplicates)")
				alreadyPrinted = []
				for (var i = 0; i < cards.length; i++) {
					if (!alreadyPrinted.includes(cards[i].name)) {
						printCard(message, cards[i])
						alreadyPrinted.push(cards[i].name)
					}
				}
				message.reply("End of search results. Yielded " + alreadyPrinted.length + " distinct cards")
			})
		}
	}

})

client.login(fs.readFileSync('tokens/' + name + '.token', 'utf8'))
<|endoftext|>


<commit_before>//{{{ ZOOMING AND CHANGING
var supportsOrientationChange = "onorientationchange" in window;
var orientationEvent = supportsOrientationChange ? "orientationchange" : "resize";
window.addEventListener(
  orientationEvent,
  function() {
    if (navigator.userAgent.match(/android/i)) {
      setTimeout("redraw()", 500); //Only wait for Android
    } else {
      redraw();
    }
  },
  false
);

document.getElementById("render-lines").addEventListener("change", changeLines, false);
document.getElementById("render-depth").addEventListener("change", changeLines, false);
//  an alternative so that it waits for you to lift up your mouse/finger:
//document.getElementById("render-depth").addEventListener("mouseup", changeLines, false);
//document.getElementById("render-depth").addEventListener("touchend", changeLines, false);
document.getElementById("render-method").addEventListener("change", changeLines, false);

// TODO: make this relative to maxBinRenderSize
var zoomExtents = [Math.pow(2, -32), Math.pow(2,3)];

d3.select("#zoomin").on("click", zoomin);
d3.select("#zoomout").on("click", zoomout);
d3.select("#scrollleft").on("click", scrollleft);
d3.select("#scrollright").on("click", scrollright);

// ZOOMING AND CHANGING }}}

//{{{ VARIABLES

var plots = []; //an array of all plots

// TODO: sync this with the one in bridgeChart.js
var margin = {top: 10, right: 10, bottom: 25, left: 40};

var zoomSVG = d3.select("#zoomSVG");
var zoomRect = d3.select("#zoomRect");

// these are the overall scales which are modified by zooming
// they should be set as the default for new plots
var xScale = d3.scale.linear().domain([0, 1000]).range([0, document.getElementById("chartContainer").offsetWidth]);
var yScale = d3.scale.linear();

var frequency = 200; //Hz

// VARIABLES }}}

//{{{ HELPER FUNCTIONS

var redraw = function () {
  plots.forEach(function (plt) {
    plt.containerWidth(document.getElementById("chartContainer").offsetWidth).update();
  });
  d3.select("#charts").attr("width", document.getElementById("chartContainer").offsetWidth);
  zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
          .attr("height", document.getElementById("chartContainer").offsetHeight);
  zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
           .attr("height", document.getElementById("chartContainer").offsetHeight)
           .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  //update the zoom for the new plot size
  updateZoom();
}

function transitionAllNextTime() {
  plots.forEach(function (plt) {
    plt.transitionNextTime(true);
  });
}

function initPlot(data) {
  var plot = binnedLineChart(data);
  plot.xScale(copyScale(xScale));

  var pl = d3.select("#charts").append("g").call(plot);

  plot.containerWidth(document.getElementById("chartContainer").offsetWidth).height(75).marginTop(120*plots.length + 10).update();

  plots.push(plot);

  redraw();

  d3.select("#charts").attr("height", 120*plots.length).attr("width", document.getElementById("chartContainer").offsetWidth); //TODO: make this dynamic

  zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
         .attr("height", document.getElementById("chartContainer").offsetHeight)
         .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
          .attr("height", document.getElementById("chartContainer").offsetHeight - margin.top)
          .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  zoomRect.attr("fill", "rgba(0,0,0,0)")
          .call(zoom);

  // Redefine this function now that we have data for it to work from
  updateZoom = function () {
    xScale = plot.xScale();
    yScale = plot.yScale();
    zoom.x(xScale);
    zoom.y(yScale);
  };

  updateZoom();
}

// this will be changed once 'news' is sent from the server
// for now it's just a dummy
var updateZoom = function () { return 0; };

function copyScale(scal) {
  return d3.scale.linear().domain([scal.domain()[0], scal.domain()[1]]).range([scal.range()[0], scal.range()[1]]);
}

function zoomAll() {
  plots.forEach(function (plt) {
    plt.xScale(copyScale(xScale)).update();
  });
}

var zoom = d3.behavior.zoom()
  .scaleExtent(zoomExtents)
  .on("zoom", zoomAll);

function changeLines () {
  plots.forEach(function (plt) {
    plt.setSelectedLines()/*.reRenderTheNextTime(true)*/.update();
  });
}

// func1 is the function which modifies the domain start in terms of the old domain start, and xdist
// func2 is the function which modifies the domain end in terms of the old domain end, and xdist
function changeZoom(func1, func2) {
  var xdist = xScale.domain()[1] - xScale.domain()[0];

  var tmpScale = d3.scale.linear().range(xScale.range());
  tmpScale.domain([
    func1(xScale.domain()[0], xdist),
    func2(xScale.domain()[1], xdist)
  ]);

  xScale = changeXScale(xScale, tmpScale);

  zoom.x(xScale);
  transitionAllNextTime();
  zoomAll();
}

function getScaleValue(scal) {
  // gives a result which has units pixels / samples
  return (scal.range()[1] - scal.range()[0])/ (scal.domain()[1] - scal.domain()[0]);
}

function changeXScale (scal, value) {
  // set zoom extents for button zooming as well
  // TODO: set zoom to correspond with this. Currently they don't work together.
  if (getScaleValue(value) < zoomExtents[1] && getScaleValue(value) > zoomExtents[0] ) {
    return value;
  } else {
    console.log("bad extents: " + getScaleValue(value));
    return scal;
  }
}

function zoomin() {
  changeZoom(
    function (a, b) { return a + (b/4); },
    function (a, b) { return a - (b/4); }
  );
}

function zoomout() {
  changeZoom(
    function (a, b) { return a - (b/2); },
    function (a, b) { return a + (b/2); }
  );
}

function scrollleft() {
  changeZoom(
    function (a, b) { return a + (b/4); },
    function (a, b) { return a + (b/4); }
  );
}

function scrollright() {
  changeZoom(
    function (a, b) { return a - (b/4); },
    function (a, b) { return a - (b/4); }
  );
}

// HELPER FUNCTIONS }}}

//{{{ SERVER COMMUNICATIONS

var socket = io.connect('130.179.231.28:8080/');
var firstTime = true;

//socket.on('connect_failed', function () {
//  console.log("connect_failed :(");
//});
//
//socket.on('connecting', function () {
//  console.log("connecting :!");
//});
//
//socket.on('connect', function () {
//  console.log("connected !!");
//});
//
//socket.on('disconnect', function () {
//  console.log("disconnected !!");
//});

socket.on('news', function (data) {
  // only do this once, so that plots don't get overlapped whenever the server restarts.
  if (!firstTime) {
    return;
  }
  // TODO: extract data about which "level" the data is for.
  // SPB is 200Hz

  firstTime = false;

  // deleting all example plots -->
  _.times(plots.length, function (i) {
    delete plots[i];
  });
  svg = document.getElementById("charts");
  while (svg.lastChild) {
    svg.removeChild(svg.lastChild);
  }
  plots = []; // delete the previous plots
  // <-- done deleting all example plots

  var json = JSON.parse(data);
  socket.emit('ack', "Message received!");

  //initPlot(json);
  initPlot(_.map(json, function (d) { return { ESGgirder18: d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
  initPlot(_.map(json, function (d) { return { ESGgirder18: Math.random() * 5 + d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
});

// SERVER COMMUNICATIONS }}}

//{{{ OFFLINE DEMO

// A demonstration with example data in case the server is down:
// wait 2 seconds to give the server a chance to send the data (to avoid the demo popping up and then disappearing)
setTimeout(rundemo, 1500);

function rundemo() {
  d3.json("Server/esg_sample_index.js", function (error, data) {
    if (error || plots.length > 0) {
      return;
    }

    var json = data;

    //initPlot(json);
    initPlot(_.map(json, function (d) { return { ESGgirder18: d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
  });
}

// OFFLINE DEMO }}}

/* vim: set foldmethod=marker: */
<commit_msg>a big mess, getting closer?<commit_after>//{{{ ZOOMING AND CHANGING
var supportsOrientationChange = "onorientationchange" in window;
var orientationEvent = supportsOrientationChange ? "orientationchange" : "resize";
window.addEventListener(
  orientationEvent,
  function() {
    if (navigator.userAgent.match(/android/i)) {
      setTimeout("redraw()", 500); //Only wait for Android
    } else {
      redraw();
    }
  },
  false
);

document.getElementById("render-lines").addEventListener("change", changeLines, false);
document.getElementById("render-depth").addEventListener("change", changeLines, false);
//  an alternative so that it waits for you to lift up your mouse/finger:
//document.getElementById("render-depth").addEventListener("mouseup", changeLines, false);
//document.getElementById("render-depth").addEventListener("touchend", changeLines, false);
document.getElementById("render-method").addEventListener("change", changeLines, false);

// TODO: magic: make this relative to maxBinRenderSize
var zoomExtents = [Math.pow(2, -32), Math.pow(2,3)];

d3.select("#zoomin").on("click", zoomin);
d3.select("#zoomout").on("click", zoomout);
d3.select("#scrollleft").on("click", scrollleft);
d3.select("#scrollright").on("click", scrollright);

// ZOOMING AND CHANGING }}}

//{{{ VARIABLES

var plots = []; //an array of all plots

// TODO: sync this with the one in bridgeChart.js
var margin = {top: 10, right: 10, bottom: 25, left: 40};

var zoomSVG = d3.select("#zoomSVG");
var zoomRect = d3.select("#zoomRect");

// these are the overall scales which are modified by zooming
// they should be set as the default for new plots
var xScale = d3.scale.linear().domain([0, 1000]).range([0, document.getElementById("chartContainer").offsetWidth]);
var yScale = d3.scale.linear();

var frequency = 200; //Hz

// VARIABLES }}}

//{{{ HELPER FUNCTIONS

var redraw = function () {
  plots.forEach(function (plt) {
    plt.containerWidth(document.getElementById("chartContainer").offsetWidth).update();
  });
  d3.select("#charts").attr("width", document.getElementById("chartContainer").offsetWidth);
  zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
          .attr("height", document.getElementById("chartContainer").offsetHeight);
  zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
           .attr("height", document.getElementById("chartContainer").offsetHeight)
           .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  //update the zoom for the new plot size
  updateZoom();
}

function transitionAllNextTime() {
  plots.forEach(function (plt) {
    plt.transitionNextTime(true);
  });
}

function initPlot(data) {
  var plot = binnedLineChart(data);
  plot.xScale(copyScale(xScale));

  var pl = d3.select("#charts").append("g").call(plot);

  plot.containerWidth(document.getElementById("chartContainer").offsetWidth).height(75).marginTop(120*plots.length + 10).update();

  plots.push(plot);

  redraw();

  d3.select("#charts").attr("height", 120*plots.length).attr("width", document.getElementById("chartContainer").offsetWidth); //TODO: make this dynamic

  zoomSVG.attr("width", document.getElementById("chartContainer").offsetWidth)
         .attr("height", document.getElementById("chartContainer").offsetHeight)
         .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  zoomRect.attr("width", document.getElementById("chartContainer").offsetWidth - margin.left - margin.right)
          .attr("height", document.getElementById("chartContainer").offsetHeight - margin.top)
          .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

  zoomRect.attr("fill", "rgba(0,0,0,0)")
          .call(zoom);

  // Redefine this function now that we have data for it to work from
  updateZoom = function () {
    xScale = plot.xScale();
    yScale = plot.yScale();
    zoom.x(xScale);
    zoom.y(yScale);
  };

  updateZoom();
}

// this will be changed once 'news' is sent from the server
// for now it's just a dummy
var updateZoom = function () { return 0; };

function copyScale(scal) {
  return d3.scale.linear().domain([scal.domain()[0], scal.domain()[1]]).range([scal.range()[0], scal.range()[1]]);
}

function zoomAll() {
  console.log(zoom.translate());
  plots.forEach(function (plt) {
    plt.xScale(copyScale(xScale)).update();
  });
}

var zoom = d3.behavior.zoom()
  .scaleExtent(zoomExtents)
  .on("zoom", zoomAll);

function changeLines () {
  plots.forEach(function (plt) {
    plt.setSelectedLines()/*.reRenderTheNextTime(true)*/.update();
  });
}

// func1 is the function which modifies the domain start in terms of the old domain start, and xdist
// func2 is the function which modifies the domain end in terms of the old domain end, and xdist
function changeZoom(func1, func2) {
  var xdist = xScale.domain()[1] - xScale.domain()[0];

  var tmpScale = d3.scale.linear().range(xScale.range());
  tmpScale.domain([
    func1(xScale.domain()[0], xdist),
    func2(xScale.domain()[1], xdist)
  ]);

  //xScale =
    changeXScale(xScale, tmpScale);

  transitionAllNextTime();
  zoomAll();
}

function getScaleValue(scal) {
  // gives a result which has units pixels / samples
  return (scal.range()[1] - scal.range()[0])/ (scal.domain()[1] - scal.domain()[0]);
}

function changeXScale (scal, value) {
  // set zoom extents for button zooming as well
  // TODO: set zoom to correspond with this. Currently they don't work together.
  if (getScaleValue(value) < zoomExtents[1] && getScaleValue(value) > zoomExtents[0] ) {
    // Doing this manually because cursor zooming and button zooming
    //   don't place nice together when extents are involved.
    // To be more specific, applying the x scale again resets
    //   the zoom scale.
      console.log("----------");
    var ratio = getScaleValue(scal) / getScaleValue(value);
    // find the difference between the averages
    var avg = function (dom) {
      return (dom[0] + dom[1]) / 2.0;
    }

    somerat = getScaleValue(scal);

    var old = value.domain()[1] - value.domain()[0];
    console.log("ratio: " + ratio);
    console.log("old: " + old);
    console.log("old/ratio: " + (old*ratio));

    var diff = value.domain()[0] - scal.domain()[0];
    // centreValue is the offset in PIXELS...
    // TODO: translate pixels to domain
    // .invert(val)
    var centreValue = (scal.domain()[1] - scal.domain()[0]) / 2;
    console.log("centreValue: " + centreValue);
    console.log("centreValue * somerat: " + centreValue * somerat);
    console.log("centreValue / somerat: " + centreValue / somerat);
    console.log("scal: " + scal.domain() + " val: " + value.domain());
    console.log("diff: " + diff);
      //console.log(ratio);
      console.log("zoom.scale(): " + zoom.scale());
      //console.log("getscv(xScale): " + getScaleValue(scal))
      //console.log("getscv(value): " + getScaleValue(value))
      console.log("translate: " + zoom.translate());
    //zoom.x(value);
    var sc = zoom.scale();
    var tr = zoom.translate();
      //console.log(sc);
    //zoom.translate([zoom.translate()[0] + diff - (old*ratio), zoom.translate()[1]]);
    //zoom.translate([zoom.translate()[0] + (diff/2), zoom.translate()[1]]);
    //zoom.translate([tr[0] - (diff/4), tr[1]]);
      console.log("translate: " + zoom.translate());
    //zoom.translate([tr[0]/ratio, tr[1]]);
    zoom.translate([value.invert(centreValue), tr[1]]);
    var range = value.range()[1] - value.range()[0];
      console.log("range: " + range);
      console.log("value.invert(centreValue): " + value.invert(centreValue));
      console.log( zoom.translate()[0] * (896 / (value.range()[1] - value.range()[0])));
    zoom.scale(sc / ratio);
      console.log("zoom.scale(): " + zoom.scale());
      console.log(document.getElementById("chartContainer").offsetWidth);
    return value;
  } else {
    console.log("bad extents: " + getScaleValue(value));
    return scal;
  }
}

function zoomin() {
  changeZoom(
    function (a, b) { return a + (b/4); },
    function (a, b) { return a - (b/4); }
  );
}

function zoomout() {
  changeZoom(
    function (a, b) { return a - (b/2); },
    function (a, b) { return a + (b/2); }
  );
}

function scrollleft() {
  changeZoom(
    function (a, b) { return a + (b/4); },
    function (a, b) { return a + (b/4); }
  );
}

function scrollright() {
  changeZoom(
    function (a, b) { return a - (b/4); },
    function (a, b) { return a - (b/4); }
  );
}

// HELPER FUNCTIONS }}}

//{{{ SERVER COMMUNICATIONS

var socket = io.connect('130.179.231.28:8080/');
var firstTime = true;

//socket.on('connect_failed', function () {
//  console.log("connect_failed :(");
//});
//
//socket.on('connecting', function () {
//  console.log("connecting :!");
//});
//
//socket.on('connect', function () {
//  console.log("connected !!");
//});
//
//socket.on('disconnect', function () {
//  console.log("disconnected !!");
//});

socket.on('news', function (data) {
  // only do this once, so that plots don't get overlapped whenever the server restarts.
  if (!firstTime) {
    return;
  }
  // TODO: extract data about which "level" the data is for.
  // SPB is 200Hz

  firstTime = false;

  // deleting all example plots -->
  _.times(plots.length, function (i) {
    delete plots[i];
  });
  svg = document.getElementById("charts");
  while (svg.lastChild) {
    svg.removeChild(svg.lastChild);
  }
  plots = []; // delete the previous plots
  // <-- done deleting all example plots

  var json = JSON.parse(data);
  socket.emit('ack', "Message received!");

  //initPlot(json);
  initPlot(_.map(json, function (d) { return { ESGgirder18: d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
  initPlot(_.map(json, function (d) { return { ESGgirder18: Math.random() * 5 + d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
});

// SERVER COMMUNICATIONS }}}

//{{{ OFFLINE DEMO

// A demonstration with example data in case the server is down:
// wait 2 seconds to give the server a chance to send the data (to avoid the demo popping up and then disappearing)
setTimeout(rundemo, 1500);

function rundemo() {
  d3.json("Server/esg_sample_index.js", function (error, data) {
    if (error || plots.length > 0) {
      return;
    }

    var json = data;

    //initPlot(json);
    initPlot(_.map(json, function (d) { return { ESGgirder18: d.ESGgirder18, SampleIndex: d.SampleIndex*(1000/frequency)}; }));
  });
}

// OFFLINE DEMO }}}

/* vim: set foldmethod=marker: */
<|endoftext|>


<commit_before>var BrowserView = function(args) {
  var gridCanvas = document.querySelector('canvas#tetris-grid');
  var previewCanvas = document.querySelector('canvas#tetris-preview');

  this.tableOverlay = document.querySelector('canvas#table-overlay');
  this.tableCanvas = document.querySelector('canvas#tetris-table');

  BLOCK_SPACING_HEIGHT = gridCanvas.getAttribute('height') / 20;
  BLOCK_SPACING_WIDTH = gridCanvas.getAttribute('width') / 10;
  BLOCK_HEIGHT = BLOCK_SPACING_HEIGHT - 10;
  BLOCK_WIDTH = BLOCK_SPACING_WIDTH - 10;

  this.elementDescrip = document.getElementById('element-description');
  this.elementName = document.getElementById('element-name');
  this.atomicNumDisplay = document.getElementById('atomic-number');
  this.elementLink = document.getElementById('element-link');
  this.playerScore = document.getElementById('player-score');
  this.gameLevel = document.getElementById('game-level');

  this.gridContext = gridCanvas.getContext('2d');
  this.previewContext = previewCanvas.getContext('2d');
  this.tableContext = this.tableCanvas.getContext('2d');
  this.overlayContext = this.tableOverlay.getContext('2d');

  this.overlayContext.fillStyle = 'rgba(255, 255, 255, 0.5)';

  this.gameBoard = args.gameBoard;
  this.previewBoard = new PreviewBoard();
  this.tableBoard = new PeriodicTable();

  this.cycleDropBlock = args.cycleDropBlock;
  this.isPaused = true;
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };
  this.interval = {
    slide: null,
    rotate: null
  };

  addEventListener('keydown', this.keyDown.bind(this));
  addEventListener('keyup', this.keyUp.bind(this));

  document.querySelector('#level-right').addEventListener('mousedown', this.buttonDown.bind(this));
  document.querySelector('#level-right').addEventListener('mouseup', this.buttonUp.bind(this));

  this.tableOverlay.addEventListener('mousedown', function(event) {
    var mouseX = Math.floor((event.pageX - this.tableOverlay.offsetLeft) / (540 / 18));
    var mouseY = Math.floor((event.pageY - this.tableOverlay.offsetTop) / (270 / 9));
    var element = this.tableBoard.board[mouseY][mouseX];

    if(element > 0) {
      this.updateElementDescrip(element);
    }
  }.bind(this));
  this.tableOverlay.addEventListener('mousemove', function(event) {
    var mouseX = Math.floor((event.pageX - this.tableOverlay.offsetLeft) / (540 / 18));
    var mouseY = Math.floor((event.pageY - this.tableOverlay.offsetTop) / (270 / 9));
    var element = this.tableBoard.board[mouseY][mouseX];

    if(element > 0) {
      this.overlayContext.clearRect(0, 0, 540, 270);
      this.overlayContext.fillRect((mouseX * BLOCK_SPACING_WIDTH) + 5, (mouseY * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
    }
    else {
      this.overlayContext.clearRect(0, 0, 540, 270);
    }
  }.bind(this));
}
BrowserView.prototype.keyDown = function(event) {
  var pressedKey = KEY_CODES[event.keyCode];
  if(this.isPaused) {
    if(pressedKey == 'space') {
      event.preventDefault();
      this.isPaused = false;
      this.cycleDropBlock();
    }
  }
  else {
    if(pressedKey == 'left' || pressedKey == 'right') {
      event.preventDefault();
      if(this.pressed.slide == false) {
        this.pressed.slide = pressedKey;
        clearInterval(this.interval.slide);
        this.gameBoard.slideBlock(this.pressed.slide);
        this.interval.slide = setInterval(this.gameBoard.slideBlock.bind(this.gameBoard, this.pressed.slide), SLIDE_DELAY);
      }
    }
    else if(pressedKey == 'down') {
      event.preventDefault();
      if(this.pressed.drop == false) {
        this.pressed.drop = true;
        clearTimeout(this.dropTimeout);
        this.cycleDropBlock({quickly: true});
      }
    }
    else if(pressedKey == 'up') {
      event.preventDefault();
      if(this.pressed.rotate == false) {
        this.pressed.rotate = true;
        clearInterval(this.interval.rotate);
        this.gameBoard.rotateBlock('counter');
        this.interval.rotate = setInterval(this.gameBoard.rotateBlock.bind(this.gameBoard, 'counter'), ROTATE_DELAY);
      }
    }
    else if(pressedKey == 'space') {
      event.preventDefault();
      clearTimeout(this.gameBoard.dropInterval);
      clearTimeout(this.dropTimeout);
      clearInterval(this.interval.rotate);
      clearInterval(this.interval.slide);
      this.isPaused = true;
    }
  }
};
BrowserView.prototype.keyUp = function(event){
  var releasedKey = KEY_CODES[event.keyCode];
  if(this.isPaused === false) {
    if(releasedKey == 'left' || releasedKey == 'right') {
      event.preventDefault();
      clearInterval(this.interval.slide);
      this.pressed.slide = false;
    }
    if(releasedKey == 'down') {
      event.preventDefault();
      clearTimeout(this.dropTimeout);
      this.pressed.drop = false;
      this.cycleDropBlock();
    }
    if(releasedKey == 'up') {
      event.preventDefault();
      clearInterval(this.interval.rotate);
      this.pressed.rotate = false;
    }
  }
};
BrowserView.prototype.buttonDown = function(event) {
  if(event.target.nodeName == "BUTTON") {
    var buttonPressed = event.target.dataset.key;
    if(this.isPaused) {
      if(buttonPressed == 'space') {
        this.isPaused = false;
        this.cycleDropBlock();
      }
    }
    else {
      if(buttonPressed == 'left' || buttonPressed == 'right') {
        if(this.pressed.slide == false) {
          this.pressed.slide = buttonPressed;
          clearInterval(this.interval.slide);
          this.interval.slide = setInterval(this.handleInput.bind(this), SLIDE_DELAY);
        }
      }
      else if(buttonPressed == 'down') {
        if(this.pressed.drop == false) {
          this.pressed.drop = true;
          clearTimeout(this.dropTimeout);
          this.cycleDropBlock({quickly: true});
        }
      }
      else if(buttonPressed == 'up') {
        if(this.pressed.rotate == false) {
          this.pressed.rotate = true;
          clearInterval(this.interval.rotate);
          this.interval.rotate = setInterval(this.handleInput.bind(this), ROTATE_DELAY);
        }
      }
      else if(buttonPressed == 'space') {
        clearTimeout(this.gameBoard.dropInterval);
        clearTimeout(this.dropTimeout);
        clearInterval(this.interval.rotate);
        clearInterval(this.interval.slide);
        this.isPaused = true;
      }
      this.handleInput();
    }
  }
};
BrowserView.prototype.buttonUp = function(event) {
  if(this.isPaused === false) {
    if(this.pressed.drop) {
      clearTimeout(this.dropTimeout);
      this.cycleDropBlock();
    }
    clearInterval(this.interval.slide);
    clearInterval(this.interval.rotate);
    this.releaseAllKeys();
  }
};
BrowserView.prototype.handleInput = function() {
  if(this.pressed.slide) {
    this.gameBoard.slideBlock(this.pressed.slide);
  }
  else if(this.pressed.rotate) {
    this.gameBoard.rotateBlock('counter');
  }
};
BrowserView.prototype.releaseAllKeys = function() {
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };
};
BrowserView.prototype.drawBoard = function(board, context) {
  var gridContext = context;
  gridContext.lineWidth = 4;

  board.forEach(function(row, rIndex) {
    row.forEach(function(col, cIndex) {
      gridContext.fillStyle = CHEMICAL_ELEMENTS[col]['background-color'];
      gridContext.strokeStyle = CHEMICAL_ELEMENTS[col]['border-color'];

      var xPos = (cIndex * BLOCK_SPACING_WIDTH) + 5;
      var yPos = (rIndex * BLOCK_SPACING_HEIGHT) + 5;
      var textX = (cIndex * BLOCK_SPACING_WIDTH) + (BLOCK_SPACING_WIDTH / 2);
      var textY = (rIndex * BLOCK_SPACING_HEIGHT) + (BLOCK_SPACING_HEIGHT / 2) + 4;

      var textLen = CHEMICAL_ELEMENTS[col].symbol.length;
      if(textLen == 1) {
        gridContext.font = FONT_SIZE + BLOCK_FONT;
        textX -= 3;
      }
      else if(textLen == 2) {
        gridContext.font = FONT_SIZE + BLOCK_FONT;
        textX -= 7;
      }
      else if(textLen == 3) {
        gridContext.font = (FONT_SIZE - 2) + BLOCK_FONT;
        textX -= 7;
      }

      gridContext.fillRect(xPos, yPos, BLOCK_WIDTH, BLOCK_HEIGHT);
      gridContext.strokeRect(xPos, yPos, BLOCK_WIDTH, BLOCK_HEIGHT);
      gridContext.fillStyle = CHEMICAL_ELEMENTS[col]['color'];
      gridContext.fillText(CHEMICAL_ELEMENTS[col].symbol, textX, textY);
    });
  });
};
BrowserView.prototype.drawAllBoards = function() {
  this.drawBoard(this.gameBoard.board, this.gridContext);
  this.drawBoard(this.previewBoard.board, this.previewContext);
  this.drawBoard(this.tableBoard.board, this.tableContext);
};
BrowserView.prototype.animateGame = function() {
  var lastTime = null;
  var progress = true;

  var animate = function(time) {
    if(lastTime) {
      var timeStep = Math.min(time - lastTime, 100) / 1000;
      progress = timeStep < 1000;
    }
    lastTime = time;

    if(progress) {
      this.drawAllBoards();
      this.updatePlayerScore(this.gameBoard.score);
      this.updateGameLevel();
      requestAnimationFrame(animate.bind(this));
    }
  }
  requestAnimationFrame(animate.bind(this));
};
BrowserView.prototype.updateElementDescrip = function(element) {
  if(element === undefined) {
    var element = this.previewBoard.tetrinimo.element;
  }

  this.elementName.innerHTML = CHEMICAL_ELEMENTS[element].name + ' [' + CHEMICAL_ELEMENTS[element].symbol + ']';
  this.atomicNumDisplay.innerHTML = element;
  this.elementDescrip.innerHTML = CHEMICAL_ELEMENTS[element].descrip;

  this.elementLink.href = CHEMISTRY_URL + CHEMICAL_ELEMENTS[element].name.toLowerCase();
  this.elementLink.innerHTML = 'Learn more about ' + CHEMICAL_ELEMENTS[element].name.toLowerCase();
};
BrowserView.prototype.updatePlayerScore = function(score) {
  this.playerScore.innerHTML = score;
};
BrowserView.prototype.updateGameLevel = function() {
  var newLevel = scoreToLevel(this.gameBoard.score);
  if(this.level != newLevel) {
    this.level = newLevel;
    this.gameLevel.innerHTML = this.level;
    clearTimeout(this.dropTimeout);
    this.cycleDropBlock();
  }
};
<commit_msg>Add browser view function to turn game mode dropdown into static text<commit_after>var BrowserView = function(args) {
  var gridCanvas = document.querySelector('canvas#tetris-grid');
  var previewCanvas = document.querySelector('canvas#tetris-preview');

  this.tableOverlay = document.querySelector('canvas#table-overlay');
  this.tableCanvas = document.querySelector('canvas#tetris-table');

  BLOCK_SPACING_HEIGHT = gridCanvas.getAttribute('height') / 20;
  BLOCK_SPACING_WIDTH = gridCanvas.getAttribute('width') / 10;
  BLOCK_HEIGHT = BLOCK_SPACING_HEIGHT - 10;
  BLOCK_WIDTH = BLOCK_SPACING_WIDTH - 10;

  this.elementDescrip = document.getElementById('element-description');
  this.elementName = document.getElementById('element-name');
  this.atomicNumDisplay = document.getElementById('atomic-number');
  this.elementLink = document.getElementById('element-link');
  this.playerScore = document.getElementById('player-score');
  this.gameLevel = document.getElementById('game-level');
  this.gameMode = document.getElementById('game-mode');
  this.gameModeDropdown;

  for(var node in this.gameMode.childNodes) {
    var currentNode = this.gameMode.childNodes[node];
    if(currentNode.tagName == 'SELECT') {
      this.gameModeDropdown = currentNode;
      break;
    }
  }

  this.gridContext = gridCanvas.getContext('2d');
  this.previewContext = previewCanvas.getContext('2d');
  this.tableContext = this.tableCanvas.getContext('2d');
  this.overlayContext = this.tableOverlay.getContext('2d');

  this.overlayContext.fillStyle = 'rgba(255, 255, 255, 0.5)';

  this.gameBoard = args.gameBoard;
  this.previewBoard = new PreviewBoard();
  this.tableBoard = new PeriodicTable();

  this.cycleDropBlock = args.cycleDropBlock;
  this.isPaused = true;
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };
  this.interval = {
    slide: null,
    rotate: null
  };

  addEventListener('keydown', this.keyDown.bind(this));
  addEventListener('keyup', this.keyUp.bind(this));

  document.querySelector('#level-right').addEventListener('mousedown', this.buttonDown.bind(this));
  document.querySelector('#level-right').addEventListener('mouseup', this.buttonUp.bind(this));

  this.tableOverlay.addEventListener('mousedown', function(event) {
    var mouseX = Math.floor((event.pageX - this.tableOverlay.offsetLeft) / (540 / 18));
    var mouseY = Math.floor((event.pageY - this.tableOverlay.offsetTop) / (270 / 9));
    var element = this.tableBoard.board[mouseY][mouseX];

    if(element > 0) {
      this.updateElementDescrip(element);
    }
  }.bind(this));
  this.tableOverlay.addEventListener('mousemove', function(event) {
    var mouseX = Math.floor((event.pageX - this.tableOverlay.offsetLeft) / (540 / 18));
    var mouseY = Math.floor((event.pageY - this.tableOverlay.offsetTop) / (270 / 9));
    var element = this.tableBoard.board[mouseY][mouseX];

    if(element > 0) {
      this.overlayContext.clearRect(0, 0, 540, 270);
      this.overlayContext.fillRect((mouseX * BLOCK_SPACING_WIDTH) + 5, (mouseY * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
    }
    else {
      this.overlayContext.clearRect(0, 0, 540, 270);
    }
  }.bind(this));
}
BrowserView.prototype.keyDown = function(event) {
  var pressedKey = KEY_CODES[event.keyCode];
  if(this.isPaused) {
    if(pressedKey == 'space') {
      event.preventDefault();
      this.isPaused = false;
      this.cycleDropBlock();
    }
  }
  else {
    if(pressedKey == 'left' || pressedKey == 'right') {
      event.preventDefault();
      if(this.pressed.slide == false) {
        this.pressed.slide = pressedKey;
        clearInterval(this.interval.slide);
        this.gameBoard.slideBlock(this.pressed.slide);
        this.interval.slide = setInterval(this.gameBoard.slideBlock.bind(this.gameBoard, this.pressed.slide), SLIDE_DELAY);
      }
    }
    else if(pressedKey == 'down') {
      event.preventDefault();
      if(this.pressed.drop == false) {
        this.pressed.drop = true;
        clearTimeout(this.dropTimeout);
        this.cycleDropBlock({quickly: true});
      }
    }
    else if(pressedKey == 'up') {
      event.preventDefault();
      if(this.pressed.rotate == false) {
        this.pressed.rotate = true;
        clearInterval(this.interval.rotate);
        this.gameBoard.rotateBlock('counter');
        this.interval.rotate = setInterval(this.gameBoard.rotateBlock.bind(this.gameBoard, 'counter'), ROTATE_DELAY);
      }
    }
    else if(pressedKey == 'space') {
      event.preventDefault();
      clearTimeout(this.gameBoard.dropInterval);
      clearTimeout(this.dropTimeout);
      clearInterval(this.interval.rotate);
      clearInterval(this.interval.slide);
      this.isPaused = true;
    }
  }
};
BrowserView.prototype.keyUp = function(event){
  var releasedKey = KEY_CODES[event.keyCode];
  if(this.isPaused === false) {
    if(releasedKey == 'left' || releasedKey == 'right') {
      event.preventDefault();
      clearInterval(this.interval.slide);
      this.pressed.slide = false;
    }
    if(releasedKey == 'down') {
      event.preventDefault();
      clearTimeout(this.dropTimeout);
      this.pressed.drop = false;
      this.cycleDropBlock();
    }
    if(releasedKey == 'up') {
      event.preventDefault();
      clearInterval(this.interval.rotate);
      this.pressed.rotate = false;
    }
  }
};
BrowserView.prototype.buttonDown = function(event) {
  if(event.target.nodeName == "BUTTON") {
    var buttonPressed = event.target.dataset.key;
    if(this.isPaused) {
      if(buttonPressed == 'space') {
        this.isPaused = false;
        this.cycleDropBlock();
      }
    }
    else {
      if(buttonPressed == 'left' || buttonPressed == 'right') {
        if(this.pressed.slide == false) {
          this.pressed.slide = buttonPressed;
          clearInterval(this.interval.slide);
          this.interval.slide = setInterval(this.handleInput.bind(this), SLIDE_DELAY);
        }
      }
      else if(buttonPressed == 'down') {
        if(this.pressed.drop == false) {
          this.pressed.drop = true;
          clearTimeout(this.dropTimeout);
          this.cycleDropBlock({quickly: true});
        }
      }
      else if(buttonPressed == 'up') {
        if(this.pressed.rotate == false) {
          this.pressed.rotate = true;
          clearInterval(this.interval.rotate);
          this.interval.rotate = setInterval(this.handleInput.bind(this), ROTATE_DELAY);
        }
      }
      else if(buttonPressed == 'space') {
        clearTimeout(this.gameBoard.dropInterval);
        clearTimeout(this.dropTimeout);
        clearInterval(this.interval.rotate);
        clearInterval(this.interval.slide);
        this.isPaused = true;
      }
      this.handleInput();
    }
  }
};
BrowserView.prototype.buttonUp = function(event) {
  if(this.isPaused === false) {
    if(this.pressed.drop) {
      clearTimeout(this.dropTimeout);
      this.cycleDropBlock();
    }
    clearInterval(this.interval.slide);
    clearInterval(this.interval.rotate);
    this.releaseAllKeys();
  }
};
BrowserView.prototype.handleInput = function() {
  if(this.pressed.slide) {
    this.gameBoard.slideBlock(this.pressed.slide);
  }
  else if(this.pressed.rotate) {
    this.gameBoard.rotateBlock('counter');
  }
};
BrowserView.prototype.releaseAllKeys = function() {
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };
};
BrowserView.prototype.drawBoard = function(board, context) {
  var gridContext = context;
  gridContext.lineWidth = 4;

  board.forEach(function(row, rIndex) {
    row.forEach(function(col, cIndex) {
      gridContext.fillStyle = CHEMICAL_ELEMENTS[col]['background-color'];
      gridContext.strokeStyle = CHEMICAL_ELEMENTS[col]['border-color'];

      var xPos = (cIndex * BLOCK_SPACING_WIDTH) + 5;
      var yPos = (rIndex * BLOCK_SPACING_HEIGHT) + 5;
      var textX = (cIndex * BLOCK_SPACING_WIDTH) + (BLOCK_SPACING_WIDTH / 2);
      var textY = (rIndex * BLOCK_SPACING_HEIGHT) + (BLOCK_SPACING_HEIGHT / 2) + 4;

      var textLen = CHEMICAL_ELEMENTS[col].symbol.length;
      if(textLen == 1) {
        gridContext.font = FONT_SIZE + BLOCK_FONT;
        textX -= 3;
      }
      else if(textLen == 2) {
        gridContext.font = FONT_SIZE + BLOCK_FONT;
        textX -= 7;
      }
      else if(textLen == 3) {
        gridContext.font = (FONT_SIZE - 2) + BLOCK_FONT;
        textX -= 7;
      }

      gridContext.fillRect(xPos, yPos, BLOCK_WIDTH, BLOCK_HEIGHT);
      gridContext.strokeRect(xPos, yPos, BLOCK_WIDTH, BLOCK_HEIGHT);
      gridContext.fillStyle = CHEMICAL_ELEMENTS[col]['color'];
      gridContext.fillText(CHEMICAL_ELEMENTS[col].symbol, textX, textY);
    });
  });
};
BrowserView.prototype.drawAllBoards = function() {
  this.drawBoard(this.gameBoard.board, this.gridContext);
  this.drawBoard(this.previewBoard.board, this.previewContext);
  this.drawBoard(this.tableBoard.board, this.tableContext);
};
BrowserView.prototype.animateGame = function() {
  var lastTime = null;
  var progress = true;

  var animate = function(time) {
    if(lastTime) {
      var timeStep = Math.min(time - lastTime, 100) / 1000;
      progress = timeStep < 1000;
    }
    lastTime = time;

    if(progress) {
      this.drawAllBoards();
      this.updatePlayerScore(this.gameBoard.score);
      this.updateGameLevel();
      requestAnimationFrame(animate.bind(this));
    }
  }
  requestAnimationFrame(animate.bind(this));
};
BrowserView.prototype.updateElementDescrip = function(element) {
  if(element === undefined) {
    var element = this.previewBoard.tetrinimo.element;
  }

  this.elementName.innerHTML = CHEMICAL_ELEMENTS[element].name + ' [' + CHEMICAL_ELEMENTS[element].symbol + ']';
  this.atomicNumDisplay.innerHTML = element;
  this.elementDescrip.innerHTML = CHEMICAL_ELEMENTS[element].descrip;

  this.elementLink.href = CHEMISTRY_URL + CHEMICAL_ELEMENTS[element].name.toLowerCase();
  this.elementLink.innerHTML = 'Learn more about ' + CHEMICAL_ELEMENTS[element].name.toLowerCase();
};
BrowserView.prototype.updatePlayerScore = function(score) {
  this.playerScore.innerHTML = score;
};
BrowserView.prototype.updateGameLevel = function() {
  var newLevel = scoreToLevel(this.gameBoard.score);
  if(this.level != newLevel) {
    this.level = newLevel;
    this.gameLevel.innerHTML = this.level;
    clearTimeout(this.dropTimeout);
    this.cycleDropBlock();
  }
};
BrowserView.prototype.makeModeStatic = function() {
  var modeIndex = this.gameModeDropdown.selectedIndex;
  this.gameMode.innerHTML = GAME_MODES[modeIndex];
};
<|endoftext|>


<commit_before>const _ = require('underscore')
const os = require('os');
const fs = require('fs');
const path = require('path')
const mkdirp = require('mkdirp');
const detectThread = require('./detect-thread');
const extractFiles = require('./extract-files');
const extractContacts = require('./extract-contacts');
const MessageFactory = require('../shared/message-factory')
const LocalDatabaseConnector = require('../shared/local-database-connector');

const MAX_QUEUE_LENGTH = 500
const PROCESSING_DELAY = 0

class MessageProcessor {

  constructor() {
    // The queue is a chain of Promises
    this._queue = Promise.resolve()
    this._queueLength = 0
  }

  queueLength() {
    return this._queueLength
  }

  queueIsFull() {
    return this._queueLength >= MAX_QUEUE_LENGTH
  }

  /**
   * @returns Promise that resolves when message has been processed
   * This promise will never reject, given that this function is meant to be
   * called as a fire and forget operation
   * If message processing fails, we will register the failure in the folder
   * syncState
   */
  queueMessageForProcessing({accountId, folderId, imapMessage, struct, desiredParts}) {
    return new Promise((resolve) => {
      this._queueLength++
      this._queue = this._queue.then(async () => {
        await this._processMessage({accountId, folderId, imapMessage, struct, desiredParts})
        this._queueLength--

        // To save memory, we reset the Promise chain if the queue reaches a
        // length of 0, otherwise we will continue referencing the entire chain
        // of promises that came before
        if (this._queueLength === 0) {
          this._queue = Promise.resolve()
        }
        resolve()

        // Throttle message processing to meter cpu usage
        await new Promise(r => setTimeout(r, PROCESSING_DELAY))
      })
    })
  }

  async _processMessage({accountId, folderId, imapMessage, struct, desiredParts}) {
    const db = await LocalDatabaseConnector.forAccount(accountId);
    const {Message, Folder} = db
    const folder = await Folder.findById(folderId)
    try {
      const messageValues = await MessageFactory.parseFromImap(imapMessage, desiredParts, {
        db,
        folder,
        accountId,
      });
      const existingMessage = await Message.find({where: {id: messageValues.id}});
      let processedMessage;
      if (existingMessage) {
        // TODO: optimize to not do a full message parse for existing messages
        processedMessage = await this._processExistingMessage(existingMessage, messageValues, struct)
      } else {
        processedMessage = await this._processNewMessage(messageValues, struct)
      }

      // Inflate the serialized oldestProcessedDate value, if it exists
      let oldestProcessedDate;
      if (folder.syncState && folder.syncState.oldestProcessedDate) {
        oldestProcessedDate = new Date(folder.syncState.oldestProcessedDate);
      }
      const justProcessedDate = messageValues.date ? new Date(messageValues.date) : new Date()

      // Update the oldestProcessedDate if:
      //   a) justProcessedDate is after the year 1980. We don't want to base this
      //      off of messages with borked 1970 dates.
      // AND
      //   b) i) We haven't set oldestProcessedDate yet
      //     OR
      //      ii) justProcessedDate is before oldestProcessedDate and in a different
      //          month. (We only use this to update the sync status in Nylas Mail,
      //          which uses month precision. Updating a folder's syncState triggers
      //          many re-renders in Nylas Mail, so we only do it as necessary.)
      if (justProcessedDate > new Date("1980") && (
            !oldestProcessedDate || (
              (justProcessedDate.getMonth() !== oldestProcessedDate.getMonth() ||
                justProcessedDate.getFullYear() !== oldestProcessedDate.getFullYear()) &&
              justProcessedDate < oldestProcessedDate))) {
        await folder.updateSyncState({oldestProcessedDate: justProcessedDate})
      }

      console.log(`🔃 ✉ "${messageValues.subject}" - ${messageValues.date}`)
      return processedMessage
    } catch (err) {
      console.error(`FetchMessagesInFolder: Could not build message`, {
        err,
        imapMessage,
        desiredParts,
      })

      // Keep track of uids we failed to fetch
      const {failedUIDs = []} = folder.syncState
      const {uid} = imapMessage.attributes
      if (uid) {
        await folder.updateSyncState({failedUIDs: _.uniq(failedUIDs.concat([uid]))})
      }

      // Save parse errors for future debugging
      const outJSON = JSON.stringify({imapMessage, desiredParts, result: {}});
      const outDir = path.join(os.tmpdir(), "k2-parse-errors", folder.name)
      const outFile = path.join(outDir, imapMessage.attributes.uid.toString());
      mkdirp.sync(outDir);
      fs.writeFileSync(outFile, outJSON);
      return null
    }
  }

  // Replaces ["<rfc2822messageid>", ...] with [[object Reference], ...]
  // Creates references that do not yet exist, and adds the correct
  // associations as well
  async _addReferences(db, message, thread, references) {
    const {Reference} = db;

    let existingReferences = [];
    if (references.length > 0) {
      existingReferences = await Reference.findAll({
        where: {
          rfc2822MessageId: references,
        },
      });
    }

    const refByMessageId = {};
    for (const ref of existingReferences) {
      refByMessageId[ref.rfc2822MessageId] = ref;
    }
    for (const mid of references) {
      if (!refByMessageId[mid]) {
        refByMessageId[mid] = await Reference.create({rfc2822MessageId: mid, threadId: thread.id});
      }
    }

    const referencesInstances = references.map(mid => refByMessageId[mid]);
    message.addReferences(referencesInstances);
    message.referencesOrder = referencesInstances.map(ref => ref.id);
    thread.addReferences(referencesInstances);
  }

  async _processNewMessage(messageValues, struct) {
    const {accountId} = messageValues;
    const db = await LocalDatabaseConnector.forAccount(accountId);
    const {Message} = db

    const existingMessage = await Message.findById(messageValues.id)
    if (existingMessage) {
      // This is an extremely rare case when 2 or more /new/ messages with
      // the exact same headers were queued for creation (same subject,
      // participants, timestamp, and message-id header). In this case, we
      // will ignore it and report the error
      console.warn('MessageProcessor: Encountered 2 new messages with the same id', messageValues)
      return null
    }
    const thread = await detectThread({db, messageValues});
    messageValues.threadId = thread.id;
    const createdMessage = await Message.create(messageValues);

    if (messageValues.labels) {
      await createdMessage.addLabels(messageValues.labels)
      // Note that the labels aren't officially associated until save() is called later
    }

    await this._addReferences(db, createdMessage, thread, messageValues.references);

    // TODO: need to delete dangling references somewhere (maybe at the
    // end of the sync loop?)

    const files = await extractFiles({db, messageValues, struct});
    if (files.length > 0 && !thread.hasAttachments) {
      thread.hasAttachments = true;
      await thread.save();
    }
    await extractContacts({db, messageValues});

    createdMessage.isProcessed = true;
    await createdMessage.save()
    return createdMessage
  }

  /**
   * When we send a message we store an incomplete copy in the local
   * database while we wait for the sync loop to discover the actually
   * delivered one. We store this to keep track of our delivered state and
   * to ensure it's in the sent folder.
   *
   * We also get already processed messages because they may have had their
   * folders or labels changed or had some other property updated with them.
   *
   * It'll have the basic ID, but no thread, labels, etc.
   */
  async _processExistingMessage(existingMessage, parsedMessage, struct) {
    const {accountId} = parsedMessage;
    const db = await LocalDatabaseConnector.forAccount(accountId);
    await existingMessage.update(parsedMessage);
    if (parsedMessage.labels && parsedMessage.labels.length > 0) {
      await existingMessage.setLabels(parsedMessage.labels)
    }

    let thread = await existingMessage.getThread();
    if (!existingMessage.isProcessed) {
      if (!thread) {
        thread = await detectThread({db, messageValues: parsedMessage});
        existingMessage.threadId = thread.id;
      }
      const files = await extractFiles({db, messageValues: existingMessage, struct});
      if (files.length > 0 && !thread.hasAttachments) {
        thread.hasAttachments = true;
        await thread.save();
      }
      await extractContacts({db, messageValues: existingMessage});
      existingMessage.isProcessed = true;
    } else {
      if (!thread) {
        throw new Error(`Existing processed message ${existingMessage.id} doesn't have thread`)
      }
    }

    await existingMessage.save();
    await thread.updateLabelsAndFolders();
    return existingMessage
  }
}

module.exports = new MessageProcessor()
<commit_msg>[local-sync] Correctly add references when processing existing messages<commit_after>const _ = require('underscore')
const os = require('os');
const fs = require('fs');
const path = require('path')
const mkdirp = require('mkdirp');
const detectThread = require('./detect-thread');
const extractFiles = require('./extract-files');
const extractContacts = require('./extract-contacts');
const MessageFactory = require('../shared/message-factory')
const LocalDatabaseConnector = require('../shared/local-database-connector');

const MAX_QUEUE_LENGTH = 500
const PROCESSING_DELAY = 0

class MessageProcessor {

  constructor() {
    // The queue is a chain of Promises
    this._queue = Promise.resolve()
    this._queueLength = 0
  }

  queueLength() {
    return this._queueLength
  }

  queueIsFull() {
    return this._queueLength >= MAX_QUEUE_LENGTH
  }

  /**
   * @returns Promise that resolves when message has been processed
   * This promise will never reject, given that this function is meant to be
   * called as a fire and forget operation
   * If message processing fails, we will register the failure in the folder
   * syncState
   */
  queueMessageForProcessing({accountId, folderId, imapMessage, struct, desiredParts}) {
    return new Promise((resolve) => {
      this._queueLength++
      this._queue = this._queue.then(async () => {
        await this._processMessage({accountId, folderId, imapMessage, struct, desiredParts})
        this._queueLength--

        // To save memory, we reset the Promise chain if the queue reaches a
        // length of 0, otherwise we will continue referencing the entire chain
        // of promises that came before
        if (this._queueLength === 0) {
          this._queue = Promise.resolve()
        }
        resolve()

        // Throttle message processing to meter cpu usage
        await new Promise(r => setTimeout(r, PROCESSING_DELAY))
      })
    })
  }

  async _processMessage({accountId, folderId, imapMessage, struct, desiredParts}) {
    const db = await LocalDatabaseConnector.forAccount(accountId);
    const {Message, Folder} = db
    const folder = await Folder.findById(folderId)
    try {
      const messageValues = await MessageFactory.parseFromImap(imapMessage, desiredParts, {
        db,
        folder,
        accountId,
      });
      const existingMessage = await Message.find({where: {id: messageValues.id}});
      let processedMessage;
      if (existingMessage) {
        // TODO: optimize to not do a full message parse for existing messages
        processedMessage = await this._processExistingMessage(existingMessage, messageValues, struct)
      } else {
        processedMessage = await this._processNewMessage(messageValues, struct)
      }

      // Inflate the serialized oldestProcessedDate value, if it exists
      let oldestProcessedDate;
      if (folder.syncState && folder.syncState.oldestProcessedDate) {
        oldestProcessedDate = new Date(folder.syncState.oldestProcessedDate);
      }
      const justProcessedDate = messageValues.date ? new Date(messageValues.date) : new Date()

      // Update the oldestProcessedDate if:
      //   a) justProcessedDate is after the year 1980. We don't want to base this
      //      off of messages with borked 1970 dates.
      // AND
      //   b) i) We haven't set oldestProcessedDate yet
      //     OR
      //      ii) justProcessedDate is before oldestProcessedDate and in a different
      //          month. (We only use this to update the sync status in Nylas Mail,
      //          which uses month precision. Updating a folder's syncState triggers
      //          many re-renders in Nylas Mail, so we only do it as necessary.)
      if (justProcessedDate > new Date("1980") && (
            !oldestProcessedDate || (
              (justProcessedDate.getMonth() !== oldestProcessedDate.getMonth() ||
                justProcessedDate.getFullYear() !== oldestProcessedDate.getFullYear()) &&
              justProcessedDate < oldestProcessedDate))) {
        await folder.updateSyncState({oldestProcessedDate: justProcessedDate})
      }

      console.log(`🔃 ✉ "${messageValues.subject}" - ${messageValues.date}`)
      return processedMessage
    } catch (err) {
      console.error(`FetchMessagesInFolder: Could not build message`, {
        err,
        imapMessage,
        desiredParts,
      })

      // Keep track of uids we failed to fetch
      const {failedUIDs = []} = folder.syncState
      const {uid} = imapMessage.attributes
      if (uid) {
        await folder.updateSyncState({failedUIDs: _.uniq(failedUIDs.concat([uid]))})
      }

      // Save parse errors for future debugging
      const outJSON = JSON.stringify({imapMessage, desiredParts, result: {}});
      const outDir = path.join(os.tmpdir(), "k2-parse-errors", folder.name)
      const outFile = path.join(outDir, imapMessage.attributes.uid.toString());
      mkdirp.sync(outDir);
      fs.writeFileSync(outFile, outJSON);
      return null
    }
  }

  // Replaces ["<rfc2822messageid>", ...] with [[object Reference], ...]
  // Creates references that do not yet exist, and adds the correct
  // associations as well
  async _addReferences(db, message, thread, references) {
    const {Reference} = db;

    let existingReferences = [];
    if (references.length > 0) {
      existingReferences = await Reference.findAll({
        where: {
          rfc2822MessageId: references,
        },
      });
    }

    const refByMessageId = {};
    for (const ref of existingReferences) {
      refByMessageId[ref.rfc2822MessageId] = ref;
    }
    for (const mid of references) {
      if (!refByMessageId[mid]) {
        refByMessageId[mid] = await Reference.create({rfc2822MessageId: mid, threadId: thread.id});
      }
    }

    const referencesInstances = references.map(mid => refByMessageId[mid]);
    message.addReferences(referencesInstances);
    message.referencesOrder = referencesInstances.map(ref => ref.id);
    thread.addReferences(referencesInstances);
  }

  async _processNewMessage(messageValues, struct) {
    const {accountId} = messageValues;
    const db = await LocalDatabaseConnector.forAccount(accountId);
    const {Message} = db

    const existingMessage = await Message.findById(messageValues.id)
    if (existingMessage) {
      // This is an extremely rare case when 2 or more /new/ messages with
      // the exact same headers were queued for creation (same subject,
      // participants, timestamp, and message-id header). In this case, we
      // will ignore it and report the error
      console.warn('MessageProcessor: Encountered 2 new messages with the same id', messageValues)
      return null
    }
    const thread = await detectThread({db, messageValues});
    messageValues.threadId = thread.id;
    const createdMessage = await Message.create(messageValues);

    if (messageValues.labels) {
      await createdMessage.addLabels(messageValues.labels)
      // Note that the labels aren't officially associated until save() is called later
    }

    await this._addReferences(db, createdMessage, thread, messageValues.references);

    // TODO: need to delete dangling references somewhere (maybe at the
    // end of the sync loop?)

    const files = await extractFiles({db, messageValues, struct});
    if (files.length > 0 && !thread.hasAttachments) {
      thread.hasAttachments = true;
      await thread.save();
    }
    await extractContacts({db, messageValues});

    createdMessage.isProcessed = true;
    await createdMessage.save()
    return createdMessage
  }

  /**
   * When we send a message we store an incomplete copy in the local
   * database while we wait for the sync loop to discover the actually
   * delivered one. We store this to keep track of our delivered state and
   * to ensure it's in the sent folder.
   *
   * We also get already processed messages because they may have had their
   * folders or labels changed or had some other property updated with them.
   *
   * It'll have the basic ID, but no thread, labels, etc.
   */
  async _processExistingMessage(existingMessage, parsedMessage, struct) {
    const {accountId} = parsedMessage;
    const db = await LocalDatabaseConnector.forAccount(accountId);
    await existingMessage.update(parsedMessage);
    if (parsedMessage.labels && parsedMessage.labels.length > 0) {
      await existingMessage.setLabels(parsedMessage.labels)
    }

    let thread = await existingMessage.getThread();
    if (!existingMessage.isProcessed) {
      if (!thread) {
        thread = await detectThread({db, messageValues: parsedMessage});
        existingMessage.threadId = thread.id;
      }
      await this._addReferences(db, existingMessage, thread, parsedMessage.references);
      const files = await extractFiles({db, messageValues: existingMessage, struct});
      if (files.length > 0 && !thread.hasAttachments) {
        thread.hasAttachments = true;
        await thread.save();
      }
      await extractContacts({db, messageValues: existingMessage});
      existingMessage.isProcessed = true;
    } else {
      if (!thread) {
        throw new Error(`Existing processed message ${existingMessage.id} doesn't have thread`)
      }
    }

    await existingMessage.save();
    await thread.updateLabelsAndFolders();
    return existingMessage
  }
}

module.exports = new MessageProcessor()
<|endoftext|>


<commit_before>import EventEmitter from 'events';
import invariant from 'invariant';

export default (
  repository,
  {
    methods = [
      'findById',
      'findOne',
      'findMany',
      'createOne',
      'createMany',
      'updateOne',
      'updateMany',
      'replaceOne',
      'deleteOne',
      'deleteMany',
      'save',
    ],
    emitter,
  },
) => {
  invariant(emitter instanceof EventEmitter, 'emitter has to be an instance of EventEmitter!');

  methods.forEach(method => {
    const previous = repository[method];
    Object.assign(repository, {
      [method]: async (...args) => {
        emitter.emit(`before:${method}`, ...args);
        const result = await previous.call(repository, ...args);
        emitter.emit(`after:${method}`, result, ...args);
        return result;
      },
    });
  });

  return repository;
};
<commit_msg>[oors-mongodb] withEvents decorator namespace support<commit_after>import EventEmitter from 'events';
import invariant from 'invariant';

export default (
  repository,
  {
    methods = [
      'findById',
      'findOne',
      'findMany',
      'createOne',
      'createMany',
      'updateOne',
      'updateMany',
      'replaceOne',
      'deleteOne',
      'deleteMany',
      'save',
    ],
    emitter,
    prefix = '',
  },
) => {
  invariant(emitter instanceof EventEmitter, 'emitter has to be an instance of EventEmitter!');

  methods.forEach(method => {
    const previous = repository[method];
    Object.assign(repository, {
      [method]: async (...args) => {
        emitter.emit(`${prefix}before:${method}`, ...args);
        const result = await previous.call(repository, ...args);
        emitter.emit(`${prefix}after:${method}`, result, ...args);
        return result;
      },
    });
  });

  return repository;
};
<|endoftext|>