linny-r 1.3.4 → 1.4.1
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/package.json +1 -1
- package/server.js +48 -6
- package/static/index.html +14 -12
- package/static/linny-r.css +28 -2
- package/static/scripts/linny-r-ctrl.js +51 -8
- package/static/scripts/linny-r-gui.js +150 -75
- package/static/scripts/linny-r-model.js +467 -154
- package/static/scripts/linny-r-utils.js +141 -12
- package/static/scripts/linny-r-vm.js +931 -435
@@ -135,6 +135,14 @@ function msecToTime(msec) {
|
|
135
135
|
return hms + '.' + ms.slice(0, 1) + ' sec';
|
136
136
|
}
|
137
137
|
|
138
|
+
function compactClockTime() {
|
139
|
+
// Returns current time (no date) in 6 digits hhmmss.
|
140
|
+
const d = new Date();
|
141
|
+
return d.getHours().toString().padStart(2, '0') +
|
142
|
+
d.getMinutes().toString().padStart(2, '0') +
|
143
|
+
d.getSeconds().toString().padStart(2, '0');
|
144
|
+
}
|
145
|
+
|
138
146
|
function uniformDecimals(data) {
|
139
147
|
// Formats the numbers in the array `data` so that they have uniform decimals
|
140
148
|
// NOTE: (1) this routine assumes that all number strings have sig4Dig format;
|
@@ -327,44 +335,149 @@ function patternList(str) {
|
|
327
335
|
}
|
328
336
|
|
329
337
|
function patternMatch(str, patterns) {
|
330
|
-
// Returns TRUE when `str` matches the &|^-pattern
|
331
|
-
// NOTE:
|
338
|
+
// Returns TRUE when `str` matches the &|^-pattern.
|
339
|
+
// NOTE: If a pattern starts with equals sign = then `str` must
|
332
340
|
// equal the rest of the pattern to match; if it starts with a tilde
|
333
|
-
// ~ then `str` must start with the rest of the pattern to match
|
341
|
+
// ~ then `str` must start with the rest of the pattern to match.
|
334
342
|
for(let i = 0; i < patterns.length; i++) {
|
335
343
|
const p = patterns[i];
|
344
|
+
// NOTE: `p` is an OR sub-pattern that tests for a set of "plus"
|
345
|
+
// sub-sub-patterns (all of which should match) and a set of "min"
|
346
|
+
// sub-sub-patters (all should NOT match)
|
336
347
|
let pm,
|
348
|
+
re,
|
337
349
|
match = true;
|
338
|
-
for(let j = 0; j < p.plus.length; j++) {
|
350
|
+
for(let j = 0; match && j < p.plus.length; j++) {
|
339
351
|
pm = p.plus[j];
|
340
352
|
if(pm.startsWith('=')) {
|
341
|
-
match =
|
353
|
+
match = (str === pm.substring(1));
|
342
354
|
} else if(pm.startsWith('~')) {
|
343
|
-
match =
|
355
|
+
match = str.startsWith(pm.substring(1));
|
344
356
|
} else {
|
345
|
-
match =
|
357
|
+
match = (str.indexOf(pm) >= 0);
|
358
|
+
}
|
359
|
+
// If no match, check whether pattern contains wildcards
|
360
|
+
if(!match && pm.indexOf('#') >= 0) {
|
361
|
+
// If so, rematch using regular expression that tests for a
|
362
|
+
// number or a ?? wildcard
|
363
|
+
let res = pm.split('#');
|
364
|
+
for(let i = 0; i < res.length; i++) {
|
365
|
+
res[i] = escapeRegex(res[i]);
|
366
|
+
}
|
367
|
+
res = res.join('(\\d+|\\?\\?)');
|
368
|
+
if(pm.startsWith('=')) {
|
369
|
+
res = '^' + res + '$';
|
370
|
+
} else if(pm.startsWith('~')) {
|
371
|
+
res = '^' + res;
|
372
|
+
}
|
373
|
+
re = new RegExp(res, 'g');
|
374
|
+
match = re.test(str);
|
346
375
|
}
|
347
376
|
}
|
348
|
-
|
377
|
+
// Any "min" match indicates NO match for this sub-pattern,
|
378
|
+
for(let j = 0; match && j < p.min.length; j++) {
|
349
379
|
pm = p.min[j];
|
350
380
|
if(pm.startsWith('=')) {
|
351
|
-
match =
|
381
|
+
match = (str !== pm.substring(1));
|
352
382
|
} else if(pm.startsWith('~')) {
|
353
|
-
match =
|
383
|
+
match = !str.startsWith(pm.substring(1));
|
354
384
|
} else {
|
355
|
-
match =
|
385
|
+
match = (str.indexOf(pm) < 0);
|
386
|
+
}
|
387
|
+
// If still matching, check whether pattern contains wildcards
|
388
|
+
if(match && pm.indexOf('#') >= 0) {
|
389
|
+
// If so, now "negatively" rematch using regular expressions
|
390
|
+
let res = pm.split('#');
|
391
|
+
for(let i = 0; i < res.length; i++) {
|
392
|
+
res[i] = escapeRegex(res[i]);
|
393
|
+
}
|
394
|
+
res = res.join('(\\d+|\\?\\?)');
|
395
|
+
if(pm.startsWith('=')) {
|
396
|
+
res = '^' + res + '$';
|
397
|
+
} else if(pm.startsWith('~')) {
|
398
|
+
res = '^' + res;
|
399
|
+
}
|
400
|
+
re = new RegExp(res, 'g');
|
401
|
+
match = !re.test(str);
|
356
402
|
}
|
357
403
|
}
|
404
|
+
// Iterating through OR list, so any match indicates TRUE
|
358
405
|
if(match) return true;
|
359
406
|
}
|
360
407
|
return false;
|
361
408
|
}
|
362
409
|
|
410
|
+
function matchingWildcardNumber(str, patterns) {
|
411
|
+
// Returns the number that, when substituted for #, caused `str` to
|
412
|
+
// match with the pattern list `patterns`, or FALSE if no such number
|
413
|
+
// exists.
|
414
|
+
// First get the list of all groups of consecutive digits in `str`.
|
415
|
+
let nlist = str.match(/(\d+)/g);
|
416
|
+
// If none, then evidently no number caused the match.
|
417
|
+
if(!nlist) return false;
|
418
|
+
// Now for each number check whether `str` still matches when the
|
419
|
+
// number is replaced by the wildcard #.
|
420
|
+
const mlist = [];
|
421
|
+
for(let i = 0; i < nlist.length; i++) {
|
422
|
+
const
|
423
|
+
rstr = str.replaceAll(nlist[i], '#'),
|
424
|
+
pm = patternMatch(rstr, patterns);
|
425
|
+
// If it still matches, add it to the list.
|
426
|
+
if(pm) addDistinct(nlist[i], mlist);
|
427
|
+
}
|
428
|
+
// NOTE This is only a quick and dirty heuristic. For intricate patterns
|
429
|
+
// there may be mutliple matches, and there may be false positives for
|
430
|
+
// patterns like "abc1#2" (so a hashtag with adjacent digits) but for
|
431
|
+
// now this is good enough.
|
432
|
+
if(mlist.length) return mlist[0];
|
433
|
+
return false;
|
434
|
+
}
|
435
|
+
|
363
436
|
function escapeRegex(str) {
|
364
437
|
// Returns `str` with its RegEx special characters escaped
|
365
438
|
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
366
439
|
}
|
367
440
|
|
441
|
+
function wildcardMatchRegex(name, equation=false) {
|
442
|
+
// Returns a RegEx object that will match wildcards in an entity name
|
443
|
+
// with an integer number (\d+), or NULL if `name` does not contain
|
444
|
+
// wildcards
|
445
|
+
// By default, # denotes a wildcard, but this may be changed to ??
|
446
|
+
// when an equation name is parsed
|
447
|
+
const sl = name.split(equation ? '??' : '#');
|
448
|
+
if(sl.length < 2) return null;
|
449
|
+
for(let i = 0; i < sl.length; i++) {
|
450
|
+
sl[i] = escapeRegex(sl[i]);
|
451
|
+
}
|
452
|
+
// NOTE: match against integer numbers, but also match for ?? because
|
453
|
+
// wildcard equation entities also match!
|
454
|
+
return new RegExp(`^${sl.join('(\\d+|\\?\\?)')}$`, 'gi');
|
455
|
+
}
|
456
|
+
|
457
|
+
function wildcardFormat(name, modifier=false) {
|
458
|
+
// Returns string with CSS classes if it contains wildcards
|
459
|
+
// NOTE: modifiers can contain * and single ? as wildcards;
|
460
|
+
// equation names can contain at most one ?? as wildcard
|
461
|
+
const re = (modifier ? /(\?+|\*)/ : /(\?\?)/g);
|
462
|
+
return name.replace(re, '<span class="wildcard">$1</span>');
|
463
|
+
}
|
464
|
+
|
465
|
+
function matchingNumber(m, s) {
|
466
|
+
// Returns an integer value if string `m` matches selector pattern `s`
|
467
|
+
// (where asterisks match 0 or more characters, and question marks 1
|
468
|
+
// character) and the matching parts jointly denote an integer.
|
469
|
+
let raw = s.replace(/\*/g, '(.*)').replace(/\?/g, '(.)'),
|
470
|
+
match = m.match(new RegExp(`^${raw}$`)),
|
471
|
+
n = '';
|
472
|
+
if(match) {
|
473
|
+
// Concatenate all matching characters (need not be digits)
|
474
|
+
m = match.slice(1).join('');
|
475
|
+
n = parseInt(m);
|
476
|
+
}
|
477
|
+
// Return number only if when match is parsed as integer
|
478
|
+
return (n == m ? n : false);
|
479
|
+
}
|
480
|
+
|
368
481
|
function compareSelectors(s1, s2) {
|
369
482
|
// Dataset selectors comparison is case-insensitive, and puts wildcards
|
370
483
|
// last, where * comes later than ?
|
@@ -517,6 +630,21 @@ function xmlDecoded(str) {
|
|
517
630
|
).replace(/\&/g, '&').replace(/\$\$\\n/g, '\n\n');
|
518
631
|
}
|
519
632
|
|
633
|
+
function customizeXML(str) {
|
634
|
+
// NOTE: this function can be customized to pre-process a model file,
|
635
|
+
// for example to rename entities in one go -- USE WITH CARE!
|
636
|
+
// First modify `str` -- by default, do nothing
|
637
|
+
|
638
|
+
/*
|
639
|
+
if(str.indexOf('<author>XXX</author>') >= 0) {
|
640
|
+
str = str.replace(/<url>NL\/(\w+)\.csv<\/url>/g, '<url></url>');
|
641
|
+
}
|
642
|
+
*/
|
643
|
+
|
644
|
+
// Finally, return the modified string
|
645
|
+
return str;
|
646
|
+
}
|
647
|
+
|
520
648
|
function cleanXML(node) {
|
521
649
|
// Removes all unnamed text nodes and comment nodes from the XML
|
522
650
|
// subtree under node
|
@@ -536,7 +664,7 @@ function cleanXML(node) {
|
|
536
664
|
function parseXML(xml) {
|
537
665
|
// Parses string `xml` into an XML document, and returns its root node
|
538
666
|
// (or null if errors)
|
539
|
-
xml = XML_PARSER.parseFromString(xml, 'application/xml');
|
667
|
+
xml = XML_PARSER.parseFromString(customizeXML(xml), 'application/xml');
|
540
668
|
const
|
541
669
|
de = xml.documentElement,
|
542
670
|
pe = de.getElementsByTagName('parsererror').item(0);
|
@@ -789,6 +917,7 @@ if(NODE) module.exports = {
|
|
789
917
|
rangeToList: rangeToList,
|
790
918
|
dateToString: dateToString,
|
791
919
|
msecToTime: msecToTime,
|
920
|
+
compactClockTime: compactClockTime,
|
792
921
|
uniformDecimals: uniformDecimals,
|
793
922
|
ellipsedText: ellipsedText,
|
794
923
|
earlierVersion: earlierVersion,
|