linny-r 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/console.js +973 -0
- package/package.json +32 -0
- package/server.js +1547 -0
- package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
- package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
- package/static/fonts/Hack-Bold.ttf +0 -0
- package/static/fonts/Hack-BoldItalic.ttf +0 -0
- package/static/fonts/Hack-Italic.ttf +0 -0
- package/static/fonts/Hack-Regular.ttf +0 -0
- package/static/fonts/Lato-Bold.ttf +0 -0
- package/static/fonts/Lato-BoldItalic.ttf +0 -0
- package/static/fonts/Lato-Italic.ttf +0 -0
- package/static/fonts/Lato-Regular.ttf +0 -0
- package/static/fonts/mplus-1m-bold.ttf +0 -0
- package/static/fonts/mplus-1m-light.ttf +0 -0
- package/static/fonts/mplus-1m-medium.ttf +0 -0
- package/static/fonts/mplus-1m-regular.ttf +0 -0
- package/static/fonts/mplus-1m-thin.ttf +0 -0
- package/static/images/access.png +0 -0
- package/static/images/actor.png +0 -0
- package/static/images/actors.png +0 -0
- package/static/images/add-selector.png +0 -0
- package/static/images/add.png +0 -0
- package/static/images/back.png +0 -0
- package/static/images/black-box.png +0 -0
- package/static/images/by-sa.svg +74 -0
- package/static/images/cancel.png +0 -0
- package/static/images/chart.png +0 -0
- package/static/images/check-disab.png +0 -0
- package/static/images/check-off.png +0 -0
- package/static/images/check-on.png +0 -0
- package/static/images/check-x.png +0 -0
- package/static/images/clone.png +0 -0
- package/static/images/close.png +0 -0
- package/static/images/cluster.png +0 -0
- package/static/images/compare.png +0 -0
- package/static/images/compress.png +0 -0
- package/static/images/constraint.png +0 -0
- package/static/images/copy.png +0 -0
- package/static/images/data-to-clpbrd.png +0 -0
- package/static/images/dataset.png +0 -0
- package/static/images/delete.png +0 -0
- package/static/images/diagram.png +0 -0
- package/static/images/down.png +0 -0
- package/static/images/edit-chart.png +0 -0
- package/static/images/edit.png +0 -0
- package/static/images/eq.png +0 -0
- package/static/images/equation.png +0 -0
- package/static/images/experiment.png +0 -0
- package/static/images/favicon.ico +0 -0
- package/static/images/fewer-dec.png +0 -0
- package/static/images/filter.png +0 -0
- package/static/images/find.png +0 -0
- package/static/images/forward.png +0 -0
- package/static/images/host-logo.png +0 -0
- package/static/images/icon.png +0 -0
- package/static/images/icon.svg +23 -0
- package/static/images/ignore.png +0 -0
- package/static/images/include.png +0 -0
- package/static/images/info-to-clpbrd.png +0 -0
- package/static/images/info.png +0 -0
- package/static/images/is-black-box.png +0 -0
- package/static/images/lbl.png +0 -0
- package/static/images/lift.png +0 -0
- package/static/images/link.png +0 -0
- package/static/images/linny-r.icns +0 -0
- package/static/images/linny-r.ico +0 -0
- package/static/images/linny-r.png +0 -0
- package/static/images/linny-r.svg +21 -0
- package/static/images/logo.png +0 -0
- package/static/images/model-info.png +0 -0
- package/static/images/module.png +0 -0
- package/static/images/monitor.png +0 -0
- package/static/images/more-dec.png +0 -0
- package/static/images/ne.png +0 -0
- package/static/images/new.png +0 -0
- package/static/images/note.png +0 -0
- package/static/images/ok.png +0 -0
- package/static/images/open.png +0 -0
- package/static/images/outcome.png +0 -0
- package/static/images/parent.png +0 -0
- package/static/images/paste.png +0 -0
- package/static/images/pause.png +0 -0
- package/static/images/print-chart.png +0 -0
- package/static/images/print.png +0 -0
- package/static/images/process.png +0 -0
- package/static/images/product.png +0 -0
- package/static/images/pwlf.png +0 -0
- package/static/images/receiver.png +0 -0
- package/static/images/redo.png +0 -0
- package/static/images/remove.png +0 -0
- package/static/images/rename.png +0 -0
- package/static/images/repo-logo.png +0 -0
- package/static/images/repository.png +0 -0
- package/static/images/reset.png +0 -0
- package/static/images/resize.png +0 -0
- package/static/images/restore.png +0 -0
- package/static/images/save-chart.png +0 -0
- package/static/images/save-data.png +0 -0
- package/static/images/save-diagram.png +0 -0
- package/static/images/save.png +0 -0
- package/static/images/sensitivity.png +0 -0
- package/static/images/settings.png +0 -0
- package/static/images/solve.png +0 -0
- package/static/images/solver-logo.png +0 -0
- package/static/images/stats-to-clpbrd.png +0 -0
- package/static/images/stats.png +0 -0
- package/static/images/stop.png +0 -0
- package/static/images/store.png +0 -0
- package/static/images/stretch.png +0 -0
- package/static/images/table-to-clpbrd.png +0 -0
- package/static/images/table.png +0 -0
- package/static/images/tree.png +0 -0
- package/static/images/tudelft.png +0 -0
- package/static/images/ubl.png +0 -0
- package/static/images/undo.png +0 -0
- package/static/images/up.png +0 -0
- package/static/images/zoom-in.png +0 -0
- package/static/images/zoom-out.png +0 -0
- package/static/index.html +3088 -0
- package/static/linny-r.css +4722 -0
- package/static/scripts/iro.min.js +7 -0
- package/static/scripts/linny-r-config.js +105 -0
- package/static/scripts/linny-r-ctrl.js +1199 -0
- package/static/scripts/linny-r-gui.js +14814 -0
- package/static/scripts/linny-r-milp.js +286 -0
- package/static/scripts/linny-r-model.js +10405 -0
- package/static/scripts/linny-r-utils.js +687 -0
- package/static/scripts/linny-r-vm.js +7079 -0
- package/static/show-diff.html +84 -0
- package/static/show-png.html +113 -0
- package/static/sounds/error.wav +0 -0
- package/static/sounds/notification.wav +0 -0
- package/static/sounds/warning.wav +0 -0
@@ -0,0 +1,687 @@
|
|
1
|
+
/*
|
2
|
+
Linny-R is an executable graphical specification language for (mixed integer)
|
3
|
+
linear programming (MILP) problems, especially unit commitment problems (UCP).
|
4
|
+
The Linny-R language and tool have been developed by Pieter Bots at Delft
|
5
|
+
University of Technology, starting in 2009. The project to develop a browser-
|
6
|
+
based version started in 2017. See https://linny-r.org for more information.
|
7
|
+
|
8
|
+
This JavaScript file (linny-r-utils.js) defines a variety of "helper" functions
|
9
|
+
that are used in other Linny-R modules.
|
10
|
+
*/
|
11
|
+
/*
|
12
|
+
Copyright (c) 2017-2022 Delft University of Technology
|
13
|
+
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
16
|
+
in the Software without restriction, including without limitation the rights to
|
17
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
18
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
19
|
+
so, subject to the following conditions:
|
20
|
+
|
21
|
+
The above copyright notice and this permission notice shall be included in
|
22
|
+
all copies or substantial portions of the Software.
|
23
|
+
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
30
|
+
SOFTWARE.
|
31
|
+
*/
|
32
|
+
|
33
|
+
//
|
34
|
+
// Functions that facilitate HTTP requests
|
35
|
+
//
|
36
|
+
|
37
|
+
function postData(obj) {
|
38
|
+
// Converts a JavaScript object to an object that can be passed to a server
|
39
|
+
// in a POST request
|
40
|
+
const fields = [];
|
41
|
+
for(let k in obj) if(obj.hasOwnProperty(k)) {
|
42
|
+
fields.push(encodeURIComponent(k) + "=" + encodeURIComponent(obj[k]));
|
43
|
+
}
|
44
|
+
return {
|
45
|
+
method: 'post',
|
46
|
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
47
|
+
mode: 'no-cors',
|
48
|
+
body: fields.join('&')
|
49
|
+
};
|
50
|
+
}
|
51
|
+
|
52
|
+
//
|
53
|
+
// Functions that convert numbers to strings, or strings to numbers
|
54
|
+
//
|
55
|
+
|
56
|
+
function pluralS(n, s, special='') {
|
57
|
+
// Returns string with noun `s` in singular only if `n` = 1
|
58
|
+
// NOTE: third parameter can be used for nouns with irregular plural form
|
59
|
+
return (n === 0 ? 'No ' : n + ' ') +
|
60
|
+
// NOTE: to accomodate for plural form of ex-ante unknown entity types,
|
61
|
+
// nouns ending on "s" (specifically "process") form a special case
|
62
|
+
(n === 1 ? s : (special ? special : s + (s.endsWith('s') ? 'es' : 's')));
|
63
|
+
}
|
64
|
+
|
65
|
+
function safeStrToFloat(str, val=0) {
|
66
|
+
// Returns numeric value of floating point string, interpreting both
|
67
|
+
// dot and comma as decimal point
|
68
|
+
// NOTE: returns default value val if str is empty, null or undefined
|
69
|
+
const f = (str ? parseFloat(str.replace(',', '.')) : val);
|
70
|
+
return (isNaN(f) ? val : f);
|
71
|
+
}
|
72
|
+
|
73
|
+
function safeStrToInt(str, val=0) {
|
74
|
+
// Returns numeric value of integer string, IGNORING decimals after
|
75
|
+
// point or comma.
|
76
|
+
// NOTE: returns default value `val` if `str` is empty, null or undefined
|
77
|
+
const n = (str ? parseInt(str) : val);
|
78
|
+
return (isNaN(n) ? val : n);
|
79
|
+
}
|
80
|
+
|
81
|
+
function rangeToList(str, max=0) {
|
82
|
+
// Parses ranges "n-m/i" into a list of integers
|
83
|
+
// Returns FALSE if range is not valid according to the convention below
|
84
|
+
// The part "/i" is optional and denotes the increment; by default, i = 1.
|
85
|
+
// The returned list will contain all integers starting at n and up to
|
86
|
+
// at most (!) m, with increments of i, so [n, n+i, n+2i, ...]
|
87
|
+
// If `str` contains only the "/i" part, the range is assumed to start at 0
|
88
|
+
// and end at `max`; if only one number precedes the "/i", this denotes the
|
89
|
+
// first number in the range, while `max` again defines the highest number
|
90
|
+
// that can be included
|
91
|
+
const
|
92
|
+
list = [],
|
93
|
+
ssep = str.split('/');
|
94
|
+
if(ssep.length > 2) return false;
|
95
|
+
let incr = (ssep.length === 2 ? parseInt(ssep[1]) : 1);
|
96
|
+
if(isNaN(incr)) return false;
|
97
|
+
let range = ssep[0].trim(),
|
98
|
+
first = 0,
|
99
|
+
last = max;
|
100
|
+
if(range.length > 0) {
|
101
|
+
range = range.split('-');
|
102
|
+
if(range.length > 2) return false;
|
103
|
+
first = parseInt(range[0]);
|
104
|
+
if(range.length === 2) last = parseInt(range[1]);
|
105
|
+
if(isNaN(first) || isNaN(last)) return false;
|
106
|
+
}
|
107
|
+
// Create the range number list
|
108
|
+
for(let i = first; i <= last; i += incr) list.push(i);
|
109
|
+
return list;
|
110
|
+
}
|
111
|
+
|
112
|
+
function dateToString(d) {
|
113
|
+
// Returns date-time `d` in UTC format, accounting for time zone
|
114
|
+
const offset = d.getTimezoneOffset();
|
115
|
+
d = new Date(d.getTime() - offset*60000);
|
116
|
+
return d.toISOString().split('T')[0];
|
117
|
+
}
|
118
|
+
|
119
|
+
function msecToTime(msec) {
|
120
|
+
// Returns milliseconds as "minimal" string hh:mm:ss.msec
|
121
|
+
const ts = new Date(msec).toISOString().slice(11, -1).split('.');
|
122
|
+
let hms = ts[0], ms = ts[1];
|
123
|
+
// Trim zero hours and minutes
|
124
|
+
while(hms.startsWith('00:')) hms = hms.substr(3);
|
125
|
+
// Trim leading zero on first number
|
126
|
+
if(hms.startsWith('00')) hms = hms.substr(1);
|
127
|
+
// Trim msec when minutes > 0
|
128
|
+
if(hms.indexOf(':') > 0) return hms;
|
129
|
+
// If < 1 second, return as milliseconds
|
130
|
+
if(parseInt(hms) === 0) return parseInt(ms) + ' msec';
|
131
|
+
// Otherwise, return seconds with one decimal
|
132
|
+
return hms + '.' + ms.slice(0, 1) + ' sec';
|
133
|
+
}
|
134
|
+
|
135
|
+
function uniformDecimals(data) {
|
136
|
+
// Formats the numbers in the array `data` so that they have uniform decimals
|
137
|
+
// NOTE: (1) this routine assumes that all number strings have sig4Dig format;
|
138
|
+
// (2) it changes the values of the `data` array elements to strings
|
139
|
+
// STEP 1: Scan the data array to get the longest integer part, the shortest
|
140
|
+
// fraction part, and longest exponent part
|
141
|
+
let ss, x, maxi = 0, maxf = 0, maxe = 0;
|
142
|
+
for(let i = 0; i < data.length; i++) {
|
143
|
+
const v = data[i].toString();
|
144
|
+
ss = v.split('e');
|
145
|
+
if(ss.length > 1) {
|
146
|
+
maxe = Math.max(maxe, ss[1].length);
|
147
|
+
}
|
148
|
+
ss = ss[0].split('.');
|
149
|
+
if(ss.length > 1) {
|
150
|
+
maxf = Math.max(maxf, ss[1].length);
|
151
|
+
}
|
152
|
+
maxi = Math.max(maxi, ss[0].length);
|
153
|
+
}
|
154
|
+
// STEP 2: Convert the data to a uniform format
|
155
|
+
for(let i = 0; i < data.length; i++) {
|
156
|
+
const f = parseFloat(data[i]);
|
157
|
+
if(isNaN(f)) {
|
158
|
+
data[i] = '\u26A0'; // Unicode warning sign
|
159
|
+
} else if(maxe > 0) {
|
160
|
+
// Convert ALL numbers to exponential notation with one decimal (1.3e7)
|
161
|
+
const v = f.toExponential(1);
|
162
|
+
ss = v.split('e');
|
163
|
+
x = ss[1];
|
164
|
+
if(x.length < maxe) {
|
165
|
+
x = x[0] + '0' + x.substr(1);
|
166
|
+
}
|
167
|
+
data[i] = ss[0] + 'e' + x;
|
168
|
+
} else if(maxi > 3) {
|
169
|
+
// Round to integer if longest integer part has 4 or more digits
|
170
|
+
data[i] = Math.round(f).toString();
|
171
|
+
} else {
|
172
|
+
// Round fractions to `maxf` digits (but at most 4)
|
173
|
+
data[i] = f.toFixed(Math.min(4 - maxi, maxf));
|
174
|
+
}
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
function ellipsedText(text, n=50, m=10) {
|
179
|
+
// Returns `text` with ellipsis " ... " between its first `n` and last `m`
|
180
|
+
// characters
|
181
|
+
if(text.length <= n + m + 3) return text;
|
182
|
+
return text.slice(0, n) + ' \u2026 ' + text.slice(text.length - m);
|
183
|
+
}
|
184
|
+
|
185
|
+
//
|
186
|
+
// Functions used when comparing two Linny-R models
|
187
|
+
//
|
188
|
+
|
189
|
+
function differences(a, b, props) {
|
190
|
+
// Compares values of properties (in list `props`) of entities `a` and `b`,
|
191
|
+
// and returns a "dictionary" object with differences
|
192
|
+
const d = {};
|
193
|
+
// Only compare entities of the same type
|
194
|
+
if(a.type === b.type) {
|
195
|
+
for(let i = 0; i < props.length; i++) {
|
196
|
+
const p = props[i];
|
197
|
+
// NOTE: model entity properties can be expressions => compare their text
|
198
|
+
if(a[p] instanceof Expression) {
|
199
|
+
if(a[p].text !== b[p].text) d[p] = {A: a[p].text, B: b[p].text};
|
200
|
+
} else if(a[p] instanceof Date) {
|
201
|
+
if(Math.abs(a[p].getTime() - b[p].getTime()) > 1000) {
|
202
|
+
d[p] = {A: dateToString(a[p]), B: dateToString(b[p])};
|
203
|
+
}
|
204
|
+
} else if(a[p] !== b[p]) {
|
205
|
+
d[p] = {A: a[p], B: b[p]};
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
209
|
+
// NOTE: `d` may still be an empty object {}
|
210
|
+
return d;
|
211
|
+
}
|
212
|
+
|
213
|
+
function markFirstDifference(s1, s2) {
|
214
|
+
// Returns `s1` with bold-faced from point of first difference with `s2`
|
215
|
+
// up to position where `s1` and `s2` have the same tail
|
216
|
+
// NOTE: ensure that both parameters are strings
|
217
|
+
s1 = '' + s1;
|
218
|
+
s2 = '' + s2;
|
219
|
+
const l = Math.min(s1.length, s2.length);
|
220
|
+
let i = 0;
|
221
|
+
while(i < l && s1.charAt(i) === s2.charAt(i)) i++;
|
222
|
+
if(i >= s1.length) {
|
223
|
+
// No differences, but tail may have been cut
|
224
|
+
if(i < s2.length) s1 += '<span class="mc-hilite">…</span>';
|
225
|
+
return s1;
|
226
|
+
}
|
227
|
+
let j1 = s1.length - 1,
|
228
|
+
j2 = s2.length - 1;
|
229
|
+
while(j1 > 0 && j2 > 0 && s1.charAt(j1) === s2.charAt(j2)) {
|
230
|
+
j1--;
|
231
|
+
j2--;
|
232
|
+
}
|
233
|
+
return s1.substring(0, i) + '<span class="mc-hilite">' +
|
234
|
+
s1.substring(i, j1 + 1) + '</span>' + s1.substring(j1 + 1);
|
235
|
+
}
|
236
|
+
|
237
|
+
//
|
238
|
+
// Functions that perform string search, comparison and/or substitution
|
239
|
+
//
|
240
|
+
|
241
|
+
function endsWithDigits(str) {
|
242
|
+
// Returns trailing digts of `str` (empty string will evaluate as FALSE)
|
243
|
+
let i = str.length - 1,
|
244
|
+
c = str[i],
|
245
|
+
d = '';
|
246
|
+
while(i >= 0 && '0123456789'.indexOf(c) >= 0) {
|
247
|
+
d = c + d;
|
248
|
+
i--;
|
249
|
+
c = str[i];
|
250
|
+
}
|
251
|
+
return d;
|
252
|
+
}
|
253
|
+
|
254
|
+
function indexOfMatchingBracket(str, offset) {
|
255
|
+
// Returns index of closing bracket, ignoring matched [...] inside
|
256
|
+
// NOTE: starts at offset + 1, assuming that character at offset = '['
|
257
|
+
let ob = 0, c;
|
258
|
+
for(let i = offset + 1; i < str.length; i++) {
|
259
|
+
c = str.charAt(i);
|
260
|
+
if(c === '[') {
|
261
|
+
ob++;
|
262
|
+
} else if (c === ']') {
|
263
|
+
if(ob > 0) {
|
264
|
+
ob--;
|
265
|
+
} else {
|
266
|
+
return i;
|
267
|
+
}
|
268
|
+
}
|
269
|
+
}
|
270
|
+
// No matching bracket => return -1
|
271
|
+
return -1;
|
272
|
+
}
|
273
|
+
|
274
|
+
function patternList(str) {
|
275
|
+
// Returns the &|^-pattern defined by `str`
|
276
|
+
// Pattern operators: & (and), ^ (not) and | (or) in sequence, e.g.,
|
277
|
+
// this&that^not this|just this|^just not that
|
278
|
+
const
|
279
|
+
pat = str.split('|'),
|
280
|
+
or_list = [];
|
281
|
+
for(let i = 0; i < pat.length; i++) {
|
282
|
+
const
|
283
|
+
pm = ({plus:[], min: []}),
|
284
|
+
term = pat[i].split('&');
|
285
|
+
for(let j = 0; j < term.length; j++) {
|
286
|
+
const subterm = term[j].split('^');
|
287
|
+
for(let k = 0; k < subterm.length; k++) {
|
288
|
+
const s = subterm[k];
|
289
|
+
if(s) {
|
290
|
+
// NOTE: first subterm is a MUST!
|
291
|
+
if(k == 0) {
|
292
|
+
pm.plus.push(s);
|
293
|
+
} else {
|
294
|
+
pm.min.push(s);
|
295
|
+
}
|
296
|
+
}
|
297
|
+
}
|
298
|
+
}
|
299
|
+
if(pm.plus.length + pm.min.length > 0) {
|
300
|
+
or_list.push(pm);
|
301
|
+
}
|
302
|
+
}
|
303
|
+
return or_list;
|
304
|
+
}
|
305
|
+
|
306
|
+
function patternMatch(str, patterns) {
|
307
|
+
// Returns TRUE when `str` matches the &|^-pattern
|
308
|
+
for(let i = 0; i < patterns.length; i++) {
|
309
|
+
const p = patterns[i];
|
310
|
+
let match = true;
|
311
|
+
for(let j = 0; j < p.plus.length; j++) {
|
312
|
+
match = match && str.indexOf(p.plus[j]) >= 0;
|
313
|
+
}
|
314
|
+
for(let j = 0; j < p.min.length; j++) {
|
315
|
+
match = match && str.indexOf(p.min[j]) < 0;
|
316
|
+
}
|
317
|
+
if(match) {
|
318
|
+
return true;
|
319
|
+
}
|
320
|
+
}
|
321
|
+
return false;
|
322
|
+
}
|
323
|
+
|
324
|
+
function escapeRegex(str) {
|
325
|
+
// Returns `str` with its RegEx special characters escaped
|
326
|
+
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
327
|
+
}
|
328
|
+
|
329
|
+
//
|
330
|
+
// Functions that perform set-like operations on lists of string
|
331
|
+
//
|
332
|
+
|
333
|
+
function addDistinct(e, list) {
|
334
|
+
// Adds element `e` to `list` only if it does not already occur in `list`
|
335
|
+
if(list.indexOf(e) < 0) list.push(e);
|
336
|
+
}
|
337
|
+
|
338
|
+
function setString(sl) {
|
339
|
+
// Returns elements of stringlist `sl` in set notation
|
340
|
+
return '{' + sl.join(', ') + '}';
|
341
|
+
}
|
342
|
+
|
343
|
+
function tupelString(sl) {
|
344
|
+
// Returns elements of stringlist `sl` in tupel notation
|
345
|
+
return '(' + sl.join(', ') + ')';
|
346
|
+
}
|
347
|
+
|
348
|
+
function tupelSetString(ssl) {
|
349
|
+
// Returns string of stringlists `sll` as set of tuples
|
350
|
+
const tl = [];
|
351
|
+
for(let i = 0; i < ssl.length; i++) {
|
352
|
+
tl.push(tupelString(ssl[i]));
|
353
|
+
}
|
354
|
+
return setString(tl);
|
355
|
+
}
|
356
|
+
|
357
|
+
function tupelIndex(sl, ssl) {
|
358
|
+
// Returns index of stringlist `sl` if it exists in `ssl`, otherwise -1
|
359
|
+
for(let i = 0; i < ssl.length; i++) {
|
360
|
+
let n = 0;
|
361
|
+
for(let j = 0; j < sl.length; j++) {
|
362
|
+
if(ssl[i].indexOf(sl[j]) < 0) break;
|
363
|
+
n++;
|
364
|
+
}
|
365
|
+
if(n == sl.length) return i;
|
366
|
+
}
|
367
|
+
return -1;
|
368
|
+
}
|
369
|
+
|
370
|
+
function intersection(sl1, sl2) {
|
371
|
+
// Returns the list of common elements of stringlists `l1` and `l2`
|
372
|
+
const shared = [];
|
373
|
+
for(let i = 0; i < sl1.length; i++) {
|
374
|
+
if(sl2.indexOf(sl1[i]) >= 0) shared.push(sl1[i]);
|
375
|
+
}
|
376
|
+
return shared;
|
377
|
+
}
|
378
|
+
|
379
|
+
function complement(sl1, sl2) {
|
380
|
+
// Returns the list of elements of stringlist `l1` that are NOT in `l2`
|
381
|
+
const cmplmnt = [];
|
382
|
+
for(let i = 0; i < sl1.length; i++) {
|
383
|
+
if(sl2.indexOf(sl1[i]) < 0) cmplmnt.push(sl1[i]);
|
384
|
+
}
|
385
|
+
return cmplmnt;
|
386
|
+
}
|
387
|
+
|
388
|
+
//
|
389
|
+
// Functions that support loading and saving data and models
|
390
|
+
//
|
391
|
+
|
392
|
+
function xmlEncoded(str) {
|
393
|
+
// Replaces &, <, >, ' and " by their HTML entity code
|
394
|
+
return str.replace(/\&/g, '&').replace(/</g, '<'
|
395
|
+
).replace(/>/g, '>').replace(/\'/g, '''
|
396
|
+
).replace(/\"/g, '"');
|
397
|
+
}
|
398
|
+
|
399
|
+
function xmlDecoded(str) {
|
400
|
+
// Replaces HTML entity code for &, <, >, ' and " by the original character
|
401
|
+
// NOTE: also replaces Linny-R legacy newline encoding $$\n by two newline
|
402
|
+
// characters
|
403
|
+
return str.replace(/\</g, '<').replace(/\>/g, '>'
|
404
|
+
).replace(/\'/g, '\'').replace(/\"/g, '"'
|
405
|
+
).replace(/\&/g, '&').replace(/\$\$\\n/g, '\n\n');
|
406
|
+
}
|
407
|
+
|
408
|
+
function cleanXML(node) {
|
409
|
+
// Removes all unnamed text nodes and comment nodes from the XML
|
410
|
+
// subtree under node
|
411
|
+
const cn = node.childNodes;
|
412
|
+
if(cn) {
|
413
|
+
for(let i = cn.length - 1; i >= 0; i--) {
|
414
|
+
let n = cn[i];
|
415
|
+
if(n.nodeType === 3 && !/\S/.test(n.nodeValue) || n.nodeType === 8) {
|
416
|
+
node.removeChild(n);
|
417
|
+
} else if(n.nodeType === 1) {
|
418
|
+
cleanXML(n);
|
419
|
+
}
|
420
|
+
}
|
421
|
+
}
|
422
|
+
}
|
423
|
+
|
424
|
+
function parseXML(xml) {
|
425
|
+
// Parses string `xml` into an XML document, and returns its root node
|
426
|
+
// (or null if errors)
|
427
|
+
xml = XML_PARSER.parseFromString(xml, 'application/xml');
|
428
|
+
const
|
429
|
+
de = xml.documentElement,
|
430
|
+
pe = de.getElementsByTagName('parsererror').item(0);
|
431
|
+
if(pe) throw de.nodeValue;
|
432
|
+
cleanXML(de);
|
433
|
+
return de;
|
434
|
+
}
|
435
|
+
|
436
|
+
function childNodeByTag(node, tag) {
|
437
|
+
// Returns the XML child node of `node` having node name `tag`, or NULL if
|
438
|
+
// no such child node exists
|
439
|
+
let cn = null;
|
440
|
+
for (let i = 0; i < node.children.length; i++) {
|
441
|
+
if(node.children[i].tagName === tag) {
|
442
|
+
cn = node.children[i];
|
443
|
+
break;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
return cn;
|
447
|
+
}
|
448
|
+
|
449
|
+
function nodeContentByTag(node, tag) {
|
450
|
+
// Returns the text content of the child node of `node` having name `tag`,
|
451
|
+
// or the empty string if no such node exists
|
452
|
+
return nodeContent(childNodeByTag(node, tag));
|
453
|
+
}
|
454
|
+
|
455
|
+
function nodeContent(node) {
|
456
|
+
// Returns the text content of XML element `node`
|
457
|
+
if(node) {
|
458
|
+
// For text nodes, return their value
|
459
|
+
if(node.nodeType === 3) return node.nodeValue;
|
460
|
+
// For empty nodes, return empty string
|
461
|
+
if(node.childNodes.length === 0) return '';
|
462
|
+
// If first child is text, return its value
|
463
|
+
const fcn = node.childNodes.item(0);
|
464
|
+
if(fcn && fcn.nodeType === 3) return fcn.nodeValue;
|
465
|
+
console.log('UNEXPECTED XML', fcn.nodeType, node);
|
466
|
+
}
|
467
|
+
return '';
|
468
|
+
}
|
469
|
+
|
470
|
+
function nodeParameterValue(node, param) {
|
471
|
+
// Returns the value of parameter `param` as string if `node` has
|
472
|
+
// this parameter, otherwise the empty string
|
473
|
+
const a = node.getAttribute(param);
|
474
|
+
return a || '';
|
475
|
+
}
|
476
|
+
|
477
|
+
//
|
478
|
+
// Functions that support naming and identifying Linny-R entities
|
479
|
+
//
|
480
|
+
|
481
|
+
function letterCode(n) {
|
482
|
+
// Encodes a non-negative integer as base-26 (0 = A, 25 = Z, 26 = AA, etc.)
|
483
|
+
const r = n % 26, d = (n - r) / 26, c = String.fromCharCode(65 + r);
|
484
|
+
// NOTE: recursion!
|
485
|
+
if(d) return letterCode(d) + c;
|
486
|
+
return c;
|
487
|
+
}
|
488
|
+
|
489
|
+
function parseLetterCode(lc) {
|
490
|
+
// Decodes a base-26 code into an integer. NOTE: does not check whether
|
491
|
+
// the code is indeed base-26
|
492
|
+
let n = 0;
|
493
|
+
for(let i = 0; i < lc.length; i++) {
|
494
|
+
n = 10*n + (lc.charCodeAt(i) - 65);
|
495
|
+
}
|
496
|
+
return n;
|
497
|
+
}
|
498
|
+
|
499
|
+
function randomID() {
|
500
|
+
// Generates a 22+ hex digit ID: timestamp plus 12 random bits as suffix
|
501
|
+
// plus 8 more random hex digits (earlier shorter version caused doubles!)
|
502
|
+
const d = ((new Date()).getTime() + Math.random()) * 4096,
|
503
|
+
e = Math.floor(Math.random() * 4294967296);
|
504
|
+
return (Math.floor(d)).toString(16) + e.toString(16);
|
505
|
+
}
|
506
|
+
|
507
|
+
function escapedSingleQuotes(s) {
|
508
|
+
// Returns string `s` with "escaped" single quotes
|
509
|
+
return s.replace('\'', '\\\'');
|
510
|
+
}
|
511
|
+
|
512
|
+
function nameToLines(name, actor_name = '') {
|
513
|
+
// Returns the name of a Linny-R entity as a string-with-line-breaks that
|
514
|
+
// fits nicely in an oblong box. For efficiency reasons, a fixed width/height
|
515
|
+
// ratio is assumed, as this produces quite acceptable results
|
516
|
+
let m = actor_name.length;
|
517
|
+
const
|
518
|
+
d = Math.floor(Math.sqrt(0.3 * name.length)),
|
519
|
+
// Do not wrap strings shorter than 13 characters (about 50 pixels)
|
520
|
+
limit = Math.max(Math.ceil(name.length / d), m, 13),
|
521
|
+
a = name.split(' ');
|
522
|
+
// Split words at '-' when wider than limit
|
523
|
+
for(let j = 0; j < a.length; j++) {
|
524
|
+
if(a[j].length > limit) {
|
525
|
+
const sw = a[j].split('-');
|
526
|
+
if(sw.length > 1) {
|
527
|
+
// Replace j-th word by last fragment of split string
|
528
|
+
a[j] = sw.pop();
|
529
|
+
// Insert remaining fragments before
|
530
|
+
while(sw.length > 0) a.splice(j, 0, sw.pop() + '-');
|
531
|
+
}
|
532
|
+
}
|
533
|
+
}
|
534
|
+
const ww = [];
|
535
|
+
for(let i = 0; i < a.length; i++) {
|
536
|
+
ww[i] = a[i].length;
|
537
|
+
m = Math.max(m, ww[i]);
|
538
|
+
}
|
539
|
+
const lines = [a[0]];
|
540
|
+
let n = 0,
|
541
|
+
l = ww[n],
|
542
|
+
space;
|
543
|
+
for(let i = 1; i < a.length; i++) {
|
544
|
+
if(l + ww[i] < limit) {
|
545
|
+
space = (lines[n].endsWith('-') ? '' : ' ');
|
546
|
+
lines[n] += space + a[i];
|
547
|
+
l += ww[i] + space.length;
|
548
|
+
} else {
|
549
|
+
n++;
|
550
|
+
lines[n] = a[i];
|
551
|
+
l = ww[i];
|
552
|
+
}
|
553
|
+
}
|
554
|
+
return lines.join('\n');
|
555
|
+
}
|
556
|
+
|
557
|
+
//
|
558
|
+
// Encryption-related functions
|
559
|
+
//
|
560
|
+
|
561
|
+
function hexToBytes(hex) {
|
562
|
+
// Converts a hex string to a Uint8Array
|
563
|
+
const bytes = [];
|
564
|
+
for(let i = 0; i < hex.length; i += 2) {
|
565
|
+
bytes.push(parseInt(hex.substr(i, 2), 16));
|
566
|
+
}
|
567
|
+
return new Uint8Array(bytes);
|
568
|
+
}
|
569
|
+
|
570
|
+
function bytesToHex(bytes) {
|
571
|
+
// Converts a byte array to a hex string
|
572
|
+
return Array.from(bytes,
|
573
|
+
function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }
|
574
|
+
).join('');
|
575
|
+
}
|
576
|
+
|
577
|
+
function arrayBufferToBase64(buffer) {
|
578
|
+
let binary = '';
|
579
|
+
const
|
580
|
+
bytes = new Uint8Array(buffer),
|
581
|
+
l = bytes.byteLength;
|
582
|
+
for(let i = 0; i < l; i++) {
|
583
|
+
binary += String.fromCharCode(bytes[i]);
|
584
|
+
}
|
585
|
+
return window.btoa(binary);
|
586
|
+
}
|
587
|
+
|
588
|
+
function base64ToArrayBuffer(base64) {
|
589
|
+
let binary = window.atob(base64);
|
590
|
+
const
|
591
|
+
l = binary.length,
|
592
|
+
bytes = new Uint8Array(l);
|
593
|
+
for(let i = 0; i < l; i++) {
|
594
|
+
bytes[i] = binary.charCodeAt(i);
|
595
|
+
}
|
596
|
+
return bytes.buffer;
|
597
|
+
}
|
598
|
+
|
599
|
+
async function encryptionKey(password) {
|
600
|
+
let material = await window.crypto.subtle.importKey(
|
601
|
+
'raw', new TextEncoder().encode(password), 'PBKDF2', false,
|
602
|
+
['deriveBits', 'deriveKey']);
|
603
|
+
let key = await window.crypto.subtle.deriveKey(
|
604
|
+
{name: 'PBKDF2', salt: new TextEncoder().encode(ENCRYPTION.salt),
|
605
|
+
iterations: ENCRYPTION.iterations, hash: 'SHA-256'}, material,
|
606
|
+
{name: 'AES-GCM', length: 256}, true, ['encrypt', 'decrypt']);
|
607
|
+
return key;
|
608
|
+
}
|
609
|
+
|
610
|
+
async function encryptMessage(msg, key) {
|
611
|
+
let encoded = new TextEncoder().encode(msg),
|
612
|
+
iv = window.crypto.getRandomValues(new Uint8Array(12)),
|
613
|
+
ciphertext = await window.crypto.subtle.encrypt(
|
614
|
+
{name: 'AES-GCM', iv: iv}, key, encoded);
|
615
|
+
return {encryption: arrayBufferToBase64(ciphertext), latch: bytesToHex(iv)};
|
616
|
+
}
|
617
|
+
|
618
|
+
async function decryptMessage(msg, key) {
|
619
|
+
const
|
620
|
+
latch = hexToBytes(msg.latch),
|
621
|
+
buffer = base64ToArrayBuffer(msg.encryption);
|
622
|
+
let decrypted = await window.crypto.subtle.decrypt(
|
623
|
+
{name: 'AES-GCM', iv: latch}, key, buffer);
|
624
|
+
return new TextDecoder().decode(decrypted);
|
625
|
+
}
|
626
|
+
|
627
|
+
async function tryToDecrypt(msg, password, on_ok, on_error) {
|
628
|
+
// Attempts decryption with the entered password, and performs the
|
629
|
+
// post-decryption action on the decrypted data if successful
|
630
|
+
let data = null;
|
631
|
+
try {
|
632
|
+
const key = await encryptionKey(password);
|
633
|
+
data = await decryptMessage(msg, key);
|
634
|
+
on_ok(data);
|
635
|
+
} catch(err) {
|
636
|
+
on_error(err);
|
637
|
+
}
|
638
|
+
}
|
639
|
+
|
640
|
+
///////////////////////////////////////////////////////////////////////
|
641
|
+
// Define exports so that this file can also be included as a module //
|
642
|
+
///////////////////////////////////////////////////////////////////////
|
643
|
+
|
644
|
+
if(NODE) module.exports = {
|
645
|
+
postData: postData,
|
646
|
+
pluralS: pluralS,
|
647
|
+
safeStrToFloat: safeStrToFloat,
|
648
|
+
safeStrToInt: safeStrToInt,
|
649
|
+
dateToString: dateToString,
|
650
|
+
msecToTime: msecToTime,
|
651
|
+
uniformDecimals: uniformDecimals,
|
652
|
+
ellipsedText: ellipsedText,
|
653
|
+
differences: differences,
|
654
|
+
markFirstDifference: markFirstDifference,
|
655
|
+
endsWithDigits: endsWithDigits,
|
656
|
+
indexOfMatchingBracket: indexOfMatchingBracket,
|
657
|
+
patternList: patternList,
|
658
|
+
patternMatch: patternMatch,
|
659
|
+
escapeRegex: escapeRegex,
|
660
|
+
addDistinct: addDistinct,
|
661
|
+
setString: setString,
|
662
|
+
tupelString: tupelString,
|
663
|
+
tupelSetString: tupelSetString,
|
664
|
+
tupelIndex: tupelIndex,
|
665
|
+
intersection: intersection,
|
666
|
+
complement: complement,
|
667
|
+
xmlEncoded: xmlEncoded,
|
668
|
+
xmlDecoded: xmlDecoded,
|
669
|
+
cleanXML: cleanXML,
|
670
|
+
parseXML: parseXML,
|
671
|
+
childNodeByTag: childNodeByTag,
|
672
|
+
nodeContentByTag: nodeContentByTag,
|
673
|
+
nodeContent: nodeContent,
|
674
|
+
nodeParameterValue: nodeParameterValue,
|
675
|
+
letterCode: letterCode,
|
676
|
+
parseLetterCode: parseLetterCode,
|
677
|
+
randomID: randomID,
|
678
|
+
escapedSingleQuotes: escapedSingleQuotes,
|
679
|
+
nameToLines: nameToLines,
|
680
|
+
hexToBytes: hexToBytes,
|
681
|
+
arrayBufferToBase64: arrayBufferToBase64,
|
682
|
+
base64ToArrayBuffer: base64ToArrayBuffer,
|
683
|
+
encryptionKey: encryptionKey,
|
684
|
+
encryptMessage: encryptMessage,
|
685
|
+
decryptMessage: decryptMessage,
|
686
|
+
tryToDecrypt: tryToDecrypt
|
687
|
+
}
|