block by nitaku cfa2cc08e583f640551c

Space-separated values (PEG.js)

Full Screen

An example of a simple Domain-Specific Language created with PEG.js.

The language is a sort-of TSV, where spaces separate cell values, and the header row is mandatory. The grammar allows C-like identifiers within the header, and integers only as values. A brutal check on the number of columns is also performed, to ensure the well-formedness of the code.

Valid code produces an array of objects representing the described data, that is then rendered as a table in a separate view.

Basic error messages are available in the editor’s status bar.

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>PEG.js example</title>
    <link rel="stylesheet" href="index.css">
    <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>
    
    <!-- your views go here -->
    <script src="AppView.js"></script>
    <script src="Editor.js"></script>
    <script src="Table.js"></script>
    
    <!-- your models go here -->
    <script src="Data.js"></script>
  </head>
  <body>
    <script src="index.js"></script>
  </body>
</html>

AppView.coffee

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

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


      table = new Table
        model: @model

      @el.appendChild(table.el)
      table.render()
      

AppView.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.AppView = Backbone.D3View.extend({
    initialize: function() {
      return d3.text('ssv.peg.js', (function(_this) {
        return function(grammar) {
          var editor, table;
          editor = new Editor({
            model: _this.model,
            grammar: grammar
          });
          _this.el.appendChild(editor.el);
          editor.render();
          table = new Table({
            model: _this.model
          });
          _this.el.appendChild(table.el);
          return table.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
    
    @textarea = @d3el.append 'textarea'
    @status_bar = @d3el.append 'div'
      .attr
        class: 'status_bar'
    
    @parser = PEG.buildParser conf.grammar
    
    # example code
    @textarea.node().value = '''
      Apples Oranges Coconuts
      5      5       1
      2      4       1
    '''
    @compile()
  
  compile: () ->
    @status_bar.text 'All ok.'
    @status_bar.classed 'error', false
    
    try
      rows = @parser.parse @textarea.node().value
      @model.set 'rows', rows
    catch e
      @status_bar.text "Line #{e.location.start.line}: #{e.message}"
      @status_bar.classed 'error', true
    

Editor.js

// Generated by CoffeeScript 1.10.0
(function() {
  window.Editor = Backbone.D3View.extend({
    namespace: null,
    tagName: 'div',
    events: {
      input: 'compile'
    },
    initialize: function(conf) {
      this.d3el.classed('editor', true);
      this.textarea = this.d3el.append('textarea');
      this.status_bar = this.d3el.append('div').attr({
        "class": 'status_bar'
      });
      this.parser = PEG.buildParser(conf.grammar);
      this.textarea.node().value = 'Apples Oranges Coconuts\n5      5       1\n2      4       1';
      return this.compile();
    },
    compile: function() {
      var e, error, rows;
      this.status_bar.text('All ok.');
      this.status_bar.classed('error', false);
      try {
        rows = this.parser.parse(this.textarea.node().value);
        return this.model.set('rows', rows);
      } catch (error) {
        e = error;
        this.status_bar.text("Line " + e.location.start.line + ": " + e.message);
        return this.status_bar.classed('error', true);
      }
    }
  });

}).call(this);

Table.coffee

window.Table = Backbone.D3View.extend
  namespace: null
  tagName: 'table'
  
  initialize: () ->
    @listenTo @model, 'change:rows', @render
  
  render: () ->
    rows_data = @model.get 'rows'
    
    header = if rows_data.length > 0 then Object.keys(rows_data[0]) else []
    
    # redraw
    @d3el.selectAll '*'
      .remove()
      
    header_cells = @d3el.append('tr').selectAll 'th'
      .data header
      
    header_cells.enter().append 'th'
      .text (d) -> d
      
    rows = @d3el.append('tbody').selectAll 'tr'
      .data rows_data
      
    rows.enter().append 'tr'
    
    data_cells = rows.selectAll 'td'
      .data (d) -> Object.keys(d).map (k) -> d[k]
      
    data_cells.enter().append 'td'
      .text (d) -> d
      
      

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

index.coffee

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

index.css

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

body {
  display: flex;
  flex-direction: row;
}
body > * {
  width: 0;
  flex-grow: 1;
}

.editor {
  display: flex;
  flex-direction: column;
  border-right: 2px solid gray;
}

textarea {
  flex-grow: 1;
  height: 0;
  resize: none;
  border: 0;
  outline: 0;
}

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

table, th, td {
  border-collapse: collapse;
  border: 1px solid #DDD;
  margin: 4px;
  padding: 4px;
}
th {
  background: #EEE;
  border: 1px solid#CCC;
  text-align: center;
}
td {
  text-align: right;
}

ssv.peg.js

// CSV-like format with mandatory header and integer fields
// also tries to check the number of columns, sometimes giving clumsy error messages
/*
a b c
1 2 3
4 5 6
*/
{
  var size;
}

start = h:Header rows:(Newline Row)* {
  var data = rows.map(function(d){
    var datum = {};
    h.forEach(function(k, i){
      datum[k] = d[1][i];
    });
    return datum;
  });
  return data;
}

Header 'header'
  = head:Id tails:(_ Id)* {
    var header = [head].concat(tails.map(function(d){ return d[1];}));
    size = header.length;
    return header;
  }

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

Row 'row'
  = head:Int tails:(_ Int)* {
    var row = [head].concat(tails.map(function(d){ return d[1];}));
    if(row.length < size)
      error('Row too short. Please add more values to match header length.');
    else if(row.length > size)
      error('Row too long. Please remove some values to match header length.');
    return row;
  }

Int 'integer'
  = [0-9]+ { return parseInt(text(),10); }

_ 'whitespace'
  = [ ]+

Newline 'newline'
  = '\n'