Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active November 8, 2019 07:14
Show Gist options
  • Save nitaku/4d2849e3eab28149b2540925d415df19 to your computer and use it in GitHub Desktop.
Save nitaku/4d2849e3eab28149b2540925d415df19 to your computer and use it in GitHub Desktop.
Inline BreakDown editor
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 {
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;
}
// 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);
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()
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('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);
// 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;
}));
// 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 = ')'
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
# 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 .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;
}
// 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);
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>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="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/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>
// Generated by CoffeeScript 1.10.0
(function() {
var app;
app = new AppView({
el: 'body',
model: new Data
});
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment