anchor.diff:      if (text.indexOf("\r\n") !== -1) {
         delim = "\r\n";
       }//if
  +
  +    var STATES = {
  +      UNKNOWN: 0,
  +      TOPICS_NEED_HEADERS: 1,
  +      SYNAPSES_NEED_HEADERS: 2,
  +      TOPICS: 3,
  +      SYNAPSES: 4,
  +    };
  +
  +    // state & lines determine parser behaviour
  +    var state = STATES.UNKNOWN;
       var lines = text.split(delim);
  -
  -    // get csv-style headers to name the object fields
  -    var headers = lines[0].split('	'); //tab character
  -
  -    var results = [];
  -    lines.forEach(function(line, index) {
  -      if (index === 0) return;
  -      if (line == "") return;
  -
  -      var topic = {};
  -      line.split("	").forEach(function(field, index) {
  -				if (self.headersWhitelist.indexOf(headers[index]) === -1) return;
  -        topic[headers[index]] = field;
  +    var results = { topics: [], synapses: [] }
  +    var topicHeaders = [];
  +    var synapseHeaders = [];
  +
  +    lines.forEach(function(line_raw, index) {
  +      var line = line_raw.split('	'); // tab character
  +      var noblanks = line.filter(function(elt) {
  +        return elt !== "";
         });
  -      results.push(topic);
  -    });
  +      switch(state) {
  +        case STATES.UNKNOWN:
  +          if (noblanks.length === 0) {
  +            state = STATES.UNKNOWN;
  +            break;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') {
  +            state = STATES.TOPICS_NEED_HEADERS;
  +            break;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') {
  +            state = STATES.SYNAPSES_NEED_HEADERS;
  +            break;
  +          }
  +          state = STATES.TOPICS_NEED_HEADERS;
  +          // FALL THROUGH - if we're not sure what to do, pretend
  +          // we're on the TOPICS_NEED_HEADERS state and parse some headers
  +
  +        case STATES.TOPICS_NEED_HEADERS:
  +          if (noblanks.length < 2) {
  +            return self.abort("Not enough topic headers on line " + index);
  +          }
  +          topicHeaders = line.map(function(header, index) {
  +            return header.toLowerCase().replace('description', 'desc');
  +          });
  +          state = STATES.TOPICS;
  +          break;
  +
  +        case STATES.SYNAPSES_NEED_HEADERS:
  +          if (noblanks.length < 2) {
  +            return self.abort("Not enough synapse headers on line " + index);
  +          }
  +          synapseHeaders = line.map(function(header, index) {
  +            return header.toLowerCase().replace('description', 'desc');
  +          });
  +          state = STATES.SYNAPSES;
  +          break;
  +
  +        case STATES.TOPICS:
  +          if (noblanks.length === 0) {
  +            state = STATES.UNKNOWN;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') {
  +            state = STATES.TOPICS_NEED_HEADERS;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') {
  +            state = STATES.SYNAPSES_NEED_HEADERS;
  +          } else {
  +            var topic = {};
  +            line.forEach(function(field, index) {
  +              var header = topicHeaders[index];
  +              if (self.topicWhitelist.indexOf(header) === -1) return;
  +              topic[header] = field;
  +              if (header === 'x' || header === 'y') {
  +                topic[header] = parseInt(topic[header]);
  +              }//if
  +            });
  +            results.topics.push(topic);
  +          }
  +          break;
  +
  +        case STATES.SYNAPSES:
  +          if (noblanks.length === 0) {
  +            state = STATES.UNKNOWN;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') {
  +            state = STATES.TOPICS_NEED_HEADERS;
  +          } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') {
  +            state = STATES.SYNAPSES_NEED_HEADERS;
  +          } else {
  +            var synapse = {};
  +            line.forEach(function(field, index) {
  +              var header = synapseHeaders[index];
  +              if (self.synapseWhitelist.indexOf(header) === -1) return;
  +              synapse[header] = field;
  +              if (header === 'topic1' || header === 'topic2') {
  +                synapse[header] = parseInt(header);
  +              }//if
  +            });
  +            results.synapses.push(synapse);
  +          }
  +          break;
  +
  +        default:
  +          return self.abort("Invalid state while parsing import data. " +
  +                            "Check code.");
  +      }
  +    });
  +
       return results;
     },
   
  +
  +  importTopics: function(parsedTopics) {
  +    var self = Metamaps.Import;
  +
  +    // up to 25 topics: scale 100
  +    // up to 81 topics: scale 200
  +    // up to 169 topics: scale 300
  +    var scale = Math.floor((Math.sqrt(parsedTopics.length) - 1) / 4) * 100;
  +    if (scale < 100) scale = 100;
  +    var autoX = -scale;
  +    var autoY = -scale;
  +
  +    parsedTopics.forEach(function(topic) {
  +      var x, y;
  +      if (topic.x && topic.y) {
  +        x = topic.x;
  +        y = topic.y;
  +      } else {
  +        x = autoX;
  +        y = autoY;
  +        autoX += 50;
  +        if (autoX > scale) {
  +          autoY += 50;
  +          autoX = -scale;
  +        }
  +      }
  +
  +      self.createTopicWithParameters(
  +        topic.name, topic.metacode, topic.permission,
  +        topic.desc, topic.link, x, y, topic.id
  +      );
  +    });
  +  },
  +
  +  importSynapses: function(parsedSynapses) {
  +    var self = Metamaps.Import;
  +
  +    parsedSynapses.forEach(function(synapse) {
  +      self.createSynapseWithParameters(
  +        synapse.desc, synapse.category, synapse.permission,
  +        synapse.topic1, synapse.topic2
  +      );
  +    });
  +  },
  +
     createTopicWithParameters: function(name, metacode_name, permission, desc,
  -                                      link, xloc, yloc) {
  -    var self = Metamaps.Topic;
  -
  +                                      link, xloc, yloc, import_id) {
  +    $(document).trigger(Metamaps.Map.events.editedByActiveMapper);
       var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null;
       if (metacode === null) return console.error("metacode not found");
   
  
positive.code:       metacode_id: metacode.id,
        permission: permission || Metamaps.Active.Map.get('permission'),
        desc: desc,
        link: link
      });
      Metamaps.Topics.add(topic);
  
  
negative.code:  */
  
  Metamaps.Import = {
    topicWhitelist: [
      'name', 'metacode', 'description', 'link', 'permission'
  	],
    synapseWhitelist: [
      'desc', 'description', 'category', 'topic1', 'topic2', 'permission'
    ],
  
    init: function() {
      var self = Metamaps.Import;
      $('body').bind('paste', function(e) {
        var text = e.originalEvent.clipboardData.getData('text/plain');
  
        var results = self.parseTabbedString(text);
        var topics = results.topics;
        var synapses = results.synapses;
  
        if (topics.length > 0 || synapses.length > 0) {
          if (confirm("Are you sure you want to create " + topics.length +
                      " new topics and " + synapses.length + " new synapses?")) {
            self.importTopics(topics);
            self.importSynapses(synapses);
          }//if
        }//if
      });
    },
  
    abort: function(message) {
      console.error(message);
    },
  
    parseTabbedString: function(text) {
  