<commit_before>const express = require('express');
const path = require('path');
const favicon = require('serve-favicon');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const debug = require('debug')('app-ui:server');
const http = require('http');
const stream = require('stream');
const fs = require('fs');
const Promise = require('bluebird');

const config = require('../config');

// make fetch() available as a global just like it is on the client side
global.fetch = require('node-fetch');

const db = require('./db');
const logger = require('./logger');

const app = express();
const server = http.createServer(app);

require('./io').set(server);

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

// view engine setup
app.set('views', path.join(__dirname, '../', 'views'));

app.engine('html', function (filePath, options, callback) {
  fs.readFile(filePath, function (err, content) {
    if (err) { return callback(err);}

    return callback(null, content.toString());
  });
});
app.set('view engine', 'html');

app.use(favicon(path.join(__dirname, '../..', 'public', 'favicon.png')));
app.use(morgan('dev', {
  stream: new stream.Writable({
    write(chunk, encoding, next) {
      logger.info(chunk.toString('utf8').trim());

      next();
    }
  })
}));

app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../..', 'public')));

app.use('/', require('./routes/'));

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  let err = new Error('Not Found');
  err.status = 404;

  next(err);
});

// on thrown error in route, send http 500 and send just the error text message
app.use(function (err, req, res, next) {
  res.status(err.status || 500);
  res.send(err.message);

  next(err);
});

let port = normalizePort(config.PORT);

app.set('port', port);

function normalizePort(val) {
  let port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

server.on('error', onError);
server.on('listening', onListening);

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  let bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      logger.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      logger.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

function onListening() {
  let addr = server.address();
  let bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

// set up rethinkdb
Promise.try( () => {
  let log = (...msg) => function( val ){ logger.debug( ...msg ); return val; };
  let access = name => db.accessTable( name );
  let setup = name => {
    return access( name )
      .then( log('Accessed table "%s"', name) )
      .then( log('Set up synching for "%s"', name) )
    ;
  };

  return Promise.all( ['pathways'].map( setup ) );
} ).then( () => {
  server.listen(port);
} );

module.exports = app;
<commit_msg>Disable DB for now<commit_after>const express = require('express');
const path = require('path');
const favicon = require('serve-favicon');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const debug = require('debug')('app-ui:server');
const http = require('http');
const stream = require('stream');
const fs = require('fs');
const Promise = require('bluebird');

const config = require('../config');

// make fetch() available as a global just like it is on the client side
global.fetch = require('node-fetch');

const db = require('./db');
const logger = require('./logger');

const app = express();
const server = http.createServer(app);

require('./io').set(server);

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

// view engine setup
app.set('views', path.join(__dirname, '../', 'views'));

app.engine('html', function (filePath, options, callback) {
  fs.readFile(filePath, function (err, content) {
    if (err) { return callback(err);}

    return callback(null, content.toString());
  });
});
app.set('view engine', 'html');

app.use(favicon(path.join(__dirname, '../..', 'public', 'favicon.png')));
app.use(morgan('dev', {
  stream: new stream.Writable({
    write(chunk, encoding, next) {
      logger.info(chunk.toString('utf8').trim());

      next();
    }
  })
}));

app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../..', 'public')));

app.use('/', require('./routes/'));

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  let err = new Error('Not Found');
  err.status = 404;

  next(err);
});

// on thrown error in route, send http 500 and send just the error text message
app.use(function (err, req, res, next) {
  res.status(err.status || 500);
  res.send(err.message);

  next(err);
});

let port = normalizePort(config.PORT);

app.set('port', port);

function normalizePort(val) {
  let port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

server.on('error', onError);
server.on('listening', onListening);

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  let bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      logger.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      logger.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

function onListening() {
  let addr = server.address();
  let bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

let setUpDb = () => {
  let log = (...msg) => function( val ){ logger.debug( ...msg ); return val; };
  let access = name => db.accessTable( name );
  let setup = name => {
    return access( name )
      .then( log('Accessed table "%s"', name) )
      .then( log('Set up synching for "%s"', name) )
    ;
  };

  return Promise.all( ['pathways'].map( setup ) );
};

// for now disable db stuff...
setUpDb = () => {};

// set up rethinkdb
Promise.try( setUpDb ).then( () => {
  server.listen(port);
} );

module.exports = app;
<|endoftext|>


<commit_before>// ==UserScript==
// @name         Spam Reporter
// @namespace    http://github.com/Tiny-Giant
// @version      1.0.0.0
// @description  Adds a link to the post menu for most posts.
// @author       @TinyGiant
// @include      /^https?:\/\/\w*.?(stackexchange.com|stackoverflow.com|serverfault.com|superuser.com|askubuntu.com|stackapps.com|mathoverflow.net)\/q(uestions)?\/\d+/
// @grant        GM_xmlhttpRequest
// ==/UserScript==
/* jshint -W097 */

(function(){
    'use strict';

    var notify = (function(){
        var count = 0, timeout;
        return function(m,t) {
            console.log(m,t);
            if($('#notify-' + count).length) {
                clearTimeout(timeout);
                StackExchange.notify.close(count);
            }
            StackExchange.notify.show(m,++count);
            if(t) timeout = setTimeout(StackExchange.notify.close.bind(null,count), t);
        };
    })();

    var engage = function(scope) {

        if (!scope) {
            return false;
        }

        var room = 41570; // use 68414 for testing

        var postLink = scope.querySelector('.short-link').href;

        var reportSent = function(response) {
            console.log(response);

            if (response.status !== 200) {
                notify('Error sending request: ' + resp.responseText);
                return false;
            }

            notify('Close vote request sent.',1000);
        };

        var sendReport = function(response) {
            console.log(response);

            if (response.status !== 200) {
                notify('Failed sending report, check the console for more information.');
                return false;
            }

            var fkey = response.responseText.match(/hidden" value="([\dabcdef]{32})/)[1];

            if (!fkey) {
                notify('Failed retrieving key, is the room URL valid?');
                return false;
            }

            var reportStr = '!!/report ' + postLink;

            var options = {
                method: 'POST',
                url: 'http://chat.stackoverflow.com/chats/' + room + '/messages/new',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                data: 'text=' + encodeURIComponent(reportStr) + '&fkey=' + fkey,
                onload: reportSent
            };

            GM_xmlhttpRequest(options);
        };

        var report = function(e) {
			e.preventDefault();

            if(!confirm('Do you really want to report this post?')) {
                return false;
            }

            var options = {
                method: 'GET',
                url: 'http://chat.stackoverflow.com/rooms/' + room,
                onload: sendReport
            };

            GM_xmlhttpRequest(options);
        };

        var sep = document.createElement('span');
        sep.className = 'lsep';
        sep.textContent = '|';
        scope.appendChild(sep);

        var link = document.createElement('a');
        link.href = '#';
        link.textContent = 'report';
        link.addEventListener('click', report, false);
        scope.appendChild(link);
    };

    var menus = document.querySelectorAll('.post-menu');

    for(var i in Object.keys(menus)) engage(menus[i]);

    $(document).ajaxComplete(function(){
        var url = arguments[2].url;
        if (/^\/posts\/ajax-load-realtime\//.test(url)) engage(/\d+/.exec(url)[0]);
    });
})();
<commit_msg>Prevent from going to top of page when clicking on Report<commit_after>// ==UserScript==
// @name         Spam Reporter
// @namespace    http://github.com/Tiny-Giant
// @version      1.0.0.0
// @description  Adds a link to the post menu for most posts.
// @author       @TinyGiant
// @include      /^https?:\/\/\w*.?(stackexchange.com|stackoverflow.com|serverfault.com|superuser.com|askubuntu.com|stackapps.com|mathoverflow.net)\/q(uestions)?\/\d+/
// @grant        GM_xmlhttpRequest
// ==/UserScript==
/* jshint -W097 */

(function(){
    'use strict';

    var notify = (function(){
        var count = 0, timeout;
        return function(m,t) {
            console.log(m,t);
            if($('#notify-' + count).length) {
                clearTimeout(timeout);
                StackExchange.notify.close(count);
            }
            StackExchange.notify.show(m,++count);
            if(t) timeout = setTimeout(StackExchange.notify.close.bind(null,count), t);
        };
    })();

    var engage = function(scope) {

        if (!scope) {
            return false;
        }

        var room = 41570; // use 68414 for testing

        var postLink = scope.querySelector('.short-link').href;

        var reportSent = function(response) {
            console.log(response);

            if (response.status !== 200) {
                notify('Error sending request: ' + resp.responseText);
                return false;
            }

            notify('Close vote request sent.',1000);
        };

        var sendReport = function(response) {
            console.log(response);

            if (response.status !== 200) {
                notify('Failed sending report, check the console for more information.');
                return false;
            }

            var fkey = response.responseText.match(/hidden" value="([\dabcdef]{32})/)[1];

            if (!fkey) {
                notify('Failed retrieving key, is the room URL valid?');
                return false;
            }

            var reportStr = '!!/report ' + postLink;

            var options = {
                method: 'POST',
                url: 'http://chat.stackoverflow.com/chats/' + room + '/messages/new',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                data: 'text=' + encodeURIComponent(reportStr) + '&fkey=' + fkey,
                onload: reportSent
            };

            GM_xmlhttpRequest(options);
        };

        var report = function(e) {
            e.preventDefault();

            if(!confirm('Do you really want to report this post?')) {
                return false;
            }

            var options = {
                method: 'GET',
                url: 'http://chat.stackoverflow.com/rooms/' + room,
                onload: sendReport
            };

            GM_xmlhttpRequest(options);
        };

        var sep = document.createElement('span');
        sep.className = 'lsep';
        sep.textContent = '|';
        scope.appendChild(sep);

        var link = document.createElement('a');
        link.href = '#';
        link.textContent = 'report';
        link.addEventListener('click', report, false);
        scope.appendChild(link);
    };

    var menus = document.querySelectorAll('.post-menu');

    for(var i in Object.keys(menus)) engage(menus[i]);

    $(document).ajaxComplete(function(){
        var url = arguments[2].url;
        if (/^\/posts\/ajax-load-realtime\//.test(url)) engage(/\d+/.exec(url)[0]);
    });
})();
<|endoftext|>


<commit_before>(function(global) {

  'use strict';

  var fabric = global.fabric || (global.fabric = { }),
      toFixed = fabric.util.toFixed;

  if (fabric.Shadow) {
    fabric.warn('fabric.Shadow is already defined.');
    return;
  }

  /**
   * Shadow class
   * @class fabric.Shadow
   * @see {@link http://fabricjs.com/shadows/|Shadow demo}
   * @see {@link fabric.Shadow#initialize} for constructor definition
   */
  fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ {

    /**
     * Shadow color
     * @type String
     * @default
     */
    color: 'rgb(0,0,0)',

    /**
     * Shadow blur
     * @type Number
     */
    blur: 0,

    /**
     * Shadow horizontal offset
     * @type Number
     * @default
     */
    offsetX: 0,

    /**
     * Shadow vertical offset
     * @type Number
     * @default
     */
    offsetY: 0,

    /**
     * Whether the shadow should affect stroke operations
     * @type Boolean
     * @default
     */
    affectStroke: false,

    /**
     * Indicates whether toObject should include default values
     * @type Boolean
     * @default
     */
    includeDefaultValues: true,

    /**
     * Constructor
     * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)")
     * @return {fabric.Shadow} thisArg
     */
    initialize: function(options) {

      if (typeof options === 'string') {
        options = this._parseShadow(options);
      }

      for (var prop in options) {
        this[prop] = options[prop];
      }

      this.id = fabric.Object.__uid++;
    },

    /**
     * @private
     * @param {String} shadow Shadow value to parse
     * @return {Object} Shadow object with color, offsetX, offsetY and blur
     */
    _parseShadow: function(shadow) {
      var shadowStr = shadow.trim(),
          offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [ ],
          color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)';

      return {
        color: color.trim(),
        offsetX: parseInt(offsetsAndBlur[1], 10) || 0,
        offsetY: parseInt(offsetsAndBlur[2], 10) || 0,
        blur: parseInt(offsetsAndBlur[3], 10) || 0
      };
    },

    /**
     * Returns a string representation of an instance
     * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow
     * @return {String} Returns CSS3 text-shadow declaration
     */
    toString: function() {
      return [this.offsetX, this.offsetY, this.blur, this.color].join('px ');
    },

    /* _TO_SVG_START_ */
    /**
     * Returns SVG representation of a shadow
     * @param {fabric.Object} object
     * @return {String} SVG representation of a shadow
     */
    toSVG: function(object) {
      var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS,
          offset = fabric.util.rotateVector(
            { x: this.offsetX, y: this.offsetY },
            fabric.util.degreesToRadians(-object.angle)),
          BLUR_BOX = 20;

      if (object.width && object.height) {
        //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion
        // we add some extra space to filter box to contain the blur ( 20 )
        fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
        fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
      }
      if (object.flipX) {
        offset.x *= -1;
      }
      if (object.flipY) {
        offset.y *= -1;
      }
      return (
        '<filter id="SVGID_' + this.id + '" y="-' + fBoxY + '%" height="' + (100 + 2 * fBoxY) + '%" ' +
          'x="-' + fBoxX + '%" width="' + (100 + 2 * fBoxX) + '%" ' + '>\n' +
          '\t<feGaussianBlur in="SourceAlpha" stdDeviation="' +
            toFixed(this.blur ? this.blur / 2 : 0, NUM_FRACTION_DIGITS) + '"></feGaussianBlur>\n' +
          '\t<feOffset dx="' + toFixed(offset.x, NUM_FRACTION_DIGITS) +
          '" dy="' + toFixed(offset.y, NUM_FRACTION_DIGITS) + '" result="oBlur" ></feOffset>\n' +
          '\t<feFlood flood-color="' + this.color + '"/>\n' +
          '\t<feComposite in2="oBlur" operator="in" />\n' +
          '\t<feMerge>\n' +
            '\t\t<feMergeNode></feMergeNode>\n' +
            '\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n' +
          '\t</feMerge>\n' +
        '</filter>\n');
    },
    /* _TO_SVG_END_ */

    /**
     * Returns object representation of a shadow
     * @return {Object} Object representation of a shadow instance
     */
    toObject: function() {
      if (this.includeDefaultValues) {
        return {
          color: this.color,
          blur: this.blur,
          offsetX: this.offsetX,
          offsetY: this.offsetY,
          affectStroke: this.affectStroke
        };
      }
      var obj = { }, proto = fabric.Shadow.prototype;

      ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke'].forEach(function(prop) {
        if (this[prop] !== proto[prop]) {
          obj[prop] = this[prop];
        }
      }, this);

      return obj;
    }
  });

  /**
   * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px")
   * @static
   * @field
   * @memberOf fabric.Shadow
   */
  fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/;

})(typeof exports !== 'undefined' ? exports : this);
<commit_msg>deleted forward slashes in docs urls<commit_after>(function(global) {

  'use strict';

  var fabric = global.fabric || (global.fabric = { }),
      toFixed = fabric.util.toFixed;

  if (fabric.Shadow) {
    fabric.warn('fabric.Shadow is already defined.');
    return;
  }

  /**
   * Shadow class
   * @class fabric.Shadow
   * @see {@link http://fabricjs.com/shadows|Shadow demo}
   * @see {@link fabric.Shadow#initialize} for constructor definition
   */
  fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ {

    /**
     * Shadow color
     * @type String
     * @default
     */
    color: 'rgb(0,0,0)',

    /**
     * Shadow blur
     * @type Number
     */
    blur: 0,

    /**
     * Shadow horizontal offset
     * @type Number
     * @default
     */
    offsetX: 0,

    /**
     * Shadow vertical offset
     * @type Number
     * @default
     */
    offsetY: 0,

    /**
     * Whether the shadow should affect stroke operations
     * @type Boolean
     * @default
     */
    affectStroke: false,

    /**
     * Indicates whether toObject should include default values
     * @type Boolean
     * @default
     */
    includeDefaultValues: true,

    /**
     * Constructor
     * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)")
     * @return {fabric.Shadow} thisArg
     */
    initialize: function(options) {

      if (typeof options === 'string') {
        options = this._parseShadow(options);
      }

      for (var prop in options) {
        this[prop] = options[prop];
      }

      this.id = fabric.Object.__uid++;
    },

    /**
     * @private
     * @param {String} shadow Shadow value to parse
     * @return {Object} Shadow object with color, offsetX, offsetY and blur
     */
    _parseShadow: function(shadow) {
      var shadowStr = shadow.trim(),
          offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [ ],
          color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)';

      return {
        color: color.trim(),
        offsetX: parseInt(offsetsAndBlur[1], 10) || 0,
        offsetY: parseInt(offsetsAndBlur[2], 10) || 0,
        blur: parseInt(offsetsAndBlur[3], 10) || 0
      };
    },

    /**
     * Returns a string representation of an instance
     * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow
     * @return {String} Returns CSS3 text-shadow declaration
     */
    toString: function() {
      return [this.offsetX, this.offsetY, this.blur, this.color].join('px ');
    },

    /* _TO_SVG_START_ */
    /**
     * Returns SVG representation of a shadow
     * @param {fabric.Object} object
     * @return {String} SVG representation of a shadow
     */
    toSVG: function(object) {
      var fBoxX = 40, fBoxY = 40, NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS,
          offset = fabric.util.rotateVector(
            { x: this.offsetX, y: this.offsetY },
            fabric.util.degreesToRadians(-object.angle)),
          BLUR_BOX = 20;

      if (object.width && object.height) {
        //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion
        // we add some extra space to filter box to contain the blur ( 20 )
        fBoxX = toFixed((Math.abs(offset.x) + this.blur) / object.width, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
        fBoxY = toFixed((Math.abs(offset.y) + this.blur) / object.height, NUM_FRACTION_DIGITS) * 100 + BLUR_BOX;
      }
      if (object.flipX) {
        offset.x *= -1;
      }
      if (object.flipY) {
        offset.y *= -1;
      }
      return (
        '<filter id="SVGID_' + this.id + '" y="-' + fBoxY + '%" height="' + (100 + 2 * fBoxY) + '%" ' +
          'x="-' + fBoxX + '%" width="' + (100 + 2 * fBoxX) + '%" ' + '>\n' +
          '\t<feGaussianBlur in="SourceAlpha" stdDeviation="' +
            toFixed(this.blur ? this.blur / 2 : 0, NUM_FRACTION_DIGITS) + '"></feGaussianBlur>\n' +
          '\t<feOffset dx="' + toFixed(offset.x, NUM_FRACTION_DIGITS) +
          '" dy="' + toFixed(offset.y, NUM_FRACTION_DIGITS) + '" result="oBlur" ></feOffset>\n' +
          '\t<feFlood flood-color="' + this.color + '"/>\n' +
          '\t<feComposite in2="oBlur" operator="in" />\n' +
          '\t<feMerge>\n' +
            '\t\t<feMergeNode></feMergeNode>\n' +
            '\t\t<feMergeNode in="SourceGraphic"></feMergeNode>\n' +
          '\t</feMerge>\n' +
        '</filter>\n');
    },
    /* _TO_SVG_END_ */

    /**
     * Returns object representation of a shadow
     * @return {Object} Object representation of a shadow instance
     */
    toObject: function() {
      if (this.includeDefaultValues) {
        return {
          color: this.color,
          blur: this.blur,
          offsetX: this.offsetX,
          offsetY: this.offsetY,
          affectStroke: this.affectStroke
        };
      }
      var obj = { }, proto = fabric.Shadow.prototype;

      ['color', 'blur', 'offsetX', 'offsetY', 'affectStroke'].forEach(function(prop) {
        if (this[prop] !== proto[prop]) {
          obj[prop] = this[prop];
        }
      }, this);

      return obj;
    }
  });

  /**
   * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px")
   * @static
   * @field
   * @memberOf fabric.Shadow
   */
  fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/;

})(typeof exports !== 'undefined' ? exports : this);
<|endoftext|>


<commit_before>import Interface  from './../helpers/Interface.js';
import Dispatcher from './../helpers/Dispatcher.js';
import Events     from './../helpers/Events.js';
import utility    from './../helpers/Utility.js';

// Features.
import Selectable from './features/Selectable.js';

/**
 * @module Blueprint
 * @submodule Shape
 * @author Adam Timberlake
 * @link https://github.com/Wildhoney/Blueprint
 */
export default class Shape {

    /**
     * @method constructor
     * @param {String} [label='']
     * @constructor
     */
    constructor(label = '') {
        this.element = null;
        this.group = null;
        this.label = label;
        this.interface = null;
        this.features = {};
    }

    /**
     * @method setElement
     * @param {Object} element
     * @return {void}
     */
    setElement(element) {
        this.element = element;
    }

    /**
     * @method setGroup
     * @param {Object} group
     * @return {void}
     */
    setGroup(group) {
        this.group = group;
    }

    /**
     * @method setDispatcher
     * @param {Dispatcher} dispatcher
     * @return {void}
     */
    setDispatcher(dispatcher) {

        this.dispatcher = dispatcher;

        this.dispatcher.listen(Events.DESELECT, () => this.tryInvokeOnEachFeature('cancel'));

    }

    /**
     * @method tryInvokeOnEachFeature
     * @param {String} methodName
     */
    tryInvokeOnEachFeature(methodName) {

        _.forIn(this.features, (feature) => {

            if (_.isFunction(feature[methodName])) {
                feature[methodName]();
            }

        });

    }

    /**
     * @method setOptions
     * @param {Object} options
     * @return {void}
     */
    setOptions(options) {
        this.options = options;
    }

    /**
     * Should be overwritten for shape types that have a different name to their SVG tag name, such as a `foreignObject`
     * element using the `rect` shape name.
     *
     * @method getName
     * @return {String}
     */
    getName() {
        return this.getTag();
    }

    /**
     * @method getTag
     * @throws Exception
     * @return {String}
     */
    getTag() {
        utility.throwException(`Shape<${this.label}> must define a \`getTag\` method`);
        return '';
    }

    /**
     * @method getInterface
     * @return {Interface}
     */
    getInterface() {

        if (this.interface === null) {

            this.interface = new Interface(this.label);
            var dispatcher = new Dispatcher();
            this.interface.setDispatcher(dispatcher);

            /**
             * @method getAttributes
             * @return {Object}
             */
            var getAttributes = () => {

                var zIndex = { z: d3.select(this.element.node().parentNode).datum().z },
                    model  = _.assign(this.element.datum(), zIndex);
                return utility.retransformAttributes(model);

            };

            // Listeners that hook up the interface and the shape object.
            dispatcher.listen(Events.REMOVE, (model) => this.dispatcher.send(Events.REMOVE, model));
            dispatcher.listen(Events.ATTRIBUTE_GET_ALL, getAttributes);
            dispatcher.listen(Events.ATTRIBUTE_SET, (model) => { this.setAttributes(model.attributes); });

            if (_.isFunction(this.addMethods)) {
                this.interface = _.assign(this.interface, this.addMethods());
            }

        }

        return this.interface;

    }

    /**
     * @method setAttributes
     * @param {Object} attributes
     * @return {Object}
     */
    setAttributes(attributes = {}) {

        attributes = _.assign(this.element.datum() || {}, attributes);
        attributes = utility.transformAttributes(attributes);

        if (!_.isUndefined(attributes.z)) {

            // When the developer specifies the `z` attribute, it actually pertains to the group
            // element that the shape is a child of. We'll therefore need to remove the `z` property
            // from the `attributes` object, and instead apply it to the shape's group element.
            // Afterwards we'll need to broadcast an event to reorder the elements using D3's magical
            // `sort` method.
            var group = d3.select(this.element.node().parentNode);
            group.datum({ z: attributes.z });
            delete attributes.z;
            this.dispatcher.send(Events.REORDER, {
                group: group
            });

        }

        this.element.datum(attributes);
        this.element.attr(this.element.datum());
        return this.interface;

    }

    /**
     * @method getAttributes
     * @return {Object}
     */
    getAttributes() {

        var attributes = { x: 0, y: 0 };

        if (_.isFunction(this.addAttributes)) {
            attributes = _.assign(attributes, this.addAttributes());
        }

        return attributes;

    }

    /**
     * @method addElements
     * @return {Object}
     */
    addElements() {
        return {};
    }

    /**
     * @method addFeatures
     * @return {void}
     */
    addFeatures() {

        var dispatcher = new Dispatcher();

        this.features = {
            selectable: new Selectable(this).setDispatcher(dispatcher)
        };

        dispatcher.listen(Events.DESELECT, () => {
            this.dispatcher.send(Events.DESELECT);
        });

    }

    /**
     * @method dispatchAttributeEvent
     * @param {Object} properties = {}
     * @return {void}
     */
    dispatchAttributeEvent(properties = {}) {
        properties.element = this;
        this.dispatcher.send(Events.ATTRIBUTE, properties);
    }

    /**
     * @method toString
     * @return {String}
     */
    toString() {

        var tag = this.getTag().charAt(0).toUpperCase() + this.getTag().slice(1);

        if (this.label) {
            return `[object ${tag}: ${this.label}]`;
        }

        return `[object ${tag}]`;

    }

}<commit_msg>Minor refactor for relaying dispatcher events<commit_after>import Interface  from './../helpers/Interface.js';
import Dispatcher from './../helpers/Dispatcher.js';
import Events     from './../helpers/Events.js';
import utility    from './../helpers/Utility.js';

// Features.
import Selectable from './features/Selectable.js';

/**
 * @module Blueprint
 * @submodule Shape
 * @author Adam Timberlake
 * @link https://github.com/Wildhoney/Blueprint
 */
export default class Shape {

    /**
     * @method constructor
     * @param {String} [label='']
     * @constructor
     */
    constructor(label = '') {
        this.element = null;
        this.group = null;
        this.label = label;
        this.interface = null;
        this.features = {};
    }

    /**
     * @method setElement
     * @param {Object} element
     * @return {void}
     */
    setElement(element) {
        this.element = element;
    }

    /**
     * @method setGroup
     * @param {Object} group
     * @return {void}
     */
    setGroup(group) {
        this.group = group;
    }

    /**
     * @method setDispatcher
     * @param {Dispatcher} dispatcher
     * @return {void}
     */
    setDispatcher(dispatcher) {

        this.dispatcher = dispatcher;

        this.dispatcher.listen(Events.DESELECT, () => this.tryInvokeOnEachFeature('cancel'));

    }

    /**
     * @method tryInvokeOnEachFeature
     * @param {String} methodName
     */
    tryInvokeOnEachFeature(methodName) {

        _.forIn(this.features, (feature) => {

            if (_.isFunction(feature[methodName])) {
                feature[methodName]();
            }

        });

    }

    /**
     * @method setOptions
     * @param {Object} options
     * @return {void}
     */
    setOptions(options) {
        this.options = options;
    }

    /**
     * Should be overwritten for shape types that have a different name to their SVG tag name, such as a `foreignObject`
     * element using the `rect` shape name.
     *
     * @method getName
     * @return {String}
     */
    getName() {
        return this.getTag();
    }

    /**
     * @method getTag
     * @throws Exception
     * @return {String}
     */
    getTag() {
        utility.throwException(`Shape<${this.label}> must define a \`getTag\` method`);
        return '';
    }

    /**
     * @method getInterface
     * @return {Interface}
     */
    getInterface() {

        if (this.interface === null) {

            this.interface = new Interface(this.label);
            var dispatcher = new Dispatcher();
            this.interface.setDispatcher(dispatcher);

            /**
             * @method getAttributes
             * @return {Object}
             */
            var getAttributes = () => {

                var zIndex = { z: d3.select(this.element.node().parentNode).datum().z },
                    model  = _.assign(this.element.datum(), zIndex);
                return utility.retransformAttributes(model);

            };

            // Listeners that hook up the interface and the shape object.
            dispatcher.listen(Events.REMOVE, (model) => this.dispatcher.send(Events.REMOVE, model));
            dispatcher.listen(Events.ATTRIBUTE_GET_ALL, getAttributes);
            dispatcher.listen(Events.ATTRIBUTE_SET, (model) => { this.setAttributes(model.attributes); });

            if (_.isFunction(this.addMethods)) {
                this.interface = _.assign(this.interface, this.addMethods());
            }

        }

        return this.interface;

    }

    /**
     * @method setAttributes
     * @param {Object} attributes
     * @return {Object}
     */
    setAttributes(attributes = {}) {

        attributes = _.assign(this.element.datum() || {}, attributes);
        attributes = utility.transformAttributes(attributes);

        if (!_.isUndefined(attributes.z)) {

            // When the developer specifies the `z` attribute, it actually pertains to the group
            // element that the shape is a child of. We'll therefore need to remove the `z` property
            // from the `attributes` object, and instead apply it to the shape's group element.
            // Afterwards we'll need to broadcast an event to reorder the elements using D3's magical
            // `sort` method.
            var group = d3.select(this.element.node().parentNode);
            group.datum({ z: attributes.z });
            delete attributes.z;
            this.dispatcher.send(Events.REORDER, {
                group: group
            });

        }

        this.element.datum(attributes);
        this.element.attr(this.element.datum());
        return this.interface;

    }

    /**
     * @method getAttributes
     * @return {Object}
     */
    getAttributes() {

        var attributes = { x: 0, y: 0 };

        if (_.isFunction(this.addAttributes)) {
            attributes = _.assign(attributes, this.addAttributes());
        }

        return attributes;

    }

    /**
     * @method addElements
     * @return {Object}
     */
    addElements() {
        return {};
    }

    /**
     * @method addFeatures
     * @return {void}
     */
    addFeatures() {

        var dispatcher = new Dispatcher();

        this.features = {
            selectable: new Selectable(this).setDispatcher(dispatcher)
        };

        dispatcher.listen(Events.DESELECT, () => this.dispatcher.send(Events.DESELECT));

    }

    /**
     * @method dispatchAttributeEvent
     * @param {Object} properties = {}
     * @return {void}
     */
    dispatchAttributeEvent(properties = {}) {
        properties.element = this;
        this.dispatcher.send(Events.ATTRIBUTE, properties);
    }

    /**
     * @method toString
     * @return {String}
     */
    toString() {

        var tag = this.getTag().charAt(0).toUpperCase() + this.getTag().slice(1);

        if (this.label) {
            return `[object ${tag}: ${this.label}]`;
        }

        return `[object ${tag}]`;

    }

}<|endoftext|>


<commit_before>/* Simple getters - getting a property of a given point in time */

export const getYear = (date) => {
  if (date instanceof Date) {
    return date.getFullYear();
  }

  if (typeof date === 'number') {
    return date;
  }

  const year = parseInt(date, 10);

  if (typeof date === 'string' && !isNaN(year)) {
    return year;
  }

  throw new Error(`Failed to get year from date: ${date}.`);
};

export const getMonthIndex = date => date.getMonth();

export const getDay = date => date.getDate();

export const getDayOfWeek = (date, calendarType = 'ISO 8601') => {
  const weekday = date.getDay();

  switch (calendarType) {
    case 'ISO 8601':
      // Shifts days of the week so that Monday is 0, Sunday is 6
      return (weekday + 6) % 7;
    case 'US':
      return weekday;
    default:
      throw new Error('Unsupported calendar type.');
  }
};

/* Complex getters - getting a property somehow related to a given point in time */

export const getBeginOfCenturyYear = (date) => {
  const year = getYear(date) - 1;
  return year + (-year % 100) + 1;
};

export const getBeginOfCentury = (date) => {
  const beginOfCenturyYear = getBeginOfCenturyYear(date);
  return new Date(beginOfCenturyYear, 0, 1);
};

export const getEndOfCentury = (date) => {
  const beginOfCenturyYear = getBeginOfCenturyYear(date);
  return new Date(beginOfCenturyYear + 100, 0, 1, 0, 0, 0, -1);
};

export const getCenturyRange = date => [
  getBeginOfCentury(date),
  getEndOfCentury(date),
];

export const getBeginOfPreviousCentury = (date) => {
  const previousCenturyYear = getYear(date) - 100;
  return getBeginOfCentury(previousCenturyYear);
};

export const getEndOfPreviousCentury = (date) => {
  const previousCenturyYear = getYear(date) - 100;
  return getEndOfCentury(previousCenturyYear);
};

export const getBeginOfNextCentury = (date) => {
  const nextCenturyYear = getYear(date) + 100;
  return getBeginOfCentury(nextCenturyYear);
};

export const getBeginOfDecadeYear = (date) => {
  const year = getYear(date) - 1;
  return year + (-year % 10) + 1;
};

export const getBeginOfDecade = (date) => {
  const beginOfDecadeYear = getBeginOfDecadeYear(date);
  return new Date(beginOfDecadeYear, 0, 1);
};

export const getEndOfDecade = (date) => {
  const beginOfDecadeYear = getBeginOfDecadeYear(date);
  return new Date(beginOfDecadeYear + 10, 0, 1, 0, 0, 0, -1);
};

export const getDecadeRange = date => [
  getBeginOfDecade(date),
  getEndOfDecade(date),
];

export const getBeginOfPreviousDecade = (date, offset = 10) => {
  const previousDecadeYear = getBeginOfDecadeYear(date) - offset;
  return getBeginOfDecade(previousDecadeYear);
};

export const getEndOfPreviousDecade = (date, offset = 10) => {
  const previousDecadeYear = getBeginOfDecadeYear(date) - offset;
  return getEndOfDecade(previousDecadeYear);
};

export const getBeginOfNextDecade = (date, offset = 10) => {
  const nextDecadeYear = getBeginOfDecadeYear(date) + offset;
  return getBeginOfDecade(nextDecadeYear);
};

/**
 * Returns the beginning of a given year.
 *
 * @param {Date} date Date.
 */
export const getBeginOfYear = (date) => {
  const year = getYear(date);
  return new Date(year, 0, 1);
};

/**
 * Returns the end of a given year.
 *
 * @param {Date} date Date.
 */
export const getEndOfYear = (date) => {
  const year = getYear(date);
  return new Date(year + 1, 0, 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given year.
 *
 * @param {Date} date Date.
 */
export const getYearRange = date => [
  getBeginOfYear(date),
  getEndOfYear(date),
];

export const getBeginOfPreviousYear = (date, offset = 1) => {
  const previousYear = getYear(date) - offset;
  return getBeginOfYear(previousYear);
};

export const getEndOfPreviousYear = (date, offset = 1) => {
  const previousYear = getYear(date) - offset;
  return getEndOfYear(previousYear);
};

export const getBeginOfNextYear = (date, offset = 1) => {
  const nextYear = getYear(date) + offset;
  return getBeginOfYear(nextYear);
};

/**
 * Returns the beginning of a given month.
 *
 * @param {Date} date Date.
 */
export const getBeginOfMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex, 1);
};

/**
 * Returns the end of a given month.
 *
 * @param {Date} date Date.
 */
export const getEndOfMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex + 1, 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given month.
 *
 * @param {Date} date Date.
 */
export const getMonthRange = date => [
  getBeginOfMonth(date),
  getEndOfMonth(date),
];

export const getBeginOfPreviousMonth = (date, offset = 1) => {
  const year = getYear(date);
  const previousMonthIndex = getMonthIndex(date) - offset;
  return getBeginOfMonth(new Date(year, previousMonthIndex, 1));
};

export const getEndOfPreviousMonth = (date, offset = 1) => {
  const year = getYear(date);
  const previousMonthIndex = getMonthIndex(date) - offset;
  return getEndOfMonth(new Date(year, previousMonthIndex, 1));
};

export const getBeginOfNextMonth = (date, offset = 1) => {
  const year = getYear(date);
  const nextMonthIndex = getMonthIndex(date) + offset;
  return getBeginOfMonth(new Date(year, nextMonthIndex, 1));
};

export const getBeginOfDay = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  const day = getDay(date);
  return new Date(year, monthIndex, day);
};

export const getEndOfDay = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  const day = getDay(date);
  return new Date(year, monthIndex, day + 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given day.
 *
 * @param {Date} date Date.
 */
export const getDayRange = date => [
  getBeginOfDay(date),
  getEndOfDay(date),
];

export const getWeekNumber = (date, calendarType = 'ISO 8601') => {
  const tempDate = new Date(+date);
  tempDate.setDate(getDay(tempDate) + (4 - getDayOfWeek(tempDate, calendarType)));
  const yearStart = getBeginOfYear(tempDate);
  return Math.ceil((((tempDate - yearStart) / 8.64e7)) / 7);
};

/**
 * Returns an array with the beginning and the end of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getRange = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getCenturyRange(date);
    case 'decade':
      return getDecadeRange(date);
    case 'year':
      return getYearRange(date);
    case 'month':
      return getMonthRange(date);
    case 'day':
      return getDayRange(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns the beginning of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getBegin = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfCentury(date);
    case 'decade':
      return getBeginOfDecade(date);
    case 'year':
      return getBeginOfYear(date);
    case 'month':
      return getBeginOfMonth(date);
    case 'day':
      return getBeginOfDay(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginPrevious = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfPreviousCentury(date);
    case 'decade':
      return getBeginOfPreviousDecade(date);
    case 'year':
      return getBeginOfPreviousYear(date);
    case 'month':
      return getBeginOfPreviousMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginNext = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfNextCentury(date);
    case 'decade':
      return getBeginOfNextDecade(date);
    case 'year':
      return getBeginOfNextYear(date);
    case 'month':
      return getBeginOfNextMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginPrevious2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getBeginOfPreviousDecade(date, 100);
    case 'year':
      return getBeginOfPreviousYear(date, 10);
    case 'month':
      return getBeginOfPreviousMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginNext2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getBeginOfNextDecade(date, 100);
    case 'year':
      return getBeginOfNextYear(date, 10);
    case 'month':
      return getBeginOfNextMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns the end of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getEnd = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getEndOfCentury(date);
    case 'decade':
      return getEndOfDecade(date);
    case 'year':
      return getEndOfYear(date);
    case 'month':
      return getEndOfMonth(date);
    case 'day':
      return getEndOfDay(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getEndPrevious = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getEndOfPreviousCentury(date);
    case 'decade':
      return getEndOfPreviousDecade(date);
    case 'year':
      return getEndOfPreviousYear(date);
    case 'month':
      return getEndOfPreviousMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getEndPrevious2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getEndOfPreviousDecade(date, 100);
    case 'year':
      return getEndOfPreviousYear(date, 10);
    case 'month':
      return getEndOfPreviousMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns a number of days in a month of a given date.
 *
 * @param {Date} date Date.
 */
export const getDaysInMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex + 1, 0).getDate();
};

/**
 * Returns a string labelling a century of a given date.
 * For example, for 2017 it will return 2001-2100.
 *
 * @param {Date|String|Number} date Date or a year as a string or as a number.
 */
export const getCenturyLabel = (date) => {
  const [start, end] = getCenturyRange(date);
  const startLabel = getYear(start);
  const endLabel = getYear(end);
  return `${startLabel} – ${endLabel}`;
};

/**
 * Returns a string labelling a century of a given date.
 * For example, for 2017 it will return 2011-2020.
 *
 * @param {Date|String|Number} date Date or a year as a string or as a number.
 */
export const getDecadeLabel = (date) => {
  const [start, end] = getDecadeRange(date);
  const startLabel = getYear(start);
  const endLabel = getYear(end);
  return `${startLabel} – ${endLabel}`;
};

/**
 * Returns a boolean determining whether a given date is on Saturday or Sunday.
 * @param {Date} date Date.
 */
export const isWeekend = (date) => {
  const weekday = getDayOfWeek(date);
  return weekday >= 5;
};
<commit_msg>Fix getWeekNumber returning 0 instead of 52 in some cases<commit_after>/* Simple getters - getting a property of a given point in time */

export const getYear = (date) => {
  if (date instanceof Date) {
    return date.getFullYear();
  }

  if (typeof date === 'number') {
    return date;
  }

  const year = parseInt(date, 10);

  if (typeof date === 'string' && !isNaN(year)) {
    return year;
  }

  throw new Error(`Failed to get year from date: ${date}.`);
};

export const getMonthIndex = date => date.getMonth();

export const getDay = date => date.getDate();

export const getDayOfWeek = (date, calendarType = 'ISO 8601') => {
  const weekday = date.getDay();

  switch (calendarType) {
    case 'ISO 8601':
      // Shifts days of the week so that Monday is 0, Sunday is 6
      return (weekday + 6) % 7;
    case 'US':
      return weekday;
    default:
      throw new Error('Unsupported calendar type.');
  }
};

/* Complex getters - getting a property somehow related to a given point in time */

export const getBeginOfCenturyYear = (date) => {
  const year = getYear(date) - 1;
  return year + (-year % 100) + 1;
};

export const getBeginOfCentury = (date) => {
  const beginOfCenturyYear = getBeginOfCenturyYear(date);
  return new Date(beginOfCenturyYear, 0, 1);
};

export const getEndOfCentury = (date) => {
  const beginOfCenturyYear = getBeginOfCenturyYear(date);
  return new Date(beginOfCenturyYear + 100, 0, 1, 0, 0, 0, -1);
};

export const getCenturyRange = date => [
  getBeginOfCentury(date),
  getEndOfCentury(date),
];

export const getBeginOfPreviousCentury = (date) => {
  const previousCenturyYear = getYear(date) - 100;
  return getBeginOfCentury(previousCenturyYear);
};

export const getEndOfPreviousCentury = (date) => {
  const previousCenturyYear = getYear(date) - 100;
  return getEndOfCentury(previousCenturyYear);
};

export const getBeginOfNextCentury = (date) => {
  const nextCenturyYear = getYear(date) + 100;
  return getBeginOfCentury(nextCenturyYear);
};

export const getBeginOfDecadeYear = (date) => {
  const year = getYear(date) - 1;
  return year + (-year % 10) + 1;
};

export const getBeginOfDecade = (date) => {
  const beginOfDecadeYear = getBeginOfDecadeYear(date);
  return new Date(beginOfDecadeYear, 0, 1);
};

export const getEndOfDecade = (date) => {
  const beginOfDecadeYear = getBeginOfDecadeYear(date);
  return new Date(beginOfDecadeYear + 10, 0, 1, 0, 0, 0, -1);
};

export const getDecadeRange = date => [
  getBeginOfDecade(date),
  getEndOfDecade(date),
];

export const getBeginOfPreviousDecade = (date, offset = 10) => {
  const previousDecadeYear = getBeginOfDecadeYear(date) - offset;
  return getBeginOfDecade(previousDecadeYear);
};

export const getEndOfPreviousDecade = (date, offset = 10) => {
  const previousDecadeYear = getBeginOfDecadeYear(date) - offset;
  return getEndOfDecade(previousDecadeYear);
};

export const getBeginOfNextDecade = (date, offset = 10) => {
  const nextDecadeYear = getBeginOfDecadeYear(date) + offset;
  return getBeginOfDecade(nextDecadeYear);
};

/**
 * Returns the beginning of a given year.
 *
 * @param {Date} date Date.
 */
export const getBeginOfYear = (date) => {
  const year = getYear(date);
  return new Date(year, 0, 1);
};

/**
 * Returns the end of a given year.
 *
 * @param {Date} date Date.
 */
export const getEndOfYear = (date) => {
  const year = getYear(date);
  return new Date(year + 1, 0, 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given year.
 *
 * @param {Date} date Date.
 */
export const getYearRange = date => [
  getBeginOfYear(date),
  getEndOfYear(date),
];

export const getBeginOfPreviousYear = (date, offset = 1) => {
  const previousYear = getYear(date) - offset;
  return getBeginOfYear(previousYear);
};

export const getEndOfPreviousYear = (date, offset = 1) => {
  const previousYear = getYear(date) - offset;
  return getEndOfYear(previousYear);
};

export const getBeginOfNextYear = (date, offset = 1) => {
  const nextYear = getYear(date) + offset;
  return getBeginOfYear(nextYear);
};

/**
 * Returns the beginning of a given month.
 *
 * @param {Date} date Date.
 */
export const getBeginOfMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex, 1);
};

/**
 * Returns the end of a given month.
 *
 * @param {Date} date Date.
 */
export const getEndOfMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex + 1, 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given month.
 *
 * @param {Date} date Date.
 */
export const getMonthRange = date => [
  getBeginOfMonth(date),
  getEndOfMonth(date),
];

export const getBeginOfPreviousMonth = (date, offset = 1) => {
  const year = getYear(date);
  const previousMonthIndex = getMonthIndex(date) - offset;
  return getBeginOfMonth(new Date(year, previousMonthIndex, 1));
};

export const getEndOfPreviousMonth = (date, offset = 1) => {
  const year = getYear(date);
  const previousMonthIndex = getMonthIndex(date) - offset;
  return getEndOfMonth(new Date(year, previousMonthIndex, 1));
};

export const getBeginOfNextMonth = (date, offset = 1) => {
  const year = getYear(date);
  const nextMonthIndex = getMonthIndex(date) + offset;
  return getBeginOfMonth(new Date(year, nextMonthIndex, 1));
};

export const getBeginOfDay = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  const day = getDay(date);
  return new Date(year, monthIndex, day);
};

export const getEndOfDay = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  const day = getDay(date);
  return new Date(year, monthIndex, day + 1, 0, 0, 0, -1);
};

/**
 * Returns an array with the beginning and the end of a given day.
 *
 * @param {Date} date Date.
 */
export const getDayRange = date => [
  getBeginOfDay(date),
  getEndOfDay(date),
];

export const getWeekNumber = (date, calendarType = 'ISO 8601') => {
  const tempDate = new Date(+date);
  tempDate.setDate(getDay(tempDate) + (4 - getDayOfWeek(tempDate, calendarType)));
  const yearStart = getBeginOfYear(tempDate);
  const weekNumber = Math.ceil((((tempDate - yearStart) / 8.64e7)) / 7);
  return weekNumber === 0 ? 52 : weekNumber;
};

/**
 * Returns an array with the beginning and the end of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getRange = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getCenturyRange(date);
    case 'decade':
      return getDecadeRange(date);
    case 'year':
      return getYearRange(date);
    case 'month':
      return getMonthRange(date);
    case 'day':
      return getDayRange(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns the beginning of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getBegin = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfCentury(date);
    case 'decade':
      return getBeginOfDecade(date);
    case 'year':
      return getBeginOfYear(date);
    case 'month':
      return getBeginOfMonth(date);
    case 'day':
      return getBeginOfDay(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginPrevious = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfPreviousCentury(date);
    case 'decade':
      return getBeginOfPreviousDecade(date);
    case 'year':
      return getBeginOfPreviousYear(date);
    case 'month':
      return getBeginOfPreviousMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginNext = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getBeginOfNextCentury(date);
    case 'decade':
      return getBeginOfNextDecade(date);
    case 'year':
      return getBeginOfNextYear(date);
    case 'month':
      return getBeginOfNextMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginPrevious2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getBeginOfPreviousDecade(date, 100);
    case 'year':
      return getBeginOfPreviousYear(date, 10);
    case 'month':
      return getBeginOfPreviousMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getBeginNext2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getBeginOfNextDecade(date, 100);
    case 'year':
      return getBeginOfNextYear(date, 10);
    case 'month':
      return getBeginOfNextMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns the end of a given range.
 *
 * @param {String} rangeType Range type (e.g. 'day')
 * @param {Date} date Date.
 */
export const getEnd = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getEndOfCentury(date);
    case 'decade':
      return getEndOfDecade(date);
    case 'year':
      return getEndOfYear(date);
    case 'month':
      return getEndOfMonth(date);
    case 'day':
      return getEndOfDay(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getEndPrevious = (rangeType, date) => {
  switch (rangeType) {
    case 'century':
      return getEndOfPreviousCentury(date);
    case 'decade':
      return getEndOfPreviousDecade(date);
    case 'year':
      return getEndOfPreviousYear(date);
    case 'month':
      return getEndOfPreviousMonth(date);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

export const getEndPrevious2 = (rangeType, date) => {
  switch (rangeType) {
    case 'decade':
      return getEndOfPreviousDecade(date, 100);
    case 'year':
      return getEndOfPreviousYear(date, 10);
    case 'month':
      return getEndOfPreviousMonth(date, 12);
    default:
      throw new Error(`Invalid rangeType: ${rangeType}`);
  }
};

/**
 * Returns a number of days in a month of a given date.
 *
 * @param {Date} date Date.
 */
export const getDaysInMonth = (date) => {
  const year = getYear(date);
  const monthIndex = getMonthIndex(date);
  return new Date(year, monthIndex + 1, 0).getDate();
};

/**
 * Returns a string labelling a century of a given date.
 * For example, for 2017 it will return 2001-2100.
 *
 * @param {Date|String|Number} date Date or a year as a string or as a number.
 */
export const getCenturyLabel = (date) => {
  const [start, end] = getCenturyRange(date);
  const startLabel = getYear(start);
  const endLabel = getYear(end);
  return `${startLabel} – ${endLabel}`;
};

/**
 * Returns a string labelling a century of a given date.
 * For example, for 2017 it will return 2011-2020.
 *
 * @param {Date|String|Number} date Date or a year as a string or as a number.
 */
export const getDecadeLabel = (date) => {
  const [start, end] = getDecadeRange(date);
  const startLabel = getYear(start);
  const endLabel = getYear(end);
  return `${startLabel} – ${endLabel}`;
};

/**
 * Returns a boolean determining whether a given date is on Saturday or Sunday.
 * @param {Date} date Date.
 */
export const isWeekend = (date) => {
  const weekday = getDayOfWeek(date);
  return weekday >= 5;
};
<|endoftext|>


<commit_before>/**
 * Checks if we're running in a browser environment.
 */
export const isBrowser = typeof window !== 'undefined';

/**
 * Checks whether we're running from a local file system.
 */
export const isLocalFileSystem = isBrowser && window.location.protocol === 'file:';

/**
 * Checks whether we're running on a production build or not.
 */
export const isProduction = process.env.NODE_ENV === 'production';

/**
 * Checks whether a variable is defined.
 *
 * @param {*} variable Variable to check
 */
export const isDefined = (variable) => typeof variable !== 'undefined';

/**
 * Checks whether a variable is defined and not null.
 *
 * @param {*} variable Variable to check
 */
export const isProvided = (variable) => isDefined(variable) && variable !== null;

/**
 * Checkes whether a variable provided is a string.
 *
 * @param {*} variable Variable to check
 */
export const isString = (variable) => typeof variable === 'string';

/**
 * Checks whether a variable provided is an ArrayBuffer.
 *
 * @param {*} variable Variable to check
 */
export const isArrayBuffer = (variable) => variable instanceof ArrayBuffer;

/**
 * Checkes whether a variable provided is a Blob.
 *
 * @param {*} variable Variable to check
 */
export const isBlob = (variable) => {
  if (!isBrowser) {
    throw new Error('Attempted to check if a variable is a Blob on a non-browser environment.');
  }

  return variable instanceof Blob;
};

/**
 * Checkes whether a variable provided is a File.
 *
 * @param {*} variable Variable to check
 */
export const isFile = (variable) => {
  if (!isBrowser) {
    throw new Error('Attempted to check if a variable is a File on a non-browser environment.');
  }

  return variable instanceof File;
};

/**
 * Checks whether a string provided is a data URI.
 *
 * @param {string} str String to check
 */
export const isDataURI = (str) => isString(str) && /^data:/.test(str);

export const dataURItoByteString = (dataURI) => {
  if (!isDataURI(dataURI)) {
    throw new Error('Invalid data URI.');
  }

  const [headersString, dataString] = dataURI.split(',');
  const headers = headersString.split(';');

  if (headers.indexOf('base64') !== -1) {
    return atob(dataString);
  }

  return unescape(dataString);
};

export const getPixelRatio = () => (isBrowser && window.devicePixelRatio) || 1;

const consoleOnDev = (method, ...message) => {
  if (!isProduction) {
    // eslint-disable-next-line no-console
    console[method](...message);
  }
};

export const warnOnDev = (...message) => consoleOnDev('warn', ...message);

export const errorOnDev = (...message) => consoleOnDev('error', ...message);

export const displayCORSWarning = () => {
  if (isLocalFileSystem) {
    warnOnDev('Loading PDF as base64 strings/URLs might not work on protocols other than HTTP/HTTPS. On Google Chrome, you can use --allow-file-access-from-files flag for debugging purposes.');
  }
};

export const cancelRunningTask = (runningTask) => {
  if (runningTask && runningTask.cancel) runningTask.cancel();
};

export const makePageCallback = (page, scale) => {
  Object.defineProperty(page, 'width', { get() { return this.view[2] * scale; }, configurable: true });
  Object.defineProperty(page, 'height', { get() { return this.view[3] * scale; }, configurable: true });
  Object.defineProperty(page, 'originalWidth', { get() { return this.view[2]; }, configurable: true });
  Object.defineProperty(page, 'originalHeight', { get() { return this.view[3]; }, configurable: true });
  return page;
};

export const isCancelException = (error) => error.name === 'RenderingCancelledException';

export const loadFromFile = (file) => new Promise((resolve, reject) => {
  const reader = new FileReader();

  reader.onload = () => resolve(new Uint8Array(reader.result));
  reader.onerror = (event) => {
    switch (event.target.error.code) {
      case event.target.error.NOT_FOUND_ERR:
        return reject(new Error('Error while reading a file: File not found.'));
      case event.target.error.NOT_READABLE_ERR:
        return reject(new Error('Error while reading a file: File not readable.'));
      case event.target.error.SECURITY_ERR:
        return reject(new Error('Error while reading a file: Security error.'));
      case event.target.error.ABORT_ERR:
        return reject(new Error('Error while reading a file: Aborted.'));
      default:
        return reject(new Error('Error while reading a file.'));
    }
  };
  reader.readAsArrayBuffer(file);

  return null;
});
<commit_msg>Change arrow functions to normal functions<commit_after>/**
 * Checks if we're running in a browser environment.
 */
export const isBrowser = typeof window !== 'undefined';

/**
 * Checks whether we're running from a local file system.
 */
export const isLocalFileSystem = isBrowser && window.location.protocol === 'file:';

/**
 * Checks whether we're running on a production build or not.
 */
export const isProduction = process.env.NODE_ENV === 'production';

/**
 * Checks whether a variable is defined.
 *
 * @param {*} variable Variable to check
 */
export function isDefined(variable) {
  return typeof variable !== 'undefined';
}

/**
 * Checks whether a variable is defined and not null.
 *
 * @param {*} variable Variable to check
 */
export function isProvided(variable) {
  return isDefined(variable) && variable !== null;
}

/**
 * Checkes whether a variable provided is a string.
 *
 * @param {*} variable Variable to check
 */
export function isString(variable) {
  return typeof variable === 'string';
}

/**
 * Checks whether a variable provided is an ArrayBuffer.
 *
 * @param {*} variable Variable to check
 */
export function isArrayBuffer(variable) {
  return variable instanceof ArrayBuffer;
}

/**
 * Checkes whether a variable provided is a Blob.
 *
 * @param {*} variable Variable to check
 */
export function isBlob(variable) {
  if (!isBrowser) {
    throw new Error('Attempted to check if a variable is a Blob on a non-browser environment.');
  }

  return variable instanceof Blob;
}

/**
 * Checkes whether a variable provided is a File.
 *
 * @param {*} variable Variable to check
 */
export function isFile(variable) {
  if (!isBrowser) {
    throw new Error('Attempted to check if a variable is a File on a non-browser environment.');
  }

  return variable instanceof File;
}

/**
 * Checks whether a string provided is a data URI.
 *
 * @param {string} str String to check
 */
export function isDataURI(str) {
  return isString(str) && /^data:/.test(str);
}

export function dataURItoByteString(dataURI) {
  if (!isDataURI(dataURI)) {
    throw new Error('Invalid data URI.');
  }

  const [headersString, dataString] = dataURI.split(',');
  const headers = headersString.split(';');

  if (headers.indexOf('base64') !== -1) {
    return atob(dataString);
  }

  return unescape(dataString);
}

export function getPixelRatio() {
  return (isBrowser && window.devicePixelRatio) || 1;
}

function consoleOnDev(method, ...message) {
  if (!isProduction) {
    // eslint-disable-next-line no-console
    console[method](...message);
  }
}

export function warnOnDev(...message) {
  consoleOnDev('warn', ...message);
}

export function errorOnDev(...message) {
  consoleOnDev('error', ...message);
}

export function displayCORSWarning() {
  if (isLocalFileSystem) {
    warnOnDev('Loading PDF as base64 strings/URLs might not work on protocols other than HTTP/HTTPS. On Google Chrome, you can use --allow-file-access-from-files flag for debugging purposes.');
  }
}

export function cancelRunningTask(runningTask) {
  if (runningTask && runningTask.cancel) runningTask.cancel();
}

export function makePageCallback(page, scale) {
  Object.defineProperty(page, 'width', { get() { return this.view[2] * scale; }, configurable: true });
  Object.defineProperty(page, 'height', { get() { return this.view[3] * scale; }, configurable: true });
  Object.defineProperty(page, 'originalWidth', { get() { return this.view[2]; }, configurable: true });
  Object.defineProperty(page, 'originalHeight', { get() { return this.view[3]; }, configurable: true });
  return page;
}

export function isCancelException(error) {
  return error.name === 'RenderingCancelledException';
}

export function loadFromFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = () => resolve(new Uint8Array(reader.result));
    reader.onerror = (event) => {
      switch (event.target.error.code) {
        case event.target.error.NOT_FOUND_ERR:
          return reject(new Error('Error while reading a file: File not found.'));
        case event.target.error.NOT_READABLE_ERR:
          return reject(new Error('Error while reading a file: File not readable.'));
        case event.target.error.SECURITY_ERR:
          return reject(new Error('Error while reading a file: Security error.'));
        case event.target.error.ABORT_ERR:
          return reject(new Error('Error while reading a file: Aborted.'));
        default:
          return reject(new Error('Error while reading a file.'));
      }
    };
    reader.readAsArrayBuffer(file);

    return null;
  });
}
<|endoftext|>


<commit_before>"use strict";

function layoutContainer(units) {

    var options = [], hash = {}, minSurface = 1e6;

    function normalizeOption(option) {
        option = JSON.parse(option);

        var minSize = 1;
        option.state.units.forEach(function (glyph) {
            if (glyph.size[0] < minSize) {
                minSize = Math.max(glyph.size[0], glyph.size[1]);
            }
        });

        option.size = [option.size[0] / minSize, option.size[1] / minSize];
        option.surface /= minSize * minSize;
        option.state.units.forEach(function (glyph) {
            glyph.size[0] /= minSize;
            glyph.size[1] /= minSize;
            glyph.position[0] /= minSize;
            glyph.position[1] /= minSize;
        });

        return option;
    }

    function go(units, state, iterator) {
        var newSize, newState, newForbidden, i, j,
            goesDown, index, length,
            prevSize, unitPosition, newOption;

        // place the first unit directly
        if (!state) {
            var singleSize = units[0].size;
            newState = {
                units: [{unit: units[0], size: singleSize, position: [0, 0]}],
                size: singleSize,
                forbidden: [JSON.stringify(singleSize)]
            };

            if (units.length === 1) {
                newOption = {
                    type: 'container',
                    state: newState,
                    size: singleSize,
                    ratio: singleSize[0] / singleSize[1],
                    normedRatio: singleSize[0] / singleSize[1] < 1 ? singleSize[0] / singleSize[1] : singleSize[1] / singleSize[0],
                    surface: singleSize[0] * singleSize[1]
                };
                options.push(newOption);
                hash[JSON.stringify(newOption)] = newOption;
                return;
            }
            for (j = 1; j < units.length; j++) {
                go(units, newState, {goesDown: false, index: 1, length: j});
                go(units, newState, {goesDown: true, index: 1, length: j});
            }

            return;
        }

        // set up aliases, start parameters and clone the state
        goesDown = iterator.goesDown;
        index = iterator.index;
        length = iterator.length;
        newState = JSON.parse(JSON.stringify(state));
        newSize = newState.size;
        newForbidden = newState.forbidden;
        prevSize = units[index].size;
        unitPosition = [!goesDown ? state.size[0] : 0, goesDown ? state.size[1] : 0];

        // determine size sum and check if all following glyphs are compatible
        var sizeSum = [0, 0], addSize;
        for (i = index; i < index + length; i++) {
            addSize = units[i].size;
            if ((goesDown && addSize[1] !== prevSize[1]) ||
                (!goesDown && addSize[0] !== prevSize[0])) {
                return;
            }
            sizeSum = [sizeSum[0] + addSize[0], sizeSum[1] + addSize[1]];
        }

        // now add the units one by one
        for (i = index; i < index + length; i++) {
            var glyphSize = [], add = 0,
                addAxis = goesDown ? 1 : 0,
                fixedAxis = goesDown ? 0 : 1;

            addSize = units[i].size;

            add = addSize[addAxis] * state.size[fixedAxis] / sizeSum[fixedAxis];
            glyphSize[addAxis] = add;
            glyphSize[fixedAxis] = addSize[fixedAxis] * add / addSize[addAxis];

            if (i === index) {
                newSize[addAxis] += add;
            }
            if (goesDown && newForbidden.indexOf(JSON.stringify(unitPosition)) > -1) {
                return;
            }

            prevSize = addSize;
            newState.units.push({unit: units[i], size: glyphSize, position: unitPosition});

            // update glyph position to slot for next glyph
            unitPosition = [unitPosition[0] + (goesDown ? glyphSize[0] : 0),
                unitPosition[1] + (!goesDown ? glyphSize[1] : 0)];

            // forbid next glyphs to start at the lower-right corner of a previous glyph (this breaks the reading flow rules
            newForbidden.push(JSON.stringify([unitPosition[0] + (!goesDown ? glyphSize[0] : 0),
                unitPosition[1] + (goesDown ? glyphSize[1] : 0)]));
        }

        // if all units are used up we can stop and add the final result
        if (index + length === units.length) {
            newOption = {
                type: 'container',
                state: newState,
                size: newSize,
                ratio: newSize[0] / newSize[1],
                normedRatio: newSize[0] / newSize[1] < 1 ? newSize[0] / newSize[1] : newSize[1] / newSize[0],
                surface: newSize[0] * newSize[1]
            };

            newOption = normalizeOption(JSON.stringify(newOption));

            minSurface = Math.min(minSurface, newOption.surface);

            // surface can't be 3 times larger than the optimum
            if (!hash[JSON.stringify(newOption)] && newOption.surface / minSurface < 3) {
                options.push(newOption);
                hash[JSON.stringify(newOption)] = newOption;
            }
            return;
        }

        for (j = 1; j < units.length - (index + length) + 1; j++) {
            go(units, newState, {goesDown: false, index: index + length, length: j});
            go(units, newState, {goesDown: true, index: index + length, length: j});
        }
    }

    go(units);
    return options;
}

function convertNounPhrase(tokens) {
    var smallModifiers = ['kon', 'lili', 'mute', 'sin'],
        narrowModifiers = ['wan', 'tu', 'anu', 'en', 'kin'],
        punctuation = ['period', 'comma', 'exclamation'],
        largePunctuation = ['la'],
        options;

    function getSizeOf(token) {
        if (punctuation.indexOf(token) > -1) {
            return [4, 0.75];
        } else if (largePunctuation.indexOf(token) > -1) {
            return [4, 1];
        } else if (smallModifiers.indexOf(token) > -1) {
            return [1, 0.5];
        } else if (narrowModifiers.indexOf(token) > -1) {
            return [0.5, 1];
        } else {
            return [1, 1];
        }
    }

    var units = [];
    tokens.forEach(function (token) {
        units.push({rule: 'word-glyph', token: token, size: getSizeOf(token)});
    });

    options = layoutContainer(units);

    return options;
}

function layoutCompound(sentence) {
    var hashMap = [], compoundOptions = [];
    console.log(sentence);
    sentence.forEach(function (part) {
        var npOptions = [];
        if (part.parts) {
            npOptions = layoutCompound(part.parts);
        } else {
            npOptions = convertNounPhrase(part.tokens);
        }

        hashMap.push({part: part.part, sep: part.sep, options: npOptions});
    });

    function go(index, units) {
        hashMap[index].options.forEach(function (option) {
            var newIndex = index + 1,
                newUnits = units.slice(0);

            option.type = hashMap[index].part === 'punctuation' ? 'punctuation' : 'container';
            option.separator = hashMap[index].sep;
            newUnits.push(option);
            if (newIndex < hashMap.length) {
                go(newIndex, newUnits);
            } else {
                compoundOptions.push.apply(compoundOptions, layoutContainer(newUnits));
            }
        });
    }

    if (hashMap.length > 0) {
        go(0, []);
    } else {
        console.log('WARNING: empty text to layout');
    }
    return compoundOptions;
}

function renderCompoundSentence(sentence, target, optimalRatio) {
    var compounds = [];

    // create sentence parts
    var sentenceCompound = [];
    sentence.forEach(function (part) {
        sentenceCompound.push(part);
        if (part.part === 'punctuation') {
            compounds.push(sentenceCompound);
            sentenceCompound = [];
        }
    });
    if (sentenceCompound.length > 0) {
        compounds.push(sentenceCompound);
    }

    var bestOptions = [];
    compounds.forEach(function (compound) {
        var sorter = function (optimal) {
            return function (a, b) {
                return Math.abs(optimal - a.ratio) - Math.abs(optimal - b.ratio);
            };
        };

        var compoundOptions = layoutCompound(compound);
        compoundOptions.sort(sorter(optimalRatio));

        bestOptions.push(compoundOptions[0]);
    });

    return sitelenRenderer.renderComplexLayout(bestOptions, target, {exportable: true});
}

function renderInteractiveSentence(sentence) {
    var compound = document.createElement('div'),
        settings = {exportable: true},
        sorter = function (optimal) {
            return function (a, b) {
                return Math.abs(optimal - a.ratio) - Math.abs(optimal - b.ratio);
            };
        };

    document.getElementById('sitelen').appendChild(compound);

    var compoundOptions = layoutCompound(sentence);

    compound.addEventListener('mousemove', function (event) {
        if (event.clientY - compound.offsetTop < 150) {
            var optimal = 0.5 + 1.5 * ((event.clientX - compound.offsetLeft) / compound.clientWidth);
            render(optimal);
        }
    });

    render(0.8);

    function render(optimal) {
        var tokens = [];
        sentence.forEach(function (part) {
            if (part.tokens) {
                part.tokens.forEach(function (token) {
                    tokens.push(token);
                });
            }
        });

        var filename = tokens.join('-') + '.svg';

        compound.innerHTML = "";
        renderCompoundSentence(sentence, compound, optimal);

        var text = document.getElementById('sitelen').firstElementChild.innerHTML;
        var pom = document.createElement('a');
        pom.innerHTML = 'download as SVG';
        pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        pom.setAttribute('download', filename);
        compound.appendChild(pom);
    }

    return compound;
}
<commit_msg>Allow less size difference in the glyphs.<commit_after>"use strict";

function layoutContainer(units) {

    var options = [], hash = {}, minSurface = 1e6;

    function normalizeOption(option) {
        option = JSON.parse(option);

        var minSize = 1;
        option.state.units.forEach(function (glyph) {
            if (glyph.size[0] < minSize) {
                minSize = Math.max(glyph.size[0], glyph.size[1]);
            }
        });

        option.size = [option.size[0] / minSize, option.size[1] / minSize];
        option.surface /= minSize * minSize;
        option.state.units.forEach(function (glyph) {
            glyph.size[0] /= minSize;
            glyph.size[1] /= minSize;
            glyph.position[0] /= minSize;
            glyph.position[1] /= minSize;
        });

        return option;
    }

    function go(units, state, iterator) {
        var newSize, newState, newForbidden, i, j,
            goesDown, index, length,
            prevSize, unitPosition, newOption;

        // place the first unit directly
        if (!state) {
            var singleSize = units[0].size;
            newState = {
                units: [{unit: units[0], size: singleSize, position: [0, 0]}],
                size: singleSize,
                forbidden: [JSON.stringify(singleSize)]
            };

            if (units.length === 1) {
                newOption = {
                    type: 'container',
                    state: newState,
                    size: singleSize,
                    ratio: singleSize[0] / singleSize[1],
                    normedRatio: singleSize[0] / singleSize[1] < 1 ? singleSize[0] / singleSize[1] : singleSize[1] / singleSize[0],
                    surface: singleSize[0] * singleSize[1]
                };
                options.push(newOption);
                hash[JSON.stringify(newOption)] = newOption;
                return;
            }
            for (j = 1; j < units.length; j++) {
                go(units, newState, {goesDown: false, index: 1, length: j});
                go(units, newState, {goesDown: true, index: 1, length: j});
            }

            return;
        }

        // set up aliases, start parameters and clone the state
        goesDown = iterator.goesDown;
        index = iterator.index;
        length = iterator.length;
        newState = JSON.parse(JSON.stringify(state));
        newSize = newState.size;
        newForbidden = newState.forbidden;
        prevSize = units[index].size;
        unitPosition = [!goesDown ? state.size[0] : 0, goesDown ? state.size[1] : 0];

        // determine size sum and check if all following glyphs are compatible
        var sizeSum = [0, 0], addSize;
        for (i = index; i < index + length; i++) {
            addSize = units[i].size;
            if ((goesDown && addSize[1] !== prevSize[1]) ||
                (!goesDown && addSize[0] !== prevSize[0])) {
                return;
            }
            sizeSum = [sizeSum[0] + addSize[0], sizeSum[1] + addSize[1]];
        }

        // now add the units one by one
        for (i = index; i < index + length; i++) {
            var glyphSize = [], add = 0,
                addAxis = goesDown ? 1 : 0,
                fixedAxis = goesDown ? 0 : 1;

            addSize = units[i].size;

            add = addSize[addAxis] * state.size[fixedAxis] / sizeSum[fixedAxis];
            glyphSize[addAxis] = add;
            glyphSize[fixedAxis] = addSize[fixedAxis] * add / addSize[addAxis];

            if (i === index) {
                newSize[addAxis] += add;
            }
            if (goesDown && newForbidden.indexOf(JSON.stringify(unitPosition)) > -1) {
                return;
            }

            prevSize = addSize;
            newState.units.push({unit: units[i], size: glyphSize, position: unitPosition});

            // update glyph position to slot for next glyph
            unitPosition = [unitPosition[0] + (goesDown ? glyphSize[0] : 0),
                unitPosition[1] + (!goesDown ? glyphSize[1] : 0)];

            // forbid next glyphs to start at the lower-right corner of a previous glyph (this breaks the reading flow rules
            newForbidden.push(JSON.stringify([unitPosition[0] + (!goesDown ? glyphSize[0] : 0),
                unitPosition[1] + (goesDown ? glyphSize[1] : 0)]));
        }

        // if all units are used up we can stop and add the final result
        if (index + length === units.length) {
            newOption = {
                type: 'container',
                state: newState,
                size: newSize,
                ratio: newSize[0] / newSize[1],
                normedRatio: newSize[0] / newSize[1] < 1 ? newSize[0] / newSize[1] : newSize[1] / newSize[0],
                surface: newSize[0] * newSize[1]
            };

            newOption = normalizeOption(JSON.stringify(newOption));

            minSurface = Math.min(minSurface, newOption.surface);

            // surface can't be 2 times larger than the optimum
            if (!hash[JSON.stringify(newOption)] && newOption.surface / minSurface < 2) {
                options.push(newOption);
                hash[JSON.stringify(newOption)] = newOption;
            }
            return;
        }

        for (j = 1; j < units.length - (index + length) + 1; j++) {
            go(units, newState, {goesDown: false, index: index + length, length: j});
            go(units, newState, {goesDown: true, index: index + length, length: j});
        }
    }

    go(units);
    return options;
}

function convertNounPhrase(tokens) {
    var smallModifiers = ['kon', 'lili', 'mute', 'sin'],
        narrowModifiers = ['wan', 'tu', 'anu', 'en', 'kin'],
        punctuation = ['period', 'comma', 'exclamation'],
        largePunctuation = ['la'],
        options;

    function getSizeOf(token) {
        if (punctuation.indexOf(token) > -1) {
            return [4, 0.75];
        } else if (largePunctuation.indexOf(token) > -1) {
            return [4, 1];
        } else if (smallModifiers.indexOf(token) > -1) {
            return [1, 0.5];
        } else if (narrowModifiers.indexOf(token) > -1) {
            return [0.5, 1];
        } else {
            return [1, 1];
        }
    }

    var units = [];
    tokens.forEach(function (token) {
        units.push({rule: 'word-glyph', token: token, size: getSizeOf(token)});
    });

    options = layoutContainer(units);

    return options;
}

function layoutCompound(sentence) {
    var hashMap = [], compoundOptions = [];
    console.log(sentence);
    sentence.forEach(function (part) {
        var npOptions = [];
        if (part.parts) {
            npOptions = layoutCompound(part.parts);
        } else {
            npOptions = convertNounPhrase(part.tokens);
        }

        hashMap.push({part: part.part, sep: part.sep, options: npOptions});
    });

    function go(index, units) {
        hashMap[index].options.forEach(function (option) {
            var newIndex = index + 1,
                newUnits = units.slice(0);

            option.type = hashMap[index].part === 'punctuation' ? 'punctuation' : 'container';
            option.separator = hashMap[index].sep;
            newUnits.push(option);
            if (newIndex < hashMap.length) {
                go(newIndex, newUnits);
            } else {
                compoundOptions.push.apply(compoundOptions, layoutContainer(newUnits));
            }
        });
    }

    if (hashMap.length > 0) {
        go(0, []);
    } else {
        console.log('WARNING: empty text to layout');
    }
    return compoundOptions;
}

function renderCompoundSentence(sentence, target, optimalRatio) {
    var compounds = [];

    // create sentence parts
    var sentenceCompound = [];
    sentence.forEach(function (part) {
        sentenceCompound.push(part);
        if (part.part === 'punctuation') {
            compounds.push(sentenceCompound);
            sentenceCompound = [];
        }
    });
    if (sentenceCompound.length > 0) {
        compounds.push(sentenceCompound);
    }

    var bestOptions = [];
    compounds.forEach(function (compound) {
        var sorter = function (optimal) {
            return function (a, b) {
                return Math.abs(optimal - a.ratio) - Math.abs(optimal - b.ratio);
            };
        };

        var compoundOptions = layoutCompound(compound);
        compoundOptions.sort(sorter(optimalRatio));

        bestOptions.push(compoundOptions[0]);
    });

    return sitelenRenderer.renderComplexLayout(bestOptions, target, {exportable: true});
}

function renderInteractiveSentence(sentence) {
    var compound = document.createElement('div'),
        settings = {exportable: true},
        sorter = function (optimal) {
            return function (a, b) {
                return Math.abs(optimal - a.ratio) - Math.abs(optimal - b.ratio);
            };
        };

    document.getElementById('sitelen').appendChild(compound);

    var compoundOptions = layoutCompound(sentence);

    compound.addEventListener('mousemove', function (event) {
        if (event.clientY - compound.offsetTop < 150) {
            var optimal = 0.5 + 1.5 * ((event.clientX - compound.offsetLeft) / compound.clientWidth);
            render(optimal);
        }
    });

    render(0.8);

    function render(optimal) {
        var tokens = [];
        sentence.forEach(function (part) {
            if (part.tokens) {
                part.tokens.forEach(function (token) {
                    tokens.push(token);
                });
            }
        });

        var filename = tokens.join('-') + '.svg';

        compound.innerHTML = "";
        renderCompoundSentence(sentence, compound, optimal);

        var text = document.getElementById('sitelen').firstElementChild.innerHTML;
        var pom = document.createElement('a');
        pom.innerHTML = 'download as SVG';
        pom.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        pom.setAttribute('download', filename);
        compound.appendChild(pom);
    }

    return compound;
}
<|endoftext|>


<commit_before>/**
 * Pew / Project / Keyboard
 */
Pew.Project.prototype.keyboard = (function(){

    var keys = [], //curent down keys
        events_cb = {}, //keycode event callback
        aliases = { // aliases for special key
            'SHIFT'     : 16,
            'TAB'       : 9,
            'CTRL'      : 17,
            'ALT'       : 18,
            'SPACE'     : 32,
            'BACKSPACE' : 8,
            'ENTER'     : 13,
            'ESC'       : 27,
            'LEFT'      : 37,
            'RIGHT'     : 39,
            'UP'        : 38,
            'DOWN'      : 40,
        };

    // add f1 to f12 keycode to aliases, the art of laziness
    for(i=1;i<13;++i) aliases["F"+i] = 111 + i;

    /**
     * Bind event keydown and keypress over document
     */
    function bindEvent() {

        // keyboard events
        document.body.addEventListener("keydown", function (e) {

            keys[e.keyCode] = true;

            if(events_cb[e.keyCode]) {
                triggerCallback(e.keyCode);
                e.preventDefault();
            }
        });

        document.body.addEventListener("keyup", function (e) {
            keys[e.keyCode] = false;
        });
    }

    /**
     * Trigger manually an event
     *
     * @param  string|integer k (keycode or keychar)
     */
    function triggerCallback(k) {

        var code = toKeyCode(k);

        if(events_cb[code]) {
            events_cb[code]({
                "char" : toKeyChar(code).toLowerCase(),
                "code" : code
            });
        }
    }

    /**
     * Transform char/string to keycode.
     *
     * @param  string str
     * @return integer
     */
    function toKeyCode(str) {
        if(isNaN(str)) {
            var str = aliases[str.toUpperCase()] || str;
            if(isNaN(str)) {
                str = str.toUpperCase().charCodeAt(0);
            }
        }
        return str;
    }

    /**
     * Transform code to char/alias string.
     *
     * @param  integer str
     * @return string
     */
    function toKeyChar(code) {

        if(isNaN(code)) return code;

        for(key in aliases) {
            if(aliases[key] == code) {
                return key;
            }
        }
        return String.fromCharCode(code);
    }

    /**
     * Public
     */
    return {

        init: bindEvent,
        toKeyCode: toKeyCode,
        toKeyChar: toKeyChar,
        trigger: triggerCallback,

        /**
         * Check for keyboard keycode input key code.
         *
         * @param  integer code
         * @return bool
         */
        key: function(code) {

            code = toKeyCode(code);

            if(keys[code] === undefined)
                keys[code] = false;

            return keys[code];
        },

        /**
         * Return an array of current keydown keycodes
         * @return array
         */
        keys: function() {

            var result = [];

            for(key in keys) {
                if(keys[key] != false)
                    result.push(key);
            }
            // return current keys
            return result;
        },

        /**
         * Bind key(s) event
         *
         * @param  mixed    k   keycode/keyalias or an array of keycode/keyalias
         * @param  function c
         */
        on: function(k, c) {

            if(Object.prototype.toString.call(k) === '[object Array]') {
                for(var i in k) {
                    events_cb[toKeyCode(k[i])] = c;
                }
            }
            else {
                events_cb[toKeyCode(k)] = c;
            }
        },

        /**
         * Unbind "on" event(s), or unbind all events if no arg
         *
         * @param mixed k
         */
        off: function(k) {

            if(k === undefined) {
                for(var k in events_cb) delete events_cb[k];
                return;
            }

            if(Object.prototype.toString.call(k) === '[object Array]') {
                for(var i in k) {
                    delete events_cb[toKeyCode(k[i])];
                }
            }
            else {
                delete events_cb[toKeyCode(k)];
            }
        },
    }
})();
<commit_msg>fixed missing var keyword<commit_after>/**
 * Pew / Project / Keyboard
 */
Pew.Project.prototype.keyboard = (function(){

    var keys = [], //curent down keys
        events_cb = {}, //keycode event callback
        aliases = { // aliases for special key
            'SHIFT'     : 16,
            'TAB'       : 9,
            'CTRL'      : 17,
            'ALT'       : 18,
            'SPACE'     : 32,
            'BACKSPACE' : 8,
            'ENTER'     : 13,
            'ESC'       : 27,
            'LEFT'      : 37,
            'RIGHT'     : 39,
            'UP'        : 38,
            'DOWN'      : 40,
        };

    // add f1 to f12 keycode to aliases, the art of laziness
    for(var i=1;i<13;++i) aliases["F"+i] = 111 + i;

    /**
     * Bind event keydown and keypress over document
     */
    function bindEvent() {

        // keyboard events
        document.body.addEventListener("keydown", function (e) {

            keys[e.keyCode] = true;

            if(events_cb[e.keyCode]) {
                triggerCallback(e.keyCode);
                e.preventDefault();
            }
        });

        document.body.addEventListener("keyup", function (e) {
            keys[e.keyCode] = false;
        });
    }

    /**
     * Trigger manually an event
     *
     * @param  string|integer k (keycode or keychar)
     */
    function triggerCallback(k) {

        var code = toKeyCode(k);

        if(events_cb[code]) {
            events_cb[code]({
                "char" : toKeyChar(code).toLowerCase(),
                "code" : code
            });
        }
    }

    /**
     * Transform char/string to keycode.
     *
     * @param  string str
     * @return integer
     */
    function toKeyCode(str) {
        if(isNaN(str)) {
            var str = aliases[str.toUpperCase()] || str;
            if(isNaN(str)) {
                str = str.toUpperCase().charCodeAt(0);
            }
        }
        return str;
    }

    /**
     * Transform code to char/alias string.
     *
     * @param  integer str
     * @return string
     */
    function toKeyChar(code) {

        if(isNaN(code)) return code;

        for(var key in aliases) {
            if(aliases[key] == code) {
                return key;
            }
        }
        return String.fromCharCode(code);
    }

    /**
     * Public
     */
    return {

        init: bindEvent,
        toKeyCode: toKeyCode,
        toKeyChar: toKeyChar,
        trigger: triggerCallback,

        /**
         * Check for keyboard keycode input key code.
         *
         * @param  integer code
         * @return bool
         */
        key: function(code) {

            code = toKeyCode(code);

            if(keys[code] === undefined)
                keys[code] = false;

            return keys[code];
        },

        /**
         * Return an array of current keydown keycodes
         * @return array
         */
        keys: function() {

            var result = [];

            for(var key in keys) {
                if(keys[key] != false)
                    result.push(key);
            }
            // return current keys
            return result;
        },

        /**
         * Bind key(s) event
         *
         * @param  mixed    k   keycode/keyalias or an array of keycode/keyalias
         * @param  function c
         */
        on: function(k, c) {

            if(Object.prototype.toString.call(k) === '[object Array]') {
                for(var i in k) {
                    events_cb[toKeyCode(k[i])] = c;
                }
            }
            else {
                events_cb[toKeyCode(k)] = c;
            }
        },

        /**
         * Unbind "on" event(s), or unbind all events if no arg
         *
         * @param mixed k
         */
        off: function(k) {

            if(k === undefined) {
                for(var k in events_cb) delete events_cb[k];
                return;
            }

            if(Object.prototype.toString.call(k) === '[object Array]') {
                for(var i in k) {
                    delete events_cb[toKeyCode(k[i])];
                }
            }
            else {
                delete events_cb[toKeyCode(k)];
            }
        },
    }
})();
<|endoftext|>