<commit_before>import {stringify} from 'ini';
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

/**
 * @memberof Realm
 */
class Sync {
    /**
     * Add a sync listener to listen to changes across multiple Realms
     * @param {string} server_url - the sync server to listen to
     * @param {SyncUser} admin_user - an admin user obtained by calling `new Realm.Sync.User.adminUser`
     * @param {string} regex - a regular expression used to determine which cahnged Realms should trigger events -
     *  Use `.*` to match all all Realms
     * @param {string} name - The name of event that should cause the callback to be called
     *   _Currently only the 'change' event is supported_
     * @param {function(change_event)} change_callback - called when changes are made to any Realm which
     *  match the given regular expression
     */
    static addListener(server_url, admin_user, regex, name, change_callback) {}

    /**
     * Remove a previously registered sync listener
     * @param {string} regex - the regular expression previously used to register the listener
     * @param {string} name - The event name
     *   _Currently only the 'change' event is supported_
     * @param {function(change_event)} change_callback - the previously registered callback to be removed
     */
    static removeListener(regex, name, change_callback) {}

    /**
     * Remove all previously regiestered listeners
     * @param {string} [name] - The name of the event whose listeners should be removed.
     *   _Currently only the 'change' event is supported_
     */
    static removeAllListeners(name) {}

    /**
     * Set the sync log level.
     * @param {string} log_level
     */
    static setLogLevel(log_level) {}

}

/**
 * Change info passed when receiving sync 'change' events
 * @memberof Realm.Sync
 */
class ChangeEvent {
    /**
     * The path of the changed Realm
     * @type {string}
     */
    get path() {}

    /**
     * The changed realm
     * @type {Realm}
     */
    get realm() {}

    /**
     * The changed Realm at the old state before the changes were applied
     * @type {Realm}
     */
    get oldRealm() {}

    /**
     * The change indexes for all added, removed, and modified objects in the changed Realm.
     * This object is a hashmap of object types to arrays of indexes for all changed objects:
     * @example
     * {
     *   object_type_1: {
     *     insertions:    [indexes...],
     *     deletions:     [indexes...],
     *     modifications: [indexes...]
     *   },
     *   object_type_2:
     *     ...
     * }
     * @type {object}
     */
    get changes() {}
}

/**
 * @typedef Realm.Sync~LogLevel
 * @type {("error"|"info"|"debug")}
 */

/**
 * Class that describes authentication errors in the Realm Object Server
 * @memberof Realm.Sync
 */
class AuthError extends Error {
    /**
     * The numerical code for this error.
     * @type {number}
     */
    get code() {}

    /**
     * The unique help URI that describes this error.
     * @type {string}
     */
    get type() {}
}

/**
 * Class for logging in and managing Sync users.
 * @memberof Realm.Sync
 */
class User {
    /**
     * Login a sync user with username and password.
     * @param {string} server - authentication server
     * @param {string} username
     * @param {string} password
     * @param {function(error, user)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static login(server, username, password, callback) {}

    /**
     * Register/login a sync user using an external login provider.
     * @param {string} server - authentication server
   * @param {object} options - options, containing the following:
     * @param {string} options.provider - The provider type
     * @param {string} options.providerToken - The access token for the given provider
   * @param {object} [options.userInfo] - A map containing additional data required by the provider
     * @param {function(error, User)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static registerWithProvider(server, options, callback) {}

    /**
     * Register a sync user with username and password.
     * @param {string} server - authentication server
     * @param {string} username
     * @param {string} password
     * @param {function(error, user)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static register(server, username, password, callback) {}

    /**
     * Create an admin user for the given authentication server with an existing token
     * @param {string} adminToken - existing admin token
     * @return {User} - admin user populated with the given token and server
     */
    static adminUser(adminToken) {}

    /**
     * A dictionary containing users that are currently logged in.
     * The keys in the dictionary are user identities, values are corresponding User objects.
     * @type {object}
     */
    static get all() {}

    /**
     * Get the currently logged in user.
     * Throws error if > 1 user logged in, returns undefined if no users logged in.
     * @type {User}
     */
    static get current() {}

    /**
     * Gets the server URL that was used for authentication.
     * @type {string}
     */
    get server() {}

    /**
     * Gets the identity of this user on the Realm Object Server.
     * The identity is a guaranteed to be unique among all users on the Realm Object Server.
     * @type {string}
     */
    get identity() {}

    /**
     * Gets this user's refresh token. This is the user's credential for accessing the Realm
     * Object Server and should be treated as sensitive data.
     * @type {string}
     */
    get token() {}

    /**
     * Returns true if this user is an administrator
     * @type {bool}
     */
    get isAdmin() {}

    /**
     * Logs out the user from the Realm Object Server.
     */
    logout() {}

    /**
     * Get the management realm for this User.
     * This Realm can be used to control access and permissions for Realms owned by the user.
     * This includes giving others access to the Realms.
     * @returns {Realm}
     */
    openManagementRealm() {}
}

/**
 * An object encapsulating a Realm Object Server session. Sessions represent the communication between the
 * client (and a local Realm file on disk), and the server (and a remote Realm at a given URL stored on a Realm Object Server).
 * Sessions are always created by the SDK and vended out through various APIs. The lifespans of sessions
 * associated with Realms are managed automatically.
 * @memberof Realm.Sync
 */
class Session {
    /**
     * Gets the Sync-part of the configuration that the corresponding Realm was
     * constructed with.
     * @type {object}
     */
    get config() {}

    /**
     * Gets the User that this session was created with.
     * @type {User}
     */
    get user() {}

    /**
     * Gets the URL of the Realm Object Server that this session is connected to.
     * @type {string}
     */
    get url() {}

    /**
     * Gets the current state of the session.
     * Can be either:
     *  - "active": The session is connected to the Realm Object Server and is actively transferring data.
     *  - "inactive": The session is not currently communicating with the Realm Object Server.
     *  - "invalid": A non-recoverable error has occurred, and this session is semantically invalid. A new session should be created.
     * @type {string}
     */
    get state() {}
}
<commit_msg>Two more indentation fixes<commit_after>import {stringify} from 'ini';
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

/**
 * @memberof Realm
 */
class Sync {
    /**
     * Add a sync listener to listen to changes across multiple Realms
     * @param {string} server_url - the sync server to listen to
     * @param {SyncUser} admin_user - an admin user obtained by calling `new Realm.Sync.User.adminUser`
     * @param {string} regex - a regular expression used to determine which cahnged Realms should trigger events -
     *  Use `.*` to match all all Realms
     * @param {string} name - The name of event that should cause the callback to be called
     *   _Currently only the 'change' event is supported_
     * @param {function(change_event)} change_callback - called when changes are made to any Realm which
     *  match the given regular expression
     */
    static addListener(server_url, admin_user, regex, name, change_callback) {}

    /**
     * Remove a previously registered sync listener
     * @param {string} regex - the regular expression previously used to register the listener
     * @param {string} name - The event name
     *   _Currently only the 'change' event is supported_
     * @param {function(change_event)} change_callback - the previously registered callback to be removed
     */
    static removeListener(regex, name, change_callback) {}

    /**
     * Remove all previously regiestered listeners
     * @param {string} [name] - The name of the event whose listeners should be removed.
     *   _Currently only the 'change' event is supported_
     */
    static removeAllListeners(name) {}

    /**
     * Set the sync log level.
     * @param {string} log_level
     */
    static setLogLevel(log_level) {}

}

/**
 * Change info passed when receiving sync 'change' events
 * @memberof Realm.Sync
 */
class ChangeEvent {
    /**
     * The path of the changed Realm
     * @type {string}
     */
    get path() {}

    /**
     * The changed realm
     * @type {Realm}
     */
    get realm() {}

    /**
     * The changed Realm at the old state before the changes were applied
     * @type {Realm}
     */
    get oldRealm() {}

    /**
     * The change indexes for all added, removed, and modified objects in the changed Realm.
     * This object is a hashmap of object types to arrays of indexes for all changed objects:
     * @example
     * {
     *   object_type_1: {
     *     insertions:    [indexes...],
     *     deletions:     [indexes...],
     *     modifications: [indexes...]
     *   },
     *   object_type_2:
     *     ...
     * }
     * @type {object}
     */
    get changes() {}
}

/**
 * @typedef Realm.Sync~LogLevel
 * @type {("error"|"info"|"debug")}
 */

/**
 * Class that describes authentication errors in the Realm Object Server
 * @memberof Realm.Sync
 */
class AuthError extends Error {
    /**
     * The numerical code for this error.
     * @type {number}
     */
    get code() {}

    /**
     * The unique help URI that describes this error.
     * @type {string}
     */
    get type() {}
}

/**
 * Class for logging in and managing Sync users.
 * @memberof Realm.Sync
 */
class User {
    /**
     * Login a sync user with username and password.
     * @param {string} server - authentication server
     * @param {string} username
     * @param {string} password
     * @param {function(error, user)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static login(server, username, password, callback) {}

    /**
     * Register/login a sync user using an external login provider.
     * @param {string} server - authentication server
     * @param {object} options - options, containing the following:
     * @param {string} options.provider - The provider type
     * @param {string} options.providerToken - The access token for the given provider
     * @param {object} [options.userInfo] - A map containing additional data required by the provider
     * @param {function(error, User)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static registerWithProvider(server, options, callback) {}

    /**
     * Register a sync user with username and password.
     * @param {string} server - authentication server
     * @param {string} username
     * @param {string} password
     * @param {function(error, user)} callback - called with the following arguments:
     *   - `error` - an Error object is provided on failure
     *   - `user` - a valid User object on success
     */
    static register(server, username, password, callback) {}

    /**
     * Create an admin user for the given authentication server with an existing token
     * @param {string} adminToken - existing admin token
     * @return {User} - admin user populated with the given token and server
     */
    static adminUser(adminToken) {}

    /**
     * A dictionary containing users that are currently logged in.
     * The keys in the dictionary are user identities, values are corresponding User objects.
     * @type {object}
     */
    static get all() {}

    /**
     * Get the currently logged in user.
     * Throws error if > 1 user logged in, returns undefined if no users logged in.
     * @type {User}
     */
    static get current() {}

    /**
     * Gets the server URL that was used for authentication.
     * @type {string}
     */
    get server() {}

    /**
     * Gets the identity of this user on the Realm Object Server.
     * The identity is a guaranteed to be unique among all users on the Realm Object Server.
     * @type {string}
     */
    get identity() {}

    /**
     * Gets this user's refresh token. This is the user's credential for accessing the Realm
     * Object Server and should be treated as sensitive data.
     * @type {string}
     */
    get token() {}

    /**
     * Returns true if this user is an administrator
     * @type {bool}
     */
    get isAdmin() {}

    /**
     * Logs out the user from the Realm Object Server.
     */
    logout() {}

    /**
     * Get the management realm for this User.
     * This Realm can be used to control access and permissions for Realms owned by the user.
     * This includes giving others access to the Realms.
     * @returns {Realm}
     */
    openManagementRealm() {}
}

/**
 * An object encapsulating a Realm Object Server session. Sessions represent the communication between the
 * client (and a local Realm file on disk), and the server (and a remote Realm at a given URL stored on a Realm Object Server).
 * Sessions are always created by the SDK and vended out through various APIs. The lifespans of sessions
 * associated with Realms are managed automatically.
 * @memberof Realm.Sync
 */
class Session {
    /**
     * Gets the Sync-part of the configuration that the corresponding Realm was
     * constructed with.
     * @type {object}
     */
    get config() {}

    /**
     * Gets the User that this session was created with.
     * @type {User}
     */
    get user() {}

    /**
     * Gets the URL of the Realm Object Server that this session is connected to.
     * @type {string}
     */
    get url() {}

    /**
     * Gets the current state of the session.
     * Can be either:
     *  - "active": The session is connected to the Realm Object Server and is actively transferring data.
     *  - "inactive": The session is not currently communicating with the Realm Object Server.
     *  - "invalid": A non-recoverable error has occurred, and this session is semantically invalid. A new session should be created.
     * @type {string}
     */
    get state() {}
}
<|endoftext|>


<commit_before>import React from 'react'
import classnames from 'classnames'
import classes from './Drawer.scss'
import { Link } from 'react-router'

const components = {
  dashboard: {
    name: 'Dashboard',
    path: '/'
  },
  components: {
    name: 'Components',
    path: '/components'
  }
}

export const Drawer = ({}, { router }) => {
  const drawerItems = Object.keys(components).map((key) => {
    return (
      <Link key={key} to={components[key].path} className='mdl-navigation__link' activeClassName={classes.active} onlyActiveOnIndex>
        { components[key].name }
      </Link>
    )
  })

  return (

      {drawerItems}

)
}

Drawer.contextTypes = {
  router: React.PropTypes.object.isRequired
}

export default Drawer
<commit_msg>Follow eslint<commit_after>import React from 'react'
import classnames from 'classnames'
import classes from './Drawer.scss'
import { Link } from 'react-router'

const components = {
  dashboard: {
    name: 'Dashboard',
    path: '/'
  },
  components: {
    name: 'Components',
    path: '/components'
  }
}

export const Drawer = () => {
  const drawerItems = Object.keys(components).map((key) => {
    return (
      <Link key={key} to={components[key].path} className='mdl-navigation__link'
        activeClassName={classes.active} onlyActiveOnIndex>
        { components[key].name }
      </Link>
    )
  })

  return (

      {drawerItems}

)
}

Drawer.contextTypes = {
  router: React.PropTypes.object.isRequired
}

export default Drawer
<|endoftext|>


<commit_before>module.exports = {

	// These are variables will be accessible via our templates

	templateData: {
		authors: require("./authors.js"),

		site: {
			title: "DevsNorte",
			description: "Um lugar para discutir sobre as melhores práticas de desenvolvimento",
			url: "http://devsnorte.com.br",
			image: "http://devsnorte.com.br/images/logo.png"
		},

		github: {
			user: "pvhjs",
			repo: "devsnorte.com.br"
		},

		/* Helpers
		===================================================================== */
		getAuthor: function(author) {
			if(author)
				return this.authors[author];

			return this.authors["Comunidade"];
		},

		getAuthorImage: function(author) {
			if(this.authors[author].gravatar)
				return "https://2.gravatar.com/avatar/" + this.authors[authors].gravatar;

			return this.authors[author].image;
		},

		getAuthorGooglePlus: function(author) {
			return "https://plus.google.com/" + this.authors[author].gplus + "/";
		},

		getAuthorTwitter: function(author) {
			return "https://twitter.com/" + this.authors[author].twitter;
		},

		getGitgubUrl: function() {
			return "https://github.com/" + this.github.user + "/" + this.github.repo;
		},

		getGithubEditUrl: function(path) {
			return this.getGithubUrl() + "/edit/master/src/documents/" + path;
		},

		getTitle: function() {
			if(this.document.title)
				return "" + this.document.title + "—" + this.site.title;

			return "" + this.site.title;
		},

		getDescription: function() {
			if(this.document.description)
				return this.document.description;

			return this.site.description;
		},

		getImage: function() {
			if(this.document.image)
				return "" + this.site.url + "/images/stories/" + this.document.image;

			return this.site.image;
		},

		/* Development Environment
		===================================================================== */
		environments: {
			development: {
				templateData: {
					site: {
						url: "http://localhost:9778"
					}
				}
			}
		},

		/* Collections
		===================================================================== */
		collections: {

		},

		/* Plugins Configurations
		===================================================================== */
		plugins: {
			grunt: {
				writeAfter: false,
				generateAfter: ["default"]
			}
		}
	}
}
<commit_msg>Altera caractere<commit_after>module.exports = {

	// These are variables will be accessible via our templates

	templateData: {
		authors: require("./authors.js"),

		site: {
			title: "DevsNorte",
			description: "Um lugar para discutir sobre as melhores práticas de desenvolvimento",
			url: "http://devsnorte.com.br",
			image: "http://devsnorte.com.br/images/logo.png"
		},

		github: {
			user: "pvhjs",
			repo: "devsnorte.com.br"
		},

		/* Helpers
		===================================================================== */
		getAuthor: function(author) {
			if(author)
				return this.authors[author];

			return this.authors["Comunidade"];
		},

		getAuthorImage: function(author) {
			if(this.authors[author].gravatar)
				return "https://2.gravatar.com/avatar/" + this.authors[authors].gravatar;

			return this.authors[author].image;
		},

		getAuthorGooglePlus: function(author) {
			return "https://plus.google.com/" + this.authors[author].gplus + "/";
		},

		getAuthorTwitter: function(author) {
			return "https://twitter.com/" + this.authors[author].twitter;
		},

		getGitgubUrl: function() {
			return "https://github.com/" + this.github.user + "/" + this.github.repo;
		},

		getGithubEditUrl: function(path) {
			return this.getGithubUrl() + "/edit/master/src/documents/" + path;
		},

		getTitle: function() {
			if(this.document.title)
				return "" + this.document.title + " — " + this.site.title;

			return "" + this.site.title;
		},

		getDescription: function() {
			if(this.document.description)
				return this.document.description;

			return this.site.description;
		},

		getImage: function() {
			if(this.document.image)
				return "" + this.site.url + "/images/stories/" + this.document.image;

			return this.site.image;
		},

		/* Development Environment
		===================================================================== */
		environments: {
			development: {
				templateData: {
					site: {
						url: "http://localhost:9778"
					}
				}
			}
		},

		/* Collections
		===================================================================== */
		collections: {

		},

		/* Plugins Configurations
		===================================================================== */
		plugins: {
			grunt: {
				writeAfter: false,
				generateAfter: ["default"]
			}
		}
	}
}
<|endoftext|>


<commit_before>var path = require('path');

module.exports = {
  chrome: {
    pathQuery: 'which google-chrome',
    process: 'chrome',
    opensTab: true
  },
  firefox: {
    pathQuery: 'which firefox',
    process: 'firefox',
    // See https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options
    // Assuming Firefox >= 20
    args: ['--private-window']
  },
  opera: {
    pathQuery: 'which opera',
    process: 'opera'
  },
  phantom: {
    pathQuery: 'which phantomjs',
    process: 'phantomjs',
    args: [path.join(__dirname, '..', '..', '..', 'resources/phantom.js')],
    multi: true
  }
};
<commit_msg>Linux Firefox can start multiple instances with private windows.<commit_after>var path = require('path');

module.exports = {
  chrome: {
    pathQuery: 'which google-chrome',
    process: 'chrome',
    opensTab: true
  },
  firefox: {
    pathQuery: 'which firefox',
    process: 'firefox',
    multi: true,
    // See https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options
    // Assuming Firefox >= 20
    args: ['--private-window']
  },
  opera: {
    pathQuery: 'which opera',
    process: 'opera'
  },
  phantom: {
    pathQuery: 'which phantomjs',
    process: 'phantomjs',
    args: [path.join(__dirname, '..', '..', '..', 'resources/phantom.js')],
    multi: true
  }
};
<|endoftext|>


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

var http = require('http');
var url = require('url');

var static = require('node-static');
var redis  = require('redis');

var countdown = require('countdown');

function Manager(queueName) {
  var that = this;

  var listClient = redis.createClient();
  listClient.on('error', function(err) {
    console.log(err);
  });

  var zsetClient = redis.createClient();
  zsetClient.on('error', function(err) {
    console.log(err);
  });

  var addListKey = queueName+ '_a';
  var pendingListKey = queueName+ '_p';
  var takenListKey = queueName+ '_t';
  var zsetKey = queueName+ '_z';

  var timebox = 3000; // 3s

  var pickTimeoutId = null;

  var queuedAdds = [];

  function isTimeboxEnd(t) {
    return Math.floor(t) !== t;
  }

  function handleList() {

    console.error('pulling from', addListKey);

    // FIXME: what if the manager crashes before processing this message? don't just pop it, move it to a separate queue

    listClient.blpop(addListKey, 0, function(err, results) {
      if(err !== null) {
        console.error('blpop', err);
        return setTimeout(handleList, 300);
      }
      console.error('manager pulled out', results);
      var mJSON = results[1];
      var m;
      try {
        m = JSON.parse(mJSON);
      } catch(parseErr) {
        console.error(parseErr);
        return process.nextTick(handleList);
      }
      console.error('m',m);

      var taskId = m.id;
      var taskWhen = m.when;

      if(taskId === undefined) {
        console.error('undefined taskId');
        return process.nextTick(handleList);
      }
      if(taskWhen === undefined) {
        console.error('undefined task-when');
        return process.nextTick(handleList);
      }
      if(taskWhen !== null && (typeof taskWhen !== 'number' || Math.floor(taskWhen) !== taskWhen)) {
        console.error('task-when must be null or an integer');
        return process.nextTick(handleList);
      }

      console.error('queueing', m);
      queuedAdds.push(m);
      console.error('nudging pick');
      nudgePick();

      process.nextTick(handleList);
    });
  }

  function exec(taskId) {
    console.log('todo', taskId);
    zsetClient.lpush(pendingListKey, taskId, function(err, result) {
      if(err) {
        console.error('could not push', taskId, 'to pending');
        // FIXME: keep on trying?
        return;
      }
      console.log(taskId, 'pushed to pending');
    });
  }

  var inPick = false;

  function pickTimeout() {
    inPickTimeout = true;
    if(pickTimeoutId !== null) {
      clearTimeout(pickTimeoutId);
      pickTimeoutId = null;
    }

    console.error('pick in...');
    inPick = true;

    if(queuedAdds.length > 0) {
      var m = queuedAdds.shift();
      console.error('shifted', m);
      var taskId = m.id;
      var taskWhen = m.when;
      if(taskWhen === null) {
        zsetClient.zrem(zsetKey, taskId, function(err, result) {
          if(err) {
            console.error(err);
            // FIXME: handle this
            throw 'fixme';
          }
          inPick = false;
          pickTimeoutId = setTimeout(pickTimeout, 0);
        });
      } else {
        console.error('zadd(1)', taskWhen, 'on', taskId);
        zsetClient.zadd(zsetKey, taskWhen, taskId, function(err, result) {
          if(err) {
            console.error(err);
            // FIXME: handle this
            throw 'fixme';
          }
          console.error('zadd(1) succeeded');
          taskCache[taskId] = taskWhen;
          inPick = false;
          pickTimeoutId = setTimeout(pickTimeout, 0);
        });
      }
      return;
    }

    console.error('calling pick...');
    pick(function(nextTime, now) {
      console.log('nextTime = ', nextTime);
      if(queuedAdds.length > 0)
        nextTime = now;
      if(nextTime !== null) {
        var wait = nextTime - now;
        console.error('wait', wait);
        pickTimeoutId = setTimeout(pickTimeout, wait);
      } else
        console.error('nothing to wait for');
      inPick = false;
      console.error('... out pick');
    });
  }

  function nudgePick() {
    // if inPick we can rely on the queue being check before setting inPick back to false
    // but otherwise we want to make sure the pick timeout will run right away
    if(!inPick) {
      console.error('!inPick');
      if(pickTimeoutId) {
        clearTimeout(pickTimeoutId);
        pickTimeoutId = null;
      }
      console.error('setting 0 timeout');
      return pickTimeoutId = setTimeout(pickTimeout, 0);
    }
  }

  function pick(cb) {
    zsetClient.zrangebyscore(zsetKey, '-inf', '+inf', 'WITHSCORES', 'LIMIT', 0, 1, function(err, result) {
      if(err) {
        throw err;
      }

      var now = new Date().getTime();

      if(result.length === 0) {
        console.error('nothing yet to pick');
        return cb(null, now);
      }

      console.log('result', result);

      var taskId = result[0];
      var targetTime = parseFloat(result[1]);

      taskCache[taskId] = targetTime;

      if(targetTime > now) {
        console.log('next task', taskId, 'only at', targetTime, 'now', now);
        return cb(targetTime, now);
      }

      var isTb = isTimeboxEnd(targetTime);

      if(isTb) {
        console.error(taskId, 'taking too long');
        // FIXME: check if it was picked up
      } else {
        exec(taskId);
      }

      var tbTime = now + timebox + 0.5;
      console.log('zadd(2)', tbTime, 'on', taskId);
      zsetClient.zadd(zsetKey, tbTime , taskId, function(err, result) {
        if(err)
          throw err; // FIXME - handle this properly
        console.log('zadd(2) successful');
        taskCache[taskId] = tbTime;
        console.error('will check on', taskId, 'at', tbTime);
        return cb(tbTime, now);
      });
    });
  }

  function handlezset() {
    pickTimeout();
  }

  var taskCache = {};

  that.go = function() {
    zsetClient.zrange(zsetKey, 0, 10000, 'withscores', function(err, result) {
      if(err) {
        throw 'FIXME';
      }
      var i;
      for(i = 0; i < result.length; i += 2) {
        var taskId = result[i];
        var taskTime = result[i + 1];
        taskCache[taskId] = taskTime;
      }
      console.log(result);
      handleList();
      handlezset();
    });
  };

  http.createServer(function(req, res) {
    function descTimeRemaining(ta, tb) {
      var dt = tb - ta;
      if(dt < 1000)
        return 'less than 1 second';
      return countdown(new Date(ta), new Date(tb)).toString(2);
    }

    var parsedUrl = url.parse(req.url, true);
    if(parsedUrl.pathname === '/') {
      var now = new Date();
      var nowTime = now.getTime();
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('');
      res.write('
');
      var i;
      for(taskId in taskCache) {
        var taskTime = taskCache[taskId];
        res.write('
');
        res.write(''+ taskId+ '');
        res.write(':');
        if(nowTime >= taskTime)
          res.write('just starting');
        else {
          if(Math.floor(taskTime) === taskTime)
            res.write('due in '+ descTimeRemaining(nowTime, taskTime)+ '');
          else
            res.write('running, will timeout in '+ descTimeRemaining(nowTime, taskTime)+ '');
        }
        res.write('
');
      }
      res.write('
');
      res.write('');
      res.end();
    } else {
      res.writeHead(404, {'Content-Type': 'text/plain'});
      return res.end('page not found');
    }
  }).listen(9999);
}

var m = new Manager('q');
m.go();
<commit_msg>countdown can handle timestamps<commit_after>#!/usr/bin/env node

var http = require('http');
var url = require('url');

var static = require('node-static');
var redis  = require('redis');

var countdown = require('countdown');

function Manager(queueName) {
  var that = this;

  var listClient = redis.createClient();
  listClient.on('error', function(err) {
    console.log(err);
  });

  var zsetClient = redis.createClient();
  zsetClient.on('error', function(err) {
    console.log(err);
  });

  var addListKey = queueName+ '_a';
  var pendingListKey = queueName+ '_p';
  var takenListKey = queueName+ '_t';
  var zsetKey = queueName+ '_z';

  var timebox = 3000; // 3s

  var pickTimeoutId = null;

  var queuedAdds = [];

  function isTimeboxEnd(t) {
    return Math.floor(t) !== t;
  }

  function handleList() {

    console.error('pulling from', addListKey);

    // FIXME: what if the manager crashes before processing this message? don't just pop it, move it to a separate queue

    listClient.blpop(addListKey, 0, function(err, results) {
      if(err !== null) {
        console.error('blpop', err);
        return setTimeout(handleList, 300);
      }
      console.error('manager pulled out', results);
      var mJSON = results[1];
      var m;
      try {
        m = JSON.parse(mJSON);
      } catch(parseErr) {
        console.error(parseErr);
        return process.nextTick(handleList);
      }
      console.error('m',m);

      var taskId = m.id;
      var taskWhen = m.when;

      if(taskId === undefined) {
        console.error('undefined taskId');
        return process.nextTick(handleList);
      }
      if(taskWhen === undefined) {
        console.error('undefined task-when');
        return process.nextTick(handleList);
      }
      if(taskWhen !== null && (typeof taskWhen !== 'number' || Math.floor(taskWhen) !== taskWhen)) {
        console.error('task-when must be null or an integer');
        return process.nextTick(handleList);
      }

      console.error('queueing', m);
      queuedAdds.push(m);
      console.error('nudging pick');
      nudgePick();

      process.nextTick(handleList);
    });
  }

  function exec(taskId) {
    console.log('todo', taskId);
    zsetClient.lpush(pendingListKey, taskId, function(err, result) {
      if(err) {
        console.error('could not push', taskId, 'to pending');
        // FIXME: keep on trying?
        return;
      }
      console.log(taskId, 'pushed to pending');
    });
  }

  var inPick = false;

  function pickTimeout() {
    inPickTimeout = true;
    if(pickTimeoutId !== null) {
      clearTimeout(pickTimeoutId);
      pickTimeoutId = null;
    }

    console.error('pick in...');
    inPick = true;

    if(queuedAdds.length > 0) {
      var m = queuedAdds.shift();
      console.error('shifted', m);
      var taskId = m.id;
      var taskWhen = m.when;
      if(taskWhen === null) {
        zsetClient.zrem(zsetKey, taskId, function(err, result) {
          if(err) {
            console.error(err);
            // FIXME: handle this
            throw 'fixme';
          }
          inPick = false;
          pickTimeoutId = setTimeout(pickTimeout, 0);
        });
      } else {
        console.error('zadd(1)', taskWhen, 'on', taskId);
        zsetClient.zadd(zsetKey, taskWhen, taskId, function(err, result) {
          if(err) {
            console.error(err);
            // FIXME: handle this
            throw 'fixme';
          }
          console.error('zadd(1) succeeded');
          taskCache[taskId] = taskWhen;
          inPick = false;
          pickTimeoutId = setTimeout(pickTimeout, 0);
        });
      }
      return;
    }

    console.error('calling pick...');
    pick(function(nextTime, now) {
      console.log('nextTime = ', nextTime);
      if(queuedAdds.length > 0)
        nextTime = now;
      if(nextTime !== null) {
        var wait = nextTime - now;
        console.error('wait', wait);
        pickTimeoutId = setTimeout(pickTimeout, wait);
      } else
        console.error('nothing to wait for');
      inPick = false;
      console.error('... out pick');
    });
  }

  function nudgePick() {
    // if inPick we can rely on the queue being check before setting inPick back to false
    // but otherwise we want to make sure the pick timeout will run right away
    if(!inPick) {
      console.error('!inPick');
      if(pickTimeoutId) {
        clearTimeout(pickTimeoutId);
        pickTimeoutId = null;
      }
      console.error('setting 0 timeout');
      return pickTimeoutId = setTimeout(pickTimeout, 0);
    }
  }

  function pick(cb) {
    zsetClient.zrangebyscore(zsetKey, '-inf', '+inf', 'WITHSCORES', 'LIMIT', 0, 1, function(err, result) {
      if(err) {
        throw err;
      }

      var now = new Date().getTime();

      if(result.length === 0) {
        console.error('nothing yet to pick');
        return cb(null, now);
      }

      console.log('result', result);

      var taskId = result[0];
      var targetTime = parseFloat(result[1]);

      taskCache[taskId] = targetTime;

      if(targetTime > now) {
        console.log('next task', taskId, 'only at', targetTime, 'now', now);
        return cb(targetTime, now);
      }

      var isTb = isTimeboxEnd(targetTime);

      if(isTb) {
        console.error(taskId, 'taking too long');
        // FIXME: check if it was picked up
      } else {
        exec(taskId);
      }

      var tbTime = now + timebox + 0.5;
      console.log('zadd(2)', tbTime, 'on', taskId);
      zsetClient.zadd(zsetKey, tbTime , taskId, function(err, result) {
        if(err)
          throw err; // FIXME - handle this properly
        console.log('zadd(2) successful');
        taskCache[taskId] = tbTime;
        console.error('will check on', taskId, 'at', tbTime);
        return cb(tbTime, now);
      });
    });
  }

  function handlezset() {
    pickTimeout();
  }

  var taskCache = {};

  that.go = function() {
    zsetClient.zrange(zsetKey, 0, 10000, 'withscores', function(err, result) {
      if(err) {
        throw 'FIXME';
      }
      var i;
      for(i = 0; i < result.length; i += 2) {
        var taskId = result[i];
        var taskTime = result[i + 1];
        taskCache[taskId] = taskTime;
      }
      console.log(result);
      handleList();
      handlezset();
    });
  };

  http.createServer(function(req, res) {
    function descTimeRemaining(ta, tb) {
      var dt = tb - ta;
      if(dt < 1000)
        return 'less than 1 second';
      return countdown(ta, tb).toString(2);
    }

    var parsedUrl = url.parse(req.url, true);
    if(parsedUrl.pathname === '/') {
      var now = new Date();
      var nowTime = now.getTime();
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.write('');
      res.write('
');
      var i;
      for(taskId in taskCache) {
        var taskTime = taskCache[taskId];
        res.write('
');
        res.write(''+ taskId+ '');
        res.write(':');
        if(nowTime >= taskTime)
          res.write('just starting');
        else {
          if(Math.floor(taskTime) === taskTime)
            res.write('due in '+ descTimeRemaining(nowTime, taskTime)+ '');
          else
            res.write('running, will timeout in '+ descTimeRemaining(nowTime, taskTime)+ '');
        }
        res.write('
');
      }
      res.write('
');
      res.write('');
      res.end();
    } else {
      res.writeHead(404, {'Content-Type': 'text/plain'});
      return res.end('page not found');
    }
  }).listen(9999);
}

var m = new Manager('q');
m.go();
<|endoftext|>


<commit_before>/* eslint no-useless-escape: 0 */
const mimelib = require('mimelib');
const encoding = require('encoding');
const he = require('he');
const os = require('os');
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const {Errors: {APIError}} = require('isomorphic-core');
const {N1CloudAPI, RegExpUtils, Utils} = require('nylas-exports');

// Aiming for the former in length, but the latter is the hard db cutoff
const SNIPPET_SIZE = 100;
const SNIPPET_MAX_SIZE = 255;

// Format of input: ['a@example.com, B ', 'c@example.com'],
// where each element of the array is the unparsed contents of a single
// element of the same header field. (It's totally valid to have multiple
// From/To/etc. headers on the same email.)
function computeContacts(input) {
  if (!input || input.length === 0 || !input[0]) {
    return [];
  }
  let contacts = [];
  for (const headerLine of input) {
    const values = mimelib.parseAddresses(headerLine);
    if (!values || values.length === 0) {
      continue;
    }
    contacts = contacts.concat(values.map(v => {
      if (!v || v.length === 0) {
        return null
      }
      const {name, address: email} = v;
      return {name, email};
    })
    .filter(c => c != null))
  }
  return contacts;
}

function computeSnippet(body) {
  const doc = new DOMParser().parseFromString(body, 'text/html')
  const skipTags = new Set(['TITLE', 'SCRIPT', 'STYLE', 'IMG']);
  const noSpaceTags = new Set(['B', 'I', 'STRONG', 'EM', 'SPAN']);

  const treeWalker = document.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => {
    if (skipTags.has(node.tagName)) {
      // skip this node and all its children
      return NodeFilter.FILTER_REJECT;
    }
    if (node.nodeType === Node.TEXT_NODE) {
      const nodeValue = node.nodeValue ? node.nodeValue.trim() : null;
      if (nodeValue) {
        return NodeFilter.FILTER_ACCEPT;
      }
      return NodeFilter.FILTER_SKIP;
    }
    return NodeFilter.FILTER_ACCEPT;
  });

  let extractedText = "";
  let lastNodeTag = "";
  while (treeWalker.nextNode()) {
    if (treeWalker.currentNode.nodeType === Node.ELEMENT_NODE) {
      lastNodeTag = treeWalker.currentNode.nodeName;
    } else {
      if (extractedText && !noSpaceTags.has(lastNodeTag)) {
        extractedText += " ";
      }
      extractedText += treeWalker.currentNode.nodeValue;
      if (extractedText.length > SNIPPET_MAX_SIZE) {
        break;
      }
    }
  }
  const snippetText = extractedText.trim();

  // clean up and trim snippet
  let trimmed = snippetText.replace(/[\n\r]/g, ' ').replace(/\s\s+/g, ' ').substr(0, SNIPPET_MAX_SIZE);
  if (trimmed) {
    // TODO: strip quoted text from snippets also
    // trim down to approx. SNIPPET_SIZE w/out cutting off words right in the
    // middle (if possible)
    const wordBreak = trimmed.indexOf(' ', SNIPPET_SIZE);
    if (wordBreak !== -1) {
      trimmed = trimmed.substr(0, wordBreak);
    }
  }
  return trimmed;
}

function htmlifyPlaintext(text) {
  const escapedText = he.escape(text);
  return `<pre class="nylas-plaintext">${escapedText}</pre>`;
}

function replaceMessageIdInBodyTrackingLinks(messageId, originalBody) {
  const regex = new RegExp(`(${N1CloudAPI.APIRoot}.+?)MESSAGE_ID`, 'g')
  return originalBody.replace(regex, `$1${messageId}`)
}

function stripTrackingLinksFromBody(originalBody) {
  let body = originalBody.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:/.]*)">/g, () => {
    return "";
  });
  body = body.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
    const param = url.split("?")[1];
    if (param) {
      const link = decodeURIComponent(param.split("=")[1]);
      return `${prefix}${link}${suffix}${content}${closingTag}`;
    }
    return match;
  });
  return body;
}

function buildTrackingBodyForRecipient({baseMessage, recipient, usesOpenTracking, usesLinkTracking} = {}) {
  const {id: messageId, body} = baseMessage
  const encodedEmail = btoa(recipient.email)
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
  let customBody = body
  if (usesOpenTracking) {
    customBody = customBody.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:/.]*)">/g, (match, url) => {
      return `<img class="n1-open" width="0" height="0" style="border:0; width:0; height:0;" src="${url}?r=${encodedEmail}">`;
    });
  }
  if (usesLinkTracking) {
    customBody = customBody.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
      return `${prefix}${url}&r=${encodedEmail}${suffix}${content}${closingTag}`;
    });
  }
  return replaceMessageIdInBodyTrackingLinks(messageId, customBody);
}

function getReplyHeaders(messageReplyingTo) {
  let inReplyTo;
  let references;
  if (messageReplyingTo.headerMessageId) {
    inReplyTo = messageReplyingTo.headerMessageId;
    if (messageReplyingTo.references) {
      references = messageReplyingTo.references.concat(messageReplyingTo.headerMessageId);
    } else {
      references = [messageReplyingTo.headerMessageId];
    }
  }
  return {inReplyTo, references}
}

function bodyFromParts(imapMessage, desiredParts) {
  let body = '';
  for (const {id, mimeType, transferEncoding, charset} of desiredParts) {
    let decoded = '';
    // see https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html
    if (!transferEncoding || new Set(['7bit', '8bit', 'binary']).has(transferEncoding.toLowerCase())) {
      // NO transfer encoding has been performed --- how to decode to a string
      // depends ONLY on the charset, which defaults to 'ascii' according to
      // https://tools.ietf.org/html/rfc2045#section-5.2
      decoded = encoding.convert(imapMessage.parts[id], 'utf-8', charset || 'ascii').toString('utf-8');
    } else if (transferEncoding.toLowerCase() === 'quoted-printable') {
      decoded = mimelib.decodeQuotedPrintable(imapMessage.parts[id], charset || 'ascii');
    } else if (transferEncoding.toLowerCase() === 'base64') {
      decoded = mimelib.decodeBase64(imapMessage.parts[id], charset || 'ascii');
    } else {
      // custom x-token content-transfer-encodings
      return Promise.reject(new Error(`Unsupported Content-Transfer-Encoding ${transferEncoding}, mimetype ${mimeType}`))
    }
    // desiredParts are in order of the MIME tree walk, e.g. 1.1, 1.2, 2...,
    // and for multipart/alternative arrays, we have already pulled out the
    // highest fidelity part (generally HTML).
    //
    // Therefore, the correct way to display multiple parts is to simply
    // concatenate later ones with the body of the previous MIME parts.
    //
    // This may seem kind of weird, but some MUAs _do_ send out whack stuff
    // like an HTML body followed by a plaintext footer.
    if (mimeType === 'text/plain') {
      body += htmlifyPlaintext(decoded);
    } else {
      body += decoded;
    }
  }
  // sometimes decoding results in a NUL-terminated body string, which makes
  // SQLite blow up with an 'unrecognized token' error
  body = body.replace(/\0/g, '');

  return body;
}

// Since we only fetch the MIME structure and specific desired MIME parts from
// IMAP, we unfortunately can't use an existing library like mailparser to parse
// the message, and have to do fun stuff like deal with character sets and
// content-transfer-encodings ourselves.
async function parseFromImap(imapMessage, desiredParts, {db, accountId, folder}) {
  const {Message, Label} = db;
  const {attributes} = imapMessage;

  // this key name can change depending on which subset of headers we're downloading,
  // so to prevent having to update this code every time we change the set,
  // dynamically look up the key instead
  const headerKey = Object.keys(imapMessage.parts).filter(k => k.startsWith('HEADER'))[0]
  const headers = imapMessage.parts[headerKey].toString('ascii')
  const parsedHeaders = mimelib.parseHeaders(headers);
  for (const key of ['x-gm-thrid', 'x-gm-msgid', 'x-gm-labels']) {
    parsedHeaders[key] = attributes[key];
  }

  const parsedMessage = {
    to: computeContacts(parsedHeaders.to),
    cc: computeContacts(parsedHeaders.cc),
    bcc: computeContacts(parsedHeaders.bcc),
    from: computeContacts(parsedHeaders.from),
    replyTo: computeContacts(parsedHeaders['reply-to']),
    accountId: accountId,
    body: bodyFromParts(imapMessage, desiredParts),
    snippet: null,
    unread: !attributes.flags.includes('\\Seen'),
    starred: attributes.flags.includes('\\Flagged'),
    // We prefer the date from the message headers because the date is one of
    // the fields we use for generating unique message IDs, and the server
    // INTERNALDATE, `attributes.date`, may differ across accounts for the same
    // message. If the Date header is not included in the message, we fall
    // back to the INTERNALDATE and it's possible we'll generate different IDs
    // for the same message delivered to different accounts (which is better
    // than having message ID collisions for different messages, which could
    // happen if we did not include the date).
    date: parsedHeaders.date ? parsedHeaders.date[0] : imapMessage.attributes.date,
    folderImapUID: attributes.uid,
    folderId: folder.id,
    folder: null,
    labels: [],
    headerMessageId: parsedHeaders['message-id'] ? parsedHeaders['message-id'][0] : '',
    gMsgId: parsedHeaders['x-gm-msgid'],
    gThrId: parsedHeaders['x-gm-thrid'],
    subject: parsedHeaders.subject ? parsedHeaders.subject[0] : '(no subject)',
  }
  // Inversely to `buildForSend`, we leave the date header as it is so that the
  // format is consistent for the generative IDs, then convert it to a Date object
  parsedMessage.id = Message.hash(parsedMessage)
  parsedMessage.date = new Date(Date.parse(parsedMessage.date))

  parsedMessage.snippet = computeSnippet(parsedMessage.body);
  parsedMessage.folder = folder;

  // TODO: unclear if this is necessary given we already have parsed labels
  const xGmLabels = attributes['x-gm-labels']
  if (xGmLabels) {
    parsedMessage.folderImapXGMLabels = JSON.stringify(xGmLabels)
    parsedMessage.labels = await Label.findXGMLabels(xGmLabels)
  }

  if (process.env.NYLAS_DEBUG) {
    const outJSON = JSON.stringify({imapMessage, desiredParts, result: parsedMessage});
    const outDir = path.join(os.tmpdir(), "k2-parse-output", folder.name)
    const outFile = path.join(outDir, imapMessage.attributes.uid.toString());
    mkdirp.sync(outDir);
    fs.writeFileSync(outFile, outJSON);
  }

  return parsedMessage;
}

async function buildForSend(db, json) {
  const {Thread, Message} = db
  let replyToThread;
  let replyToMessage;

  if (json.thread_id != null) {
    replyToThread = await Thread.find({
      where: {id: json.thread_id},
      include: [{
        model: Message,
        as: 'messages',
        attributes: ['id'],
      }],
    });
  }

  if (json.reply_to_message_id != null) {
    replyToMessage = await Message.findById(json.reply_to_message_id);
  }

  if (replyToThread && replyToMessage) {
    if (!replyToThread.messages.find((msg) => msg.id === replyToMessage.id)) {
      throw new APIError(`Message ${replyToMessage.id} is not in thread ${replyToThread.id}`, 400)
    }
  }

  let thread;
  let replyHeaders = {};
  if (replyToMessage) {
    replyHeaders = getReplyHeaders(replyToMessage);
    thread = await replyToMessage.getThread();
  } else if (replyToThread) {
    thread = replyToThread;
    const previousMessages = thread.messages.filter(msg => !msg.isDraft);
    if (previousMessages.length > 0) {
      const lastMessage = previousMessages[previousMessages.length - 1]
      replyHeaders = getReplyHeaders(lastMessage);
    }
  }

  const {inReplyTo, references} = replyHeaders
  const date = new Date()
  const message = {
    accountId: json.account_id,
    threadId: thread ? thread.id : null,
    headerMessageId: Message.buildHeaderMessageId(json.client_id),
    from: json.from,
    to: json.to,
    cc: json.cc,
    bcc: json.bcc,
    references,
    inReplyTo,
    replyTo: json.reply_to,
    subject: json.subject,
    body: json.body,
    unread: true,
    isDraft: json.draft,
    isSent: false,
    version: 0,
    date: date,
    uploads: json.uploads,
  }
  // We have to clone the message and change the date for hashing because the
  // date we get later when we parse from IMAP is a different format, per the
  // nodemailer buildmail function that gives us the raw message and replaces
  // the date header with this modified UTC string
  // https://github.com/nodemailer/buildmail/blob/master/lib/buildmail.js#L470
  const messageForHashing = Utils.deepClone(message)
  messageForHashing.date = date.toUTCString().replace(/GMT/, '+0000')
  message.id = Message.hash(messageForHashing)
  message.body = replaceMessageIdInBodyTrackingLinks(message.id, message.body)
  return Message.build(message)
}

module.exports = {
  buildForSend,
  parseFromImap,
  computeSnippet,
  computeContacts,
  stripTrackingLinksFromBody,
  buildTrackingBodyForRecipient,
  replaceMessageIdInBodyTrackingLinks,
}
<commit_msg>[local-sync] Messages generated for send should not be unread<commit_after>/* eslint no-useless-escape: 0 */
const mimelib = require('mimelib');
const encoding = require('encoding');
const he = require('he');
const os = require('os');
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const {Errors: {APIError}} = require('isomorphic-core');
const {N1CloudAPI, RegExpUtils, Utils} = require('nylas-exports');

// Aiming for the former in length, but the latter is the hard db cutoff
const SNIPPET_SIZE = 100;
const SNIPPET_MAX_SIZE = 255;

// Format of input: ['a@example.com, B ', 'c@example.com'],
// where each element of the array is the unparsed contents of a single
// element of the same header field. (It's totally valid to have multiple
// From/To/etc. headers on the same email.)
function computeContacts(input) {
  if (!input || input.length === 0 || !input[0]) {
    return [];
  }
  let contacts = [];
  for (const headerLine of input) {
    const values = mimelib.parseAddresses(headerLine);
    if (!values || values.length === 0) {
      continue;
    }
    contacts = contacts.concat(values.map(v => {
      if (!v || v.length === 0) {
        return null
      }
      const {name, address: email} = v;
      return {name, email};
    })
    .filter(c => c != null))
  }
  return contacts;
}

function computeSnippet(body) {
  const doc = new DOMParser().parseFromString(body, 'text/html')
  const skipTags = new Set(['TITLE', 'SCRIPT', 'STYLE', 'IMG']);
  const noSpaceTags = new Set(['B', 'I', 'STRONG', 'EM', 'SPAN']);

  const treeWalker = document.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => {
    if (skipTags.has(node.tagName)) {
      // skip this node and all its children
      return NodeFilter.FILTER_REJECT;
    }
    if (node.nodeType === Node.TEXT_NODE) {
      const nodeValue = node.nodeValue ? node.nodeValue.trim() : null;
      if (nodeValue) {
        return NodeFilter.FILTER_ACCEPT;
      }
      return NodeFilter.FILTER_SKIP;
    }
    return NodeFilter.FILTER_ACCEPT;
  });

  let extractedText = "";
  let lastNodeTag = "";
  while (treeWalker.nextNode()) {
    if (treeWalker.currentNode.nodeType === Node.ELEMENT_NODE) {
      lastNodeTag = treeWalker.currentNode.nodeName;
    } else {
      if (extractedText && !noSpaceTags.has(lastNodeTag)) {
        extractedText += " ";
      }
      extractedText += treeWalker.currentNode.nodeValue;
      if (extractedText.length > SNIPPET_MAX_SIZE) {
        break;
      }
    }
  }
  const snippetText = extractedText.trim();

  // clean up and trim snippet
  let trimmed = snippetText.replace(/[\n\r]/g, ' ').replace(/\s\s+/g, ' ').substr(0, SNIPPET_MAX_SIZE);
  if (trimmed) {
    // TODO: strip quoted text from snippets also
    // trim down to approx. SNIPPET_SIZE w/out cutting off words right in the
    // middle (if possible)
    const wordBreak = trimmed.indexOf(' ', SNIPPET_SIZE);
    if (wordBreak !== -1) {
      trimmed = trimmed.substr(0, wordBreak);
    }
  }
  return trimmed;
}

function htmlifyPlaintext(text) {
  const escapedText = he.escape(text);
  return `<pre class="nylas-plaintext">${escapedText}</pre>`;
}

function replaceMessageIdInBodyTrackingLinks(messageId, originalBody) {
  const regex = new RegExp(`(${N1CloudAPI.APIRoot}.+?)MESSAGE_ID`, 'g')
  return originalBody.replace(regex, `$1${messageId}`)
}

function stripTrackingLinksFromBody(originalBody) {
  let body = originalBody.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:/.]*)">/g, () => {
    return "";
  });
  body = body.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
    const param = url.split("?")[1];
    if (param) {
      const link = decodeURIComponent(param.split("=")[1]);
      return `${prefix}${link}${suffix}${content}${closingTag}`;
    }
    return match;
  });
  return body;
}

function buildTrackingBodyForRecipient({baseMessage, recipient, usesOpenTracking, usesLinkTracking} = {}) {
  const {id: messageId, body} = baseMessage
  const encodedEmail = btoa(recipient.email)
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
  let customBody = body
  if (usesOpenTracking) {
    customBody = customBody.replace(/<img class="n1-open"[^<]+src="([a-zA-Z0-9-_:/.]*)">/g, (match, url) => {
      return `<img class="n1-open" width="0" height="0" style="border:0; width:0; height:0;" src="${url}?r=${encodedEmail}">`;
    });
  }
  if (usesLinkTracking) {
    customBody = customBody.replace(RegExpUtils.urlLinkTagRegex(), (match, prefix, url, suffix, content, closingTag) => {
      return `${prefix}${url}&r=${encodedEmail}${suffix}${content}${closingTag}`;
    });
  }
  return replaceMessageIdInBodyTrackingLinks(messageId, customBody);
}

function getReplyHeaders(messageReplyingTo) {
  let inReplyTo;
  let references;
  if (messageReplyingTo.headerMessageId) {
    inReplyTo = messageReplyingTo.headerMessageId;
    if (messageReplyingTo.references) {
      references = messageReplyingTo.references.concat(messageReplyingTo.headerMessageId);
    } else {
      references = [messageReplyingTo.headerMessageId];
    }
  }
  return {inReplyTo, references}
}

function bodyFromParts(imapMessage, desiredParts) {
  let body = '';
  for (const {id, mimeType, transferEncoding, charset} of desiredParts) {
    let decoded = '';
    // see https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html
    if (!transferEncoding || new Set(['7bit', '8bit', 'binary']).has(transferEncoding.toLowerCase())) {
      // NO transfer encoding has been performed --- how to decode to a string
      // depends ONLY on the charset, which defaults to 'ascii' according to
      // https://tools.ietf.org/html/rfc2045#section-5.2
      decoded = encoding.convert(imapMessage.parts[id], 'utf-8', charset || 'ascii').toString('utf-8');
    } else if (transferEncoding.toLowerCase() === 'quoted-printable') {
      decoded = mimelib.decodeQuotedPrintable(imapMessage.parts[id], charset || 'ascii');
    } else if (transferEncoding.toLowerCase() === 'base64') {
      decoded = mimelib.decodeBase64(imapMessage.parts[id], charset || 'ascii');
    } else {
      // custom x-token content-transfer-encodings
      return Promise.reject(new Error(`Unsupported Content-Transfer-Encoding ${transferEncoding}, mimetype ${mimeType}`))
    }
    // desiredParts are in order of the MIME tree walk, e.g. 1.1, 1.2, 2...,
    // and for multipart/alternative arrays, we have already pulled out the
    // highest fidelity part (generally HTML).
    //
    // Therefore, the correct way to display multiple parts is to simply
    // concatenate later ones with the body of the previous MIME parts.
    //
    // This may seem kind of weird, but some MUAs _do_ send out whack stuff
    // like an HTML body followed by a plaintext footer.
    if (mimeType === 'text/plain') {
      body += htmlifyPlaintext(decoded);
    } else {
      body += decoded;
    }
  }
  // sometimes decoding results in a NUL-terminated body string, which makes
  // SQLite blow up with an 'unrecognized token' error
  body = body.replace(/\0/g, '');

  return body;
}

// Since we only fetch the MIME structure and specific desired MIME parts from
// IMAP, we unfortunately can't use an existing library like mailparser to parse
// the message, and have to do fun stuff like deal with character sets and
// content-transfer-encodings ourselves.
async function parseFromImap(imapMessage, desiredParts, {db, accountId, folder}) {
  const {Message, Label} = db;
  const {attributes} = imapMessage;

  // this key name can change depending on which subset of headers we're downloading,
  // so to prevent having to update this code every time we change the set,
  // dynamically look up the key instead
  const headerKey = Object.keys(imapMessage.parts).filter(k => k.startsWith('HEADER'))[0]
  const headers = imapMessage.parts[headerKey].toString('ascii')
  const parsedHeaders = mimelib.parseHeaders(headers);
  for (const key of ['x-gm-thrid', 'x-gm-msgid', 'x-gm-labels']) {
    parsedHeaders[key] = attributes[key];
  }

  const parsedMessage = {
    to: computeContacts(parsedHeaders.to),
    cc: computeContacts(parsedHeaders.cc),
    bcc: computeContacts(parsedHeaders.bcc),
    from: computeContacts(parsedHeaders.from),
    replyTo: computeContacts(parsedHeaders['reply-to']),
    accountId: accountId,
    body: bodyFromParts(imapMessage, desiredParts),
    snippet: null,
    unread: !attributes.flags.includes('\\Seen'),
    starred: attributes.flags.includes('\\Flagged'),
    // We prefer the date from the message headers because the date is one of
    // the fields we use for generating unique message IDs, and the server
    // INTERNALDATE, `attributes.date`, may differ across accounts for the same
    // message. If the Date header is not included in the message, we fall
    // back to the INTERNALDATE and it's possible we'll generate different IDs
    // for the same message delivered to different accounts (which is better
    // than having message ID collisions for different messages, which could
    // happen if we did not include the date).
    date: parsedHeaders.date ? parsedHeaders.date[0] : imapMessage.attributes.date,
    folderImapUID: attributes.uid,
    folderId: folder.id,
    folder: null,
    labels: [],
    headerMessageId: parsedHeaders['message-id'] ? parsedHeaders['message-id'][0] : '',
    gMsgId: parsedHeaders['x-gm-msgid'],
    gThrId: parsedHeaders['x-gm-thrid'],
    subject: parsedHeaders.subject ? parsedHeaders.subject[0] : '(no subject)',
  }
  // Inversely to `buildForSend`, we leave the date header as it is so that the
  // format is consistent for the generative IDs, then convert it to a Date object
  parsedMessage.id = Message.hash(parsedMessage)
  parsedMessage.date = new Date(Date.parse(parsedMessage.date))

  parsedMessage.snippet = computeSnippet(parsedMessage.body);
  parsedMessage.folder = folder;

  // TODO: unclear if this is necessary given we already have parsed labels
  const xGmLabels = attributes['x-gm-labels']
  if (xGmLabels) {
    parsedMessage.folderImapXGMLabels = JSON.stringify(xGmLabels)
    parsedMessage.labels = await Label.findXGMLabels(xGmLabels)
  }

  if (process.env.NYLAS_DEBUG) {
    const outJSON = JSON.stringify({imapMessage, desiredParts, result: parsedMessage});
    const outDir = path.join(os.tmpdir(), "k2-parse-output", folder.name)
    const outFile = path.join(outDir, imapMessage.attributes.uid.toString());
    mkdirp.sync(outDir);
    fs.writeFileSync(outFile, outJSON);
  }

  return parsedMessage;
}

async function buildForSend(db, json) {
  const {Thread, Message} = db
  let replyToThread;
  let replyToMessage;

  if (json.thread_id != null) {
    replyToThread = await Thread.find({
      where: {id: json.thread_id},
      include: [{
        model: Message,
        as: 'messages',
        attributes: ['id'],
      }],
    });
  }

  if (json.reply_to_message_id != null) {
    replyToMessage = await Message.findById(json.reply_to_message_id);
  }

  if (replyToThread && replyToMessage) {
    if (!replyToThread.messages.find((msg) => msg.id === replyToMessage.id)) {
      throw new APIError(`Message ${replyToMessage.id} is not in thread ${replyToThread.id}`, 400)
    }
  }

  let thread;
  let replyHeaders = {};
  if (replyToMessage) {
    replyHeaders = getReplyHeaders(replyToMessage);
    thread = await replyToMessage.getThread();
  } else if (replyToThread) {
    thread = replyToThread;
    const previousMessages = thread.messages.filter(msg => !msg.isDraft);
    if (previousMessages.length > 0) {
      const lastMessage = previousMessages[previousMessages.length - 1]
      replyHeaders = getReplyHeaders(lastMessage);
    }
  }

  const {inReplyTo, references} = replyHeaders
  const date = new Date()
  const message = {
    accountId: json.account_id,
    threadId: thread ? thread.id : null,
    headerMessageId: Message.buildHeaderMessageId(json.client_id),
    from: json.from,
    to: json.to,
    cc: json.cc,
    bcc: json.bcc,
    references,
    inReplyTo,
    replyTo: json.reply_to,
    subject: json.subject,
    body: json.body,
    unread: false,
    isDraft: json.draft,
    isSent: false,
    version: 0,
    date: date,
    uploads: json.uploads,
  }
  // We have to clone the message and change the date for hashing because the
  // date we get later when we parse from IMAP is a different format, per the
  // nodemailer buildmail function that gives us the raw message and replaces
  // the date header with this modified UTC string
  // https://github.com/nodemailer/buildmail/blob/master/lib/buildmail.js#L470
  const messageForHashing = Utils.deepClone(message)
  messageForHashing.date = date.toUTCString().replace(/GMT/, '+0000')
  message.id = Message.hash(messageForHashing)
  message.body = replaceMessageIdInBodyTrackingLinks(message.id, message.body)
  return Message.build(message)
}

module.exports = {
  buildForSend,
  parseFromImap,
  computeSnippet,
  computeContacts,
  stripTrackingLinksFromBody,
  buildTrackingBodyForRecipient,
  replaceMessageIdInBodyTrackingLinks,
}
<|endoftext|>


<commit_before>//     =============================================
//
//     WWW.QUIS.CC
//     ---------------------------------------------
//
//     By Chris Hill-Scott, except where noted.
//
//     =============================================

$(function() {

    for (var module in QUIS) {

      if (QUIS[module].init) QUIS[module].init();

    }

    if (!QUIS.isMobile) {

      $(".next-link")
          .hide();

    }

    // Automatically load new photos when scrolling near bottom of page
    $("#photos")
      .infinitescroll(
        {
          navSelector: "footer",
          nextSelector: ".next-link a",
          itemSelector: ".unit",
          loadingText: "Loading more photos",
          donetext: "No more photos",
          callback: function(path, pageID) {

            // size newly-loaded images
            $("#" + pageID + " " + QUIS.imageSelector)
              .imageScale();

            QUIS.map.renderMap();

            if (_gaq) {
              // Notify Google Analytics of page view
              _gaq.push(['_trackPageview', path]);
            }

          }
        }
      );

    // Keyboard navigation controller
    $(document)
      .keydown(
        function(event) {

          switch (event.keyCode || event.which) {
            case 74: // j
            case 40: // down arrow
              return scrollPhotos.go(1);
            case 75: // k
            case 38: // up arrow
              return scrollPhotos.go(-1);
          }

        }
      );

});
<commit_msg>Remove reference to old imagescaling in infinite scroll<commit_after>//     =============================================
//
//     WWW.QUIS.CC
//     ---------------------------------------------
//
//     By Chris Hill-Scott, except where noted.
//
//     =============================================

$(function() {

    for (var module in QUIS) {

      if (QUIS[module].init) QUIS[module].init();

    }

    if (!QUIS.isMobile) {

      $(".next-link")
          .hide();

    }

    // Automatically load new photos when scrolling near bottom of page
    $("#photos")
      .infinitescroll(
        {
          navSelector: "footer",
          nextSelector: ".next-link a",
          itemSelector: ".unit",
          loadingText: "Loading more photos",
          donetext: "No more photos",
          callback: function(path, pageID) {

            QUIS.map.renderMap();

            if (_gaq) {
              // Notify Google Analytics of page view
              _gaq.push(['_trackPageview', path]);
            }

          }
        }
      );

    // Keyboard navigation controller
    $(document)
      .keydown(
        function(event) {

          switch (event.keyCode || event.which) {
            case 74: // j
            case 40: // down arrow
              return scrollPhotos.go(1);
            case 75: // k
            case 38: // up arrow
              return scrollPhotos.go(-1);
          }

        }
      );

});
<|endoftext|>


<commit_before>Template.materializeButtons.rendered = function() {
  Session.set("orion_autoformLoading", undefined);
};
<commit_msg>Update buttons.js<commit_after>Template.materializeButtons.onRendered(function() {
  Session.set("orion_autoformLoading", undefined);
});
<|endoftext|>


<commit_before>// Copyright 2018-2021, University of Colorado Boulder

/**
 * Model for the 'Focus & Directrix' screen.
 * Alternate vertex form of the quadratic equation is: y = (1/(4p)(x - h)^2 - k
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import DerivedProperty from '../../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../../axon/js/NumberProperty.js';
import RangeWithValue from '../../../../dot/js/RangeWithValue.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import Vector2Property from '../../../../dot/js/Vector2Property.js';
import merge from '../../../../phet-core/js/merge.js';
import StringUtils from '../../../../phetcommon/js/util/StringUtils.js';
import GQColors from '../../common/GQColors.js';
import GQConstants from '../../common/GQConstants.js';
import GQModel from '../../common/model/GQModel.js';
import Quadratic from '../../common/model/Quadratic.js';
import graphingQuadratics from '../../graphingQuadratics.js';

// constants
const P_RANGE = new RangeWithValue( -9, 9, 2 ); // p coefficient
const H_RANGE = new RangeWithValue( -6, 6, 0 ); // h coefficient
const K_RANGE = new RangeWithValue( -6, 6, 0 ); // k coefficient
const POINT_X = 5; // default x value for point on parabola

class FocusAndDirectrixModel extends GQModel {

  /**
   * @param {Tandem} tandem
   */
  constructor( tandem ) {

    // p
    const pProperty = new NumberProperty( P_RANGE.defaultValue, merge( {
      range: P_RANGE,
      isValidValue: value => ( value !== 0 ), // zero is not supported
      tandem: tandem.createTandem( 'pProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'p' } )
    }, {
      // Opt out of providing a slider in Studio. A generic slider will cause problems, since zero is not supported.
      // See https://github.com/phetsims/graphing-quadratics/issues/52
      phetioStudioControl: false
    } ) );
    phet.log && pProperty.link( p => { phet.log( `p=${p}` ); } );

    // h
    const hProperty = new NumberProperty( H_RANGE.defaultValue, {
      range: H_RANGE,
      tandem: tandem.createTandem( 'hProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'h' } )
    } );
    phet.log && hProperty.link( h => { phet.log( `h=${h}` ); } );

    // k
    const kProperty = new NumberProperty( K_RANGE.defaultValue, {
      range: K_RANGE,
      tandem: tandem.createTandem( 'kProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'k' } )
    } );
    phet.log && kProperty.link( k => { phet.log( `k=${k}` ); } );

    // {DerivedProperty.<Quadratic>}
    const quadraticProperty = new DerivedProperty(
      [ pProperty, hProperty, kProperty ],
      ( p, h, k ) => Quadratic.createFromAlternateVertexForm( p, h, k, {
        color: GQColors.FOCUS_AND_DIRECTRIX_INTERACTIVE_CURVE
      } ), {
        tandem: tandem.createTandem( 'quadraticProperty' ),
        phetioDocumentation: 'the interactive quadratic, derived from p, h, and k',
        phetioType: DerivedProperty.DerivedPropertyIO( Quadratic.QuadraticIO )
      } );
    phet.log && quadraticProperty.link( quadratic => {
      phet.log( `quadratic: y = (1/(4(${quadratic.p})))(x - ${quadratic.h})^2 + ${quadratic.k}` );
    } );

    super( quadraticProperty, tandem );

    // @public
    this.pProperty = pProperty;
    this.hProperty = hProperty;
    this.kProperty = kProperty;

    const initialPoint = new Vector2( POINT_X, this.quadraticProperty.value.solveY( POINT_X ) );

    // @public
    this.pointOnParabolaProperty = new Vector2Property( initialPoint, {
      tandem: tandem.createTandem( 'pointOnParabolaProperty' ),
      phetioDocumentation: 'the interactive point on the parabola'
    } );

    // update the point
    this.quadraticProperty.lazyLink( ( quadratic, oldQuadratic ) => {

      assert && assert( quadratic.vertex, `expected quadratic.vertex: ${quadratic.vertex}` );
      assert && assert( oldQuadratic.vertex, `expected oldQuadratic.vertex: ${oldQuadratic.vertex}` );

      const dx = quadratic.vertex.x - oldQuadratic.vertex.x;
      const x = this.pointOnParabolaProperty.value.x + dx;
      this.pointOnParabolaProperty.value = quadratic.getClosestPointInRange( x, this.graph.xRange, this.graph.yRange );
    } );
  }

  /**
   * @public
   * @override
   */
  reset() {
    super.reset();
    this.pProperty.reset();
    this.hProperty.reset();
    this.kProperty.reset();
    this.pointOnParabolaProperty.reset();
  }
}

graphingQuadratics.register( 'FocusAndDirectrixModel', FocusAndDirectrixModel );
export default FocusAndDirectrixModel;<commit_msg>do not set pointOnParabolaProperty indirectly when restoring PhET-iO state, https://github.com/phetsims/graphing-quadratics/issues/162<commit_after>// Copyright 2018-2021, University of Colorado Boulder

/**
 * Model for the 'Focus & Directrix' screen.
 * Alternate vertex form of the quadratic equation is: y = (1/(4p)(x - h)^2 - k
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import DerivedProperty from '../../../../axon/js/DerivedProperty.js';
import NumberProperty from '../../../../axon/js/NumberProperty.js';
import RangeWithValue from '../../../../dot/js/RangeWithValue.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import Vector2Property from '../../../../dot/js/Vector2Property.js';
import merge from '../../../../phet-core/js/merge.js';
import StringUtils from '../../../../phetcommon/js/util/StringUtils.js';
import GQColors from '../../common/GQColors.js';
import GQConstants from '../../common/GQConstants.js';
import GQModel from '../../common/model/GQModel.js';
import Quadratic from '../../common/model/Quadratic.js';
import graphingQuadratics from '../../graphingQuadratics.js';

// constants
const P_RANGE = new RangeWithValue( -9, 9, 2 ); // p coefficient
const H_RANGE = new RangeWithValue( -6, 6, 0 ); // h coefficient
const K_RANGE = new RangeWithValue( -6, 6, 0 ); // k coefficient
const POINT_X = 5; // default x value for point on parabola

class FocusAndDirectrixModel extends GQModel {

  /**
   * @param {Tandem} tandem
   */
  constructor( tandem ) {

    // p
    const pProperty = new NumberProperty( P_RANGE.defaultValue, merge( {
      range: P_RANGE,
      isValidValue: value => ( value !== 0 ), // zero is not supported
      tandem: tandem.createTandem( 'pProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'p' } )
    }, {
      // Opt out of providing a slider in Studio. A generic slider will cause problems, since zero is not supported.
      // See https://github.com/phetsims/graphing-quadratics/issues/52
      phetioStudioControl: false
    } ) );
    phet.log && pProperty.link( p => { phet.log( `p=${p}` ); } );

    // h
    const hProperty = new NumberProperty( H_RANGE.defaultValue, {
      range: H_RANGE,
      tandem: tandem.createTandem( 'hProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'h' } )
    } );
    phet.log && hProperty.link( h => { phet.log( `h=${h}` ); } );

    // k
    const kProperty = new NumberProperty( K_RANGE.defaultValue, {
      range: K_RANGE,
      tandem: tandem.createTandem( 'kProperty' ),
      phetioDocumentation: StringUtils.fillIn( GQConstants.VALUE_DOC, { symbol: 'k' } )
    } );
    phet.log && kProperty.link( k => { phet.log( `k=${k}` ); } );

    // {DerivedProperty.<Quadratic>}
    const quadraticProperty = new DerivedProperty(
      [ pProperty, hProperty, kProperty ],
      ( p, h, k ) => Quadratic.createFromAlternateVertexForm( p, h, k, {
        color: GQColors.FOCUS_AND_DIRECTRIX_INTERACTIVE_CURVE
      } ), {
        tandem: tandem.createTandem( 'quadraticProperty' ),
        phetioDocumentation: 'the interactive quadratic, derived from p, h, and k',
        phetioType: DerivedProperty.DerivedPropertyIO( Quadratic.QuadraticIO )
      } );
    phet.log && quadraticProperty.link( quadratic => {
      phet.log( `quadratic: y = (1/(4(${quadratic.p})))(x - ${quadratic.h})^2 + ${quadratic.k}` );
    } );

    super( quadraticProperty, tandem );

    // @public
    this.pProperty = pProperty;
    this.hProperty = hProperty;
    this.kProperty = kProperty;

    const initialPoint = new Vector2( POINT_X, this.quadraticProperty.value.solveY( POINT_X ) );

    // @public
    this.pointOnParabolaProperty = new Vector2Property( initialPoint, {
      tandem: tandem.createTandem( 'pointOnParabolaProperty' ),
      phetioDocumentation: 'the interactive point on the parabola'
    } );

    // update the point
    this.quadraticProperty.lazyLink( ( quadratic, oldQuadratic ) => {
      assert && assert( quadratic.vertex, `expected quadratic.vertex: ${quadratic.vertex}` );
      assert && assert( oldQuadratic.vertex, `expected oldQuadratic.vertex: ${oldQuadratic.vertex}` );

      if ( !phet.joist.sim.isSettingPhetioStateProperty.value ) {
        const dx = quadratic.vertex.x - oldQuadratic.vertex.x;
        const x = this.pointOnParabolaProperty.value.x + dx;
        this.pointOnParabolaProperty.value = quadratic.getClosestPointInRange( x, this.graph.xRange, this.graph.yRange );
      }
    } );
  }

  /**
   * @public
   * @override
   */
  reset() {
    super.reset();
    this.pProperty.reset();
    this.hProperty.reset();
    this.kProperty.reset();
    this.pointOnParabolaProperty.reset();
  }
}

graphingQuadratics.register( 'FocusAndDirectrixModel', FocusAndDirectrixModel );
export default FocusAndDirectrixModel;<|endoftext|>


<commit_before>/*
 * testdatefmtrange_Latn_NE.js - test the date range formatter object Uzbek/Uzbekistan for Latin script
 *
 * Copyright © 2012-2013, JEDLSoft
 *
 * Licensed unuzr the Apache License, Version 2.0 (the "License");
 * you may not use tens file except in compliance with the License.
 * You may obtaiN a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed unuzr the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KinD, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations unuzr the License.
 */

function testDateRngFmt_ha_Latn_NERangeInDayShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 – 14:30 11-12-31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeInDayMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 – 14:30 31 12, 2011", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeInDayLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 – 14:30 2011 Dis 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeInDayFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 – 14:30 31 Disamba, 2011", fmt.format(start, end));
}

function testDateRngFmt_ha_Latn_NERangeNextDayShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 30,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 11–12–30 – 14:30 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextDayMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 30,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 2011–12–30 – 14:30 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextDayLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 30,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 2011–Dis–30 – 14:30 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextDayFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 30,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("13:45 2011–Disamba–30 – 14:30 31", fmt.format(start, end));
}

function testDateRngFmt_ha_Latn_NERangeMultiDayShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("11–12–20 – 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiDayMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–12–20 – 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiDayLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Dis–20 – 31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiDayFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Disamba–20 – 31", fmt.format(start, end));
}

function testDateRngFmt_ha_Latn_NERangeNextMonthShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("11–11–20 – 12–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextMonthMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–11–20 – 12–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextMonthLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuw–20 – Dis–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextMonthFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2011,
		month: 12,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuwamba–20 – Disamba–31", fmt.format(start, end));
}

function testDateRngFmt_ha_Latn_NERangeNextYearShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2012,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("11–11–20 – 12–1–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextYearMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2012,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–11–20 – 2012–01–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextYearLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2012,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuw–20 – 2012–Jan–31", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeNextYearFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2012,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuwamba–20 – 2012–Janairu–31", fmt.format(start, end));
}

function testDateRngFmt_ha_Latn_NERangeMultiYearShort() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "short"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2014,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("11–11 – 14–1", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiYearMedium() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "medium"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2014,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–11 – 2014–01", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiYearLong() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "long"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2014,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuw – 2014–Jan", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NERangeMultiYearFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2014,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011–Nuwamba – 2014–Janairu", fmt.format(start, end));
}
function testDateRngFmt_ha_Latn_NEManyYearsFull() {
    var fmt = new ilib.DateRngFmt({locale: "ha-Latn-NE", length: "full"});
    assertNotNull(fmt);

    var start = new ilib.Date.GregDate({
		year: 2011,
		month: 11,
		day: 20,
		hour: 13,
		minute: 45,
		second: 0,
		millisecond: 0
	});
    var end = new ilib.Date.GregDate({
		year: 2064,
		month: 1,
		day: 31,
		hour: 14,
		minute: 30,
		second: 0,
		millisecond: 0
	});
    assertEquals("2011 – 2064", fmt.format(start, end));
}
<commit_msg>Remove dateRange for ha_Latn_NE<commit_after><|endoftext|>


<commit_before>var assert = require("node-opcua-assert");
var _ = require("underscore");

var subscription_service = require("node-opcua-service-subscription");
var StatusCodes = require("node-opcua-status-code").StatusCodes;
var AttributeIds = require("node-opcua-data-model").AttributeIds;

var UAVariable = require("node-opcua-address-space").UAVariable;
var NodeId = require("node-opcua-nodeid").NodeId;
var EventFilter = require("node-opcua-service-filter").EventFilter;

function __validateDataChangeFilter(filter,itemToMonitor,node) {

    assert(itemToMonitor.attributeId  === AttributeIds.Value);
    assert(filter instanceof subscription_service.DataChangeFilter);

    if (!(node instanceof UAVariable)) {
        return StatusCodes.BadNodeIdInvalid;
    }

    assert(node instanceof UAVariable);

    // if node is not Numerical=> DataChangeFilter
    assert(node.dataType instanceof NodeId);
    var dataType = node.addressSpace.findDataType(node.dataType);

    var dataTypeNumber = node.addressSpace.findDataType("Number");
    if (!dataType.isSupertypeOf(dataTypeNumber)) {
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter.deadbandType === subscription_service.DeadbandType.Percent) {
        if (filter.deadbandValue < 0 || filter.deadbandValue > 100) {
            return StatusCodes.BadDeadbandFilterInvalid;
        }

        // node must also have a valid euRange
        if (!node.euRange) {
            console.log(" node has no euRange ! DeadbandPercent cannot be used on node "+ node.nodeId.toString());
            return StatusCodes.BadFilterNotAllowed;
        }
    }
    return StatusCodes.Good;
}

function __validateFilter(filter, itemToMonitor ,node) {

    // handle filter information
    if (filter && filter instanceof EventFilter && itemToMonitor.attributeId !== AttributeIds.EventNotifier) {
        // invalid filter on Event
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter && filter instanceof subscription_service.DataChangeFilter && itemToMonitor.attributeId !== AttributeIds.Value) {
        // invalid DataChange filter on non Value Attribute
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter && itemToMonitor.attributeId !== AttributeIds.EventNotifier && itemToMonitor.attributeId !== AttributeIds.Value) {
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter instanceof subscription_service.DataChangeFilter) {

        return __validateDataChangeFilter(filter,itemToMonitor,node);
    }

    return StatusCodes.Good;
}

exports.validateFilter = __validateFilter;
<commit_msg>fix status code to be BadMonitoredItemFilterUnsupported when  a DeadbandPercent filter is requested and the uaVariable has no euRange<commit_after>var assert = require("node-opcua-assert");
var _ = require("underscore");

var subscription_service = require("node-opcua-service-subscription");
var StatusCodes = require("node-opcua-status-code").StatusCodes;
var AttributeIds = require("node-opcua-data-model").AttributeIds;

var UAVariable = require("node-opcua-address-space").UAVariable;
var NodeId = require("node-opcua-nodeid").NodeId;
var EventFilter = require("node-opcua-service-filter").EventFilter;

function __validateDataChangeFilter(filter,itemToMonitor,node) {

    assert(itemToMonitor.attributeId  === AttributeIds.Value);
    assert(filter instanceof subscription_service.DataChangeFilter);

    if (!(node instanceof UAVariable)) {
        return StatusCodes.BadNodeIdInvalid;
    }

    assert(node instanceof UAVariable);

    // if node is not Numerical=> DataChangeFilter
    assert(node.dataType instanceof NodeId);
    var dataType = node.addressSpace.findDataType(node.dataType);

    var dataTypeNumber = node.addressSpace.findDataType("Number");
    if (!dataType.isSupertypeOf(dataTypeNumber)) {
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter.deadbandType === subscription_service.DeadbandType.Percent) {
        if (filter.deadbandValue < 0 || filter.deadbandValue > 100) {
            return StatusCodes.BadDeadbandFilterInvalid;
        }

        // node must also have a valid euRange
        if (!node.euRange) {
            console.log(" node has no euRange ! DeadbandPercent cannot be used on node "+ node.nodeId.toString());
            return StatusCodes.BadMonitoredItemFilterUnsupported;
        }
    }
    return StatusCodes.Good;
}

function __validateFilter(filter, itemToMonitor ,node) {

    // handle filter information
    if (filter && filter instanceof EventFilter && itemToMonitor.attributeId !== AttributeIds.EventNotifier) {
        // invalid filter on Event
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter && filter instanceof subscription_service.DataChangeFilter && itemToMonitor.attributeId !== AttributeIds.Value) {
        // invalid DataChange filter on non Value Attribute
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter && itemToMonitor.attributeId !== AttributeIds.EventNotifier && itemToMonitor.attributeId !== AttributeIds.Value) {
        return StatusCodes.BadFilterNotAllowed;
    }

    if (filter instanceof subscription_service.DataChangeFilter) {

        return __validateDataChangeFilter(filter,itemToMonitor,node);
    }

    return StatusCodes.Good;
}

exports.validateFilter = __validateFilter;
<|endoftext|>


<commit_before>const StatsGroup = require('../lib/groups/StatsGroup');

describe('StatsGroup', function () {
    {
        StatsGroup.validOptions().map((option) => {
            it(`should handle ${option} option`, () => {
                const statsGroup = new StatsGroup([
                    {
                        stats: option,
                    },
                ]);

                const result = statsGroup.run();
                expect(result.options.stats).toEqual(option);
            });
        });
    }
});
<commit_msg>tests: add cases for json and verbose (#1573)<commit_after>const StatsGroup = require('../lib/groups/StatsGroup');

describe('StatsGroup', function () {
    {
        StatsGroup.validOptions().map((option) => {
            it(`should handle ${option} option`, () => {
                const statsGroup = new StatsGroup([
                    {
                        stats: option,
                    },
                ]);

                const result = statsGroup.run();
                expect(result.options.stats).toEqual(option);
            });
        });
    }

    it('should handle verbose', () => {
        const group = new StatsGroup([
            {
                verbose: true,
            },
        ]);

        const result = group.run();
        expect(result.options.stats).toEqual('verbose');
    });

    it('should handle json', () => {
        const group = new StatsGroup([
            {
                json: true,
            },
        ]);

        const result = group.run();
        expect(result.outputOptions.json).toBeTruthy();
    });
});
<|endoftext|>