Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active July 22, 2016 14:59
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/90165a2fca0dcc16541a3ffe02395da4 to your computer and use it in GitHub Desktop.
Save nitaku/90165a2fca0dcc16541a3ffe02395da4 to your computer and use it in GitHub Desktop.
Clavius DSL

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

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 {
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;
}
// 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);
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()
body {
display: flex;
flex-direction: row;
}
body > * {
width: 0;
flex-grow: 1;
}
.Editor {
border-right: 1px solid gray;
}
// 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);
// 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;
}));
{
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
}
window.Data = Backbone.Model.extend
defaults:
data: null
// Generated by CoffeeScript 1.10.0
(function() {
window.Data = Backbone.Model.extend({
defaults: {
data: null
}
});
}).call(this);
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 .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;
}
// 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);
app = new AppView
el: 'body'
model: new Data
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
<!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="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>
<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>
// Generated by CoffeeScript 1.10.0
(function() {
var app;
app = new AppView({
el: 'body',
model: new Data
});
}).call(this);
// 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