<commit_before>import { Class } from '../core/Core';
import '../core/Array';  // For Array#erase
import Obj from '../foundation/Object';
import '../foundation/EventTarget';  // For Function#on

/**
    Module: IO

    The IO module provides classes for two-way communication with a server.
*/

/**
    Class: O.IOQueue

    Extends: O.Object

    Manage concurrent HTTP requests.
*/

const QUEUE = 1;
const IGNORE = 2;
const ABORT = 3;

const IOQueue = Class({

    Extends: Obj,

    /**
        Property (private): O.IOQueue#_queue
        Type: Array

        Queue of request objects waiting for current transactions to finish.
    */

    /**
        Property: O.IOQueue#_recent
        Type: (O.HttpRequest|null)

        A reference to the most recent request.
    */

    /**
        Property: O.IOQueue#activeConnections
        Type: Number

        The number of active connections
    */

    /**
        Property: O.IOQueue#link
        Type: Number
        Default: O.IOQueue.QUEUE

        The property is used to determine what to do if a request is made and
        there are already the maximum allowed number of connections. Accepted
        values are the constants IOQueue.QUEUE, IOQueue.IGNORE and
        IOQueue.ABORT.

        * QUEUE: adds the request to a queue and then waits for the next active
          connection to finish before dispatching the oldest waiting request
          and so on until the queue is empty.
        * IGNORE: ignores the request if there are no free connections.
        * ABORT: aborts the most recent active request and immediately
          dispatches the new request.
    */
    link: QUEUE,

    /**
        Property: O.IOQueue#maxConnections
        Type: Number
        Default: 1

        The maximum number of concurrent connections to make with this IOQueue
        object. Note, this is a per-instance value; each IOQueue instance may
        make up to maxConnections to the server as defined on that object.
    */
    maxConnections: 1,

    /**
        Constructor: O.IOQueue

        Parameters:
            ...mixins - {Object} An object containing new defaults for any of
                        the public properties defined on the object. Can also
                        contain methods to override the normal methods to create
                        an anonymous subclass.
    */
    // eslint-disable-next-line object-shorthand
    init: function (/* ...mixins */) {
        this._queue = [];
        this._recent = null;
        this.activeConnections = 0;

        IOQueue.parent.constructor.apply( this, arguments );
    },

    /**
        Method: O.IOQueue#send

        If the number of active requests is equal to the maximum allowed number
        of concurrent connections, the request will be queued, ignored or cause
        the most recent active request to abort as specified in the
        <O.IOQueue#link> property.

        Parameters:
            request - {O.HttpRequest}

        Returns:
            {O.IOQueue} Returns self.
    */
    send ( request ) {
        if ( this.get( 'activeConnections' ) >= this.get( 'maxConnections' ) ) {
            switch ( this.get( 'link' ) ) {
                case QUEUE:
                    this._queue.push( request );
                    /* falls through */
                case IGNORE:
                    return this;
                case ABORT:
                    this._recent.abort();
                    break;
                default:
                    throw new Error( 'Invalid O.IOQueue link type.' );
            }
        }

        this.increment( 'activeConnections', 1 );

        // If already set, presume it will bubble to us
        if ( !request.get( 'nextEventTarget' ) ) {
            request.set( 'nextEventTarget', this );
        }

        // Store reference in case we need to abort a request.
        this._recent = request.send();

        return this;
    },

    /**
        Method: O.IOQueue#abort

        Abort the request if it is currently running, or remove it from the
        waiting queue if it has not yet run.

        Parameters:
            request - {O.HttpRequest}

        Returns:
            {O.IOQueue} Returns self.
    */
    abort ( request ) {
        this._queue.erase( request );
        request.abort();
        return this;
    },

    /**
        Method (private): O.IOQueue#_complete

        Cleans up any state set by the IOQueue methods on the Transport object
        and starts the next request in the queue, if any.

        Parameters:
            transport - {Transport} The transport object.
    */
    _complete: function ( event ) {
        const request = event.target;
        if ( this._recent === request ) {
            this._recent = null;
        }
        if ( request.get( 'nextEventTarget' ) === this ) {
            request.set( 'nextEventTarget', null );
        }
        this.increment( 'activeConnections', -1 );

        if ( this._queue.length ) {
            this.send( this._queue.shift() );
        }
    }.on( 'io:end' ),
});

IOQueue.QUEUE = 1;
IOQueue.IGNORE = 2;
IOQueue.ABORT = 3;

export default IOQueue;
<commit_msg>Export IOQueue.* constants individually<commit_after>import { Class } from '../core/Core';
import '../core/Array';  // For Array#erase
import Obj from '../foundation/Object';
import '../foundation/EventTarget';  // For Function#on

/**
    Module: IO

    The IO module provides classes for two-way communication with a server.
*/

/**
    Class: O.IOQueue

    Extends: O.Object

    Manage concurrent HTTP requests.
*/

const QUEUE = 1;
const IGNORE = 2;
const ABORT = 3;

const IOQueue = Class({

    Extends: Obj,

    /**
        Property (private): O.IOQueue#_queue
        Type: Array

        Queue of request objects waiting for current transactions to finish.
    */

    /**
        Property: O.IOQueue#_recent
        Type: (O.HttpRequest|null)

        A reference to the most recent request.
    */

    /**
        Property: O.IOQueue#activeConnections
        Type: Number

        The number of active connections
    */

    /**
        Property: O.IOQueue#link
        Type: Number
        Default: O.IOQueue.QUEUE

        The property is used to determine what to do if a request is made and
        there are already the maximum allowed number of connections. Accepted
        values are the constants IOQueue.QUEUE, IOQueue.IGNORE and
        IOQueue.ABORT.

        * QUEUE: adds the request to a queue and then waits for the next active
          connection to finish before dispatching the oldest waiting request
          and so on until the queue is empty.
        * IGNORE: ignores the request if there are no free connections.
        * ABORT: aborts the most recent active request and immediately
          dispatches the new request.
    */
    link: QUEUE,

    /**
        Property: O.IOQueue#maxConnections
        Type: Number
        Default: 1

        The maximum number of concurrent connections to make with this IOQueue
        object. Note, this is a per-instance value; each IOQueue instance may
        make up to maxConnections to the server as defined on that object.
    */
    maxConnections: 1,

    /**
        Constructor: O.IOQueue

        Parameters:
            ...mixins - {Object} An object containing new defaults for any of
                        the public properties defined on the object. Can also
                        contain methods to override the normal methods to create
                        an anonymous subclass.
    */
    // eslint-disable-next-line object-shorthand
    init: function (/* ...mixins */) {
        this._queue = [];
        this._recent = null;
        this.activeConnections = 0;

        IOQueue.parent.constructor.apply( this, arguments );
    },

    /**
        Method: O.IOQueue#send

        If the number of active requests is equal to the maximum allowed number
        of concurrent connections, the request will be queued, ignored or cause
        the most recent active request to abort as specified in the
        <O.IOQueue#link> property.

        Parameters:
            request - {O.HttpRequest}

        Returns:
            {O.IOQueue} Returns self.
    */
    send ( request ) {
        if ( this.get( 'activeConnections' ) >= this.get( 'maxConnections' ) ) {
            switch ( this.get( 'link' ) ) {
                case QUEUE:
                    this._queue.push( request );
                    /* falls through */
                case IGNORE:
                    return this;
                case ABORT:
                    this._recent.abort();
                    break;
                default:
                    throw new Error( 'Invalid O.IOQueue link type.' );
            }
        }

        this.increment( 'activeConnections', 1 );

        // If already set, presume it will bubble to us
        if ( !request.get( 'nextEventTarget' ) ) {
            request.set( 'nextEventTarget', this );
        }

        // Store reference in case we need to abort a request.
        this._recent = request.send();

        return this;
    },

    /**
        Method: O.IOQueue#abort

        Abort the request if it is currently running, or remove it from the
        waiting queue if it has not yet run.

        Parameters:
            request - {O.HttpRequest}

        Returns:
            {O.IOQueue} Returns self.
    */
    abort ( request ) {
        this._queue.erase( request );
        request.abort();
        return this;
    },

    /**
        Method (private): O.IOQueue#_complete

        Cleans up any state set by the IOQueue methods on the Transport object
        and starts the next request in the queue, if any.

        Parameters:
            transport - {Transport} The transport object.
    */
    _complete: function ( event ) {
        const request = event.target;
        if ( this._recent === request ) {
            this._recent = null;
        }
        if ( request.get( 'nextEventTarget' ) === this ) {
            request.set( 'nextEventTarget', null );
        }
        this.increment( 'activeConnections', -1 );

        if ( this._queue.length ) {
            this.send( this._queue.shift() );
        }
    }.on( 'io:end' ),
});

IOQueue.QUEUE = QUEUE;
IOQueue.IGNORE = IGNORE;
IOQueue.ABORT = ABORT;

export default IOQueue;
export { QUEUE, IGNORE, ABORT };
<|endoftext|>


<commit_before>/*eslint-env node*/
/*global global*/
/*eslint no-console:0*/

var $               = require('gulp-load-plugins')();
var gulp            = require('gulp');
var router          = require('front-router');
var sequence        = require('run-sequence');

// Builds your entire app once, without starting a server
gulp.task('build', function(cb) {
  sequence('clean', 'writeDevConfig', ['copy', 'copy:foundation', 'sass', 'uglify'], 'copy:templates', 'copy:config', cb);
});

// Copies everything in the client folder except templates, Sass, and JS
gulp.task('copy', function() {
  return gulp.src(global.paths.assets, {
    base: './client/'
  })
    .pipe(gulp.dest('./build'));
});

// Copies your app's page templates and generates URLs for them
gulp.task('copy:templates', function() {
  return gulp.src('./client/templates/**/*.html')
    .pipe(router({
      path: 'build/assets/js/routes.js',
      root: 'client'
    }))
    .pipe(gulp.dest('./build/templates'));
});

// Compiles the Foundation for Apps directive partials into a single JavaScript file
gulp.task('copy:foundation', function(cb) {
  gulp.src('bower_components/foundation-apps/js/angular/components/**/*.html')
    .pipe($.ngHtml2js({
      prefix: 'components/',
      moduleName: 'foundation',
      declareModule: false
    }))
    .pipe($.uglify())
    .pipe($.concat('templates.js'))
    .pipe(gulp.dest('./build/assets/js'));

  // Iconic SVG icons
  gulp.src('./bower_components/foundation-apps/iconic/**/*')
    .pipe(gulp.dest('./build/assets/img/iconic/'));

  cb();
});

// Compiles and copies the Foundation for Apps JavaScript, as well as your app's custom JS
gulp.task('uglify', ['uglify:foundation', 'uglify:app']);

gulp.task('uglify:foundation', function() {
  var uglify = $.if(global.isProduction, $.uglify()
    .on('error', function (e) {
      console.log(e);
    }));

  return gulp.src(global.paths.foundationJS)
    .pipe(uglify)
    .pipe($.concat('foundation.js'))
    .pipe(gulp.dest('./build/assets/js/'));
});

gulp.task('uglify:app', function() {
  var uglify = $.if(global.isProduction, $.uglify()
    .on('error', function (e) {
      console.log(e);
    })),
    sourcemapsInit = $.if(!global.isProduction, $.sourcemaps.init()),
    sourcemapsWrite = $.if(!global.isProduction, $.sourcemaps.write('.'));

  return gulp.src(global.paths.appJS, { base: 'client' })
    .pipe(sourcemapsInit)
    .pipe(uglify)
    .pipe($.plumber())
    .pipe($.ngAnnotate())
    .pipe($.directiveReplace({root: 'client'}))
    .pipe($.concat('app.js'))
    .pipe(sourcemapsWrite)
    .pipe(gulp.dest('./build/assets/js/'));
});
<commit_msg>Fixed copy:config task placement for build task<commit_after>/*eslint-env node*/
/*global global*/
/*eslint no-console:0*/

var $               = require('gulp-load-plugins')();
var gulp            = require('gulp');
var router          = require('front-router');
var sequence        = require('run-sequence');

// Builds your entire app once, without starting a server
gulp.task('build', function(cb) {
  sequence('clean', 'copy:config', 'writeDevConfig', ['copy', 'copy:foundation', 'sass', 'uglify'], 'copy:templates', cb);
});

// Copies everything in the client folder except templates, Sass, and JS
gulp.task('copy', function() {
  return gulp.src(global.paths.assets, {
    base: './client/'
  })
    .pipe(gulp.dest('./build'));
});

// Copies your app's page templates and generates URLs for them
gulp.task('copy:templates', function() {
  return gulp.src('./client/templates/**/*.html')
    .pipe(router({
      path: 'build/assets/js/routes.js',
      root: 'client'
    }))
    .pipe(gulp.dest('./build/templates'));
});

// Compiles the Foundation for Apps directive partials into a single JavaScript file
gulp.task('copy:foundation', function(cb) {
  gulp.src('bower_components/foundation-apps/js/angular/components/**/*.html')
    .pipe($.ngHtml2js({
      prefix: 'components/',
      moduleName: 'foundation',
      declareModule: false
    }))
    .pipe($.uglify())
    .pipe($.concat('templates.js'))
    .pipe(gulp.dest('./build/assets/js'));

  // Iconic SVG icons
  gulp.src('./bower_components/foundation-apps/iconic/**/*')
    .pipe(gulp.dest('./build/assets/img/iconic/'));

  cb();
});

// Compiles and copies the Foundation for Apps JavaScript, as well as your app's custom JS
gulp.task('uglify', ['uglify:foundation', 'uglify:app']);

gulp.task('uglify:foundation', function() {
  var uglify = $.if(global.isProduction, $.uglify()
    .on('error', function (e) {
      console.log(e);
    }));

  return gulp.src(global.paths.foundationJS)
    .pipe(uglify)
    .pipe($.concat('foundation.js'))
    .pipe(gulp.dest('./build/assets/js/'));
});

gulp.task('uglify:app', function() {
  var uglify = $.if(global.isProduction, $.uglify()
    .on('error', function (e) {
      console.log(e);
    })),
    sourcemapsInit = $.if(!global.isProduction, $.sourcemaps.init()),
    sourcemapsWrite = $.if(!global.isProduction, $.sourcemaps.write('.'));

  return gulp.src(global.paths.appJS, { base: 'client' })
    .pipe(sourcemapsInit)
    .pipe(uglify)
    .pipe($.plumber())
    .pipe($.ngAnnotate())
    .pipe($.directiveReplace({root: 'client'}))
    .pipe($.concat('app.js'))
    .pipe(sourcemapsWrite)
    .pipe(gulp.dest('./build/assets/js/'));
});
<|endoftext|>


<commit_before>/**
 * Bridge proxy
 *
 */

function WSBridgeProxy(config){

	// Simple websocket server

	var port = config.port;
	var master=null;

	(new (require('ws').Server)({
		port: port
	})).on('connection', function(wsclient){

		if(master===null){
			master=wsclient;
			console.log('connected a master');
		}else{

			var a=master;
			master=null;
			var b=wsclient;
			console.log('connected a client');

			a.on('message', function message(data, flags) {
				console.log('master sent '+data);
				b.send(data);
			}).on('error',function(error){
				console.log('a error: '+error)
			}).on('close',function(code, message){
				console.log('a close: '+code+' '+message);
				a=null;
				if(b){
					b.close();
				}
			});

			b.on('message', function message(data, flags) {
				console.log('client sent '+data);
				a.send(data);

			}).on('error',function(error){
				console.log('b error: '+error)
			}).on('close',function(code, message){
				console.log('b close: '+code+' '+message);
				b=null;
				if(a){
					a.close();
				}

			});

		}

	}).on('error', function(error){
		console.log('error: '+error);
	})

	console.log('websocket listening on: '+port);

};

module.exports=WSBridgeProxy;

//console.log(argv);

if(process.argv){

	console.log(process.argv);

	var fs=require('fs');
	fs.realpath(process.argv[1],function(err, p1){

		fs.realpath(__filename,function(err, p2){

			console.log(p1+' '+p2);

			if(p1===p2){

				if(process.argc==3){
					new WSBridgeProxy({port:parseInt(process.argv[2])});
				}else{
					new WSBridgeProxy(require('./bridgeproxy.json'));
				}

			}

		});
	});
}

<commit_msg>add headers check<commit_after>/**
 * Bridge proxy
 *
 */

function WSBridgeProxy(config){

	// Simple websocket server

	var port = config.port;
	var master=null;

	(new (require('ws').Server)({
		port: port
	})).on('headers',function(h){
		console.log(JSON.stringify(h));
	})on('connection', function(wsclient){

		if(master===null){
			master=wsclient;
			console.log('connected a master');
		}else{

			var a=master;
			master=null;
			var b=wsclient;
			console.log('connected a client');

			a.on('message', function message(data, flags) {
				console.log('master sent '+data);
				b.send(data);
			}).on('error',function(error){
				console.log('a error: '+error)
			}).on('close',function(code, message){
				console.log('a close: '+code+' '+message);
				a=null;
				if(b){
					b.close();
				}
			});

			b.on('message', function message(data, flags) {
				console.log('client sent '+data);
				a.send(data);

			}).on('error',function(error){
				console.log('b error: '+error)
			}).on('close',function(code, message){
				console.log('b close: '+code+' '+message);
				b=null;
				if(a){
					a.close();
				}

			});

		}

	}).on('error', function(error){
		console.log('error: '+error);
	})

	console.log('websocket listening on: '+port);

};

module.exports=WSBridgeProxy;

//console.log(argv);

if(process.argv){

	console.log(process.argv);

	var fs=require('fs');
	fs.realpath(process.argv[1],function(err, p1){

		fs.realpath(__filename,function(err, p2){

			console.log(p1+' '+p2);

			if(p1===p2){

				if(process.argc==3){
					new WSBridgeProxy({port:parseInt(process.argv[2])});
				}else{
					new WSBridgeProxy(require('./bridgeproxy.json'));
				}

			}

		});
	});
}

<|endoftext|>


<commit_before>'use strict';

var gulp = require('gulp');

var paths = gulp.paths;

var $ = require('gulp-load-plugins')({
  pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del']
});

gulp.task('partials', function () {
  return gulp.src([
    paths.src + '/{app,components}/**/*.html',
    paths.tmp + '/{app,components}/**/*.html'
  ])
    .pipe($.minifyHtml({
      empty: true,
      spare: true,
      quotes: true
    }))
    .pipe($.angularTemplatecache('templateCacheHtml.js', {
      module: 'vleApp'
    }))
    .pipe(gulp.dest(paths.tmp + '/partials/'));
});

gulp.task('html', ['inject', 'partials'], function () {
  var partialsInjectFile = gulp.src(paths.tmp + '/partials/templateCacheHtml.js', { read: false });
  var partialsInjectOptions = {
    starttag: '<!-- inject:partials -->',
    ignorePath: paths.tmp + '/partials',
    addRootSlash: false
  };

  var htmlFilter = $.filter('*.html');
  var jsFilter = $.filter('**/*.js');
  var cssFilter = $.filter('**/*.css');
  var assets;

  return gulp.src(paths.tmp + '/serve/*.html')
    .pipe($.inject(partialsInjectFile, partialsInjectOptions))
    .pipe(assets = $.useref.assets())
    .pipe($.rev())
    .pipe(jsFilter)
    .pipe($.ngAnnotate())
    .pipe($.uglify({preserveComments: $.uglifySaveLicense}))
    .pipe(jsFilter.restore())
    .pipe(cssFilter)
    .pipe($.replace('../../bower_components/fontawesome/fonts', '../fonts'))
    .pipe($.csso())
    .pipe(cssFilter.restore())
    .pipe(assets.restore())
    .pipe($.useref())
    .pipe($.revReplace())
    .pipe(htmlFilter)
    .pipe($.minifyHtml({
      empty: true,
      spare: true,
      quotes: true
    }))
    .pipe(htmlFilter.restore())
    .pipe(gulp.dest(paths.dist + '/'))
    .pipe($.size({ title: paths.dist + '/', showFiles: true }));
});

gulp.task('assets', function () {
  return gulp.src(paths.src + '/assets/**/*')
    .pipe(gulp.dest(paths.dist + '/assets/'));
});

gulp.task('data', function () {
  return gulp.src(paths.src + '/data/*')
    .pipe(gulp.dest(paths.dist + '/data/'));
});

gulp.task('fonts', function () {
  return gulp.src($.mainBowerFiles())
    .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
    .pipe($.flatten())
    .pipe(gulp.dest(paths.dist + '/fonts/'));
});

gulp.task('misc', function () {
  return gulp.src(paths.src + '/**/*.ico')
    .pipe(gulp.dest(paths.dist + '/'));
});

gulp.task('zeroclipboard', function () {
  return gulp.src('bower_components/zeroclipboard/dist/ZeroClipboard.swf')
    .pipe(gulp.dest(paths.dist + '/bower_components/zeroclipboard/dist/'));
});

gulp.task('clean', function (done) {
  $.del([paths.dist + '/', paths.tmp + '/'], done);
});

gulp.task('build', ['copydep', 'html', 'assets', 'data', 'fonts', 'misc', 'zeroclipboard']);
<commit_msg>update build.js using the new app name,<commit_after>'use strict';

var gulp = require('gulp');

var paths = gulp.paths;

var $ = require('gulp-load-plugins')({
  pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del']
});

gulp.task('partials', function () {
  return gulp.src([
    paths.src + '/{app,components}/**/*.html',
    paths.tmp + '/{app,components}/**/*.html'
  ])
    .pipe($.minifyHtml({
      empty: true,
      spare: true,
      quotes: true
    }))
    .pipe($.angularTemplatecache('templateCacheHtml.js', {
      module: 'polestar'
    }))
    .pipe(gulp.dest(paths.tmp + '/partials/'));
});

gulp.task('html', ['inject', 'partials'], function () {
  var partialsInjectFile = gulp.src(paths.tmp + '/partials/templateCacheHtml.js', { read: false });
  var partialsInjectOptions = {
    starttag: '<!-- inject:partials -->',
    ignorePath: paths.tmp + '/partials',
    addRootSlash: false
  };

  var htmlFilter = $.filter('*.html');
  var jsFilter = $.filter('**/*.js');
  var cssFilter = $.filter('**/*.css');
  var assets;

  return gulp.src(paths.tmp + '/serve/*.html')
    .pipe($.inject(partialsInjectFile, partialsInjectOptions))
    .pipe(assets = $.useref.assets())
    .pipe($.rev())
    .pipe(jsFilter)
    .pipe($.ngAnnotate())
    .pipe($.uglify({preserveComments: $.uglifySaveLicense}))
    .pipe(jsFilter.restore())
    .pipe(cssFilter)
    .pipe($.replace('../../bower_components/fontawesome/fonts', '../fonts'))
    .pipe($.csso())
    .pipe(cssFilter.restore())
    .pipe(assets.restore())
    .pipe($.useref())
    .pipe($.revReplace())
    .pipe(htmlFilter)
    .pipe($.minifyHtml({
      empty: true,
      spare: true,
      quotes: true
    }))
    .pipe(htmlFilter.restore())
    .pipe(gulp.dest(paths.dist + '/'))
    .pipe($.size({ title: paths.dist + '/', showFiles: true }));
});

gulp.task('assets', function () {
  return gulp.src(paths.src + '/assets/**/*')
    .pipe(gulp.dest(paths.dist + '/assets/'));
});

gulp.task('data', function () {
  return gulp.src(paths.src + '/data/*')
    .pipe(gulp.dest(paths.dist + '/data/'));
});

gulp.task('fonts', function () {
  return gulp.src($.mainBowerFiles())
    .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
    .pipe($.flatten())
    .pipe(gulp.dest(paths.dist + '/fonts/'));
});

gulp.task('misc', function () {
  return gulp.src(paths.src + '/**/*.ico')
    .pipe(gulp.dest(paths.dist + '/'));
});

gulp.task('zeroclipboard', function () {
  return gulp.src('bower_components/zeroclipboard/dist/ZeroClipboard.swf')
    .pipe(gulp.dest(paths.dist + '/bower_components/zeroclipboard/dist/'));
});

gulp.task('clean', function (done) {
  $.del([paths.dist + '/', paths.tmp + '/'], done);
});

gulp.task('build', ['copydep', 'html', 'assets', 'data', 'fonts', 'misc', 'zeroclipboard']);
<|endoftext|>


<commit_before>var BrowserView = function(args) {
  var canvas = document.querySelector('canvas');

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

  this.context = document.querySelector('canvas').getContext('2d');
  this.gameBoard = args.gameBoard;
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };

  addEventListener('keydown', this.keyDown.bind(this));
  addEventListener('keyup', this.keyUp.bind(this));
}
BrowserView.prototype.keyDown = function(event) {
  var pressedKey = KEY_CODES[event.keyCode];
  if(pressedKey == 'left' || pressedKey == 'right') {
    event.preventDefault();
    if(this.pressed.slide == false) {
      this.pressed.slide = pressedKey;
      this.handleInput.bind(this).call();
    }
  }
  if(pressedKey == 'down') {
    event.preventDefault();
    if(this.pressed.drop == false) {
      this.pressed.drop = true;
      this.handleInput.bind(this).call();
    }
  }
  if(pressedKey == 'up') {
    event.preventDefault();
    if(this.pressed.rotate == false) {
      this.pressed.rotate = true;
      this.handleInput.bind(this).call();
    }
  }
};
BrowserView.prototype.keyUp = function(event){
  var releasedKey = KEY_CODES[event.keyCode];
  if(releasedKey == 'left' || releasedKey == 'right') {
    event.preventDefault();
    this.pressed.slide = false;
  }
  if(releasedKey == 'down') {
    event.preventDefault();
    this.pressed.drop = false;
    this.gameBoard.cycleDropBlock();
  }
  if(releasedKey == 'up') {
    event.preventDefault();
    this.pressed.rotate = false;
  }
};
BrowserView.prototype.handleInput = function() {
  if(this.pressed.slide) {
    this.gameBoard.slideBlock(this.pressed.slide);
    setTimeout(this.handleInput.bind(this), INPUT_DELAY);
  }
  if(this.pressed.drop) {
    this.gameBoard.cycleDropBlock({quickly: true});
  }
  if(this.pressed.rotate) {
    this.gameBoard.rotateBlock('counter');
    setTimeout(this.handleInput.bind(this), INPUT_DELAY);
  }
};
BrowserView.prototype.drawBoard = function(board) {
  var context = this.context
  context.lineWidth = 4;
  context.font = BLOCK_FONT;

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

      context.fillRect((cIndex * BLOCK_SPACING_WIDTH) + 5, (rIndex * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
      context.strokeRect((cIndex * BLOCK_SPACING_WIDTH) + 5, (rIndex * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
      context.fillStyle = CHEMICAL_ELEMENTS[col]['color'];
      context.fillText(CHEMICAL_ELEMENTS[col].symbol, (cIndex * BLOCK_SPACING_WIDTH) + (BLOCK_SPACING_WIDTH / 2) - 4, (rIndex * BLOCK_SPACING_HEIGHT) + (BLOCK_SPACING_HEIGHT / 2) + 4);
    });
  });
};
BrowserView.prototype.animate = function() {
  var lastTime = null;
  var progress = true;
  var self = this;

  var animate = function(time) {
    if(lastTime) {
      var timeStep = Math.min(time - lastTime, 100) / 1000;
      progress = timeStep < 2000;
    }
    lastTime = time;
    self.drawBoard(self.gameBoard.board);
    if(progress) {
      requestAnimationFrame(animate);
    }
  }
  requestAnimationFrame(animate);
};
<commit_msg>Refactor BrowserView to use "canvas" variable<commit_after>var BrowserView = function(args) {
  var canvas = document.querySelector('canvas');

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

  this.context = canvas.getContext('2d');
  this.gameBoard = args.gameBoard;
  this.pressed = {
    slide: false,
    drop: false,
    rotate: false
  };

  addEventListener('keydown', this.keyDown.bind(this));
  addEventListener('keyup', this.keyUp.bind(this));
}
BrowserView.prototype.keyDown = function(event) {
  var pressedKey = KEY_CODES[event.keyCode];
  if(pressedKey == 'left' || pressedKey == 'right') {
    event.preventDefault();
    if(this.pressed.slide == false) {
      this.pressed.slide = pressedKey;
      this.handleInput.bind(this).call();
    }
  }
  if(pressedKey == 'down') {
    event.preventDefault();
    if(this.pressed.drop == false) {
      this.pressed.drop = true;
      this.handleInput.bind(this).call();
    }
  }
  if(pressedKey == 'up') {
    event.preventDefault();
    if(this.pressed.rotate == false) {
      this.pressed.rotate = true;
      this.handleInput.bind(this).call();
    }
  }
};
BrowserView.prototype.keyUp = function(event){
  var releasedKey = KEY_CODES[event.keyCode];
  if(releasedKey == 'left' || releasedKey == 'right') {
    event.preventDefault();
    this.pressed.slide = false;
  }
  if(releasedKey == 'down') {
    event.preventDefault();
    this.pressed.drop = false;
    this.gameBoard.cycleDropBlock();
  }
  if(releasedKey == 'up') {
    event.preventDefault();
    this.pressed.rotate = false;
  }
};
BrowserView.prototype.handleInput = function() {
  if(this.pressed.slide) {
    this.gameBoard.slideBlock(this.pressed.slide);
    setTimeout(this.handleInput.bind(this), INPUT_DELAY);
  }
  if(this.pressed.drop) {
    this.gameBoard.cycleDropBlock({quickly: true});
  }
  if(this.pressed.rotate) {
    this.gameBoard.rotateBlock('counter');
    setTimeout(this.handleInput.bind(this), INPUT_DELAY);
  }
};
BrowserView.prototype.drawBoard = function(board) {
  var context = this.context
  context.lineWidth = 4;
  context.font = BLOCK_FONT;

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

      context.fillRect((cIndex * BLOCK_SPACING_WIDTH) + 5, (rIndex * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
      context.strokeRect((cIndex * BLOCK_SPACING_WIDTH) + 5, (rIndex * BLOCK_SPACING_HEIGHT) + 5, BLOCK_WIDTH, BLOCK_HEIGHT);
      context.fillStyle = CHEMICAL_ELEMENTS[col]['color'];
      context.fillText(CHEMICAL_ELEMENTS[col].symbol, (cIndex * BLOCK_SPACING_WIDTH) + (BLOCK_SPACING_WIDTH / 2) - 4, (rIndex * BLOCK_SPACING_HEIGHT) + (BLOCK_SPACING_HEIGHT / 2) + 4);
    });
  });
};
BrowserView.prototype.animate = function() {
  var lastTime = null;
  var progress = true;
  var self = this;

  var animate = function(time) {
    if(lastTime) {
      var timeStep = Math.min(time - lastTime, 100) / 1000;
      progress = timeStep < 2000;
    }
    lastTime = time;
    self.drawBoard(self.gameBoard.board);
    if(progress) {
      requestAnimationFrame(animate);
    }
  }
  requestAnimationFrame(animate);
};
<|endoftext|>


<commit_before>'use strict';

const gulp = require('gulp');
const exec = require('child_process').exec;
const replace = require('gulp-replace');
const clean = require('gulp-clean');
const fs = require('fs');
const glob = require("glob")

gulp.task('clean', function () {
    return gulp.src('dist')
    .pipe(clean({force: true}));
});

gulp.task('create-load', function (done) {

    let loadFileContents = `// Created by gulp create-load
import 'css';
import 'text';
import 'plugin-babel';
`;

    glob('src/{theme,plugins}/**/*.{html,js}', {}, function (er, files) {
        files.forEach((file) => {
            loadFileContents += "import '" + file.replace('src/', 'OpenGroup/').replace('.html', '.html!text') + "';\n";
        });

        fs.writeFile('src/load.js', loadFileContents, function(err) {
            if (err) {
                return console.log(err);
            }

            done();
        });
    });

});

gulp.task('build', ['clean', 'create-load', 'css'], function (done) {

    exec('jspm bundle app.js + load.js dist/build.js', function (err, stdout, stderr) {
        console.log(stdout);
        console.log(stderr);

        fs.writeFile('dist/CNAME', 'opengroup.io', function(err) {
            if (err) {
                return console.log(err);
            }

            done();
        });
    });

    gulp.src('./src/index.html')
    .pipe(replace('<script>System.import("./app.js").catch(console.error.bind(console));</script>', '<script src="build.js"></script><script>System.import("./app.js").catch(console.error.bind(console));</script>'))
    .pipe(gulp.dest('./dist'));

    gulp.src(['src/lib/system.js']).pipe(gulp.dest('./dist/lib/'));
    gulp.src(['src/jspm.config.js']).pipe(gulp.dest('./dist/'));

    gulp.src([
        'src/theme/{css,images}/**/*'
    ])
    .pipe(gulp.dest('./dist/theme/'));
});
<commit_msg>no jekyll<commit_after>'use strict';

const gulp = require('gulp');
const exec = require('child_process').exec;
const replace = require('gulp-replace');
const clean = require('gulp-clean');
const fs = require('fs');
const glob = require("glob")

gulp.task('clean', function () {
    return gulp.src('dist')
    .pipe(clean({force: true}));
});

gulp.task('create-load', function (done) {

    let loadFileContents = `// Created by gulp create-load
import 'css';
import 'text';
import 'plugin-babel';
`;

    glob('src/{theme,plugins}/**/*.{html,js}', {}, function (er, files) {
        files.forEach((file) => {
            loadFileContents += "import '" + file.replace('src/', 'OpenGroup/').replace('.html', '.html!text') + "';\n";
        });

        fs.writeFile('src/load.js', loadFileContents, function(err) {
            if (err) {
                return console.log(err);
            }

            done();
        });
    });

});

gulp.task('build', ['clean', 'create-load', 'css'], function (done) {

    exec('jspm bundle app.js + load.js dist/build.js', function (err, stdout, stderr) {
        console.log(stdout);
        console.log(stderr);

        fs.writeFile('dist/CNAME', 'opengroup.io', function(err) {
            if (err) {
                return console.log(err);
            }

            fs.writeFile('dist/.nojekyll', '', function(err) {
                if (err) {
                    return console.log(err);
                }

                done();
            });
        });
    });

    gulp.src('./src/index.html')
    .pipe(replace('<script>System.import("./app.js").catch(console.error.bind(console));</script>', '<script src="build.js"></script><script>System.import("./app.js").catch(console.error.bind(console));</script>'))
    .pipe(gulp.dest('./dist'));

    gulp.src(['src/lib/system.js']).pipe(gulp.dest('./dist/lib/'));
    gulp.src(['src/jspm.config.js']).pipe(gulp.dest('./dist/'));

    gulp.src([
        'src/theme/{css,images}/**/*'
    ])
    .pipe(gulp.dest('./dist/theme/'));
});
<|endoftext|>


<commit_before>const _ = require('lodash');
const juice = require('juice');
const template = require('./template');
const labsTemplate = require('./template-labs');
const settingsCache = require('../../../shared/settings-cache');
const urlUtils = require('../../../shared/url-utils');
const labs = require('../../../shared/labs');
const moment = require('moment-timezone');
const cheerio = require('cheerio');
const api = require('../../api');
const {URL} = require('url');
const mobiledocLib = require('../../lib/mobiledoc');
const htmlToText = require('html-to-text');
const {isUnsplashImage, isLocalContentImage} = require('@tryghost/kg-default-cards/lib/utils');
const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@tryghost/color-utils');
const logging = require('@tryghost/logging');

const ALLOWED_REPLACEMENTS = ['first_name'];

const getSite = () => {
    const publicSettings = settingsCache.getPublic();
    return Object.assign({}, publicSettings, {
        url: urlUtils.urlFor('home', true),
        iconUrl: publicSettings.icon ? urlUtils.urlFor('image', {image: publicSettings.icon}, true) : null
    });
};

const htmlToPlaintext = (html) => {
    // same options as used in Post model for generating plaintext but without `wordwrap: 80`
    // to avoid replacement strings being split across lines and for mail clients to handle
    // word wrapping based on user preferences
    return htmlToText.fromString(html, {
        wordwrap: false,
        ignoreImage: true,
        hideLinkHrefIfSameAsText: true,
        preserveNewlines: true,
        returnDomByDefault: true,
        uppercaseHeadings: false
    });
};

/**
 * createUnsubscribeUrl
 *
 * Takes a member uuid and returns the url that should be used to unsubscribe
 * In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
 *
 * @param {string} uuid
 */
const createUnsubscribeUrl = (uuid) => {
    const siteUrl = urlUtils.getSiteUrl();
    const unsubscribeUrl = new URL(siteUrl);
    unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
    if (uuid) {
        unsubscribeUrl.searchParams.set('uuid', uuid);
    } else {
        unsubscribeUrl.searchParams.set('preview', '1');
    }

    return unsubscribeUrl.href;
};

// NOTE: serialization is needed to make sure we are using current API and do post transformations
//       such as image URL transformation from relative to absolute
const serializePostModel = async (model, apiVersion = 'v4') => {
    // fetch mobiledoc rather than html and plaintext so we can render email-specific contents
    const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
    const docName = 'posts';

    await api.shared
        .serializers
        .handle
        .output(model, {docName: docName, method: 'read'}, api[apiVersion].serializers.output, frame);

    return frame.response[docName][0];
};

// removes %% wrappers from unknown replacement strings in email content
const normalizeReplacementStrings = (email) => {
    // we don't want to modify the email object in-place
    const emailContent = _.pick(email, ['html', 'plaintext']);

    const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
    const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;

    ['html', 'plaintext'].forEach((format) => {
        emailContent[format] = emailContent[format].replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
            const match = replacementStr.match(REPLACEMENT_STRING_REGEX);

            if (match) {
                const {recipientProperty} = match.groups;

                if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
                    // keeps wrapping %% for later replacement with real data
                    return replacementMatch;
                }
            }

            // removes %% so output matches user supplied content
            return replacementStr;
        });
    });

    return emailContent;
};

/**
 * Parses email content and extracts an array of replacements with desired fallbacks
 *
 * @param {Object} email
 * @param {string} email.html
 * @param {string} email.plaintext
 *
 * @returns {Object[]} replacements
 */
const parseReplacements = (email) => {
    const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
    const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;

    const replacements = [];

    ['html', 'plaintext'].forEach((format) => {
        let result;
        while ((result = EMAIL_REPLACEMENT_REGEX.exec(email[format])) !== null) {
            const [replacementMatch, replacementStr] = result;
            const match = replacementStr.match(REPLACEMENT_STRING_REGEX);

            if (match) {
                const {recipientProperty, fallback} = match.groups;

                if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
                    const id = `replacement_${replacements.length + 1}`;

                    replacements.push({
                        format,
                        id,
                        match: replacementMatch,
                        recipientProperty: `member_${recipientProperty}`,
                        fallback
                    });
                }
            }
        }
    });

    return replacements;
};

const getTemplateSettings = async () => {
    const accentColor = settingsCache.get('accent_color');
    const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
    const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();

    const templateSettings = {
        headerImage: settingsCache.get('newsletter_header_image'),
        showHeaderIcon: settingsCache.get('newsletter_show_header_icon') && settingsCache.get('icon'),
        showHeaderTitle: settingsCache.get('newsletter_show_header_title'),
        showFeatureImage: settingsCache.get('newsletter_show_feature_image'),
        titleFontCategory: settingsCache.get('newsletter_title_font_category'),
        titleAlignment: settingsCache.get('newsletter_title_alignment'),
        bodyFontCategory: settingsCache.get('newsletter_body_font_category'),
        showBadge: settingsCache.get('newsletter_show_badge'),
        footerContent: settingsCache.get('newsletter_footer_content'),
        accentColor,
        adjustedAccentColor,
        adjustedAccentContrastColor
    };

    if (templateSettings.headerImage) {
        if (isUnsplashImage(templateSettings.headerImage)) {
            // Unsplash images have a minimum size so assuming 1200px is safe
            const unsplashUrl = new URL(templateSettings.headerImage);
            unsplashUrl.searchParams.set('w', '1200');

            templateSettings.headerImage = unsplashUrl.href;
            templateSettings.headerImageWidth = 600;
        } else {
            const {imageSize} = require('../../lib/image');
            try {
                const size = await imageSize.getImageSizeFromUrl(templateSettings.headerImage);

                if (size.width >= 600) {
                    // keep original image, just set a fixed width
                    templateSettings.headerImageWidth = 600;
                }

                if (isLocalContentImage(templateSettings.headerImage, urlUtils.getSiteUrl())) {
                    // we can safely request a 1200px image - Ghost will serve the original if it's smaller
                    templateSettings.headerImage = templateSettings.headerImage.replace(/\/content\/images\//, '/content/images/size/w1200/');
                }
            } catch (err) {
                // log and proceed. Using original header image without fixed width isn't fatal.
                logging.error(err);
            }
        }
    }

    return templateSettings;
};

const serialize = async (postModel, options = {isBrowserPreview: false, apiVersion: 'v4'}) => {
    const post = await serializePostModel(postModel, options.apiVersion);

    const timezone = settingsCache.get('timezone');
    const momentDate = post.published_at ? moment(post.published_at) : moment();
    post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');

    post.authors = post.authors && post.authors.map(author => author.name).join(',');
    if (post.posts_meta) {
        post.email_subject = post.posts_meta.email_subject;
    }

    // we use post.excerpt as a hidden piece of text that is picked up by some email
    // clients as a "preview" when listing emails. Our current plaintext/excerpt
    // generation outputs links as "Link [https://url/]" which isn't desired in the preview
    if (!post.custom_excerpt && post.excerpt) {
        post.excerpt = post.excerpt.replace(/\s\[http(.*?)\]/g, '');
    }

    post.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(post.mobiledoc), {target: 'email'});

    // perform any email specific adjustments to the mobiledoc->HTML render output
    let _cheerio = cheerio.load(post.html);
    // remove leading/trailing HRs
    _cheerio(':root > hr:first-child, :root > div:first-child > hr:first-child').remove();
    _cheerio(':root > hr:last-child, :root > div:last-child > hr:last-child').remove();
    post.html = _cheerio.html();

    post.plaintext = htmlToPlaintext(post.html);

    // Outlook will render feature images at full-size breaking the layout.
    // Content images fix this by rendering max 600px images - do the same for feature image here
    if (post.feature_image) {
        if (isUnsplashImage(post.feature_image)) {
            // Unsplash images have a minimum size so assuming 1200px is safe
            const unsplashUrl = new URL(post.feature_image);
            unsplashUrl.searchParams.set('w', '1200');

            post.feature_image = unsplashUrl.href;
            post.feature_image_width = 600;
        } else {
            const {imageSize} = require('../../lib/image');
            try {
                const size = await imageSize.getImageSizeFromUrl(post.feature_image);

                if (size.width >= 600) {
                    // keep original image, just set a fixed width
                    post.feature_image_width = 600;
                }

                if (isLocalContentImage(post.feature_image, urlUtils.getSiteUrl())) {
                    // we can safely request a 1200px image - Ghost will serve the original if it's smaller
                    post.feature_image = post.feature_image.replace(/\/content\/images\//, '/content/images/size/w1200/');
                }
            } catch (err) {
                // log and proceed. Using original feature_image without fixed width isn't fatal.
                logging.error(err);
            }
        }
    }

    const templateSettings = await getTemplateSettings();

    const render = labs.isSet('emailCardSegments') ? labsTemplate : template;

    let htmlTemplate = render({post, site: getSite(), templateSettings});

    if (options.isBrowserPreview) {
        const previewUnsubscribeUrl = createUnsubscribeUrl(null);
        htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
    }

    // Inline css to style attributes, turn on support for pseudo classes.
    const juiceOptions = {inlinePseudoElements: true};
    let juicedHtml = juice(htmlTemplate, juiceOptions);

    // convert juiced HTML to a DOM-like interface for further manipulation
    // happens after inlining of CSS so we can change element types without worrying about styling
    _cheerio = cheerio.load(juicedHtml);
    // force all links to open in new tab
    _cheerio('a').attr('target','_blank');
    // convert figure and figcaption to div so that Outlook applies margins
    _cheerio('figure, figcaption').each((i, elem) => !!(elem.tagName = 'div'));
    juicedHtml = _cheerio.html();

    // Fix any unsupported chars in Outlook
    juicedHtml = juicedHtml.replace(/'/g, ''');

    // Clean up any unknown replacements strings to get our final content
    const {html, plaintext} = normalizeReplacementStrings({
        html: juicedHtml,
        plaintext: post.plaintext
    });

    return {
        subject: post.email_subject || post.title,
        html,
        plaintext
    };
};

function renderEmailForSegment(email, memberSegment) {
    const result = {...email};
    const $ = cheerio.load(result.html);

    $('[data-gh-segment]').get().forEach((node) => {
        if (node.attribs['data-gh-segment'] !== memberSegment) { //TODO: replace with NQL interpretation
            $(node).remove();
        } else {
            // Getting rid of the attribute for a cleaner html output
            $(node).removeAttr('data-gh-segment');
        }
    });
    result.html = $.html();
    result.plaintext = htmlToPlaintext(result.html);

    return result;
}

module.exports = {
    serialize,
    createUnsubscribeUrl,
    renderEmailForSegment,
    parseReplacements
};
<commit_msg>Fixed leading/trailing HR removal when rendering email content<commit_after>const _ = require('lodash');
const juice = require('juice');
const template = require('./template');
const labsTemplate = require('./template-labs');
const settingsCache = require('../../../shared/settings-cache');
const urlUtils = require('../../../shared/url-utils');
const labs = require('../../../shared/labs');
const moment = require('moment-timezone');
const cheerio = require('cheerio');
const api = require('../../api');
const {URL} = require('url');
const mobiledocLib = require('../../lib/mobiledoc');
const htmlToText = require('html-to-text');
const {isUnsplashImage, isLocalContentImage} = require('@tryghost/kg-default-cards/lib/utils');
const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@tryghost/color-utils');
const logging = require('@tryghost/logging');

const ALLOWED_REPLACEMENTS = ['first_name'];

const getSite = () => {
    const publicSettings = settingsCache.getPublic();
    return Object.assign({}, publicSettings, {
        url: urlUtils.urlFor('home', true),
        iconUrl: publicSettings.icon ? urlUtils.urlFor('image', {image: publicSettings.icon}, true) : null
    });
};

const htmlToPlaintext = (html) => {
    // same options as used in Post model for generating plaintext but without `wordwrap: 80`
    // to avoid replacement strings being split across lines and for mail clients to handle
    // word wrapping based on user preferences
    return htmlToText.fromString(html, {
        wordwrap: false,
        ignoreImage: true,
        hideLinkHrefIfSameAsText: true,
        preserveNewlines: true,
        returnDomByDefault: true,
        uppercaseHeadings: false
    });
};

/**
 * createUnsubscribeUrl
 *
 * Takes a member uuid and returns the url that should be used to unsubscribe
 * In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
 *
 * @param {string} uuid
 */
const createUnsubscribeUrl = (uuid) => {
    const siteUrl = urlUtils.getSiteUrl();
    const unsubscribeUrl = new URL(siteUrl);
    unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
    if (uuid) {
        unsubscribeUrl.searchParams.set('uuid', uuid);
    } else {
        unsubscribeUrl.searchParams.set('preview', '1');
    }

    return unsubscribeUrl.href;
};

// NOTE: serialization is needed to make sure we are using current API and do post transformations
//       such as image URL transformation from relative to absolute
const serializePostModel = async (model, apiVersion = 'v4') => {
    // fetch mobiledoc rather than html and plaintext so we can render email-specific contents
    const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
    const docName = 'posts';

    await api.shared
        .serializers
        .handle
        .output(model, {docName: docName, method: 'read'}, api[apiVersion].serializers.output, frame);

    return frame.response[docName][0];
};

// removes %% wrappers from unknown replacement strings in email content
const normalizeReplacementStrings = (email) => {
    // we don't want to modify the email object in-place
    const emailContent = _.pick(email, ['html', 'plaintext']);

    const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
    const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;

    ['html', 'plaintext'].forEach((format) => {
        emailContent[format] = emailContent[format].replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
            const match = replacementStr.match(REPLACEMENT_STRING_REGEX);

            if (match) {
                const {recipientProperty} = match.groups;

                if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
                    // keeps wrapping %% for later replacement with real data
                    return replacementMatch;
                }
            }

            // removes %% so output matches user supplied content
            return replacementStr;
        });
    });

    return emailContent;
};

/**
 * Parses email content and extracts an array of replacements with desired fallbacks
 *
 * @param {Object} email
 * @param {string} email.html
 * @param {string} email.plaintext
 *
 * @returns {Object[]} replacements
 */
const parseReplacements = (email) => {
    const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
    const REPLACEMENT_STRING_REGEX = /\{(?<recipientProperty>\w*?)(?:,? *(?:"|")(?<fallback>.*?)(?:"|"))?\}/;

    const replacements = [];

    ['html', 'plaintext'].forEach((format) => {
        let result;
        while ((result = EMAIL_REPLACEMENT_REGEX.exec(email[format])) !== null) {
            const [replacementMatch, replacementStr] = result;
            const match = replacementStr.match(REPLACEMENT_STRING_REGEX);

            if (match) {
                const {recipientProperty, fallback} = match.groups;

                if (ALLOWED_REPLACEMENTS.includes(recipientProperty)) {
                    const id = `replacement_${replacements.length + 1}`;

                    replacements.push({
                        format,
                        id,
                        match: replacementMatch,
                        recipientProperty: `member_${recipientProperty}`,
                        fallback
                    });
                }
            }
        }
    });

    return replacements;
};

const getTemplateSettings = async () => {
    const accentColor = settingsCache.get('accent_color');
    const adjustedAccentColor = accentColor && darkenToContrastThreshold(accentColor, '#ffffff', 2).hex();
    const adjustedAccentContrastColor = accentColor && textColorForBackgroundColor(adjustedAccentColor).hex();

    const templateSettings = {
        headerImage: settingsCache.get('newsletter_header_image'),
        showHeaderIcon: settingsCache.get('newsletter_show_header_icon') && settingsCache.get('icon'),
        showHeaderTitle: settingsCache.get('newsletter_show_header_title'),
        showFeatureImage: settingsCache.get('newsletter_show_feature_image'),
        titleFontCategory: settingsCache.get('newsletter_title_font_category'),
        titleAlignment: settingsCache.get('newsletter_title_alignment'),
        bodyFontCategory: settingsCache.get('newsletter_body_font_category'),
        showBadge: settingsCache.get('newsletter_show_badge'),
        footerContent: settingsCache.get('newsletter_footer_content'),
        accentColor,
        adjustedAccentColor,
        adjustedAccentContrastColor
    };

    if (templateSettings.headerImage) {
        if (isUnsplashImage(templateSettings.headerImage)) {
            // Unsplash images have a minimum size so assuming 1200px is safe
            const unsplashUrl = new URL(templateSettings.headerImage);
            unsplashUrl.searchParams.set('w', '1200');

            templateSettings.headerImage = unsplashUrl.href;
            templateSettings.headerImageWidth = 600;
        } else {
            const {imageSize} = require('../../lib/image');
            try {
                const size = await imageSize.getImageSizeFromUrl(templateSettings.headerImage);

                if (size.width >= 600) {
                    // keep original image, just set a fixed width
                    templateSettings.headerImageWidth = 600;
                }

                if (isLocalContentImage(templateSettings.headerImage, urlUtils.getSiteUrl())) {
                    // we can safely request a 1200px image - Ghost will serve the original if it's smaller
                    templateSettings.headerImage = templateSettings.headerImage.replace(/\/content\/images\//, '/content/images/size/w1200/');
                }
            } catch (err) {
                // log and proceed. Using original header image without fixed width isn't fatal.
                logging.error(err);
            }
        }
    }

    return templateSettings;
};

const serialize = async (postModel, options = {isBrowserPreview: false, apiVersion: 'v4'}) => {
    const post = await serializePostModel(postModel, options.apiVersion);

    const timezone = settingsCache.get('timezone');
    const momentDate = post.published_at ? moment(post.published_at) : moment();
    post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');

    post.authors = post.authors && post.authors.map(author => author.name).join(',');
    if (post.posts_meta) {
        post.email_subject = post.posts_meta.email_subject;
    }

    // we use post.excerpt as a hidden piece of text that is picked up by some email
    // clients as a "preview" when listing emails. Our current plaintext/excerpt
    // generation outputs links as "Link [https://url/]" which isn't desired in the preview
    if (!post.custom_excerpt && post.excerpt) {
        post.excerpt = post.excerpt.replace(/\s\[http(.*?)\]/g, '');
    }

    post.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(post.mobiledoc), {target: 'email'});

    // perform any email specific adjustments to the mobiledoc->HTML render output
    // body wrapper is required so we can get proper top-level selections
    let _cheerio = cheerio.load(`<body>${post.html}</body>`);
    // remove leading/trailing HRs
    _cheerio(`
        body > hr:first-child,
        body > hr:last-child,
        body > div:first-child > hr:first-child,
        body > div:last-child > hr:last-child
    `).remove();
    post.html = _cheerio('body').html();

    post.plaintext = htmlToPlaintext(post.html);

    // Outlook will render feature images at full-size breaking the layout.
    // Content images fix this by rendering max 600px images - do the same for feature image here
    if (post.feature_image) {
        if (isUnsplashImage(post.feature_image)) {
            // Unsplash images have a minimum size so assuming 1200px is safe
            const unsplashUrl = new URL(post.feature_image);
            unsplashUrl.searchParams.set('w', '1200');

            post.feature_image = unsplashUrl.href;
            post.feature_image_width = 600;
        } else {
            const {imageSize} = require('../../lib/image');
            try {
                const size = await imageSize.getImageSizeFromUrl(post.feature_image);

                if (size.width >= 600) {
                    // keep original image, just set a fixed width
                    post.feature_image_width = 600;
                }

                if (isLocalContentImage(post.feature_image, urlUtils.getSiteUrl())) {
                    // we can safely request a 1200px image - Ghost will serve the original if it's smaller
                    post.feature_image = post.feature_image.replace(/\/content\/images\//, '/content/images/size/w1200/');
                }
            } catch (err) {
                // log and proceed. Using original feature_image without fixed width isn't fatal.
                logging.error(err);
            }
        }
    }

    const templateSettings = await getTemplateSettings();

    const render = labs.isSet('emailCardSegments') ? labsTemplate : template;

    let htmlTemplate = render({post, site: getSite(), templateSettings});

    if (options.isBrowserPreview) {
        const previewUnsubscribeUrl = createUnsubscribeUrl(null);
        htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
    }

    // Inline css to style attributes, turn on support for pseudo classes.
    const juiceOptions = {inlinePseudoElements: true};
    let juicedHtml = juice(htmlTemplate, juiceOptions);

    // convert juiced HTML to a DOM-like interface for further manipulation
    // happens after inlining of CSS so we can change element types without worrying about styling
    _cheerio = cheerio.load(juicedHtml);
    // force all links to open in new tab
    _cheerio('a').attr('target','_blank');
    // convert figure and figcaption to div so that Outlook applies margins
    _cheerio('figure, figcaption').each((i, elem) => !!(elem.tagName = 'div'));
    juicedHtml = _cheerio.html();

    // Fix any unsupported chars in Outlook
    juicedHtml = juicedHtml.replace(/'/g, ''');

    // Clean up any unknown replacements strings to get our final content
    const {html, plaintext} = normalizeReplacementStrings({
        html: juicedHtml,
        plaintext: post.plaintext
    });

    return {
        subject: post.email_subject || post.title,
        html,
        plaintext
    };
};

function renderEmailForSegment(email, memberSegment) {
    const result = {...email};
    const $ = cheerio.load(result.html);

    $('[data-gh-segment]').get().forEach((node) => {
        if (node.attribs['data-gh-segment'] !== memberSegment) { //TODO: replace with NQL interpretation
            $(node).remove();
        } else {
            // Getting rid of the attribute for a cleaner html output
            $(node).removeAttr('data-gh-segment');
        }
    });
    result.html = $.html();
    result.plaintext = htmlToPlaintext(result.html);

    return result;
}

module.exports = {
    serialize,
    createUnsubscribeUrl,
    renderEmailForSegment,
    parseReplacements
};
<|endoftext|>


<commit_before>require('reflect-metadata');
const fs            = require('fs');
const fsa           = require('fs-extra');
const exec          = require('child_process').exec;
const gulp          = require('gulp');
const deleteEmpty   = require('delete-empty');
const sift          = require('sift');
const glob          = require('glob');
const mocha         = require('gulp-mocha');
const chalk         = require('chalk');
const install       = require('gulp-install');
const istanbul      = require('gulp-istanbul');
const peg           = require('gulp-peg');
const webpack       = require('webpack');
const chokidar      = require('chokidar');
const gutil         = require('gulp-util');
const notify        = require('gulp-notify');
const symdest       = require('gulp-symdest');
const rename        = require('gulp-rename');
const named         = require('vinyl-named');
const electron      = require('gulp-atom-electron');
const vfs           = require('vinyl-fs');
const gulpSequence  = require('gulp-sequence');
const _             = require('highland');

const { spawn }                   = require('child_process');
const { merge, omit }             = require('lodash');
const { join, dirname, basename } = require('path');

const {
  GREP,
  argv,
  WATCH,
  BUILD_DIR,
  SRC_DIR,
  OUT_DIR,
  PACKAGES,
  BASE_DIR,
  PACKAGE_NAMES,
  INTEGRATIONS_DIR,
  NODE_MODULES_DIR,
  MONO_PKG_FILE_PATH,
  OUT_NODE_MODULES_DIR,
} = require('./config');

/******************************
 * Default tasks
 ******************************/

gulp.task('default', gulpSequence('prepare', 'build'));

/******************************
 * Build tasks
 ******************************/

const buildTasks = [
  'build:peg',
  'build:typescript',
  'build:symlinks',
  'build:electron'
];

gulp.task('build', WATCH ? buildTasks : gulpSequence(...buildTasks));

gulp.task('build:typescript', function(done) {
  const proc = spawn('node_modules/.bin/tsc', ['--declaration', '--pretty'].concat(WATCH ? '--watch' : []), {
    cwd: BASE_DIR
  });
  proc.stdout.pipe(process.stdout);
  proc.stderr.pipe(process.stderr);
  proc.on('exit', done);
});

gulp.task('build:peg', function() {
  return gulp
  .src(join(SRC_DIR, '**', '*.peg'))
  .pipe((peg()))
  .pipe(rename((file) => {
    file.extname = '.peg.js';
  }))
  .pipe(gulp.dest(OUT_DIR));
});

gulp.task('build:symlinks', () => {
  return gulp
  .src(join(OUT_DIR, '*'))
  .pipe(vfs.symlink(NODE_MODULES_DIR));
});

gulp.task('build:electron', gulpSequence(
  'prepare:electron',
  'build:electron:browser',
  'build:electron:server',
  'build:electron:package'
));

gulp.task('build:electron:browser', (done) => {
  const pkg = getElectronPackage();
  bundleElectron(join(SRC_DIR, pkg.name, pkg.entries.browser), require('./webpack/browser'), done);
});

gulp.task('build:electron:server', (done) => {
  const pkg = getElectronPackage();
  bundleElectron(join(SRC_DIR, pkg.name, pkg.entries.server), require('./webpack/node').create(getElectronBundleDir() + "/node_modules"), done);
});

gulp.task('build:electron:package', () => {
  const electronPackage = getElectronPackage();

  const version = electronPackage.version;
  const platform = process.platform;
  const arch = process.env.ELECTRON_PLATFORM || (platform === 'win32' ? 'ia32' : process.arch)

  return gulp.src(join(getElectronBundleDir(), "**"))
  .pipe(electron({ version, platform, arch }))
  .pipe(symdest(join(getElectronBundleDir(), "app")));
});

/******************************
 * Prepare tasks
 ******************************/

gulp.task('prepare', gulpSequence(
  'prepare:copy-assets',
  'prepare:mono-package',
  'prepare:integrations'
));

gulp.task('prepare:copy-assets', () => {
  return gulp.src(join(SRC_DIR, '**', '!(*.ts|*.peg)'))
  .pipe(gulp.dest(OUT_DIR));
});

gulp.task('prepare:electron', gulpSequence(

  // clean electron node_modules to prevent stale local @tandem dependencies
  'clean:electron',
  'prepare:electron:create-bundle-out',
  'prepare:electron:clean-empty-dirs',
  'prepare:electron:install'
));

gulp.task('prepare:electron:create-bundle-out', () => {
  const electronDir = join(OUT_DIR, getElectronPackage().name);
  const electronSrcDir = join(SRC_DIR, getElectronPackage().name);
  const bundleDir = getElectronBundleDir();
  fsa.mkdirpSync(bundleDir);

  return gulp.src(join(electronSrcDir, '**', '!(*.js|*.ts)'))
  .pipe(gulp.dest(bundleDir));
});

gulp.task('prepare:electron:clean-empty-dirs', () => {
  deleteEmpty.sync(getElectronBundleDir());
});

gulp.task('prepare:electron:install', () => {
  return gulp.src(join(getElectronBundleDir(), "package.json"))
  .pipe(install());
});

gulp.task('prepare:mono-package', gulpSequence(
  'prepare:create-mono-package',
  'prepare:install-mono-package'
));

/**
 * Creates a monolithic package of all the dependencies
 * required by all packages in the source directory.
 */

gulp.task('prepare:create-mono-package', () => {

  const { dependencies, devDependencies } = merge(...PACKAGES);

  // one mono package for everything to go under
  const monoPackage = {
    dependencies: omit(Object.assign({}, dependencies, devDependencies), PACKAGE_NAMES)
  };

  const monoPackageContent  = JSON.stringify(monoPackage, null, 2);

  fs.writeFileSync(MONO_PKG_FILE_PATH, monoPackageContent, 'utf8');
});

gulp.task('prepare:install-mono-package', ['clean:symlinks'], () => {
  return gulp
  .src(MONO_PKG_FILE_PATH)
  .pipe(gulp.dest(OUT_DIR))
  .pipe(install())
});

gulp.task('prepare:integrations', [
  'prepare:vscode-extension'
]);

gulp.task('prepare:vscode-extension', [
  'prepare:vscode-extension-symlinks'
]);

gulp.task('prepare:vscode-extension-symlinks', () => {
  return gulp
  .src(join(OUT_DIR, '*'))
  .pipe(vfs.symlink(join(INTEGRATIONS_DIR, 'tandem-vscode-extension', 'node_modules')));
});

/******************************
 * Clean tasks
 ******************************/

gulp.task('clean', gulpSequence(
  'clean:out',
  'clean:symlinks'
));

gulp.task('clean:javascript', function() {
  glob.sync(join(OUT_DIR, `{${PACKAGE_NAMES.join(',')}}`, '**', '*.js'))
  .forEach((filePath) => {
    fsa.removeSync(filePath);
  });
});

gulp.task('clean:electron', function() {
  fsa.removeSync(getElectronBundleDir());
});

gulp.task('clean:out', function() {
  fsa.removeSync(OUT_DIR);
});

gulp.task('clean:mono-package', function() {
  fsa.removeSync(OUT_NODE_MODULES_DIR);
});

gulp.task('clean:symlinks', function() {
  if (!fs.existsSync(NODE_MODULES_DIR)) return;
  fs.readdirSync(NODE_MODULES_DIR).forEach((dir) => {
    const filePath = join(NODE_MODULES_DIR, dir);
    if (fs.lstatSync(filePath).isSymbolicLink()) {
      fsa.removeSync(filePath);
    }
  });
});

/******************************
 * Publish tasks
 ******************************/

gulp.task('publish:all', ['test'], function() {

});

gulp.task('publish', function() {
  // inquier here about what package to publish
});

/******************************
 * Test tasks
 ******************************/

gulp.task('test', ['test:flag', 'test:all']);

gulp.task('test:flag', () => {
  process.env.TESTING   = true;
  process.env.SOURCE_MAPS = 1;
  process.env.SANDBOXED = true;
});

let coverageVariable;

gulp.task('hook:istanbul', function() {
  if (!argv.coverage) return;
  coverageVariable = '$$cov_' + new Date().getTime() + '$$';
  return gulp
  .src(getPackageOutDirs().map(dir => join(dir, '**', '*.js')))
  .pipe(istanbul())
  .pipe(istanbul.hookRequire());
});

gulp.task('test:all', ['hook:istanbul'], function(done) {
  const packageDirs = getPackageOutDirs();
  const testFiles = packageDirs.map(dir => join(dir, '**', '*-test.js'));

  let running = false;

  function run() {
    if (running) return;
    running = true;
    let stream = gulp
    .src(testFiles)
    .pipe(mocha({
      reporter: argv.reporter || 'dot',
      timeout: argv.timeout || 1000 * 2,
      grep: GREP,
      bail: argv.bail
    }))

    .on('error', error => console.error(error.stack))
    .on('error', notify.onError({ message: `Error: <%= error.message %>`, title: `Error running tests` }));

    if (argv.coverage) {
      stream = stream
      .pipe(istanbul.writeReports())
      .pipe(istanbul.enforceThresholds({
        coverageVariable: coverageVariable,
        reporters: ['lcov', 'text-summary', 'html'],
        thresholds: {
          global: Number(argv.coverage) || 0
        }
      }))
    }

    stream.once("end", () => running = false);

    return stream;
  }

  if (WATCH) {
    const watcher = chokidar.watch(packageDirs.map(dir => join(dir, '**', '*.js')), {
      usePolling: false,
      alwaysStat: true
    });
    watcher.on("change", run);
    run();
  } else {
    return run();
  }
});

/******************************
 * Utilities
 ******************************/

function bundleElectron(entry, config, done) {
  bundle(entry, Object.assign({}, config, {
    output: {
      path: dirname(entry.replace(SRC_DIR + "/" + getElectronPackage().name, getElectronBundleDir()))
    }
  }), done);
}

function bundle(srcEntryPath, config, done) {
   const outDir  = dirname(srcEntryPath.replace(SRC_DIR, OUT_DIR));
   const outBasename = basename(srcEntryPath.replace(/\.(js|ts)$/, ".bundle.js"));

   webpack(Object.assign({}, config, {
    entry: srcEntryPath,
    output: {
      path: config.output && config.output.path || outDir,
      filename: outBasename
    }
   }), (err, stats) => {
    if(err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString(config.stats));
    if (!WATCH) {
      done();
    }
  });
}

function getPackageOutDirs() {
  return PACKAGE_NAMES.map(name => join(OUT_DIR, name));
}

function noop() { }

function getElectronAppDir() {
  return join(OUT_DIR, getElectronPackage().name);
}

function getElectronBundleDir() {
  return join(BUILD_DIR, getElectronPackage().name);
}

function getElectronPackage() {
  return PACKAGES.find(sift({ 'electronVersion': { $exists: true }}));
}<commit_msg>add stop for gulp<commit_after>require('reflect-metadata');
const fs            = require('fs');
const fsa           = require('fs-extra');
const exec          = require('child_process').exec;
const gulp          = require('gulp');
const deleteEmpty   = require('delete-empty');
const sift          = require('sift');
const glob          = require('glob');
const mocha         = require('gulp-mocha');
const chalk         = require('chalk');
const install       = require('gulp-install');
const istanbul      = require('gulp-istanbul');
const peg           = require('gulp-peg');
const webpack       = require('webpack');
const chokidar      = require('chokidar');
const gutil         = require('gulp-util');
const notify        = require('gulp-notify');
const symdest       = require('gulp-symdest');
const rename        = require('gulp-rename');
const named         = require('vinyl-named');
const electron      = require('gulp-atom-electron');
const vfs           = require('vinyl-fs');
const gulpSequence  = require('gulp-sequence');
const _             = require('highland');

const { spawn }                   = require('child_process');
const { merge, omit }             = require('lodash');
const { join, dirname, basename } = require('path');

const {
  GREP,
  argv,
  WATCH,
  BUILD_DIR,
  SRC_DIR,
  OUT_DIR,
  PACKAGES,
  BASE_DIR,
  PACKAGE_NAMES,
  INTEGRATIONS_DIR,
  NODE_MODULES_DIR,
  MONO_PKG_FILE_PATH,
  OUT_NODE_MODULES_DIR,
} = require('./config');

/******************************
 * Default tasks
 ******************************/

gulp.task('default', gulpSequence('prepare', 'build'));

/******************************
 * Build tasks
 ******************************/

const buildTasks = [
  'build:peg',
  'build:typescript',
  'build:symlinks',
  'build:electron'
];

gulp.task('build', WATCH ? buildTasks : gulpSequence(...buildTasks));

gulp.task('build:typescript', function(done) {
  const proc = spawn('node_modules/.bin/tsc', ['--declaration', '--pretty'].concat(WATCH ? '--watch' : []), {
    cwd: BASE_DIR
  });
  proc.stdout.pipe(process.stdout);
  proc.stderr.pipe(process.stderr);
  proc.on('exit', done);
});

gulp.task('build:peg', function() {
  return gulp
  .src(join(SRC_DIR, '**', '*.peg'))
  .pipe((peg()))
  .pipe(rename((file) => {
    file.extname = '.peg.js';
  }))
  .pipe(gulp.dest(OUT_DIR));
});

gulp.task('build:symlinks', () => {
  return gulp
  .src(join(OUT_DIR, '*'))
  .pipe(vfs.symlink(NODE_MODULES_DIR));
});

gulp.task('build:electron', gulpSequence(
  'prepare:electron',
  'build:electron:browser',
  'build:electron:server',
  'build:electron:package'
));

gulp.task('build:electron:browser', (done) => {
  const pkg = getElectronPackage();
  bundleElectron(join(SRC_DIR, pkg.name, pkg.entries.browser), require('./webpack/browser'), done);
});

gulp.task('build:electron:server', (done) => {
  const pkg = getElectronPackage();
  bundleElectron(join(SRC_DIR, pkg.name, pkg.entries.server), require('./webpack/node').create(getElectronBundleDir() + "/node_modules"), done);
});

gulp.task('build:electron:package', () => {
  const electronPackage = getElectronPackage();

  const version = electronPackage.version;
  const platform = process.platform;
  const arch = process.env.ELECTRON_PLATFORM || (platform === 'win32' ? 'ia32' : process.arch)

  return gulp.src(join(getElectronBundleDir(), "**"))
  .pipe(electron({ version, platform, arch }))
  .pipe(symdest(join(getElectronBundleDir(), "app")));
});

/******************************
 * Prepare tasks
 ******************************/

gulp.task('prepare', gulpSequence(
  'prepare:copy-assets',
  'prepare:mono-package',
  'prepare:integrations'
));

gulp.task('prepare:copy-assets', () => {
  return gulp.src(join(SRC_DIR, '**', '!(*.ts|*.peg)'))
  .pipe(gulp.dest(OUT_DIR));
});

gulp.task('prepare:electron', gulpSequence(

  // clean electron node_modules to prevent stale local @tandem dependencies
  'clean:electron',
  'prepare:electron:create-bundle-out',
  'prepare:electron:clean-empty-dirs',
  'prepare:electron:install'
));

gulp.task('prepare:electron:create-bundle-out', () => {
  const electronDir = join(OUT_DIR, getElectronPackage().name);
  const electronSrcDir = join(SRC_DIR, getElectronPackage().name);
  const bundleDir = getElectronBundleDir();
  fsa.mkdirpSync(bundleDir);

  return gulp.src(join(electronSrcDir, '**', '!(*.js|*.ts)'))
  .pipe(gulp.dest(bundleDir));
});

gulp.task('prepare:electron:clean-empty-dirs', () => {
  deleteEmpty.sync(getElectronBundleDir());
});

gulp.task('prepare:electron:install', () => {
  return gulp.src(join(getElectronBundleDir(), "package.json"))
  .pipe(install());
});

gulp.task('prepare:mono-package', gulpSequence(
  'prepare:create-mono-package',
  'prepare:install-mono-package'
));

/**
 * Creates a monolithic package of all the dependencies
 * required by all packages in the source directory.
 */

gulp.task('prepare:create-mono-package', () => {

  const { dependencies, devDependencies } = merge(...PACKAGES);

  // one mono package for everything to go under
  const monoPackage = {
    dependencies: omit(Object.assign({}, dependencies, devDependencies), PACKAGE_NAMES)
  };

  const monoPackageContent  = JSON.stringify(monoPackage, null, 2);

  fs.writeFileSync(MONO_PKG_FILE_PATH, monoPackageContent, 'utf8');
});

gulp.task('prepare:install-mono-package', ['clean:symlinks'], () => {
  return gulp
  .src(MONO_PKG_FILE_PATH)
  .pipe(gulp.dest(OUT_DIR))
  .pipe(install())
});

gulp.task('prepare:integrations', [
  'prepare:vscode-extension'
]);

gulp.task('prepare:vscode-extension', [
  'prepare:vscode-extension-symlinks'
]);

gulp.task('prepare:vscode-extension-symlinks', () => {
  return gulp
  .src(join(OUT_DIR, '*'))
  .pipe(vfs.symlink(join(INTEGRATIONS_DIR, 'tandem-vscode-extension', 'node_modules')));
});

/******************************
 * Clean tasks
 ******************************/

gulp.task('clean', gulpSequence(
  'clean:out',
  'clean:symlinks'
));

gulp.task('clean:javascript', function() {
  glob.sync(join(OUT_DIR, `{${PACKAGE_NAMES.join(',')}}`, '**', '*.js'))
  .forEach((filePath) => {
    fsa.removeSync(filePath);
  });
});

gulp.task('clean:electron', function() {
  fsa.removeSync(getElectronBundleDir());
});

gulp.task('clean:out', function() {
  fsa.removeSync(OUT_DIR);
});

gulp.task('clean:mono-package', function() {
  fsa.removeSync(OUT_NODE_MODULES_DIR);
});

gulp.task('clean:symlinks', function() {
  if (!fs.existsSync(NODE_MODULES_DIR)) return;
  fs.readdirSync(NODE_MODULES_DIR).forEach((dir) => {
    const filePath = join(NODE_MODULES_DIR, dir);
    if (fs.lstatSync(filePath).isSymbolicLink()) {
      fsa.removeSync(filePath);
    }
  });
});

/******************************
 * Publish tasks
 ******************************/

gulp.task('publish:all', ['test'], function() {

});

gulp.task('publish', function() {
  // inquier here about what package to publish
});

/******************************
 * Test tasks
 ******************************/

gulp.task('test', ['test:flag', 'test:all']);

gulp.task('test:flag', () => {
  process.env.TESTING   = true;
  process.env.SOURCE_MAPS = 1;
  process.env.SANDBOXED = true;
});

let coverageVariable;

gulp.task('hook:istanbul', function() {
  if (!argv.coverage) return;
  coverageVariable = '$$cov_' + new Date().getTime() + '$$';
  return gulp
  .src(getPackageOutDirs().map(dir => join(dir, '**', '*.js')))
  .pipe(istanbul())
  .pipe(istanbul.hookRequire());
});

gulp.task('test:all', ['hook:istanbul'], function(done) {
  const packageDirs = getPackageOutDirs();
  const testFiles = packageDirs.map(dir => join(dir, '**', '*-test.js'));

  let running = false;

  function run() {
    if (running) return;
    running = true;
    let stream = gulp
    .src(testFiles)
    .pipe(mocha({
      reporter: argv.reporter || 'dot',
      timeout: argv.timeout || 1000 * 2,
      grep: GREP,
      bail: argv.bail
    }))

    .on('error', error => console.error(error.stack))
    .on('error', notify.onError({ message: `Error: <%= error.message %>`, title: `Error running tests` }));

    if (argv.coverage) {
      stream = stream
      .pipe(istanbul.writeReports())
      .pipe(istanbul.enforceThresholds({
        coverageVariable: coverageVariable,
        reporters: ['lcov', 'text-summary', 'html'],
        thresholds: {
          global: Number(argv.coverage) || 0
        }
      }))
    }

    stream.once("end", () => running = false);

    return stream;
  }

  if (WATCH) {
    const watcher = chokidar.watch(packageDirs.map(dir => join(dir, '**', '*.js')), {
      usePolling: false,
      alwaysStat: true
    });
    watcher.on("change", run);
    run();
  } else {
    return run();
  }
});

/******************************
 * Utilities
 ******************************/

function bundleElectron(entry, config, done) {
  bundle(entry, Object.assign({}, config, {
    output: {
      path: dirname(entry.replace(SRC_DIR + "/" + getElectronPackage().name, getElectronBundleDir()))
    }
  }), done);
}

function bundle(srcEntryPath, config, done) {
   const outDir  = dirname(srcEntryPath.replace(SRC_DIR, OUT_DIR));
   const outBasename = basename(srcEntryPath.replace(/\.(js|ts)$/, ".bundle.js"));

   webpack(Object.assign({}, config, {
    entry: srcEntryPath,
    output: {
      path: config.output && config.output.path || outDir,
      filename: outBasename
    }
   }), (err, stats) => {
    if(err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString(config.stats));
    if (!WATCH) {
      done();
    }
  });
}

function getPackageOutDirs() {
  return PACKAGE_NAMES.map(name => join(OUT_DIR, name));
}

function noop() { }

function getElectronAppDir() {
  return join(OUT_DIR, getElectronPackage().name);
}

function getElectronBundleDir() {
  return join(BUILD_DIR, getElectronPackage().name);
}

function getElectronPackage() {
  return PACKAGES.find(sift({ 'electronVersion': { $exists: true }}));
}

gulp.on('stop', () => {
  if (!WATCH) process.exit();
});<|endoftext|>