-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathopenmath.js
1471 lines (1396 loc) · 64.2 KB
/
openmath.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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// # OpenMath module
//
// This module implements an encoding of OpenMath objects as JSON. It is *not*
// an official encoding endorsed by the OpenMath Society. It is merely my own
// choice of how to do the encoding, in the absence of an official standard
// (that I could find).
//
// Objects are encoded as follows. (If these phrases are unfamiliar to you,
// see [the OpenMath Standard,
// v2.0](http://www.openmath.org/standard/om20-2004-06-30/).)
// * OMI - `{ t : 'i', v : 6 }` (where `t` stands for type and `v` for value),
// and integers may also be stored as strings if desired (e.g., `-6`)
// * OMF - `{ t : 'f', v : -0.521 }`
// * OMSTR - `{ t : 'st', v : 'example' }`
// * OMB - `{ t : 'ba', v : aUint8ArrayHere }`
// * OMS - `{ t : 'sy', n : 'symbolName', cd : 'cd', uri : 'http://...' }`,
// where the URI is optional
// * OMV - `{ t : 'v', n : 'name' }`
// * OMA - `{ t : 'a', c : [ child, objects, here ] }` (children are the
// required operator, followed by zero or more operands)
// * OMATTR - rather than wrap things in OMATTR nodes, simply add the
// attributes object (a mapping from string keys to objects) to the existing
// object, with 'a' as its key. To create the string key for an OM symbol,
// just use its JSON form (fully compact, as created by `JSON.stringify`
// with one argument).
// * OMBIND - `{ t : 'bi', s : object, v : [ bound, vars ], b : object }`,
// where `s` stands for the head symbol and `b` for the body
// * OMERR - `{ t : 'e', s : object, c : [ child, nodes, here ] }`, where `s`
// stands for the head symbol, and `c` can be omitted if empty.
// * No encoding for foreign objects is specified here.
//
// ## OpenMath Node class
// We declare the following structure for use in the simple encoding and
// decoding routines defined later in the OMNode class.
const tokenTypes = [ {
name : 'symbol',
pattern : /[:A-Za-z_][:A-Za-z_0-9-]*\.[:A-Za-z_][:A-Za-z_0-9-]*/
}, {
name : 'variable',
pattern : /[:A-Za-z_][:A-Za-z_0-9-]*/
}, {
name : 'float',
pattern : /[+-]?(?:[0-9]+\.[0-9]*|[0-9]*\.[0-9]+)/
}, {
name : 'integer',
pattern : /[+-]?[0-9]+/
}, {
name : 'string',
pattern : /"(?:[^"\\]|\\"|\\\\)*"|'(?:[^'\\]|\\'|\\\\)*'/
}, {
name : 'comma',
pattern : /,/
}, {
name : 'openParen',
pattern : /\(/
}, {
name : 'closeParen',
pattern : /\)/
}, {
name : 'openBracket',
pattern : /\[/
}, {
name : 'closeBracket',
pattern : /\]/
} ];
export class OMNode {
// ### Class ("static") methods
//
// The following class method checks to see if an object is of any one of the
// formats specified above; if so, it returns null, and if not, it returns an
// error describing why not. It is recursive, verifying that children are also
// of the correct form.
//
// It either returns a string, meaning that the object is invalid, and the
// string contains the reason why, or it returns null, meaning that the object
// is valid.
static checkJSON( object ) {
let key, reason;
let child;
if (!(object instanceof Object)) {
return `Expected an object, found ${typeof object}`;
}
// If the object has attributes, we must verify that their keys are the
// stringified forms of JSON objects representing OpenMath symbols and their
// values also pass this same validity test, recursively.
if (object.hasOwnProperty('a')) {
for (key of Object.keys(object.a || {})) {
var symbol;
const value = object.a[key];
try {
symbol = JSON.parse(key);
} catch (e) {
return `Key ${key} invalid JSON`;
}
if (symbol.t !== 'sy') {
return `Key ${key} is not a symbol`;
}
if (reason = this.checkJSON(symbol)) { return reason; }
if (reason = this.checkJSON(value)) { return reason; }
}
}
// This function verifies that the object doesn't have any keys beyond those on
// the list, plus 't' for type and 'a' for attributes.
const checkKeys = function( ...list ) {
for (key of Object.keys(object)) {
if (!list.includes(key) && (key !== 't') && (key !== 'a')) {
return `Key ${key} not valid in object of type ${object.t}`;
}
}
return null;
};
// This is not nearly the full range of Unicode symbols permitted for
// identifiers in the OpenMath specification, but is a useful subset for this
// first implementation. See page 14 of [the
// standard](http://www.openmath.org/standard/om20-2004-06-30/omstd20.pdf) for
// the exact regular expression.
const identRE =
/^[:A-Za-z_\u0374-\u03FF][:A-Za-z_\u0374-\u03FF.0-9-]*$/;
// Now we consider each type of object separately.
switch (object.t) {
// Integers must have t and v keys, and the latter must look like an integer,
// whether it's actually one or a string doesn't matter.
case 'i':
if (reason = checkKeys('v')) { return reason; }
if (!/^[+-]?[0-9]+$/.test(`${object.v}`)) {
return `Not an integer: ${object.v}`;
}
break;
// Floats must have t and v keys, and the latter must be a number.
case 'f':
if (reason = checkKeys('v')) { return reason; }
if (typeof object.v !== 'number') {
return `Not a number: ${object.v} of type ${typeof object.v}`;
}
if (isNaN(object.v)) {
return 'OpenMath floats cannot be NaN';
}
if (!isFinite(object.v)) {
return 'OpenMath floats must be finite';
}
break;
// Strings must have t and v keys, and the latter must be a string.
case 'st':
if (reason = checkKeys('v')) { return reason; }
if (typeof object.v !== 'string') {
return `Value for st type was ${typeof object.v}, not string`;
}
break;
// Byte Arrays must have t and v keys, the latter of which is a `Uint8Array`.
case 'ba':
if (reason = checkKeys('v')) { return reason; }
if (!(object.v instanceof Uint8Array)) {
return `Value for ba type was not an instance of Uint8Array`;
}
break;
// Symbols must have t, n, and cd keys, with an optional uri key, all of which
// must be strings. The n key (for "name") must be a valid identifier, in that
// it must match the regular expression defined above.
case 'sy':
if (reason = checkKeys('n','cd','uri')) { return reason; }
if (typeof object.n !== 'string') {
return `Name for sy type was ${typeof object.n}, not string`;
}
if (typeof object.cd !== 'string') {
return `CD for sy type was ${typeof object.cd}, not string`;
}
if ((object.uri != null) && (typeof object.uri !== 'string')) {
return `URI for sy type was ${typeof object.uri}, not string`;
}
if (!identRE.test(object.n)) {
return `Invalid identifier as symbol name: ${object.n}`;
}
if (!identRE.test(object.cd)) {
return `Invalid identifier as symbol CD: ${object.cd}`;
}
break;
// Variables must have t and n keys, the latter of which must be a valid
// identifier, matching the same regular expression as above.
case 'v':
if (reason = checkKeys('n')) { return reason; }
if (typeof object.n !== 'string') {
return `Name for v type was ${typeof object.n}, not string`;
}
if (!identRE.test(object.n)) {
return `Invalid identifier as variable name: ${object.n}`;
}
break;
// Applications must have t and c keys, the latter of which must be an array of
// objects that pass this same validity test, applied recursively. It may not
// be empty.
case 'a':
if (reason = checkKeys('c')) { return reason; }
if (!(object.c instanceof Array)) {
return `Children of application object was not an array`;
}
if (object.c.length === 0) {
return `Application object must have at least one child`;
}
for (child of object.c) {
if (reason = this.checkJSON(child)) { return reason; }
}
break;
// Bindings must have t, s, v, and b keys, where s is a symbol, v an array of
// variables, and b any OpenMath node.
case 'bi':
if (reason = checkKeys('s', 'v', 'b')) { return reason; }
if (reason = this.checkJSON(object.s)) { return reason; }
if (object.s.t !== 'sy') {
return "Head of a binding must be a symbol";
}
if (!(object.v instanceof Array)) {
return "In a binding, the v value must be an array";
}
for (let variable of object.v) {
if (reason = this.checkJSON(variable)) { return reason; }
if (variable.t !== 'v') {
return `In a binding, all values in the v array must have type v`;
}
}
if (reason = this.checkJSON(object.b)) { return reason; }
break;
// Errors must have t, s, and c keys, with s a symbol and c an array of child
// nodes.
case 'e':
if (reason = checkKeys('s', 'c')) { return reason; }
if (reason = this.checkJSON(object.s)) { return reason; }
if (object.s.t !== 'sy') {
return "Head of an error must be a symbol";
}
if (!(object.c instanceof Array)) {
return "In an error, the c key must be an array";
}
for (child of object.c) {
if (reason = this.checkJSON(child)) { return reason; }
}
break;
// If the object's type is not on that list, it's not valid.
default:
return `Invalid type: ${object.t}`;
}
// If all of the above checks pass then we return null, meaning the object is
// valid (no errors).
return null;
}
// The following function converts a string encoding of an OpenMath structure
// and creates an instance of `OMNode` for the corresponding structure.
// * If the string contains invalid JSON, this routine will return an
// error message string rather than an OMNode object.
// * If it contains JSON for a structure that doesn't pass `checkJSON`, above,
// again, an error message string is returned.
// * Otherwise it adds appropriate parent pointers to the nodes in the
// resulting tree, then wraps it in an instance of OMNode and returns it.
// The function can also take an object that has been parsed from such JSON
// text.
static decode( json ) {
let reason;
if (typeof json === 'string') {
try { json = JSON.parse(json); } catch (e) { return e.message; }
}
let fixByteArrays = function ( node ) {
if ( node.t === 'ba' && node.v instanceof Array )
node.v = new Uint8Array( node.v )
for (let c of node.c != null ? node.c : [ ]) { // children, if any
fixByteArrays(c);
}
const object = node.a != null ? node.a : { };
for (let k of Object.keys(object)) { // attribute values, if any
fixByteArrays(object[k]);
}
if (node.b != null) { fixByteArrays(node.b); } // body, if any
}
fixByteArrays( json )
if (reason = this.checkJSON(json)) { return reason; }
let setParents = function( node ) {
let v;
for (let c of node.c != null ? node.c : [ ]) { // children, if any
c.p = node;
setParents(c);
}
if (node.t === 'bi') {
for (v of node.v != null ? node.v : [ ]) { // bound variables, if any
v.p = node;
setParents(v);
}
}
const object = node.a != null ? node.a : { };
for (let k of Object.keys(object)) { // attribute values, if any
v = object[k];
v.p = node;
setParents(v);
}
// head symbol and body object, if any
if (node.s != null) { node.s.p = node; setParents(node.s); }
if (node.b != null) { node.b.p = node; setParents(node.b); }
};
setParents(json);
json.p = null;
return new OMNode(json);
}
// ### Constructor
//
// The above factory function uses the following constructor.
constructor( tree ) {
this.tree = tree;
}
// Define getters for the common attributes type, value, name, cd, uri, symbol, body,
// children, and variables. These all return undefined if they do not apply to the
// current structure, except children and variables, which return empty arrays
// in that case.
get parent () {
if (this.tree.p) { return new OMNode(this.tree.p); } else { return undefined; }
}
get type () { return this.tree.t; }
get value () {
if (this.tree.t !== 'bi') { return this.tree.v; } else { return undefined; }
}
get name () { return this.tree.n; }
get cd () { return this.tree.cd; }
get uri () { return this.tree.uri; }
get symbol () {
if (this.tree.s) { return new OMNode(this.tree.s); } else { return undefined; }
}
get body () {
if (this.tree.b) { return new OMNode(this.tree.b); } else { return undefined; }
}
get children () {
return (this.tree.c != null ? this.tree.c : [ ]).map(child => new OMNode(child));
}
get variables () {
if (this.tree.t === 'bi') {
return this.tree.v.map(variable => new OMNode(variable));
} else {
return [ ];
}
}
// ### Serialization
//
// Unserializing an `OMNode` object from a string is done by the `decode`
// method, above. Serializing is done by its inverse, here, which simply uses
// `JSON.stringify`, but filters out parent pointers.
encode() {
return JSON.stringify(this.tree, function( k, v ) {
if (k === 'p') {
return undefined;
} else if (v instanceof Uint8Array) {
return Array.from( v )
} else {
return v;
}
});
}
// ### Copies and equality
//
// Two instances will often want to be compared for equality, structurally.
// This is essentially the same activity as comparing equality of two JSON
// structures, except parent pointers should be ignored so that the recursion
// remains acyclic.
//
// You can pass a second parameter indicating whether to pay attention to
// attributes in the comparison. By default it is true, meaning consider all
// attributes. If it is false, no attributes will be considered. Other values
// may be supported in the future.
equals( other, attributes ) {
if (attributes == null) { attributes = true; }
var recur = function( a, b ) {
// If they are atomically equal, we're done.
let key, value;
if (a === b) { return true; }
// If they're arrays, ensure they have the same length, type, and contents.
if (a instanceof Array || a instanceof Uint8Array) {
if (( a instanceof Array ) && (!( b instanceof Array ))) {
return false;
}
if (( a instanceof Uint8Array ) &&
(!( b instanceof Uint8Array ))) {
return false;
}
if (a.length !== b.length) { return false; }
for (let index = 0; index < a.length; index++) {
const element = a[index];
if (!recur(element, b[index])) { return false; }
}
return true;
}
// Otherwise, they must be objects, with all the same key-value pairs.
// The one exception to this is that for OpenMath attributes (which are stored
// under key "a"), it is the same if the "a" key is simply absent (meaning no
// attributes) or if its value is the empty object `{ }` (also meaning no
// attributes).
if (!(a instanceof Object)) { return false; }
if (!(b instanceof Object)) { return false; }
for (key of Object.keys(a)) {
value = a[key];
if ((key === 'p') || (!attributes && (key === 'a'))) {
continue;
}
if (!b.hasOwnProperty(key)) {
if (key === 'a') { return recur(value, { }); }
return false;
}
if (!recur(value, b[key])) { return false; }
}
for (key of Object.keys(b)) {
value = b[key];
if ((key === 'p') || (!attributes && (key === 'a'))) {
continue;
}
if (!a.hasOwnProperty(key)) {
if (key === 'a') { return recur(value, { }); }
return false;
}
}
return true;
};
return recur(this.tree, other.tree);
}
// There is also a much stricter notion of equality: Do the two OMNode objects
// actually wrap the same object underneath? That is, are they pointing to the
// same tree in memory? This function can detect that.
sameObjectAs( other ) { return this.tree === (other != null ? other.tree : undefined); }
// On a similar note, you may want to create a distinct copy of any given
// OMNode instance. Here is a method for doing so.
copy() {
var recur = function( tree ) {
let result;
switch (tree.t) {
// Integers, floats, and strings are easy to copy; just duplicate type and
// value. Variables and symbols are easy for the same reason, but different
// atomic members.
case 'i': case 'f': case 'st':
result = { t : tree.t, v : tree.v };
break
case 'v':
result = { t : 'v', n : tree.n };
break
case 'sy':
result = { t : 'sy', n : tree.n, cd : tree.cd };
if (tree.hasOwnProperty('uri')) {
result.uri = tree.uri;
}
break
// Byte arrays require making a copy of the byte array object, which can be
// accomplished with the constructor.
case 'ba':
result = { t : 'ba', v : new Uint8Array(tree.v) };
break
// For errors and applications, we copy the children array; for errors we also
// include the symbol.
case 'e': case 'a':
result = {
t : tree.t,
c : tree.c.map(child => recur(child))
};
if (tree.t === 'e') { result.s = recur(tree.s); }
break
// Lastly, for bindings, we copy each sub-part: symbol, body, variable list.
case 'bi':
result = {
t : 'bi',
s : recur(tree.s),
v : tree.v.map(variable => recur(variable)),
b : recur(tree.b)
};
break
}
// Then no matter what we created, we copy the attributes over as well.
const object = tree.a != null ? tree.a : { };
for (let key of Object.keys(object)) {
const value = object[key];
( result.a != null ? result.a : (result.a = { }) )[key] = recur(value);
}
return result;
};
// Apply the recursive function.
return OMNode.decode(recur(this.tree));
}
// ### Factory functions
//
// We provide here functions for creating each type of OMNode, from integer to
// error. Each is a "static" (class) method, documented separately. It
// returns an error message as a string if there was an error, instead of the
// desired OMNode instance.
//
// The integer factory function creates an OpenMath integer node, and must be
// passed a single parameter containing either an integer or a string
// representation of an integer, e.g., `OM.integer 100`.
static integer( value ) {
return OMNode.decode({ t : 'i', v : value });
}
// The float factory function creates an OpenMath float node, and must be
// passed a single parameter containing a number, e.g., `OM.integer 1.234`,
// and that number cannot be infinite or NaN.
static float( value ) {
return OMNode.decode({ t : 'f', v : value });
}
// The string factory function creates an OpenMath string node, and must be
// passed a single parameter containing a string, e.g., `OM.integer 'hi'`.
static string( value ) {
return OMNode.decode({ t : 'st', v : value });
}
// The byte array factory function creates an OpenMath byte array node, and
// must be passed a single parameter that is an instance of `Uint8Array`.
static bytearray( value ) {
return OMNode.decode({ t : 'ba', v : value });
}
// The symbol factory function creates an OpenMath symbol node, and must be
// passed two or three parameters, in this order: symbol name (a string),
// content dictionary name (a string), and optionally the CD's base URI (a
// string).
static symbol( name, cd, uri ) {
return OMNode.decode((uri != null) ?
{ t : 'sy', n : name, cd, uri }
:
{ t : 'sy', n : name, cd });
}
// The variable factory function creates an OpenMath variable node, and must be
// passed one parameter, the variable name (a string).
static variable( name ) {
return OMNode.decode({ t : 'v', n : name });
}
// The application factory creates an OpenMath application node, and accepts a
// variable number of arguments, each of which must be either an `OMNode`
// instance or the JSON object that could function as the tree within such an
// instance. `OMNode` instances are copied, objects are used as-is.
static application( ...args ) {
const result = { t : 'a', c : [ ] };
for (let arg of args) {
result.c.push(arg instanceof OMNode ?
JSON.parse(arg.encode()) // copy without parent pointers
:
arg
);
}
return OMNode.decode(result);
}
// The attribution factory creates an OpenMath node from its first argument,
// and attaches to it the attributes specified by the remaining arguments.
// Those remaining arguments must come in pairs k1, v1, through kn, vn, and
// each ki,vi pair must be an OpenMath symbol node followed by any OpenMath
// node. As in the case of applications, such nodes may be JSON objects or
// `OMNode` instances; the former are used as-is and the latter copied. The
// first parameter can also be either a JSON object or an `OMNode` instance,
// and in the latter case it, too, is copied.
static attribution( node, ...attrs ) {
if (!(node instanceof Object)) {
return 'Invalid first parameter to attribution';
}
if ((attrs.length % 2) !== 0) {
return 'Incomplete key-value pair in attribution';
}
if (node instanceof OMNode) { node = JSON.parse(node.encode()); }
while (attrs.length > 0) {
if (node.a == null) { node.a = { }; }
let key = attrs.shift();
key = key instanceof OMNode ?
key.encode()
:
JSON.stringify(key);
const value = attrs.shift();
node.a[key] = value instanceof OMNode ?
JSON.parse(value.encode()) // copy without parent pointers
:
value;
}
return OMNode.decode(node);
}
// The binding factory functions exactly like the application factory, except
// that it has restrictions on the types of its arguments. The first must be a
// symbol (used as the head of the binding), the last can be any OpenMath node,
// and all those in between must be variables. Furthermore, there must be at
// least two arguments, so that there is a head and a body. Just as in the
// case of applications, `OMNode` instances are copied, but straight JSON
// objects are used as-is.
static binding( head, ...rest ) {
const adjustedLength = Math.max(rest.length, 1), vars = rest.slice(0, adjustedLength - 1), body = rest[adjustedLength - 1];
if (!(head instanceof Object)) {
return 'Invalid first parameter to binding';
}
if (!(body instanceof Object)) {
return 'Invalid last parameter to binding';
}
const result = {
t : 'bi',
s : head instanceof OMNode ?
JSON.parse(head.encode())
:
head,
v : [ ],
b : body instanceof OMNode ?
JSON.parse(body.encode())
:
body
};
for (let variable of vars) {
result.v.push(variable instanceof OMNode ?
JSON.parse(variable.encode()) // copy w/o parent pointers
:
variable
);
}
return OMNode.decode(result);
}
// The error factory functions exactly like the application factory, except
// that it has one restriction on the types of its arguments: The first must
// be a symbol. Just as in the case of applications, `OMNode` instances are
// copied, but straight JSON objects are used as-is.
static error( head, ...others ) {
if (!(head instanceof Object)) {
return 'Invalid first parameter to binding';
}
const result = {
t : 'e',
s : head instanceof OMNode ?
JSON.parse(head.encode())
:
head,
c : [ ]
};
for (let other of others) {
result.c.push(other instanceof OMNode ?
JSON.parse(other.encode()) // copy without parent pointers
:
other
);
}
return OMNode.decode(result);
}
// For documentation of this routine, refer to the simple encoding/decoding
// entry in our API documentation.
static simpleDecode( input ) {
// Ensure the input is a string.
if (typeof input !== 'string') {
return 'Input was not a string';
}
// Tokenize it using the above token data.
const tokens = [ ];
while (input.length > 0) {
const originally = input.length;
for (let tokenType of tokenTypes) {
const match = tokenType.pattern.exec(input);
if ((match != null) && (match.index === 0)) {
tokens.push({
type : tokenType.name,
text : match[0]});
input = input.slice(match[0].length);
}
}
if (input.length === originally) {
return `Could not understand from here: ${input.slice(0, 11)}`;
}
}
// Parse tokens using two states: one for when an expression is about to start,
// and one for when an expression just ended. Maintain a stack of expressions
// already parsed, for forming application and binding expressions.
let state = 'expression about to start';
const stack = [ ];
while (tokens.length > 0) {
var type;
var expr, i, index;
var asc, end;
var asc1, end1;
let next = tokens.shift();
switch (state) {
case 'expression about to start':
switch (next.type) {
case 'symbol':
var halves = next.text.split('.');
stack.unshift({
node :
OMNode.symbol(halves[1], halves[0])});
break;
case 'variable':
stack.unshift({
node : OMNode.variable(next.text)});
break;
case 'integer':
var int = parseInt(next.text);
if (/\./.test(int)) { int = next.text; }
stack.unshift({node : OMNode.integer(int)});
break;
case 'float':
stack.unshift({
node : OMNode.float(parseFloat(next.text))});
break;
case 'string':
type = next.text[0];
next = next.text.slice(1, -1).replace(
RegExp( `\\\\${type}`, 'g' ), type);
stack.unshift({node : OMNode.string(next)});
break;
default: return `Unexpected ${next.text}`;
}
state = 'expression ended';
break;
case 'expression ended':
switch (next.type) {
case 'comma':
state = 'expression about to start';
break;
case 'openParen':
stack[0].head = 'application';
if ( tokens && tokens[0] && tokens[0].type === 'closeParen' ) {
tokens.shift();
stack.unshift({
node : OMNode.application(
stack.shift().node)
});
state = 'expression ended';
} else {
state = 'expression about to start';
}
break;
case 'openBracket':
stack[0].head = 'binding';
state = 'expression about to start';
break;
case 'closeParen':
for (index = 0; index < stack.length; index++) {
expr = stack[index];
if (expr.head === 'application') { break; }
if (expr.head === 'binding') {
return "Mismatch: [ closed by )";
}
}
if (index === stack.length) {
return "Unexpected )";
}
var children = [ ];
for (i = 0, end = index, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
children.unshift(stack.shift().node);
}
stack.unshift({
node : OMNode.application.apply(
null, children)
});
break;
case 'closeBracket':
for (index = 0; index < stack.length; index++) {
expr = stack[index];
if (expr.head === 'binding') { break; }
if (expr.head === 'application') {
return "Mismatch: ( closed by ]";
}
}
if (index === stack.length) {
return "Unexpected ]";
}
children = [ ];
for (i = 0, end1 = index, asc1 = 0 <= end1; asc1 ? i <= end1 : i >= end1; asc1 ? i++ : i--) {
children.unshift(stack.shift().node);
}
stack.unshift({
node : OMNode.binding.apply(
null, children)
});
break;
default: return `Unexpected ${next.text}`;
}
break;
}
if (typeof (stack != null ? stack[0].node : undefined) === 'string') {
return stack[0].node; // error in building OMNode
}
}
// Parsing complete so there should be just one node on the stack, the result.
// If there is more than one, we have an error.
if (stack.length > 1) {
return "Unexpected end of input";
} else {
return stack[0].node;
}
}
simpleEncode() {
var recur = function( tree ) {
switch ((tree != null ? tree.t : undefined)) {
case 'i': case 'f': return `${tree.v}`;
case 'v': return tree.n;
case 'st': return `'${tree.v.replace(/'/g, '\\\'')}'`;
case 'sy': return `${tree.cd}.${tree.n}`;
case 'ba': return "'byte array'";
case 'e': return "'error'";
case 'a':
var children = ( tree.c.map(c => recur(c)) );
var head = children.shift();
return `${head}(${children.join(',')})`;
case 'bi':
var variables = ( tree.v.map(v => recur(v)) );
head = recur(tree.s);
var body = recur(tree.b);
return `${head}[${variables.join(',')},${body}]`;
default: return `Error: Invalid OpenMath type ${(tree != null ? tree.t : undefined)}`;
}
};
return recur(this.tree);
}
// ### Parent-child relationships
//
// The functions in this category make, break, or report the relationship of an
// OMNode instance to its parents or children.
//
// This first function reports where the node is in its parent. The return
// value will be one of five types:
// * a string containing "c" followed by a number, as in 'c7' - this means
// that the node is in it's parent's `children` array, and is at index 7
// * a string containing "v" followed by a number, as in 'v0' - this is the
// same as the previous, but for the parent's `variables` array
// * the string "b" - this means that the node is the body and its parent is
// a binding
// * the string "s" - this means that the node is a symbol for its parent,
// which is either an error or a binding
// * a lengthier string beginning with "{" - this is the JSON encoded version
// of the attribute key for which the node is the corresponding value
// * undefined if none of the above apply (e.g., no parent, or invalid tree
// structure)
findInParent() {
let index;
if (!this.parent) { return undefined; }
for (index = 0; index < this.parent.children.length; index++) {
const child = this.parent.children[index];
if (this.sameObjectAs(child)) { return `c${index}`; }
}
if (this.type === 'v') {
for (index = 0; index < this.parent.variables.length; index++) {
const variable = this.parent.variables[index];
if (this.sameObjectAs(variable)) { return `v${index}`; }
}
}
if (this.sameObjectAs(this.parent.symbol)) { return 's'; }
if (this.sameObjectAs(this.parent.body)) { return 'b'; }
const object = this.parent.tree.a != null ? this.parent.tree.a : { };
for (let key of Object.keys(object)) {
const value = object[key];
if (this.tree === value) { return key; }
}
return undefined; // should not happen
}
// The inverse of the previous function takes a string output by that function
// and returns the corresponding child/variables/symbol/body immediately inside
// this node. That is, `x.parent.findChild x.findInParent()` will give us back
// the same tree as `x` itself. An invalid input will return undefined.
findChild( indexInParent ) {
switch (indexInParent[0]) {
case 'c': return this.children[parseInt(indexInParent.slice(1))];
case 'v': return this.variables[parseInt(indexInParent.slice(1))];
case 's': return this.symbol;
case 'b': return this.body;
case '{': return this.getAttribute(OMNode.decode(indexInParent));
}
}
// The `findInParent()` function can be generalized to find a node in any of
// its ancestors, the result being an array of `findInParent()` results as you
// walk downward from the ancestor to the descendant. For instance, the first
// bound variable within the second child of an application would have the
// address `[ 'c1', 'v0' ]` (since indices are zero-based). The following
// function computes the array in question, the node's "address" within the
// given ancestor.
//
// If no ancestor is specified, the highest-level one is used. If a value is
// passed that is not an ancestor of this node, then it is treated as if no
// value had been passed. If this node has no parent, or if this node itself
// is passed as the parameter, then the empty array is returned.
address( inThis ) {
if (!this.parent || this.sameObjectAs(inThis)) { return [ ]; }
return this.parent.address( inThis ).concat([ this.findInParent() ]);
}
// The `address` function has the following inverse, which looks up in an
// ancestor node a descendant that has the given address within that ancestor.
// So, in particular, `x.index y.address( x )` should equal `y`. Furthermore,
// `x.index [ ]` will always yield `x`. An invalid input will return
// undefined.
index( address ) {
if (!(address instanceof Array)) { return undefined; }
if (address.length === 0) { return this; }
const child = this.findChild( address[0] )
return typeof child === 'undefined' || child === null ? undefined :
child.index(address.slice(1));
}
// The following function breaks the relationship of the object with its
// parent. In some cases, this can invalidate the parent (e.g., by giving a
// binding or error object no head symbol, or a binding no body, or no bound
// variables). If the object has no parent or its position in that parent is
// undefined (as determined by `@findInParent()`) then this does nothing.
remove() {
let index;
if (!(index = this.findInParent())) { return; }
switch (index[0]) {
case 'c':
this.parent.tree.c.splice(parseInt( index.slice(1) ), 1);
break;
case 'v':
this.parent.tree.v.splice(parseInt( index.slice(1) ), 1);
break;
case 'b': delete this.parent.tree.b; break;
case 's': delete this.parent.tree.s; break;
case '{': delete this.parent.tree.a[index]; break;
}
delete this.tree.p;
}
// It will also be useful in later functions in this class to be able to
// replace a subtree in-place with a new one. The following method
// accomplishes this, replacing this object in its context with the parameter.
// This works whether this tree is a child, variable, head symbol, body, or
// attribute value of its parent. If this object has no parent, then we make
// no modifications to that parent, since it does not exist.
//
// In all other cases, the parameter is `remove()`d from its context, and this
// node, if it has a parent, is `remove()`d from it as well. Furthermore, this
// OMNode instance becomes a wrapper to the given node instead of its current
// contents. The removed node is returned.
replaceWith( other ) {
if (this.sameObjectAs(other)) { return; }
const index = this.findInParent();
// If you attempt to replace a binding's or error's head symbol with a
// non-symbol, this routine does nothing. If you attempt to replace one of a
// binding's variables with a non-variable, this routine does nothing. When
// this routine does nothing, it returns undefined.
if ((index === 's') && (other.type !== 'sy')) { return; }
if (((index != null ? index[0] : undefined) === 'v') && (other.type !== 'v')) { return; }
other.remove();
const original = new OMNode(this.tree);
this.tree = other.tree;
switch ((index != null ? index[0] : undefined)) {
case 'c':
original.parent.tree.c[parseInt(index.slice(1))] = this.tree;
break;
case 'v':
original.parent.tree.v[parseInt(index.slice(1))] = this.tree;
break;
case 'b': original.parent.tree.b = this.tree; break;
case 's': original.parent.tree.s = this.tree; break;
case '{': original.parent.tree.a[index] = this.tree; break;
default: return; // didn't have a parent
}
this.tree.p = original.tree.p;
delete original.tree.p;
return original;
}
// ### Attributes
//
// Here we have three functions that let us manipulate attributes without