<commit_before>import R from 'ramda';
import Promise from 'bluebird';
import chalk from 'chalk';
import express from 'express';
import fsPath from 'path';
import shell from 'shelljs';
import semver from 'semver';
import webpackConfig from './webpack-config';
import webpackDevServer from './webpack-dev-server';
import uihPackageJson from '../../package.json';
import {
  formatSpecPaths,
  rootModulePath,
  trimRootModulePath,
  REACT_PATH,
} from './paths';
import log from '../shared/log';
import initRelay from '../relay/init-relay';
import * as yamlConfig from './yaml-config';

const REQUIRED_NODE_VERSION = '>=5.5.0';
const ROOT_PATH = rootModulePath();
const YAML_CONFIG = yamlConfig.load() || {};

const displayPath = (path) => trimRootModulePath(path);

/**
 * Starts the UIHarness development server.
 *
 * @param {Object} options:
 *
 *    --entry:             Required. Path to the specs files (comma seperated if more than one).
 *
 *    --port:              Optional. The port to start the server on.
 *                         Default: 3030
 *
 *    --proxy:             Optional. An object containing { path, host } mappings
 *                         to proxy server requests to.
 *                         (https://webpack.github.io/docs/webpack-dev-server.html#proxy)
 *
 *   -- graphqlSchema:     Optional. A path to the GraphQL `schema.js` or `schema.json` file.
 *                         If not specified Relay will not be enabled.
 *
 * @return {Promise}.
 */
export default (options = {}) => new Promise((resolve, reject) => {
  (async () => {
    // Setup initial conditions.
    const packageJson = require(fsPath.resolve('./package.json'));

    // Extract options or default values.
    const entry = options.entry || YAML_CONFIG.entry;
    const env = process.env.NODE_ENV || 'development';
    const port = options.port || YAML_CONFIG.port || 3030;
    const proxy = options.proxy || YAML_CONFIG.proxy;
    const graphqlSchema = options.graphqlSchema || YAML_CONFIG.graphqlSchema;
    const images = options.images
      || YAML_CONFIG.images
      || { baseUrl: `/${ packageJson.name }/images`, dir: 'images' };
    const cssModules = options.cssModules || YAML_CONFIG.cssModules;

    // Ensure required values exist.
    if (R.isNil(entry) || R.isEmpty(entry)) { throw new Error('Entry path(s) must be specified.'); }

    // Ensure the minimum version of node is supported.
    const nodeVersion = semver.clean(shell.exec('node -v', { silent: true }).stdout);
    if (!semver.satisfies(nodeVersion, REQUIRED_NODE_VERSION)) {
      return reject(new Error(`The UIHarness requires node version ${ REQUIRED_NODE_VERSION }.`));
    }

    // Ensure ES6+ within the specs can be imported.
    require('babel-register');

    // Initialize the Relay/GraphQL schema (if specified).
    const isRelayEnabled = R.is(String, graphqlSchema);
    if (isRelayEnabled) {
      try {
        await initRelay(graphqlSchema);
      } catch (err) {
        return reject(err);
      }
    }

    // Prepare the Webpack configuration.
    const specs = formatSpecPaths(entry);
    const config = webpackConfig({
      isRelayEnabled,
      entry: specs,
      outputFile: 'specs.js',
      cssModules,
    });

    // Create the development server.
    const app = webpackDevServer(config, { proxy });
    app.use('/', express.static(fsPath.resolve(__dirname, '../../public')));

    // Create an end-point to serve images from.
    app.use(images.baseUrl, express.static(fsPath.join(ROOT_PATH, images.dir)));

    // Start the server.
    log.info('\n');
    log.info(chalk.grey(`Starting (${ env })...`));
    app.listen(port, () => {
      // Server details.
      const reactJson = require(fsPath.join(REACT_PATH, 'package.json'));
      const moduleVersion = packageJson.version || '0.0.0';
      const packageName = chalk.magenta(packageJson.name);
      const packageVersion = chalk.grey(`(v${ moduleVersion })`);
      log.info();
      log.info(chalk.green('UIHarness:'));
      log.info(chalk.grey(' - module:   '), packageName, packageVersion);
      log.info(chalk.grey(' - port:     '), port);
      log.info(chalk.grey(' - react:    '), `v${ reactJson.version }`);
      log.info(chalk.grey(' - uiharness:'), `v${ uihPackageJson.version }`);
      if (isRelayEnabled) {
        log.info(chalk.grey(' - graphql:  '), displayPath(graphqlSchema));
      }
      // Specs.
      log.info(chalk.grey(' - specs:    '), displayPath(specs[0]) || chalk.magenta('None.'));
      R.takeLast(specs.length - 1, specs).forEach(path => {
        log.info(chalk.grey('             '), displayPath(path));
      });

      // Images.
      log.info(chalk.grey(' - images:   '), chalk.grey(`${ images.baseUrl } =>`), images.dir);

      // Proxy.
      if (YAML_CONFIG.proxy) {
        const formatProxy = (item) => `${ chalk.grey(item.from, '=>') } ${ item.to }`;
        const proxyItems = Object
          .keys(YAML_CONFIG.proxy)
          .map(key => ({ from: key, to: proxy[key] }));
        log.info(chalk.grey(' - proxy:    '), formatProxy(proxyItems[0]));
        R.takeLast(proxyItems.length - 1, proxyItems).forEach(item => {
          log.info(chalk.grey('             '), formatProxy(item));
        });
      }

      // Finish up.
      log.info('');
      resolve({});
    });
    return undefined;
  })();
});
<commit_msg>Move UIHarness version output.<commit_after>import R from 'ramda';
import Promise from 'bluebird';
import chalk from 'chalk';
import express from 'express';
import fsPath from 'path';
import shell from 'shelljs';
import semver from 'semver';
import webpackConfig from './webpack-config';
import webpackDevServer from './webpack-dev-server';
import uiharnessJson from '../../package.json';
import {
  formatSpecPaths,
  rootModulePath,
  trimRootModulePath,
  REACT_PATH,
} from './paths';
import log from '../shared/log';
import initRelay from '../relay/init-relay';
import * as yamlConfig from './yaml-config';

const REQUIRED_NODE_VERSION = '>=5.5.0';
const ROOT_PATH = rootModulePath();
const YAML_CONFIG = yamlConfig.load() || {};

const displayPath = (path) => trimRootModulePath(path);

/**
 * Starts the UIHarness development server.
 *
 * @param {Object} options:
 *
 *    --entry:             Required. Path to the specs files (comma seperated if more than one).
 *
 *    --port:              Optional. The port to start the server on.
 *                         Default: 3030
 *
 *    --proxy:             Optional. An object containing { path, host } mappings
 *                         to proxy server requests to.
 *                         (https://webpack.github.io/docs/webpack-dev-server.html#proxy)
 *
 *   -- graphqlSchema:     Optional. A path to the GraphQL `schema.js` or `schema.json` file.
 *                         If not specified Relay will not be enabled.
 *
 * @return {Promise}.
 */
export default (options = {}) => new Promise((resolve, reject) => {
  (async () => {
    // Setup initial conditions.
    const packageJson = require(fsPath.resolve('./package.json'));

    // Extract options or default values.
    const entry = options.entry || YAML_CONFIG.entry;
    const env = process.env.NODE_ENV || 'development';
    const port = options.port || YAML_CONFIG.port || 3030;
    const proxy = options.proxy || YAML_CONFIG.proxy;
    const graphqlSchema = options.graphqlSchema || YAML_CONFIG.graphqlSchema;
    const images = options.images
      || YAML_CONFIG.images
      || { baseUrl: `/${ packageJson.name }/images`, dir: 'images' };
    const cssModules = options.cssModules || YAML_CONFIG.cssModules;

    // Ensure required values exist.
    if (R.isNil(entry) || R.isEmpty(entry)) { throw new Error('Entry path(s) must be specified.'); }

    // Ensure the minimum version of node is supported.
    const nodeVersion = semver.clean(shell.exec('node -v', { silent: true }).stdout);
    if (!semver.satisfies(nodeVersion, REQUIRED_NODE_VERSION)) {
      return reject(new Error(`The UIHarness requires node version ${ REQUIRED_NODE_VERSION }.`));
    }

    // Ensure ES6+ within the specs can be imported.
    require('babel-register');

    // Initialize the Relay/GraphQL schema (if specified).
    const isRelayEnabled = R.is(String, graphqlSchema);
    if (isRelayEnabled) {
      try {
        await initRelay(graphqlSchema);
      } catch (err) {
        return reject(err);
      }
    }

    // Prepare the Webpack configuration.
    const specs = formatSpecPaths(entry);
    const config = webpackConfig({
      isRelayEnabled,
      entry: specs,
      outputFile: 'specs.js',
      cssModules,
    });

    // Create the development server.
    const app = webpackDevServer(config, { proxy });
    app.use('/', express.static(fsPath.resolve(__dirname, '../../public')));

    // Create an end-point to serve images from.
    app.use(images.baseUrl, express.static(fsPath.join(ROOT_PATH, images.dir)));

    // Start the server.
    log.info('\n');
    log.info(chalk.grey(`Starting (${ env })...`));
    app.listen(port, () => {
      // Server details.
      const reactJson = require(fsPath.join(REACT_PATH, 'package.json'));
      const moduleVersion = packageJson.version || '0.0.0';
      const packageName = chalk.magenta(packageJson.name);
      const packageVersion = chalk.grey(`(v${ moduleVersion })`);
      log.info();
      log.info(chalk.green(`UIHarness${ chalk.grey('@' + uiharnessJson.version) }`));
      log.info(chalk.grey(' - module:   '), packageName, packageVersion);
      log.info(chalk.grey(' - port:     '), port);
      log.info(chalk.grey(' - react:    '), `v${ reactJson.version }`);
      if (isRelayEnabled) {
        log.info(chalk.grey(' - graphql:  '), displayPath(graphqlSchema));
      }
      // Specs.
      log.info(chalk.grey(' - specs:    '), displayPath(specs[0]) || chalk.magenta('None.'));
      R.takeLast(specs.length - 1, specs).forEach(path => {
        log.info(chalk.grey('             '), displayPath(path));
      });

      // Images.
      log.info(chalk.grey(' - images:   '), chalk.grey(`${ images.baseUrl } =>`), images.dir);

      // Proxy.
      if (YAML_CONFIG.proxy) {
        const formatProxy = (item) => `${ chalk.grey(item.from, '=>') } ${ item.to }`;
        const proxyItems = Object
          .keys(YAML_CONFIG.proxy)
          .map(key => ({ from: key, to: proxy[key] }));
        log.info(chalk.grey(' - proxy:    '), formatProxy(proxyItems[0]));
        R.takeLast(proxyItems.length - 1, proxyItems).forEach(item => {
          log.info(chalk.grey('             '), formatProxy(item));
        });
      }

      // Finish up.
      log.info('');
      resolve({});
    });
    return undefined;
  })();
});
<|endoftext|>


<commit_before>const path = require('path')
const fs = require('fs')
const terminate = require('terminate')
const { fork } = require('child_process')

const logger = require('./logger')

class Worker {
  constructor (userDataPath) {
    this.userDataPath = userDataPath
    this.loc = path.join(userDataPath, 'pid')
  }

  create ({socketName, filepath}, cb) {
    this.cleanup((err) => {
      if (err) logger.debug('Not fatal', err)
      logger.debug('Starting background process')
      this.process = fork(filepath, [
        '--subprocess',
        socketName,
        this.userDataPath
      ])
      return cb(null, this.process)
    })
  }

  _remove (cb) {
    return fs.unlink(this.loc, cb)
  }

  _exists () {
    return fs.existsSync(this.loc)
  }

  pid (cb) {
    var done = () => {
      logger.info('writing pid', this.process.pid)
      fs.writeFile(this.loc, this.process.pid.toString(), cb)
    }
    if (this._exists()) this.cleanup(done)
    else done()
  }

  read () {
    return parseInt(fs.readFileSync(this.loc).toString())
  }

  cleanup (cb) {
    if (this.process) this.process.kill()
    this.process = null
    if (!this._exists()) return cb(new Error('Nothing to clean up!'))
    var pid = this.read()
    logger.info('Terminating PID', pid)
    terminate(pid, (err) => {
      if (err) logger.error(err)
      if (err && err.code !== 'ESRCH') return cb(err)
      this._remove(cb)
    })
  }
}

module.exports = Worker
<commit_msg>fix: write correct pid file<commit_after>const path = require('path')
const fs = require('fs')
const terminate = require('terminate')
const { fork } = require('child_process')

const logger = require('./logger')

class Worker {
  constructor (userDataPath) {
    this.userDataPath = userDataPath
    this.loc = path.join(userDataPath, 'pid')
  }

  create ({socketName, filepath}, cb) {
    this.cleanup((err) => {
      if (err) logger.debug('Not fatal', err)
      logger.debug('Starting background process')
      this.process = fork(filepath, [
        '--subprocess',
        socketName,
        this.userDataPath
      ])
      return cb(null, this.process)
    })
  }

  _remove (cb) {
    return fs.unlink(this.loc, cb)
  }

  _exists () {
    return fs.existsSync(this.loc)
  }

  pid (cb) {
    // Write the current pid of this process to the pid file.
    var done = () => {
      logger.info('writing pid', process.pid)
      fs.writeFile(this.loc, process.pid.toString(), cb)
    }
    if (this._exists()) this.cleanup(done)
    else done()
  }

  read () {
    return parseInt(fs.readFileSync(this.loc).toString())
  }

  cleanup (cb) {
    if (this.process) this.process.kill()
    this.process = null
    if (!this._exists()) return cb(new Error('Nothing to clean up!'))
    var pid = this.read()
    logger.info('Terminating PID', pid)
    terminate(pid, (err) => {
      if (err) logger.error(err)
      if (err && err.code !== 'ESRCH') return cb(err)
      this._remove(cb)
    })
  }
}

module.exports = Worker
<|endoftext|>


<commit_before>var Shape =  require('./Shape')
,    vec2 =  require('../math/vec2')
,    Utils = require('../utils/Utils');

module.exports = Plane;

/**
 * Plane shape class. The plane is facing in the Y direction.
 * @class Plane
 * @extends Shape
 * @constructor
 * @param {object} [options] (Note that this options object will be passed on to the {{#crossLink "Shape"}}{{/crossLink}} constructor.)
 */
function Plane(options){
    options = options ? Utils.shallowClone(options) : {};
    options.type = Shape.PLANE;
    Shape.call(this, options);
}
Plane.prototype = new Shape();
Plane.prototype.constructor = Plane;

/**
 * Compute moment of inertia
 * @method computeMomentOfInertia
 */
Plane.prototype.computeMomentOfInertia = function(/*mass*/){
    return 0; // Plane is infinite. The inertia should therefore be infinty but by convention we set 0 here
};

/**
 * Update the bounding radius
 * @method updateBoundingRadius
 */
Plane.prototype.updateBoundingRadius = function(){
    this.boundingRadius = Number.MAX_VALUE;
};

/**
 * @method computeAABB
 * @param  {AABB}   out
 * @param  {Array}  position
 * @param  {Number} angle
 */
Plane.prototype.computeAABB = function(out, position, angle){
    var a = angle % (2 * Math.PI);
    var set = vec2.set;
    var max = 1e7;
    var lowerBound = out.lowerBound;
    var upperBound = out.upperBound;

    // Set max bounds
    set(lowerBound, -max, -max);
    set(upperBound,  max,  max);

    if(a === 0){
        // y goes from -inf to 0
        upperBound[1] = 0;
        // set(lowerBound, -max, -max);
        // set(upperBound,  max,  0);

    } else if(a === Math.PI / 2){

        // x goes from 0 to inf
        lowerBound[0] = 0;
        // set(lowerBound, 0, -max);
        // set(upperBound,      max,  max);

    } else if(a === Math.PI){

        // y goes from 0 to inf
        lowerBound[1] = 0;
        // set(lowerBound, -max, 0);
        // set(upperBound,  max, max);

    } else if(a === 3*Math.PI/2){

        // x goes from -inf to 0
        upperBound[0] = 0;
        // set(lowerBound, -max,     -max);
        // set(upperBound,  0,  max);

    }
};

Plane.prototype.updateArea = function(){
    this.area = Number.MAX_VALUE;
};

var intersectPlane_planePointToFrom = vec2.create();
var intersectPlane_normal = vec2.create();
var intersectPlane_len = vec2.create();

/**
 * @method raycast
 * @param  {RayResult} result
 * @param  {Ray} ray
 * @param  {array} position
 * @param  {number} angle
 */
Plane.prototype.raycast = function(result, ray, position, angle){
    var from = ray.from;
    var to = ray.to;
    var direction = ray.direction;
    var planePointToFrom = intersectPlane_planePointToFrom;
    var normal = intersectPlane_normal;
    var len = intersectPlane_len;

    // Get plane normal
    vec2.set(normal, 0, 1);
    vec2.rotate(normal, normal, angle);

    vec2.sub(len, from, position);
    var planeToFrom = vec2.dot(len, normal);
    vec2.sub(len, to, position);
    var planeToTo = vec2.dot(len, normal);

    if(planeToFrom * planeToTo > 0){
        // "from" and "to" are on the same side of the plane... bail out
        return;
    }

    if(vec2.squaredDistance(from, to) < planeToFrom * planeToFrom){
        return;
    }

    var n_dot_dir = vec2.dot(normal, direction);

    vec2.sub(planePointToFrom, from, position);
    var t = -vec2.dot(normal, planePointToFrom) / n_dot_dir / ray.length;

    ray.reportIntersection(result, t, normal, -1);
};<commit_msg>fixed bug in Plane AABB computation and cleaned up code<commit_after>var Shape =  require('./Shape')
,    vec2 =  require('../math/vec2')
,    Utils = require('../utils/Utils');

module.exports = Plane;

/**
 * Plane shape class. The plane is facing in the Y direction.
 * @class Plane
 * @extends Shape
 * @constructor
 * @param {object} [options] (Note that this options object will be passed on to the {{#crossLink "Shape"}}{{/crossLink}} constructor.)
 */
function Plane(options){
    options = options ? Utils.shallowClone(options) : {};
    options.type = Shape.PLANE;
    Shape.call(this, options);
}
Plane.prototype = new Shape();
Plane.prototype.constructor = Plane;

/**
 * Compute moment of inertia
 * @method computeMomentOfInertia
 */
Plane.prototype.computeMomentOfInertia = function(/*mass*/){
    return 0; // Plane is infinite. The inertia should therefore be infinty but by convention we set 0 here
};

/**
 * Update the bounding radius
 * @method updateBoundingRadius
 */
Plane.prototype.updateBoundingRadius = function(){
    this.boundingRadius = Number.MAX_VALUE;
};

/**
 * @method computeAABB
 * @param  {AABB}   out
 * @param  {Array}  position
 * @param  {Number} angle
 */
Plane.prototype.computeAABB = function(out, position, angle){
    var a = angle % (2 * Math.PI);
    var set = vec2.set;
    var max = 1e7;
    var lowerBound = out.lowerBound;
    var upperBound = out.upperBound;

    // Set max bounds
    set(lowerBound, -max, -max);
    set(upperBound,  max,  max);

    if(a === 0){
        // y goes from -inf to 0
        upperBound[1] = position[1];

    } else if(a === Math.PI / 2){

        // x goes from 0 to inf
        lowerBound[0] = position[0];

    } else if(a === Math.PI){

        // y goes from 0 to inf
        lowerBound[1] = position[1];

    } else if(a === 3*Math.PI/2){

        // x goes from -inf to 0
        upperBound[0] = position[0];

    }
};

Plane.prototype.updateArea = function(){
    this.area = Number.MAX_VALUE;
};

var intersectPlane_planePointToFrom = vec2.create();
var intersectPlane_normal = vec2.create();
var intersectPlane_len = vec2.create();

/**
 * @method raycast
 * @param  {RayResult} result
 * @param  {Ray} ray
 * @param  {array} position
 * @param  {number} angle
 */
Plane.prototype.raycast = function(result, ray, position, angle){
    var from = ray.from;
    var to = ray.to;
    var direction = ray.direction;
    var planePointToFrom = intersectPlane_planePointToFrom;
    var normal = intersectPlane_normal;
    var len = intersectPlane_len;

    // Get plane normal
    vec2.set(normal, 0, 1);
    vec2.rotate(normal, normal, angle);

    vec2.sub(len, from, position);
    var planeToFrom = vec2.dot(len, normal);
    vec2.sub(len, to, position);
    var planeToTo = vec2.dot(len, normal);

    if(planeToFrom * planeToTo > 0){
        // "from" and "to" are on the same side of the plane... bail out
        return;
    }

    if(vec2.squaredDistance(from, to) < planeToFrom * planeToFrom){
        return;
    }

    var n_dot_dir = vec2.dot(normal, direction);

    vec2.sub(planePointToFrom, from, position);
    var t = -vec2.dot(normal, planePointToFrom) / n_dot_dir / ray.length;

    ray.reportIntersection(result, t, normal, -1);
};<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016 Jan Hoffmann
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

var parser = require('./parser');

function readMenu(callback) {
    var request = new XMLHttpRequest();

    request.onload = function() {
        parseFeed(this.responseText, callback);
    };

    request.onerror = function() {
        console.log('Error while loading data.');
        callback(false, null);
    }

    request.open('GET', 'https://www.studierendenwerk-stuttgart.de/speiseangebot_rss');
    request.send();
}

function parseFeed(text, callback) {
    var data = [];

    var feed = parser.parseString(text);

    var items = feed.findElementsByName('item');
    for (var i = 0; i < items.length; i++) {
        try {
            var item = items[i];
            var title = items[i].findElementsByName('title')[0].content();
            var description = items[i].findElementsByName('description')[0].content();
            var date = parseDate(title);
            var menu = parseMenu(description);
            var message = parseMessage(menu);
            data.push({ 'date': date, 'menu': ((message.length == 0) ? menu : []), 'message': message });
        } catch (err) {
            console.log('Could not parse item: ' + err);
            callback(false, null);
            return;
        }
    }

    callback(true, data);
}

function parseDate(text) {
    var months = {
        'Januar': 0,
        'Februar': 1,
        'März': 2,
        'April': 3,
        'Mai': 4,
        'Juni': 5,
        'Juli': 6,
        'August': 7,
        'September': 8,
        'Oktober': 9,
        'November': 10,
        'Dezember': 11,
    };

    var result = /([0-9]{1,2})\.\s+([^\s]+)\s+([0-9]{4})/.exec(text);
    if (result == null) {
        throw 'Date not found.';
    }

    var day = parseInt(result[1]);
    var month = (result[2] in months) ? months[result[2]] : 0;
    var year = parseInt(result[3]);

    // set hour to -1 to compensate for timezone (standard time only, one hour off for daylight saving)
    var date = Date.UTC(year, month, day, -1) / 1000;
    return date;
}

function parseMenu(text) {
    var menu = [];
    var category = null;

    var table = parser.parseString(text);
    try {
        var tbody = table.findElementsByName('tbody')[0];
        var rows = tbody.findElementsByName('tr');

        for (var i = 0; i < rows.length; i++) {
            var columns = rows[i].findElementsByName('td');

            if (columns.length == 1) {
                // category
                var title = columns[0].content();
                var classes = rows[i].attributes()["class"];
                if (classes !== undefined && classes.indexOf("field_cntn_mn_ml_plt") != -1) {
					title = "Preisrenner";
				}
                category = { 'title': title, 'meals': [] };
                menu.push(category);
            } else if (columns.length == 4) {
                // meal
                var title = columns[0].content();
                var priceStudent = columns[1].content();
                var priceGuest = columns[2].content();
                category.meals.push({ 'title': title, 'priceStudent': priceStudent, 'priceGuest': priceGuest });
            } else {
                throw 'Unexpected table format: row with ' + columns.length + ' columns';
            }
        }
    } catch (err) {
        throw 'Error parsing meal table: ' + err;
    }

    return menu;
}

function parseMessage(menu) {
    var message = null;

    for (var i = 0; i < menu.length; i++) {
        var category = menu[i];
        var meals = category.meals;
        for (var j = 0; j < meals.length; j++) {
            var meal = meals[j];
            if (message == null) {
                message = meal.title;
            } else if (message != meal.title) {
                return '';
            }
        }
    }

    return message;
}

module.exports.readMenu = readMenu;
<commit_msg>Fix indentation<commit_after>/*
 * Copyright (c) 2016 Jan Hoffmann
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

var parser = require('./parser');

function readMenu(callback) {
    var request = new XMLHttpRequest();

    request.onload = function() {
        parseFeed(this.responseText, callback);
    };

    request.onerror = function() {
        console.log('Error while loading data.');
        callback(false, null);
    }

    request.open('GET', 'https://www.studierendenwerk-stuttgart.de/speiseangebot_rss');
    request.send();
}

function parseFeed(text, callback) {
    var data = [];

    var feed = parser.parseString(text);

    var items = feed.findElementsByName('item');
    for (var i = 0; i < items.length; i++) {
        try {
            var item = items[i];
            var title = items[i].findElementsByName('title')[0].content();
            var description = items[i].findElementsByName('description')[0].content();
            var date = parseDate(title);
            var menu = parseMenu(description);
            var message = parseMessage(menu);
            data.push({ 'date': date, 'menu': ((message.length == 0) ? menu : []), 'message': message });
        } catch (err) {
            console.log('Could not parse item: ' + err);
            callback(false, null);
            return;
        }
    }

    callback(true, data);
}

function parseDate(text) {
    var months = {
        'Januar': 0,
        'Februar': 1,
        'März': 2,
        'April': 3,
        'Mai': 4,
        'Juni': 5,
        'Juli': 6,
        'August': 7,
        'September': 8,
        'Oktober': 9,
        'November': 10,
        'Dezember': 11,
    };

    var result = /([0-9]{1,2})\.\s+([^\s]+)\s+([0-9]{4})/.exec(text);
    if (result == null) {
        throw 'Date not found.';
    }

    var day = parseInt(result[1]);
    var month = (result[2] in months) ? months[result[2]] : 0;
    var year = parseInt(result[3]);

    // set hour to -1 to compensate for timezone (standard time only, one hour off for daylight saving)
    var date = Date.UTC(year, month, day, -1) / 1000;
    return date;
}

function parseMenu(text) {
    var menu = [];
    var category = null;

    var table = parser.parseString(text);
    try {
        var tbody = table.findElementsByName('tbody')[0];
        var rows = tbody.findElementsByName('tr');

        for (var i = 0; i < rows.length; i++) {
            var columns = rows[i].findElementsByName('td');

            if (columns.length == 1) {
                // category
                var title = columns[0].content();
                var classes = rows[i].attributes()["class"];
                if (classes !== undefined && classes.indexOf("field_cntn_mn_ml_plt") != -1) {
                    title = "Preisrenner";
                }
                category = { 'title': title, 'meals': [] };
                menu.push(category);
            } else if (columns.length == 4) {
                // meal
                var title = columns[0].content();
                var priceStudent = columns[1].content();
                var priceGuest = columns[2].content();
                category.meals.push({ 'title': title, 'priceStudent': priceStudent, 'priceGuest': priceGuest });
            } else {
                throw 'Unexpected table format: row with ' + columns.length + ' columns';
            }
        }
    } catch (err) {
        throw 'Error parsing meal table: ' + err;
    }

    return menu;
}

function parseMessage(menu) {
    var message = null;

    for (var i = 0; i < menu.length; i++) {
        var category = menu[i];
        var meals = category.meals;
        for (var j = 0; j < meals.length; j++) {
            var meal = meals[j];
            if (message == null) {
                message = meal.title;
            } else if (message != meal.title) {
                return '';
            }
        }
    }

    return message;
}

module.exports.readMenu = readMenu;
<|endoftext|>


<commit_before>/*globals fdom:true */
/*jslint indent:2,white:true,node:true,sloppy:true */
if (typeof fdom === 'undefined') {
  fdom = {};
}
fdom.port = fdom.port || {};

/**
 * The external Port face of a module on a hub.
 * @class Module
 * @extends Port
 * @param {String} manifestURL The manifest this module loads.
 * @param {String[]} creator The lineage of creation for this module.
 * @constructor
 */
fdom.port.Module = function(manifestURL, manifest, creator) {
  this.config = {};
  this.id = manifestURL + Math.random();
  this.manifestId = manifestURL;
  this.manifest = manifest;
  this.lineage = [this.manifestId].concat(creator);
  this.externalPortMap = {};
  this.internalPortMap = {};
  this.started = false;

  fdom.util.handleEvents(this);
};

/**
 * Receive a message for the Module.
 * @method onMessage
 * @param {String} flow The origin of the message.
 * @param {Object} message The message received.
 */
fdom.port.Module.prototype.onMessage = function(flow, message) {
  if (flow === 'control') {
    if (message.type === 'setup') {
      this.controlChannel = message.channel;
      fdom.util.mixin(this.config, message.config);
      this.emit(this.controlChannel, {
        type: 'Core Provider',
        request: 'core'
      });
      this.start();
      return;
    } else if (message.type === 'createLink' && message.channel) {
      this.externalPortMap[message.name] = message.channel;
      if (this.internalPortMap[message.name] === undefined) {
        this.internalPortMap[message.name] = false;
      }
      this.emit(message.channel, {
        type: 'default channel announcement',
        channel: message.reverse
      });
      return;
    } else if (message.core) {
      this.core = new message.core();
      this.emit('core', message.core);
      return;
    } else if (message.type === 'close') {
      // Closing channel.
      if (message.channel === 'control') {
        this.stop();
      }
      this.deregisterFlow(message.channel, false);
    } else {
      this.port.onMessage(flow, message);
    }
  } else {
    if ((this.externalPortMap[flow] === false ||
        !this.externalPortMap[flow])&& message.channel) {
      //console.log('handling channel announcement for ' + flow);
      this.externalPortMap[flow] = message.channel;
      if (this.internalPortMap[flow] === undefined) {
        this.internalPortMap[flow] = false;

        // New incoming connection attempts should get routed to modInternal.
        if (this.manifest.provides && this.modInternal) {
          this.port.onMessage(this.modInternal, {
            type: 'Connection',
            channel: flow
          });
        } else if (this.manifest.provides) {
          this.once('modInternal', function(flow) {
            this.port.onMessage(this.modInternal,{
              type: 'Connection',
              channel: flow
            });
          }.bind(this, flow));
        }
      }
      return;
    } else if (!this.started) {
      this.once('start', this.onMessage.bind(this, flow, message));
    } else {
      if (this.internalPortMap[flow] === false) {
        this.once('internalChannelReady', this.onMessage.bind(this, flow, message));
      } else if (!this.internalPortMap[flow]) {
        fdom.debug.error('Unexpected message from ' + flow);
        return;
      } else {
        this.port.onMessage(this.internalPortMap[flow], message);
      }
    }
  }
};

/**
 * Clean up after a flow which is no longer used / needed.
 * @method deregisterFLow
 * @param {String} flow The flow to remove mappings for.
 * @param {Boolean} internal If the flow name is the internal identifier.
 * @returns {Boolean} Whether the flow was successfully deregistered.
 * @private
 */
fdom.port.Module.prototype.deregisterFlow = function(flow, internal) {
  var key,
      map = internal ? this.internalPortMap : this.externalPortMap;
  // TODO: this is inefficient, but seems less confusing than a 3rd
  // reverse lookup map.
  for (key in map) {
    if (map[key] === flow) {
      if (internal) {
        this.emit(this.controlChannel, {
          type: 'Channel Teardown',
          request: 'unlink',
          to: this.externalPortMap[key]
        });
      } else {
        this.port.onMessage('control', {
          type: 'close',
          channel: this.internalPortMap[key]
        });
      }
      delete this.externalPortMap[key];
      delete this.internalPortMap[key];
      return true;
    }
  }
  return false;
};

/**
 * Attempt to start the module once the remote freedom context
 * exists.
 * @method start
 * @private
 */
fdom.port.Module.prototype.start = function() {
  if (this.started || this.port) {
    return false;
  }
  if (this.controlChannel) {
    this.loadLinks();
    this.port = new fdom.link[this.config.portType](this.manifestId);
    // Listen to all port messages.
    this.port.on(this.emitMessage.bind(this));
    // Tell the local port to ask us for help.
    this.port.onMessage('control', {
      channel: 'control',
      config: this.config
    });

    // Tell the remote location to delegate debugging.
    this.port.onMessage('control', {
      type: 'Redirect',
      request: 'delegate',
      flow: 'debug'
    });
    this.port.onMessage('control', {
      type: 'Redirect',
      request: 'delegate',
      flow: 'core'
    });

    // Tell the container to instantiate the counterpart to this external view.
    this.port.onMessage('control', {
      type: 'Environment Configuration',
      request: 'port',
      name: 'ModInternal',
      service: 'ModuleInternal',
      exposeManager: true
    });
  }
};

/**
 * Stop the module when it is no longer needed, and tear-down state.
 * @method stop
 * @private
 */
fdom.port.Module.prototype.stop = function() {
  if (!this.started) {
    return;
  }
  if (this.port) {
    this.port.off();
    this.port.onMessage('control', {
      type: 'close',
      channel: 'control'
    });
    delete this.port;
  }
  this.started = false;
};

/**
 * Textual Description of the Port
 * @method toString
 * @return {String} The description of this Port.
 */
fdom.port.Module.prototype.toString = function() {
  var manifest = this.manifestId;
  if (manifest.indexOf('/') > -1) {
    manifest = manifest.substr(manifest.lastIndexOf('/') + 1);
  }
  return "[Module " + manifest + "]";
};

/**
 * Intercept messages as they arrive from the module,
 * mapping them between internal and external flow names.
 * @method emitMessage
 * @param {String} name The destination the module wants to send to.
 * @param {Object} message The message to send.
 * @private
 */
fdom.port.Module.prototype.emitMessage = function(name, message) {
  if (this.internalPortMap[name] === false && message.channel) {
    this.internalPortMap[name] = message.channel;
    this.emit('internalChannelReady');
    return;
  }
  // Terminate debug redirection requested in start().
  if (name === 'control') {
    if (message.flow === 'debug' && message.message) {
      fdom.debug.format(message.message.severity,
          this.toString(),
          message.message.msg);
    } else if (message.flow === 'core' && message.message) {
      if (!this.core) {
        this.once('core', this.emitMessage.bind(this, name, message));
        return;
      }
      if (message.message.type === 'register') {
        message.message.reply = this.port.onMessage.bind(this.port, 'control');
        this.externalPortMap[message.message.id] = false;
      }
      this.core.onMessage(this, message.message);
    } else if (message.name === 'ModInternal' && !this.modInternal) {
      this.modInternal = message.channel;
      this.port.onMessage(this.modInternal, {
        type: 'Initialization',
        id: this.manifestId,
        appId: this.id,
        manifest: this.manifest,
        lineage: this.lineage,
        channel: message.reverse
      });
      this.emit('modInternal');
    } else if (message.type === 'createLink') {
      // A design decision was that the default channel is
      // overridden when acting as a provider.
      if (this.manifest.provides &&
          this.manifest.provides.indexOf(message.name) === 0) {
        this.internalPortMap['default'] = message.channel;
      }

      this.internalPortMap[message.name] = message.channel;
      this.port.onMessage(message.channel, {
        type: 'channel announcement',
        channel: message.reverse
      });
      this.emit('internalChannelReady');
    } else if (message.type === 'close') {
      this.deregisterFlow(message.channel, true);
    }
  } else if (name === 'ModInternal' && message.type === 'ready' && !this.started) {
    this.started = true;
    this.emit('start');
  } else if (name === 'ModInternal' && message.type === 'resolve') {
    fdom.resources.get(this.manifestId, message.data).then(function(id, data) {
      this.port.onMessage(this.modInternal, {
        type: 'resolve response',
        id: id,
        data: data
      });
    }.bind(this, message.id), function() {
      fdom.debug.warn('Error Resolving URL for Module.');
    });
  } else {
    this.emit(this.externalPortMap[name], message);
  }
  return false;
};

/**
 * Request the external routes used by this module.
 * @method loadLinks
 * @private
 */
fdom.port.Module.prototype.loadLinks = function() {
  var i, channels = ['default'], name, dep,
      finishLink = function(dep, provider) {
        dep.getInterface().provideAsynchronous(provider);
      };
  if (this.manifest.permissions) {
    for (i = 0; i < this.manifest.permissions.length; i += 1) {
      name = this.manifest.permissions[i];
      if (channels.indexOf(name) < 0 && name.indexOf('core.') === 0) {
        channels.push(name);
        dep = new fdom.port.Provider(fdom.apis.get(name).definition);
        fdom.apis.getCore(name, this).then(finishLink.bind(this, dep));

        this.emit(this.controlChannel, {
          type: 'Core Link to ' + name,
          request: 'link',
          name: name,
          to: dep
        });
      }
    }
  }
  if (this.manifest.dependencies) {
    fdom.util.eachProp(this.manifest.dependencies, function(desc, name) {
      if (channels.indexOf(name) < 0) {
        channels.push(name);
      }
      fdom.resources.get(this.manifestId, desc.url).then(function (url) {
        this.config.policy.get(this.lineage, url).then(function(dep) {
          this.emit(this.controlChannel, {
            type: 'Link to ' + name,
            request: 'link',
            name: name,
            overrideDest: name + '.' + this.id,
            to: dep
          });
        }.bind(this));
        fdom.resources.getContents(url).then(this.updateEnv.bind(this, name));
      }.bind(this));
    }.bind(this));
  }
  // Note that messages can be synchronous, so some ports may already be bound.
  for (i = 0; i < channels.length; i += 1) {
    this.externalPortMap[channels[i]] = this.externalPortMap[channels[i]] || false;
    this.internalPortMap[channels[i]] = false;
  }
};

/**
 * Update the module environment with information about a dependent manifest.
 * @method updateEnv
 * @param {String} dep The dependency
 * @param {Object} manifest The manifest of the dependency
 */
fdom.port.Module.prototype.updateEnv = function(dep, manifest) {
  if (!manifest) {
    return;
  }
  if (!this.modInternal) {
    this.once('modInternal', this.updateEnv.bind(this, dep, manifest));
  }
  // Decide if/what other properties should be exported.
  // Keep in sync with ModuleInternal.updateEnv
  var metadata = {
    name: manifest.name,
    icon: manifest.icon,
    description: manifest.description
  };

  this.port.onMessage(this.modInternal, {
    type: 'manifest',
    name: dep,
    manifest: metadata
  });
};

fdom.port.Module.prototype.serialize = function() {
  return JSON.serialize({
    manifestId: this.manifestId,
    externalPortMap: this.externalPortMap,
    internalPortMap: this.internalPortMap,
    manifest: this.manifest,
    controlChannel: this.controlChannel
  });
};
<commit_msg>naming of modules in console output. Close #34<commit_after>/*globals fdom:true */
/*jslint indent:2,white:true,node:true,sloppy:true */
if (typeof fdom === 'undefined') {
  fdom = {};
}
fdom.port = fdom.port || {};

/**
 * The external Port face of a module on a hub.
 * @class Module
 * @extends Port
 * @param {String} manifestURL The manifest this module loads.
 * @param {String[]} creator The lineage of creation for this module.
 * @constructor
 */
fdom.port.Module = function(manifestURL, manifest, creator) {
  this.config = {};
  this.id = manifestURL + Math.random();
  this.manifestId = manifestURL;
  this.manifest = manifest;
  this.lineage = [this.manifestId].concat(creator);
  this.externalPortMap = {};
  this.internalPortMap = {};
  this.started = false;

  fdom.util.handleEvents(this);
};

/**
 * Receive a message for the Module.
 * @method onMessage
 * @param {String} flow The origin of the message.
 * @param {Object} message The message received.
 */
fdom.port.Module.prototype.onMessage = function(flow, message) {
  if (flow === 'control') {
    if (message.type === 'setup') {
      this.controlChannel = message.channel;
      fdom.util.mixin(this.config, message.config);
      this.emit(this.controlChannel, {
        type: 'Core Provider',
        request: 'core'
      });
      this.start();
      return;
    } else if (message.type === 'createLink' && message.channel) {
      this.externalPortMap[message.name] = message.channel;
      if (this.internalPortMap[message.name] === undefined) {
        this.internalPortMap[message.name] = false;
      }
      this.emit(message.channel, {
        type: 'default channel announcement',
        channel: message.reverse
      });
      return;
    } else if (message.core) {
      this.core = new message.core();
      this.emit('core', message.core);
      return;
    } else if (message.type === 'close') {
      // Closing channel.
      if (message.channel === 'control') {
        this.stop();
      }
      this.deregisterFlow(message.channel, false);
    } else {
      this.port.onMessage(flow, message);
    }
  } else {
    if ((this.externalPortMap[flow] === false ||
        !this.externalPortMap[flow])&& message.channel) {
      //console.log('handling channel announcement for ' + flow);
      this.externalPortMap[flow] = message.channel;
      if (this.internalPortMap[flow] === undefined) {
        this.internalPortMap[flow] = false;

        // New incoming connection attempts should get routed to modInternal.
        if (this.manifest.provides && this.modInternal) {
          this.port.onMessage(this.modInternal, {
            type: 'Connection',
            channel: flow
          });
        } else if (this.manifest.provides) {
          this.once('modInternal', function(flow) {
            this.port.onMessage(this.modInternal,{
              type: 'Connection',
              channel: flow
            });
          }.bind(this, flow));
        }
      }
      return;
    } else if (!this.started) {
      this.once('start', this.onMessage.bind(this, flow, message));
    } else {
      if (this.internalPortMap[flow] === false) {
        this.once('internalChannelReady', this.onMessage.bind(this, flow, message));
      } else if (!this.internalPortMap[flow]) {
        fdom.debug.error('Unexpected message from ' + flow);
        return;
      } else {
        this.port.onMessage(this.internalPortMap[flow], message);
      }
    }
  }
};

/**
 * Clean up after a flow which is no longer used / needed.
 * @method deregisterFLow
 * @param {String} flow The flow to remove mappings for.
 * @param {Boolean} internal If the flow name is the internal identifier.
 * @returns {Boolean} Whether the flow was successfully deregistered.
 * @private
 */
fdom.port.Module.prototype.deregisterFlow = function(flow, internal) {
  var key,
      map = internal ? this.internalPortMap : this.externalPortMap;
  // TODO: this is inefficient, but seems less confusing than a 3rd
  // reverse lookup map.
  for (key in map) {
    if (map[key] === flow) {
      if (internal) {
        this.emit(this.controlChannel, {
          type: 'Channel Teardown',
          request: 'unlink',
          to: this.externalPortMap[key]
        });
      } else {
        this.port.onMessage('control', {
          type: 'close',
          channel: this.internalPortMap[key]
        });
      }
      delete this.externalPortMap[key];
      delete this.internalPortMap[key];
      return true;
    }
  }
  return false;
};

/**
 * Attempt to start the module once the remote freedom context
 * exists.
 * @method start
 * @private
 */
fdom.port.Module.prototype.start = function() {
  if (this.started || this.port) {
    return false;
  }
  if (this.controlChannel) {
    this.loadLinks();
    this.port = new fdom.link[this.config.portType](this.manifestId);
    // Listen to all port messages.
    this.port.on(this.emitMessage.bind(this));
    // Tell the local port to ask us for help.
    this.port.onMessage('control', {
      channel: 'control',
      config: this.config
    });

    // Tell the remote location to delegate debugging.
    this.port.onMessage('control', {
      type: 'Redirect',
      request: 'delegate',
      flow: 'debug'
    });
    this.port.onMessage('control', {
      type: 'Redirect',
      request: 'delegate',
      flow: 'core'
    });

    // Tell the container to instantiate the counterpart to this external view.
    this.port.onMessage('control', {
      type: 'Environment Configuration',
      request: 'port',
      name: 'ModInternal',
      service: 'ModuleInternal',
      exposeManager: true
    });
  }
};

/**
 * Stop the module when it is no longer needed, and tear-down state.
 * @method stop
 * @private
 */
fdom.port.Module.prototype.stop = function() {
  if (!this.started) {
    return;
  }
  if (this.port) {
    this.port.off();
    this.port.onMessage('control', {
      type: 'close',
      channel: 'control'
    });
    delete this.port;
  }
  this.started = false;
};

/**
 * Textual Description of the Port
 * @method toString
 * @return {String} The description of this Port.
 */
fdom.port.Module.prototype.toString = function() {
  return "[Module " + this.manifest.name +"]";
};

/**
 * Intercept messages as they arrive from the module,
 * mapping them between internal and external flow names.
 * @method emitMessage
 * @param {String} name The destination the module wants to send to.
 * @param {Object} message The message to send.
 * @private
 */
fdom.port.Module.prototype.emitMessage = function(name, message) {
  if (this.internalPortMap[name] === false && message.channel) {
    this.internalPortMap[name] = message.channel;
    this.emit('internalChannelReady');
    return;
  }
  // Terminate debug redirection requested in start().
  if (name === 'control') {
    if (message.flow === 'debug' && message.message) {
      fdom.debug.format(message.message.severity,
          this.toString(),
          message.message.msg);
    } else if (message.flow === 'core' && message.message) {
      if (!this.core) {
        this.once('core', this.emitMessage.bind(this, name, message));
        return;
      }
      if (message.message.type === 'register') {
        message.message.reply = this.port.onMessage.bind(this.port, 'control');
        this.externalPortMap[message.message.id] = false;
      }
      this.core.onMessage(this, message.message);
    } else if (message.name === 'ModInternal' && !this.modInternal) {
      this.modInternal = message.channel;
      this.port.onMessage(this.modInternal, {
        type: 'Initialization',
        id: this.manifestId,
        appId: this.id,
        manifest: this.manifest,
        lineage: this.lineage,
        channel: message.reverse
      });
      this.emit('modInternal');
    } else if (message.type === 'createLink') {
      // A design decision was that the default channel is
      // overridden when acting as a provider.
      if (this.manifest.provides &&
          this.manifest.provides.indexOf(message.name) === 0) {
        this.internalPortMap['default'] = message.channel;
      }

      this.internalPortMap[message.name] = message.channel;
      this.port.onMessage(message.channel, {
        type: 'channel announcement',
        channel: message.reverse
      });
      this.emit('internalChannelReady');
    } else if (message.type === 'close') {
      this.deregisterFlow(message.channel, true);
    }
  } else if (name === 'ModInternal' && message.type === 'ready' && !this.started) {
    this.started = true;
    this.emit('start');
  } else if (name === 'ModInternal' && message.type === 'resolve') {
    fdom.resources.get(this.manifestId, message.data).then(function(id, data) {
      this.port.onMessage(this.modInternal, {
        type: 'resolve response',
        id: id,
        data: data
      });
    }.bind(this, message.id), function() {
      fdom.debug.warn('Error Resolving URL for Module.');
    });
  } else {
    this.emit(this.externalPortMap[name], message);
  }
  return false;
};

/**
 * Request the external routes used by this module.
 * @method loadLinks
 * @private
 */
fdom.port.Module.prototype.loadLinks = function() {
  var i, channels = ['default'], name, dep,
      finishLink = function(dep, provider) {
        dep.getInterface().provideAsynchronous(provider);
      };
  if (this.manifest.permissions) {
    for (i = 0; i < this.manifest.permissions.length; i += 1) {
      name = this.manifest.permissions[i];
      if (channels.indexOf(name) < 0 && name.indexOf('core.') === 0) {
        channels.push(name);
        dep = new fdom.port.Provider(fdom.apis.get(name).definition);
        fdom.apis.getCore(name, this).then(finishLink.bind(this, dep));

        this.emit(this.controlChannel, {
          type: 'Core Link to ' + name,
          request: 'link',
          name: name,
          to: dep
        });
      }
    }
  }
  if (this.manifest.dependencies) {
    fdom.util.eachProp(this.manifest.dependencies, function(desc, name) {
      if (channels.indexOf(name) < 0) {
        channels.push(name);
      }
      fdom.resources.get(this.manifestId, desc.url).then(function (url) {
        this.config.policy.get(this.lineage, url).then(function(dep) {
          this.emit(this.controlChannel, {
            type: 'Link to ' + name,
            request: 'link',
            name: name,
            overrideDest: name + '.' + this.id,
            to: dep
          });
        }.bind(this));
        fdom.resources.getContents(url).then(this.updateEnv.bind(this, name));
      }.bind(this));
    }.bind(this));
  }
  // Note that messages can be synchronous, so some ports may already be bound.
  for (i = 0; i < channels.length; i += 1) {
    this.externalPortMap[channels[i]] = this.externalPortMap[channels[i]] || false;
    this.internalPortMap[channels[i]] = false;
  }
};

/**
 * Update the module environment with information about a dependent manifest.
 * @method updateEnv
 * @param {String} dep The dependency
 * @param {Object} manifest The manifest of the dependency
 */
fdom.port.Module.prototype.updateEnv = function(dep, manifest) {
  if (!manifest) {
    return;
  }
  if (!this.modInternal) {
    this.once('modInternal', this.updateEnv.bind(this, dep, manifest));
  }
  // Decide if/what other properties should be exported.
  // Keep in sync with ModuleInternal.updateEnv
  var metadata = {
    name: manifest.name,
    icon: manifest.icon,
    description: manifest.description
  };

  this.port.onMessage(this.modInternal, {
    type: 'manifest',
    name: dep,
    manifest: metadata
  });
};

fdom.port.Module.prototype.serialize = function() {
  return JSON.serialize({
    manifestId: this.manifestId,
    externalPortMap: this.externalPortMap,
    internalPortMap: this.internalPortMap,
    manifest: this.manifest,
    controlChannel: this.controlChannel
  });
};
<|endoftext|>


<commit_before>/* Write pseudocode and plan for each challenge.
For example, move right x2, move up x1 and ATTACK! */

/* Challenge 1 Pseudocode
move down x 2
move rigth x1
move up x2
move right x2
move down x 1
then ATTACK!

Path that I chose:
*/

this.moveDown();
this.moveDown();
this.moveRight();
this.moveUp();
this.moveUp();
this.moveRight();
this.moveRight();
this.moveDown();
this.attackNearbyEnemy();

//Challenge 2 Pseudocode: Grab the Mushroom
//move up x 1
//move right x1
//move left x 1
//move up x 1
//ATTACK!!!!!

this.moveUp();
this.moveRight();
this.moveLeft();
this.moveUp();
this.attackNearbyEnemy();

//Challenge 3 Pseudocode: Drink the potion
//ATTACK x 1
//move right x 1
//move down x 1
//move up x 1
//move right x 1
//ATTACK

this.attackNearbyEnemy();
this.moveRight();
this.moveDown();
this.moveUp();
this.moveRight();
this.attackNearbyEnemy();

//Challenge #4: Taunt the Guards
//move Right x 1
//bust down door x 1
//move right x 1
//say something x 1
//move left x 2
//say something x 1
//move rigth x 1
//say something x 1
//move right x 2
//move up x 1
//move right x 1
//say something x 1
//say attack x 2
//say follow me x 1
//move right x 2

this.moveRight();
this.bustDownDoor();

// Delete the "//" in front of each line below.
this.moveRight();
this.say("Hey there!");
this.moveLeft();
this.moveLeft();
this.say("Attack!");

// Now get Phoebe to follow through the dungeon.
this.moveRight();
this.say("Follow me.");
this.moveRight();
this.moveRight();
this.moveUp();
this.moveRight();
this.say("leeroy Jenkinss!");
this.say("Attack!");
this.say("Attack!");
this.say("Follow me");
this.moveRight();
this.moveRight();

//Challenge #5: It's a Trap
//move down x 2
//say something
//move up x 2
//say something x 2

// Taunt the Ogre and run for cover!
this.moveDown();
this.moveDown();
this.say("Hey Punk Ass");
this.moveUp();
this.moveUp();
this.say("Attack!");

//Challenge #6: Taunt
//Say something x 3

this.say("Hey big head..");
this.say("Yeah, you heard me");
this.say("I hear your mother makes a nice lobster bisque");

//Challenge #7: Cowardly Taunt
//move coordinates x 3
//say something x 1
//move coordinates x 1
//taunt x 1
//move coordinates x 1 (retreat)

this.moveXY(50, 16);
this.moveXY(63, 20);
this.moveXY(70, 10);  // This is a safe spot.

this.say("I can lure them in here.");

// Run your soldier out where the ogres can hear you.

// Say something!

// Then run back behind the arrow towers for safety.

// You can shift+click on the map to insert coordinatethis
this.moveXY(55, 30);
this.say("Come on dummy!");
this.moveXY(70, 10);

//Challenge #8: Commanding army
//move coordinates x 3
//greeting x 1
//say follow me x 1
//move group x 1
//say attack x 1
//NOBODY MESSES WITH THARIN

this.moveXY(49, 66);
this.moveXY(60, 63);
this.moveXY(75, 63);
this.say("Hail, friends!");

// Saying anything with "follow" in it will get nearby soldiers
// to follow you

// Saying anything with "attack" will send nearby troops into
// a battle mode.

// Make sure Tharin is safe!
this.say("follow me");
this.moveXY(65, 44);
this.say("Attack!");

//Challenge #9 Break the Prison (This one was out of order)
//move right and check each door

// Write this isFriend(name) spell to tell friend from foe.
// Return true for friends' names, false for ogres' names.
if(name === "William" || name === "Lucas")
    return true;
if(name === "Krogg" || name === "Brack" || name === "MARCUS")
    return false;          // <-- Start here; Krogg is not a friend!

return true;

//Challenge # 10 : Mobile Artillery
//Move coordinates x 1
//attack coordinates x 1
//move coordinates x 1
//attack coordinates x 3
//move coordinates x 1
//attack coordinates x 1

this.moveXY(30, 26);  // Move into range
this.attackXY(46, 5);  // Shoot once in the middle of the ogres

// If they survive a hit, ogres retaliate!
// Larger ogres take more than one hit...
// So, position and aim carefully.
this.moveXY(49, 41);
this.attackXY(49, 67);
this.attackXY(49, 51);//second shot to kill big guy
this.attackXY(70, 57);
this.moveXY(39, 4);
this.attackXY(44, 18);//second shot to kill 2nd big guy
<commit_msg>Code Combat final<commit_after>/* Write pseudocode and plan for each challenge.
For example, move right x2, move up x1 and ATTACK! */

/* Challenge 1 Pseudocode
move down x 2
move rigth x1
move up x2
move right x2
move down x 1
then ATTACK!

Path that I chose:
*/

this.moveDown();
this.moveDown();
this.moveRight();
this.moveUp();
this.moveUp();
this.moveRight();
this.moveRight();
this.moveDown();
this.attackNearbyEnemy();

//Challenge 2 Pseudocode: Grab the Mushroom
//move up x 1
//move right x1
//move left x 1
//move up x 1
//ATTACK!!!!!

this.moveUp();
this.moveRight();
this.moveLeft();
this.moveUp();
this.attackNearbyEnemy();

//Challenge 3 Pseudocode: Drink the potion
//ATTACK x 1
//move right x 1
//move down x 1
//move up x 1
//move right x 1
//ATTACK

this.attackNearbyEnemy();
this.moveRight();
this.moveDown();
this.moveUp();
this.moveRight();
this.attackNearbyEnemy();

//Challenge #4: Taunt the Guards
//move Right x 1
//bust down door x 1
//move right x 1
//say something x 1
//move left x 2
//say something x 1
//move rigth x 1
//say something x 1
//move right x 2
//move up x 1
//move right x 1
//say something x 1
//say attack x 2
//say follow me x 1
//move right x 2

this.moveRight();
this.bustDownDoor();

// Delete the "//" in front of each line below.
this.moveRight();
this.say("Hey there!");
this.moveLeft();
this.moveLeft();
this.say("Attack!");

// Now get Phoebe to follow through the dungeon.
this.moveRight();
this.say("Follow me.");
this.moveRight();
this.moveRight();
this.moveUp();
this.moveRight();
this.say("leeroy Jenkinss!");
this.say("Attack!");
this.say("Attack!");
this.say("Follow me");
this.moveRight();
this.moveRight();

//Challenge #5: It's a Trap
//move down x 2
//say something
//move up x 2
//say something x 2

// Taunt the Ogre and run for cover!
this.moveDown();
this.moveDown();
this.say("Hey Punk Ass");
this.moveUp();
this.moveUp();
this.say("Attack!");

//Challenge #6: Taunt
//Say something x 3

this.say("Hey big head..");
this.say("Yeah, you heard me");
this.say("I hear your mother makes a nice lobster bisque");

//Challenge #7: Cowardly Taunt
//move coordinates x 3
//say something x 1
//move coordinates x 1
//taunt x 1
//move coordinates x 1 (retreat)

this.moveXY(50, 16);
this.moveXY(63, 20);
this.moveXY(70, 10);  // This is a safe spot.

this.say("I can lure them in here.");

// Run your soldier out where the ogres can hear you.

// Say something!

// Then run back behind the arrow towers for safety.

// You can shift+click on the map to insert coordinatethis
this.moveXY(55, 30);
this.say("Come on dummy!");
this.moveXY(70, 10);

//Challenge #8: Commanding army
//move coordinates x 3
//greeting x 1
//say follow me x 1
//move group x 1
//say attack x 1
//NOBODY MESSES WITH THARIN

this.moveXY(49, 66);
this.moveXY(60, 63);
this.moveXY(75, 63);
this.say("Hail, friends!");

// Saying anything with "follow" in it will get nearby soldiers
// to follow you

// Saying anything with "attack" will send nearby troops into
// a battle mode.

// Make sure Tharin is safe!
this.say("follow me");
this.moveXY(65, 44);
this.say("Attack!");

//Challenge #9 Break the Prison (This one was out of order)
//move right and check each door

// Write this isFriend(name) spell to tell friend from foe.
// Return true for friends' names, false for ogres' names.
if(name === "William" || name === "Lucas")
    return true;
if(name === "Krogg" || name === "Brack" || name === "MARCUS")
    return false;          // <-- Start here; Krogg is not a friend!

return true;

//Challenge # 10 : Mobile Artillery
//Move coordinates x 1
//attack coordinates x 1
//move coordinates x 1
//attack coordinates x 3
//move coordinates x 1
//attack coordinates x 1

this.moveXY(30, 26);  // Move into range
this.attackXY(46, 5);  // Shoot once in the middle of the ogres

// If they survive a hit, ogres retaliate!
// Larger ogres take more than one hit...
// So, position and aim carefully.
this.moveXY(49, 41);
this.attackXY(49, 67);
this.attackXY(49, 51);//second shot to kill big guy
this.attackXY(70, 57);
this.moveXY(39, 4);
this.attackXY(44, 18);//second shot to kill 2nd big guy.
<|endoftext|>


<commit_before>/*
* SSDP Service
*
* Written by Scott Ware
*/

var os = require("os");
var ssdp = require('ssdp')
var dbus = require('dbus-native');
var cbus = dbus.systemBus();
var sbus = ssdp()
var fs = require('fs');

var name="intel-iot-device";
var manufacturer="Intel";
var manufacturerUrl="http://www.intel.com/content/www/us/en/homepage.html";
var model="Intel IoT Device";
var modelUrl="http://www.intel.co.uk/content/www/uk/en/internet-of-things/overview.html";
var version="1.0.0";
var serial="12345678";

var PORT = 8080;
var edisonSerialPath = "/factory/serial_number"
var defaultSerialPath = "/sys/class/dmi/id/board_serial"
var deviceInfoPath = "/etc/device-info"
var versionPath = "/etc/version"
var namePath = "/etc/hostname"

// Exit cleanly when executed using systemd
process.on('SIGTERM', function() {
    	console.log('Stopping service.')
    	sbus.stop(function (error) {
    		process.exit(error ? 1 : 0)
  	})
});

process.on('SIGINT', function() {
    	console.log('Stopping service.')
    	sbus.stop(function (error) {
    		process.exit(error ? 1 : 0)
  	})
});

// Checks connection state using Connman
var checkState = function () {
    	cbus.invoke({
        	destination: 'net.connman',
        	path: '/',
        	'interface': 'net.connman.Manager',
        	member: 'GetProperties',
        	type: dbus.messageType.methodCall,
    	}, function(error, response) {
        	if (error) {
        		console.error('Connman Error!', error);
	    		sbus.stop(function (error) {
    				process.exit(error ? 1 : 0)
  			})
        	} else {
	    		if(response[0][1][1] != 'online') {
				console.info('Network not connected, exiting!')
				sbus.stop(function (error) {
    					process.exit(error ? 1 : 0)
  				})
	    		}
        	}
    	});

    	setTimeout(checkState,10000);
}

//
// Populate device information if available
//

// Check for Edison serial
if(fs.existsSync(edisonSerialPath)) {
	try {
		data = fs.readFileSync(edisonSerialPath);
		serial = data.toString();
	} catch (err) {
		console.log("Failed to read Edison serial file!")
	}
}
// Use Board Serial
else if(fs.existsSync(defaultSerialPath)) {
	try {
		data = fs.readFileSync(defaultSerialPath);
		serial = data.toString();
	} catch (err) {
		console.log("Failed to read board serial file!")
	}
}

// Get version
if(fs.existsSync(versionPath)) {
	try {
		data = fs.readFileSync(versionPath);
		version = data.toString();
	} catch (err) {
		console.log("Failed to read version file!")
	}
}

// Get name
if(fs.existsSync(namePath)) {
	try {
		data = fs.readFileSync(namePath);
		name = data.toString();
	} catch (err) {
		console.log("Failed to read name file!")
	}
}

// Get device info
if(fs.existsSync(deviceInfoPath)) {
	try {
		data = fs.readFileSync(deviceInfoPath);

		deviceInfo = data.toString().split("\n");
		for(i in deviceInfo) {
			if(deviceInfo[i].indexOf("Manufacturer=") > -1) {
				manufacturer = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("ManufacturerUrl=") > -1) {
				manufacturerUrl = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("Model=") > -1) {
				model = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("ModelUrl=") > -1) {
				modelUrl = deviceInfo[i].split("=")[1];
			}
		}
	} catch (err) {
		console.log("Failed to read device info file!")
	}
}

//
// Start server
//

// Print error messages to the console
sbus.on('error', console.error)

// Start polling connection
checkState()

console.log("Start advertising device")

// Advertise device
sbus.advertise({
	usn: 'upnp:rootdevice',
	interval: 10000,
  	details: function (callback) {
    		callback(null, {
      			'$': {
        			'xmlns': 'urn:schemas-upnp-org:device-1-0',
				'configId': '1'
      			},
      			'specVersion': {
        			'major': '1',
        			'minor': '1'
      			},
      			'device': {
        			'deviceType': 'urn:schemas-upnp-org:device:Basic:1',
				'productName':'Intel IoT Reference Platform',
        			'friendlyName': name,
        			'manufacturer': manufacturer,
        			'manufacturerURL': manufacturerUrl,
				'modelDescription': '',
        			'modelName': model,
        			'modelNumber': version,
        			'modelURL': modelUrl,
        			'serialNumber': serial,
				'UDN': sbus.options.udn,
        			'presentationURL': ''
      			}
		})
	}
})
<commit_msg>Added more robust connection check to stop SSDP service initialising without connection. Implemented callback for connection state.<commit_after>/*
* SSDP Service
*
* Written by Scott Ware
*/

var os = require("os");
var ssdp = require('ssdp')
var dbus = require('dbus-native');
var cbus = dbus.systemBus();
var sbus = ssdp()
var fs = require('fs');

var name="intel-iot-device";
var manufacturer="Intel";
var manufacturerUrl="http://www.intel.com/content/www/us/en/homepage.html";
var model="Intel IoT Device";
var modelUrl="http://www.intel.co.uk/content/www/uk/en/internet-of-things/overview.html";
var version="1.0.0";
var serial="12345678";

var PORT = 8080;
var edisonSerialPath = "/factory/serial_number"
var defaultSerialPath = "/sys/class/dmi/id/board_serial"
var deviceInfoPath = "/etc/device-info"
var versionPath = "/etc/version"
var namePath = "/etc/hostname"

// Exit cleanly when executed using systemd
process.on('SIGTERM', function() {
    	console.log('Stopping service.')
    	sbus.stop(function (error) {
    		process.exit(error ? 1 : 0)
  	})
});

process.on('SIGINT', function() {
    	console.log('Stopping service.')
    	sbus.stop(function (error) {
    		process.exit(error ? 1 : 0)
  	})
});

// Checks connection state using Connman
var checkState = function (callback) {
	var online = null;

    	cbus.invoke({
        	destination: 'net.connman',
        	path: '/',
        	'interface': 'net.connman.Manager',
        	member: 'GetProperties',
        	type: dbus.messageType.methodCall,
    	}, function(error, response) {
        	if (error) {
        		console.error('Connman Error!', error);
	    		online = false;
        	} else {
	    		if(response[0][1][1] != 'online') {
				online = false;
			} else {
				online = true;
			}
        	}

		callback(online);
    	});
}

var pollState = function () {
	checkState(function(result) {
		if(result == false) {
			sbus.stop(function (error) {
    				process.exit(error ? 1 : 0)
			})
		}
	});

	setTimeout(pollState,10000);
}

//
// Populate device information if available
//

// Check for Edison serial
if(fs.existsSync(edisonSerialPath)) {
	try {
		data = fs.readFileSync(edisonSerialPath);
		serial = data.toString();
	} catch (err) {
		console.log("Failed to read Edison serial file!")
	}
}
// Use Board Serial
else if(fs.existsSync(defaultSerialPath)) {
	try {
		data = fs.readFileSync(defaultSerialPath);
		serial = data.toString();
	} catch (err) {
		console.log("Failed to read board serial file!")
	}
}

// Get version
if(fs.existsSync(versionPath)) {
	try {
		data = fs.readFileSync(versionPath);
		version = data.toString();
	} catch (err) {
		console.log("Failed to read version file!")
	}
}

// Get name
if(fs.existsSync(namePath)) {
	try {
		data = fs.readFileSync(namePath);
		name = data.toString();
	} catch (err) {
		console.log("Failed to read name file!")
	}
}

// Get device info
if(fs.existsSync(deviceInfoPath)) {
	try {
		data = fs.readFileSync(deviceInfoPath);

		deviceInfo = data.toString().split("\n");
		for(i in deviceInfo) {
			if(deviceInfo[i].indexOf("Manufacturer=") > -1) {
				manufacturer = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("ManufacturerUrl=") > -1) {
				manufacturerUrl = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("Model=") > -1) {
				model = deviceInfo[i].split("=")[1];
			} else if(deviceInfo[i].indexOf("ModelUrl=") > -1) {
				modelUrl = deviceInfo[i].split("=")[1];
			}
		}
	} catch (err) {
		console.log("Failed to read device info file!")
	}
}

//
// Service Begin
//

// Check connection status and start SSDP if connected
checkState(function(result) {
	if(result == false) {
		console.log('Network not connected... Exiting!');
		process.exit(0);
	} else {
		// Print error messages to the console
		sbus.on('error', console.error)

		console.log("Start advertising device")

		// Advertise device
		sbus.advertise({
			usn: 'upnp:rootdevice',
			interval: 10000,
		  	details: function (callback) {
		    		callback(null, {
		      			'$': {
						'xmlns': 'urn:schemas-upnp-org:device-1-0',
						'configId': '1'
		      			},
		      			'specVersion': {
						'major': '1',
						'minor': '1'
		      			},
		      			'device': {
						'deviceType': 'urn:schemas-upnp-org:device:Basic:1',
						'productName':'Intel IoT Reference Platform',
						'friendlyName': name,
						'manufacturer': manufacturer,
						'manufacturerURL': manufacturerUrl,
						'modelDescription': '',
						'modelName': model,
						'modelNumber': version,
						'modelURL': modelUrl,
						'serialNumber': serial,
						'UDN': sbus.options.udn,
						'presentationURL': ''
		      			}
				})
			}
		})

		// Start polling connection
		pollState()
	}
});
<|endoftext|>


<commit_before>odoo.define('mail.ChatThread', function (require) {
"use strict";

var core = require('web.core');
var time = require('web.time');
var DocumentViewer = require('mail.DocumentViewer');
var Widget = require('web.Widget');

var QWeb = core.qweb;
var _t = core._t;

var ORDER = {
    ASC: 1,
    DESC: -1,
};

var read_more = _t('read more');
var read_less = _t('read less');

function time_from_now(date) {
    if (moment().diff(date, 'seconds') < 45) {
        return _t("now");
    }
    return date.fromNow();
}

var Thread = Widget.extend({
    className: 'o_mail_thread',

    events: {
        "click a": "on_click_redirect",
        "click img": "on_click_redirect",
        "click strong": "on_click_redirect",
        "click .o_thread_show_more": "on_click_show_more",
        "click .o_attachment_download": "_onAttachmentDownload",
        "click .o_attachment_view": "_onAttachmentView",
        "click .o_thread_message_needaction": function (event) {
            var message_id = $(event.currentTarget).data('message-id');
            this.trigger("mark_as_read", message_id);
        },
        "click .o_thread_message_star": function (event) {
            var message_id = $(event.currentTarget).data('message-id');
            this.trigger("toggle_star_status", message_id);
        },
        "click .o_thread_message_reply": function (event) {
            this.selected_id = $(event.currentTarget).data('message-id');
            this.$('.o_thread_message').removeClass('o_thread_selected_message');
            this.$('.o_thread_message[data-message-id="' + this.selected_id + '"]')
                .addClass('o_thread_selected_message');
            this.trigger('select_message', this.selected_id);
            event.stopPropagation();
        },
        "click .oe_mail_expand": function (event) {
            event.preventDefault();
            var $message = $(event.currentTarget).parents('.o_thread_message');
            $message.addClass('o_message_expanded');
            this.expanded_msg_ids.push($message.data('message-id'));
        },
        "click .o_thread_message": function (event) {
            $(event.currentTarget).toggleClass('o_thread_selected_message');
        },
        "click": function () {
            if (this.selected_id) {
                this.unselect();
                this.trigger('unselect_message');
            }
        },
    },

    init: function (parent, options) {
        this._super.apply(this, arguments);
        this.options = _.defaults(options || {}, {
            display_order: ORDER.ASC,
            display_needactions: true,
            display_stars: true,
            display_document_link: true,
            display_avatar: true,
            squash_close_messages: true,
            display_email_icon: true,
            display_reply_icon: false,
        });
        this.expanded_msg_ids = [];
        this.selected_id = null;
    },

    render: function (messages, options) {
        var self = this;
        var msgs = _.map(messages, this._preprocess_message.bind(this));
        if (this.options.display_order === ORDER.DESC) {
            msgs.reverse();
        }
        options = _.extend({}, this.options, options);

        // Hide avatar and info of a message if that message and the previous
        // one are both comments wrote by the same author at the same minute
        // and in the same document (users can now post message in documents
        // directly from a channel that follows it)
        var prev_msg;
        _.each(msgs, function (msg) {
            if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
                prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
                (prev_msg.author_id[0] !== msg.author_id[0]) || prev_msg.model !== msg.model ||
                prev_msg.res_id !== msg.res_id) {
                msg.display_author = true;
            } else {
                msg.display_author = !options.squash_close_messages;
            }
            prev_msg = msg;
        });

        this.$el.html(QWeb.render('mail.ChatThread', {
            messages: msgs,
            options: options,
            ORDER: ORDER,
            date_format: time.getLangDatetimeFormat(),
        }));

        this.attachments = _.uniq(_.flatten(_.map(messages, 'attachment_ids')));

        _.each(msgs, function(msg) {
            var $msg = self.$('.o_thread_message[data-message-id="'+ msg.id +'"]');
            $msg.find('.o_mail_timestamp').data('date', msg.date);

            self.insert_read_more($msg);
        });

        if (!this.update_timestamps_interval) {
            this.update_timestamps_interval = setInterval(function() {
                self.update_timestamps();
            }, 1000*60);
        }
    },

    /**
     * Modifies $element to add the 'read more/read less' functionality
     * All element nodes with "data-o-mail-quote" attribute are concerned.
     * All text nodes after a ``#stopSpelling`` element are concerned.
     * Those text nodes need to be wrapped in a span (toggle functionality).
     * All consecutive elements are joined in one 'read more/read less'.
     */
    insert_read_more: function ($element) {
        var self = this;

        var groups = [];
        var read_more_nodes;

        // nodeType 1: element_node
        // nodeType 3: text_node
        var $children = $element.contents()
            .filter(function() {
                return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim();
            });

        _.each($children, function(child) {
            var $child = $(child);

            // Hide Text nodes if "stopSpelling"
            if (child.nodeType === 3 && $child.prevAll("[id*='stopSpelling']").length > 0) {
                // Convert Text nodes to Element nodes
                var $child = $('<span>', {
                    text: child.textContent,
                    "data-o-mail-quote": "1",
                });
                child.parentNode.replaceChild($child[0], child);
            }

            // Create array for each "read more" with nodes to toggle
            if ($child.attr('data-o-mail-quote') || ($child.get(0).nodeName === 'BR' && $child.prev("[data-o-mail-quote='1']").length > 0)) {
                if (!read_more_nodes) {
                    read_more_nodes = [];
                    groups.push(read_more_nodes);
                }
                $child.hide();
                read_more_nodes.push($child);
            } else {
                read_more_nodes = undefined;
                self.insert_read_more($child);
            }
        });

        _.each(groups, function(group) {
            // Insert link just before the first node
            var $read_more = $('<a>', {
                class: "o_mail_read_more",
                href: "#",
                text: read_more,
            }).insertBefore(group[0]);

            // Toggle All next nodes
            var is_read_more = true;
            $read_more.click(function(e) {
                e.preventDefault();
                is_read_more = !is_read_more;
                _.each(group, function ($child) {
                    $child.hide();
                    $child.toggle(!is_read_more);
                });
                $read_more.text(is_read_more ? read_more : read_less);
            });
        });
    },
    update_timestamps: function () {
        var isAtBottom = this.is_at_bottom();
        this.$('.o_mail_timestamp').each(function() {
            var date = $(this).data('date');
            $(this).html(time_from_now(date));
        });
        if (isAtBottom && !this.is_at_bottom()) {
            this.scroll_to();
        }
    },
    on_click_redirect: function (event) {
        // ignore inherited branding
        if ($(event.target).data('oe-field') !== undefined) {
            return;
        }
        var id = $(event.target).data('oe-id');
        if (id) {
            event.preventDefault();
            var model = $(event.target).data('oe-model');
            var options = model && (model !== 'mail.channel') ? {model: model, id: id} : {channel_id: id};
            this._redirect(options);
        }
    },

    _redirect: _.debounce(function (options) {
        if ('channel_id' in options) {
            this.trigger('redirect_to_channel', options.channel_id);
        } else {
            this.trigger('redirect', options.model, options.id);
        }
    }, 500, true),

    on_click_show_more: function () {
        this.trigger('load_more_messages');
    },

    _preprocess_message: function (message) {
        var msg = _.extend({}, message);

        msg.date = moment.min(msg.date, moment());
        msg.hour = time_from_now(msg.date);

        var date = msg.date.format('YYYY-MM-DD');
        if (date === moment().format('YYYY-MM-DD')) {
            msg.day = _t("Today");
        } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) {
            msg.day = _t("Yesterday");
        } else {
            msg.day = msg.date.format('LL');
        }

        if (_.contains(this.expanded_msg_ids, message.id)) {
            msg.expanded = true;
        }

        msg.display_subject = message.subject && message.message_type !== 'notification' && !(message.model && (message.model !== 'mail.channel'));
        msg.is_selected = msg.id === this.selected_id;
        return msg;
    },

    /**
     * Removes a message and re-renders the thread
     * @param {int} [message_id] the id of the removed message
     * @param {array} [messages] the list of messages to display, without the removed one
     * @param {object} [options] options for the thread rendering
     */
    remove_message_and_render: function (message_id, messages, options) {
        var self = this;
        var done = $.Deferred();
        this.$('.o_thread_message[data-message-id="' + message_id + '"]').fadeOut({
            done: function () { self.render(messages, options); done.resolve();},
            duration: 200,
        });
        return done;
    },

    /**
     * Scrolls the thread to a given message or offset if any, to bottom otherwise
     * @param {int} [options.id] optional: the id of the message to scroll to
     * @param {int} [options.offset] optional: the number of pixels to scroll
     */
    scroll_to: function (options) {
        options = options || {};
        if (options.id !== undefined) {
            var $target = this.$('.o_thread_message[data-message-id="' + options.id + '"]');
            if (options.only_if_necessary) {
                var delta = $target.parent().height() - $target.height();
                var offset = delta < 0 ? 0 : delta - ($target.offset().top - $target.offsetParent().offset().top);
                offset = - Math.min(offset, 0);
                this.$el.scrollTo("+=" + offset + "px", options);
            } else if ($target.length) {
                this.$el.scrollTo($target);
            }
        } else if (options.offset !== undefined) {
            this.$el.scrollTop(options.offset);
        } else {
            this.$el.scrollTop(this.el.scrollHeight);
        }
    },
    get_scrolltop: function () {
        return this.$el.scrollTop();
    },
    is_at_bottom: function () {
        return this.el.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight() < 5;
    },
    unselect: function () {
        this.$('.o_thread_message').removeClass('o_thread_selected_message');
        this.selected_id = null;
    },
    destroy: function () {
        clearInterval(this.update_timestamps_interval);
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAttachmentDownload: function (event) {
        event.stopPropagation();
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAttachmentView: function (event) {
        event.stopPropagation();
        var activeAttachmentID = $(event.currentTarget).data('id');
        if (activeAttachmentID) {
            var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
            attachmentViewer.appendTo($('body'));
        }
    },
});

Thread.ORDER = ORDER;

return Thread;

});
<commit_msg>[FIX] mail: translate global variable<commit_after>odoo.define('mail.ChatThread', function (require) {
"use strict";

var core = require('web.core');
var time = require('web.time');
var DocumentViewer = require('mail.DocumentViewer');
var Widget = require('web.Widget');

var QWeb = core.qweb;
var _t = core._t;
var _lt = core._lt;

var ORDER = {
    ASC: 1,
    DESC: -1,
};

var read_more = _lt('read more');
var read_less = _lt('read less');

function time_from_now(date) {
    if (moment().diff(date, 'seconds') < 45) {
        return _t("now");
    }
    return date.fromNow();
}

var Thread = Widget.extend({
    className: 'o_mail_thread',

    events: {
        "click a": "on_click_redirect",
        "click img": "on_click_redirect",
        "click strong": "on_click_redirect",
        "click .o_thread_show_more": "on_click_show_more",
        "click .o_attachment_download": "_onAttachmentDownload",
        "click .o_attachment_view": "_onAttachmentView",
        "click .o_thread_message_needaction": function (event) {
            var message_id = $(event.currentTarget).data('message-id');
            this.trigger("mark_as_read", message_id);
        },
        "click .o_thread_message_star": function (event) {
            var message_id = $(event.currentTarget).data('message-id');
            this.trigger("toggle_star_status", message_id);
        },
        "click .o_thread_message_reply": function (event) {
            this.selected_id = $(event.currentTarget).data('message-id');
            this.$('.o_thread_message').removeClass('o_thread_selected_message');
            this.$('.o_thread_message[data-message-id="' + this.selected_id + '"]')
                .addClass('o_thread_selected_message');
            this.trigger('select_message', this.selected_id);
            event.stopPropagation();
        },
        "click .oe_mail_expand": function (event) {
            event.preventDefault();
            var $message = $(event.currentTarget).parents('.o_thread_message');
            $message.addClass('o_message_expanded');
            this.expanded_msg_ids.push($message.data('message-id'));
        },
        "click .o_thread_message": function (event) {
            $(event.currentTarget).toggleClass('o_thread_selected_message');
        },
        "click": function () {
            if (this.selected_id) {
                this.unselect();
                this.trigger('unselect_message');
            }
        },
    },

    init: function (parent, options) {
        this._super.apply(this, arguments);
        this.options = _.defaults(options || {}, {
            display_order: ORDER.ASC,
            display_needactions: true,
            display_stars: true,
            display_document_link: true,
            display_avatar: true,
            squash_close_messages: true,
            display_email_icon: true,
            display_reply_icon: false,
        });
        this.expanded_msg_ids = [];
        this.selected_id = null;
    },

    render: function (messages, options) {
        var self = this;
        var msgs = _.map(messages, this._preprocess_message.bind(this));
        if (this.options.display_order === ORDER.DESC) {
            msgs.reverse();
        }
        options = _.extend({}, this.options, options);

        // Hide avatar and info of a message if that message and the previous
        // one are both comments wrote by the same author at the same minute
        // and in the same document (users can now post message in documents
        // directly from a channel that follows it)
        var prev_msg;
        _.each(msgs, function (msg) {
            if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
                prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
                (prev_msg.author_id[0] !== msg.author_id[0]) || prev_msg.model !== msg.model ||
                prev_msg.res_id !== msg.res_id) {
                msg.display_author = true;
            } else {
                msg.display_author = !options.squash_close_messages;
            }
            prev_msg = msg;
        });

        this.$el.html(QWeb.render('mail.ChatThread', {
            messages: msgs,
            options: options,
            ORDER: ORDER,
            date_format: time.getLangDatetimeFormat(),
        }));

        this.attachments = _.uniq(_.flatten(_.map(messages, 'attachment_ids')));

        _.each(msgs, function(msg) {
            var $msg = self.$('.o_thread_message[data-message-id="'+ msg.id +'"]');
            $msg.find('.o_mail_timestamp').data('date', msg.date);

            self.insert_read_more($msg);
        });

        if (!this.update_timestamps_interval) {
            this.update_timestamps_interval = setInterval(function() {
                self.update_timestamps();
            }, 1000*60);
        }
    },

    /**
     * Modifies $element to add the 'read more/read less' functionality
     * All element nodes with "data-o-mail-quote" attribute are concerned.
     * All text nodes after a ``#stopSpelling`` element are concerned.
     * Those text nodes need to be wrapped in a span (toggle functionality).
     * All consecutive elements are joined in one 'read more/read less'.
     */
    insert_read_more: function ($element) {
        var self = this;

        var groups = [];
        var read_more_nodes;

        // nodeType 1: element_node
        // nodeType 3: text_node
        var $children = $element.contents()
            .filter(function() {
                return this.nodeType === 1 || this.nodeType === 3 && this.nodeValue.trim();
            });

        _.each($children, function(child) {
            var $child = $(child);

            // Hide Text nodes if "stopSpelling"
            if (child.nodeType === 3 && $child.prevAll("[id*='stopSpelling']").length > 0) {
                // Convert Text nodes to Element nodes
                var $child = $('<span>', {
                    text: child.textContent,
                    "data-o-mail-quote": "1",
                });
                child.parentNode.replaceChild($child[0], child);
            }

            // Create array for each "read more" with nodes to toggle
            if ($child.attr('data-o-mail-quote') || ($child.get(0).nodeName === 'BR' && $child.prev("[data-o-mail-quote='1']").length > 0)) {
                if (!read_more_nodes) {
                    read_more_nodes = [];
                    groups.push(read_more_nodes);
                }
                $child.hide();
                read_more_nodes.push($child);
            } else {
                read_more_nodes = undefined;
                self.insert_read_more($child);
            }
        });

        _.each(groups, function(group) {
            // Insert link just before the first node
            var $read_more = $('<a>', {
                class: "o_mail_read_more",
                href: "#",
                text: read_more,
            }).insertBefore(group[0]);

            // Toggle All next nodes
            var is_read_more = true;
            $read_more.click(function(e) {
                e.preventDefault();
                is_read_more = !is_read_more;
                _.each(group, function ($child) {
                    $child.hide();
                    $child.toggle(!is_read_more);
                });
                $read_more.text(is_read_more ? read_more : read_less);
            });
        });
    },
    update_timestamps: function () {
        var isAtBottom = this.is_at_bottom();
        this.$('.o_mail_timestamp').each(function() {
            var date = $(this).data('date');
            $(this).html(time_from_now(date));
        });
        if (isAtBottom && !this.is_at_bottom()) {
            this.scroll_to();
        }
    },
    on_click_redirect: function (event) {
        // ignore inherited branding
        if ($(event.target).data('oe-field') !== undefined) {
            return;
        }
        var id = $(event.target).data('oe-id');
        if (id) {
            event.preventDefault();
            var model = $(event.target).data('oe-model');
            var options = model && (model !== 'mail.channel') ? {model: model, id: id} : {channel_id: id};
            this._redirect(options);
        }
    },

    _redirect: _.debounce(function (options) {
        if ('channel_id' in options) {
            this.trigger('redirect_to_channel', options.channel_id);
        } else {
            this.trigger('redirect', options.model, options.id);
        }
    }, 500, true),

    on_click_show_more: function () {
        this.trigger('load_more_messages');
    },

    _preprocess_message: function (message) {
        var msg = _.extend({}, message);

        msg.date = moment.min(msg.date, moment());
        msg.hour = time_from_now(msg.date);

        var date = msg.date.format('YYYY-MM-DD');
        if (date === moment().format('YYYY-MM-DD')) {
            msg.day = _t("Today");
        } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) {
            msg.day = _t("Yesterday");
        } else {
            msg.day = msg.date.format('LL');
        }

        if (_.contains(this.expanded_msg_ids, message.id)) {
            msg.expanded = true;
        }

        msg.display_subject = message.subject && message.message_type !== 'notification' && !(message.model && (message.model !== 'mail.channel'));
        msg.is_selected = msg.id === this.selected_id;
        return msg;
    },

    /**
     * Removes a message and re-renders the thread
     * @param {int} [message_id] the id of the removed message
     * @param {array} [messages] the list of messages to display, without the removed one
     * @param {object} [options] options for the thread rendering
     */
    remove_message_and_render: function (message_id, messages, options) {
        var self = this;
        var done = $.Deferred();
        this.$('.o_thread_message[data-message-id="' + message_id + '"]').fadeOut({
            done: function () { self.render(messages, options); done.resolve();},
            duration: 200,
        });
        return done;
    },

    /**
     * Scrolls the thread to a given message or offset if any, to bottom otherwise
     * @param {int} [options.id] optional: the id of the message to scroll to
     * @param {int} [options.offset] optional: the number of pixels to scroll
     */
    scroll_to: function (options) {
        options = options || {};
        if (options.id !== undefined) {
            var $target = this.$('.o_thread_message[data-message-id="' + options.id + '"]');
            if (options.only_if_necessary) {
                var delta = $target.parent().height() - $target.height();
                var offset = delta < 0 ? 0 : delta - ($target.offset().top - $target.offsetParent().offset().top);
                offset = - Math.min(offset, 0);
                this.$el.scrollTo("+=" + offset + "px", options);
            } else if ($target.length) {
                this.$el.scrollTo($target);
            }
        } else if (options.offset !== undefined) {
            this.$el.scrollTop(options.offset);
        } else {
            this.$el.scrollTop(this.el.scrollHeight);
        }
    },
    get_scrolltop: function () {
        return this.$el.scrollTop();
    },
    is_at_bottom: function () {
        return this.el.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight() < 5;
    },
    unselect: function () {
        this.$('.o_thread_message').removeClass('o_thread_selected_message');
        this.selected_id = null;
    },
    destroy: function () {
        clearInterval(this.update_timestamps_interval);
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAttachmentDownload: function (event) {
        event.stopPropagation();
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAttachmentView: function (event) {
        event.stopPropagation();
        var activeAttachmentID = $(event.currentTarget).data('id');
        if (activeAttachmentID) {
            var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
            attachmentViewer.appendTo($('body'));
        }
    },
});

Thread.ORDER = ORDER;

return Thread;

});
<|endoftext|>