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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/console.js +973 -0
  4. package/package.json +32 -0
  5. package/server.js +1547 -0
  6. package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
  7. package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
  8. package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
  9. package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
  10. package/static/fonts/Hack-Bold.ttf +0 -0
  11. package/static/fonts/Hack-BoldItalic.ttf +0 -0
  12. package/static/fonts/Hack-Italic.ttf +0 -0
  13. package/static/fonts/Hack-Regular.ttf +0 -0
  14. package/static/fonts/Lato-Bold.ttf +0 -0
  15. package/static/fonts/Lato-BoldItalic.ttf +0 -0
  16. package/static/fonts/Lato-Italic.ttf +0 -0
  17. package/static/fonts/Lato-Regular.ttf +0 -0
  18. package/static/fonts/mplus-1m-bold.ttf +0 -0
  19. package/static/fonts/mplus-1m-light.ttf +0 -0
  20. package/static/fonts/mplus-1m-medium.ttf +0 -0
  21. package/static/fonts/mplus-1m-regular.ttf +0 -0
  22. package/static/fonts/mplus-1m-thin.ttf +0 -0
  23. package/static/images/access.png +0 -0
  24. package/static/images/actor.png +0 -0
  25. package/static/images/actors.png +0 -0
  26. package/static/images/add-selector.png +0 -0
  27. package/static/images/add.png +0 -0
  28. package/static/images/back.png +0 -0
  29. package/static/images/black-box.png +0 -0
  30. package/static/images/by-sa.svg +74 -0
  31. package/static/images/cancel.png +0 -0
  32. package/static/images/chart.png +0 -0
  33. package/static/images/check-disab.png +0 -0
  34. package/static/images/check-off.png +0 -0
  35. package/static/images/check-on.png +0 -0
  36. package/static/images/check-x.png +0 -0
  37. package/static/images/clone.png +0 -0
  38. package/static/images/close.png +0 -0
  39. package/static/images/cluster.png +0 -0
  40. package/static/images/compare.png +0 -0
  41. package/static/images/compress.png +0 -0
  42. package/static/images/constraint.png +0 -0
  43. package/static/images/copy.png +0 -0
  44. package/static/images/data-to-clpbrd.png +0 -0
  45. package/static/images/dataset.png +0 -0
  46. package/static/images/delete.png +0 -0
  47. package/static/images/diagram.png +0 -0
  48. package/static/images/down.png +0 -0
  49. package/static/images/edit-chart.png +0 -0
  50. package/static/images/edit.png +0 -0
  51. package/static/images/eq.png +0 -0
  52. package/static/images/equation.png +0 -0
  53. package/static/images/experiment.png +0 -0
  54. package/static/images/favicon.ico +0 -0
  55. package/static/images/fewer-dec.png +0 -0
  56. package/static/images/filter.png +0 -0
  57. package/static/images/find.png +0 -0
  58. package/static/images/forward.png +0 -0
  59. package/static/images/host-logo.png +0 -0
  60. package/static/images/icon.png +0 -0
  61. package/static/images/icon.svg +23 -0
  62. package/static/images/ignore.png +0 -0
  63. package/static/images/include.png +0 -0
  64. package/static/images/info-to-clpbrd.png +0 -0
  65. package/static/images/info.png +0 -0
  66. package/static/images/is-black-box.png +0 -0
  67. package/static/images/lbl.png +0 -0
  68. package/static/images/lift.png +0 -0
  69. package/static/images/link.png +0 -0
  70. package/static/images/linny-r.icns +0 -0
  71. package/static/images/linny-r.ico +0 -0
  72. package/static/images/linny-r.png +0 -0
  73. package/static/images/linny-r.svg +21 -0
  74. package/static/images/logo.png +0 -0
  75. package/static/images/model-info.png +0 -0
  76. package/static/images/module.png +0 -0
  77. package/static/images/monitor.png +0 -0
  78. package/static/images/more-dec.png +0 -0
  79. package/static/images/ne.png +0 -0
  80. package/static/images/new.png +0 -0
  81. package/static/images/note.png +0 -0
  82. package/static/images/ok.png +0 -0
  83. package/static/images/open.png +0 -0
  84. package/static/images/outcome.png +0 -0
  85. package/static/images/parent.png +0 -0
  86. package/static/images/paste.png +0 -0
  87. package/static/images/pause.png +0 -0
  88. package/static/images/print-chart.png +0 -0
  89. package/static/images/print.png +0 -0
  90. package/static/images/process.png +0 -0
  91. package/static/images/product.png +0 -0
  92. package/static/images/pwlf.png +0 -0
  93. package/static/images/receiver.png +0 -0
  94. package/static/images/redo.png +0 -0
  95. package/static/images/remove.png +0 -0
  96. package/static/images/rename.png +0 -0
  97. package/static/images/repo-logo.png +0 -0
  98. package/static/images/repository.png +0 -0
  99. package/static/images/reset.png +0 -0
  100. package/static/images/resize.png +0 -0
  101. package/static/images/restore.png +0 -0
  102. package/static/images/save-chart.png +0 -0
  103. package/static/images/save-data.png +0 -0
  104. package/static/images/save-diagram.png +0 -0
  105. package/static/images/save.png +0 -0
  106. package/static/images/sensitivity.png +0 -0
  107. package/static/images/settings.png +0 -0
  108. package/static/images/solve.png +0 -0
  109. package/static/images/solver-logo.png +0 -0
  110. package/static/images/stats-to-clpbrd.png +0 -0
  111. package/static/images/stats.png +0 -0
  112. package/static/images/stop.png +0 -0
  113. package/static/images/store.png +0 -0
  114. package/static/images/stretch.png +0 -0
  115. package/static/images/table-to-clpbrd.png +0 -0
  116. package/static/images/table.png +0 -0
  117. package/static/images/tree.png +0 -0
  118. package/static/images/tudelft.png +0 -0
  119. package/static/images/ubl.png +0 -0
  120. package/static/images/undo.png +0 -0
  121. package/static/images/up.png +0 -0
  122. package/static/images/zoom-in.png +0 -0
  123. package/static/images/zoom-out.png +0 -0
  124. package/static/index.html +3088 -0
  125. package/static/linny-r.css +4722 -0
  126. package/static/scripts/iro.min.js +7 -0
  127. package/static/scripts/linny-r-config.js +105 -0
  128. package/static/scripts/linny-r-ctrl.js +1199 -0
  129. package/static/scripts/linny-r-gui.js +14814 -0
  130. package/static/scripts/linny-r-milp.js +286 -0
  131. package/static/scripts/linny-r-model.js +10405 -0
  132. package/static/scripts/linny-r-utils.js +687 -0
  133. package/static/scripts/linny-r-vm.js +7079 -0
  134. package/static/show-diff.html +84 -0
  135. package/static/show-png.html +113 -0
  136. package/static/sounds/error.wav +0 -0
  137. package/static/sounds/notification.wav +0 -0
  138. 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">&hellip;</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, '&amp;').replace(/</g, '&lt;'
395
+ ).replace(/>/g, '&gt;').replace(/\'/g, '&apos;'
396
+ ).replace(/\"/g, '&quot;');
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(/\&lt;/g, '<').replace(/\&gt;/g, '>'
404
+ ).replace(/\&apos;/g, '\'').replace(/\&quot;/g, '"'
405
+ ).replace(/\&amp;/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
+ }