A first implementation of the BreakDown language, a DSL for text annotation made with PEG.js.
Last active
September 13, 2019 10:59
-
-
Save nitaku/46237e6510654522c946f611abed3c90 to your computer and use it in GitHub Desktop.
BreakDown language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.AnnotationView = Backbone.D3View.extend | |
namespace: null | |
tagName: 'table' | |
initialize: () -> | |
@listenTo @model, 'change:annotations', @render | |
render: () -> | |
annotations_data = @model.get 'annotations' | |
annotations = @d3el.selectAll '.annotation' | |
.data annotations_data | |
annotations.enter().append 'div' | |
.attr | |
class: 'annotation' | |
annotations | |
.html (d) -> "<span class='id'>#{d.id}</span> <span class='#{d.type}'>#{d.text.replace(/\n/g,'↵')}</span>" | |
annotations.exit() | |
.remove() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
window.AnnotationView = Backbone.D3View.extend({ | |
namespace: null, | |
tagName: 'table', | |
initialize: function() { | |
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('div').attr({ | |
"class": 'annotation' | |
}); | |
annotations.html(function(d) { | |
return "<span class='id'>" + d.id + "</span> <span class='" + d.type + "'>" + (d.text.replace(/\n/g, '↵')) + "</span>"; | |
}); | |
return annotations.exit().remove(); | |
} | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
var plain_text_offset = 0; | |
var unidentified_span_next_id = 0; | |
var unidentified_spans_stack = []; | |
var open_spans = {}; | |
var result = { | |
spans: [], | |
plain_text: "" | |
}; | |
} | |
start = (TripleSection / Body) (Newlines (TripleSection / Body))* { | |
if(unidentified_spans_stack.length > 0 || Object.keys(open_spans).length > 0 ) { | |
error('Span not closed at end of input.'); // FIXME better error handling: show the line of the first unclosed span | |
} | |
return result; | |
} | |
Body 'text body' | |
= (Text / Operator)* | |
TripleSection 'triple section' | |
= '+++' (Newlines Triples)? Newlines '+++' | |
Triples | |
= Triple (Newlines Triple)* | |
Triple | |
= Subject Spaces Predicate Spaces Object | |
Subject = Id | |
Predicate = (!(Spaces) .)+ | |
Object = (!(Newlines) .)+ | |
Operator | |
= SpanOpen / SpanClose | |
SpanOpen | |
= id:SpanOpenCode { | |
if(id === "") { | |
// store unidentified spans in stack | |
unidentified_spans_stack.push({ | |
type: 'span', | |
start: plain_text_offset, | |
code_location: location() | |
}); | |
} | |
else { | |
// store identified spans in an index | |
open_spans[id] = { | |
type: 'span', | |
id: id, | |
start: plain_text_offset, | |
code_location: location() | |
}; | |
} | |
} | |
SpanClose | |
= id:SpanCloseCode { | |
var span; | |
if(id in open_spans) { | |
// span found in index: move it to results | |
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 | |
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; | |
} | |
span.id = id; | |
} | |
} | |
span.end = plain_text_offset; | |
span.text = result.plain_text.slice(span.start, span.end); | |
result.spans.push(span); | |
} | |
NoText | |
= SpanOpenCode / SpanCloseCode / '\n+++' / '+++\n' / '+++' | |
SpanOpenCode = '<' id:NullableId '<' { return id; } | |
SpanCloseCode = '>' id:NullableId '>' { return id; } | |
Newlines = [ \t\r\n]+ | |
Spaces = [ \t]+ | |
NullableId 'nullable identifier' | |
= $(Id / '') { return text(); } | |
Id 'identifier' | |
= [a-zA-Z0-9][_a-zA-Z0-9]* { return text(); } | |
Text 'text node' | |
= (!NoText .)+ { | |
result.plain_text += text(); | |
plain_text_offset += text().length; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
window.Data = Backbone.Model.extend | |
defaults: | |
annotations: [] | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
window.Data = Backbone.Model.extend({ | |
defaults: { | |
annotations: [] | |
} | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = ''' | |
This is <<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> <3<supports <4<a syntax>3> for overlapping spans>4>. | |
Actual annotations are based on RDF triples, and are included in one or more sections like the following one: | |
+++ | |
A foaf:page https://en.wikipedia.org/wiki/Annotation | |
+++ | |
''' | |
@compile() | |
compile: () -> | |
@status_bar.text 'All ok.' | |
@status_bar.classed 'error', false | |
try | |
data = @parser.parse @textarea.node().value | |
@model.set 'annotations', data.spans | |
catch e | |
@status_bar.text "Line #{e.location.start.line}: #{e.message}" | |
@status_bar.classed 'error', true | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 = 'This is <<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> <3<supports <4<a syntax>3> for overlapping spans>4>.\n\nActual annotations are based on RDF triples, and are included in one or more sections like the following one:\n+++\nA foaf:page https://en.wikipedia.org/wiki/Annotation\n+++'; | |
return this.compile(); | |
}, | |
compile: function() { | |
var data, e, error; | |
this.status_bar.text('All ok.'); | |
this.status_bar.classed('error', false); | |
try { | |
data = this.parser.parse(this.textarea.node().value); | |
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); | |
} | |
} | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app = new AppView | |
el: 'body' | |
model: new Data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
.annotation { | |
font-family: sans-serif; | |
font-size: 12px; | |
margin: 8px; | |
margin-bottom: 12px; | |
} | |
.annotation .id { | |
color: #555; | |
} | |
.annotation .span { | |
background: #B9DBF3; | |
padding: 4px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>BreakDown language</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="AnnotationView.js"></script> | |
<!-- your models go here --> | |
<script src="Data.js"></script> | |
</head> | |
<body> | |
<script src="index.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Generated by CoffeeScript 1.10.0 | |
(function() { | |
var app; | |
app = new AppView({ | |
el: 'body', | |
model: new Data | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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