block by nitaku 4d2849e3eab28149b2540925d415df19

Inline BreakDown editor

Full Screen

-

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>BreakDown editor</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/4.6.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/4.6.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/4.6.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:annotations', @render

  render: () ->
    annotations_data = @model.get 'annotations'

    annotations = @d3el.selectAll '.annotation'
      .data annotations_data

    annotations.enter().append 'table'
      .attr
        class: 'annotation'

    annotations
      .html (d) ->
        # hide automatic IDs
        id = if d.id[0] is '_' then '' else d.id
        
        # hide undefined body
        body = if d.body? then d.body else ''
        
        return "<tr><td class='id'>#{id}</td><td class='text'>#{d.text.replace(/\n/g,'↵')}</td><td class='body'>#{body}</td></tr>"

    annotations.exit()
      .remove()

AnnotationView.css

.AnnotationView {
  overflow-y: auto;
}

.AnnotationView .annotation {
  font-family: sans-serif;
  font-size: 12px;
  margin: 4px;
  border: 1px solid #DFDFDF;
  border-collapse: collapse;
}

.AnnotationView td {
  padding: 4px;
  min-width: 14px;
}

.AnnotationView .annotation .id {
  color: #555;
}
.AnnotationView .annotation .text {
  background: rgba(255, 165, 0, 0.15);
}
.AnnotationView .annotation .body {
  color: #444;
}

AnnotationView.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.AnnotationView = Backbone.D3View.extend({
    namespace: null,
    tagName: 'div',
    initialize: function() {
      this.d3el.attr({
        "class": 'AnnotationView'
      });
      return this.listenTo(this.model, 'change:annotations', this.render);
    },
    render: function() {
      var annotations, annotations_data;
      annotations_data = this.model.get('annotations');
      annotations = this.d3el.selectAll('.annotation').data(annotations_data);
      annotations.enter().append('table').attr({
        "class": 'annotation'
      });
      annotations.html(function(d) {
        var body, id;
        id = d.id[0] === '_' ? '' : d.id;
        body = d.body != null ? d.body : '';
        return "<tr><td class='id'>" + id + "</td><td class='text'>" + (d.text.replace(/\n/g, '↵')) + "</td><td class='body'>" + body + "</td></tr>";
      });
      return annotations.exit().remove();
    }
  });

}).call(this);

AppView.coffee

window.AppView = Backbone.D3View.extend
  initialize: () ->
    # retrieve the grammar from an external file
    d3.text 'breakdown.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('breakdown.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:
    rows: []
    

Data.js

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

}).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

    @editor = CodeMirror wrapper.node(), {
      #lineNumbers: true,
      lineWrapping: true,
      value: '''
        This is a new version of <<BreakDown>>(B), a language that can be used to select portions of text for <<annotation>>(A).

<<Nested <<spans>>(1) are allowed>>(2), as well as <<newlines
within spans>>(N). Moreover, <<BreakDown>>(B) <1<supports <2<a syntax>1>(ONE) for specifying overlapping spans>2>(TWO). You can also mark a portion of text as <<interesting>> without linking something to it.

The actual meaning of the <<annotation>>(A) is left open. <<BreakDown>>(B) merely links a body (the part between round brackets) with the section of the text surrounded by angle brackets.
      '''
    }

    @editor.on 'change', () =>
      @compile()

    @compile()

  render: () ->
    @editor.refresh()

  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()
      @spans_highlight(data.spans)
      @model.set 'annotations', data.spans
    catch e
      @status_bar.text "Line #{e.location.start.line}: #{e.message}"
      @status_bar.classed 'error', true

  spans_highlight: (spans) ->
    spans.forEach (s) =>
      @editor.markText {line: s.start_code_location.end.line-1, ch: s.start_code_location.end.column-1}, {line: s.end_code_location.start.line-1, ch: s.end_code_location.start.column-1}, {className: 'span'}
      
      @editor.markText {line: s.start_code_location.start.line-1, ch: s.start_code_location.start.column-1}, {line: s.start_code_location.end.line-1, ch: s.start_code_location.end.column-1}, {className: 'code'}
      @editor.markText {line: s.end_code_location.start.line-1, ch: s.end_code_location.start.column-1}, {line: s.end_code_location.end.line-1, ch: s.end_code_location.end.column-1}, {className: 'code'}

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 .span {
  background: rgba(255, 165, 0, 0.15);
}
.Editor .code {
  color: rgba(200, 76, 0, 0.8);
  font-weight: bold;
}
.Editor .error {
  background: rgba(255,0,0,0.2);
  border-bottom: 2px solid red;
}

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: 'This is a new version of <<BreakDown>>(B), a language that can be used to select portions of text for <<annotation>>(A).\n\n<<Nested <<spans>>(1) are allowed>>(2), as well as <<newlines\nwithin spans>>(N). Moreover, <<BreakDown>>(B) <1<supports <2<a syntax>1>(ONE) for specifying overlapping spans>2>(TWO). You can also mark a portion of text as <<interesting>> without linking something to it.\n\nThe actual meaning of the <<annotation>>(A) is left open. <<BreakDown>>(B) merely links a body (the part between round brackets) with the section of the text surrounded by angle brackets.'
      });
      this.editor.on('change', (function(_this) {
        return function() {
          return _this.compile();
        };
      })(this));
      return this.compile();
    },
    render: function() {
      return this.editor.refresh();
    },
    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.spans_highlight(data.spans);
        return this.model.set('annotations', data.spans);
      } catch (error) {
        e = error;
        this.status_bar.text("Line " + e.location.start.line + ": " + e.message);
        return this.status_bar.classed('error', true);
      }
    },
    spans_highlight: function(spans) {
      return spans.forEach((function(_this) {
        return function(s) {
          _this.editor.markText({
            line: s.start_code_location.end.line - 1,
            ch: s.start_code_location.end.column - 1
          }, {
            line: s.end_code_location.start.line - 1,
            ch: s.end_code_location.start.column - 1
          }, {
            className: 'span'
          });
          _this.editor.markText({
            line: s.start_code_location.start.line - 1,
            ch: s.start_code_location.start.column - 1
          }, {
            line: s.start_code_location.end.line - 1,
            ch: s.start_code_location.end.column - 1
          }, {
            className: 'code'
          });
          return _this.editor.markText({
            line: s.end_code_location.start.line - 1,
            ch: s.end_code_location.start.column - 1
          }, {
            line: s.end_code_location.end.line - 1,
            ch: s.end_code_location.end.column - 1
          }, {
            className: 'code'
          });
        };
      })(this));
    }
  });

}).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;
}));

breakdown.peg.js

// Version 2.3 "inline" 28/07/2017
{
  var plain_text_offset = 0;
  var unidentified_span_next_id = 0;
  var unidentified_spans_stack = [];
  var open_spans = {};
  var result = {
    spans: [],
    plain_text: ""
  };
  var last_span = undefined;
}

start = Doc {
  return result;
}

Doc 'document'
  = (Text / SpanOpen / SpanClose)*

SpanOpen = id:SpanOpenCode {
  if(id === "") {
    // store unidentified spans in stack
    unidentified_spans_stack.push({
      start: plain_text_offset,
      start_code_location: location()
    });
  }
  else {
    // store identified spans in an index
    open_spans[id] = {
      id: id,
      start: plain_text_offset,
      start_code_location: location()
    };
  }
}
SpanClose = d:SpanCloseCode {
  var id = d.id;
  
  if(id in open_spans) {
    // span found in index: move it to results
    last_span = open_spans[id];
    delete open_spans[id];
  }
  else {
    if(unidentified_spans_stack.length === 0) {
      error('Trying to close a span without opening it.');
    }
    else {
      // span found in stack: move it to results
      last_span = unidentified_spans_stack.pop();

      // give unidentified spans an ID (underscore as first character is not allowed by syntax)
      if(id === '') {
        id = '_'+unidentified_span_next_id;
        unidentified_span_next_id += 1;
      }
      last_span.id = id;
    }
  }

  last_span.end = plain_text_offset;
  last_span.end_code_location = location();
  last_span.text = result.plain_text.slice(last_span.start, last_span.end);
  
  if(d.body !== undefined) {
    last_span.body = d.body;
  }
  
  result.spans.push(last_span);
}

Text = NoSpanCode {
  result.plain_text += text();
  plain_text_offset += text().length;
}

NoSpanCode = (!SpanCode .)+ { return text(); }
SpanCode = SpanOpenCode / SpanCloseCode

SpanOpenCode = '<' id:NullableId '<' { return id; }
SpanCloseCode =
  '>' id:NullableId '>' body:Body?
  {
    return {id: id, body: body};
  }
  
Body = BodyOpenCode body:NoBodyCode BodyCloseCode { return body; }

NullableId 'nullable identifier'
  = $(Id / '') { return text(); }

Id 'identifier'
  = [a-zA-Z0-9][_a-zA-Z0-9]* { return text(); }

NoBodyCode = (!BodyCode .)+ { return text(); }
BodyCode = BodyOpenCode / BodyCloseCode

BodyOpenCode = '('
BodyCloseCode = ')'

index.coffee

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

index.css

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