Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active February 28, 2016 15:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/cfa2cc08e583f640551c to your computer and use it in GitHub Desktop.
Save nitaku/cfa2cc08e583f640551c to your computer and use it in GitHub Desktop.
Space-separated values (PEG.js)

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.

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()
// 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);
// 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;
}));
window.Data = Backbone.Model.extend
defaults:
rows: []
// Generated by CoffeeScript 1.10.0
(function() {
window.Data = Backbone.Model.extend({
defaults: {
rows: []
}
});
}).call(this);
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
// 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);
app = new AppView
el: 'body'
model: new Data
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;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PEG.js example</title>
<link rel="stylesheet" href="index.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="/webvis/tmp/peg-0.9.0.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://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>
// Generated by CoffeeScript 1.10.0
(function() {
var app;
app = new AppView({
el: 'body',
model: new Data
});
}).call(this);
// 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'
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
// 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment