-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmrs.js
377 lines (322 loc) · 14.5 KB
/
mrs.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
function MRS(parentElement, mrsData){
// Constant pixel sizes used
const MAXWIDTH = 600; // width before a list of elements is wrapped
const XGAP = 5; // horizontal gap between elements
const YGAP = 5; // vertical gap between 1-line features
const YGAPSTRUCT = 10; // vertical gap between full feature structures
const BRACKETYPAD = 5; // distance bracket extents above/below box
const BRACKETXWIDTH = 5; // width of right-angle corner thing
const ANGLEHEIGHT = 50; // Height of angle brackets
const ANGLEWIDTH = XGAP; // Width of angle brackets
const FEATNAMEXGAP = 80;
// Global offset for keeping track of where next thing needs to be drawn on
// the Y axis.
var CURRENTY = 0;
function drawMrs() {
var svg = SVG(parentElement);
var mrs = svg.group();
var container = mrs.group();
CURRENTY = 0;
drawFeatStructType(container, '');
drawFeatValPair(container, 'TOP', mrsData.top);
drawFeatValPair(container, 'INDEX', mrsData.index);
drawFeatValPair(container, 'RELS', mrsData.relations);
drawFeatValPair(container, 'HCONS', mrsData.constraints);
drawFeatValPair(container, 'ICONS', mrsData.constraints);
drawSquareBrackets(mrs, XGAP);
// transform the MRS to take into account the square brackets drawn at
// negative coordinates
mrs.transform({x:XGAP+10, y:BRACKETYPAD});
// update the dimensions of the final SVG to match those of the now
// rendered MRS
var finalBbox = mrs.tbox();
svg.size(finalBbox.width+10, finalBbox.height+5);
return svg.node;
}
function drawFeatStructType(parent, name) {
var text = parent.plain(name).y(CURRENTY).attr({'font-style':'italic'});
CURRENTY += text.tbox().height;
return text;
}
function drawFeatValPair(parent, name, value) {
var group = parent.group();
if (typeof value === 'string' || value instanceof String) {
// value is a string
var featText = group.plain(name).y(CURRENTY);
var attrs = {'title': value,
'font-style': 'italic'};
if (name != 'CARG'){
// If it's not 'CARG' then it's a variable
attrs['class'] = 'variable';
attrs['data-var'] = value;
}
var featVal = group.plain(value).move(FEATNAMEXGAP, CURRENTY).attr(attrs);
} else if (Object.prototype.toString.call(value) === '[object Array]'){
// value is a list
if (name == 'RELS') {
var itemFunc = relFeatStruct;
} else if (name == 'HCONS') {
// filter out ICONS values from the constraints list
var value = value.filter(constraint => constraint.high != null && constraint.high.match(/h(andle)?\d+/));
var itemFunc = hconsFeatStruct;
} else if (name == 'ICONS') {
// filter out HCONS values from the constraints list
var value = value.filter(constraint => constraint.high == null || !constraint.high.match(/h(andle)?\d+/));
var itemFunc = iconsFeatStruct;
}
if (value.length == 0)
// no constraints needed to render
return null;
var featText = group.plain(name);
var list = drawList(group, value, itemFunc);
var firstLine = list.select('g').first();
featText.cy(firstLine.tbox().cy);
// transform the group to make space
var diff = group.previous().tbox().y2 - list.tbox().y;
if (diff > 0)
group.transform({y:diff + YGAPSTRUCT});
}
CURRENTY += group.tbox().height;
return group;
}
function relFeatStruct(parent, rel) {
var pred = rel['predicate'] +'⟨'+rel.lnk.from+':'+rel.lnk.to+'⟩';
drawFeatStructType(parent, pred);
drawFeatValPair(parent, 'LBL', rel['label']);
var args = Object.keys(rel.arguments).sort(rargcompare);
for (var j=0; j < args.length; j++) {
var arg = args[j];
drawFeatValPair(parent, arg, rel.arguments[arg]);
}
}
function hconsFeatStruct(parent, hcon) {
drawFeatStructType(parent, hcon.relation);
drawFeatValPair(parent, 'HARG', hcon.high);
drawFeatValPair(parent, 'LARG', hcon.low);
}
function iconsFeatStruct(parent, icon) {
drawFeatStructType(parent, icon.relation);
Object.keys(icon).forEach(function(key,index) {
if (key == 'relation')
return;
drawFeatValPair(parent, key.toUpperCase(), icon[key]);
});
}
function drawList(parent, itemsData, itemFunc) {
// The list may need to be broken up into multiple lines
var lines = [];
var startX = FEATNAMEXGAP + BRACKETXWIDTH;
var currX = startX;
var currY = 0;
var list = parent.group();
var leftAngle = list.polyline();
var line = list.group();
// draw all the items. note that x and y coordinates for item drawing are
// relative to the parent group containing the whole list
for (var i=0; i < itemsData.length; i++) {
CURRENTY = 0;
var itemGroup = line.group();
itemFunc(itemGroup, itemsData[i]);
drawSquareBrackets(itemGroup, 5);
itemGroup.transform({x:currX, y:currY});
// update item offsets
if (i == itemsData.length - 1) {
// we're done with items
break;
} else if (currX >= MAXWIDTH) {
// Starting a new line of items
currX = startX;
currY += line.tbox().height + YGAPSTRUCT;
lines.push(line);
line = list.group();
} else {
// move along by last itemGroup width
currX += itemGroup.tbox().width + 2*XGAP;
}
}
lines.push(line);
//vertically align the items in each line
for (var i=0; i < lines.length; i++) {
var thisLine = lines[i];
thisLine.cy = thisLine.tbox().cy;
var items = thisLine.children();
for (var j=0; j < items.length; j++) {
// align each item vertically and add vertically aligned comma.
// need to account for negative y values using BRACKETYPAD
var item = items[j];
item.cy(thisLine.cy + BRACKETYPAD);
var tbox = item.tbox();
var comma = item.plain(',').center(tbox.width-1, -BRACKETYPAD + tbox.height/2);
}
}
// remove trailing comma
comma.remove();
var firstLine = lines[0];
//update left angle dimensions, now we know where the midpoint is
var firstRelTbox = firstLine.children()[0].tbox();
leftAngle.plot([
[firstRelTbox.x - BRACKETXWIDTH, firstLine.cy + ANGLEHEIGHT/2],
[firstRelTbox.x - BRACKETXWIDTH - ANGLEWIDTH, firstLine.cy],
[firstRelTbox.x - BRACKETXWIDTH, firstLine.cy - ANGLEHEIGHT/2]]).fill('none').stroke('black');
//Add right angle bracket
var lastLine = lines[lines.length - 1];
var lastLineTbox = lastLine.tbox();
var rightAngle = line.polyline([
[lastLineTbox.x2 + BRACKETXWIDTH, lastLine.cy + ANGLEHEIGHT/2],
[lastLineTbox.x2 + BRACKETXWIDTH + ANGLEWIDTH, lastLine.cy],
[lastLineTbox.x2 + BRACKETXWIDTH, lastLine.cy - ANGLEHEIGHT/2]]).fill('none').stroke('black');
return list;
}
function drawSquareBrackets(element, xpad) {
// xpad -- distance bracket extends left/right of box
// Draw left and right square brackets
var tbox = element.tbox();
// left vertical line; top right angle; bottom right angle
element.line(tbox.x-xpad, tbox.y-BRACKETYPAD, tbox.x-xpad,
tbox.y2+BRACKETYPAD).stroke({width:1});
element.line(tbox.x-xpad, tbox.y-BRACKETYPAD, tbox.x-xpad+BRACKETXWIDTH,
tbox.y-BRACKETYPAD).stroke({width:1});
element.line(tbox.x-xpad, tbox.y2+BRACKETYPAD, tbox.x-xpad+BRACKETXWIDTH,
tbox.y2+BRACKETYPAD).stroke({width:1});
// left vertical line; top right angle; bottom right angle
element.line(tbox.x2+xpad, tbox.y-BRACKETYPAD, tbox.x2+xpad,
tbox.y2+BRACKETYPAD).stroke({width:1});
element.line(tbox.x2+xpad-BRACKETXWIDTH, tbox.y-BRACKETYPAD, tbox.x2+xpad,
tbox.y-BRACKETYPAD).stroke({width:1});
element.line(tbox.x2+xpad-BRACKETXWIDTH, tbox.y2+BRACKETYPAD, tbox.x2+xpad,
tbox.y2+BRACKETYPAD).stroke({width:1});
}
function rargcompare(a, b) {
// Compare function for sorting rel arguments
var a = a.toUpperCase();
var b = b.toUpperCase();
if (a === 'BODY' || a === 'CARG' || a.substr(-4) === 'HNDL') {
return 1;
} else if (b === 'BODY' || b === 'CARG' || b.substr(-4) === 'HNDL') {
return -1;
} else {
return a > b;
}
}
function xArgKey(arg) {
// Key function for x variable properties
var arg = arg.toUpperCase();
var xvar = ['PERS', 'NUM', 'PT', 'IND'];
var index = xvar.indexOf(arg);
return index > 0 ? index: 9999;
}
function eArgKey(arg) {
// Key function for event variable properties
var arg = arg.toUpperCase();
var evar = ['SF', 'TENSE', 'MOOD', 'PROG', 'PERF'];
var index = evar.indexOf(arg);
return index > 0 ? index: 9999;
}
function keySort(array, func){
// Returns a copy of 'array' sorted based on first applying 'func' to
// each element
var mapped = array.map(function(el, i){
return {index: i, value: func(el)};
});
mapped.sort(function(a, b) {
return +(a.value < b.value) || +(a.value === b.value) - 1;
});
return mapped.map(function(el){
return array[el.index];
});
}
function addHandlers(node){
$(node).find('.variable').hover(
function (event){
event.stopPropagation();
var $this = $(this);
// highlight variables
var dataQuery = "[data-var='" + $this.data('var') + "']";
$(node).find(dataQuery).css({fill: 'red'});
// highlight input text span if variable has lnk data
var lnks = argZeroes[this.innerHTML];
if (lnks == undefined)
// no lnks for this variable
return;
var $inputElem = $('#text-input');
var inputText = $inputElem.html();
// create an arrary of binary values indicating which characters
// should be highlighted
var chars = Array.apply(null, Array(inputText.length)).map(function(){return 0});
for (var i=0; i < lnks.length; i++) {
for (var j=lnks[i].from; j < lnks[i].to; j++)
chars[j] = 1;
}
// create a list of highlighted or not highlighted tokens
// to be joined together and then used to replace the text
var tokens = [];
var inside = false;
var start = 0;
for (var c=0; c < chars.length; c++){
var status = chars[c];
var atEnd = (c == chars.length - 1);
var end = atEnd ? c+1 : c;
if (inside && (!status || atEnd)) {
tokens.push('<span class="highlighted">' + inputText.slice(start, end) + '</span>');
inside = false;
start = c;
} else if (!inside && (status || atEnd)) {
tokens.push(inputText.slice(start, end));
inside = true;
start = c;
}
}
$inputElem.html(tokens.join(''));
}
,
function (event){
// remove highlighted variables
var dataQuery = "[data-var='" + $(this).data('var') + "']";
$(node).find(dataQuery).css({fill: 'black'});
// reset highlighted input string
var $inputElem = $('#text-input');
$inputElem.html($inputElem.html().replace(/<\/?span[^>]*>/g,""));
}
).filter(function (){
// only draw tooltip for variables of type e and x probably should
// be doing the test against whether corresponding mrs variable
// object has "properties" field or not.
var varName = $(this).data('var');
return mrsData.variables[varName].hasOwnProperty("properties");
}).tooltip({
track: true,
tooltipClass: 'mrs-variable-info',
content: function(){
var varName = $(this).data('var');
var variable = mrsData.variables[varName];
var func = variable.type == 'e' ? eArgKey : xArgKey;
var features = keySort(Object.keys(variable.properties), func);
var rows = [];
for (var i=0; i < features.length; i++) {
var attr = features[i];
rows.push('<tr><td class="variable-feat-name">'+attr+'</td><td class="variable-feat-val">'+variable.properties[attr]+'</td></tr>');
}
return '<table>' + rows.join('') + '</table>';
}
});
}
function getArgZeroLinks(){
argZeroLinks = {};
for (var i=0; i < mrsData.relations.length; i++) {
var rel = mrsData.relations[i];
if (argZeroLinks.hasOwnProperty(rel.arguments.ARG0))
argZeroLinks[rel.arguments.ARG0].push(rel.lnk);
else
argZeroLinks[rel.arguments.ARG0] = [rel.lnk];
}
return argZeroLinks;
}
var argZeroes = getArgZeroLinks();
var self = {
parent: parentElement,
data: mrsData,
element: drawMrs()
};
addHandlers(self.element);
return self;
}