<commit_before>'use strict';

var request = require('request');
var webremix = require('./web-remix');
var userDb = require('./user');

var escapeHtml = function(text) {
  return text
    .replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>');
};

/* API check from current <-> legacy */
exports.getUser = function(req) {
  if (req.session.passport.user._json && req.session.passport.user._json.data) {
    return req.session.passport.user._json.data;
  } else {
    return req.session.passport.user;
  }
};

exports.getUserId = function(req) {
  if (req.session.passport.user._json && req.session.passport.user._json.data) {
    return req.session.passport.user._json.data.id;
  } else {
    return req.session.passport.user.id;
  }
};

var createClientMessage = function(options, callback) {
  process.nextTick(function() {
    /* maxId is used for starred posts, since they can't be paginated
     * by their post_id - reason is that posts can be starred in any
     * order and therefore have their own id system on pnut.io for sorting.
     */
    var minId = null;
    var isSelf = false;
    var isThread = false;
    var repostDetails = { id: null };

    if (options.recent.repostDetails) {
      repostDetails = options.recent.repostDetails;
    }

    if (options.metadata && options.metadata.min_id) {
      minId = options.metadata.min_id;
    }

    if (typeof options.user.id !== 'undefined' && options.user.id === options.userId) {
      isSelf = true;
    }

    if (typeof options.recent.counts === 'undefined') {
        options.recent.counts = {replies:0,bookmarks:0,reposts:0};
    }

    if (options.recent.reply_to || options.recent.counts.replies) {
      isThread = true;
    }

    // We want to preserve the line breaks if there are any
    var message = options.tokenized.join('').replace(/\n/, '
');

    // Annotations
    message += options.moods + options.photos;

    var userReferences = {};
    var replyTo = null;

    userReferences['@' + options.user.username] = '@' + options.user.username;

    options.userMentions.forEach(function(user) {
      userReferences['@' + user.text] = '@' + user.text;
    });

    delete userReferences['@' + options.username];
    delete userReferences['@' + options.user.username];

    if (options.recent.repost_of) {
      replyTo = options.recent.repost_of.id;
    }

    var userReferenceArr = [];

    for (var username in userReferences) {
      userReferenceArr.push(username);
    }

    var messageData = {
      id: options.recent.id,
      createdAt: options.recent.created_at,
      crosspost: options.crosspost,
      message: message,
      text: options.recent.content.text,
      user: options.user.content.avatar_image.link,
      name: options.user.name,
      userId: options.user.id,
      username: options.user.username,
      isSelf: isSelf,
      isThread: isThread,
      isStarred: options.recent.you_bookmarked,
      isRepost: options.recent.you_reposted,
      repostDetails: repostDetails,
      isFollowing: options.user.you_follow,
      isMuted: options.user.you_muted,
      numStars: options.recent.counts.bookmarks,
      numReposts: options.recent.counts.reposts,
      numReplies: options.recent.counts.replies,
      appSource: options.recent.source.name,
      appSourceUrl: options.recent.source.link,
      mentions: userReferenceArr.join(' '),
      minId: minId,
      replyTo: replyTo,
      inReplyToId: options.recent.reply_to
    };

    options.newMessages.push(messageData);

    if (options.newMessages.length === options.recentMessagesLength) {
      if (!options.paginated) {
        options.newMessages = options.newMessages.sort(function(a, b) {
          return parseInt(a.id, 10) - parseInt(b.id, 10);
        });
      }

      callback(options.newMessages);
    }
  });
};

var continueRendering = function(entities, options, callback) {
  process.nextTick(function() {
    var htmlMessages = [];
    var textIdx = 0;

    entities.sort(function(entity1, entity2) {
      return entity1.pos - entity2.pos;
    });

    entities.forEach(function(entity) {
      // Before entity
      htmlMessages.push(escapeHtml(options.recent.content.text.substring(textIdx, entity.pos)));

      htmlMessages.push(entity.html);
      textIdx = entity.pos + entity.len;
    });
    // After last entity
    htmlMessages.push(escapeHtml(options.recent.content.text.substring(textIdx)));

    options.tokenized = htmlMessages;
    createClientMessage(options, callback);
  });
};

var processLink = function(idx, entities, options, callback) {
  var link = options.recent.content.entities.links[idx];

  var media = webremix.generate(link);

  entities.push({
    pos: link.pos,
    len: link.len,
    html: media
  });

  if (idx === options.recent.content.entities.links.length - 1) {
    continueRendering(entities, options, callback);
  } else {
    processLink(idx + 1, entities, options, callback);
  }
};

/* Generate the feed content
 * Requires: web request, recentMessages, redis client, pagination boolean, callback
 * Returns: the messages as JSON
 */
exports.generateFeed = function(req, recentMessages, paginated, callback) {
  var self = this;

  process.nextTick(function() {
    var newMessages = [];
    var messageCount = 0;
    var userSession = self.getUser(req);
    var userId = userSession.id;
    var username = userSession.username;
    var metadata = false;

    if (recentMessages.meta) {
      metadata = recentMessages.meta;
    }

    if (recentMessages.data) {
      recentMessages = recentMessages.data;
    }

    if (recentMessages.length > 0) {
      for (var i = 0; i < recentMessages.length; i ++) {
        var recent = recentMessages[i];
        if (recent.data) {
          recent = recent.data;
        }

        if (typeof recent.is_deleted !== 'undefined') {
          messageCount ++;
          recent.content = {text:''};

          var options = {
            crosspost: '',
            recent: recent,
            newMessages: newMessages,
            recentMessagesLength: recentMessages.length,
            user: recent.user,
            userId: userId,
            username: username,
            userMentions: [],
            messageCount: messageCount,
            metadata: metadata,
            moods: '',
            photos: '',
            paginated: paginated
          };

          continueRendering([], options, callback);
        } else if (typeof recent.content !== 'undefined' && typeof recent.content.entities !== 'undefined') {
          messageCount ++;

          if (recent.repost_of) {
            var repost = recent;
            recent = recent.repost_of;
            recent.repostDetails = { id: repost.id, user_id: repost.user.id, username: repost.user.username, avatar: repost.user.content.avatar_image.link };
          }

          var user = recent.user;
          var moods = '';
          var photos = '';
          var entities = [];
          var crosspost = '';
          var userMentions = recent.content.entities.mentions;

          // raw
          // var moodArray = ['happy', 'sad', 'tired', 'angry', 'calm', 'omg'];

          if (recent.raw) {
            recent.raw.forEach(function(annotation) {
              // if (annotation.type === 'net.xtendr.mood' && moodArray.indexOf(annotation.value.mood) > -1) {
              //   var mood = '';

              //   moods += mood;

              // } else
              if (annotation.type === 'io.pnut.core.oembed') {
                if (annotation.value.type === 'photo' &&
                  recent.content.text.indexOf(annotation.value.url) === -1) {
                  var linkUrl = annotation.value.embeddable_url || annotation.value.url;
                  var photo = '

' +
                    '' +
                    linkUrl + '';

                  photos += photo;
                }
              } else if (annotation.type === 'io.pnut.core.crosspost') {
                crosspost = annotation.value.canonical_url;
              } else if (annotation.type === 'nl.chimpnut.blog.post') {
                recent.content.text += '\n' + escapeHtml(annotation.value.body);
              }
            });
          }

          // Mentions
          userMentions.forEach(function(mention) {
            var name = recent.content.text.substr(mention.pos, mention.len);
            entities.push({
              pos: mention.pos,
              len: mention.len,
              html: '' + name + ''
            });
          });

          // Hashtags
          recent.content.entities.tags.forEach(function(hashtag) {
            var name = recent.content.text.substr(hashtag.pos, hashtag.len);
            entities.push({
              pos: hashtag.pos,
              len: hashtag.len,
              html: '' + name + ''
            });
          });

          var options = {
            crosspost: crosspost,
            recent: recent,
            newMessages: newMessages,
            recentMessagesLength: recentMessages.length,
            user: user,
            userId: userId,
            username: username,
            userMentions: userMentions,
            messageCount: messageCount,
            metadata: metadata,
            moods: moods,
            photos: photos,
            paginated: paginated
          };

          if (recent.content.entities.links.length > 0) {
            processLink(0, entities, options, callback);
          } else {
            continueRendering(entities, options, callback);
          }
        }
      }
    } else {
      callback(newMessages);
    }
  });
};

/* Generate the interaction content
 * Requires: web request, recentMessages, redis client, pagination boolean, callback
 * Returns: the messages as JSON
 */
exports.generateInteractions = function(req, recentMessages, paginated, callback) {
  var newMessages = [];
  var recentList = recentMessages.data;
  var userSession = this.getUser(req);
  var username = userSession.username;
  var messageCount = 0;

  if (recentList.length > 0) {
    var ct = 0;
    recentList.forEach(function(recent) {
      ct = ct+1;
      // temp fix
      try {
          var messageItem = recent.objects[0];
          var message = '';
          var icon = '';

          var users = [];
          recent.users.forEach(function(user) {
            users.push(user);
          });

          if (recent.action === 'reply') {
            message = 'Replied to your post:
' + messageItem.content.html + '
';
            icon = '';
          } else if (recent.action === 'follow') {
            message = 'Followed you';
            icon = '';
          } else if (recent.action === 'bookmark') {
            message = 'Bookmarked your post:
' + messageItem.content.html + '
';
            icon = '';
          } else if (recent.action === 'repost') {
            message = 'Reposted your post:
' + messageItem.content.html + '
';
            icon = '';
          }

          var options = {
            id: messageItem.id,
            pagination_id: recent.pagination_id,
            users: users,
            message: message,
            minId: recentMessages.meta.min_id,
            icon: icon
          };

          newMessages.unshift(options);

          if (paginated) {
            newMessages = newMessages.sort(function(a, b) {
              return parseInt(b.pagination_id, 10) - parseInt(a.pagination_id, 10);
            });
          }
      } catch (err) {
          console.log(recent);
          return false;
      }
      if (ct === recentList.length) {
        callback(newMessages)
      }
    });
  } else {
    callback(newMessages);
  }
};
<commit_msg>fix missing user check<commit_after>'use strict';

var request = require('request');
var webremix = require('./web-remix');
var userDb = require('./user');

var escapeHtml = function(text) {
  return text
    .replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>');
};

/* API check from current <-> legacy */
exports.getUser = function(req) {
  if (req.session.passport.user._json && req.session.passport.user._json.data) {
    return req.session.passport.user._json.data;
  } else {
    return req.session.passport.user;
  }
};

exports.getUserId = function(req) {
  if (req.session.passport.user._json && req.session.passport.user._json.data) {
    return req.session.passport.user._json.data.id;
  } else {
    return req.session.passport.user.id;
  }
};

var createClientMessage = function(options, callback) {
  process.nextTick(function() {
    /* maxId is used for starred posts, since they can't be paginated
     * by their post_id - reason is that posts can be starred in any
     * order and therefore have their own id system on pnut.io for sorting.
     */
    var minId = null;
    var isSelf = false;
    var isThread = false;
    var repostDetails = { id: null };

    if (typeof options.user === 'undefined') {
        return false;
    }

    if (options.recent.repostDetails) {
      repostDetails = options.recent.repostDetails;
    }

    if (options.metadata && options.metadata.min_id) {
      minId = options.metadata.min_id;
    }

    if (typeof options.user !== 'undefined' && options.user.id === options.userId) {
      isSelf = true;
    }

    if (typeof options.recent.counts === 'undefined') {
        options.recent.counts = {replies:0,bookmarks:0,reposts:0};
    }

    if (options.recent.reply_to || options.recent.counts.replies) {
      isThread = true;
    }

    // We want to preserve the line breaks if there are any
    var message = options.tokenized.join('').replace(/\n/, '
');

    // Annotations
    message += options.moods + options.photos;

    var userReferences = {};
    var replyTo = null;

    userReferences['@' + options.user.username] = '@' + options.user.username;

    options.userMentions.forEach(function(user) {
      userReferences['@' + user.text] = '@' + user.text;
    });

    delete userReferences['@' + options.username];
    delete userReferences['@' + options.user.username];

    if (options.recent.repost_of) {
      replyTo = options.recent.repost_of.id;
    }

    var userReferenceArr = [];

    for (var username in userReferences) {
      userReferenceArr.push(username);
    }

    var messageData = {
      id: options.recent.id,
      createdAt: options.recent.created_at,
      crosspost: options.crosspost,
      message: message,
      text: options.recent.content.text,
      user: options.user.content.avatar_image.link,
      name: options.user.name,
      userId: options.user.id,
      username: options.user.username,
      isSelf: isSelf,
      isThread: isThread,
      isStarred: options.recent.you_bookmarked,
      isRepost: options.recent.you_reposted,
      repostDetails: repostDetails,
      isFollowing: options.user.you_follow,
      isMuted: options.user.you_muted,
      numStars: options.recent.counts.bookmarks,
      numReposts: options.recent.counts.reposts,
      numReplies: options.recent.counts.replies,
      appSource: options.recent.source.name,
      appSourceUrl: options.recent.source.link,
      mentions: userReferenceArr.join(' '),
      minId: minId,
      replyTo: replyTo,
      inReplyToId: options.recent.reply_to
    };

    options.newMessages.push(messageData);

    if (options.newMessages.length === options.recentMessagesLength) {
      if (!options.paginated) {
        options.newMessages = options.newMessages.sort(function(a, b) {
          return parseInt(a.id, 10) - parseInt(b.id, 10);
        });
      }

      callback(options.newMessages);
    }
  });
};

var continueRendering = function(entities, options, callback) {
  process.nextTick(function() {
    var htmlMessages = [];
    var textIdx = 0;

    entities.sort(function(entity1, entity2) {
      return entity1.pos - entity2.pos;
    });

    entities.forEach(function(entity) {
      // Before entity
      htmlMessages.push(escapeHtml(options.recent.content.text.substring(textIdx, entity.pos)));

      htmlMessages.push(entity.html);
      textIdx = entity.pos + entity.len;
    });
    // After last entity
    htmlMessages.push(escapeHtml(options.recent.content.text.substring(textIdx)));

    options.tokenized = htmlMessages;
    createClientMessage(options, callback);
  });
};

var processLink = function(idx, entities, options, callback) {
  var link = options.recent.content.entities.links[idx];

  var media = webremix.generate(link);

  entities.push({
    pos: link.pos,
    len: link.len,
    html: media
  });

  if (idx === options.recent.content.entities.links.length - 1) {
    continueRendering(entities, options, callback);
  } else {
    processLink(idx + 1, entities, options, callback);
  }
};

/* Generate the feed content
 * Requires: web request, recentMessages, redis client, pagination boolean, callback
 * Returns: the messages as JSON
 */
exports.generateFeed = function(req, recentMessages, paginated, callback) {
  var self = this;

  process.nextTick(function() {
    var newMessages = [];
    var messageCount = 0;
    var userSession = self.getUser(req);
    var userId = userSession.id;
    var username = userSession.username;
    var metadata = false;

    if (recentMessages.meta) {
      metadata = recentMessages.meta;
    }

    if (recentMessages.data) {
      recentMessages = recentMessages.data;
    }

    if (recentMessages.length > 0) {
      for (var i = 0; i < recentMessages.length; i ++) {
        var recent = recentMessages[i];
        if (recent.data) {
          recent = recent.data;
        }

        if (typeof recent.is_deleted !== 'undefined') {
          messageCount ++;
          recent.content = {text:''};

          var options = {
            crosspost: '',
            recent: recent,
            newMessages: newMessages,
            recentMessagesLength: recentMessages.length,
            user: recent.user,
            userId: userId,
            username: username,
            userMentions: [],
            messageCount: messageCount,
            metadata: metadata,
            moods: '',
            photos: '',
            paginated: paginated
          };

          continueRendering([], options, callback);
        } else if (typeof recent.content !== 'undefined' && typeof recent.content.entities !== 'undefined') {
          messageCount ++;

          if (recent.repost_of) {
            var repost = recent;
            recent = recent.repost_of;
            recent.repostDetails = { id: repost.id, user_id: repost.user.id, username: repost.user.username, avatar: repost.user.content.avatar_image.link };
          }

          var user = recent.user;
          var moods = '';
          var photos = '';
          var entities = [];
          var crosspost = '';
          var userMentions = recent.content.entities.mentions;

          // raw
          // var moodArray = ['happy', 'sad', 'tired', 'angry', 'calm', 'omg'];

          if (recent.raw) {
            recent.raw.forEach(function(annotation) {
              // if (annotation.type === 'net.xtendr.mood' && moodArray.indexOf(annotation.value.mood) > -1) {
              //   var mood = '';

              //   moods += mood;

              // } else
              if (annotation.type === 'io.pnut.core.oembed') {
                if (annotation.value.type === 'photo' &&
                  recent.content.text.indexOf(annotation.value.url) === -1) {
                  var linkUrl = annotation.value.embeddable_url || annotation.value.url;
                  var photo = '

' +
                    '' +
                    linkUrl + '';

                  photos += photo;
                }
              } else if (annotation.type === 'io.pnut.core.crosspost') {
                crosspost = annotation.value.canonical_url;
              } else if (annotation.type === 'nl.chimpnut.blog.post') {
                recent.content.text += '\n' + escapeHtml(annotation.value.body);
              }
            });
          }

          // Mentions
          userMentions.forEach(function(mention) {
            var name = recent.content.text.substr(mention.pos, mention.len);
            entities.push({
              pos: mention.pos,
              len: mention.len,
              html: '' + name + ''
            });
          });

          // Hashtags
          recent.content.entities.tags.forEach(function(hashtag) {
            var name = recent.content.text.substr(hashtag.pos, hashtag.len);
            entities.push({
              pos: hashtag.pos,
              len: hashtag.len,
              html: '' + name + ''
            });
          });

          var options = {
            crosspost: crosspost,
            recent: recent,
            newMessages: newMessages,
            recentMessagesLength: recentMessages.length,
            user: user,
            userId: userId,
            username: username,
            userMentions: userMentions,
            messageCount: messageCount,
            metadata: metadata,
            moods: moods,
            photos: photos,
            paginated: paginated
          };

          if (recent.content.entities.links.length > 0) {
            processLink(0, entities, options, callback);
          } else {
            continueRendering(entities, options, callback);
          }
        }
      }
    } else {
      callback(newMessages);
    }
  });
};

/* Generate the interaction content
 * Requires: web request, recentMessages, redis client, pagination boolean, callback
 * Returns: the messages as JSON
 */
exports.generateInteractions = function(req, recentMessages, paginated, callback) {
  var newMessages = [];
  var recentList = recentMessages.data;
  var userSession = this.getUser(req);
  var username = userSession.username;
  var messageCount = 0;

  if (recentList.length > 0) {
    var ct = 0;
    recentList.forEach(function(recent) {
      ct = ct+1;
      // temp fix
      try {
          var messageItem = recent.objects[0];
          var message = '';
          var icon = '';

          var users = [];
          recent.users.forEach(function(user) {
            users.push(user);
          });

          if (recent.action === 'reply') {
            message = 'Replied to your post:
' + messageItem.content.html + '
';
            icon = '';
          } else if (recent.action === 'follow') {
            message = 'Followed you';
            icon = '';
          } else if (recent.action === 'bookmark') {
            message = 'Bookmarked your post:
' + messageItem.content.html + '
';
            icon = '';
          } else if (recent.action === 'repost') {
            message = 'Reposted your post:
' + messageItem.content.html + '
';
            icon = '';
          }

          var options = {
            id: messageItem.id,
            pagination_id: recent.pagination_id,
            users: users,
            message: message,
            minId: recentMessages.meta.min_id,
            icon: icon
          };

          newMessages.unshift(options);

          if (paginated) {
            newMessages = newMessages.sort(function(a, b) {
              return parseInt(b.pagination_id, 10) - parseInt(a.pagination_id, 10);
            });
          }
      } catch (err) {
          console.log(recent);
          return false;
      }
      if (ct === recentList.length) {
        callback(newMessages)
      }
    });
  } else {
    callback(newMessages);
  }
};
<|endoftext|>


<commit_before>#!/usr/bin/env node
/*
 * TOOL kafka topic helper
 *
 * Author: Rafal Michalski (c) 2015
 *
 * Usage:
 *
 * kafka-topics [topic-name [[group-id] offset]]
 *
*/
require('colors');
var assert = require('assert');
var numeral = require('numeral');
var kafka_tools = require('../index.js')
  , thousands = kafka_tools.thousands
  , pad       = kafka_tools.pad;
var kafka = require('kafka-node');

var connectionString = process.env.ADVERTINE_KAFKA_SERVER || 'kafka.advertine.com:2181/kafka081';
var clientId = 'advertine-kafka-topics';
var clientOptions = {
                      sessionTimeout: 5000,
                      spinDelay : 500,
                      retries : 0
                    };

numeral.language('pl', {
    delimiters: {
        thousands: "'",
        decimal: ','
    }
});

// switch between languages
numeral.language('pl');

var client = new kafka.Client(connectionString, clientId, clientOptions);

var args = process.argv.slice(2);

console.log('Kafka: ' + connectionString.green);

function showUsageAndExit() {
  console.log("Usage:");
  console.log("  kafka-topics topic-name [[group-id] offset]")
  console.log("               offset - set offset to (boundaries are checked)");
  showBrokersAndExit();
}

function showBrokersAndExit() {
  console.log("\nBrokers: ");
  client.zk.listBrokers(function(brokers) {
    Object.keys(brokers).forEach(function(no) {
      var brokerInfo = brokers[no]
      console.log("%d: %s:%s ts: %s ver: %d", no,
        brokerInfo.host.green, String(brokerInfo.port).magenta,
        new Date(parseInt(brokerInfo.timestamp)).toISOString().cyan,
        brokerInfo.version);
    });
    console.log('');
    showTopicsAndExit();
  });
}

function showTopicsAndExit() {
  client.zk.listTopics(function(err, topics) {
    assert.ifError(err);

    console.log("Topics:");
    var padlen = topics.reduce(function(max, topic) {
      return topic.name.length > max ? topic.name.length : max}, 0);
    topics.forEach(function(topic) {
      var partitions = Object.keys(topic.partitions);
      var numPartitions = partitions.length
        , replication = topic.partitions[partitions[0]].length
      console.log("%s (part: %s repl: %s ver: %s)",
        pad(topic.name, padlen).yellow, numPartitions, replication, topic.version);
      // for(var partition in topic.partitions) {
      //   console.log("  Partition: %s %s ", partition, topic.partitions[partition].join(','));
      // };
    });
    process.exit();
  });
}

if (args.length < 1) {

  showUsageAndExit();

} else {

  var topic = args[0];

  client.topicExists([topic], function(err) {
    if (err) {
      console.log(err.message.red);
      process.exit(1);
    }

    showOffsets(topic, new kafka.Offset(client));
  });
}

function getPartitions(topic, callback) {
  var partitions = client.topicPartitions[topic];
  if (partitions)
    return callback(null, partitions);
  client.refreshMetadata([topic], function(err) {
    if (err) {
      callback(err);
    } else
      callback(null, client.topicPartitions[topic]);
  });
}

function showOffsets(topic, offset) {
  getPartitions(topic, function(err, partitions) {

    assert.ifError(err);

    var payloads = partitions.map(function(partition) {
      return { topic: topic, partition: partition, time: -2, maxNum: 1 };
    });

    offset.fetch(payloads, function (err, data) {

      assert.ifError(err);

      payloads.forEach(function(payload) {
        payload.time = -1;
      });

      offset.fetch(payloads, function (err, data2) {

        assert.ifError(err);

        console.log("Topic: " + topic.yellow);

        if (args.length < 2) {
          partitions.forEach(function(partition) {
            var min = data[topic][partition][0], max = data2[topic][partition][0]
            console.log("Partition %s: %s - %s buf: %s",
              pad(partition, partitions.length < 10 ? 1 : 2),
              thousands(min), thousands(max),
              thousands(max - min).cyan);
          });

          showTopicGroups(topic);

        } else {

          if (args[1] === '--delete') {
            console.log('Attempting to delete topic....'.red);
            client.zk.deleteTopics([topic], function(err, stat) {
              assert.ifError(err);
              console.log('No errors, but will it work?'.grey);
            });
          } else {

            showGroupId(topic, offset, partitions, args[1], data, data2);

          }

        }
      });
    });
  });
}

function showTopicGroups(topic) {
  client.zk.listGroupsPerTopic(topic, function(err, groupNames) {
      assert.ifError(err);

      if (groupNames.length) {
        console.log("Groups: ");

        groupNames.forEach(function(name) {
          console.log("  %s", name.cyan);
        });
      } else {
        console.log("Groups: %s", 'none'.grey);
      }

      process.exit();
  });
}

function showGroupId(topic, offset, partitions, groupId, mins, maxs) {
  var payloads = partitions.map(function(partition) {
    return { topic: topic, partition: partition };
  });
  offset.fetchCommits(groupId, payloads, function (err, data) {

    assert.ifError(err);

    console.log("Group ID: " + groupId.cyan);

    if (args.length >= 3) {
      var setTo = parseInt(args[2]);
      if (! isNaN(setTo))
        return setGroupId(topic, offset, setTo, partitions, groupId, mins, maxs, data);
    }

    partitions.forEach(function(partition) {
      var min = mins[topic][partition][0]
        , max = maxs[topic][partition][0]
        , cur = data[topic][partition]
        , buf = (max - min)
        , lag = max - cur
        , perc = lag / buf
        ;
      console.log("Partition %s: %s - %s ofs: %s lag: %s%% %s",
        pad(partition, partitions.length < 10 ? 1 : 2),
        thousands(min), thousands(max),
        thousands(cur).magenta,
        pad((perc*100).toFixed(0), 3),
        thousands(max - cur).red);
    });

    process.exit();

  });
}

function setGroupId(topic, offset, setTo, partitions, groupId, mins, maxs, curs) {
  var payloads = partitions.map(function(partition) {
    var value = setTo;
    if (value < mins[topic][partition][0])
      value = mins[topic][partition][0];
    if (value > maxs[topic][partition][0])
      value = maxs[topic][partition][0];
    return { topic: topic, partition: partition, offset: value };
  });

  offset.commit(groupId, payloads, function(err, data) {
    assert.ifError(err);

    partitions.forEach(function(partition) {
      var min = mins[topic][partition][0]
        , max = maxs[topic][partition][0]
        , cur = curs[topic][partition]
        ;
      console.log("Partition %s: %s - %s ofs: %s -> %s",
        pad(partition, partitions.length < 10 ? 1 : 2),
        thousands(min), thousands(max),
        thousands(cur).magenta,
        thousands(payloads[partition|0].offset).green);
    });

    process.exit();
  });

}
<commit_msg>bin files +x<commit_after>#!/usr/bin/env node
/*
 * TOOL kafka topic helper
 *
 * Author: Rafal Michalski (c) 2015
 *
 * Usage:
 *
 * kafka-topics [topic-name [[group-id] offset]]
 *
*/
require('colors');
var assert = require('assert');
var numeral = require('numeral');
var kafka_tools = require('../index.js')
  , thousands = kafka_tools.thousands
  , pad       = kafka_tools.pad;
var kafka = require('kafka-node');

var connectionString = process.env.ADVERTINE_KAFKA_SERVER || 'kafka.advertine.com:2181/kafka081';
var clientId = 'advertine-kafka-topics';
var clientOptions = {
                      sessionTimeout: 5000,
                      spinDelay : 500,
                      retries : 0
                    };

numeral.language('pl', {
    delimiters: {
        thousands: "'",
        decimal: ','
    }
});

// switch between languages
numeral.language('pl');

var client = new kafka.Client(connectionString, clientId, clientOptions);

var args = process.argv.slice(2);

console.log('Kafka: ' + connectionString.green);

function showUsageAndExit() {
  console.log("Usage:");
  console.log("  kafka-topics topic-name [[group-id] offset]")
  console.log("               offset - set offset to (boundaries are checked)");
  showBrokersAndExit();
}

function showBrokersAndExit() {
  console.log("\nBrokers: ");
  client.zk.listBrokers(function(brokers) {
    Object.keys(brokers).forEach(function(no) {
      var brokerInfo = brokers[no]
      console.log("%d: %s:%s ts: %s ver: %d", no,
        brokerInfo.host.green, String(brokerInfo.port).magenta,
        new Date(parseInt(brokerInfo.timestamp)).toISOString().cyan,
        brokerInfo.version);
    });
    console.log('');
    showTopicsAndExit();
  });
}

function showTopicsAndExit() {
  client.zk.listTopics(function(err, topics) {
    assert.ifError(err);

    console.log("Topics:");
    var padlen = topics.reduce(function(max, topic) {
      return topic.name.length > max ? topic.name.length : max}, 0);
    topics.forEach(function(topic) {
      var partitions = Object.keys(topic.partitions);
      var numPartitions = partitions.length
        , replication = topic.partitions[partitions[0]].length
      console.log("%s (part: %s repl: %s ver: %s)",
        pad(topic.name, padlen).yellow, numPartitions, replication, topic.version);
      // for(var partition in topic.partitions) {
      //   console.log("  Partition: %s %s ", partition, topic.partitions[partition].join(','));
      // };
    });
    process.exit();
  });
}

if (args.length < 1) {

  showUsageAndExit();

} else {

  var topic = args[0];

  client.topicExists([topic], function(err) {
    if (err) {
      console.log(err.message.red);
      process.exit(1);
    }

    showOffsets(topic, new kafka.Offset(client));
  });
}

function getPartitions(topic, callback) {
  var partitions = client.topicPartitions[topic];
  if (partitions)
    return callback(null, partitions);
  client.refreshMetadata([topic], function(err) {
    if (err) {
      callback(err);
    } else
      callback(null, client.topicPartitions[topic]);
  });
}

function showOffsets(topic, offset) {
  getPartitions(topic, function(err, partitions) {

    assert.ifError(err);

    var payloads = partitions.map(function(partition) {
      return { topic: topic, partition: partition, time: -2, maxNum: 1 };
    });

    offset.fetch(payloads, function (err, data) {

      assert.ifError(err);

      payloads.forEach(function(payload) {
        payload.time = -1;
      });

      offset.fetch(payloads, function (err, data2) {

        assert.ifError(err);

        console.log("Topic: " + topic.yellow);

        if (args.length < 2) {
          partitions.forEach(function(partition) {
            var min = data[topic][partition][0], max = data2[topic][partition][0]
            console.log("Partition %s: %s - %s buf: %s",
              pad(partition, partitions.length < 10 ? 1 : 2),
              thousands(min), thousands(max),
              thousands(max - min).cyan);
          });

          showTopicGroups(topic);

        } else {

          if (args[1] === '--delete') {
            console.log('Attempting to delete topic....'.red);
            client.zk.deleteTopics([topic], function(err, stat) {
              assert.ifError(err);
              console.log('No errors, but will it work?'.grey);
            });
          } else {

            showGroupId(topic, offset, partitions, args[1], data, data2);

          }

        }
      });
    });
  });
}

function showTopicGroups(topic) {
  client.zk.listGroupsPerTopic(topic, function(err, groupNames) {
      assert.ifError(err);

      if (groupNames.length) {
        console.log("Groups: ");

        groupNames.forEach(function(name) {
          console.log("  %s", name.cyan);
        });
      } else {
        console.log("Groups: %s", 'none'.grey);
      }

      process.exit();
  });
}

function showGroupId(topic, offset, partitions, groupId, mins, maxs) {
  var payloads = partitions.map(function(partition) {
    return { topic: topic, partition: partition };
  });
  offset.fetchCommits(groupId, payloads, function (err, data) {

    assert.ifError(err);

    console.log("Group ID: " + groupId.cyan);

    if (args.length >= 3) {
      var setTo = parseInt(args[2]);
      if (! isNaN(setTo))
        return setGroupId(topic, offset, setTo, partitions, groupId, mins, maxs, data);
    }

    partitions.forEach(function(partition) {
      var min = mins[topic][partition][0]
        , max = maxs[topic][partition][0]
        , cur = data[topic][partition]
        , buf = (max - min)
        , lag = max - cur
        , perc = lag / buf
        ;
      console.log("Partition %s: %s - %s ofs: %s lag: %s%% %s",
        pad(partition, partitions.length < 10 ? 1 : 2),
        thousands(min), thousands(max),
        thousands(cur).magenta,
        pad((perc*100).toFixed(0), 3),
        thousands(max - cur).red);
    });

    process.exit();

  });
}

function setGroupId(topic, offset, setTo, partitions, groupId, mins, maxs, curs) {
  var payloads = partitions.map(function(partition) {
    var value = setTo;
    if (value < mins[topic][partition][0])
      value = mins[topic][partition][0];
    if (value > maxs[topic][partition][0])
      value = maxs[topic][partition][0];
    return { topic: topic, partition: partition, offset: value };
  });

  offset.commit(groupId, payloads, function(err, data) {
    assert.ifError(err);

    partitions.forEach(function(partition) {
      var min = mins[topic][partition][0]
        , max = maxs[topic][partition][0]
        , cur = curs[topic][partition]
        ;
      console.log("Partition %s: %s - %s ofs: %s -> %s",
        pad(partition, partitions.length < 10 ? 1 : 2),
        thousands(min), thousands(max),
        thousands(cur).magenta,
        thousands(payloads[partition|0].offset).green);
    });

    process.exit();
  });

}
<|endoftext|>


<commit_before>// Copyright 2012 The Obvious Corporation.

/*
 * A simple stream reader valve, for reliably capturing stream events
 * across tick boundaries.
 */

/*
 * Modules used
 */

"use strict";

var stream = require("stream");
var _      = require("underscore");

var consts = require("./consts");

/*
 * Exported bindings
 */

/**
 * Create a data valve for the given data/end/error event source. This is
 * an instance of `Stream`.
 *
 * An instance starts out in the "off" position, buffering up
 * events. As soon as it is turned "on", it immediately emits the
 * buffered events (in order), and then passes through further events
 * as they are received.
 */
function create(source) {
    var self;
    var buffering = true;
    var buffer = []; // list of events

    source.on(consts.DATA, onData);
    source.on(consts.END, onEnd);
    source.on(consts.ERROR, onError);

    function onData(data) {
        if (buffering) {
            buffer.push({ func: onData, arg: data });
        } else {
            self.emit(consts.DATA, data);
        }
    }

    function onEnd() {
        if (buffering) {
            buffer.push({ func: onEnd });
        } else {
            self.emit(consts.END);
        }
    }

    function onError(error) {
        if (buffering) {
            buffer.push({ func: onError, arg: error });
        } else {
            self.emit(consts.ERROR, error);
        }
    }

    /**
     * Turn on the event flow.
     */
    function turnOn() {
        if (!buffering) {
            return;
        }

        buffering = false;
        for (var i = 0; i < buffer.length; i++) {
            var one = buffer[i];
            one.func(one.arg);
        }
    }

    self = new stream.Stream();
    self.turnOn = turnOn;

    return self;
}

module.exports = {
    create: create
};
<commit_msg>Pass "close" events through.<commit_after>// Copyright 2012 The Obvious Corporation.

/*
 * A simple stream reader valve, for reliably capturing stream events
 * across tick boundaries.
 */

/*
 * Modules used
 */

"use strict";

var stream = require("stream");
var _      = require("underscore");

var consts = require("./consts");

/*
 * Exported bindings
 */

/**
 * Create a data valve for the given data/end/error event source. This is
 * an instance of `Stream`.
 *
 * An instance starts out in the "off" position, buffering up
 * events. As soon as it is turned "on", it immediately emits the
 * buffered events (in order), and then passes through further events
 * as they are received.
 */
function create(source) {
    var self;
    var buffering = true;
    var buffer = []; // list of events

    source.on(consts.CLOSE, onClose);
    source.on(consts.DATA, onData);
    source.on(consts.END, onEnd);
    source.on(consts.ERROR, onError);

    function onClose() {
        if (buffering) {
            buffer.push({ func: onClose });
        } else {
            self.emit(consts.CLOSE);
        }
    }

    function onData(data) {
        if (buffering) {
            buffer.push({ func: onData, arg: data });
        } else {
            self.emit(consts.DATA, data);
        }
    }

    function onEnd() {
        if (buffering) {
            buffer.push({ func: onEnd });
        } else {
            self.emit(consts.END);
        }
    }

    function onError(error) {
        if (buffering) {
            buffer.push({ func: onError, arg: error });
        } else {
            self.emit(consts.ERROR, error);
        }
    }

    /**
     * Turn on the event flow.
     */
    function turnOn() {
        if (!buffering) {
            return;
        }

        buffering = false;
        for (var i = 0; i < buffer.length; i++) {
            var one = buffer[i];
            one.func(one.arg);
        }
    }

    self = new stream.Stream();
    self.turnOn = turnOn;

    return self;
}

module.exports = {
    create: create
};
<|endoftext|>


<commit_before>/**
 * vCard
 * @constructor
 * @return {vCard}
 */
function vCard() {

  if( !(this instanceof vCard) )
    return new vCard()

  /** @type {String} Version number */
  this.version = vCard.versions[ vCard.versions.length - 1 ]
  /** @type {Object} Card data */
  this.data = {}

}

/**
 * vCard MIME type
 * @type {String}
 */
vCard.mimeType = 'text/vcard'

/**
 * vCard file extension
 * @type {String}
 */
vCard.extension = '.vcf'

/**
 * vCard versions
 * @type {Array}
 */
vCard.versions = [ '2.1', '3.0', '4.0' ]

/**
 * Folds a long line according to the RFC 5322.
 * @see http://tools.ietf.org/html/rfc5322#section-2.1.1
 * @param  {String}  input
 * @param  {Number}  maxLength
 * @param  {Boolean} hardWrap
 * @return {String}
 */
vCard.foldLine = require( 'foldline' )

/**
 * Normalizes input (cast to string, line folding, whitespace)
 * @param  {String} input
 * @return {String}
 */
vCard.normalize = function( input ) {
  return ( input + '' )
    // Trim whitespace
    .replace( /^[\s\r\n]+|[\s\r\n]+$/g, '' )
    // Trim blank lines
    .replace( /(\r\n)[\x09\x20]?(\r\n)|$/g, '$1' )
    // Unfold folded lines
    .replace( /\r\n[\x20\x09]/g, "" )
}

/**
 * Check whether a given version is supported
 * @param  {String} version
 * @return {Boolean}
 */
vCard.isSupported = function( version ) {
  return /^\d\.\d$/.test( version ) &&
    vCard.versions.indexOf( version ) !== -1
}

/**
 * Parses a string or buffer into a vCard object
 * @param  {String|Buffer} value
 * @return {Array<vCard>}
 */
vCard.parse = function( value ) {

  var objects = ( value + '' ).split( /(?=BEGIN\:VCARD)/gi )
  var cards = []

  for( var i = 0; i < objects.length; i++ ) {
    cards.push( new vCard().parse( objects[i] ) )
  }

  return cards

}

/**
 * Parse an array of vcf formatted lines
 * @internal used by `vCard#parse()`
 * @type {Function}
 */
vCard.parseLines = require( './parse-lines' )

/**
 * Constructs a vCard from jCard data
 * @param  {Array} jcard
 * @return {vCard}
 */
vCard.fromJSON = function( jcard ) {

  jcard = typeof jcard === 'string' ?
    JSON.parse( jcard ) : jcard

  if( jcard == null || !Array.isArray( jcard ) )
    return new vCard()

  if( !/vcard/i.test( jcard[0] ) )
    throw new Error( 'Object not in jCard format' )

  var card = new vCard()

  jcard[1].forEach( function( prop ) {
    card.addProperty( vCard.Property.fromJSON( prop ) )
  })

  return card

}

/**
 * Format a card object according to the given version
 * @param  {vCard}  card
 * @param  {String} version
 * @return {String}
 */
vCard.format = function( card, version ) {

  version = version || card.version ||
    vCard.versions[ vCard.versions.length - 1 ]

  if( !vCard.isSupported( version ) )
    throw new Error( 'Unsupported vCard version "' + version + '"' )

  var vcf = []

  vcf.push( 'BEGIN:VCARD' )
  vcf.push( 'VERSION:' + version )

  var props = Object.keys( card.data )
  var prop = ''

  for( var i = 0; i < props.length; i++ ) {
    if( props[i] === 'version' ) continue;
    prop = card.data[ props[i] ]
    if( Array.isArray( prop ) ) {
      for( var k = 0; k < prop.length; k++ ) {
        if( prop[k].isEmpty() ) continue
        vcf.push( vCard.foldLine( prop[k].toString( version ), 75 ) )
      }
    } else if( !prop.isEmpty() ) {
      vcf.push( vCard.foldLine( prop.toString( version ), 75 ) )
    }
  }

  vcf.push( 'END:VCARD' )

  return vcf.join( '\n' )

}

// vCard Property constructor
vCard.Property = require( './property' )

/**
 * vCard prototype
 * @type {Object}
 */
vCard.prototype = {

  constructor: vCard,

  /**
   * Get a vCard property
   * @param  {String} key
   * @return {Object|Array}
   */
  get: function( key ) {

    if( this.data[ key ] == null ) {
      return this.data[ key ]
    }

    if( Array.isArray( this.data[ key ] ) ) {
      return this.data[ key ].map( function( prop ) {
        return prop.clone()
      })
    } else {
      return this.data[ key ].clone()
    }

  },

  /**
   * Set a vCard property
   * @param {String} key
   * @param {String} value
   * @param {Object} params
   */
  set: function( key, value, params ) {
    return this.setProperty( new vCard.Property( key, value, params ) )
  },

  /**
   * Add a vCard property
   * @param {String} key
   * @param {String} value
   * @param {Object} params
   */
  add: function( key, value, params ) {
    var prop = new vCard.Property( key, value, params )
    this.addProperty( prop )
    return this
  },

  /**
   * Set a vCard property from an already
   * constructed vCard.Property
   * @param {vCard.Property} prop
   */
  setProperty: function( prop ) {
    this.data[ prop._field ] = prop
    return this
  },

  /**
   * Add a vCard property from an already
   * constructed vCard.Property
   * @param {vCard.Property} prop
   */
  addProperty: function( prop ) {

    var key = prop._field

    if( Array.isArray( this.data[ key ] ) ) {
      this.data[ key ].push( prop )
    } else if( this.data[ key ] != null ) {
      this.data[ key ] = [ this.data[ key ], prop ]
    } else {
      this.data[ key ] = prop
    }

    return this

  },

  /**
   * Parse a vcf formatted vCard
   * @param  {String} value
   * @return {vCard}
   */
  parse: function( value ) {

    // Normalize & split
    var lines = vCard.normalize( value )
      .split( /\r?\n/g )

    // Keep begin and end markers
    // for eventual error messages
    var begin = lines[0]
    var version = lines[1]
    var end = lines[ lines.length - 1 ]

    if( !/BEGIN:VCARD/i.test( begin ) )
      throw new SyntaxError( 'Invalid vCard: Expected "BEGIN:VCARD" but found "'+ begin +'"' )

    if( !/END:VCARD/i.test( end ) )
      throw new SyntaxError( 'Invalid vCard: Expected "END:VCARD" but found "'+ end +'"' )

    // TODO: For version 2.1, the VERSION can be anywhere between BEGIN & END
    if( !/VERSION:\d\.\d/i.test( version ) )
      throw new SyntaxError( 'Invalid vCard: Expected "VERSION:\\d.\\d" but found "'+ version +'"' )

    this.version = version.substring( 8, 11 )

    if( !vCard.isSupported( this.version ) )
      throw new Error( 'Unsupported version "' + this.version + '"' )

    this.data = vCard.parseLines( lines )

    return this

  },

  /**
   * Format the vCard as vcf with given version
   * @param  {String} version
   * @param  {String} charset
   * @return {String}
   */
  toString: function( version, charset ) {
    version = version || this.version
    return vCard.format( this, version )
  },

  /**
   * Format the card as jCard
   * @param {String} version='4.0'
   * @return {Array} jCard
   */
  toJCard: function( version ) {

    version = version || '4.0'

    var keys = Object.keys( this.data )
    var data = [ [ 'version', {}, 'text', version ] ]
    var prop = null

    for( var i = 0; i < keys.length; i++ ) {
      if( keys[i] === 'version' ) continue;
      prop = this.data[ keys[i] ]
      if( Array.isArray( prop ) ) {
        for( var k = 0; k < prop.length; k++ ) {
          data.push( prop[k].toJSON() )
        }
      } else {
        data.push( prop.toJSON() )
      }
    }

    return [ 'vcard', data ]

  },

  /**
   * Format the card as jCard
   * @return {Array} jCard
   */
  toJSON: function() {
    return this.toJCard( this.version )
  },

}

// Exports
module.exports = vCard
<commit_msg>force \r\n for delimiting properties as specified in RFC,<commit_after>/**
 * vCard
 * @constructor
 * @return {vCard}
 */
function vCard() {

  if( !(this instanceof vCard) )
    return new vCard()

  /** @type {String} Version number */
  this.version = vCard.versions[ vCard.versions.length - 1 ]
  /** @type {Object} Card data */
  this.data = {}

}

/**
 * vCard MIME type
 * @type {String}
 */
vCard.mimeType = 'text/vcard'

/**
 * vCard file extension
 * @type {String}
 */
vCard.extension = '.vcf'

/**
 * vCard versions
 * @type {Array}
 */
vCard.versions = [ '2.1', '3.0', '4.0' ]

/**
 * Folds a long line according to the RFC 5322.
 * @see http://tools.ietf.org/html/rfc5322#section-2.1.1
 * @param  {String}  input
 * @param  {Number}  maxLength
 * @param  {Boolean} hardWrap
 * @return {String}
 */
vCard.foldLine = require( 'foldline' )

/**
 * Normalizes input (cast to string, line folding, whitespace)
 * @param  {String} input
 * @return {String}
 */
vCard.normalize = function( input ) {
  return ( input + '' )
    // Trim whitespace
    .replace( /^[\s\r\n]+|[\s\r\n]+$/g, '' )
    // Trim blank lines
    .replace( /(\r\n)[\x09\x20]?(\r\n)|$/g, '$1' )
    // Unfold folded lines
    .replace( /\r\n[\x20\x09]/g, "" )
}

/**
 * Check whether a given version is supported
 * @param  {String} version
 * @return {Boolean}
 */
vCard.isSupported = function( version ) {
  return /^\d\.\d$/.test( version ) &&
    vCard.versions.indexOf( version ) !== -1
}

/**
 * Parses a string or buffer into a vCard object
 * @param  {String|Buffer} value
 * @return {Array<vCard>}
 */
vCard.parse = function( value ) {

  var objects = ( value + '' ).split( /(?=BEGIN\:VCARD)/gi )
  var cards = []

  for( var i = 0; i < objects.length; i++ ) {
    cards.push( new vCard().parse( objects[i] ) )
  }

  return cards

}

/**
 * Parse an array of vcf formatted lines
 * @internal used by `vCard#parse()`
 * @type {Function}
 */
vCard.parseLines = require( './parse-lines' )

/**
 * Constructs a vCard from jCard data
 * @param  {Array} jcard
 * @return {vCard}
 */
vCard.fromJSON = function( jcard ) {

  jcard = typeof jcard === 'string' ?
    JSON.parse( jcard ) : jcard

  if( jcard == null || !Array.isArray( jcard ) )
    return new vCard()

  if( !/vcard/i.test( jcard[0] ) )
    throw new Error( 'Object not in jCard format' )

  var card = new vCard()

  jcard[1].forEach( function( prop ) {
    card.addProperty( vCard.Property.fromJSON( prop ) )
  })

  return card

}

/**
 * Format a card object according to the given version
 * @param  {vCard}  card
 * @param  {String} version
 * @return {String}
 */
vCard.format = function( card, version ) {

  version = version || card.version ||
    vCard.versions[ vCard.versions.length - 1 ]

  if( !vCard.isSupported( version ) )
    throw new Error( 'Unsupported vCard version "' + version + '"' )

  var vcf = []

  vcf.push( 'BEGIN:VCARD' )
  vcf.push( 'VERSION:' + version )

  var props = Object.keys( card.data )
  var prop = ''

  for( var i = 0; i < props.length; i++ ) {
    if( props[i] === 'version' ) continue;
    prop = card.data[ props[i] ]
    if( Array.isArray( prop ) ) {
      for( var k = 0; k < prop.length; k++ ) {
        if( prop[k].isEmpty() ) continue
        vcf.push( vCard.foldLine( prop[k].toString( version ), 75 ) )
      }
    } else if( !prop.isEmpty() ) {
      vcf.push( vCard.foldLine( prop.toString( version ), 75 ) )
    }
  }

  vcf.push( 'END:VCARD' )

  return vcf.join( '\n' )

}

// vCard Property constructor
vCard.Property = require( './property' )

/**
 * vCard prototype
 * @type {Object}
 */
vCard.prototype = {

  constructor: vCard,

  /**
   * Get a vCard property
   * @param  {String} key
   * @return {Object|Array}
   */
  get: function( key ) {

    if( this.data[ key ] == null ) {
      return this.data[ key ]
    }

    if( Array.isArray( this.data[ key ] ) ) {
      return this.data[ key ].map( function( prop ) {
        return prop.clone()
      })
    } else {
      return this.data[ key ].clone()
    }

  },

  /**
   * Set a vCard property
   * @param {String} key
   * @param {String} value
   * @param {Object} params
   */
  set: function( key, value, params ) {
    return this.setProperty( new vCard.Property( key, value, params ) )
  },

  /**
   * Add a vCard property
   * @param {String} key
   * @param {String} value
   * @param {Object} params
   */
  add: function( key, value, params ) {
    var prop = new vCard.Property( key, value, params )
    this.addProperty( prop )
    return this
  },

  /**
   * Set a vCard property from an already
   * constructed vCard.Property
   * @param {vCard.Property} prop
   */
  setProperty: function( prop ) {
    this.data[ prop._field ] = prop
    return this
  },

  /**
   * Add a vCard property from an already
   * constructed vCard.Property
   * @param {vCard.Property} prop
   */
  addProperty: function( prop ) {

    var key = prop._field

    if( Array.isArray( this.data[ key ] ) ) {
      this.data[ key ].push( prop )
    } else if( this.data[ key ] != null ) {
      this.data[ key ] = [ this.data[ key ], prop ]
    } else {
      this.data[ key ] = prop
    }

    return this

  },

  /**
   * Parse a vcf formatted vCard
   * @param  {String} value
   * @return {vCard}
   */
  parse: function( value ) {

    // Normalize & split
    var lines = vCard.normalize( value )
      .split( /\r\n/g )

    // Keep begin and end markers
    // for eventual error messages
    var begin = lines[0]
    var version = lines[1]
    var end = lines[ lines.length - 1 ]

    if( !/BEGIN:VCARD/i.test( begin ) )
      throw new SyntaxError( 'Invalid vCard: Expected "BEGIN:VCARD" but found "'+ begin +'"' )

    if( !/END:VCARD/i.test( end ) )
      throw new SyntaxError( 'Invalid vCard: Expected "END:VCARD" but found "'+ end +'"' )

    // TODO: For version 2.1, the VERSION can be anywhere between BEGIN & END
    if( !/VERSION:\d\.\d/i.test( version ) )
      throw new SyntaxError( 'Invalid vCard: Expected "VERSION:\\d.\\d" but found "'+ version +'"' )

    this.version = version.substring( 8, 11 )

    if( !vCard.isSupported( this.version ) )
      throw new Error( 'Unsupported version "' + this.version + '"' )

    this.data = vCard.parseLines( lines )

    return this

  },

  /**
   * Format the vCard as vcf with given version
   * @param  {String} version
   * @param  {String} charset
   * @return {String}
   */
  toString: function( version, charset ) {
    version = version || this.version
    return vCard.format( this, version )
  },

  /**
   * Format the card as jCard
   * @param {String} version='4.0'
   * @return {Array} jCard
   */
  toJCard: function( version ) {

    version = version || '4.0'

    var keys = Object.keys( this.data )
    var data = [ [ 'version', {}, 'text', version ] ]
    var prop = null

    for( var i = 0; i < keys.length; i++ ) {
      if( keys[i] === 'version' ) continue;
      prop = this.data[ keys[i] ]
      if( Array.isArray( prop ) ) {
        for( var k = 0; k < prop.length; k++ ) {
          data.push( prop[k].toJSON() )
        }
      } else {
        data.push( prop.toJSON() )
      }
    }

    return [ 'vcard', data ]

  },

  /**
   * Format the card as jCard
   * @return {Array} jCard
   */
  toJSON: function() {
    return this.toJCard( this.version )
  },

}

// Exports
module.exports = vCard
<|endoftext|>


<commit_before>"use strict";

var _ = require("underscore");
var $$ = require("substance-application").$$;
var Fragmenter = require("substance-util").Fragmenter;
var Annotator = require("substance-document").Annotator;

var NodeView = require("../node").View;

function _getAnnotationBehavior(doc) {
  var annotationBehavior = doc.getAnnotationBehavior();
  if (!annotationBehavior) {
    throw new Error("Missing AnnotationBehavior.");
  }
  return annotationBehavior;
}

var __id__ = 0;

var TextView = function(node, renderer, options) {
  NodeView.call(this, node);
  options = options || {};

  // NOTE: left for debugging purposes
  this.__id__ = __id__++;
  // console.log("Creating text view...", this.__id__);

  this.property = options.property || "content";
  this.propertyPath = options.propertyPath || [this.node.id, this.property];

  this.$el.addClass('content-node text');

  if (node.type === "text") {
    this.$el.addClass("text-node");
  }

  // If TextView is used to display a custom property,
  // we don't have an id. Only full-fledged text nodes
  // have id's.
  if (options.property) {
    // Note: currently NodeView sets the id. In this mode the must not be set
    // as we are displaying a textish property of a node, not a text node.
    // IMO it is ok to have the id set by default, as it is the 99% case.
    this.$el.removeAttr('id');
    this.$el.removeClass('content-node');
    this.$el.addClass(options.property);
  }

  this.$el.attr('data-path', this.property);

  this._annotations = {};
  this.annotationBehavior = _getAnnotationBehavior(node.document);

  // Note: due to (nested) annotations this DOM node is fragmented
  // into several child nodes which contain a primitive DOM TextNodes.
  // We wrap each of these nodes into a Fragment object.
  // A Fragment object offers the chance to override things like the
  // interpreted length or the manipulation behavior.
  this._fragments = [];
};

var _findTextEl;

TextView.Prototype = function() {

  var __super__ = NodeView.prototype;
  // Rendering
  // =============================
  //

  this.render = function(enhancer) {
    __super__.render.call(this, enhancer);

    this.renderContent();

    return this;
  };

  this.renderContent = function() {
    // console.log("TextView.renderContent", this.__id__);
    this.content.innerHTML = "";
    this._annotations = this.node.document.getIndex("annotations").get(this.propertyPath);
    this.renderWithAnnotations(this._annotations);

    // EXPERIMENTAL: trying to fix some issues that we think other implementations handle
    // with a trailing

    var br = window.document.createElement("BR");
    this.content.appendChild(br);

  };

  this.insert = function(pos, str) {
    // console.log("TextView.insert",pos, str, this.__id__);
    var result = this._lookupPostion(pos);
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;
    text = text.substring(0, offset) + str + text.substring(offset);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos += str.length;
    }
  };

  this.delete = function(pos, length) {
    // console.log("TextView.delete", pos, length, this.__id__);
    if (length === 0) {
      console.log("FIXME: received empty deletion which could be avoided.")
      return;
    }

    var result = this._lookupPostion(pos, "delete");
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;

    // can not do this incrementally if it is a greater delete
    if (offset+length > text.length) {
      this.renderContent();
      return;
    }

    text = text.substring(0, offset) + text.substring(offset+length);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos -= length;
    }
  };

  // Lookup a fragment for the given position.
  // ----
  // For insertions, the annotation level is considered on annotation boundaries,
  // i.e., if the annotation is exclusive, then the outer element/fragment is returned.
  // For deletions the annotation exclusivity is not important
  // i.e., the position belongs to the next fragment
  this._lookupPostion = function(pos, is_delete) {
    var frag, l;
    for (var i = 0; i < this._fragments.length; i++) {
      frag = this._fragments[i];
      l = frag.getLength();

      // right in the middle of a fragment
      if (pos < l) {
        return [frag, pos];
      }
      // the position is not within this fragment
      else if (pos > l) {
        pos -= l;
      }
      // ... at the boundary we consider the element's level
      else {
        var next = this._fragments[i+1];
        // if the element level of the next fragment is lower then we put the cursor there
        if (next && next.level < frag.level || is_delete) {
          return [next, 0];
        }
        // otherwise we leave the cursor in the current fragment
        else {
          return [frag, l];
        }
      }
    }
    return [frag, l];
  };

  this.onNodeUpdate = function(op) {
    if (_.isEqual(op.path, this.propertyPath)) {
      // console.log("Updating text view: ", op);
      if (op.type === "update") {
        var update = op.diff;
        if (update.isInsert()) {
          this.insert(update.pos, update.str);
          return true;
        } else if (update.isDelete()) {
          this.delete(update.pos, update.str.length);
          return true;
        }
      } else if (op.type === "set") {
        this.renderContent();
        return true;
      }
    }
    return false;
  };

  this.onGraphUpdate = function(op, graph, options) {

    // HACK: chronicled operations need to be rendered non-incrementally
    if(options && options["chronicle"]) {
      console.log("non incremental update as it is a chronicled op");
      this.renderContent();
      return true;
    }

    // Call super handler and return if that has processed the operation already
    if (__super__.onGraphUpdate.call(this, op)) {
      return true;
    }

    // Otherwise deal with annotation changes
    if (Annotator.changesAnnotations(this.node.document, op, this.propertyPath)) {
      if (op.type === "create" || op.type === "delete" ||
          op.path[1] === "path" || op.path[1] === "range") {

        // NOTE: the last condition applies to all annotation range updates
        // However, due to the incremental nature of this implementation
        // it is not necessary to trigger a full rerender when content has been
        // changed incrementally.
        if (op.data && op.data["incremental"]) {
          // console.log("... change is incremental");
          return false;
        }

        // console.log("Rerendering TextView due to annotation update", op);
        this.renderContent();
        return true;
      }
    }

    return false;
  };

  this.createAnnotationElement = function(entry) {
    var el;
    if (entry.type === "link") {
      el = $$('a.annotation.'+entry.type, {
        id: entry.id,
        href: this.node.document.get(entry.id).url // "http://zive.at"
      });
    } else {
      el = $$('span.annotation.'+entry.type, {
        id: entry.id
      });
    }

    return el;
  };

  this.renderWithAnnotations = function(annotations) {
    var self = this;
    var text = this.node.document.get(this.propertyPath);
    var fragment = window.document.createDocumentFragment();

    // this splits the text and annotations into smaller pieces
    // which is necessary to generate proper HTML.
    var fragmenter = new Fragmenter(this.annotationBehavior.levels);

    this._fragments = [];

    var _entry = null;
    var _index = 0;
    var _charPos = 0;
    var _level  = 0;

    fragmenter.onText = function(context, text) {
      var el = window.document.createTextNode(text);

      // Note: we create the data structures to allow lookup fragments
      // for coordinate mapping and incremental changes
      // TODO: to override the Fragment behavior we would need to override this
      self._fragments.push(new TextView.DefaultFragment(el, _index++, _charPos, _level));
      _charPos += text.length;
      context.appendChild(el);
    };

    fragmenter.onEnter = function(entry, parentContext) {
      _entry = entry;
      _level++;
      var el = self.createAnnotationElement(entry);
      parentContext.appendChild(el);
      return el;
    };

    fragmenter.onExit = function() {
      _level--;
    };

    // this calls onText and onEnter in turns...
    fragmenter.start(fragment, text, annotations);

    // set the content
    this.content.innerHTML = "";
    this.content.appendChild(fragment);
  };

  // Free the memory
  // --------

  this.dispose = function() {
    this.stopListening();
  };
};

TextView.Prototype.prototype = NodeView.prototype;
TextView.prototype = new TextView.Prototype();

var _unshiftAll = function(arr, other) {
  for (var i = 0; i < other.length; i++) {
    arr.unshift(other[i]);
  }
};

_findTextEl = function(el, pos) {
  var childNodes = [];
  _unshiftAll(childNodes, el.childNodes);

  while(childNodes.length) {
    var next = childNodes.shift();
    if (next.nodeType === window.Node.TEXT_NODE) {
      var t = next.textContent;
      if (t.length >= pos) {
        return [next, pos];
      } else {
        pos -= t.length;
      }
    } else {
      _unshiftAll(childNodes, next.childNodes);
    }
  }
};

TextView.Fragment = function(el, index, charPos, level) {
  this.el = el;

  // the position in the fragments array
  this.index = index;

  this.charPos = charPos;

  // Note: the level is used to determine the behavior at element boundaries.
  // Basiscally, for character positions at the boundaries, a manipulation is done
  // in the node with lower level.
  this.level = level;

};

TextView.Fragment.Prototype = function() {
  this.getLength = function() {
    throw new Error("This method is abstract");
  };
};
TextView.Fragment.prototype = new TextView.Fragment.Prototype();

TextView.DefaultFragment = function() {
  TextView.Fragment.apply(this, arguments);
};
TextView.DefaultFragment.Prototype = function() {
  this.getLength = function() {
    return this.el.length;
  };
};
TextView.DefaultFragment.Prototype.prototype = TextView.Fragment.prototype;
TextView.DefaultFragment.prototype = new TextView.DefaultFragment.Prototype();

module.exports = TextView;
<commit_msg>Not a real change.<commit_after>"use strict";

var _ = require("underscore");
var $$ = require("substance-application").$$;
var Fragmenter = require("substance-util").Fragmenter;
var Annotator = require("substance-document").Annotator;

var NodeView = require("../node").View;

function _getAnnotationBehavior(doc) {
  var annotationBehavior = doc.getAnnotationBehavior();
  if (!annotationBehavior) {
    throw new Error("Missing AnnotationBehavior.");
  }
  return annotationBehavior;
}

var __id__ = 0;

var TextView = function(node, renderer, options) {
  NodeView.call(this, node);
  options = options || {};

  // NOTE: left for debugging purposes
  this.__id__ = __id__++;
  // console.log("Creating text view...", this.__id__);

  this.property = options.property || "content";
  this.propertyPath = options.propertyPath || [this.node.id, this.property];

  this.$el.addClass('content-node text');

  if (node.type === "text") {
    this.$el.addClass("text-node");
  }

  // If TextView is used to display a custom property,
  // we don't have an id. Only full-fledged text nodes
  // have id's.
  if (options.property) {
    // Note: currently NodeView sets the id. In this mode the must not be set
    // as we are displaying a textish property of a node, not a text node.
    // IMO it is ok to have the id set by default, as it is the 99% case.
    this.$el.removeAttr('id');
    this.$el.removeClass('content-node');
    this.$el.addClass(options.property);
  }

  this.$el.attr('data-path', this.property);

  this._annotations = {};
  this.annotationBehavior = _getAnnotationBehavior(node.document);

  // Note: due to (nested) annotations this DOM node is fragmented
  // into several child nodes which contain a primitive DOM TextNodes.
  // We wrap each of these nodes into a Fragment object.
  // A Fragment object offers the chance to override things like the
  // interpreted length or the manipulation behavior.
  this._fragments = [];
};

var _findTextEl;

TextView.Prototype = function() {

  var __super__ = NodeView.prototype;
  // Rendering
  // =============================
  //

  this.render = function(enhancer) {
    __super__.render.call(this, enhancer);

    this.renderContent();

    return this;
  };

  this.renderContent = function() {
    // console.log("TextView.renderContent", this.__id__);
    this.content.innerHTML = "";
    this._annotations = this.node.document.getIndex("annotations").get(this.propertyPath);
    this.renderWithAnnotations(this._annotations);

    // EXPERIMENTAL: trying to fix some issues that we think other implementations handle
    // with a trailing

    var br = window.document.createElement("BR");
    this.content.appendChild(br);

  };

  this.insert = function(pos, str) {
    // console.log("TextView.insert",pos, str, this.__id__);
    var result = this._lookupPostion(pos);
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;
    text = text.substring(0, offset) + str + text.substring(offset);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos += str.length;
    }
  };

  this.delete = function(pos, length) {
    // console.log("TextView.delete", pos, length, this.__id__);
    if (length === 0) {
      console.log("FIXME: received empty deletion which could be avoided.")
      return;
    }

    var result = this._lookupPostion(pos, "delete");
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;

    // can not do this incrementally if it is a greater delete
    if (offset+length > text.length) {
      this.renderContent();
      return;
    }

    text = text.substring(0, offset) + text.substring(offset+length);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos -= length;
    }
  };

  // Lookup a fragment for the given position.
  // ----
  // For insertions, the annotation level is considered on annotation boundaries,
  // i.e., if the annotation is exclusive, then the outer element/fragment is returned.
  // For deletions the annotation exclusivity is not important
  // i.e., the position belongs to the next fragment
  this._lookupPostion = function(pos, is_delete) {
    var frag, l;
    for (var i = 0; i < this._fragments.length; i++) {
      frag = this._fragments[i];
      l = frag.getLength();

      // right in the middle of a fragment
      if (pos < l) {
        return [frag, pos];
      }
      // the position is not within this fragment
      else if (pos > l) {
        pos -= l;
      }
      // ... at the boundary we consider the element's level
      else {
        var next = this._fragments[i+1];
        // if the element level of the next fragment is lower then we put the cursor there
        if (next && next.level < frag.level || is_delete) {
          return [next, 0];
        }
        // otherwise we leave the cursor in the current fragment
        else {
          return [frag, l];
        }
      }
    }
    return [frag, l];
  };

  this.onNodeUpdate = function(op) {
    if (_.isEqual(op.path, this.propertyPath)) {
      // console.log("Updating text view: ", op);
      if (op.type === "update") {
        var update = op.diff;
        if (update.isInsert()) {
          this.insert(update.pos, update.str);
          return true;
        } else if (update.isDelete()) {
          this.delete(update.pos, update.str.length);
          return true;
        }
      } else if (op.type === "set") {
        this.renderContent();
        return true;
      }
    }
    return false;
  };

  this.onGraphUpdate = function(op, graph, options) {

    // chronicled operations need to be rendered non-incrementally
    if(options && options["chronicle"]) {
      // console.log("non incremental update as it is a chronicled op");
      this.renderContent();
      return true;
    }

    // Call super handler and return if that has processed the operation already
    if (__super__.onGraphUpdate.call(this, op)) {
      return true;
    }

    // Otherwise deal with annotation changes
    if (Annotator.changesAnnotations(this.node.document, op, this.propertyPath)) {
      if (op.type === "create" || op.type === "delete" ||
          op.path[1] === "path" || op.path[1] === "range") {

        // NOTE: the last condition applies to all annotation range updates
        // However, due to the incremental nature of this implementation
        // it is not necessary to trigger a full rerender when content has been
        // changed incrementally.
        if (op.data && op.data["incremental"]) {
          // console.log("... change is incremental");
          return false;
        }

        // console.log("Rerendering TextView due to annotation update", op);
        this.renderContent();
        return true;
      }
    }

    return false;
  };

  this.createAnnotationElement = function(entry) {
    var el;
    if (entry.type === "link") {
      el = $$('a.annotation.'+entry.type, {
        id: entry.id,
        href: this.node.document.get(entry.id).url // "http://zive.at"
      });
    } else {
      el = $$('span.annotation.'+entry.type, {
        id: entry.id
      });
    }

    return el;
  };

  this.renderWithAnnotations = function(annotations) {
    var self = this;
    var text = this.node.document.get(this.propertyPath);
    var fragment = window.document.createDocumentFragment();

    // this splits the text and annotations into smaller pieces
    // which is necessary to generate proper HTML.
    var fragmenter = new Fragmenter(this.annotationBehavior.levels);

    this._fragments = [];

    var _entry = null;
    var _index = 0;
    var _charPos = 0;
    var _level  = 0;

    fragmenter.onText = function(context, text) {
      var el = window.document.createTextNode(text);

      // Note: we create the data structures to allow lookup fragments
      // for coordinate mapping and incremental changes
      // TODO: to override the Fragment behavior we would need to override this
      self._fragments.push(new TextView.DefaultFragment(el, _index++, _charPos, _level));
      _charPos += text.length;
      context.appendChild(el);
    };

    fragmenter.onEnter = function(entry, parentContext) {
      _entry = entry;
      _level++;
      var el = self.createAnnotationElement(entry);
      parentContext.appendChild(el);
      return el;
    };

    fragmenter.onExit = function() {
      _level--;
    };

    // this calls onText and onEnter in turns...
    fragmenter.start(fragment, text, annotations);

    // set the content
    this.content.innerHTML = "";
    this.content.appendChild(fragment);
  };

  // Free the memory
  // --------

  this.dispose = function() {
    this.stopListening();
  };
};

TextView.Prototype.prototype = NodeView.prototype;
TextView.prototype = new TextView.Prototype();

var _unshiftAll = function(arr, other) {
  for (var i = 0; i < other.length; i++) {
    arr.unshift(other[i]);
  }
};

_findTextEl = function(el, pos) {
  var childNodes = [];
  _unshiftAll(childNodes, el.childNodes);

  while(childNodes.length) {
    var next = childNodes.shift();
    if (next.nodeType === window.Node.TEXT_NODE) {
      var t = next.textContent;
      if (t.length >= pos) {
        return [next, pos];
      } else {
        pos -= t.length;
      }
    } else {
      _unshiftAll(childNodes, next.childNodes);
    }
  }
};

TextView.Fragment = function(el, index, charPos, level) {
  this.el = el;

  // the position in the fragments array
  this.index = index;

  this.charPos = charPos;

  // Note: the level is used to determine the behavior at element boundaries.
  // Basiscally, for character positions at the boundaries, a manipulation is done
  // in the node with lower level.
  this.level = level;

};

TextView.Fragment.Prototype = function() {
  this.getLength = function() {
    throw new Error("This method is abstract");
  };
};
TextView.Fragment.prototype = new TextView.Fragment.Prototype();

TextView.DefaultFragment = function() {
  TextView.Fragment.apply(this, arguments);
};
TextView.DefaultFragment.Prototype = function() {
  this.getLength = function() {
    return this.el.length;
  };
};
TextView.DefaultFragment.Prototype.prototype = TextView.Fragment.prototype;
TextView.DefaultFragment.prototype = new TextView.DefaultFragment.Prototype();

module.exports = TextView;
<|endoftext|>


<commit_before>import 'babel-polyfill';
import Observer, {
  observer as globalObserver,
} from 'binary-common-utils/lib/observer';
import fs from 'fs';
import readline from 'readline';
import program from 'commander';
import WebSocket from 'ws';
import { LiveApi } from 'binary-live-api';
import Interpreter from './Interpreter';
import TicksService from '../common/TicksService';
import { version } from '../../../package.json';

const log = (...args) =>
  console.log(`${new Date().toLocaleTimeString()}:`, ...args); // eslint-disable-line no-console

process.on('unhandledRejection', e => log('Unhandled Rejection:', e));

setInterval(() => {}, 2147483647); // Keep node alive

export const createScope = () => {
  const observer = new Observer();
  const api = new LiveApi({
    connection: new WebSocket(
      process.env.ENDPOINT ||
        'wss://ws.binaryws.com/websockets/v3?l=en&app_id=1169',
    ),
  });

  const ticksService = new TicksService(api);

  return { observer, api, ticksService };
};

export const createInterpreter = () => new Interpreter(createScope());

let filename;

program
  .version(version)
  .usage('[filename]')
  .arguments('[filename]')
  .action(fn => {
    filename = fn;
  })
  .parse(process.argv);

const lineReader = readline.createInterface({
  input: filename ? fs.createReadStream(filename) : process.stdin,
});

let code = '';

const interpreter = createInterpreter();

globalObserver.register('Error', e => log(e));

globalObserver.register('Notify', e => log(`${e[0].toUpperCase()}: ${e[1]}`));

lineReader
  .on('line', line => {
    code += `${line}\n`;
  })
  .on('error', e => log(e))
  .on('close', () =>
    interpreter.run(code).then(v => log(v.data)).catch(e => log(e)),
  );
<commit_msg>Fix instantiation of live api for cli<commit_after>import 'babel-polyfill';
import Observer, {
  observer as globalObserver,
} from 'binary-common-utils/lib/observer';
import fs from 'fs';
import readline from 'readline';
import program from 'commander';
import websocket from 'ws';
import { LiveApi } from 'binary-live-api';
import Interpreter from './Interpreter';
import TicksService from '../common/TicksService';
import { version } from '../../../package.json';

const log = (...args) =>
  console.log(`${new Date().toLocaleTimeString()}:`, ...args); // eslint-disable-line no-console

process.on('unhandledRejection', e => log('Unhandled Rejection:', e));

setInterval(() => {}, 2147483647); // Keep node alive

export const createScope = () => {
  const observer = new Observer();
  const api = new LiveApi({
    websocket,
    appId: 1169,
  });

  const ticksService = new TicksService(api);

  return { observer, api, ticksService };
};

export const createInterpreter = () => new Interpreter(createScope());

let filename;

program
  .version(version)
  .usage('[filename]')
  .arguments('[filename]')
  .action(fn => {
    filename = fn;
  })
  .parse(process.argv);

const lineReader = readline.createInterface({
  input: filename ? fs.createReadStream(filename) : process.stdin,
});

let code = '';

const interpreter = createInterpreter();

globalObserver.register('Error', e => log(e));

globalObserver.register('Notify', e => log(`${e[0].toUpperCase()}: ${e[1]}`));

lineReader
  .on('line', line => {
    code += `${line}\n`;
  })
  .on('error', e => log(e))
  .on('close', () =>
    interpreter.run(code).then(v => log(v.data)).catch(e => log(e)),
  );
<|endoftext|>