<commit_before>/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/* TODO:
     - parse multiple addresses in 'to'
     - needs better end-of-attachment handling so it doesn't detect and parse
       emails embeded as attachments in other emails
*/

var Attachment = FOAM.create({
   model_: 'Model',
   name: 'Attachment',
   label: 'Attachment',
   plural: 'Attachments',
   ids: [ 'filename' ],
   tableProperties:
   [
      'type',
      'filename',
      'position',
      'size'
   ],
   properties:
   [
      {
         model_: 'Property',
         name: 'filename',
         label: 'File Name',
         type: 'String',
         displayWidth: 50,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'type',
         label: 'Type',
         type: 'String',
         displayWidth: 30,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'size',
         label: 'Size',
         type: 'int',
         displayWidth: 10,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'position',
         label: 'Position',
         type: 'int',
         displayWidth: 10,
         view: 'TextFieldView'
      },
   ],
   actions:
   [
      {
         model_: 'Action',
         name: 'view',
         label: 'View',
         help: 'View an attachment.',
         action: function () {
         }
      }
   ]
});

var EMail = FOAM.create({
   model_: 'Model',
   name: 'EMail',
   label: 'EMail',
   plural: 'EMail',
   ids: [ 'id' ],
   tableProperties:
   [
      'from',
      'to',
      'subject',
      'attachments',
      'timestamp'
   ],
   properties:
   [
      {
         model_: 'Property',
         name: 'id',
         label: 'Message ID',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 50,
         displayHeight: 1,
         view: 'TextFieldView',
	 hidden: true,
	 defaltValue: ''
      },
      {
         model_: 'Property',
         name: 'convId',
         label: 'Conversation ID',
         type: 'String',
         mode: 'read-write',
         displayWidth: 30,
         displayHeight: 1,
         view: 'TextFieldView',
	 defaltValue: ''
      },
      {
         model_: 'Property',
         name: 'timestamp',
         label: 'Date',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 50,
         displayHeight: 1,
         view: 'TextFieldView',
         tableWidth: '100',
         preSet: function (d) {
           return typeof d === 'string' ? new Date(d) : d;
	 },
         tableFormatter: function(d) {
           return d.toDateString();
         },
         valueFactory: function() { return new Date(); }
      },
      {
         model_: 'Property',
         name: 'from',
         label: 'From',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 60,
         displayHeight: 1,
         view: 'TextFieldView',
         tableFormatter: function(t) {
           return t.replace(/"/g, '').replace(/<.*/, '');
         },
         valueFactory: function() { return GLOBAL.user || ""; }
      },
      {
         model_: 'Property',
         name: 'to',
         label: 'To',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 60,
         displayHeight: 1,
         view: 'TextFieldView',
         tableFormatter: function(t) {
           return t.replace(/"/g, '').replace(/<.*/, '');
         }
      },
      {
         model_: 'Property',
         name: 'subject',
         label: 'Subject',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 70,
         displayHeight: 1,
         tableWidth: '45%',
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'labels',
	 label: 'Labels',
	 type: 'Array[String]',
	 view: 'StringArrayView',
	 valueFactory: function() { return []; },
         postSet: function(a) {
	   for ( var i = 0 ; i < a.length ; i++ ) a[i] = a[i].intern();
         },
	 help: 'Email labels.'
      },
      {
         model_: 'Property',
	 name: 'attachments',
	 label: 'Attachments', // TODO: switch to paperclip icon
	 type: 'Array[Attachment]',
         subType: 'Attachment',
	 view: 'ArrayView',
	 valueFactory: function() { return []; },
         tableWidth: '20',
         tableFormatter: function(a) {
           return a.length ? a.length : "";
         },
	 help: 'Email attachments.'
      },
      {
         model_: 'Property',
         name: 'body',
         label: '',
         type: 'String',
         displayWidth: 70,
         displayHeight: 15,
         view: 'TextAreaView',
         help: 'Email message body.',
         summaryFormatter: function(t) {
           return '
' + t.replace(/\n/g,'
') + '
';
         }
      }
   ],
   actions:
   [
      {
         model_: 'Action',
         name: 'send',
         label: 'Send',
         help: 'Send the email.',
         action: function () {
           EmailDAO.put(this);
           stack.back();
         }
      },
      {
         model_: 'Action',
         name: 'reply',
         label: 'Reply',
         help: 'Reply to an email.',
         action: function () {
           var replyMail = EMail.create({
             to: this.from,
             from: this.to,
             subject: "Re.: " + this.subject,
             body: this.body.replace(/^|\n/g, '\n>')
           });
           DAOCreateControllerProto.newObj(replyMail, EMailDAO);
         }
      },
      {
         model_: 'Action',
         name: 'forward',
         label: 'Forward',
         help: 'Forward an email.',
         action: function () {
           var forwardedMail = EMail.create({
             from: 'me',
             subject: "Fwd.: " + this.subject,
             body: this.body.replace(/^|\n/g, '\n>')
           });
           DAOCreateControllerProto.newObj(forwardedMail, EMailDAO);
         }
      }
   ]
});

var EmailAddressParser = {
  __proto__: grammar,

  START: sym('address'),

  'until eol': repeat(notChar('\r')),

  'address list': repeat(sym('address'), seq(',', repeat(' '))),

  'address': alt(
    sym('labelled address'),
    sym('simple address')),

  'labelled address': seq(
    repeat(notChars('<,')),
    '<', sym('simple address'), '>'),

  'simple address': seq(repeat(notChar('@')), '@', repeat(notChars('\r>,')))
}.addActions({
  'labelled address': function(v) { return v[0].join('') + v[1] + v[2] + v[3]; },
  'simple address': function(v) { return v[0].join('') + v[1] + v[2].join(''); }
});

var MBOXParser = {
  __proto__: grammar,

  START: sym('line'),

  'eol': literal('\n'),

  'until eol': repeat(notChar('\r')),

  line: alt(
    sym('start of email'),
    sym('id'),
    sym('conversation id'),
    sym('to'),
    sym('from'),
    sym('subject'),
    sym('date'),
    sym('labels'),
    sym('start of block'),
    sym('start of body'),
    sym('start of attachment')
  ),

  'start of email': seq('From ', sym('until eol')),

  id: seq('Message-ID: ', sym('until eol')),

  'conversation id': seq('X-GM-THRID: ', sym('until eol')),

  address: EmailAddressParser.export('address'),

  'address list': EmailAddressParser.export('address list'),

//  to: seq('To: ', repeat(not(alt(',', '\r'))) /*sym('until eol')*/),
  to: seq('To: ', sym('until eol')),
  from: seq('From: ', sym('until eol')),

  labels: seq('X-Gmail-Labels: ', repeat(sym('label'), ',')),

  label: repeat(alt(range('a','z'), range('A', 'Z'), range('0', '9'))),

  subject: seq('Subject: ', sym('until eol')),

  date: seq('Date: ', sym('until eol')),

  'other': sym('until eol'),

  'start of block': seq(
    '--', sym('until eol')),

  'start of body': seq(
    'Content-Type: text/plain; ',
    sym('until eol')
    ),

  'end of body': alt(
    seq('Content-Transfer-Encoding:', sym('until eol')),
    '--',
    '\n--'), // TODO: remove this line when reader fixed!

  'start of attachment': seq(
    'Content-Type: ', repeat(notChar(';')), '; name="', sym("filename"), '"', sym('until eol')
//    'Content-Disposition: attachment; filename="', sym("filename"), '"', sym('until eol')
    ),

  filename: repeat(notChar('"'))

};

/** Sink which loads Emails into a DAO. **/
var MBOXLoader = {
  __proto__: MBOXParser,

  ps: StringPS.create(""),

  PARSE_HEADERS_STATE: function HEADERS(str) {
    this.parseString(str);
  },

  READ_BODY_STATE: function BODY(str) {
    if ( str.slice(0, 5) === 'From ' ) {
      this.state = this.PARSE_HEADERS_STATE;
      this.state(str);
      return;
    }

    if ( str.indexOf(this.blockId) == 2 && str.slice(-3, -1) == '--' ) {
      this.state = this.PARSE_HEADERS_STATE;
      this.blockId = undefined;
    }

    this.b.push(str.trimRight());
  },

  SKIP_ATTACHMENT_STATE: function ATTACHMENT(str) {
    var att = this.email.attachments[this.email.attachments.length-1];
    if ( str.slice(0, 5) === 'From ' ) {
      att.size = att.pos - att.position;
      this.state = this.PARSE_HEADERS_STATE;
      this.state(str);
      return;
    }

    if ( str.indexOf(this.blockId) == 2 && str.slice(-3, -1) == '--' ) {
      att.size = att.pos - att.position;
      this.state = this.PARSE_HEADERS_STATE;
      this.blockId = undefined;
    }
  },

  created: 0, // No of Emails created

  lineNo: 0,  // Current Line Number in mbox file

  pos: 0,     // Current byte position in mbox file

  segPos: 0,

  put: function(str) {
    if ( this.lineNo == 0 ) {
      this.segStartTime = this.startTime = Date.now();
      this.state = this.PARSE_HEADERS_STATE;
    }

    this.lineNo++;
    this.pos += str.length;

    if ( ! ( this.lineNo % 100000 ) ) {
      var lps = Math.floor(this.lineNo / (Date.now() - this.startTime));
      var bps = Math.floor(this.pos / (Date.now() - this.startTime));
      var slps = Math.floor(100000 / (Date.now() - this.segStartTime));
      var sbps = Math.floor((this.pos-this.segPos) / (Date.now() - this.segStartTime));

      console.log(
        'line: ' + Math.floor(this.lineNo/1000) +
        'k  time: ' + Math.floor((Date.now() - this.startTime)) +
        'ms  bytes: ' + Math.floor(this.pos/1000) +
        'k  created: ' + this.created +
        '    SEGMENT:',
        ' lps: ' + slps +
        'k bps: ' + sbps + 'k' +
        '    TOTAL:',
        ' lps: ' + lps +
        'k bps: ' + bps + 'k ' +
        'state: ' + this.state.name);

      this.segStartTime = Date.now();
      this.segPos = this.pos;
    }

    this.state(str);
  },

  eof: function() { this.saveCurrentEmail(); },

  saveCurrentEmail: function() {
    if ( this.email ) {
      this.email.body = this.b.join('\n');

      var i = this.email.body.indexOf("Content-Type:");
      if ( i != -1 ) this.email.body = this.email.body.slice(0,i);

      i = this.email.body.indexOf("Content-Transfer-Encoding: base64");
      if ( i != -1 ) this.email.body = this.email.body.slice(0,i);

      this.b = [];
      if ( this.email.to.length == 0 ) return;
      if ( this.email.to.indexOf('<<') != -1 ) return;
      if ( this.email.from.indexOf('<<') != -1 ) return;
      if ( this.email.to.indexOf('3D') != -1 ) return;
      if ( this.email.from.indexOf('3D') != -1 ) return;
      if ( this.email.from.indexOf('=') != -1 ) return;
      if ( this.email.from.indexOf('<') == 0 ) return;
      if ( this.email.from.indexOf(' ') == 0 ) return;

      this.created++;

      // console.log('creating: ', this.created);
      // console.log('creating: ', this.email.toJSON());
      if ( this.dao ) this.dao.put(this.email);
    }
  }
}.addActions({
  'start of email': function() {
    this.saveCurrentEmail();

    this.email = EMail.create();
    this.b = [];
  },

//  id: function(v) { this.email.id = v[1].join('').trim(); },
  id: function(v) { this.email.id = Math.floor(Math.random()*100000000); },

  'conversation id': function(v) { this.email.convId = v[1].join('').trim(); },

  to: function(v) {
    this.email.to = v[1].join('').trim();
    var i = this.email.to.indexOf(',');
    if ( i != -1 ) this.email.to = this.email.to.substring(0, i);
},

  from: function(v) { this.email.from = v[1].join('').trim(); },

  subject: function(v) { this.email.subject = v[1].join('').trim(); },

  date: function(v) { this.email.timestamp = new Date(v[1].join('').trim()); },

  label: function(v) { this.email.labels.push(v.join('')); },

  'start of block': function(v) {
     var blockId = v[1].join('');
     if ( blockId.slice(-2) == '--' ) return;
     this.blockId = blockId;
   },

  'start of body': function(v) {
    this.state = this.READ_BODY_STATE;
  },

  'start of attachment': function(v, unused, pos) {
    var attachment = Attachment.create({
      type: v[1].join(''),
      filename: v[3].join(''),
      position: this.pos
    });

    this.email.attachments.push(attachment);
    this.state = this.SKIP_ATTACHMENT_STATE;
  },

  // TODO: timestamp, message-id, body, attachments
  // TODO: internalize common strings to save memory (or maybe do it at the DAO level)

});

var EMailBody = FOAM.create({
    model_: 'Model',
    label: 'EMailBody',
    name: 'EMailBody',

    ids: [
        'offset',
        'size'
    ],

    properties: [
        {
            name: 'offset',
            label: 'Offset',
            type: 'Integer',
            required: true
        },
        {
            name: 'size',
            label: 'Size',
            type: 'Integer',
            required: true
        },
        {
            name: 'value',
            label: 'Value',
            type: 'String',
            defaultValue: ''
        }
    ],

    methods: {
    }
});
<commit_msg>Fixed email 'reply' and 'forward' actions.<commit_after>/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/* TODO:
     - parse multiple addresses in 'to'
     - needs better end-of-attachment handling so it doesn't detect and parse
       emails embeded as attachments in other emails
*/

var Attachment = FOAM.create({
   model_: 'Model',
   name: 'Attachment',
   label: 'Attachment',
   plural: 'Attachments',
   ids: [ 'filename' ],
   tableProperties:
   [
      'type',
      'filename',
      'position',
      'size'
   ],
   properties:
   [
      {
         model_: 'Property',
         name: 'filename',
         label: 'File Name',
         type: 'String',
         displayWidth: 50,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'type',
         label: 'Type',
         type: 'String',
         displayWidth: 30,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'size',
         label: 'Size',
         type: 'int',
         displayWidth: 10,
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'position',
         label: 'Position',
         type: 'int',
         displayWidth: 10,
         view: 'TextFieldView'
      },
   ],
   actions:
   [
      {
         model_: 'Action',
         name: 'view',
         label: 'View',
         help: 'View an attachment.',
         action: function () {
         }
      }
   ]
});

var EMail = FOAM.create({
   model_: 'Model',
   name: 'EMail',
   label: 'EMail',
   plural: 'EMail',
   ids: [ 'id' ],
   tableProperties:
   [
      'from',
      'to',
      'subject',
      'attachments',
      'timestamp'
   ],
   properties:
   [
      {
         model_: 'Property',
         name: 'id',
         label: 'Message ID',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 50,
         displayHeight: 1,
         view: 'TextFieldView',
	 hidden: true,
	 defaltValue: ''
      },
      {
         model_: 'Property',
         name: 'convId',
         label: 'Conversation ID',
         type: 'String',
         mode: 'read-write',
         displayWidth: 30,
         displayHeight: 1,
         view: 'TextFieldView',
	 defaltValue: ''
      },
      {
         model_: 'Property',
         name: 'timestamp',
         label: 'Date',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 50,
         displayHeight: 1,
         view: 'TextFieldView',
         tableWidth: '100',
         preSet: function (d) {
           return typeof d === 'string' ? new Date(d) : d;
	 },
         tableFormatter: function(d) {
           return d.toDateString();
         },
         valueFactory: function() { return new Date(); }
      },
      {
         model_: 'Property',
         name: 'from',
         label: 'From',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 60,
         displayHeight: 1,
         view: 'TextFieldView',
         tableFormatter: function(t) {
           return t.replace(/"/g, '').replace(/<.*/, '');
         },
         valueFactory: function() { return GLOBAL.user || ""; }
      },
      {
         model_: 'Property',
         name: 'to',
         label: 'To',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 60,
         displayHeight: 1,
         view: 'TextFieldView',
         tableFormatter: function(t) {
           return t.replace(/"/g, '').replace(/<.*/, '');
         }
      },
      {
         model_: 'Property',
         name: 'subject',
         label: 'Subject',
         type: 'String',
         mode: 'read-write',
         required: true,
         displayWidth: 70,
         displayHeight: 1,
         tableWidth: '45%',
         view: 'TextFieldView'
      },
      {
         model_: 'Property',
         name: 'labels',
	 label: 'Labels',
	 type: 'Array[String]',
	 view: 'StringArrayView',
	 valueFactory: function() { return []; },
         postSet: function(a) {
	   for ( var i = 0 ; i < a.length ; i++ ) a[i] = a[i].intern();
         },
	 help: 'Email labels.'
      },
      {
         model_: 'Property',
	 name: 'attachments',
	 label: 'Attachments', // TODO: switch to paperclip icon
	 type: 'Array[Attachment]',
         subType: 'Attachment',
	 view: 'ArrayView',
	 valueFactory: function() { return []; },
         tableWidth: '20',
         tableFormatter: function(a) {
           return a.length ? a.length : "";
         },
	 help: 'Email attachments.'
      },
      {
         model_: 'Property',
         name: 'body',
         label: '',
         type: 'String',
         displayWidth: 70,
         displayHeight: 15,
         view: 'TextAreaView',
         help: 'Email message body.',
         summaryFormatter: function(t) {
           return '
' + t.replace(/\n/g,'
') + '
';
         }
      }
   ],
   actions:
   [
      {
         model_: 'Action',
         name: 'send',
         label: 'Send',
         help: 'Send the email.',
         action: function () {
           EmailDAO.put(this);
           stack.back();
         }
      },
      {
         model_: 'Action',
         name: 'reply',
         label: 'Reply',
         help: 'Reply to an email.',
         action: function () {
           var replyMail = EMail.create({
             to: this.from,
             from: this.to,
             subject: "Re.: " + this.subject,
             body: this.body.replace(/^|\n/g, '\n>')
           });
           DAOCreateController.getPrototype().newObj(replyMail, EMailDAO);
         }
      },
      {
         model_: 'Action',
         name: 'forward',
         label: 'Forward',
         help: 'Forward an email.',
         action: function () {
           var forwardedMail = EMail.create({
             from: 'me',
             subject: "Fwd.: " + this.subject,
             body: this.body.replace(/^|\n/g, '\n>')
           });
           DAOCreateController.getPrototype().newObj(forwardedMail, EMailDAO);
         }
      }
   ]
});

var EmailAddressParser = {
  __proto__: grammar,

  START: sym('address'),

  'until eol': repeat(notChar('\r')),

  'address list': repeat(sym('address'), seq(',', repeat(' '))),

  'address': alt(
    sym('labelled address'),
    sym('simple address')),

  'labelled address': seq(
    repeat(notChars('<,')),
    '<', sym('simple address'), '>'),

  'simple address': seq(repeat(notChar('@')), '@', repeat(notChars('\r>,')))
}.addActions({
  'labelled address': function(v) { return v[0].join('') + v[1] + v[2] + v[3]; },
  'simple address': function(v) { return v[0].join('') + v[1] + v[2].join(''); }
});

var MBOXParser = {
  __proto__: grammar,

  START: sym('line'),

  'eol': literal('\n'),

  'until eol': repeat(notChar('\r')),

  line: alt(
    sym('start of email'),
    sym('id'),
    sym('conversation id'),
    sym('to'),
    sym('from'),
    sym('subject'),
    sym('date'),
    sym('labels'),
    sym('start of block'),
    sym('start of body'),
    sym('start of attachment')
  ),

  'start of email': seq('From ', sym('until eol')),

  id: seq('Message-ID: ', sym('until eol')),

  'conversation id': seq('X-GM-THRID: ', sym('until eol')),

  address: EmailAddressParser.export('address'),

  'address list': EmailAddressParser.export('address list'),

//  to: seq('To: ', repeat(not(alt(',', '\r'))) /*sym('until eol')*/),
  to: seq('To: ', sym('until eol')),
  from: seq('From: ', sym('until eol')),

  labels: seq('X-Gmail-Labels: ', repeat(sym('label'), ',')),

  label: repeat(alt(range('a','z'), range('A', 'Z'), range('0', '9'))),

  subject: seq('Subject: ', sym('until eol')),

  date: seq('Date: ', sym('until eol')),

  'other': sym('until eol'),

  'start of block': seq(
    '--', sym('until eol')),

  'start of body': seq(
    'Content-Type: text/plain; ',
    sym('until eol')
    ),

  'end of body': alt(
    seq('Content-Transfer-Encoding:', sym('until eol')),
    '--',
    '\n--'), // TODO: remove this line when reader fixed!

  'start of attachment': seq(
    'Content-Type: ', repeat(notChar(';')), '; name="', sym("filename"), '"', sym('until eol')
//    'Content-Disposition: attachment; filename="', sym("filename"), '"', sym('until eol')
    ),

  filename: repeat(notChar('"'))

};

/** Sink which loads Emails into a DAO. **/
var MBOXLoader = {
  __proto__: MBOXParser,

  ps: StringPS.create(""),

  PARSE_HEADERS_STATE: function HEADERS(str) {
    this.parseString(str);
  },

  READ_BODY_STATE: function BODY(str) {
    if ( str.slice(0, 5) === 'From ' ) {
      this.state = this.PARSE_HEADERS_STATE;
      this.state(str);
      return;
    }

    if ( str.indexOf(this.blockId) == 2 && str.slice(-3, -1) == '--' ) {
      this.state = this.PARSE_HEADERS_STATE;
      this.blockId = undefined;
    }

    this.b.push(str.trimRight());
  },

  SKIP_ATTACHMENT_STATE: function ATTACHMENT(str) {
    var att = this.email.attachments[this.email.attachments.length-1];
    if ( str.slice(0, 5) === 'From ' ) {
      att.size = att.pos - att.position;
      this.state = this.PARSE_HEADERS_STATE;
      this.state(str);
      return;
    }

    if ( str.indexOf(this.blockId) == 2 && str.slice(-3, -1) == '--' ) {
      att.size = att.pos - att.position;
      this.state = this.PARSE_HEADERS_STATE;
      this.blockId = undefined;
    }
  },

  created: 0, // No of Emails created

  lineNo: 0,  // Current Line Number in mbox file

  pos: 0,     // Current byte position in mbox file

  segPos: 0,

  put: function(str) {
    if ( this.lineNo == 0 ) {
      this.segStartTime = this.startTime = Date.now();
      this.state = this.PARSE_HEADERS_STATE;
    }

    this.lineNo++;
    this.pos += str.length;

    if ( ! ( this.lineNo % 100000 ) ) {
      var lps = Math.floor(this.lineNo / (Date.now() - this.startTime));
      var bps = Math.floor(this.pos / (Date.now() - this.startTime));
      var slps = Math.floor(100000 / (Date.now() - this.segStartTime));
      var sbps = Math.floor((this.pos-this.segPos) / (Date.now() - this.segStartTime));

      console.log(
        'line: ' + Math.floor(this.lineNo/1000) +
        'k  time: ' + Math.floor((Date.now() - this.startTime)) +
        'ms  bytes: ' + Math.floor(this.pos/1000) +
        'k  created: ' + this.created +
        '    SEGMENT:',
        ' lps: ' + slps +
        'k bps: ' + sbps + 'k' +
        '    TOTAL:',
        ' lps: ' + lps +
        'k bps: ' + bps + 'k ' +
        'state: ' + this.state.name);

      this.segStartTime = Date.now();
      this.segPos = this.pos;
    }

    this.state(str);
  },

  eof: function() { this.saveCurrentEmail(); },

  saveCurrentEmail: function() {
    if ( this.email ) {
      this.email.body = this.b.join('\n');

      var i = this.email.body.indexOf("Content-Type:");
      if ( i != -1 ) this.email.body = this.email.body.slice(0,i);

      i = this.email.body.indexOf("Content-Transfer-Encoding: base64");
      if ( i != -1 ) this.email.body = this.email.body.slice(0,i);

      this.b = [];
      if ( this.email.to.length == 0 ) return;
      if ( this.email.to.indexOf('<<') != -1 ) return;
      if ( this.email.from.indexOf('<<') != -1 ) return;
      if ( this.email.to.indexOf('3D') != -1 ) return;
      if ( this.email.from.indexOf('3D') != -1 ) return;
      if ( this.email.from.indexOf('=') != -1 ) return;
      if ( this.email.from.indexOf('<') == 0 ) return;
      if ( this.email.from.indexOf(' ') == 0 ) return;

      this.created++;

      // console.log('creating: ', this.created);
      // console.log('creating: ', this.email.toJSON());
      if ( this.dao ) this.dao.put(this.email);
    }
  }
}.addActions({
  'start of email': function() {
    this.saveCurrentEmail();

    this.email = EMail.create();
    this.b = [];
  },

//  id: function(v) { this.email.id = v[1].join('').trim(); },
  id: function(v) { this.email.id = Math.floor(Math.random()*100000000); },

  'conversation id': function(v) { this.email.convId = v[1].join('').trim(); },

  to: function(v) {
    this.email.to = v[1].join('').trim();
    var i = this.email.to.indexOf(',');
    if ( i != -1 ) this.email.to = this.email.to.substring(0, i);
},

  from: function(v) { this.email.from = v[1].join('').trim(); },

  subject: function(v) { this.email.subject = v[1].join('').trim(); },

  date: function(v) { this.email.timestamp = new Date(v[1].join('').trim()); },

  label: function(v) { this.email.labels.push(v.join('')); },

  'start of block': function(v) {
     var blockId = v[1].join('');
     if ( blockId.slice(-2) == '--' ) return;
     this.blockId = blockId;
   },

  'start of body': function(v) {
    this.state = this.READ_BODY_STATE;
  },

  'start of attachment': function(v, unused, pos) {
    var attachment = Attachment.create({
      type: v[1].join(''),
      filename: v[3].join(''),
      position: this.pos
    });

    this.email.attachments.push(attachment);
    this.state = this.SKIP_ATTACHMENT_STATE;
  },

  // TODO: timestamp, message-id, body, attachments
  // TODO: internalize common strings to save memory (or maybe do it at the DAO level)

});

var EMailBody = FOAM.create({
    model_: 'Model',
    label: 'EMailBody',
    name: 'EMailBody',

    ids: [
        'offset',
        'size'
    ],

    properties: [
        {
            name: 'offset',
            label: 'Offset',
            type: 'Integer',
            required: true
        },
        {
            name: 'size',
            label: 'Size',
            type: 'Integer',
            required: true
        },
        {
            name: 'value',
            label: 'Value',
            type: 'String',
            defaultValue: ''
        }
    ],

    methods: {
    }
});
<|endoftext|>


<commit_before>/* eslint-disable prefer-promise-reject-errors */

// This file does not contain anything React-specific. If need JS client for
// another framework this file could be extracted into its own package and be
// re-used.

import unfetch from "isomorphic-unfetch";

import { applyUriDefaults, parseUri, stringifyUri } from "./uri";

const DOCUMENT_DOMAIN_REGEX = /\bdocument\.domain\s*=\s*(["'])([^'"\s]+)\1/;

/*
This class fetches and caches nodes, provides global options, and keeps
`window.DJEDI_NODES` up-to-date. See the docs for more information.

The admin sidebar expects the following mapping of all rendered nodes on the page:

    window.DJEDI_NODES = {
      "<uri>": "<default>",
    }
*/
export class Djedi {
  constructor() {
    this.options = makeDefaultOptions();

    this._nodes = new Map();
    this._prefetchableNodes = new Map();
    this._renderedNodes = new Map();
    this._lastPrefetch = {};
    this._batch = makeEmptyBatch();
    this._DJEDI_NODES = {};

    if (typeof window !== "undefined") {
      if (window.DJEDI_NODES == null) {
        window.DJEDI_NODES = {};
      }
      this._DJEDI_NODES = window.DJEDI_NODES;
    }
  }

  resetOptions() {
    this.options = makeDefaultOptions();
  }

  resetNodes() {
    if (this._batch.timeoutId != null) {
      clearTimeout(this._batch.timeoutId);
    }

    this._nodes = new Map();
    this._prefetchableNodes = new Map();
    this._renderedNodes = new Map();
    this._lastPrefetch = {};
    this._batch = makeEmptyBatch();
    this._DJEDI_NODES = {};

    if (typeof window !== "undefined") {
      window.DJEDI_NODES = this._DJEDI_NODES;
    }
  }

  get(passedNode, callback) {
    const node = this._normalizeNode(passedNode);
    const { uri } = node;
    const existingNode = this._nodes.get(uri);

    if (existingNode != null) {
      this._callback(callback, existingNode);
      return;
    }

    this._fetchMany({ [node.uri]: node.value }).then(
      () => {
        this._callback(callback, this._nodes.get(uri) || missingUriError(uri));
      },
      error => {
        this._callback(callback, error);
      }
    );
  }

  getBatched(passedNode, callback) {
    if (this.options.batchInterval <= 0) {
      this.get(passedNode, callback);
      return;
    }

    const node = this._normalizeNode(passedNode);
    const existingNode = this._nodes.get(node.uri);

    if (existingNode != null) {
      this._callback(callback, existingNode);
      return;
    }

    const previous = this._batch.queue.get(node.uri) || {
      node,
      callbacks: [],
    };
    previous.callbacks.push(callback);
    this._batch.queue.set(node.uri, previous);

    if (this._batch.timeoutId != null) {
      return;
    }

    this._batch.timeoutId = setTimeout(
      this._flushBatch.bind(this),
      this.options.batchInterval
    );
  }

  reportPrefetchableNode(passedNode) {
    const node = this._normalizeNode(passedNode);
    const previous = this._prefetchableNodes.get(node.uri);
    if (previous != null && previous.value !== node.value) {
      console.warn(
        "djedi-react: Encountered two nodes with the same URI but with different default values. Using the previous value and ignoring the next.",
        {
          uri: node.uri,
          prev: previous.value,
          next: node.value,
        }
      );
      return;
    }
    this._prefetchableNodes.set(node.uri, node);
  }

  prefetch({ filter = undefined, extra = [] } = {}) {
    const nodes = {};
    this._prefetchableNodes.forEach(node => {
      if (
        !this._nodes.has(node.uri) &&
        (filter == null ||
          filter(this._parseUri(node.uri, { applyDefaults: false })))
      ) {
        nodes[node.uri] = node.value;
      }
    });
    extra.forEach(node => {
      const uri = this._normalizeUri(node.uri);
      if (!this._nodes.has(uri)) {
        nodes[uri] = node.value;
      }
    });

    const promise =
      Object.keys(nodes).length === 0
        ? Promise.resolve({})
        : this._fetchMany(nodes);

    return promise.then(results => {
      this._lastPrefetch = results;
      return results;
    });
  }

  // Needed to pick up the results from `prefetch` after server-side rendering.
  addNodes(nodes) {
    Object.keys(nodes).forEach(uri => {
      const uriObject = this._parseUri(uri);
      const value = nodes[uri];

      if (value == null) {
        return;
      }

      const node = { uri: this._stringifyUri(uriObject), value };
      this._nodes.set(node.uri, node);

      // If the returned node URI has a version, also set the versionless URI to
      // the same node. A request for `home/text.md` can return a URI ending
      // with for example `home/text.md#1` if the user has edited the node.
      if (uriObject.version) {
        const versionlessUri = this._stringifyUri({
          ...uriObject,
          version: "",
        });

        // If the versionless URI has already been set, don’t update it so that
        // if a node re-renders it doesn’t unexpectedly change its text.
        if (!this._nodes.has(versionlessUri)) {
          this._nodes.set(versionlessUri, node);
        }
      }
    });
  }

  injectAdmin() {
    if (typeof document === "undefined") {
      return Promise.resolve(false);
    }

    const url = `${this.options.baseUrl}/embed/`;

    return unfetch(url, { credentials: "include" }).then(response => {
      if (response.status >= 200 && response.status < 400) {
        return response.text().then(html => {
          // Browsers don’t allow <script> tags inserted as part of an HTML
          // chunk to modify `document.domain`, so cut out the domain and set it
          // manually.
          const [, , domain] = DOCUMENT_DOMAIN_REGEX.exec(html) || [];
          if (domain != null) {
            document.domain = domain;
          }
          document.body.insertAdjacentHTML("beforeend", html);
          return true;
        });
      }
      if (response.status === 403) {
        return false;
      }
      return Promise.reject(
        createStatusCodeError(
          response.status,
          "200 <= status < 400 or status = 403"
        )
      );
    });
  }

  reportRenderedNode(passedNode) {
    const node = this._normalizeNode(passedNode);
    const previous = this._renderedNodes.get(node.uri);

    if (previous != null) {
      if (previous.value !== node.value) {
        console.warn(
          "djedi-react: Rendering a node with with a different default value. That default will be ignored.",
          {
            uri: node.uri,
            prev: previous.value,
            next: node.value,
          }
        );
      }
      previous.numInstances++;
      return;
    }

    this._renderedNodes.set(node.uri, {
      value: node.value,
      numInstances: 1,
    });

    this._DJEDI_NODES[this._djediNodesUri(node.uri)] = node.value;
  }

  reportRemovedNode(passedUri) {
    const uri = this._normalizeUri(passedUri);
    const previous = this._renderedNodes.get(uri);

    if (previous == null) {
      return;
    }

    previous.numInstances--;

    if (previous.numInstances <= 0) {
      this._renderedNodes.delete(uri);
      delete this._DJEDI_NODES[this._djediNodesUri(uri)];
    }
  }

  element(uri) {
    const uriObject = this._parseUri(uri);
    return {
      tag: "span",
      attributes: {
        "data-i18n": this._stringifyUri({
          ...uriObject,
          scheme: "",
          ext: "",
          version: "",
        }),
      },
    };
  }

  // Calls `callback(node)` and also updates the last return value of
  // `djedi.prefetch()`. This is really ugly but needed for server-side
  // rendering.
  _callback(callback, node) {
    if (!(node instanceof Error)) {
      this._lastPrefetch[node.uri] = node.value;
    }
    callback(node);
  }

  _parseUri(uri, { applyDefaults = true } = {}) {
    const { defaults, namespaceByScheme, separators } = this.options.uri;
    const uriObject = parseUri(uri, separators);
    return applyDefaults
      ? applyUriDefaults(uriObject, defaults, namespaceByScheme)
      : uriObject;
  }

  _stringifyUri(uriObject) {
    return stringifyUri(uriObject, this.options.uri.separators);
  }

  _normalizeUri(uri) {
    return this._stringifyUri(this._parseUri(uri));
  }

  _normalizeNode(node) {
    return {
      ...node,
      uri: this._normalizeUri(node.uri),
    };
  }

  _djediNodesUri(uri) {
    const uriObject = this._parseUri(uri);
    return this._stringifyUri({ ...uriObject, version: "" });
  }

  _flushBatch() {
    const { queue } = this._batch;

    const nodes = {};
    queue.forEach((data, uri) => {
      nodes[uri] = data.node.value;
    });

    this._batch = makeEmptyBatch();

    this._fetchMany(nodes).then(
      () => {
        queue.forEach((data, uri) => {
          const node = this._nodes.get(uri) || missingUriError(uri);
          data.callbacks.forEach(callback => {
            this._callback(callback, node);
          });
        });
      },
      error => {
        queue.forEach(data => {
          data.callbacks.forEach(callback => {
            this._callback(callback, error);
          });
        });
      }
    );
  }

  _fetchMany(nodes) {
    // `JSON.stringify` excludes keys whose values are `undefined`. Change them
    // to `null` so that all keys are sent to the backend.
    const nodesWithNull = Object.keys(nodes).reduce((result, key) => {
      const value = nodes[key];
      result[key] = value === undefined ? null : value;
      return result;
    }, {});
    return this._post("/nodes/", nodesWithNull).then(results => {
      this.addNodes(results);
      return results;
    });
  }

  _post(passedUrl, data) {
    const url = `${this.options.baseUrl}${passedUrl}`;
    const info = { method: "POST", apiUrl: url, data };

    return unfetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      // Make the body easier to read in test snapshots. It’s important to still
      // call `JSON.stringify` so we know that `data` actually can be
      // stringified.
      body:
        // istanbul ignore next
        // eslint-disable-next-line no-undef
        typeof process !== "undefined" && process.env.NODE_ENV === "test"
          ? JSON.parse(JSON.stringify(data))
          : JSON.stringify(data),
    })
      .then(
        response => {
          if (response.status >= 200 && response.status < 400) {
            return response
              .json()
              .catch(error => Promise.reject({ response, error }));
          }
          return Promise.reject({
            response,
            error: createStatusCodeError(
              response.status,
              "200 <= status < 400"
            ),
          });
        },
        passedError => {
          // In IE11 the error can be a `ProgressEvent` (I guess it’s due to how
          // the `unfetch` “polyfill” is implemented). Make sure to always
          // return `Error`s so `foo instanceof Error` checks can be used.
          // istanbul ignore next
          const error =
            passedError instanceof Error
              ? passedError
              : new Error("fetch error");
          return Promise.reject({ response: undefined, error });
        }
      )
      .catch(({ response, error }) => {
        error.message = createUpdatedErrorMessage(error, info, response);
        error.status = response == null ? -1 : response.status;

        const textPromise =
          response == null || response.bodyUsed
            ? Promise.resolve("")
            : response.text().then(
                text => text,
                // istanbul ignore next
                () => ""
              );

        return textPromise.then(text => {
          error.responseText = text;
          return Promise.reject(error);
        });
      });
  }
}

// This is a function, not a constant, since it can be mutated by the user.
function makeDefaultOptions() {
  return {
    baseUrl: "/djedi",
    batchInterval: 10, // ms
    defaultRender: state => {
      switch (state.type) {
        case "loading":
          return "Loading…";
        case "error":
          return `Failed to fetch content 😞 (${state.error.status})`;
        case "success":
          return state.content;
        // istanbul ignore next
        default:
          return null;
      }
    },
    uri: {
      defaults: {
        scheme: "i18n",
        namespace: "",
        path: "",
        ext: "txt",
        version: "",
      },
      namespaceByScheme: {
        i18n: "en-us",
        l10n: "local",
        g11n: "global",
      },
      separators: {
        scheme: "://",
        namespace: "@",
        path: "/",
        ext: ".",
        version: "#",
      },
    },
  };
}

// This is a function, not a constant, since it will be mutated.
function makeEmptyBatch() {
  return {
    timeoutId: undefined,
    queue: new Map(),
  };
}

function createStatusCodeError(status, expected) {
  const error = new Error(
    `Unexpected response status code. Got ${status} but expected ${expected}.`
  );
  error.status = status;
  return error;
}

function createUpdatedErrorMessage(error, info, response) {
  return [
    `Djedi API error for request: ${info.method} ${info.apiUrl}`,
    `RequestData sent: ${JSON.stringify(info.data, null, 2)}`,
    `Response: ${
      response ? `${response.status} ${response.statusText}` : response
    }`,
    String(error),
  ].join("\n");
}

function missingUriError(uri) {
  const error = new Error(`Missing result for node: ${uri}`);
  error.status = 1404;
  return error;
}

export default new Djedi();
<commit_msg>Add comments for the state of the Djedi class<commit_after>/* eslint-disable prefer-promise-reject-errors */

// This file does not contain anything React-specific. If need JS client for
// another framework this file could be extracted into its own package and be
// re-used.

import unfetch from "isomorphic-unfetch";

import { applyUriDefaults, parseUri, stringifyUri } from "./uri";

const DOCUMENT_DOMAIN_REGEX = /\bdocument\.domain\s*=\s*(["'])([^'"\s]+)\1/;

/*
This class fetches and caches nodes, provides global options, and keeps
`window.DJEDI_NODES` up-to-date.
*/
export class Djedi {
  constructor() {
    this.options = makeDefaultOptions();

    // `Map<uri: string, Node>`. Cache of all fetched nodes.
    this._nodes = new Map();

    // `Map<uri: string, Node>`. Everything that `reportPrefetchableNode` has
    // reported. The nodes contain default values (if any).
    this._prefetchableNodes = new Map();

    // `{ [uri: string]: string }`. The return value of the last `prefetch` call.
    // Mutated by `get` and `getBatched`. The values come from fetched nodes.
    this._lastPrefetch = {};

    // Queue for `getBatched`.
    this._batch = makeEmptyBatch();

    // `Map<uri: string, Node>`. Everything that `reportRenderedNode` has
    // reported. The nodes contain default values (if any). Used to keep
    // `_DJEDI_NODES` up-to-date.
    this._renderedNodes = new Map();

    // `{ [uri: string]: string }`. The values are default values (if any). //
    // The admin sidebar expects the following mapping of all rendered nodes on
    // the page: `window.DJEDI_NODES = { "<uri>": "<default>" }`.
    this._DJEDI_NODES = {};

    if (typeof window !== "undefined") {
      if (window.DJEDI_NODES == null) {
        window.DJEDI_NODES = {};
      }
      this._DJEDI_NODES = window.DJEDI_NODES;
    }
  }

  resetOptions() {
    this.options = makeDefaultOptions();
  }

  resetNodes() {
    if (this._batch.timeoutId != null) {
      clearTimeout(this._batch.timeoutId);
    }

    this._nodes = new Map();
    this._prefetchableNodes = new Map();
    this._lastPrefetch = {};
    this._batch = makeEmptyBatch();
    this._renderedNodes = new Map();
    this._DJEDI_NODES = {};

    if (typeof window !== "undefined") {
      window.DJEDI_NODES = this._DJEDI_NODES;
    }
  }

  get(passedNode, callback) {
    const node = this._normalizeNode(passedNode);
    const { uri } = node;
    const existingNode = this._nodes.get(uri);

    if (existingNode != null) {
      this._callback(callback, existingNode);
      return;
    }

    this._fetchMany({ [node.uri]: node.value }).then(
      () => {
        this._callback(callback, this._nodes.get(uri) || missingUriError(uri));
      },
      error => {
        this._callback(callback, error);
      }
    );
  }

  getBatched(passedNode, callback) {
    if (this.options.batchInterval <= 0) {
      this.get(passedNode, callback);
      return;
    }

    const node = this._normalizeNode(passedNode);
    const existingNode = this._nodes.get(node.uri);

    if (existingNode != null) {
      this._callback(callback, existingNode);
      return;
    }

    const previous = this._batch.queue.get(node.uri) || {
      node,
      callbacks: [],
    };
    previous.callbacks.push(callback);
    this._batch.queue.set(node.uri, previous);

    if (this._batch.timeoutId != null) {
      return;
    }

    this._batch.timeoutId = setTimeout(
      this._flushBatch.bind(this),
      this.options.batchInterval
    );
  }

  reportPrefetchableNode(passedNode) {
    const node = this._normalizeNode(passedNode);
    const previous = this._prefetchableNodes.get(node.uri);
    if (previous != null && previous.value !== node.value) {
      console.warn(
        "djedi-react: Encountered two nodes with the same URI but with different default values. Using the previous value and ignoring the next.",
        {
          uri: node.uri,
          prev: previous.value,
          next: node.value,
        }
      );
      return;
    }
    this._prefetchableNodes.set(node.uri, node);
  }

  prefetch({ filter = undefined, extra = [] } = {}) {
    const nodes = {};
    this._prefetchableNodes.forEach(node => {
      if (
        !this._nodes.has(node.uri) &&
        (filter == null ||
          filter(this._parseUri(node.uri, { applyDefaults: false })))
      ) {
        nodes[node.uri] = node.value;
      }
    });
    extra.forEach(node => {
      const uri = this._normalizeUri(node.uri);
      if (!this._nodes.has(uri)) {
        nodes[uri] = node.value;
      }
    });

    const promise =
      Object.keys(nodes).length === 0
        ? Promise.resolve({})
        : this._fetchMany(nodes);

    return promise.then(results => {
      this._lastPrefetch = results;
      return results;
    });
  }

  // Needed to pick up the results from `prefetch` after server-side rendering.
  addNodes(nodes) {
    Object.keys(nodes).forEach(uri => {
      const uriObject = this._parseUri(uri);
      const value = nodes[uri];

      if (value == null) {
        return;
      }

      const node = { uri: this._stringifyUri(uriObject), value };
      this._nodes.set(node.uri, node);

      // If the returned node URI has a version, also set the versionless URI to
      // the same node. A request for `home/text.md` can return a URI ending
      // with for example `home/text.md#1` if the user has edited the node.
      if (uriObject.version) {
        const versionlessUri = this._stringifyUri({
          ...uriObject,
          version: "",
        });

        // If the versionless URI has already been set, don’t update it so that
        // if a node re-renders it doesn’t unexpectedly change its text.
        if (!this._nodes.has(versionlessUri)) {
          this._nodes.set(versionlessUri, node);
        }
      }
    });
  }

  injectAdmin() {
    if (typeof document === "undefined") {
      return Promise.resolve(false);
    }

    const url = `${this.options.baseUrl}/embed/`;

    return unfetch(url, { credentials: "include" }).then(response => {
      if (response.status >= 200 && response.status < 400) {
        return response.text().then(html => {
          // Browsers don’t allow <script> tags inserted as part of an HTML
          // chunk to modify `document.domain`, so cut out the domain and set it
          // manually.
          const [, , domain] = DOCUMENT_DOMAIN_REGEX.exec(html) || [];
          if (domain != null) {
            document.domain = domain;
          }
          document.body.insertAdjacentHTML("beforeend", html);
          return true;
        });
      }
      if (response.status === 403) {
        return false;
      }
      return Promise.reject(
        createStatusCodeError(
          response.status,
          "200 <= status < 400 or status = 403"
        )
      );
    });
  }

  reportRenderedNode(passedNode) {
    const node = this._normalizeNode(passedNode);
    const previous = this._renderedNodes.get(node.uri);

    if (previous != null) {
      if (previous.value !== node.value) {
        console.warn(
          "djedi-react: Rendering a node with with a different default value. That default will be ignored.",
          {
            uri: node.uri,
            prev: previous.value,
            next: node.value,
          }
        );
      }
      previous.numInstances++;
      return;
    }

    this._renderedNodes.set(node.uri, {
      value: node.value,
      numInstances: 1,
    });

    this._DJEDI_NODES[this._djediNodesUri(node.uri)] = node.value;
  }

  reportRemovedNode(passedUri) {
    const uri = this._normalizeUri(passedUri);
    const previous = this._renderedNodes.get(uri);

    if (previous == null) {
      return;
    }

    previous.numInstances--;

    if (previous.numInstances <= 0) {
      this._renderedNodes.delete(uri);
      delete this._DJEDI_NODES[this._djediNodesUri(uri)];
    }
  }

  element(uri) {
    const uriObject = this._parseUri(uri);
    return {
      tag: "span",
      attributes: {
        "data-i18n": this._stringifyUri({
          ...uriObject,
          scheme: "",
          ext: "",
          version: "",
        }),
      },
    };
  }

  // Calls `callback(node)` and also updates the last return value of
  // `djedi.prefetch()`. This is really ugly but needed for server-side
  // rendering.
  _callback(callback, node) {
    if (!(node instanceof Error)) {
      this._lastPrefetch[node.uri] = node.value;
    }
    callback(node);
  }

  _parseUri(uri, { applyDefaults = true } = {}) {
    const { defaults, namespaceByScheme, separators } = this.options.uri;
    const uriObject = parseUri(uri, separators);
    return applyDefaults
      ? applyUriDefaults(uriObject, defaults, namespaceByScheme)
      : uriObject;
  }

  _stringifyUri(uriObject) {
    return stringifyUri(uriObject, this.options.uri.separators);
  }

  _normalizeUri(uri) {
    return this._stringifyUri(this._parseUri(uri));
  }

  _normalizeNode(node) {
    return {
      ...node,
      uri: this._normalizeUri(node.uri),
    };
  }

  _djediNodesUri(uri) {
    const uriObject = this._parseUri(uri);
    return this._stringifyUri({ ...uriObject, version: "" });
  }

  _flushBatch() {
    const { queue } = this._batch;

    const nodes = {};
    queue.forEach((data, uri) => {
      nodes[uri] = data.node.value;
    });

    this._batch = makeEmptyBatch();

    this._fetchMany(nodes).then(
      () => {
        queue.forEach((data, uri) => {
          const node = this._nodes.get(uri) || missingUriError(uri);
          data.callbacks.forEach(callback => {
            this._callback(callback, node);
          });
        });
      },
      error => {
        queue.forEach(data => {
          data.callbacks.forEach(callback => {
            this._callback(callback, error);
          });
        });
      }
    );
  }

  _fetchMany(nodes) {
    // `JSON.stringify` excludes keys whose values are `undefined`. Change them
    // to `null` so that all keys are sent to the backend.
    const nodesWithNull = Object.keys(nodes).reduce((result, key) => {
      const value = nodes[key];
      result[key] = value === undefined ? null : value;
      return result;
    }, {});
    return this._post("/nodes/", nodesWithNull).then(results => {
      this.addNodes(results);
      return results;
    });
  }

  _post(passedUrl, data) {
    const url = `${this.options.baseUrl}${passedUrl}`;
    const info = { method: "POST", apiUrl: url, data };

    return unfetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      // Make the body easier to read in test snapshots. It’s important to still
      // call `JSON.stringify` so we know that `data` actually can be
      // stringified.
      body:
        // istanbul ignore next
        // eslint-disable-next-line no-undef
        typeof process !== "undefined" && process.env.NODE_ENV === "test"
          ? JSON.parse(JSON.stringify(data))
          : JSON.stringify(data),
    })
      .then(
        response => {
          if (response.status >= 200 && response.status < 400) {
            return response
              .json()
              .catch(error => Promise.reject({ response, error }));
          }
          return Promise.reject({
            response,
            error: createStatusCodeError(
              response.status,
              "200 <= status < 400"
            ),
          });
        },
        passedError => {
          // In IE11 the error can be a `ProgressEvent` (I guess it’s due to how
          // the `unfetch` “polyfill” is implemented). Make sure to always
          // return `Error`s so `foo instanceof Error` checks can be used.
          // istanbul ignore next
          const error =
            passedError instanceof Error
              ? passedError
              : new Error("fetch error");
          return Promise.reject({ response: undefined, error });
        }
      )
      .catch(({ response, error }) => {
        error.message = createUpdatedErrorMessage(error, info, response);
        error.status = response == null ? -1 : response.status;

        const textPromise =
          response == null || response.bodyUsed
            ? Promise.resolve("")
            : response.text().then(
                text => text,
                // istanbul ignore next
                () => ""
              );

        return textPromise.then(text => {
          error.responseText = text;
          return Promise.reject(error);
        });
      });
  }
}

// This is a function, not a constant, since it can be mutated by the user.
function makeDefaultOptions() {
  return {
    baseUrl: "/djedi",
    batchInterval: 10, // ms
    defaultRender: state => {
      switch (state.type) {
        case "loading":
          return "Loading…";
        case "error":
          return `Failed to fetch content 😞 (${state.error.status})`;
        case "success":
          return state.content;
        // istanbul ignore next
        default:
          return null;
      }
    },
    uri: {
      defaults: {
        scheme: "i18n",
        namespace: "",
        path: "",
        ext: "txt",
        version: "",
      },
      namespaceByScheme: {
        i18n: "en-us",
        l10n: "local",
        g11n: "global",
      },
      separators: {
        scheme: "://",
        namespace: "@",
        path: "/",
        ext: ".",
        version: "#",
      },
    },
  };
}

// This is a function, not a constant, since it will be mutated.
function makeEmptyBatch() {
  return {
    timeoutId: undefined,
    queue: new Map(),
  };
}

function createStatusCodeError(status, expected) {
  const error = new Error(
    `Unexpected response status code. Got ${status} but expected ${expected}.`
  );
  error.status = status;
  return error;
}

function createUpdatedErrorMessage(error, info, response) {
  return [
    `Djedi API error for request: ${info.method} ${info.apiUrl}`,
    `RequestData sent: ${JSON.stringify(info.data, null, 2)}`,
    `Response: ${
      response ? `${response.status} ${response.statusText}` : response
    }`,
    String(error),
  ].join("\n");
}

function missingUriError(uri) {
  const error = new Error(`Missing result for node: ${uri}`);
  error.status = 1404;
  return error;
}

export default new Djedi();
<|endoftext|>


<commit_before>/**
 * encode-setting.js
 * エンコード事前設定を行う
 *
 * Created by cracked-cdr
 */

/*
 下の方にある関数定義でファイル名ごとのエンコード設定が行えます
*/

// テンプレート
const SIZE = {
    SOURCE  : '',   // ソースファイルに従う
    SIZE1920: '-w 1920 -l 1080',
    SIZE1440: '-w 1440 -l 1080',
    SIZE1280: '-w 1280 -l 720',
    SIZE960 : '-w 960 -l 720',
    SIZE640 : '-w 640 -l 480'
};
const AUDIO = {
    SOURCE : '',    // ソースファイル音声のコピー
    RATE128: '128',
    RATE256: '256'
};
const FRAME_RATE = {
    SOURCE: '', // ソースファイルに従う
    RATE24: '--rate 23.976',
    RATE30: '--rate 29.97',
    RATE60: '--rate 59.94'
};

// エンコード設定本体
// ここで記述されている内容をデフォルト値として、下の関数内の変更が適用されます
var encSettings = {
    quality_or_rate : '-q 24',              // エンコード画質。数値が下がるほど画質が上がる('-b 数値' にするとkbps指定)
    video_size      : SIZE.SOURCE,          // 動画のサイズ
    crop            : '--crop 0:0:0:0',     // 動画から上下左右の範囲をカット(デフォルトはカットしない)
    anamorphic      : '--loose-anamorphic', // 動画の内部アスペクト比設定(デフォルトは元動画に従う)
    audio_rate      : AUDIO.SOURCE,         // 音声のビットレート(デフォルトはソースからコピーする)
    start_at_sec    : '0.0',                // 先頭何秒をカットするか
    chapter_skip_sec: '0.0',                // エンコード後の先頭何秒間をチャプター設定しないようにするか
    frame_rate      : FRAME_RATE.SOURCE,    // 動画のフレームレート(デフォルトはソースに従う)
    detelecine      : '--detelecine="1:1:4:4:0:0:-1"',  // 逆テレシネ
    decomb          : '--decomb="7:2:6:9:80:16:16:10:20:20:4:2:50:24:1:-1"',    // Decomb(柔軟なインターレース除去)
    other_options   : ''                    // その他Handbrakeオプションを指定したい場合はここにまとめて記述
};

module.exports.encSettings = encSettings;

/*
 こちらの関数内で番組ごとのエンコード設定を記述できます
 fileNameにはファイル名が渡されるので、それをもとにencSettingsの値を変更してください
 (どう変更できるかはHandbrakeCLI.exeのヘルプ等を参照)
 わからない場合や不要な場合はそのままにするか関数内を "return encSettings;" の行のみにしてください
*/
module.exports.createEncodeSettings = function(fileName) {

    // Example
    // fileNameに"みんなのうた"が含まれていれば以下の設定にする
    // (記述していないものはデフォルト値のまま)
    if (~fileName.indexOf('みんなのうた')) {
        encSettings.quality_or_rate  = '-q 20';
        encSettings.video_size       = SIZE.SIZE1280;
        encSettings.start_at_sec     = '15.0';
        encSettings.chapter_skip_sec = '1.0';
        encSettings.frame_rate       = FRAME_RATE.RATE30;
    }

    return encSettings;
}
<commit_msg>セミコロン修正<commit_after>/**
 * encode-setting.js
 * エンコード事前設定を行う
 *
 * Created by cracked-cdr
 */

/*
 下の方にある関数定義でファイル名ごとのエンコード設定が行えます
*/

// テンプレート
const SIZE = {
    SOURCE  : '',   // ソースファイルに従う
    SIZE1920: '-w 1920 -l 1080',
    SIZE1440: '-w 1440 -l 1080',
    SIZE1280: '-w 1280 -l 720',
    SIZE960 : '-w 960 -l 720',
    SIZE640 : '-w 640 -l 480'
};
const AUDIO = {
    SOURCE : '',    // ソースファイル音声のコピー
    RATE128: '128',
    RATE256: '256'
};
const FRAME_RATE = {
    SOURCE: '', // ソースファイルに従う
    RATE24: '--rate 23.976',
    RATE30: '--rate 29.97',
    RATE60: '--rate 59.94'
};

// エンコード設定本体
// ここで記述されている内容をデフォルト値として、下の関数内の変更が適用されます
var encSettings = {
    quality_or_rate : '-q 24',              // エンコード画質。数値が下がるほど画質が上がる('-b 数値' にするとkbps指定)
    video_size      : SIZE.SOURCE,          // 動画のサイズ
    crop            : '--crop 0:0:0:0',     // 動画から上下左右の範囲をカット(デフォルトはカットしない)
    anamorphic      : '--loose-anamorphic', // 動画の内部アスペクト比設定(デフォルトは元動画に従う)
    audio_rate      : AUDIO.SOURCE,         // 音声のビットレート(デフォルトはソースからコピーする)
    start_at_sec    : '0.0',                // 先頭何秒をカットするか
    chapter_skip_sec: '0.0',                // エンコード後の先頭何秒間をチャプター設定しないようにするか
    frame_rate      : FRAME_RATE.SOURCE,    // 動画のフレームレート(デフォルトはソースに従う)
    detelecine      : '--detelecine="1:1:4:4:0:0:-1"',  // 逆テレシネ
    decomb          : '--decomb="7:2:6:9:80:16:16:10:20:20:4:2:50:24:1:-1"',    // Decomb(柔軟なインターレース除去)
    other_options   : ''                    // その他Handbrakeオプションを指定したい場合はここにまとめて記述
};

module.exports.encSettings = encSettings;

/*
 こちらの関数内で番組ごとのエンコード設定を記述できます
 fileNameにはファイル名が渡されるので、それをもとにencSettingsの値を変更してください
 (どう変更できるかはHandbrakeCLI.exeのヘルプ等を参照)
 わからない場合や不要な場合はそのままにするか関数内を "return encSettings;" の行のみにしてください
*/
module.exports.createEncodeSettings = function(fileName) {

    // Example
    // fileNameに"みんなのうた"が含まれていれば以下の設定にする
    // (記述していないものはデフォルト値のまま)
    if (~fileName.indexOf('みんなのうた')) {
        encSettings.quality_or_rate  = '-q 20';
        encSettings.video_size       = SIZE.SIZE1280;
        encSettings.start_at_sec     = '15.0';
        encSettings.chapter_skip_sec = '1.0';
        encSettings.frame_rate       = FRAME_RATE.RATE30;
    }

    return encSettings;
};
<|endoftext|>


<commit_before>/**
 * Created by liekkas on 16/2/19.
 */
import React, { PropTypes } from 'react'
import { ECharts, Panel, Footer } from '../../components'
import style from './style.scss'
//import echarts from 'echarts'
import _ from 'lodash'
import {browserHistory} from 'react-router';

let data = []
let data2 = []
const provinces = ['江苏','湖北','安徽','湖南','河北','山东','河南']
//
//湖南省
//长沙：有线电视用户57万户
//湘潭：有线电视用户21万户
//浏阳：有线电视用户20万户
//岳阳：有线电视用户19万户
//益阳：有线电视用户18万户
//---------------------------
//  河北省
//唐山：有线电视用户26万户
//承德：有线电视用户13万户
//沧州：有线电视用户15万户
//秦皇岛：有线电视用户34万户
//---------------------------
//  山东省
//威海：有线电视用户83万户
//---------------------------
//  河南省
//周口：有线电视用户8万户

const details = {
  湖南: [
    {name: '长沙', value: '57'},
    {name: '湘潭', value: '21'},
    {name: '浏阳', value: '20'},
    {name: '岳阳', value: '19'},
    {name: '益阳', value: '18'},
  ],
  河北: [
    {name: '唐山', value: '26'},
    {name: '承德', value: '13'},
    {name: '沧州', value: '15'},
    {name: '秦皇岛', value: '34'},
  ],
  山东: [
    {name: '威海', value: '83'},
  ],
  河南: [
    {name: '周口', value: '8'},
  ]
}

const v1 = ['1885','870','447','135','88','83','8']
_.forEach(provinces, function (province, index) {
  data.push({name: province, value: v1[index]})
})

var geoCoordMap = {
  '河北':[114.48,38.03],
  '河南':[113.65,34.76],
  '安徽':[117.27,31.86],
  '江苏':[119.88,34.04],
  '山东':[117,36.65],
  '湖北':[114.31,30.52],
  '湖南':[113,28.21],
};

var convertData = function (data) {
  var res = [];
  for (var i = 0; i < data.length; i++) {
    var geoCoord = geoCoordMap[data[i].name];
    if (geoCoord) {
      res.push({
        name: data[i].name,
        value: geoCoord.concat(data[i].value)
      });
    }
  }
  return res;
};

function getDetail(name) {
  if (!details.hasOwnProperty(name)) return ''

  const cities = details[name]
  let result = '
----------------
'
  for (let i = 0; i < cities.length; i++) {
    result += cities[i].name + ': ' + cities[i].value + '万户
'
  }
  return result
}
const mapOption = {
  textStyle: {
    color: '#fff',
  },
  title: {
    text: '中信国安广视全国电视用户数据资产情况',
//    text: '中国电信全国4G用户数据资产情况',
//    subtext: 'data from PM25.in',
    left: 'center',
    top: 50,
    textStyle: {
      color: '#c0d5ff',
      fontSize: 26
    }
  },
  visualMap: {
    show: true,
//    color: ['#e94f26','#ffb74d'],
    inRange: {
//      color: ['#ffb74d', '#ffa726', '#ff9800', '#e94f26'],
      color: ['#ffb74d','#e94f26'],
      symbolSize: [30, 100]
    },
    outOfRange: {
      color: ['#121122', 'rgba(3,4,5,0.4)', 'red'],
      symbolSize: [30, 100]
    },
    min: 0,
    max: 2000,
    formatter: '{value}万户',
//    left: 'right',
//    top: 'bottom',
    bottom: 30,
    right: 30,
    text:['高','低'],           // 文本，默认为数值文本
    calculable : true,
    textStyle: {
      color: '#c0d5ff',
//      fontSize: 26
    }
  },
  tooltip : {
    padding: 10,
    trigger: 'item',
    formatter: (item) => (isNaN(item.value)
      ? null
//      : item.seriesName + '
' + item.name + ': - ' + item.value + '万户'),
      : '有线电视用户数
' + item.name + '省: ' + item.value + '万户' + getDetail(item.name)),
  },
  legend: {
    show: false,
    orient: 'vertical',
    y: 'top',
    x:'right',
    data:['pm2.5'],
    textStyle: {
      color: '#fff'
    }
  },
  series : [
    {
      name: '用户覆盖',
      type: 'map',
      top: 96,
      mapType: 'china',
      label: {
        normal: {
          show: true,
          textStyle: {
            color: '#B8E6FE'
          }
        },
        emphasis: {
          show: true,
          textStyle: {
            color: '#B8E6FE',
            fontWeight: 'bold',
//            color: '#000'
          }
        },

      },
      roam: false,
      itemStyle: {
        normal: {
          areaColor: '#6cA9EB',
          borderColor: '#447cca',
          borderWidth: 1,
        },
        emphasis: {
          //LightGreen
//          areaColor: '#9ccc65',
//          shadowColor: '#8bc34a',
          //LightBlue
          areaColor: '#03a9f4',
          shadowColor: '#039be5',
          //Green
//          areaColor: '#42bd41',
//          shadowColor: '#2bafab',
          //Lime
//          areaColor: '#d4e157',
//          shadowColor: '#cddc39',
          borderColor: '#FFF',
          shadowBlur: 10,
          shadowOffsetX: 10,
          shadowOffsetY: 10,
          opacity: 0.8,
        }
      },

      data,
    },
//
  ]
}

let barData = []
const reProvinces = provinces
//const d1 = [1885,870,447,135,88,83,8]
const d2 = [1885,870,447,135,88,83,8].reverse()
//const d2 = [1365,800,405,126,87,82,4]
const d1 = [1365,800,405,126,87,82,4].reverse()
const types = ['数字用户数(万户)','有线电视用户数(万户)']
for (let j = 0; j < types.length; j++) {
  let d = []
  for (let k = 0; k < reProvinces.length; k++) {
    d.push(j === 0 ? d1[k] : d2[k])
  }

  barData.push({
    name: types[j],
    type:'bar',
    stack: '总量',
//    itemStyle : { normal: {label : {show: true, position: j === 0 ? 'insideLeft' : 'insideLeft'}} },
    itemStyle : {
      normal: {
        label : {
          show: true,
          position: j === 0 ? 'insideRight' : 'right',
          textStyle: {
            color: '#FFF',
          }
        }
      }
    },
//    barWidth: 20,
//    barGap: '40%',
//    barCategoryGap: '40%',
    data:d
  })
}

const barOption = {
//  backgroundColor: '#fff',
//  color: ['#c23531','#40c4ff'],
  color: ['#396cbd','#c94638'],

//  tooltip : {
//    show: false,
////    trigger: 'axis',
//////    formatter: "{a}
{b} : {c} ({d}万)",
////    axisPointer : {            // 坐标轴指示器，坐标轴触发有效
////      type : 'shadow'        // 默认为直线，可选为：'line' | 'shadow'
////    }
//  },
  legend: {
    data:['数字用户数(万户)','有线电视用户数(万户)'],
//    y: 'top',
//    x: 'center',
//    itemHeight: 20,
//    itemWidth: 25,
    selectedMode: false,
    textStyle: {
      color: '#5fa4d9',
      fontSize: 14,
    },
  },
  grid: {
    left: 50,
//    right: '4%',
    bottom: '4%',
    top: 40,
//    containLabel: true
  },
  xAxis : [
    {
//      type : 'value',
      type : 'log',
      axisLine: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisTick: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisLabel: {
        show: false,
        textStyle: {
          color: '#314656',
        }
      },
      splitLine: {
        show: false,
      },
    }
  ],
  yAxis : [
    {
      type : 'category',
//      data : provinces,
      data : provinces.reverse(),
      axisLine: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisTick: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisLabel: {
        right: 20,
        textStyle: {
          color: '#5fa4d9',
        }
      },
      splitLine: {
        show: false,
      },
    }
  ],
  series : barData
};

class Home extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      mapType: 'china',
    }
  }

  onMapClick(e) {
    console.log('>>> 我靠',e)
//    this.props.history.replaceState(null, '/tvOverview');

    browserHistory.push('/tvOverview');
  }
//

  render() {
    const { foo } = this.props
    return (

        <ECharts config={{eventType: 'click', eventHandler: this.onMapClick}} option={mapOption}/>

          <ECharts option={barOption}/>

        © 2016 All Rights Reserved 中信国安广视网络有限公司 版权所有

    )
  }
}

Home.propTypes = {
  foo: PropTypes.string.isRequired,
}
Home.defaultProps = {
  foo: 'Home',
}

export default Home
<commit_msg>adjustment<commit_after>/**
 * Created by liekkas on 16/2/19.
 */
import React, { PropTypes } from 'react'
import { ECharts, Panel, Footer } from '../../components'
import style from './style.scss'
//import echarts from 'echarts'
import _ from 'lodash'
import {browserHistory} from 'react-router';

let data = []
let data2 = []
const provinces = ['江苏','湖北','安徽','湖南','河北','山东','河南']
//
//湖南省
//长沙：有线电视用户57万户
//湘潭：有线电视用户21万户
//浏阳：有线电视用户20万户
//岳阳：有线电视用户19万户
//益阳：有线电视用户18万户
//---------------------------
//  河北省
//唐山：有线电视用户26万户
//承德：有线电视用户13万户
//沧州：有线电视用户15万户
//秦皇岛：有线电视用户34万户
//---------------------------
//  山东省
//威海：有线电视用户83万户
//---------------------------
//  河南省
//周口：有线电视用户8万户

const details = {
  湖南: [
    {name: '长沙', value: '57'},
    {name: '湘潭', value: '21'},
    {name: '浏阳', value: '20'},
    {name: '岳阳', value: '19'},
    {name: '益阳', value: '18'},
  ],
  河北: [
    {name: '唐山', value: '26'},
    {name: '承德', value: '13'},
    {name: '沧州', value: '15'},
    {name: '秦皇岛', value: '34'},
  ],
  山东: [
    {name: '威海', value: '83'},
  ],
  河南: [
    {name: '周口', value: '8'},
  ]
}

const v1 = ['1885','870','447','135','88','83','8']
_.forEach(provinces, function (province, index) {
  data.push({name: province, value: v1[index]})
})

var geoCoordMap = {
  '河北':[114.48,38.03],
  '河南':[113.65,34.76],
  '安徽':[117.27,31.86],
  '江苏':[119.88,34.04],
  '山东':[117,36.65],
  '湖北':[114.31,30.52],
  '湖南':[113,28.21],
};

var convertData = function (data) {
  var res = [];
  for (var i = 0; i < data.length; i++) {
    var geoCoord = geoCoordMap[data[i].name];
    if (geoCoord) {
      res.push({
        name: data[i].name,
        value: geoCoord.concat(data[i].value)
      });
    }
  }
  return res;
};

function getDetail(name) {
  if (!details.hasOwnProperty(name)) return ''

  const cities = details[name]
  let result = '
----------------
'
  for (let i = 0; i < cities.length; i++) {
    result += cities[i].name + ': ' + cities[i].value + '万户
'
  }
  return result
}
const mapOption = {
  textStyle: {
    color: '#fff',
  },
  title: {
    text: '中信国安广视全国电视用户数据资产情况',
//    text: '中国电信全国4G用户数据资产情况',
//    subtext: 'data from PM25.in',
    left: 'center',
    top: 50,
    textStyle: {
      color: '#c0d5ff',
      fontSize: 26
    }
  },
  visualMap: {
    show: true,
//    color: ['#e94f26','#ffb74d'],
    inRange: {
//      color: ['#ffb74d', '#ffa726', '#ff9800', '#e94f26'],
      color: ['#ffb74d','#e94f26'],
      symbolSize: [30, 100]
    },
//    outOfRange: {
//      color: ['#121122', 'rgba(3,4,5,0.4)', 'red'],
//      symbolSize: [30, 100]
//    },
    min: 0,
    max: 2000,
    formatter: '{value}万户',
//    left: 'right',
//    top: 'bottom',
    bottom: 30,
    right: 30,
    text:['高','低'],           // 文本，默认为数值文本
    calculable : true,
    textStyle: {
      color: '#c0d5ff',
//      fontSize: 26
    }
  },
  tooltip : {
    padding: 10,
    trigger: 'item',
    formatter: (item) => (isNaN(item.value)
      ? null
//      : item.seriesName + '
' + item.name + ': - ' + item.value + '万户'),
      : '有线电视用户数
' + item.name + '省: ' + item.value + '万户' + getDetail(item.name)),
  },
  legend: {
    show: false,
    orient: 'vertical',
    y: 'top',
    x:'right',
    data:['pm2.5'],
    textStyle: {
      color: '#fff'
    }
  },
  series : [
    {
      name: '用户覆盖',
      type: 'map',
      top: 96,
      mapType: 'china',
      label: {
        normal: {
          show: true,
          textStyle: {
            color: '#B8E6FE'
          }
        },
        emphasis: {
          show: true,
          textStyle: {
            color: '#B8E6FE',
            fontWeight: 'bold',
//            color: '#000'
          }
        },

      },
      roam: false,
      itemStyle: {
        normal: {
          areaColor: '#6cA9EB',
          borderColor: '#447cca',
          borderWidth: 1,
        },
        emphasis: {
          //LightGreen
//          areaColor: '#9ccc65',
//          shadowColor: '#8bc34a',
          //LightBlue
          areaColor: '#03a9f4',
          shadowColor: '#039be5',
          //Green
//          areaColor: '#42bd41',
//          shadowColor: '#2bafab',
          //Lime
//          areaColor: '#d4e157',
//          shadowColor: '#cddc39',
          borderColor: '#FFF',
          shadowBlur: 10,
          shadowOffsetX: 10,
          shadowOffsetY: 10,
          opacity: 0.8,
        }
      },

      data,
    },
//
  ]
}

let barData = []
const reProvinces = provinces
//const d1 = [1885,870,447,135,88,83,8]
const d2 = [1885,870,447,135,88,83,8].reverse()
//const d2 = [1365,800,405,126,87,82,4]
const d1 = [1365,800,405,126,87,82,4].reverse()
const types = ['数字用户数(万户)','有线电视用户数(万户)']
for (let j = 0; j < types.length; j++) {
  let d = []
  for (let k = 0; k < reProvinces.length; k++) {
    d.push(j === 0 ? d1[k] : d2[k])
  }

  barData.push({
    name: types[j],
    type:'bar',
    stack: '总量',
//    itemStyle : { normal: {label : {show: true, position: j === 0 ? 'insideLeft' : 'insideLeft'}} },
    itemStyle : {
      normal: {
        label : {
          show: true,
          position: j === 0 ? 'insideRight' : 'right',
          textStyle: {
            color: '#FFF',
          }
        }
      }
    },
//    barWidth: 20,
//    barGap: '40%',
//    barCategoryGap: '40%',
    data:d
  })
}

const barOption = {
//  backgroundColor: '#fff',
//  color: ['#c23531','#40c4ff'],
  color: ['#396cbd','#c94638'],

//  tooltip : {
//    show: false,
////    trigger: 'axis',
//////    formatter: "{a}
{b} : {c} ({d}万)",
////    axisPointer : {            // 坐标轴指示器，坐标轴触发有效
////      type : 'shadow'        // 默认为直线，可选为：'line' | 'shadow'
////    }
//  },
  legend: {
    data:['数字用户数(万户)','有线电视用户数(万户)'],
//    y: 'top',
//    x: 'center',
//    itemHeight: 20,
//    itemWidth: 25,
    selectedMode: false,
    textStyle: {
      color: '#5fa4d9',
      fontSize: 14,
    },
  },
  grid: {
    left: 50,
//    right: '4%',
    bottom: '4%',
    top: 40,
//    containLabel: true
  },
  xAxis : [
    {
//      type : 'value',
      type : 'log',
      axisLine: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisTick: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisLabel: {
        show: false,
        textStyle: {
          color: '#314656',
        }
      },
      splitLine: {
        show: false,
      },
    }
  ],
  yAxis : [
    {
      type : 'category',
//      data : provinces,
      data : provinces.reverse(),
      axisLine: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisTick: {
        show: false,
        lineStyle: {
          color: 'rgba(255,255,255,0.8)',
        }
      },
      axisLabel: {
        right: 20,
        textStyle: {
          color: '#5fa4d9',
        }
      },
      splitLine: {
        show: false,
      },
    }
  ],
  series : barData
};

class Home extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      mapType: 'china',
    }
  }

  onMapClick(e) {
//    console.log('>>> 我靠',e)
//    this.props.history.replaceState(null, '/tvOverview');

    browserHistory.push('/tvOverview');
  }
//

  render() {
    const { foo } = this.props
    return (

        <ECharts config={{eventType: 'click', eventHandler: this.onMapClick}} option={mapOption}/>

          <ECharts option={barOption}/>

        © 2016 All Rights Reserved 中信国安广视网络有限公司 版权所有

    )
  }
}

Home.propTypes = {
  foo: PropTypes.string.isRequired,
}
Home.defaultProps = {
  foo: 'Home',
}

export default Home
<|endoftext|>


<commit_before>(function(){

var cont = angular.module('poll-princeton.controllers');

cont.controller('feedController', function ($scope, $filter, $http, $location) {

  document.title = 'Feed | PollPrinceton'

  $('body').css('padding-top', '50px');

  $scope.fetchedDevs = false;
  $scope.fetchedPolls = false;
  $scope.fetching = true;
  $scope.noMorePolls = false;
  $scope.polls = [];
  $scope.currentPolls = 0;
  $scope.sort = "popular";

  var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

  $scope.fetchDevs = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/devs').success(function (data, status, headers, config) {
      $scope.devs = data;
      $scope.fetchedDevs = true;
    });
  };

  $scope.deletePolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/delete/all').success(function (data, status, headers, config) {
    });
  };

  $scope.fetchAllPolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/all').success(function (data, status, headers, config) {
      $scope.polls = data;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i];
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
      }
      $scope.fetchedPolls = true;
    });
  };

  //Get call returns an array of objects containing three things: pollData (a complete poll with all data),
  //userVote (true for up, false for down) and userResponse (however Ryan implemented it. -1 for no response I think)
  $scope.fetch10Best = function () {
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/popular/'
        + user + '/' + $scope.currentPolls + '/false').success(function (data, status, headers, config) {
      $scope.fetching = false;
      if (data.err) {
        $scope.noMorePolls = true;
        return;
      }
      $scope.polls = data;
      $scope.currentPolls = 10;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i].pollData;
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
        thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
          return a + b;
        });
      }
      $scope.fetchedPolls = true;
    });
  };

  $scope.fetchNext10Best = function () {
    if ($scope.noMorePolls) return;
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/popular/'
        + user + '/' + $scope.currentPolls + '/false')
      .success(function (data, status, headers, config) {
        $scope.fetching = false;
        if (data.err) {
          $scope.noMorePolls = true;
          return;
        }
        var polls = data;
        $scope.currentPolls += 10;
        for (var i in polls) {
          var thisPoll = polls[i].pollData;
          var date = new Date(thisPoll.time);
          thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
          thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
            return a + b;
          });
          $scope.polls.push(polls[i]);
        }
      });
  };

  $scope.fetch10New = function () {
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/newest/'
        + user + '/' + $scope.currentPolls + '/false').success(function (data, status, headers, config) {
      $scope.fetching = false;
      if (data.err) {
        $scope.noMorePolls = true;
        return;
      }
      $scope.polls = data;
      $scope.currentPolls = 10;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i].pollData;
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
        thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
          return a + b;
        });
      }
      $scope.fetchedPolls = true;
    });
  };

  $scope.fetchNext10New = function () {
    if ($scope.noMorePolls) return;
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/newest/'
        + user + '/' + $scope.currentPolls + '/false')
      .success(function (data, status, headers, config) {
        $scope.fetching = false;
        if (data.err) {
          $scope.noMorePolls = true;
          return;
        }
        var polls = data;
        $scope.currentPolls += 10;
        for (var i in polls) {
          var thisPoll = polls[i].pollData;
          var date = new Date(thisPoll.time);
          thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
          thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
            return a + b;
          });
          $scope.polls.push(polls[i]);
        }
      });
  };

  $scope.fetchMore = function () {
    if ($scope.sort === 'popular') $scope.fetchNext10Best();
    else $scope.fetchNext10New();
  };

  $scope.upVote = function (pid, idx) {
    var pkg = {};
    var user = localStorage.getItem('netid');
    pkg.upOrDown = true;
    pkg.pollID = pid;
    pkg.netid = user;
    $http.post('/ppapi/polls/vote', pkg)
      .success(function (data, status, headers, config) {
        $scope.polls[idx].pollData.score = data.score;
        $scope.polls[idx].userVote = data.userVote;
      })
      .error(function (data, status, headers, config) {
        alert('Something went wrong. Please try to submit again in a few moments.')
        $('.vote-submit').attr('disabled', false);
      });
  };

  $scope.downVote = function (pid, idx) {
    var pkg = {};
    var user = localStorage.getItem('netid');
    pkg.upOrDown = false;
    pkg.pollID = pid;
    pkg.netid = user;
    $http.post('/ppapi/polls/vote', pkg)
      .success(function (data, status, headers, config) {
        $scope.polls[idx].pollData.score = data.score;
        $scope.polls[idx].userVote = data.userVote;
      })
      .error(function (data, status, headers, config) {
        alert('Something went wrong. Please try to submit again in a few moments.')
        $('.vote-submit').attr('disabled', false);
      });
  };

  $scope.deletePolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/delete/all').success(function (data, status, headers, config) {
    });
  };

  $scope.sortPopular = function () {
    if ($scope.sort !== 'popular') {
      $('.polls').fadeOut(function () {
        $scope.sort = 'popular';
        $scope.polls = [];
        $scope.currentPolls = 0;
        $scope.fetch10Best();
        $('.polls').fadeIn();
      });
    }
  };

  $scope.sortNewest = function () {
    if ($scope.sort !== 'newest') {
      $('.polls').fadeOut(function () {
        $scope.sort = 'newest';
        $scope.polls = [];
        $scope.currentPolls = 0;
        $scope.fetch10New();
        $('.polls').fadeIn();
      });
    }
  };

  $scope.goToPoll = function (pid) {
    window.location = 'http://' + window.location.hostname + window.location.pathname + '#/poll?p=' + pid;
  };

  $scope.refresh = function () {
    location.reload();
  };

  $scope.fetch10Best();

  $(window).scroll(function () {
    if ($scope.fetchedPolls && $(window).height() + $(window).scrollTop() == $(document).height()) {
      $scope.fetching = true;
      $scope.fetchMore();
    }
  });
});

})();
<commit_msg>Fixed bug with changing sort method<commit_after>(function(){

var cont = angular.module('poll-princeton.controllers');

cont.controller('feedController', function ($scope, $filter, $http, $location) {

  document.title = 'Feed | PollPrinceton'

  $('body').css('padding-top', '50px');

  $scope.fetchedDevs = false;
  $scope.fetchedPolls = false;
  $scope.fetching = true;
  $scope.noMorePolls = false;
  $scope.polls = [];
  $scope.currentPolls = 0;
  $scope.sort = "popular";

  var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

  $scope.fetchDevs = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/devs').success(function (data, status, headers, config) {
      $scope.devs = data;
      $scope.fetchedDevs = true;
    });
  };

  $scope.deletePolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/delete/all').success(function (data, status, headers, config) {
    });
  };

  $scope.fetchAllPolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/all').success(function (data, status, headers, config) {
      $scope.polls = data;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i];
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
      }
      $scope.fetchedPolls = true;
    });
  };

  //Get call returns an array of objects containing three things: pollData (a complete poll with all data),
  //userVote (true for up, false for down) and userResponse (however Ryan implemented it. -1 for no response I think)
  $scope.fetch10Best = function () {
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/popular/'
        + user + '/' + $scope.currentPolls + '/false').success(function (data, status, headers, config) {
      $scope.fetching = false;
      if (data.err) {
        $scope.noMorePolls = true;
        return;
      }
      $scope.polls = data;
      $scope.currentPolls = 10;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i].pollData;
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
        thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
          return a + b;
        });
      }
      $scope.fetchedPolls = true;
    });
  };

  $scope.fetchNext10Best = function () {
    if ($scope.noMorePolls) return;
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/popular/'
        + user + '/' + $scope.currentPolls + '/false')
      .success(function (data, status, headers, config) {
        $scope.fetching = false;
        if (data.err) {
          $scope.noMorePolls = true;
          return;
        }
        var polls = data;
        $scope.currentPolls += 10;
        for (var i in polls) {
          var thisPoll = polls[i].pollData;
          var date = new Date(thisPoll.time);
          thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
          thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
            return a + b;
          });
          $scope.polls.push(polls[i]);
        }
      });
  };

  $scope.fetch10New = function () {
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/newest/'
        + user + '/' + $scope.currentPolls + '/false').success(function (data, status, headers, config) {
      $scope.fetching = false;
      if (data.err) {
        $scope.noMorePolls = true;
        return;
      }
      $scope.polls = data;
      $scope.currentPolls = 10;
      for (var i in $scope.polls) {
        var thisPoll = $scope.polls[i].pollData;
        var date = new Date(thisPoll.time);
        thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
        thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
          return a + b;
        });
      }
      $scope.fetchedPolls = true;
    });
  };

  $scope.fetchNext10New = function () {
    if ($scope.noMorePolls) return;
    var user = localStorage.getItem('netid');
    $http.get('http://' + window.location.hostname + '/ppapi/polls/get/newest/'
        + user + '/' + $scope.currentPolls + '/false')
      .success(function (data, status, headers, config) {
        $scope.fetching = false;
        if (data.err) {
          $scope.noMorePolls = true;
          return;
        }
        var polls = data;
        $scope.currentPolls += 10;
        for (var i in polls) {
          var thisPoll = polls[i].pollData;
          var date = new Date(thisPoll.time);
          thisPoll.humanTime = months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
          thisPoll.numResponses = thisPoll.responses.reduce(function(a, b) {
            return a + b;
          });
          $scope.polls.push(polls[i]);
        }
      });
  };

  $scope.fetchMore = function () {
    if ($scope.sort === 'popular') $scope.fetchNext10Best();
    else $scope.fetchNext10New();
  };

  $scope.upVote = function (pid, idx) {
    var pkg = {};
    var user = localStorage.getItem('netid');
    pkg.upOrDown = true;
    pkg.pollID = pid;
    pkg.netid = user;
    $http.post('/ppapi/polls/vote', pkg)
      .success(function (data, status, headers, config) {
        $scope.polls[idx].pollData.score = data.score;
        $scope.polls[idx].userVote = data.userVote;
      })
      .error(function (data, status, headers, config) {
        alert('Something went wrong. Please try to submit again in a few moments.')
        $('.vote-submit').attr('disabled', false);
      });
  };

  $scope.downVote = function (pid, idx) {
    var pkg = {};
    var user = localStorage.getItem('netid');
    pkg.upOrDown = false;
    pkg.pollID = pid;
    pkg.netid = user;
    $http.post('/ppapi/polls/vote', pkg)
      .success(function (data, status, headers, config) {
        $scope.polls[idx].pollData.score = data.score;
        $scope.polls[idx].userVote = data.userVote;
      })
      .error(function (data, status, headers, config) {
        alert('Something went wrong. Please try to submit again in a few moments.')
        $('.vote-submit').attr('disabled', false);
      });
  };

  $scope.deletePolls = function () {
    $http.get('http://' + window.location.hostname + '/ppapi/polls/delete/all').success(function (data, status, headers, config) {
    });
  };

  $scope.sortPopular = function () {
    if ($scope.sort !== 'popular') {
      $('.polls').fadeOut(function () {
        $scope.noMorePolls = false;
        $scope.sort = 'popular';
        $scope.polls = [];
        $scope.currentPolls = 0;
        $scope.fetch10Best();
        $('.polls').fadeIn();
      });
    }
  };

  $scope.sortNewest = function () {
    if ($scope.sort !== 'newest') {
      $('.polls').fadeOut(function () {
        $scope.noMorePolls = false;
        $scope.sort = 'newest';
        $scope.polls = [];
        $scope.currentPolls = 0;
        $scope.fetch10New();
        $('.polls').fadeIn();
      });
    }
  };

  $scope.goToPoll = function (pid) {
    window.location = 'http://' + window.location.hostname + window.location.pathname + '#/poll?p=' + pid;
  };

  $scope.refresh = function () {
    location.reload();
  };

  $scope.fetch10Best();

  $(window).scroll(function () {
    if ($scope.fetchedPolls && $(window).height() + $(window).scrollTop() == $(document).height()) {
      $scope.fetching = true;
      $scope.fetchMore();
    }
  });
});

})();
<|endoftext|>


<commit_before>import gulp from "gulp";
import cp from "child_process";
import gutil from "gulp-util";
import postcss from "gulp-postcss";
import cssImport from "postcss-import";
import cssnext from "postcss-cssnext";
import BrowserSync from "browser-sync";
import webpack from "webpack";
import webpackConfig from "./webpack.conf";
import svgstore from "gulp-svgstore";
import svgmin from "gulp-svgmin";
import inject from "gulp-inject";
import replace from "gulp-replace";
// import cssnano from "cssnano";

const browserSync = BrowserSync.create();
const hugoBin = `./bin/hugo.${process.platform === "win32" ? "exe" : process.platform}`;
const defaultArgs = ["-d", "../dist", "-s", "site", "-v"];

gulp.task("hugo", (cb) => buildSite(cb));
gulp.task("hugo-preview", (cb) => buildSite(cb, ["--buildDrafts", "--buildFuture"]));

gulp.task("cms", () => {
  const match = process.env.REPOSITORY_URL ? process.env.REPOSITORY_URL : cp.execSync("git remote -v", {encoding: "utf-8"});
  let repo = null;
  match.replace(/github.com:(\S+)(\.git)?/, (_, m) => {
    repo = m.replace(/\.git$/, "");
  });
  gulp.src("./src/cms/*")
    .pipe(replace("<% GITHUB_REPOSITORY %>", repo))
    .pipe(gulp.dest("./dist/admin"))
    .pipe(browserSync.stream());
});

gulp.task("build", ["css", "js", "hugo", "cms"]);
gulp.task("build-preview", ["css", "js", "hugo-preview"]);

gulp.task("css", () => (
  gulp.src("./src/css/*.css")
    .pipe(postcss([cssImport({from: "./src/css/main.css"}), cssnext()]))
    .pipe(gulp.dest("./dist/css"))
    .pipe(browserSync.stream())
));

gulp.task("js", (cb) => {
  const myConfig = Object.assign({}, webpackConfig);

  webpack(myConfig, (err, stats) => {
    if (err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString({
      colors: true,
      progress: true
    }));
    browserSync.reload();
    cb();
  });
});

gulp.task("svg", () => {
  const svgs = gulp
    .src("site/static/img/icons/*.svg")
    .pipe(svgmin())
    .pipe(svgstore({inlineSvg: true}));

  function fileContents(filePath, file) {
    return file.contents.toString();
  }

  return gulp
    .src("site/layouts/partials/svg.html")
    .pipe(inject(svgs, {transform: fileContents}))
    .pipe(gulp.dest("site/layouts/partials/"));
});

gulp.task("server", ["hugo", "css", "js", "svg", "cms"], () => {
  browserSync.init({
    server: {
      baseDir: "./dist"
    }
  });
  gulp.watch("./src/js/**/*.js", ["js"]);
  gulp.watch("./src/css/**/*.css", ["css"]);
  gulp.watch("./src/cms/*", ["cms']);
  gulp.watch("./site/static/images/icons/*.svg", ["svg"]);
  gulp.watch("./site/**/*", ["hugo"]);
});

function buildSite(cb, options) {
  const args = options ? defaultArgs.concat(options) : defaultArgs;

  return cp.spawn(hugoBin, args, {stdio: "inherit"}).on("close", (code) => {
    if (code === 0) {
      browserSync.reload();
      cb();
    } else {
      browserSync.notify("Hugo build failed :(");
      cb("Hugo build failed");
    }
  });
}
<commit_msg>fixed typo in gilpfile<commit_after>import gulp from "gulp";
import cp from "child_process";
import gutil from "gulp-util";
import postcss from "gulp-postcss";
import cssImport from "postcss-import";
import cssnext from "postcss-cssnext";
import BrowserSync from "browser-sync";
import webpack from "webpack";
import webpackConfig from "./webpack.conf";
import svgstore from "gulp-svgstore";
import svgmin from "gulp-svgmin";
import inject from "gulp-inject";
import replace from "gulp-replace";
// import cssnano from "cssnano";

const browserSync = BrowserSync.create();
const hugoBin = `./bin/hugo.${process.platform === "win32" ? "exe" : process.platform}`;
const defaultArgs = ["-d", "../dist", "-s", "site", "-v"];

gulp.task("hugo", (cb) => buildSite(cb));
gulp.task("hugo-preview", (cb) => buildSite(cb, ["--buildDrafts", "--buildFuture"]));

gulp.task("cms", () => {
  const match = process.env.REPOSITORY_URL ? process.env.REPOSITORY_URL : cp.execSync("git remote -v", {encoding: "utf-8"});
  let repo = null;
  match.replace(/github.com:(\S+)(\.git)?/, (_, m) => {
    repo = m.replace(/\.git$/, "");
  });
  gulp.src("./src/cms/*")
    .pipe(replace("<% GITHUB_REPOSITORY %>", repo))
    .pipe(gulp.dest("./dist/admin"))
    .pipe(browserSync.stream());
});

gulp.task("build", ["css", "js", "hugo", "cms"]);
gulp.task("build-preview", ["css", "js", "hugo-preview"]);

gulp.task("css", () => (
  gulp.src("./src/css/*.css")
    .pipe(postcss([cssImport({from: "./src/css/main.css"}), cssnext()]))
    .pipe(gulp.dest("./dist/css"))
    .pipe(browserSync.stream())
));

gulp.task("js", (cb) => {
  const myConfig = Object.assign({}, webpackConfig);

  webpack(myConfig, (err, stats) => {
    if (err) throw new gutil.PluginError("webpack", err);
    gutil.log("[webpack]", stats.toString({
      colors: true,
      progress: true
    }));
    browserSync.reload();
    cb();
  });
});

gulp.task("svg", () => {
  const svgs = gulp
    .src("site/static/img/icons/*.svg")
    .pipe(svgmin())
    .pipe(svgstore({inlineSvg: true}));

  function fileContents(filePath, file) {
    return file.contents.toString();
  }

  return gulp
    .src("site/layouts/partials/svg.html")
    .pipe(inject(svgs, {transform: fileContents}))
    .pipe(gulp.dest("site/layouts/partials/"));
});

gulp.task("server", ["hugo", "css", "js", "svg", "cms"], () => {
  browserSync.init({
    server: {
      baseDir: "./dist"
    }
  });
  gulp.watch("./src/js/**/*.js", ["js"]);
  gulp.watch("./src/css/**/*.css", ["css"]);
  gulp.watch("./src/cms/*", ["cms"]);
  gulp.watch("./site/static/images/icons/*.svg", ["svg"]);
  gulp.watch("./site/**/*", ["hugo"]);
});

function buildSite(cb, options) {
  const args = options ? defaultArgs.concat(options) : defaultArgs;

  return cp.spawn(hugoBin, args, {stdio: "inherit"}).on("close", (code) => {
    if (code === 0) {
      browserSync.reload();
      cb();
    } else {
      browserSync.notify("Hugo build failed :(");
      cb("Hugo build failed");
    }
  });
}
<|endoftext|>