block by nitaku 90165a2fca0dcc16541a3ffe02395da4

Clavius DSL

Full Screen

A simple example of a Domain-Specific Language for the transcription of manuscripts. Specific portions of text can be marked as written by the author ({}) or by the editor ([]), gaps ([...]) can be inserted whenever the text is too hard to read, and placeholders for figures ([fig]) can be inserted into the text stream. All of these phenomena could also be commented by adding a parenthesized string right after the symbols.

Finally, the --- symbol can be used on a line by itself to represent a break (like for example a page break).

index.js

// Generated by CoffeeScript 1.10.0
(function() {
  var app;

  app = new AppView({
    el: 'body',
    model: new Data
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Clavius DSL</title>
    <link rel="stylesheet" href="index.css">
    <link rel="stylesheet" href="AppView.css">
    <link rel="stylesheet" href="Editor.css">
    <link rel="stylesheet" href="AnnotationView.css">
    <link type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.min.css" rel="stylesheet"/>
    <link type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/styles/github.min.css" rel="stylesheet"/>

    <script src="//d3js.org/d3.v3.min.js"></script>
    <script src="/webvis/tmp/peg-0.9.0.min.js"></script>
    <script src="//underscorejs.org/underscore-min.js"></script>
    <script src="//backbonejs.org/backbone-min.js"></script>
    <script src="backbone.d3view.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.min.js"></script>
    <script src="//wafi.iit.cnr.it/webvis/tmp/codemirror_mode_simple.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/addon/search/searchcursor.min.js"></script>

    <!-- your views go here -->
    <script src="AppView.js"></script>
    <script src="Editor.js"></script>
    <script src="AnnotationView.js"></script>

    <!-- your models go here -->
    <script src="Data.js"></script>
  </head>
  <body>
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.3/highlight.min.js"></script>
    <script src="index.js"></script>
  </body>
</html>

AnnotationView.coffee

window.AnnotationView = Backbone.D3View.extend
  namespace: null
  tagName: 'div'

  initialize: () ->
    @d3el
      .attr
        class: 'AnnotationView'
    @listenTo @model, 'change', @render

  render: () ->
    data = @model.get 'data'
    
    insertions = {}
    
    insert = (ins) ->
      if not insertions[ins.offset]?
        insertions[ins.offset] = []
      insertions[ins.offset].push ins.content
        
    data.marks.forEach (d) ->
      comment = if d.comment? then d.comment.content else ''
      switch d.type
        when 'gap'
          insert
            offset: d.start
            content: "<span class='gap' title='Gap#{if comment then ':\n'+comment else ''}'>[...]</span>"
        when 'figure'
          insert
            offset: d.start
            content: "<div class='figure'>Figure#{if comment then ': '+comment else ''}</div>"
        when 'author_addition'
          insert
            offset: d.start
            content: "<span class='author_addition' title='Added by the author#{if comment then ':\n'+comment else ''}'>"
          insert
            offset: d.end
            content: "</span>"
        when 'editor_addition'
          insert
            offset: d.start
            content: "<span class='editor_addition' title='Added by the editor#{if comment then ':\n'+comment else ''}'>#{d.content}</span>"
        when 'break'
          insert
            offset: d.start
            content: "</div><div class='section'>"
        
    
    text = annotate data.plain_text+' ', insertions # FIXME adding a space is necessary to emit the last index
    
    text = '<div class="section">' + text + '</div>'
    
    @d3el.html text
    
annotate = (text, insertions) ->
  i = 0
  return text.replace /.|\n/g, (character, index) ->
    to_be_inserted = ''
    
    if insertions[index]?
      to_be_inserted += insertions[index].join('')
      
    if character is '\n'
      to_be_inserted += '<br>'
      
    to_be_inserted += character
    return to_be_inserted

AnnotationView.css

.AnnotationView {
  overflow-y: auto;
  padding: 6px;
  background: #444;
}

.AnnotationView .section{
  margin: 4px;
  padding: 8px;
  border: 1px solid #CCC;
  background: white;
}

.AnnotationView .gap, .AnnotationView .editor_addition {
  color: #999;
}
.AnnotationView .author_addition {
  border-bottom: 1px solid #BBB;
}
.AnnotationView .figure {
  max-width: 300px;
  border: 1px solid #CBB;
  background: #FFF7EE;
  display: inline-block;
  color: #877;
  font-family: sans-serif;
  font-size: 10px;
  padding: 6px;
  margin: 1px;
}

AnnotationView.js

// Generated by CoffeeScript 1.10.0
(function() {
  var annotate;

  window.AnnotationView = Backbone.D3View.extend({
    namespace: null,
    tagName: 'div',
    initialize: function() {
      this.d3el.attr({
        "class": 'AnnotationView'
      });
      return this.listenTo(this.model, 'change', this.render);
    },
    render: function() {
      var data, insert, insertions, text;
      data = this.model.get('data');
      insertions = {};
      insert = function(ins) {
        if (insertions[ins.offset] == null) {
          insertions[ins.offset] = [];
        }
        return insertions[ins.offset].push(ins.content);
      };
      data.marks.forEach(function(d) {
        var comment;
        comment = d.comment != null ? d.comment.content : '';
        switch (d.type) {
          case 'gap':
            return insert({
              offset: d.start,
              content: "<span class='gap' title='Gap" + (comment ? ':\n' + comment : '') + "'>[...]</span>"
            });
          case 'figure':
            return insert({
              offset: d.start,
              content: "<div class='figure'>Figure" + (comment ? ': ' + comment : '') + "</div>"
            });
          case 'author_addition':
            insert({
              offset: d.start,
              content: "<span class='author_addition' title='Added by the author" + (comment ? ':\n' + comment : '') + "'>"
            });
            return insert({
              offset: d.end,
              content: "</span>"
            });
          case 'editor_addition':
            return insert({
              offset: d.start,
              content: "<span class='editor_addition' title='Added by the editor" + (comment ? ':\n' + comment : '') + "'>" + d.content + "</span>"
            });
          case 'break':
            return insert({
              offset: d.start,
              content: "</div><div class='section'>"
            });
        }
      });
      text = annotate(data.plain_text + ' ', insertions);
      text = '<div class="section">' + text + '</div>';
      return this.d3el.html(text);
    }
  });

  annotate = function(text, insertions) {
    var i;
    i = 0;
    return text.replace(/.|\n/g, function(character, index) {
      var to_be_inserted;
      to_be_inserted = '';
      if (insertions[index] != null) {
        to_be_inserted += insertions[index].join('');
      }
      if (character === '\n') {
        to_be_inserted += '<br>';
      }
      to_be_inserted += character;
      return to_be_inserted;
    });
  };

}).call(this);

AppView.coffee

window.AppView = Backbone.D3View.extend
  initialize: () ->
    # retrieve the grammar from an external file
    d3.text 'clavius.peg.js', (grammar) =>
      editor = new Editor
        model: @model
        grammar: grammar

      @el.appendChild(editor.el)
      editor.render()

      aw = new AnnotationView
        model: @model

      @el.appendChild(aw.el)
      aw.render()

AppView.css

body {
  display: flex;
  flex-direction: row;
}
body > * {
  width: 0;
  flex-grow: 1;
}
.Editor {
  border-right: 1px solid gray;
}

AppView.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.AppView = Backbone.D3View.extend({
    initialize: function() {
      return d3.text('clavius.peg.js', (function(_this) {
        return function(grammar) {
          var aw, editor;
          editor = new Editor({
            model: _this.model,
            grammar: grammar
          });
          _this.el.appendChild(editor.el);
          editor.render();
          aw = new AnnotationView({
            model: _this.model
          });
          _this.el.appendChild(aw.el);
          return aw.render();
        };
      })(this));
    }
  });

}).call(this);

Data.coffee

window.Data = Backbone.Model.extend
  defaults:
    data: null
    

Data.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.Data = Backbone.Model.extend({
    defaults: {
      data: null
    }
  });

}).call(this);

Editor.coffee

window.Editor = Backbone.D3View.extend
  namespace: null
  tagName: 'div'

  events:
    input: 'compile'

  initialize: (conf) ->
    @d3el.classed 'Editor', true

    # Chrome bug workaround (https://github.com/codemirror/CodeMirror/issues/3679)
    editor_div = @d3el.append 'div'
      .attr
        class: 'editor_div'
      .style
        position: 'relative'

    wrapper = editor_div.append 'div'
      .style
        position: 'absolute'
        height: '100%'
        width: '100%'

    @status_bar = @d3el.append 'div'
      .attr
        class: 'status_bar'

    @parser = PEG.buildParser conf.grammar

    # create the CodeMirror editor
    @editor = CodeMirror wrapper.node(), {
      #lineNumbers: true,
      lineWrapping: true,
      value: '''
Di presente è uscito dalle stampe la mia interpretatione dell'obelisco egittiaco, già l'anno a dietro ritrovato quì in Roma; Io che desidero continuare la servitù che professo V[ostra] Altezza, cerco quasivoglia occasione per essercitarla; perciò le ne' lò mandato con il Latore presente un'essemplare, acciò che nel medesimo tempo che lei partecipa delle mie fatiche io sia fatto degno della sua humanità, allaquale degli accersce ogni di più autorità nel commendarmi. e con quella river[ente] bacio le mani pregandole da Dio {ogni contento.}(This is written on the right side of the page.)

[fig](This figure is added to showcase a feature of the DSL.)
---
Di V[ostra] Altezza Seren[issima].
Humilissimo e divotissimo
Athanasio Kircher [...]
      '''
    }
    window.editor = @editor
    
    @editor.on 'change', () =>
      @compile()

    @compile()

  render: () ->
    @editor.refresh()
    _.defer () => @editor.setSize() # FIXME hack for CodeMirror's linewrapping bug https://github.com/codemirror/CodeMirror/issues/1642

  compile: () ->
    @status_bar.text 'All ok.'
    @status_bar.classed 'error', false

    # clear highlighting
    @editor.getAllMarks().forEach (mark) ->
      mark.clear()

    try
      data = @parser.parse @editor.getValue()
      @highlight_all(data.marks)
      @model.set 'data', data, {silent: true}
      @model.trigger 'change'
    catch e
      @status_bar.text "Line #{e.location.start.line}: #{e.message}"
      @status_bar.classed 'error', true

  highlight_all: (marks) ->
    marks.forEach (d) =>
      switch d.type
        when 'gap'
          @highlight d.code_location, 'gap'
        when 'figure'
          @highlight d.code_location, 'figure'
        when 'comment'
          @highlight d.content_code_location, 'comment'
          @highlight d.opener_code_location, 'comment_symbol'
          @highlight d.closer_code_location, 'comment_symbol'
        when 'author_addition'
          @highlight d.content_code_location, 'author_addition'
          @highlight d.opener_code_location, 'author_addition_symbol'
          @highlight d.closer_code_location, 'author_addition_symbol'
        when 'editor_addition'
          @highlight d.content_code_location, 'editor_addition'
          @highlight d.opener_code_location, 'editor_addition_symbol'
          @highlight d.closer_code_location, 'editor_addition_symbol'
        when 'break'
          @highlight d.code_location, 'break'
          
  highlight: (loc, classname) ->
    @editor.markText {line: loc.start.line-1, ch: loc.start.column-1}, {line: loc.end.line-1, ch: loc.end.column-1}, {className: classname}

Editor.css

.Editor .status_bar {
  height: 22px;
  background: #DDD;
  border-top: 1px solid gray;
  font-family: sans-serif;
  font-size: 12px;
  padding: 4px;
  box-sizing: border-box;
}
.Editor .error {
  background: #F77;
}

.Editor {
  display: flex;
  flex-direction: column;
}
.Editor .editor_div {
  flex-grow: 1;
  height: 0;
}

.Editor .CodeMirror {
  height: 100%;
  font-size: 12px;
  font-family: sans-serif;
  line-height: 1.3;
  background: #F7F7F7;
  margin-left: 4px;
  margin-right: 4px;
}
.Editor .CodeMirror-gutters {
  border-right: 0;
}
.Editor .CodeMirror-lines > * {
  border: 1px solid #DDD;
  background: white;
}

.Editor .gap, .Editor .figure {
  background: rgba(183, 58, 58, 0.1);
  color: rgb(183, 58, 58);
  font-weight: bold;
}
.Editor .break {
  color: rgb(183, 58, 58);
  font-weight: bold;
}
.Editor .comment{
  color: rgba(183, 58, 58, 0.8);
  font-style: italic;
}
.Editor .comment_symbol, .Editor .author_addition_symbol, .Editor .editor_addition_symbol {
  color: rgb(183, 58, 58);
  font-weight: bold;
}
.Editor .editor_addition {
  color: #999;
}

.Editor .error {
  background: rgba(255,0,0,0.2);
  border-bottom: 2px solid red;
}
.Editor .code {
  font-family: monospace;
  font-size: 11px;
}

Editor.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.Editor = Backbone.D3View.extend({
    namespace: null,
    tagName: 'div',
    events: {
      input: 'compile'
    },
    initialize: function(conf) {
      var editor_div, wrapper;
      this.d3el.classed('Editor', true);
      editor_div = this.d3el.append('div').attr({
        "class": 'editor_div'
      }).style({
        position: 'relative'
      });
      wrapper = editor_div.append('div').style({
        position: 'absolute',
        height: '100%',
        width: '100%'
      });
      this.status_bar = this.d3el.append('div').attr({
        "class": 'status_bar'
      });
      this.parser = PEG.buildParser(conf.grammar);
      this.editor = CodeMirror(wrapper.node(), {
        lineWrapping: true,
        value: 'Di presente è uscito dalle stampe la mia interpretatione dell\'obelisco egittiaco, già l\'anno a dietro ritrovato quì in Roma; Io che desidero continuare la servitù che professo V[ostra] Altezza, cerco quasivoglia occasione per essercitarla; perciò le ne\' lò mandato con il Latore presente un\'essemplare, acciò che nel medesimo tempo che lei partecipa delle mie fatiche io sia fatto degno della sua humanità, allaquale degli accersce ogni di più autorità nel commendarmi. e con quella river[ente] bacio le mani pregandole da Dio {ogni contento.}(This is written on the right side of the page.)\n\n[fig](This figure is added to showcase a feature of the DSL.)\n---\nDi V[ostra] Altezza Seren[issima].\nHumilissimo e divotissimo\nAthanasio Kircher [...]'
      });
      window.editor = this.editor;
      this.editor.on('change', (function(_this) {
        return function() {
          return _this.compile();
        };
      })(this));
      return this.compile();
    },
    render: function() {
      this.editor.refresh();
      return _.defer((function(_this) {
        return function() {
          return _this.editor.setSize();
        };
      })(this));
    },
    compile: function() {
      var data, e, error;
      this.status_bar.text('All ok.');
      this.status_bar.classed('error', false);
      this.editor.getAllMarks().forEach(function(mark) {
        return mark.clear();
      });
      try {
        data = this.parser.parse(this.editor.getValue());
        this.highlight_all(data.marks);
        this.model.set('data', data, {
          silent: true
        });
        return this.model.trigger('change');
      } catch (error) {
        e = error;
        this.status_bar.text("Line " + e.location.start.line + ": " + e.message);
        return this.status_bar.classed('error', true);
      }
    },
    highlight_all: function(marks) {
      return marks.forEach((function(_this) {
        return function(d) {
          switch (d.type) {
            case 'gap':
              return _this.highlight(d.code_location, 'gap');
            case 'figure':
              return _this.highlight(d.code_location, 'figure');
            case 'comment':
              _this.highlight(d.content_code_location, 'comment');
              _this.highlight(d.opener_code_location, 'comment_symbol');
              return _this.highlight(d.closer_code_location, 'comment_symbol');
            case 'author_addition':
              _this.highlight(d.content_code_location, 'author_addition');
              _this.highlight(d.opener_code_location, 'author_addition_symbol');
              return _this.highlight(d.closer_code_location, 'author_addition_symbol');
            case 'editor_addition':
              _this.highlight(d.content_code_location, 'editor_addition');
              _this.highlight(d.opener_code_location, 'editor_addition_symbol');
              return _this.highlight(d.closer_code_location, 'editor_addition_symbol');
            case 'break':
              return _this.highlight(d.code_location, 'break');
          }
        };
      })(this));
    },
    highlight: function(loc, classname) {
      return this.editor.markText({
        line: loc.start.line - 1,
        ch: loc.start.column - 1
      }, {
        line: loc.end.line - 1,
        ch: loc.end.column - 1
      }, {
        className: classname
      });
    }
  });

}).call(this);

Table.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.Table = Backbone.D3View.extend({
    namespace: null,
    tagName: 'table',
    initialize: function() {
      return this.listenTo(this.model, 'change:rows', this.render);
    },
    render: function() {
      var data_cells, header, header_cells, rows, rows_data;
      rows_data = this.model.get('rows');
      header = rows_data.length > 0 ? Object.keys(rows_data[0]) : [];
      this.d3el.selectAll('*').remove();
      header_cells = this.d3el.append('tr').selectAll('th').data(header);
      header_cells.enter().append('th').text(function(d) {
        return d;
      });
      rows = this.d3el.append('tbody').selectAll('tr').data(rows_data);
      rows.enter().append('tr');
      data_cells = rows.selectAll('td').data(function(d) {
        return Object.keys(d).map(function(k) {
          return d[k];
        });
      });
      return data_cells.enter().append('td').text(function(d) {
        return d;
      });
    }
  });

}).call(this);

backbone.d3view.js

// Backbone.D3View.js 0.3.1
// ---------------

//     (c) 2015 Adam Krebs
//     Backbone.D3View may be freely distributed under the MIT license.
//     For all details and documentation:
//     https://github.com/akre54/Backbone.D3View

(function (factory) {
  if (typeof define === 'function' && define.amd) { define(['backbone', 'd3'], factory);
  } else if (typeof exports === 'object') { module.exports = factory(require('backbone'), require('d3'));
  } else { factory(Backbone, d3); }
}(function (Backbone, d3) {

  // Cached regex to match an opening '<' of an HTML tag, possibly left-padded
  // with whitespace.
  var paddedLt = /^\s*</;

  var ElementProto = (typeof Element !== 'undefined' && Element.prototype) || {};
  var matchesSelector = ElementProto.matches ||
    ElementProto.webkitMatchesSelector ||
    ElementProto.mozMatchesSelector ||
    ElementProto.msMatchesSelector ||
    ElementProto.oMatchesSelector;

  Backbone.D3ViewMixin = {

    // A reference to the d3 selection backing the view.
    d3el: null,

    namespace: d3.ns.prefix.svg,

    $: function(selector) {
      return this.el.querySelectorAll(selector);
    },

    $$: function(selector) {
      return this.d3el.selectAll(selector);
    },

    _removeElement: function() {
      this.undelegateEvents();
      this.d3el.remove();
    },

    _createElement: function(tagName) {
      var ns = typeof this.namespace === 'function' ? this.namespace() : this.namespace;
      return ns ?
         document.createElementNS(ns, tagName) :
         document.createElement(tagName);
    },

    _setElement: function(element) {
      if (typeof element == 'string') {
        if (paddedLt.test(element)) {
          var el = document.createElement('div');
          el.innerHTML = element;
          this.el = el.firstChild;
        } else {
          this.el = document.querySelector(element);
        }
      } else {
        this.el = element;
      }

      this.d3el = d3.select(this.el);
    },

    _setAttributes: function(attributes) {
      this.d3el.attr(attributes);
    },

    // `delegate` supports two- and three-arg forms. The `selector` is optional.
    delegate: function(eventName, selector, listener) {
      if (listener === undefined) {
        listener = selector;
        selector = null;
      }

      var view = this;
      var wrapped = function(event) {
        var node = event.target,
            idx = 0,
            o = d3.event;

        d3.event = event;

        // The `event` object is stored in `d3.event` but Backbone expects it as
        // the first argument to the listener.
        if (!selector) {
          listener.call(view, d3.event, node.__data__, idx++);
          d3.event = o;
          return;
        }

        while (node && node !== view.el) {
          if (matchesSelector.call(node, selector)) {
            listener.call(view, d3.event, node.__data__, idx++);
          }
          node = node.parentNode;
        }
        d3.event = o;
      };

      var map = this._domEvents || (this._domEvents = {});
      var handlers = map[eventName] || (map[eventName] = []);
      handlers.push({selector: selector, listener: listener, wrapped: wrapped});

      this.el.addEventListener(eventName, wrapped, false);
      return this;
    },

    undelegate: function(eventName, selector, listener) {
      if (!this._domEvents || !this._domEvents[eventName]) return;

      if (typeof selector !== 'string') {
        listener = selector;
        selector = null;
      }

      var handlers = this._domEvents[eventName].slice();
      var i = handlers.length;
      while (i--) {
        var handler = handlers[i];

        var match = (listener ? handler.listener === listener : true) &&
            (selector ? handler.selector === selector : true);

        if (!match) continue;

        this.el.removeEventListener(eventName, handler.wrapped, false);
        this._domEvents[eventName].splice(i, 1);
      }
    },

    undelegateEvents: function() {
      var map = this._domEvents, el = this.el;
      if (!el || !map) return;

      Object.keys(map).forEach(function(eventName) {
        map[eventName].forEach(function(handler) {
          el.removeEventListener(eventName, handler.wrapped, false);
        });
      });

      this._domEvents = {};
      return this;
    }
  };

  Backbone.D3View = Backbone.View.extend(Backbone.D3ViewMixin);

  return Backbone.D3View;
}));

clavius.peg.js

{
  var plain_text_offset = 0;
  var result = {
    marks: [],
    plain_text: ""
  };
}

start = Body {
  //console.log(result);
  return result;
}

Body 'text body'
  = (Annotation / Break / TextChar)*
  
Annotation
  = o:Operator c:Comment? {
    o.comment = c;
    if(c) {
      c.mark = o;
    }
  }
    
Comment
  = '(' t:CommentText ')' {
    var loc = location();
    
    var o = {
      type: 'comment',
      start: plain_text_offset,
      end: plain_text_offset,
      text: '',
      code_location: loc,
      content: t,
      content_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column+1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column-1
        }
      },
      opener_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column
        },
        end: {
          line: loc.start.line,
          column: loc.start.column+1
        }
      },
      closer_code_location: {
        start: {
          line: loc.end.line,
          column: loc.end.column-1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column
        }
      }
    };
    result.marks.push(o);
    return o;
  }
  
CommentText
  = [^\)]* { return text(); }

Operator
  = Gap / Fig / AA / EA

Gap
  = ('[...]' / '[…]') {
      var o = {
        type: 'gap',
        start: plain_text_offset,
        end: plain_text_offset,
        text: '',
        code_location: location()
      };
      result.marks.push(o);
      return o;
    }

Fig
  = ('[fig]' / '[figure]') {
      var o = {
        type: 'figure',
        start: plain_text_offset,
        end: plain_text_offset,
        text: '',
        code_location: location()
      };
      result.marks.push(o);
      return o;
    }

AA
  = '{' c:[^}]* '}' {
    var loc = location();
    
    var start = plain_text_offset;
    var content = c.join('');
    
    result.plain_text += content;
    plain_text_offset += content.length;
    
    var o = {
      type: 'author_addition',
      start: start,
      end: plain_text_offset,
      text: content,
      code_location: loc,
      content: content,
      content_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column+1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column-1
        }
      },
      opener_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column
        },
        end: {
          line: loc.start.line,
          column: loc.start.column+1
        }
      },
      closer_code_location: {
        start: {
          line: loc.end.line,
          column: loc.end.column-1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column
        }
      }
    };
    result.marks.push(o);
    return o;
  }

EA
  = '[' c:[^\]]* ']' {
    var loc = location();
    var content = c.join('');
    var o = {
      type: 'editor_addition',
      start: plain_text_offset,
      end: plain_text_offset,
      text: '',
      code_location: loc,
      content: content,
      content_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column+1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column-1
        }
      },
      opener_code_location: {
        start: {
          line: loc.start.line,
          column: loc.start.column
        },
        end: {
          line: loc.start.line,
          column: loc.start.column+1
        }
      },
      closer_code_location: {
        start: {
          line: loc.end.line,
          column: loc.end.column-1
        },
        end: {
          line: loc.end.line,
          column: loc.end.column
        }
      }
    };
    result.marks.push(o);
    return o;
  }

Break
  = '---\n' {
      var o = {
        type: 'break',
        start: plain_text_offset,
        end: plain_text_offset,
        text: '',
        code_location: location()
      };
      result.marks.push(o);
      return o;
    }

TextChar 'text character'
  = . {
    result.plain_text += text();
    plain_text_offset += text().length; // should be always 1
  }

index.coffee

app = new AppView
  el: 'body'
  model: new Data

index.css

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}