<commit_before>'use strict';

console.log('Launching…');

var meta = require('./package.json');
var express = require('express');
var compression = require('compression');
var bodyParser = require('body-parser');
var ejs = require('ejs');
var spawn = require('child_process').spawn;
var exec = require('child_process').exec;
var path = require('path');
var Request = require('request');
var Promise = require('promise');
var Stack = require('immutable').Stack;

var DocumentDownloader = require("./functions.js").DocumentDownloader;
var SpecberusWrapper = require("./functions.js").SpecberusWrapper;
var ThirdPartyChecker = require("./functions.js").ThirdPartyChecker;
var TokenChecker = require("./functions.js").TokenChecker;

// Configuration file
require('./config.js');

var app = express();
var requests = {};
var argTempLocation = process.argv[2] || global.DEFAULT_TEMP_LOCATION;
var argHttpLocation  = process.argv[3] || global.DEFAULT_HTTP_LOCATION;
var port = process.argv[4] || global.DEFAULT_PORT;

app.use(compression());
app.use(bodyParser.urlencoded({ extended: false }));

app.set('view engine', 'ejs');
app.engine('.html', ejs.renderFile);

if (process.env.NODE_ENV === 'production') {
  app.set('views', __dirname + '/dist/views');
  app.use(express.static(__dirname + '/dist/assets'));
} else {
  app.set('views', __dirname + '/views');
  app.use(express.static(__dirname + '/assets'));
}

// Index Page
app.get('/', function(request, response, next) {
  response.render('index.html');
});

// API methods

app.get('/api/version', function(req, res) {
  res.send(
    meta.name +
    ' version ' + meta.version +
    ' running on ' + process.platform +
    ' and listening on port ' + port +
    '. The server time is ' + new Date().toLocaleTimeString() + '.'
  );
});

app.get('/api/status', function(req, res) {
  var result;
  var url = req.query ? req.query.url : null;
  var entry;

  if (url) {
    if (requests[url]) res.json({ 'request': requests[url] });
    else {
      res.status(500).send({ error: 'Request of URL ' + url + ' does not exist.' });
    }
  }
  else res.json({ 'requests': requests });
});

app.post('/api/request', function(req, res) {
  var url = req.body ? req.body.url : null;
  var decision = req.body ? req.body.decision : null;
  var isManifest = req.body ? req.body.isManifest === 'true' : false;

  if (!url || !decision) {
    res.status(500).send({error: 'Missing parameters {url, decision}.'});
  }
  else {
    if (requests[url]) {
      res.send('Spec at ' + url + ' is yet pending validation OR has been already submitted. Check “/api/status”.');
    }
    else {
      requests[url] = {
        'url': url,
        'decision': decision,
        'isManifest': isManifest,
        'jobs': {},
        'history': new History()
      };
      orchestrate(requests[url], isManifest).then(function () {
        console.log('Spec at ' + url + ' (decision: ' + decision + ') has FINISHED.');
      }, function (err) {
        console.log('Spec at ' + url + ' (decision: ' + decision + ') has FAILED.');
      });
      res.send('Spec at ' + url + ' (decision: ' + decision + ') added to the queue.');
    }
  }
});

function trInstaller(source, dest) {
  return new Promise(function (resolve, reject) {
    var cmd = global.TR_INSTALL_CMD + ' ' + source + ' ' + dest;
    exec(cmd, function (err, stdout, stderr) {
      if (err) reject(err);
      else resolve();
    });
  });
}

function updateTrShortlink(uri) {
  return new Promise(function (resolve, reject) {
    var cmd = global.UPDATE_TR_SHORTLINK_CMD + ' ' + uri;
    exec(cmd, function (err, stdout, stderr) {
      if (err) reject(err);
      else resolve();
    });
  });
}

function publish(metadata) {
  return new Promise(function (resolve, reject) {
    Request.post({
      url: global.W3C_PUBSYSTEM_URL,
      form: {
        specversion: {
          uri: metadata.get('thisVersion'),
          latestVersionUri: metadata.get('latestVersion'),
          // old: [{
          //   uri: metadata.get('previousVersion'),
          //   wgid: 0, // FIXME metadata.wgId
          // }],
          date: metadata.get('docDate'),
          title: metadata.get('title'),
          // wgid: 0, // FIXME metadata.wgId
          reportEditors: metadata.get('editorIds'),
          informative: false, // FIXME Not always true
          editorDraft: metadata.get('editorsDraft'),
          processRules: metadata.get('process')
        }
      }
    }, function(/*err, httpResponse, body*/){
      // console.log(err);
      // console.log(httpResponse);
      // console.log(body);
      resolve();
    });
  });
}

function Job() {
  if (typeof this !== 'object') throw new TypeError('Jobs must be constructed via new');

  this.status = '';
  this.errors = [];
}

var History = function History (facts) {
  if (typeof this !== 'object') throw new TypeError('Jobs must be constructed via new');

  this.facts = typeof(facts) === 'undefined' ? Stack() : facts;
};

History.prototype.add = function (fact) {
  return new History(this.facts.unshift({
    time: new Date(),
    fact: fact
  }));
};

// Override
History.prototype.toJSON = function () {
  return this.facts.reverse().toJSON();
};

function orchestrate(spec, isManifest) {
  spec.jobs['retrieve-resources'] = new Job();
  spec.jobs['token-checker'] = new Job();
  spec.jobs['specberus'] = new Job();
  spec.jobs['third-party-checker'] = new Job();
  spec.jobs['publish'] = new Job();
  spec.jobs['tr-install'] = new Job();
  spec.jobs['update-tr-shortlink'] = new Job();

  var date = new Date().getTime();
  var tempLocation = (argTempLocation || global.DEFAULT_TEMP_LOCATION) + path.sep + date + path.sep;
  var httpLocation = (argHttpLocation || global.DEFAULT_SPECBERUS_LOCATION) + '/' + date + '/Overview.html';
  var finalLocation = 'bar';
  var token; //@todo retrieve token

  spec.jobs['retrieve-resources'].status = 'pending';
  return DocumentDownloader.fetchAndInstall(spec.url, tempLocation, isManifest).then(function () {
    spec.jobs['retrieve-resources'].status = 'ok';
    spec.history = spec.history.add('The file has been retrieved.');

    spec.jobs['token-checker'].status = 'pending';
    var shortlink; //@todo retrieve shortlink
    return TokenChecker.check(shortlink, token).then(function(authReport) {
      if(authReport.authorized) {
        spec.jobs['token-checker'].status = 'ok';
        spec.history = spec.history.add('You are authorized to publish');

        spec.jobs['specberus'].status = 'pending';
        return SpecberusWrapper.validate(httpLocation).then(function (report) {
          if(report.errors.size === 0) {
            spec.jobs['specberus'].status = 'ok';
            spec.history = spec.history.add('The document passed specberus.');

            spec.jobs['third-party-checker'].status = 'pending';
            return ThirdPartyChecker.check(httpLocation).then(function (extResources) {
              if (extResources.length === 0) {
                spec.jobs['third-party-checker'].status = 'ok';
                spec.history = spec.history.add('The document passed the third party checker.');

                spec.jobs['publish'].status = 'pending';
                return publish(report.metadata).then(function () {
                  spec.jobs['publish'].status = 'ok';

                  spec.jobs['tr-install'].status = 'pending';
                  return trInstall(tempLocation, finalLocation).then(function () {
                    spec.jobs['tr-install'].status = 'ok';

                    spec.jobs['update-tr-shortlink'].status = 'pending';
                    return updateTrShortlink(report.metadata.get('thisVersion')).then(function () {
                      spec.jobs['update-tr-shortlink'].status = 'ok';

                      var cmd = global.SENDMAIL + ' ' + global.MAILING_LIST + ' ' + report.metadata.get('thisVersion');
                      exec(cmd, function (err, stdout, stderr) {
                        if (err) console.error(stderr);
                      });
                      spec.history = spec.history.add('The document has been published at ' + report.metadata.get('thisVersion') + '.');
                      return Promise.resolve("finished");
                    }, function (err) {
                      spec.jobs['update-tr-shortlink'].status = 'error';
                      spec.jobs['update-tr-shortlink'].errors.push(err.toString());
                      return Promise.reject(err);
                    });
                  }, function (err) {
                    spec.jobs['tr-install'].status = 'error';
                    spec.jobs['tr-install'].errors.push(err.toString());
                    return Promise.reject(err);
                  });
                }, function (err) {
                  spec.jobs['publish'].status = 'error';
                  spec.jobs['publish'].errors.push(err.toString());
                  return Promise.reject(err);
                });
              }
              else {
                spec.history = spec.history.add('The document contains non-authorized resources');
                spec.jobs['third-party-checker'].status = 'failure';
                spec.jobs['third-party-checker'].errors = extResources;
                return Promise.reject(new Error("Failed Third-Party checker"));
              }
            }, function (err) {
              spec.jobs['third-party-checker'].status = 'error';
              spec.jobs['third-party-checker'].errors.push(err.toString());
              return Promise.reject(err);
            });
          }
          else {
            spec.jobs['specberus'].status = 'failure';
            spec.jobs['specberus'].errors = report.errors;
            spec.history = spec.history.add('The document failed specberus.');
            return Promise.reject(new Error("Failed Specberus"));
          }
        }, function (err) {
          spec.jobs['specberus'].status = 'error';
          spec.jobs['specberus'].errors.push(err.toString());
          return Promise.reject(err);
        });
      }
      else {
        spec.jobs['token-checker'].status = 'failure';
        spec.jobs['token-checker'].errors.push('Not authorized');
        spec.history = spec.history.add('You are not authorized to publish');
        return Promise.reject(new Error("Failed Token checker"));
      }
    }, function (err) {
      spec.jobs['token-checker'].status = 'error';
      spec.jobs['token-checker'].errors.push(err.toString());
      return Promise.reject(err);
    });
  }, function (err) {
    spec.history = spec.history.add('The document could not be retrieved.');
    spec.jobs['retrieve-resources'].status = 'error';
    spec.jobs['retrieve-resources'].errors.push(err.toString());
    return Promise.reject(err);
  }).catch(function (err) {
    spec.history = spec.history.add('A system error occurred during the process.');
    return Promise.reject(new Error('Orchestrator has failed.'));
  });
}

app.listen(process.env.PORT || port);

console.log(meta.name +
  ' version ' + meta.version +
  ' running on ' + process.platform +
  ' and listening on port ' + port +
  '. The server time is ' + new Date().toLocaleTimeString() + '.');
<commit_msg>Remove useless require<commit_after>'use strict';

console.log('Launching…');

var meta = require('./package.json');
var express = require('express');
var compression = require('compression');
var bodyParser = require('body-parser');
var ejs = require('ejs');
var exec = require('child_process').exec;
var path = require('path');
var Request = require('request');
var Promise = require('promise');
var Stack = require('immutable').Stack;

var DocumentDownloader = require("./functions.js").DocumentDownloader;
var SpecberusWrapper = require("./functions.js").SpecberusWrapper;
var ThirdPartyChecker = require("./functions.js").ThirdPartyChecker;
var TokenChecker = require("./functions.js").TokenChecker;

// Configuration file
require('./config.js');

var app = express();
var requests = {};
var argTempLocation = process.argv[2] || global.DEFAULT_TEMP_LOCATION;
var argHttpLocation  = process.argv[3] || global.DEFAULT_HTTP_LOCATION;
var port = process.argv[4] || global.DEFAULT_PORT;

app.use(compression());
app.use(bodyParser.urlencoded({ extended: false }));

app.set('view engine', 'ejs');
app.engine('.html', ejs.renderFile);

if (process.env.NODE_ENV === 'production') {
  app.set('views', __dirname + '/dist/views');
  app.use(express.static(__dirname + '/dist/assets'));
} else {
  app.set('views', __dirname + '/views');
  app.use(express.static(__dirname + '/assets'));
}

// Index Page
app.get('/', function(request, response, next) {
  response.render('index.html');
});

// API methods

app.get('/api/version', function(req, res) {
  res.send(
    meta.name +
    ' version ' + meta.version +
    ' running on ' + process.platform +
    ' and listening on port ' + port +
    '. The server time is ' + new Date().toLocaleTimeString() + '.'
  );
});

app.get('/api/status', function(req, res) {
  var result;
  var url = req.query ? req.query.url : null;
  var entry;

  if (url) {
    if (requests[url]) res.json({ 'request': requests[url] });
    else {
      res.status(500).send({ error: 'Request of URL ' + url + ' does not exist.' });
    }
  }
  else res.json({ 'requests': requests });
});

app.post('/api/request', function(req, res) {
  var url = req.body ? req.body.url : null;
  var decision = req.body ? req.body.decision : null;
  var isManifest = req.body ? req.body.isManifest === 'true' : false;

  if (!url || !decision) {
    res.status(500).send({error: 'Missing parameters {url, decision}.'});
  }
  else {
    if (requests[url]) {
      res.send('Spec at ' + url + ' is yet pending validation OR has been already submitted. Check “/api/status”.');
    }
    else {
      requests[url] = {
        'url': url,
        'decision': decision,
        'isManifest': isManifest,
        'jobs': {},
        'history': new History()
      };
      orchestrate(requests[url], isManifest).then(function () {
        console.log('Spec at ' + url + ' (decision: ' + decision + ') has FINISHED.');
      }, function (err) {
        console.log('Spec at ' + url + ' (decision: ' + decision + ') has FAILED.');
      });
      res.send('Spec at ' + url + ' (decision: ' + decision + ') added to the queue.');
    }
  }
});

function trInstaller(source, dest) {
  return new Promise(function (resolve, reject) {
    var cmd = global.TR_INSTALL_CMD + ' ' + source + ' ' + dest;
    exec(cmd, function (err, stdout, stderr) {
      if (err) reject(err);
      else resolve();
    });
  });
}

function updateTrShortlink(uri) {
  return new Promise(function (resolve, reject) {
    var cmd = global.UPDATE_TR_SHORTLINK_CMD + ' ' + uri;
    exec(cmd, function (err, stdout, stderr) {
      if (err) reject(err);
      else resolve();
    });
  });
}

function publish(metadata) {
  return new Promise(function (resolve, reject) {
    Request.post({
      url: global.W3C_PUBSYSTEM_URL,
      form: {
        specversion: {
          uri: metadata.get('thisVersion'),
          latestVersionUri: metadata.get('latestVersion'),
          // old: [{
          //   uri: metadata.get('previousVersion'),
          //   wgid: 0, // FIXME metadata.wgId
          // }],
          date: metadata.get('docDate'),
          title: metadata.get('title'),
          // wgid: 0, // FIXME metadata.wgId
          reportEditors: metadata.get('editorIds'),
          informative: false, // FIXME Not always true
          editorDraft: metadata.get('editorsDraft'),
          processRules: metadata.get('process')
        }
      }
    }, function(/*err, httpResponse, body*/){
      // console.log(err);
      // console.log(httpResponse);
      // console.log(body);
      resolve();
    });
  });
}

function Job() {
  if (typeof this !== 'object') throw new TypeError('Jobs must be constructed via new');

  this.status = '';
  this.errors = [];
}

var History = function History (facts) {
  if (typeof this !== 'object') throw new TypeError('Jobs must be constructed via new');

  this.facts = typeof(facts) === 'undefined' ? Stack() : facts;
};

History.prototype.add = function (fact) {
  return new History(this.facts.unshift({
    time: new Date(),
    fact: fact
  }));
};

// Override
History.prototype.toJSON = function () {
  return this.facts.reverse().toJSON();
};

function orchestrate(spec, isManifest) {
  spec.jobs['retrieve-resources'] = new Job();
  spec.jobs['token-checker'] = new Job();
  spec.jobs['specberus'] = new Job();
  spec.jobs['third-party-checker'] = new Job();
  spec.jobs['publish'] = new Job();
  spec.jobs['tr-install'] = new Job();
  spec.jobs['update-tr-shortlink'] = new Job();

  var date = new Date().getTime();
  var tempLocation = (argTempLocation || global.DEFAULT_TEMP_LOCATION) + path.sep + date + path.sep;
  var httpLocation = (argHttpLocation || global.DEFAULT_SPECBERUS_LOCATION) + '/' + date + '/Overview.html';
  var finalLocation = 'bar';
  var token; //@todo retrieve token

  spec.jobs['retrieve-resources'].status = 'pending';
  return DocumentDownloader.fetchAndInstall(spec.url, tempLocation, isManifest).then(function () {
    spec.jobs['retrieve-resources'].status = 'ok';
    spec.history = spec.history.add('The file has been retrieved.');

    spec.jobs['token-checker'].status = 'pending';
    var shortlink; //@todo retrieve shortlink
    return TokenChecker.check(shortlink, token).then(function(authReport) {
      if(authReport.authorized) {
        spec.jobs['token-checker'].status = 'ok';
        spec.history = spec.history.add('You are authorized to publish');

        spec.jobs['specberus'].status = 'pending';
        return SpecberusWrapper.validate(httpLocation).then(function (report) {
          if(report.errors.size === 0) {
            spec.jobs['specberus'].status = 'ok';
            spec.history = spec.history.add('The document passed specberus.');

            spec.jobs['third-party-checker'].status = 'pending';
            return ThirdPartyChecker.check(httpLocation).then(function (extResources) {
              if (extResources.length === 0) {
                spec.jobs['third-party-checker'].status = 'ok';
                spec.history = spec.history.add('The document passed the third party checker.');

                spec.jobs['publish'].status = 'pending';
                return publish(report.metadata).then(function () {
                  spec.jobs['publish'].status = 'ok';

                  spec.jobs['tr-install'].status = 'pending';
                  return trInstall(tempLocation, finalLocation).then(function () {
                    spec.jobs['tr-install'].status = 'ok';

                    spec.jobs['update-tr-shortlink'].status = 'pending';
                    return updateTrShortlink(report.metadata.get('thisVersion')).then(function () {
                      spec.jobs['update-tr-shortlink'].status = 'ok';

                      var cmd = global.SENDMAIL + ' ' + global.MAILING_LIST + ' ' + report.metadata.get('thisVersion');
                      exec(cmd, function (err, stdout, stderr) {
                        if (err) console.error(stderr);
                      });
                      spec.history = spec.history.add('The document has been published at ' + report.metadata.get('thisVersion') + '.');
                      return Promise.resolve("finished");
                    }, function (err) {
                      spec.jobs['update-tr-shortlink'].status = 'error';
                      spec.jobs['update-tr-shortlink'].errors.push(err.toString());
                      return Promise.reject(err);
                    });
                  }, function (err) {
                    spec.jobs['tr-install'].status = 'error';
                    spec.jobs['tr-install'].errors.push(err.toString());
                    return Promise.reject(err);
                  });
                }, function (err) {
                  spec.jobs['publish'].status = 'error';
                  spec.jobs['publish'].errors.push(err.toString());
                  return Promise.reject(err);
                });
              }
              else {
                spec.history = spec.history.add('The document contains non-authorized resources');
                spec.jobs['third-party-checker'].status = 'failure';
                spec.jobs['third-party-checker'].errors = extResources;
                return Promise.reject(new Error("Failed Third-Party checker"));
              }
            }, function (err) {
              spec.jobs['third-party-checker'].status = 'error';
              spec.jobs['third-party-checker'].errors.push(err.toString());
              return Promise.reject(err);
            });
          }
          else {
            spec.jobs['specberus'].status = 'failure';
            spec.jobs['specberus'].errors = report.errors;
            spec.history = spec.history.add('The document failed specberus.');
            return Promise.reject(new Error("Failed Specberus"));
          }
        }, function (err) {
          spec.jobs['specberus'].status = 'error';
          spec.jobs['specberus'].errors.push(err.toString());
          return Promise.reject(err);
        });
      }
      else {
        spec.jobs['token-checker'].status = 'failure';
        spec.jobs['token-checker'].errors.push('Not authorized');
        spec.history = spec.history.add('You are not authorized to publish');
        return Promise.reject(new Error("Failed Token checker"));
      }
    }, function (err) {
      spec.jobs['token-checker'].status = 'error';
      spec.jobs['token-checker'].errors.push(err.toString());
      return Promise.reject(err);
    });
  }, function (err) {
    spec.history = spec.history.add('The document could not be retrieved.');
    spec.jobs['retrieve-resources'].status = 'error';
    spec.jobs['retrieve-resources'].errors.push(err.toString());
    return Promise.reject(err);
  }).catch(function (err) {
    spec.history = spec.history.add('A system error occurred during the process.');
    return Promise.reject(new Error('Orchestrator has failed.'));
  });
}

app.listen(process.env.PORT || port);

console.log(meta.name +
  ' version ' + meta.version +
  ' running on ' + process.platform +
  ' and listening on port ' + port +
  '. The server time is ' + new Date().toLocaleTimeString() + '.');
<|endoftext|>


<commit_before>import utils from 'hey-utils';
import config from './config';

const rclass = /[\t\r\n\f]/g;
const rnotwhite = (/\S+/g);

function getClass(elem) {
  return (elem.getAttribute && elem.getAttribute('class')) || '';
}
export default utils.extend({}, utils, {
  addClass(elem, value) {
    let classes;
    let cur;
    let curValue;
    let clazz;
    let j;
    let finalValue;

    if (typeof value === 'string' && value) {
      classes = value.match(rnotwhite) || [];

      curValue = getClass(elem);
      cur = elem.nodeType === 1 && (` ${curValue} `)
        .replace(rclass, ' ');

      if (cur) {
        j = 0;
        while ((clazz = classes[j++])) {
          if (cur.indexOf(` ${clazz} `) < 0) {
            cur += `${clazz} `;
          }
        }
        finalValue = cur.trim();
        if (curValue !== finalValue) {
          elem.setAttribute('class', finalValue);
        }
      }
    }
  },
  removeClass(elem, value) {
    let classes;
    let cur;
    let curValue;
    let clazz;
    let j;
    let finalValue;

    if (typeof value === 'string' && value) {
      classes = value.match(rnotwhite) || [];

      curValue = getClass(elem);

      cur = elem.nodeType === 1 && (` ${curValue} `)
        .replace(rclass, ' ');

      if (cur) {
        j = 0;
        while ((clazz = classes[j++])) {
          while (cur.indexOf(` ${clazz} `) > -1) {
            cur = cur.replace(` ${clazz} `, ' ');
          }
        }

        finalValue = cur.trim();
        if (curValue !== finalValue) {
          elem.setAttribute('class', finalValue);
        }
      }
    }

    return this;
  },
  removeDom(elem) {
    if (elem && elem.parentNode) {
      elem.parentNode.removeChild(elem);
    }
  },
  padLeft(str, size) {
    let s = `00000${str}`;
    return s.substr(s.length - size);
  },
  hasClass(elem, selector) {
    let className;
    className = ` ${selector} `;
    if (elem.nodeType === 1 && (` ${getClass(elem)} `)
      .replace(rclass, ' ')
      .indexOf(className) > -1) {
      return true;
    }

    return false;
  },
  initParam(param, paramFrom, array) {
    if (this.isArray(array) && this.isObject(param) && this.isObject(param)) {
      for (let a of array) {
        if (paramFrom[a]) param[a] = paramFrom[a];
      }
    }
    return param;
  },
  toggleValue(list, value) {
    if (!this.isArray(list)) return [value];
    let copyList = this.extend([], list);
    if (list.some(item => item == value)) {
      copyList.splice(list.indexOf(value), 1);
    } else {
      copyList.push(value);
    }
    return copyList;
  },
  toggleValueByKey(list, key, value) {
    if (!this.isArray(list)) return;
    let index = -1;
    for (let l of list) {
      if (this.isNull(l[key])) {
        continue;
      }
      index = list.indexOf(l);
      break;
    }
    if (index > -1) {
      list.splice(index, 1);
    } else {
      list.push(value);
    }
  },
  getArray(array, keyName = 'id') {
    const list = [];
    for (let a of array) {
      list.push(a[keyName]);
    }
    return list;
  },
  numList(start, end, step) {
    let data = [];
    for (let i = start; i < end; i = i + step) {
      data.push(i);
    }
    return data;
  },
  dictMapping(value, key, connector) {
    let dict = config.getDict(key);
    if (!dict || utils.isNull(value)) return '';
    if (utils.isString(value) && connector) {
      value = value.split(connector);
    }
    if (!utils.isNull(value) && value !== '' && key) {
      if (!utils.isArray(value)) {
        value = [value];
      }
    }
    if (value.length <= 0) {
      return '';
    }

    const keyField = config.getOption('dict', 'keyName');
    const titleField = config.getOption('dict', 'titleName');

    if (utils.isArray(dict)) {
      dict = utils.toObject(dict, keyField);
    }
    let result = value.map((ele) => {
      if (utils.isObject(ele)) {
        return ele[titleField];
      }
      const d = dict[ele];
      if (utils.isObject(d)) {
        return d[titleField];
      }
      return d;
    });
    return result.filter(ele => (ele && ele !== '')).join(connector || ', ');
  },
  initOptions(datas, param) {
    let key = config.getOption('dict.keyName');
    let title = config.getOption('dict.titleName');
    let options = [];
    if (this.isObject(datas)) {
      options = this.toArray(datas, key, title);
    } else if (this.isArray(datas)) {
      if (datas.length == 0) {
        options = [];
      } else {
        let data0 = datas[0];
        if (this.isObject(data0)) {
          options = this.copy(datas);
        } else {
          options = datas.map((item) => {
            return {
              [`${key}`]: item,
              [`${title}`]: item
            };
          });
        }
      }
    }
    if (param.render) {
      options.forEach((item) => {
        item[param.html] = param.render.call(null, item);
      });
    }
    return options;
  },
  generateTree(data, param) {
    if (!this.isArray(data)) {
      console.error('generateTree：data must be Array。');
      return null;
    }
    let result = [];
    let dataObj = this.toObject(data, param.keyName);
    for (let d of data) {
      let parentCode = d[param.parentName];
      let hasParent = false;
      if (!utils.isNull(parentCode)) {
        let parentCodes = [parentCode];
        if (utils.isArray(parentCode)) {
          parentCodes = parentCode;
        }
        for (let code of parentCodes) {
          if (!utils.isNull(dataObj[code])) {
            hasParent = true;
            let parent = dataObj[code];
            if (!utils.isArray(parent[param.childrenName])) {
              parent[param.childrenName] = [];
            }
            parent[param.childrenName].push(d);
          }
        }
      }
      if (utils.isNull(parentCode) || !hasParent) {
        result.push(d);
      }
    }
    return result;
  },
  isBlank(value) {
    return this.isNull(value) || value === '';
  },
  getValue(item, param) {
    let title = '';
    let key = null;
    if (utils.isObject(item)) {
      title = item[param.titleName];
      key = item[param.keyName];
    } else {
      title = item;
      key = item;
    }
    let result = {
      key,
      title,
      value: item
    };
    if (param.render && result.key) {
      result.html = param.render.call(null, result);
    }
    return result;
  }
});
<commit_msg>fix(Utils): change utils toggleValue function<commit_after>import utils from 'hey-utils';
import config from './config';

const rclass = /[\t\r\n\f]/g;
const rnotwhite = (/\S+/g);

function getClass(elem) {
  return (elem.getAttribute && elem.getAttribute('class')) || '';
}
export default utils.extend({}, utils, {
  addClass(elem, value) {
    let classes;
    let cur;
    let curValue;
    let clazz;
    let j;
    let finalValue;

    if (typeof value === 'string' && value) {
      classes = value.match(rnotwhite) || [];

      curValue = getClass(elem);
      cur = elem.nodeType === 1 && (` ${curValue} `)
        .replace(rclass, ' ');

      if (cur) {
        j = 0;
        while ((clazz = classes[j++])) {
          if (cur.indexOf(` ${clazz} `) < 0) {
            cur += `${clazz} `;
          }
        }
        finalValue = cur.trim();
        if (curValue !== finalValue) {
          elem.setAttribute('class', finalValue);
        }
      }
    }
  },
  removeClass(elem, value) {
    let classes;
    let cur;
    let curValue;
    let clazz;
    let j;
    let finalValue;

    if (typeof value === 'string' && value) {
      classes = value.match(rnotwhite) || [];

      curValue = getClass(elem);

      cur = elem.nodeType === 1 && (` ${curValue} `)
        .replace(rclass, ' ');

      if (cur) {
        j = 0;
        while ((clazz = classes[j++])) {
          while (cur.indexOf(` ${clazz} `) > -1) {
            cur = cur.replace(` ${clazz} `, ' ');
          }
        }

        finalValue = cur.trim();
        if (curValue !== finalValue) {
          elem.setAttribute('class', finalValue);
        }
      }
    }

    return this;
  },
  removeDom(elem) {
    if (elem && elem.parentNode) {
      elem.parentNode.removeChild(elem);
    }
  },
  padLeft(str, size) {
    let s = `00000${str}`;
    return s.substr(s.length - size);
  },
  hasClass(elem, selector) {
    let className;
    className = ` ${selector} `;
    if (elem.nodeType === 1 && (` ${getClass(elem)} `)
      .replace(rclass, ' ')
      .indexOf(className) > -1) {
      return true;
    }

    return false;
  },
  initParam(param, paramFrom, array) {
    if (this.isArray(array) && this.isObject(param) && this.isObject(param)) {
      for (let a of array) {
        if (paramFrom[a]) param[a] = paramFrom[a];
      }
    }
    return param;
  },
  toggleValue(list, value) {
    if (!this.isArray(list)) return [value];
    let copyList = this.extend([], list);
    let filters = list.filter(item => item == value);
    if (filters.length > 0) {
      copyList.splice(list.indexOf(filters[0]), 1);
    } else {
      copyList.push(value);
    }
    return copyList;
  },
  toggleValueByKey(list, key, value) {
    if (!this.isArray(list)) return;
    let index = -1;
    for (let l of list) {
      if (this.isNull(l[key])) {
        continue;
      }
      index = list.indexOf(l);
      break;
    }
    if (index > -1) {
      list.splice(index, 1);
    } else {
      list.push(value);
    }
  },
  getArray(array, keyName = 'id') {
    const list = [];
    for (let a of array) {
      list.push(a[keyName]);
    }
    return list;
  },
  numList(start, end, step) {
    let data = [];
    for (let i = start; i < end; i = i + step) {
      data.push(i);
    }
    return data;
  },
  dictMapping(value, key, connector) {
    let dict = config.getDict(key);
    if (!dict || utils.isNull(value)) return '';
    if (utils.isString(value) && connector) {
      value = value.split(connector);
    }
    if (!utils.isNull(value) && value !== '' && key) {
      if (!utils.isArray(value)) {
        value = [value];
      }
    }
    if (value.length <= 0) {
      return '';
    }

    const keyField = config.getOption('dict', 'keyName');
    const titleField = config.getOption('dict', 'titleName');

    if (utils.isArray(dict)) {
      dict = utils.toObject(dict, keyField);
    }
    let result = value.map((ele) => {
      if (utils.isObject(ele)) {
        return ele[titleField];
      }
      const d = dict[ele];
      if (utils.isObject(d)) {
        return d[titleField];
      }
      return d;
    });
    return result.filter(ele => (ele && ele !== '')).join(connector || ', ');
  },
  initOptions(datas, param) {
    let key = config.getOption('dict.keyName');
    let title = config.getOption('dict.titleName');
    let options = [];
    if (this.isObject(datas)) {
      options = this.toArray(datas, key, title);
    } else if (this.isArray(datas)) {
      if (datas.length == 0) {
        options = [];
      } else {
        let data0 = datas[0];
        if (this.isObject(data0)) {
          options = this.copy(datas);
        } else {
          options = datas.map((item) => {
            return {
              [`${key}`]: item,
              [`${title}`]: item
            };
          });
        }
      }
    }
    if (param.render) {
      options.forEach((item) => {
        item[param.html] = param.render.call(null, item);
      });
    }
    return options;
  },
  generateTree(data, param) {
    if (!this.isArray(data)) {
      console.error('generateTree：data must be Array。');
      return null;
    }
    let result = [];
    let dataObj = this.toObject(data, param.keyName);
    for (let d of data) {
      let parentCode = d[param.parentName];
      let hasParent = false;
      if (!utils.isNull(parentCode)) {
        let parentCodes = [parentCode];
        if (utils.isArray(parentCode)) {
          parentCodes = parentCode;
        }
        for (let code of parentCodes) {
          if (!utils.isNull(dataObj[code])) {
            hasParent = true;
            let parent = dataObj[code];
            if (!utils.isArray(parent[param.childrenName])) {
              parent[param.childrenName] = [];
            }
            parent[param.childrenName].push(d);
          }
        }
      }
      if (utils.isNull(parentCode) || !hasParent) {
        result.push(d);
      }
    }
    return result;
  },
  isBlank(value) {
    return this.isNull(value) || value === '';
  },
  getValue(item, param) {
    let title = '';
    let key = null;
    if (utils.isObject(item)) {
      title = item[param.titleName];
      key = item[param.keyName];
    } else {
      title = item;
      key = item;
    }
    let result = {
      key,
      title,
      value: item
    };
    if (param.render && result.key) {
      result.html = param.render.call(null, result);
    }
    return result;
  }
});
<|endoftext|>


<commit_before>'use strict';

// Enable actions client library debugging
process.env.DEBUG = 'actions-on-google:*';

let ApiAiAssistant = require('actions-on-google').ApiAiAssistant;
let express = require('express');
let bodyParser = require('body-parser');

let app = express();
app.set('port', (process.env.PORT || 8080));
app.use(bodyParser.json({ type: 'application/json' }));

const GENERATE_ANSWER_ACTION = 'generate_answer';
const CHECK_GUESS_ACTION = 'check_guess';

app.post('/', function (request, response) {
	console.log('headers: ' + JSON.stringify(request.headers));
	console.log('body: ' + JSON.stringify(request.body));

	const assistant = new ApiAiAssistant({ request: request, response: response });

	function getRandomNumber(min, max) {
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	function generateAnswer(assistant) {
		console.log('generateAnswer');
		var answer = getRandomNumber(0, 100);
		assistant.data.answer = answer;
		assistant.ask('I\'m thinking of a number from 0 and 100. What\'s your first guess?');
	}

	function checkGuess(assistant) {
		console.log('checkGuess');
		let answer = assistant.data.answer;
		let guess = parseInt(assistant.getArgument('guess'));
		if (answer > guess) {
			assistant.ask('It\'s higher than ' + guess + '. What\'s your next guess?');
		} else if (answer < guess) {
			assistant.ask('It\'s lower than ' + guess + '. Next guess?');
		} else {
			assistant.tell('Congratulations! You guessed ' + guess +', and I was thinking of ' + answer + '!');
		}
	}

	let actionMap = new Map();
	actionMap.set(GENERATE_ANSWER_ACTION, generateAnswer);
	actionMap.set(CHECK_GUESS_ACTION, checkGuess);

	assistant.handleRequest(actionMap);
});

// Start the server
var server = app.listen(app.get('port'), function () {
	console.log('App listening on port %s', server.address().port);
	console.log('Press Ctrl+C to quit.');
});<commit_msg>Updated to allow for a welcome state.<commit_after>'use strict';

// Enable actions client library debugging
process.env.DEBUG = 'actions-on-google:*';

let ApiAiAssistant = require('actions-on-google').ApiAiAssistant;
let express = require('express');
let bodyParser = require('body-parser');

let app = express();
app.set('port', (process.env.PORT || 8080));
app.use(bodyParser.json({ type: 'application/json' }));

const WELCOME_ACTION = 'input.welcome';
const GENERATE_ANSWER_ACTION = 'generate_answer';
const CHECK_GUESS_ACTION = 'check_guess';

app.post('/', function (request, response) {
	console.log('headers: ' + JSON.stringify(request.headers));
	console.log('body: ' + JSON.stringify(request.body));

	const assistant = new ApiAiAssistant({ request: request, response: response });

	function getRandomNumber(min, max) {
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	function generateAnswer(assistant) {
		console.log('generateAnswer');
		var answer = getRandomNumber(0, 100);
		assistant.data.answer = answer;
		assistant.ask('I\'m thinking of a number from 0 and 100. What\'s your first guess?');
	}

	function checkGuess(assistant) {
		console.log('checkGuess');
		let answer = assistant.data.answer;
		let guess = parseInt(assistant.getArgument('guess'));
		if (answer > guess) {
			assistant.ask('It\'s higher than ' + guess + '. What\'s your next guess?');
		} else if (answer < guess) {
			assistant.ask('It\'s lower than ' + guess + '. Next guess?');
		} else {
			assistant.tell('Congratulations! You guessed ' + guess +', and I was thinking of ' + answer + '!');
		}
	}

	let actionMap = new Map();
	actionMap.set(WELCOME_ACTION, generateAnswer);
	actionMap.set(GENERATE_ANSWER_ACTION, generateAnswer);
	actionMap.set(CHECK_GUESS_ACTION, checkGuess);

	assistant.handleRequest(actionMap);
});

// Start the server
var server = app.listen(app.get('port'), function () {
	console.log('App listening on port %s', server.address().port);
	console.log('Press Ctrl+C to quit.');
});<|endoftext|>


<commit_before>import React, { Component } from 'react';
import {
  View,
  FlatList,
  StatusBar,
  Text,
  Image,
  ScrollView,
} from 'react-native';

import { Icon, Button, Rating, Divider, Avatar } from 'react-native-elements';
import ParallaxView from 'react-native-parallax-view';

import Page from '../../components/Page';
import RefreshFlatList, { RefreshState } from '../../components/RefreshFlatList';
import Toast from '../../components/Toast';

import { theme } from '../../theme';
import styles from './index.style';

import { item } from '../../services/book';

/**
 *
 * @param {*} number
 * @param {*} fixed
 * @param {*} type 0: round 1: floor 2: ceil
 */
function fixedRound(number, fixed, type = 0) {
  if (!fixed) {
    fixed = 0;
  }
  // 输入必须为数字
  if (typeof number !== 'number' || typeof fixed !== 'number')
    throw new Error('Parameters should be type of number!');
  // fixed 必须为整数
  if (fixed % 1 !== 0)
    throw new Error('Parameter `fixed` should be an integer!');

  if (fixed === 0) return Math.round(number);
  var t = Math.pow(10, fixed);
  switch (type) {
    case 0: return Math.round(number * t) / t;
    case 1: return Math.floor(number * t) / t;
    case 2: return Math.ceil(number * t) / t;
    default:
      throw new Error(`No type ${type}`);
  }

}

const formatNumber = (number, fixed = 0, unit = true, type = 0) => {
  if (number >= 10000) {
    return `${fixedRound(number / 10000, fixed, type)}${unit ? '万' : ''}`;
  }
  return number;
}

const subword = (str) => {
  if (str.length > 100) {
    return `${str.substring(0, 100)}...`;
  }
  return str;
}

class BookScreen extends Component {
  static navigationOptions = ({ navigation, screenProps }) => {
    return {
      title: `${navigation.state.params.title ? navigation.state.params.title : ''}`,
      headerStyle: {
        position: 'absolute',
        backgroundColor: theme.styles.variables.colors.transparent,
        zIndex: 100,
        top: 0,
        left: 0,
        right: 0,
        borderBottomWidth: 0,
      },
      headerRight: (
        <View style={theme.styles.navRightContainer}>
          <Icon
            containerStyle={theme.styles.navButtonContainer}
            name='share-alternative'
            type='entypo'
            color={theme.styles.navButton.color}
            underlayColor={theme.styles.navButton.underlayColor}
            onPress={() => { }}
          />
        </View>
      ),
      tabBarVisible: false,
    };
  };
  constructor(props) {
    super(props);

    this.state = {
      book: {},
    }

    this.onFetch = this.onFetch.bind(this);
    this.onScrollOverTitle = this.onScrollOverTitle.bind(this);
    this.renderListStyleItem = this.renderListStyleItem.bind(this);
    this.renderBookInfo = this.renderBookInfo.bind(this);
    this.renderBookStatistics = this.renderBookStatistics.bind(this);
    this.renderBookStatisticsItem = this.renderBookStatisticsItem.bind(this);
    this.renderBookDetail = this.renderBookDetail.bind(this);
    this.renderBookHonor = this.renderBookHonor.bind(this);
    this.renderComment = this.renderComment.bind(this);
    this.renderCommmentList = this.renderCommmentList.bind(this);
    this.renderBookComment = this.renderBookComment.bind(this);
    this.renderAuthorBooks = this.renderAuthorBooks.bind(this);
    this.renderAuthorInfo = this.renderAuthorInfo.bind(this);
  }

  componentDidMount() {
    this.onFetch();
  }

  async onFetch() {
    const { err, data } = await item(this.props.navigation.state.params.bookId);
    if (err) {
      return;
    }
    if (!data) {
      return;
    }

    this.setState({
      book: data,
    });
  }

  onScrollOverTitle(y) {
    if (!this.overed && y >= 35) {
      this.overed = true;
      this.props.navigation.setParams({
        title: this.state.book.BookName,
      });
    } else if (this.overed && y < 35) {
      this.overed = false;
      this.props.navigation.setParams({
        title: '',
      });
    }
  }

  renderListStyleItem(item) {
    return (
      <View style={styles.listStyleItem.container}>
        <Text style={styles.listStyleItem.label}>{item.label ? item.label : ''}</Text>
        <Text style={styles.listStyleItem.value}>{item.value ? item.value : ''}</Text>
        <Icon
          containerStyle={styles.listStyleItem.chevron.container}
          name='chevron-right'
          type='entypo'
          size={styles.listStyleItem.chevron.size}
          color={styles.listStyleItem.chevron.color}
          underlayColor={styles.listStyleItem.chevron.color}
        />
      </View>
    )
  }

  renderBookInfo(book) {
    return (
      <View style={styles.info.container}>
        <Image source={{ uri: `https://qidian.qpic.cn/qdbimg/349573/${book.BookId}/180` }} style={styles.info.preview} />
        <View style={styles.info.text.container}>
          <Text style={styles.info.text.title}>{book.BookName ? book.BookName : '--'}</Text>
          <Text style={styles.info.text.author}>{book.Author ? book.Author : '--'}</Text>
          <View style={[styles.info.text.inline]}>
            <Rating style={styles.info.text.rating} ratingCount={5} startingValue={+book.BookStar} readonly={true} imageSize={theme.styles.variables.size.md} />
            <Text style={styles.info.text.others}>{book.BssReadTotal ? `${formatNumber(book.BssReadTotal)}人读过` : ''}</Text>
          </View>
          <Text style={styles.info.text.others}>{book.CategoryName && book.SubCategoryName ? `${book.CategoryName} | ${book.SubCategoryName}` : '--'}</Text>
          <Text style={styles.info.text.others}>{book.WordsCnt && book.BookStatus ? `${formatNumber(book.WordsCnt, 1)}字 | ${book.BookStatus}` : '--'}</Text>
        </View>
      </View>
    )
  }

  renderBookStatisticsItem(item) {
    return (
      <View key={item.label} style={styles.statistics.item.container}>
        <View style={styles.statistics.item.data.container}>
          <Text style={styles.statistics.item.data.number}>{item.number ? item.number : '--'}</Text>
          <Text style={styles.statistics.item.data.unit}>{item.unit ? item.unit : ''}</Text>
        </View>
        <Text style={styles.statistics.item.label}>{item.label}</Text>
      </View>
    )
  }

  renderBookStatistics(book) {
    const statistics = [{
      label: '月票',
      number: book.MonthTicketCount,
    }, {
      label: '推荐',
      number: formatNumber(book.RecommendAll, 0, false),
      unit: '万'
    }, {
      label: '打赏',
      number: formatNumber(book.DonateCount, 1, false),
      unit: '万次',
    }, {
      label: '粉丝',
      number: formatNumber(book.BookFansCount, 1, false, 1),
      unit: '万+',
    }];
    return (
      <View style={styles.statistics.container}>
        {statistics.map(item => this.renderBookStatisticsItem(item))}
      </View>
    )
  }

  renderBookDetail(book) {
    return (
      <View style={styles.detail.container}>
        <View style={styles.detail.description.container}>
          <Text style={styles.detail.description.title}>简介</Text>
          <Text style={styles.detail.description.text}>{book.Description ? book.Description : '--'}</Text>
        </View>
        {this.renderListStyleItem({ label: '目录', value: `连载至 ${book.TotalChapterCount} 章` })}
      </View>
    );
  }

  renderBookHonor(book) {
    return (
      <View style={styles.honor.container}>
        {this.renderListStyleItem({ label: '作品荣誉', value: `${book.BookHonor && book.BookHonor.length ? book.BookHonor[0].Honors : ''}` })}
      </View>
    );
  }

  renderComment(item) {
    return (
      <View key={item.Id} style={styles.comment.item.container}>
        <Image style={styles.comment.item.avatar} source={{ uri: `https://qidian.qpic.cn/qd_face/349573/${item.UserId}/100` }} />
        <View style={styles.comment.item.textContainer}>
          <View style={styles.comment.item.name.container}>
            <Text>{item.UserName}</Text>
          </View>
          <Text style={styles.comment.item.info}>{`${item.PostDate}：${item.From}`}</Text>
          <Text style={styles.comment.item.text}>{subword(item.Body)}</Text>
        </View>
      </View>
    );
  }

  renderCommmentList(list) {
    if (!list || !list.length) {
      return false;
    }
    return (
      <View style={styles.area.contentContainer}>
        {list.map(item => this.renderComment(item))}
      </View>
    )
  }

  renderBookComment(book) {
    return (
      <View style={styles.area.container}>
        {this.renderListStyleItem({ label: '书评', value: `${book.BookForumCount ? `${formatNumber(book.BookForumCount, 0, true, 1)}+` : ''}` })}
        {this.renderCommmentList(book.BookReviewList)}
      </View>
    );
  }

  renderAuthorBooks(books) {
    return (
      <ScrollView horizontal={true} style={styles.author.books.container}>
        {
          books.map(book => {
            return (
              <View key={book.BookId} style={styles.author.books.book.container}>
                <Image style={styles.author.books.book.preview} source={{ uri: `https://qidian.qpic.cn/qdbimg/349573/${book.BookId}/180` }} />
                <View style={styles.author.books.book.titleWrapper}>
                  <Text style={styles.author.books.book.title}>{book.BookName}</Text>
                </View>
                <Text style={styles.author.books.book.readers}>{`${formatNumber(book.BssReadTotal, 1, true, 1)}人...`}</Text>
              </View>
            )
          })
        }
      </ScrollView>
    )
  }

  renderAuthorInfo(book) {
    const { AuthorInfo: author = {}, AuthorRecommend: authorBooks = [] } = book;
    return (
      <View style={styles.area.container}>
        {this.renderListStyleItem({ label: '作家', value: '' })}
        <View style={styles.area.contentContainer}>
          <View style={styles.author.info.container}>
            <Avatar
              medium
              rounded
              source={{ uri: author.RealImageUrl }}
            />
            <View style={styles.author.info.textContainer}>
              <Text style={styles.author.info.name}>{author.AuthorName}</Text>
              <Text style={styles.author.info.info}>{author.AuthorDesc}</Text>
            </View>
          </View>
          {this.renderAuthorBooks(authorBooks)}
        </View>
      </View>
    );
  }

  render() {
    const book = this.state.book;
    return (
      <Page containerStyle={styles.page}>
        <ParallaxView
          style={{ flex: 1, paddingTop: 60, }}
          onScroll={(e) => {
            this.onScrollOverTitle(e.nativeEvent.contentOffset.y);
          }}
          scrollEventThrottle={3}
          backgroundSource={{uri: 'https://img6.bdstatic.com/img/image/public/bizhi112.png'}}
          header={this.renderBookInfo(book)}
          windowHeight={styles.info.container.height}
          backgroundHeight={styles.info.container.height + 360}
        >
          {this.renderBookStatistics(book)}
          {this.renderBookDetail(book)}
          {this.renderBookHonor(book)}
          {this.renderBookComment(book)}
          {this.renderAuthorInfo(book)}
        </ParallaxView>
      </Page>
    );
  }
}

export default BookScreen;
<commit_msg>Remove error number<commit_after>import React, { Component } from 'react';
import {
  View,
  FlatList,
  StatusBar,
  Text,
  Image,
  ScrollView,
} from 'react-native';

import { Icon, Button, Rating, Divider, Avatar } from 'react-native-elements';
import ParallaxView from 'react-native-parallax-view';

import Page from '../../components/Page';
import RefreshFlatList, { RefreshState } from '../../components/RefreshFlatList';
import Toast from '../../components/Toast';

import { theme } from '../../theme';
import styles from './index.style';

import { item } from '../../services/book';

/**
 *
 * @param {*} number
 * @param {*} fixed
 * @param {*} type 0: round 1: floor 2: ceil
 */
function fixedRound(number, fixed, type = 0) {
  if (!fixed) {
    fixed = 0;
  }
  // 输入必须为数字
  if (typeof number !== 'number' || typeof fixed !== 'number')
    throw new Error('Parameters should be type of number!');
  // fixed 必须为整数
  if (fixed % 1 !== 0)
    throw new Error('Parameter `fixed` should be an integer!');

  if (fixed === 0) return Math.round(number);
  var t = Math.pow(10, fixed);
  switch (type) {
    case 0: return Math.round(number * t) / t;
    case 1: return Math.floor(number * t) / t;
    case 2: return Math.ceil(number * t) / t;
    default:
      throw new Error(`No type ${type}`);
  }

}

const formatNumber = (number, fixed = 0, unit = true, type = 0) => {
  if (number >= 10000) {
    return `${fixedRound(number / 10000, fixed, type)}${unit ? '万' : ''}`;
  }
  return number;
}

const subword = (str) => {
  if (str.length > 100) {
    return `${str.substring(0, 100)}...`;
  }
  return str;
}

class BookScreen extends Component {
  static navigationOptions = ({ navigation, screenProps }) => {
    return {
      title: `${navigation.state.params.title ? navigation.state.params.title : ''}`,
      headerStyle: {
        position: 'absolute',
        backgroundColor: theme.styles.variables.colors.transparent,
        zIndex: 100,
        top: 0,
        left: 0,
        right: 0,
        borderBottomWidth: 0,
      },
      headerRight: (
        <View style={theme.styles.navRightContainer}>
          <Icon
            containerStyle={theme.styles.navButtonContainer}
            name='share-alternative'
            type='entypo'
            color={theme.styles.navButton.color}
            underlayColor={theme.styles.navButton.underlayColor}
            onPress={() => { }}
          />
        </View>
      ),
      tabBarVisible: false,
    };
  };
  constructor(props) {
    super(props);

    this.state = {
      book: {},
    }

    this.onFetch = this.onFetch.bind(this);
    this.onScrollOverTitle = this.onScrollOverTitle.bind(this);
    this.renderListStyleItem = this.renderListStyleItem.bind(this);
    this.renderBookInfo = this.renderBookInfo.bind(this);
    this.renderBookStatistics = this.renderBookStatistics.bind(this);
    this.renderBookStatisticsItem = this.renderBookStatisticsItem.bind(this);
    this.renderBookDetail = this.renderBookDetail.bind(this);
    this.renderBookHonor = this.renderBookHonor.bind(this);
    this.renderComment = this.renderComment.bind(this);
    this.renderCommmentList = this.renderCommmentList.bind(this);
    this.renderBookComment = this.renderBookComment.bind(this);
    this.renderAuthorBooks = this.renderAuthorBooks.bind(this);
    this.renderAuthorInfo = this.renderAuthorInfo.bind(this);
  }

  componentDidMount() {
    this.onFetch();
  }

  async onFetch() {
    const { err, data } = await item(this.props.navigation.state.params.bookId);
    if (err) {
      return;
    }
    if (!data) {
      return;
    }

    this.setState({
      book: data,
    });
  }

  onScrollOverTitle(y) {
    if (!this.overed && y >= 35) {
      this.overed = true;
      this.props.navigation.setParams({
        title: this.state.book.BookName,
      });
    } else if (this.overed && y < 35) {
      this.overed = false;
      this.props.navigation.setParams({
        title: '',
      });
    }
  }

  renderListStyleItem(item) {
    return (
      <View style={styles.listStyleItem.container}>
        <Text style={styles.listStyleItem.label}>{item.label ? item.label : ''}</Text>
        <Text style={styles.listStyleItem.value}>{item.value ? item.value : ''}</Text>
        <Icon
          containerStyle={styles.listStyleItem.chevron.container}
          name='chevron-right'
          type='entypo'
          size={styles.listStyleItem.chevron.size}
          color={styles.listStyleItem.chevron.color}
          underlayColor={styles.listStyleItem.chevron.color}
        />
      </View>
    )
  }

  renderBookInfo(book) {
    return (
      <View style={styles.info.container}>
        <Image source={{ uri: `https://qidian.qpic.cn/qdbimg/349573/${book.BookId}/180` }} style={styles.info.preview} />
        <View style={styles.info.text.container}>
          <Text style={styles.info.text.title}>{book.BookName ? book.BookName : '--'}</Text>
          <Text style={styles.info.text.author}>{book.Author ? book.Author : '--'}</Text>
          <View style={[styles.info.text.inline]}>
            <Rating style={styles.info.text.rating} ratingCount={5} startingValue={+book.BookStar} readonly={true} imageSize={theme.styles.variables.size.md} />
            <Text style={styles.info.text.others}>{book.BssReadTotal ? `${formatNumber(book.BssReadTotal)}人读过` : ''}</Text>
          </View>
          <Text style={styles.info.text.others}>{book.CategoryName && book.SubCategoryName ? `${book.CategoryName} | ${book.SubCategoryName}` : '--'}</Text>
          <Text style={styles.info.text.others}>{book.WordsCnt && book.BookStatus ? `${formatNumber(book.WordsCnt, 1)}字 | ${book.BookStatus}` : '--'}</Text>
        </View>
      </View>
    )
  }

  renderBookStatisticsItem(item) {
    return (
      <View key={item.label} style={styles.statistics.item.container}>
        <View style={styles.statistics.item.data.container}>
          <Text style={styles.statistics.item.data.number}>{item.number ? item.number : '--'}</Text>
          <Text style={styles.statistics.item.data.unit}>{item.unit ? item.unit : ''}</Text>
        </View>
        <Text style={styles.statistics.item.label}>{item.label}</Text>
      </View>
    )
  }

  renderBookStatistics(book) {
    const statistics = [{
      label: '月票',
      number: book.MonthTicketCount,
    }, {
      label: '推荐',
      number: formatNumber(book.RecommendAll, 0, false),
      unit: '万'
    }, {
      label: '打赏',
      number: formatNumber(book.DonateCount, 1, false),
      unit: '万次',
    }, {
      label: '粉丝',
      number: formatNumber(book.BookFansCount, 1, false, 1),
      unit: '万+',
    }];
    return (
      <View style={styles.statistics.container}>
        {statistics.map(item => this.renderBookStatisticsItem(item))}
      </View>
    )
  }

  renderBookDetail(book) {
    return (
      <View style={styles.detail.container}>
        <View style={styles.detail.description.container}>
          <Text style={styles.detail.description.title}>简介</Text>
          <Text style={styles.detail.description.text}>{book.Description ? book.Description : '--'}</Text>
        </View>
        {this.renderListStyleItem({ label: '目录', value: `连载至 ${book.TotalChapterCount} 章` })}
      </View>
    );
  }

  renderBookHonor(book) {
    return (
      <View style={styles.honor.container}>
        {this.renderListStyleItem({ label: '作品荣誉', value: `${book.BookHonor && book.BookHonor.length ? book.BookHonor[0].Honors : ''}` })}
      </View>
    );
  }

  renderComment(item) {
    return (
      <View key={item.Id} style={styles.comment.item.container}>
        <Image style={styles.comment.item.avatar} source={{ uri: `https://qidian.qpic.cn/qd_face/349573/${item.UserId}/100` }} />
        <View style={styles.comment.item.textContainer}>
          <View style={styles.comment.item.name.container}>
            <Text>{item.UserName}</Text>
          </View>
          <Text style={styles.comment.item.info}>{`${item.PostDate}：${item.From}`}</Text>
          <Text style={styles.comment.item.text}>{subword(item.Body)}</Text>
        </View>
      </View>
    );
  }

  renderCommmentList(list) {
    if (!list || !list.length) {
      return false;
    }
    return (
      <View style={styles.area.contentContainer}>
        {list.map(item => this.renderComment(item))}
      </View>
    )
  }

  renderBookComment(book) {
    return (
      <View style={styles.area.container}>
        {this.renderListStyleItem({ label: '书评', value: `${book.BookForumCount ? `${formatNumber(book.BookForumCount, 0, true, 1)}+` : ''}` })}
        {this.renderCommmentList(book.BookReviewList)}
      </View>
    );
  }

  renderAuthorBooks(books) {
    return (
      <ScrollView horizontal={true} style={styles.author.books.container}>
        {
          books.map(book => {
            return (
              <View key={book.BookId} style={styles.author.books.book.container}>
                <Image style={styles.author.books.book.preview} source={{ uri: `https://qidian.qpic.cn/qdbimg/349573/${book.BookId}/180` }} />
                <View style={styles.author.books.book.titleWrapper}>
                  <Text style={styles.author.books.book.title}>{book.BookName}</Text>
                </View>
                <Text style={styles.author.books.book.readers}>{`${formatNumber(book.BssReadTotal, 1, true, 1)}人...`}</Text>
              </View>
            )
          })
        }
      </ScrollView>
    )
  }

  renderAuthorInfo(book) {
    const { AuthorInfo: author = {}, AuthorRecommend: authorBooks = [] } = book;
    return (
      <View style={styles.area.container}>
        {this.renderListStyleItem({ label: '作家', value: '' })}
        <View style={styles.area.contentContainer}>
          <View style={styles.author.info.container}>
            <Avatar
              medium
              rounded
              source={{ uri: author.RealImageUrl }}
            />
            <View style={styles.author.info.textContainer}>
              <Text style={styles.author.info.name}>{author.AuthorName}</Text>
              <Text style={styles.author.info.info}>{author.AuthorDesc}</Text>
            </View>
          </View>
          {this.renderAuthorBooks(authorBooks)}
        </View>
      </View>
    );
  }

  render() {
    const book = this.state.book;
    return (
      <Page containerStyle={styles.page}>
        <ParallaxView
          style={{ flex: 1, paddingTop: 60, }}
          onScroll={(e) => {
            this.onScrollOverTitle(e.nativeEvent.contentOffset.y);
          }}
          light='light'
          scrollEventThrottle={3}
          backgroundSource={{uri: 'https://img6.bdstatic.com/img/image/public/bizhi112.png'}}
          header={this.renderBookInfo(book)}
          windowHeight={styles.info.container.height}
          backgroundHeight={styles.info.container.height + 60}
        >
          {this.renderBookStatistics(book)}
          {this.renderBookDetail(book)}
          {this.renderBookHonor(book)}
          {this.renderBookComment(book)}
          {this.renderAuthorInfo(book)}
        </ParallaxView>
      </Page>
    );
  }
}

export default BookScreen;
<|endoftext|>


<commit_before>let express = require('express');
let path = require('path');
let logger = require('morgan');
let session = require('express-session');
let SQLiteStore = require('connect-sqlite3')(session);
let bodyParser = require('body-parser');

// TODO: Refactor these into a single routes module
let routes = require('./routes/index');
let projects = require('./routes/projects');
let logout = require('./routes/logout');
let hierarchy = require('./routes/hierarchy');
let graph = require('./routes/graph');

let app = express();

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

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
  store: new SQLiteStore(),
  secret: process.env.SESSION_SECRET || 'reallyBadSecret',
  cookie: { maxAge: 24 * 60 * 60 * 1000 }, // 1 day
  resave: false,
  saveUninitialized: false
}));

// Middleware to add the teamName to the process from the .env config file
app.use((req, res, next) => {
  process.env.TEAM_NAME = process.env.TEAM_NAME !== undefined ? process.env.TEAM_NAME : 'defaultName';
  next();
});

app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.use('/projects', projects);
app.use('/logout', logout);
app.use('/hierarchy', hierarchy);
app.use('/graph', graph);

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

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.render('error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;
<commit_msg>Updated from PR comments<commit_after>let express = require('express');
let path = require('path');
let logger = require('morgan');
let session = require('express-session');
let SQLiteStore = require('connect-sqlite3')(session);
let bodyParser = require('body-parser');

// TODO: Refactor these into a single routes module
let routes = require('./routes/index');
let projects = require('./routes/projects');
let logout = require('./routes/logout');
let hierarchy = require('./routes/hierarchy');
let graph = require('./routes/graph');

let app = express();

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

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
  store: new SQLiteStore(),
  secret: process.env.SESSION_SECRET || 'reallyBadSecret',
  cookie: { maxAge: 24 * 60 * 60 * 1000 }, // 1 day
  resave: false,
  saveUninitialized: false
}));

// Middleware to add the teamName to the process from the .env config file
app.use((req, res, next) => {
  process.env.TEAM_NAME = process.env.TEAM_NAME || 'defaultName';
  next();
});

app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);
app.use('/projects', projects);
app.use('/logout', logout);
app.use('/hierarchy', hierarchy);
app.use('/graph', graph);

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

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
  app.use((err, req, res, next) => {
    res.status(err.status || 500);
    res.render('error', {
      message: err.message,
      error: err
    });
  });
}

// production error handler
// no stacktraces leaked to user
app.use((err, req, res, next) => {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});

module.exports = app;
<|endoftext|>


<commit_before>console.log('Thanks for downloading my app!')
<commit_msg>add shebang<commit_after>#!/usr/bin/env node

console.log('Thanks for downloading my app!');
<|endoftext|>


<commit_before>/*
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import Adapter from "enzyme-adapter-react-16";
import { configure, mount } from "enzyme";

import sdk from "../../../skinned-sdk";
import {mkEvent, mkStubRoom} from "../../../test-utils";
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
import * as languageHandler from "../../../../src/languageHandler";

const TextualBody = sdk.getComponent("views.messages.TextualBody");

configure({ adapter: new Adapter() });

describe("<TextualBody />", () => {
    afterEach(() => {
        MatrixClientPeg.matrixClient = null;
    });

    it("renders m.emote correctly", () => {
        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                body: "winks",
                msgtype: "m.emote",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} />);
        expect(wrapper.text()).toBe("* sender winks");
        const content = wrapper.find(".mx_EventTile_body");
        expect(content.html()).toBe('winks');
    });

    it("renders m.notice correctly", () => {
        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "bot_sender",
            content: {
                body: "this is a notice, probably from a bot",
                msgtype: "m.notice",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} />);
        expect(wrapper.text()).toBe(ev.getContent().body);
        const content = wrapper.find(".mx_EventTile_body");
        expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
    });

    describe("renders plain-text m.text correctly", () => {
        beforeEach(() => {
            MatrixClientPeg.matrixClient = {
                getRoom: () => mkStubRoom("room_id"),
                getAccountData: () => undefined,
                isGuest: () => false,
            };
        });

        it("simple message renders as expected", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "this is a plaintext message",
                    msgtype: "m.text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe(ev.getContent().body);
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
        });

        // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
        it("linkification get applied correctly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Visit https://matrix.org/",
                    msgtype: "m.text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe(ev.getContent().body);
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Visit ' +
                'https://matrix.org/');
        });
    });

    describe("renders formatted m.text correctly", () => {
        beforeEach(() => {
            MatrixClientPeg.matrixClient = {
                getRoom: () => mkStubRoom("room_id"),
                getAccountData: () => undefined,
                getUserId: () => "@me:my_server",
                getHomeserverUrl: () => "https://my_server/",
                on: () => undefined,
                removeListener: () => undefined,
                isGuest: () => false,
            };
        });

        it("italics, bold, underline and strikethrough render as expected", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "foo *baz* __bar__ del u",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "foo baz bar del u",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("foo baz bar del u");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                ev.getContent().formatted_body + '');
        });

        it("spoilers get injected properly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Hey [Spoiler for movie](mxc://someserver/somefile)",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "Hey the movie was awesome",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("Hey (movie) the movie was awesome");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Hey ' +
                '' +
                '(movie) ' +
                'the movie was awesome' +
                '');
        });

        // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
        it("pills get injected correctly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Hey User",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "Hey Member",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("Hey Member");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Hey ' +
                '' +
                'Member' +
                '');
        });

        it("pills do not appear for event permalinks", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body:
                        "An [event link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" +
                        "$16085560162aNpaH:example.com?via=example.com) with text",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body:
                        "An event link with text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("An event link with text");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(
                '' +
                'An event link with text',
            );
        });

        it("pills appear for room links with vias", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body:
                        "A [room link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" +
                        "?via=example.com&via=bob.com) with vias",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body:
                        "A room link with vias",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("A !ZxbRYPQXDXKGmDnJNg:example.com with vias");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(
                '' +
                'A ' +
                '!ZxbRYPQXDXKGmDnJNg:example.com with vias',
            );
        });
    });

    it("renders url previews correctly", () => {
        languageHandler.setMissingEntryGenerator(key => key.split('|', 2)[1]);

        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            getUrlPreview: (url) => new Promise(() => {}),
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                body: "Visit https://matrix.org/",
                msgtype: "m.text",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} />);
        expect(wrapper.text()).toBe(ev.getContent().body);

        let widgets = wrapper.find("LinkPreviewWidget");
        // at this point we should have exactly one widget
        expect(widgets.length).toBe(1);
        expect(widgets.at(0).prop("link")).toBe("https://matrix.org/");

        // simulate an event edit and check the transition from the old URL preview to the new one
        const ev2 = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                "m.new_content": {
                    body: "Visit https://vector.im/ and https://riot.im/",
                    msgtype: "m.text",
                },
            },
            event: true,
        });
        ev.makeReplaced(ev2);

        wrapper.setProps({
            mxEvent: ev,
            replacingEventId: ev.getId(),
        }, () => {
            expect(wrapper.text()).toBe(ev2.getContent()["m.new_content"].body + "(edited)");

            // XXX: this is to give TextualBody enough time for state to settle
            wrapper.setState({}, () => {
                widgets = wrapper.find("LinkPreviewWidget");
                // at this point we should have exactly two widgets (not the matrix.org one anymore)
                expect(widgets.length).toBe(2);
                expect(widgets.at(0).prop("link")).toBe("https://vector.im/");
                expect(widgets.at(1).prop("link")).toBe("https://riot.im/");
            });
        });
    });
});

<commit_msg>Fixed tests<commit_after>/*
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import Adapter from "enzyme-adapter-react-16";
import { configure, mount } from "enzyme";

import sdk from "../../../skinned-sdk";
import {mkEvent, mkStubRoom} from "../../../test-utils";
import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
import * as languageHandler from "../../../../src/languageHandler";

const TextualBody = sdk.getComponent("views.messages.TextualBody");

configure({ adapter: new Adapter() });

describe("<TextualBody />", () => {
    afterEach(() => {
        MatrixClientPeg.matrixClient = null;
    });

    it("renders m.emote correctly", () => {
        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                body: "winks",
                msgtype: "m.emote",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} />);
        expect(wrapper.text()).toBe("* sender winks");
        const content = wrapper.find(".mx_EventTile_body");
        expect(content.html()).toBe('winks');
    });

    it("renders m.notice correctly", () => {
        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "bot_sender",
            content: {
                body: "this is a notice, probably from a bot",
                msgtype: "m.notice",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} />);
        expect(wrapper.text()).toBe(ev.getContent().body);
        const content = wrapper.find(".mx_EventTile_body");
        expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
    });

    describe("renders plain-text m.text correctly", () => {
        beforeEach(() => {
            MatrixClientPeg.matrixClient = {
                getRoom: () => mkStubRoom("room_id"),
                getAccountData: () => undefined,
                isGuest: () => false,
            };
        });

        it("simple message renders as expected", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "this is a plaintext message",
                    msgtype: "m.text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe(ev.getContent().body);
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
        });

        // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
        it("linkification get applied correctly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Visit https://matrix.org/",
                    msgtype: "m.text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe(ev.getContent().body);
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Visit ' +
                'https://matrix.org/');
        });
    });

    describe("renders formatted m.text correctly", () => {
        beforeEach(() => {
            MatrixClientPeg.matrixClient = {
                getRoom: () => mkStubRoom("room_id"),
                getAccountData: () => undefined,
                getUserId: () => "@me:my_server",
                getHomeserverUrl: () => "https://my_server/",
                on: () => undefined,
                removeListener: () => undefined,
                isGuest: () => false,
            };
        });

        it("italics, bold, underline and strikethrough render as expected", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "foo *baz* __bar__ del u",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "foo baz bar del u",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("foo baz bar del u");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                ev.getContent().formatted_body + '');
        });

        it("spoilers get injected properly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Hey [Spoiler for movie](mxc://someserver/somefile)",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "Hey the movie was awesome",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("Hey (movie) the movie was awesome");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Hey ' +
                '' +
                '(movie) ' +
                'the movie was awesome' +
                '');
        });

        // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
        it("pills get injected correctly into the DOM", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body: "Hey User",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body: "Hey Member",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("Hey Member");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe('' +
                'Hey ' +
                '' +
                'Member' +
                '');
        });

        it("pills do not appear for event permalinks", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body:
                        "An [event link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" +
                        "$16085560162aNpaH:example.com?via=example.com) with text",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body:
                        "An event link with text",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("An event link with text");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(
                '' +
                'An event link with text',
            );
        });

        it("pills appear for room links with vias", () => {
            const ev = mkEvent({
                type: "m.room.message",
                room: "room_id",
                user: "sender",
                content: {
                    body:
                        "A [room link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" +
                        "?via=example.com&via=bob.com) with vias",
                    msgtype: "m.text",
                    format: "org.matrix.custom.html",
                    formatted_body:
                        "A room link with vias",
                },
                event: true,
            });

            const wrapper = mount(<TextualBody mxEvent={ev} />);
            expect(wrapper.text()).toBe("A !ZxbRYPQXDXKGmDnJNg:example.com with vias");
            const content = wrapper.find(".mx_EventTile_body");
            expect(content.html()).toBe(
                '' +
                'A ' +
                '!ZxbRYPQXDXKGmDnJNg:example.com with vias',
            );
        });
    });

    it("renders url previews correctly", () => {
        languageHandler.setMissingEntryGenerator(key => key.split('|', 2)[1]);

        MatrixClientPeg.matrixClient = {
            getRoom: () => mkStubRoom("room_id"),
            getAccountData: () => undefined,
            getUrlPreview: (url) => new Promise(() => {}),
            isGuest: () => false,
        };

        const ev = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                body: "Visit https://matrix.org/",
                msgtype: "m.text",
            },
            event: true,
        });

        const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} />);
        expect(wrapper.text()).toBe(ev.getContent().body);

        let widgets = wrapper.find("LinkPreviewWidget");
        // at this point we should have exactly one widget
        expect(widgets.length).toBe(1);
        expect(widgets.at(0).prop("link")).toBe("https://matrix.org/");

        // simulate an event edit and check the transition from the old URL preview to the new one
        const ev2 = mkEvent({
            type: "m.room.message",
            room: "room_id",
            user: "sender",
            content: {
                "m.new_content": {
                    body: "Visit https://vector.im/ and https://riot.im/",
                    msgtype: "m.text",
                },
            },
            event: true,
        });
        ev.makeReplaced(ev2);

        wrapper.setProps({
            mxEvent: ev,
            replacingEventId: ev.getId(),
        }, () => {
            expect(wrapper.text()).toBe(ev2.getContent()["m.new_content"].body + "(edited)");

            // XXX: this is to give TextualBody enough time for state to settle
            wrapper.setState({}, () => {
                widgets = wrapper.find("LinkPreviewWidget");
                // at this point we should have exactly two widgets (not the matrix.org one anymore)
                expect(widgets.length).toBe(2);
                expect(widgets.at(0).prop("link")).toBe("https://vector.im/");
                expect(widgets.at(1).prop("link")).toBe("https://riot.im/");
            });
        });
    });
});

<|endoftext|>