-
Notifications
You must be signed in to change notification settings - Fork 0
/
ai1ConvertBlocks.js
2556 lines (2369 loc) · 125 KB
/
ai1ConvertBlocks.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
/*
* ai1ConvertBlocks.js: Conversion of XML block representation in AI1 .blk files
* to XML block representation of AI2 .bky files.
*
* Author: Lyn Turbak ([email protected])
*
* Implementation History:
*
* [lyn, 2014 Jul 06-18]: Develop main architecture for converting .blk to .bky XML representations,
* including details for leaf blocks, simple component events, component property setters.
*
* [lyn, 2015 Jun 03]: Swap in code architecture from last July.
*
* [lyn, 2015 Jun 06]: Fix escapeHTML to handle spaces and newlines
*
* [lyn, 2015 Jun 07]: Implement component property getters
*
* [lyn, 2015 Jun 08]: Da beginning of da big push:
* + handle simple binary operator blocks
* + special case handling of \n in text block string
* + handle empty sockets in operators blocks
* + successfully convert A1ConvertTestEquals.zip and AIConvertEmptySockets.zip
* + generalize operator conversion to expOp, which handles all expression operators,
* including those with expandable number of args (e.g., make-text).
* + fix bug in domToPrettyText (trimming lines) so that .blk XML displays OK.
* + successfully test conversion of all text operators with empty sockets (AIConvertTextOps.zip)
* + checkout AI1 v134a to explore component database.
* - initially think simple_components.js contains database, but this is really from AI2
* and is still in repository since I didn't do ant clean!
* - learn on Jun 09 that the *real* database is in components/build/classes/XmlComponentDescription/ya_lang_def.xml
* + compare canvas-with-ball programs in AI1 and AI2 in preparation for conversion.
*
* [lyn, 2015 Jun 09]: more work on da big push:
* + Although AI1 v134a has a simple_component.json file, it only has component properties,
* and not events or methods. Event/methods are in components/build/classes/XmlComponentDescription/ya_lang_def.xml
* - Copy this file to AI1_v134a_ya_lang_def.xml
* - it's too painful to read and process XML file in JavaScript, so ...
* - create python file XMLLangDefToJsonComponentSpecs.py that processes xml lang def in AI1_v134a_ya_lang_def.xml
* to produce json component spec AI1_v134a_component_specs defined in AI1_v134a_component_specs.js.
* This json summarizes key aspects of events and methods.
* - Below, the function addComponentEntriesToAI1ConversionMap processes these specs and adds them to AI1ConversionMap
* + Handle component event and (nongeneric) methods, which takes a long time because of component spec issues above.
* Also, handling associating AI1 argument declaration names with correct event handler/procedure parameter names
* is tricky, and requires sorting blocks by declaraion status.
* + Handle event parameters and local variable declarations ("name" blocks of genus "argument")
* and local variable getters ("value" blocks of genus "getter"). One tricky aspect is handling
* orphaned getter blocks -- getter where associated where "name" block isn't plugged in or is in different scope.
* Handle these by creating getters beginning with name "*orphanedArg".
*
* [lyn, 2015 Jun 10]: yet more work on da big push:
* + Fix bug in handling of orphaned arguments
* + Convert global declarations, global getters, global setters
* + Convert all math ops
* + Disaster: fast 13" macbook becomes unusable, must move to older, slower 15" macbook.
* + Convert ifelse (in straightforward way, maybe can be fancier later)
* + Simplify passing of maps between conversion functions by bundling in a single maps variable.
* + Convert procedure declarations; much code can be shared betweedn void and fruitful versions.
*
* [lyn, 2015 Jun 11]: getting closer to the end ..
* + Back to work on fast 13" laptop. Yay!
* + Convert procedure callers. Simpler than I thought because AI1 XML has explicit arg names in
* BlockConnectors, so there's no need to get them from procedure declaration.
* + Convert all ops for lists and colors
* + Fix handling of expandables in operator conversion
* + Clean up conversion code and AI1Conversion map.
* + Convert all logic ops. "and" and "or" are tricky, because their args are expandables
* in AI1 but not mutables in AI2. So have to convert list of args to linear tree of
* binary operators.
*
* [lyn, 2015 Jun 12]: finally wrap up draft implementation and start testing
* + Complete conversion of control ops (straightforward): loops, choose, screen ops
* + Use try/catch to report errors in convertBlock
* + Test screen events, methods and operators with two screens. Everything works fine
* except openScreenAnimation and closeScreenAnimation methods, which have been
* converted from methods to properties in Form version 11. I don't handle these
* correctly yet.
* + Convert generic objects, methods, property getters/setters
* + Handle comments, collapsed blocks, deactivated/disabled blocks.
* + All functionality now converted and tested on simple tests, now begin "real"
* tests on significant AI1 programs from my Fall 2011 course.
*
* [lyn, 2015 Jun 13]: much more testing
* + Test on AI1 projects provided by power users, some of which have thousands
* of blocks, some of which have many (e.g. 37) screens
* + Investigate and handle color None by converting to (make-color (make-list 255 255 255 0))
* + Discover following problems between AI1 v134a and AI2 nb120 (mid Dec. 2013, one of earliest
* releases of AI2 (after which AI2 upgrader will handle things):
* - TinyDB.getValue has extra notFound argument in AI2. This is not even expressed in
* component version numbers, but I caught it in power user testing
* - Screen.openScreenAnimation and Screen.closeScreenAnimation changed from methods
* to properties. I had stumbled on this before. But not handled by upgrader,
* so I need to handle it manually
* - Player.IsLooping property renamed to Player.Loop. Again, not handled by
* upgrading, so converter needs to handle.
* - Twitter.SetStatus method renamed to Twitter.Tweet. Again, not handled by
* upgrading, so converter needs to handle.
* The above were found by exhaustive manual git branch comparisons between nb120
* and v134a in IntelliJ.
*
* [lyn, 2015 Jun 14]:
* + Fix all issues discovered last night
* + Old component version numbers are preventing upgrading when loading converted
* projects to AI2. I implement .scm converter that (1) updates component versions
* to more recent versions when it's safe to do so and (2) reports an error in
* an orange box when it's not safe.
* + In general, now distinguish between system errors (red) and user/project errors (orange).
* + Converter now appears to work on all my test cases and those of powerusers.
* Some converter poweruser projects have runtime errors, but it's not clear they're
* a result of conversion.
* + 2pm: Post converter version v0.1 to internal AI groups.
* + Fix missing next processing in generic method calls highlighted by Taifun's examples.
* + 5pmish: Post converter version v0.2 to internal AI groups.
*
* [lyn, 2015 Jun 17]:
* + Correctly handle the conversion of empty .blk files
* + Add an extra cancelable argument to Notifier.ShowTextDialog and Notifier.ShowChooseDialog
* if it's missing.
*
* [lyn, 2015 Jun 21]:
* + In response to Stephen Burnett Height_calc.aia buggy conversion file,
* created AI1ConvertMathOpsMore.zip file for testing behavior of all math ops.
* Discovered bugs in the translation of atan2 (which Stephen's code uses) and
* random set seed (which Stephen's code doesn't use). I fixed this
* to created version 1.1 of converter.
*
* [lyn, 2015 Jun 23]: Change for version 1.2:
* + Fixed converter to handle unicode characters.
*
* [lyn, 2015 Jun 24]: Change for version 1.2:
* + Handle renaming of ImagePicker.ImagePath property to ImagePicker.Selection
* (not handled correctinly in AI2 versioning.js).
*
* [lyn, 2015 Jun 30]: Change for version 1.2:
* + Change foo.children (doesn't work in Safari) to goog.dom.getChidren(foo).
* But there's still a problem with downloading file in Safari.
*
* /
* TODO:
* + Platform/browser problems:
* - Why doesn't .aia file download in Safari?
* - Any chance of getting this to work in IE?
* + Test various procedure error cases
* + Problematic blocks:
* - distinguishing math and numeric equality
* + Other problems
- why does PaintPoint.zip from 2011 fail, but one reloaded succeeds.
* + param names that change in AI2
* + Test version upgrading issues
* + Wish list:
* - recording usage stats
* - Fancier handling of if
*/
goog.require('goog.dom');
// Returns object {xml: ..., numBlocks: ...}
function convert_AI1_XML_to_AI2_XML(filename, AI1_XML) { // Both AI1_XML and AI2_XML are strings
if (AI1_XML.length == 0) {
reportWarning("AI1 file " + filename + " is empty!");
return {xml: createAI2EmptyXML(), numBlocks: 0, componentFeaturesMap:{events:{}, methods:{}, properties:{}}};
}
try {
var parseBlocksResult = parseBlocks(AI1_XML);
var blocksAndStubs = parseBlocksResult.blocksAndStubs;
var componentTypeMap = parseBlocksResult.componentTypeMap;
var AI1_IdMap = makeBlockIdMap(blocksAndStubs);
} catch(err) {
reportSystemError("Caught error in parseBlocks: " + err.message);
return {xml: createAI2EmptyXML (), numBlocks: 0, componentFeaturesMap:{events:{}, methods:{}, properties:{}}};
}
// var keys = [];
// for (var key in IdMap) {
// keys.push(key);
// }
// alert(JSON.stringify(keys));
try {
var converted = convertBlocks(AI1_IdMap, componentTypeMap);
return {xml: domToPrettyText(converted.xml),
numBlocks: converted.numBlocks,
componentFeaturesMap: converted.componentFeaturesMap};
} catch(err) {
reportSystemError("Caught error in convertBlocks: " + err.message);
return {xml: createAI2EmptyXML(), numBlocks: 0, componentFeaturesMap:{events:{}, methods:{}, properties:{}}};
}
}
function createAI2EmptyXML () {
/* <xml xmlns="http://www.w3.org/1999/xhtml">
<yacodeblocks ya-version="75" language-version="17"></yacodeblocks>
</xml>
*/
var xml = goog.dom.createDom('xml');
xml.setAttribute("xmlnsx", "http://www.w3.org/1999/xhtml");
xml.appendChild(createElement("yacodeblocks", {"ya-version": "75", "language-version":"17"}, []));
return domToPrettyText(xml);
}
// Return the blocks & block stubs from the XML for an AI1 file.
function parseBlocks(text) {
var oParser = new DOMParser();
var dom = oParser.parseFromString(text, 'text/xml');
var yaCodeBlocks = getChildByTagName("YACodeBlocks", dom);
var pages = getChildByTagName("Pages", yaCodeBlocks);
var page = getChildByTagName("Page", pages);
var pageBlocks = getChildByTagName("PageBlocks", page);
var youngAndroidMaps = getChildByTagName("YoungAndroidMaps", yaCodeBlocks);
var youngAndroidUuidMap = getChildByTagName("YoungAndroidUuidMap", youngAndroidMaps);
var uuidEntries = youngAndroidUuidMap.getElementsByTagName("YoungAndroidUuidEntry");
var componentTypeMap = {};
for (var i = 0, entry; entry = uuidEntries[i]; i++) {
componentTypeMap[entry.getAttribute("component-id")] = entry.getAttribute("component-genus");
}
// var yacodeblocks = getFirstElementChild("yacodeblocks", dom);
// var pages = getFirstElementChild("pages", yacodeblocks);
// var page = getFirstElementChild("page", pages);
// var pageblocks = getFirstElementChild("pageblocks", page);
// [lyn, 06/29/2015] Use goog.dom.getChildren rather than .children (which does not work in all browsers
return {// "blocksAndStubs": pageBlocks.children,
"blocksAndStubs": goog.dom.getChildren(pageBlocks),
"componentTypeMap": componentTypeMap};
}
var maxIdSoFar = 0; // Keep track of largest Id seen, for generating new ones.
// Given a list of AI1 blocks and block stubs, return a map of each id to its corresponding block
function makeBlockIdMap(blocksAndStubs) {
var IdMap = {};
for (var i = 0, blockOrStub; blockOrStub = blocksAndStubs[i]; i++) {
var block = getBlock(blockOrStub);
var id = block.attributes.getNamedItem("id").value;
maxIdSoFar = Math.max(maxIdSoFar, id);
IdMap[id] = blockOrStub; // When stub, put stub here!
}
return IdMap;
}
/* // No longer seems necessary ...
// Given a blockIdMap, return a map of procedure declaration names to their ids
// This is used by conversion process to ensure that procedure declaration argument names
// are processed before callers of the procedure are processed.
function makeProcNameIdMap(blockIdMap) {
var ids = Object.keys(blockIdMap);
var procNameIdMap = {}
for (var i = 0; i < ids.length, i++) {
var id = id[i];
var block = blockIdMap[id];
var genus = block.getAttribute("genus-name");
if (genus == "define" || genus == "define-void") {
var procName = getLabelText(block);
procNameIdMap[procName] = id;
}
}
return procNameIdMap;
}
*/
// Return next unused id. We only call this after blockIdMap is created, which simplifies implementation.
function nextId() {
maxIdSoFar++;
return maxIdSoFar;
}
// Returns a list of blocks and stubs ordered by the genus of the underlying block.
// Declaration blocks (event handlers, procedures definitions, global variable declarations)
// come before non-declaration blocks
function sortedBlocks(idMap) {
var keys = Object.keys(idMap);
var blocks = [];
for (var i = 0; i < keys.length; i++) {
blocks.push(getBlock(idMap[keys[i]]));
}
blocks.sort(blockComparator);
return blocks;
}
// Comparator for ordering block. All declaration blocks (global variables,
// event handlers, procedures) should come before non-declaration blocks (everything else).
// This guarantees that variable declarations will be processed before their uses.
function blockComparator(blk1, blk2) { // blk1 and blk2 are blocks, not stubs
var id1 = blk1.getAttribute("id");
var id2 = blk2.getAttribute("id");
if (isDeclaration(blk1)) {
if (isDeclaration(blk2)) {
return id1 - id2; // arbitrarily sort declarations by id
} else {
return -1; // declarations precede non-declarations;
}
} else { // blk1 not a declaration
if (isDeclaration(blk2)) {
return 1; // declarations precede non-declarations;
} else {
return id1 - id2; // arbitrarily sort non-declarations by id
}
}
}
function isDeclaration(block) { // block, not a stub
var genus = block.getAttribute("genus-name");
var spec = AI1ConversionMap[genus];
if (spec) {
return spec.kind == "declaration"; // i.e., true for component events, procedure declarations, global variable declarations
} else {
return false;
}
}
// inputIdMap associates AI1 block ids with the AI1 block
// outputIdMap associates AI1 block ids with the converted AI2 block
// Returns object {xml: ..., numBlocks: ...}
function convertBlocks(inputIdMap, componentTypeMap) {
var sorted = sortedBlocks(inputIdMap);
var sortedBlockIds = sorted.map(function (block) { return block.getAttribute("id"); });
// console.log("sortedBlockIds: " + sortedBlockIds);
var outputIdMap = {}; // Map id of AI1 input block to AI2 output block, effectively memoizing conversion
var parentMap = {}; // Map id of AI input block to parent id of AI1 input block. Used to determine top level AI input blocks
// = top level AI2 output blocks
var variableMap = {}; // Map variable "name" block (genus "argument") to the event-handler declaration name it should be.
var componentFeaturesMap = {events: {}, methods:{}, properties: {}}; // Keep track of which component events, methods, and properties were used.
// to inform upgrading of .scm file
/* // No longer seems necessary ...
var procNameIdMap = makeProcNameIdMap(inputIdMap); // Map procedure names to their ids
var procNameParamsMap = {}; // Map each procedure names to an array of its parameter names.
*/
// Bundle up all maps into an object to reduce number of arguments passed around in conversion
var maps = {inputIdMap: inputIdMap, outputIdMap: outputIdMap, componentTypeMap: componentTypeMap,
parentMap: parentMap, variableMap: variableMap,
componentFeaturesMap: componentFeaturesMap
// procNameIdMap: procNameIdMap, procNameParamsMap: procNameParamsMap // // No longer seems necessary ...
}
for (var i = 0, id; id = sortedBlockIds[i]; i++) {
convertBlock(id, maps);
}
// Top Level blocks are those without parents.
var topLevelBlockIds = sortedBlockIds.filter(function(blockId) {
return (blockId in outputIdMap) && !(blockId in parentMap);});
// An orphan block (not to be confused with Orphan Black ;-) ) is a top-level block
// that's not a declaration. I.e., it's a top-level expression or statement
// not connected to anything else.
// Note that orphans are determined from the structure of the input AI1 file.
// Upon loading the converted file, AI2 may create new orphans if there was a bug in conversion.
var orphanedBlockIds = topLevelBlockIds.filter(function (blockId) {
return ! isDeclaration(inputIdMap[blockId]); });
if (orphanedBlockIds.length > 0) {
reportWarning("The result of conversion has " + orphanedBlockIds.length + " orphaned block assemblies "
+ "(for input blocks " + blockLabelsAndIdsToString(orphanedBlockIds, inputIdMap) + ")."
+ " These are top-level expression or statement blocks not connected to any"
+ " declaration blocks (i.e., event handlers, procedure declarations, global variable declarations)."
+ " Orphan blocks are not necessarily an error, but you should check them if your AI2"
+ " program does not behave correctly."
);
}
var xml = goog.dom.createDom('xml')
for (var i = 0, topLevelId; topLevelId = topLevelBlockIds[i]; i++) {
// For testing only, include all converted blocks
var converted = outputIdMap[topLevelId];
// Add location information for top-level blocks
var unconverted = inputIdMap[topLevelId];
var location = getLocation(unconverted);
if (location.x && location.y) {
converted.setAttribute("x", location.x);
converted.setAttribute("y", location.y);
}
if (converted) {
xml.appendChild(converted); // Careful! Appending an element that is already a child of another element will move it.
}
}
// Add the following version information:
// <yacodeblocks ya-version="75" language-version="17"></yacodeblocks>
// 75 summarizes the component versions of AI1,
// 17 is the earliest AI2 version; upgrader will upgrade it beyond this.
xml.appendChild(createElement("yacodeblocks", {"ya-version": "75", "language-version":"17"}, []));
var numBlocksConverted = Object.keys(outputIdMap).length;
return {xml: xml, numBlocks: numBlocksConverted, componentFeaturesMap: componentFeaturesMap}
}
function convertBlock(id, maps) {
if (! conversionInProgress) {
return;
}
// console.log("convertBlock on id " + id);
try {
var block = getBlock(maps.inputIdMap[id]);
var genus = block.getAttribute("genus-name");
if (genus == "argument") {
// AI1 "argument" blocks, i.e., variable declaration name blocks, do not translate to an AI2 block.
// However, for variable translations, need to know the label on this block, so return
// the label as a string rather than any result block. We do *not* store Id for these
// blocks in the outputIdMap.
var connectors = block.getElementsByTagName("BlockConnector");
if (connectors.length == 1 && connectors[0].getAttribute("connector-kind") == "plug") {
var argName = getLabelText(block); // name on the block
if (connectors[0].getAttribute("con-block-id")) {
return argName; // return label since it will be associated with parent param name
} else { // It's an orphaned top-level block, and need to associate it with *some* variable name!
var convertedName = maps.variableMap[argName];
if (! convertedName) { // No getter for argName has been looked up yet
maps.variableMap[argName] = nameNotInValues(maps.variableMap);
} // Otherwise argName has already been converted to orphan arg name by looking up associated getter elsewhere
return argName; // Still need to return (the value is arbitrary) to avoid other processing.
}
} else {
throw new Error("convertBlock on argument: unexpected connector info");
}
}
var spec = AI1ConversionMap[genus];
if (! spec) {
resultBlock = undefined;
reportSystemError("Don't know how to translate AI1 block genus " + genus);
} else {
var resultBlock = goog.dom.createDom('block');
resultBlock.setAttribute("id", id);
resultBlock.setAttribute("type", spec.type);
// Collapsing: In AI1, only top-level blocks can be collapsed.
// We don't know yet which blocks are top-level, so we check them all.
// - In AI1, collapsing is indicated by the block containing a <Collapsed/> tag.
// - In AI2, collapsing on any block is indicated with a block attribute collapsed="true"
if (getChildByTagName("Collapsed", block)) {
resultBlock.setAttribute("collapsed", true);
}
// Deactivating/disabling:
// - In AI1, any block can be deactivated with a <Deactivated/> tag.
// - In AI2, any block can be disabled withan attribute disabled="true"
if (getChildByTagName("Deactivated", block)) {
resultBlock.setAttribute("disabled", true);
}
spec.convert(id, block, spec, resultBlock, maps); // Apply conversion function specific to spec.
maps.outputIdMap[id] = resultBlock; // Note that if genus == "argument", we return above,
// and so also do not include such blocks in the outputIdMap.
// Commenting
// - In AI1, any block can have a comment, like this:
// <Comment>
// <Text>Changed the width of the button to 200.</Text>
// <Location><X>0</X><Y>0</Y></Location><BoxSize><Width>200</Width><Height>100</Height></BoxSize>
// </Comment>
// -In AI2,comments come after fields and before sockets. They look like this.
// <comment pinned="false" h="80" w="160">This is a button.</comment>
var commentChild = getChildByTagName("Comment", block);
if (commentChild) {
var text = getElementText(getChildByTagName("Text", commentChild));
var sizeChild = getChildByTagName("BoxSize", commentChild);
var widthString = getElementText(getChildByTagName("Width", sizeChild));
var heightString = getElementText(getChildByTagName("Height", sizeChild));
var width = widthString == "" ? 50 : parseInt(widthString);
var height = heightString == "" ? 50 : parseInt(heightString);
var commentIsVisible = Boolean(getChildByTagName("Visible", commentChild));
// console.log("Creating text node for comment with text '" + text + "'");
var ai2CommentElt = createElement("comment",
{pinned: false,
// pinned: commentIsVisible
// Ignore commentIsVisible, and always set to false.
// If true, comment bubble will show, but often in
// unusual position.
w: width,
h: height},
[createTextNode(repairString(text))]);
insertComment(resultBlock, ai2CommentElt);
}
}
return resultBlock;
} catch(err) {
reportSystemError("Caught error in convertBlock of id " + id + ": " + err.message);
resultBlock = undefined;
}
}
function convertVariableGetter(id, block, spec, resultBlock, maps) {
/* Example:
A1:
<BlockStub><StubParentName>y</StubParentName><StubParentGenus>argument</StubParentGenus><Block id="819" genus-name="getter" >
<Location><X>274</X><Y>1109</Y></Location>
<Label>y</Label>
<Plug>
<BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="811" ></BlockConnector>
</Plug>
</Block>
</BlockStub>
=> AI2:
<block type="lexical_variable_get" id="37">
<mutation>
<eventparam name="y"></eventparam>
</mutation>
<field name="VAR">y</field> // In general this name might be different than label getter due to weird
// way AI1 handles naming via "name" and "value" naming blocks.
// But converter does the right thing to fill in this field appropriately
</block>
*/
var argName = getLabelText(block);
var varName = maps.variableMap[argName];
if (! varName) { // The associated "name" block (genus "argument") is an orphan, or declared in some other scope.
var varName = nameNotInValues(maps.variableMap); // create new orphaned argument name ...
maps.variableMap[argName] = varName; // and remember it
}
appendChildren(resultBlock,
[// AI2 loading will add this, so we don't need to add it here.
// createElement("mutation", {},
// [createElement("eventparam", {"name": varName}, [])]),
createFieldElement("VAR", varName),
]);
}
/*----------------------------------------------------------------------
Convert leaves of block trees.
----------------------------------------------------------------------*/
function convertLeaf(id, block, spec, resultBlock, maps) {
/* Example 1:
AI1 <Block id="670" genus-name="number" > ... <Label>17</Label> ... </Block>
=> AI2:
<block type="math_number" id="670">
<field name="NUM">17</field>
</block>
Example 2:
AI1 <Block id="417" genus-name="color-red" > ... <Label>Red</Label> ... </Block>
=> AI2:
<block type="color_red" id="417">
<field name="COLOR">#ff0000</field>
</block>
*/
// spec.fieldValue overrides label in the case of colors (want "#ff0000", not "Red")
// and boolean literals (want "TRUE", not "true")
var fieldValue = spec.fieldValue ? spec.fieldValue : getLabelText(block);
if (spec.type == "text") {
// Special case for processing text value string (to handle strings with too many '\\' characters)
fieldValue = repairString(fieldValue);
}
resultBlock.appendChild(createFieldElement(spec.fieldName, fieldValue));
}
/*----------------------------------------------------------------------
Convert component properties, events, and methods
----------------------------------------------------------------------*/
function convertComponentGetter(id, block, spec, resultBlock, maps) {
/* Example:
AI1:
<BlockStub><StubParentName>Button1.BackgroundColor</StubParentName><StubParentGenus>read-write-property</StubParentGenus>
<Block id="552" genus-name="componentGetter" >
<Location><X>272</X><Y>97</Y></Location>
<Label>Button1.BackgroundColor</Label>
<Plug><BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="550" ></BlockConnector></Plug>
</Block>
</BlockStub>
=> AI2:
<block type="component_set_get" id="552" x="272" y="97">
<mutation component_type="Button" set_or_get="get" property_name="BackgroundColor" is_generic="false" instance_name="Button1"></mutation>
<field name="COMPONENT_SELECTOR">Button1</field>
<field name="PROP">BackgroundColor</field>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Button1.Visible" => ["Button1", "Visible"]
var instanceName = splitList[0];
var propertyName = splitList[1];
var componentType = maps.componentTypeMap[instanceName];
maps.componentFeaturesMap.properties[componentType + "." + propertyName] = true;
resultBlock.setAttribute("inline", "false");
// -------------------------------------------------
// Special case for renaming properties from AI1 to AI2
if (componentType == "Player" && propertyName == "IsLooping") {
propertyName = "Loop";
} else if (componentType == "ImagePicker" && propertyName == "ImagePath") {
propertyName = "Selection";
}
// -------------------------------------------------
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
instance_name: instanceName,
property_name: propertyName,
set_or_get: "get",
is_generic: "false",
},
[]),
createFieldElement("COMPONENT_SELECTOR", instanceName),
createFieldElement("PROP", propertyName)
]);
}
function convertComponentSetter(id, block, spec, resultBlock, maps) {
/* Example:
AI1:
<Block id="549" genus-name="componentSetter" > ...
<Label>Button1.Visible</Label> ...
<AfterBlockId>553</AfterBlockId> ...
<Sockets num-sockets="1" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="to"
position-type="single" con-block-id="551" >
</BlockConnector>
</Sockets>
</Block>
=> AI2:
<block type="component_set_get" id="549" inline="false">
<mutation component_type="Button" set_or_get="set" property_name="Visible"
is_generic="false" instance_name="Button1"></mutation>
<field name="COMPONENT_SELECTOR">Button1</field>
<field name="PROP">Text</field>
<value name="VALUE">...code for block 551...</value>
<next>...code for block 553...</next>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Button1.Visible" => ["Button1", "Visible"]
var instanceName = splitList[0];
var propertyName = splitList[1];
var componentType = maps.componentTypeMap[instanceName];
maps.componentFeaturesMap.properties[componentType + "." + propertyName] = true;
resultBlock.setAttribute("inline", "false");
// -------------------------------------------------
// Special case for renaming properties from AI1 to AI2
if (componentType == "Player" && propertyName == "IsLooping") {
propertyName = "Loop";
} else if (componentType == "ImagePicker" && propertyName == "ImagePath") {
propertyName = "Selection";
}
// -------------------------------------------------
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
instance_name: instanceName,
property_name: propertyName,
set_or_get: "set",
is_generic: "false",
},
[]),
createFieldElement("COMPONENT_SELECTOR", instanceName),
createFieldElement("PROP", propertyName)
]);
convertChildWithLabel("to", "VALUE", id, block, resultBlock, maps);
convertNextStatement(id, block, resultBlock, maps);
}
function convertComponentEvent(id, block, spec, resultBlock, maps) {
/* AI: <Block id="545" genus-name="Button-Click" > ... <Label>Button1.Click</Label> ...
<Sockets num-sockets="1" >
<BlockConnector connector-kind="socket" connector-type="cmd" init-type="cmd" label="do" is-indented="yes"
position-type="single" con-block-id="555" ></BlockConnector>
</Sockets>
</Block>
=> AI2:
<block type="component_event">
<mutation component_type="Button" instance_name="Button1" event_name="Click"></mutation>
<field name="COMPONENT_SELECTOR">Button1</field>
<statement name="DO">...block for #555...</statement>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Button1.Click" => ["Button1", "Click"]
var instanceName = splitList[0];
var eventName = splitList[1]; // Same as spec["eventName"]
var componentType = maps.componentTypeMap[instanceName]; // Same as spec["componenType"]
maps.componentFeaturesMap.events[componentType + "." + eventName] = true;
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
instance_name: instanceName,
event_name: eventName,
},
[]),
createFieldElement("COMPONENT_SELECTOR", instanceName),
]);
// process argument name declarations of event handler
var socketLabels = getExpressionSocketLabels(block);
var socketIds = getExpressionSocketIds(block);
var paramNames = spec["paramNames"];
assert(sameNames(socketLabels, paramNames),
"socketLabels is " + namesToString(socketLabels) + " but paramNames is " + namesToString(paramNames));
for (var index = 0; index < paramNames.length; index++) {
var socketId = socketIds[index];
if (socketId) {
var argNameLabel = convertBlock(socketId, maps);
if (! typeof(argNameLabel) == "string") {
throw new Error("convertComponentEvent: unexpected argNameLabel " + argNameLabel);
} else {
maps.variableMap[argNameLabel] = paramNames[index]; // Remember that arg name should be mapped back to param name
// *** This assumes param name is the same in AI2, which may not be true ***
}
}
}
// process body of event handler
convertChildWithLabel("do", "DO", id, block, resultBlock, maps);
}
function convertComponentMethod(id, block, spec, resultBlock, maps) {
/* AI:
<Block id="855" genus-name="Canvas-DrawCircle" >
<Location><X>109</X><Y>1427</Y></Location>
<Label>Canvas1.DrawCircle</Label>
<BeforeBlockId>851</BeforeBlockId>
<AfterBlockId>841</AfterBlockId>
<Sockets num-sockets="3" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="x" position-type="single" con-block-id="863" ></BlockConnector>
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="y" position-type="single" con-block-id="865" ></BlockConnector>
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="r" position-type="single" con-block-id="861" ></BlockConnector>
</Sockets>
</Block>
=> AI2:
<block type="component_method" id="170" inline="false">
<mutation component_type="Canvas" method_name="DrawCircle" is_generic="false" instance_name="Canvas1"></mutation>
<field name="COMPONENT_SELECTOR">Canvas1</field>
<value name="ARG0">...conversion of block 863...</value>
<value name="ARG1">...conversion of block 865...</value>
<value name="ARG21">...conversion of block 861...</value>
<next>...code for block 841...</next>
</block> */
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Canvas1.DrawCircle" => ["Canvas1", "DrawCircle"]
var instanceName = splitList[0];
var methodName = splitList[1]; // Same as spec["methodName"]
var componentType = maps.componentTypeMap[instanceName]; // Same as spec["componentType"]
maps.componentFeaturesMap.methods[componentType + "." + methodName] = true;
// -------------------------------------------------
// Special case for renaming method from AI1 to AI2
if (componentType == "Twitter" && methodName == "SetStatus") {
methodName = "Tweet";
}
// -------------------------------------------------
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
instance_name: instanceName,
method_name: methodName,
is_generic: false
},
[]),
createFieldElement("COMPONENT_SELECTOR", instanceName),
]);
var argIds = getExpressionSocketIds(block); // List of ids/nulls for arg blocks
var numArgs = argIds.length; // Number of arg sockets in input and output block
for (var i = 0; i < numArgs; i++) {
convertChildWithId(argIds[i], "ARG" + i, id, block, resultBlock, maps);
}
if (spec.kind == "statement") {
convertNextStatement(id, block, resultBlock, maps);
}
}
/*----------------------------------------------------------------------
Convert generic objects, property getters/setters, and methods
----------------------------------------------------------------------*/
function convertGenericComponent(id, block, spec, resultBlock, maps) {
/* AI1:
<Block id="1077" genus-name="component" >
<Location><X>319</X><Y>204</Y></Location>
<Label>Ball1</Label>
<Plug><BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="1075" ></BlockConnector></Plug>
</Block>
=> AI2:
<block type="component_component_block" id="120">
<mutation component_type="Ball" instance_name="Ball1"></mutation>
<field name="COMPONENT_SELECTOR">Ball1</field>
</block>
*/
var instanceName = getLabelText(block);
var componentType = maps.componentTypeMap[instanceName];
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
instance_name: instanceName},
[]),
createFieldElement("COMPONENT_SELECTOR", instanceName),
]);
}
function convertGenericComponentGetter(id, block, spec, resultBlock, maps) {
/* AI:
<BlockStub>
<StubParentName>Ball.Speed</StubParentName><StubParentGenus>read-write-property</StubParentGenus>
<Block id="1393" genus-name="componentTypeGetter" >
<Location><X>492</X><Y>425</Y></Location>
<Label>Ball.Speed</Label>
<Plug>
<BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="1389" ></BlockConnector>
</Plug>
<Sockets num-sockets="1" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="component" position-type="single" con-block-id="1403" ></BlockConnector>
</Sockets>
</Block>
</BlockStub>
=> AI2:
<block type="component_set_get" id="1393" inline="false">
<mutation component_type="Ball" set_or_get="get" property_name="Speed" is_generic="true"></mutation>
<field name="PROP">Speed</field>
<value name="COMPONENT">...conversion of block 1403</value>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Ball.Speed" => ["Ball", "Speed"]
var componentType = splitList[0];
var propertyName = splitList[1];
maps.componentFeaturesMap.properties[componentType + "." + propertyName] = true;
resultBlock.setAttribute("inline", "false");
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
property_name: propertyName,
set_or_get: "get",
is_generic: true
},
[]),
createFieldElement("PROP", propertyName)]);
convertChildWithLabel("component", "COMPONENT", id, block, resultBlock, maps);
}
function convertGenericComponentSetter(id, block, spec, resultBlock, maps) {
/* AI:
<BlockStub>
<StubParentName>Ball.Speed</StubParentName>
<StubParentGenus>read-write-property</StubParentGenus>
<Block id="1353" genus-name="componentTypeSetter" >
<Location><X>612</X><Y>253</Y></Location>
<Label>Ball.Speed</Label>
<BeforeBlockId>1097</BeforeBlockId>
<Sockets num-sockets="2" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="component" position-type="single" con-block-id="1357" ></BlockConnector>
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="to" position-type="single" con-block-id="1359" ></BlockConnector>
</Sockets>
</Block>
</BlockStub>
=> AI2:
<block type="component_set_get" id="1353" inline="false">
<mutation component_type="Ball" set_or_get="set" property_name="Speed" is_generic="true"></mutation>
<field name="PROP">Speed</field>
<value name="COMPONENT">...conversion of block 1357</value>
<value name="VALUE">...conversion of block 1359</value>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Ball.Speed" => ["Ball", "Speed"]
var componentType = splitList[0];
var propertyName = splitList[1];
maps.componentFeaturesMap.properties[componentType + "." + propertyName] = true;
resultBlock.setAttribute("inline", "false");
appendChildren(resultBlock,
[createElement("mutation",
{component_type: componentType,
property_name: propertyName,
set_or_get: "set",
is_generic: true
},
[]),
createFieldElement("PROP", propertyName)]);
convertChildWithLabel("component", "COMPONENT", id, block, resultBlock, maps);
convertChildWithLabel("to", "VALUE", id, block, resultBlock, maps);
convertNextStatement(id, block, resultBlock, maps); // *** v0.2 fix to problem noticed by Taifun
}
function convertGenericMethodCall(id, block, spec, resultBlock, maps) {
/* AI1:
<Block id="1097" genus-name="Type-Ball-PointInDirection" >
<Location><X>612</X><Y>159</Y></Location>
<Label>Ball.PointInDirection</Label>
<BeforeBlockId>1099</BeforeBlockId>
<AfterBlockId>1353</AfterBlockId>
<Sockets num-sockets="3" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="component" position-type="single" con-block-id="1107" ></BlockConnector>
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="x" position-type="single" con-block-id="1109" ></BlockConnector>
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="y" position-type="single" con-block-id="1111" ></BlockConnector>
</Sockets>
</Block>
=> AI2:
<block type="component_method" id="1097" inline="false">
<mutation component_type="Ball" method_name="PointInDirection" is_generic="true"></mutation>
<value name="COMPONENT">...conversion of block 1107</value>
<value name="ARG0">...conversion of block 1109</value>
<value name="ARG1">...conversion of block 1111</value>
</block>
*/
var label = getLabelText(block);
var splitList = label.split("."); // E.g. "Ball.PointInDirection" => ["Ball", "PointInDirection"]
var componentType = splitList[0]; // Same as spec["componentType"]
var methodName = splitList[1]; // Same as spec["methodName"]
maps.componentFeaturesMap.methods[componentType + "." + methodName] = true;
resultBlock.appendChild(createElement("mutation",
{component_type: componentType,
method_name: methodName,
is_generic: true
},
[]));
convertChildWithLabel("component", "COMPONENT", id, block, resultBlock, maps);
var argIds = getExpressionSocketIds(block); // List of ids/nulls for arg blocks
argIds.shift(); // Remove first id, which is for component arg, already converted above.
var numArgs = argIds.length; // Number of arg sockets in input and output block
for (var i = 0; i < numArgs; i++) {
convertChildWithId(argIds[i], "ARG" + i, id, block, resultBlock, maps);
}
if (spec.kind == "statement") {
convertNextStatement(id, block, resultBlock, maps);
}
}
/*----------------------------------------------------------------------
Convert global declarations, getters and setters
----------------------------------------------------------------------*/
function convertGlobalDeclaration(id, block, spec, resultBlock, maps) {
// This conversion is special only because of field name NAME. Otherwise it would be an operator.
/* AI:
<Block id="811" genus-name="def" >...
<Location><X>292</X><Y>58</Y></Location>
<Label>clicks</Label>
<Sockets num-sockets="1" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="as" position-type="single" con-block-id="823" ></BlockConnector>
</Sockets>
</Block>
=> AI2:
<block type="global_declaration" id="811" inline="false" x="292" y="58">
<field name="NAME">clicks</field>
<value name="VALUE">...conversion of block 823...</value>
</block>
*/
var globalName = getLabelText(block);
resultBlock.appendChild(createFieldElement("NAME", globalName));
convertChildWithLabel("as", "VALUE", id, block, resultBlock, maps);
}
function convertGlobalGetter(id, block, spec, resultBlock, maps) {
/* AI:
<BlockStub><StubParentName>clicks</StubParentName><StubParentGenus>def</StubParentGenus>
<Block id="849" genus-name="getterGlobal" >...
<Label>clicks</Label>
<Plug>
<BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="845" ></BlockConne
ctor>
</Plug>
</Block>
</BlockStub>
=> AI2:
<block type="lexical_variable_get" id="849">
<field name="VAR">global clicks</field>
</block>
*/
var globalName = "global " + getLabelText(block);
resultBlock.appendChild(createFieldElement("VAR", globalName));
}
function convertGlobalSetter(id, block, spec, resultBlock, maps) {
// This conversion is special only because of field name VAR. Otherwise it would be an operator.
/* AI:
<BlockStub><StubParentName>clicks</StubParentName><StubParentGenus>def</StubParentGenus>
<Block id="829" genus-name="setterGlobal" >...
<Label>clicks</Label>
<BeforeBlockId>825</BeforeBlockId>
<AfterBlockId>786</AfterBlockId>
<Sockets num-sockets="1" >
<BlockConnector connector-kind="socket" connector-type="poly" init-type="poly" label="to" position-type="single" con-block-id="833" ></BlockConnector>
</Sockets>
</Block>
</BlockStub>
=> AI2:
<block type="lexical_variable_set" id="101" inline="false">
<field name="VAR">global count</field>
<value name="VALUE">...conversion of block 833...</value>
<next>...conversion of block 786...</next>
<block>
*/
var globalName = "global " + getLabelText(block);
resultBlock.appendChild(createFieldElement("VAR", globalName));
convertChildWithLabel("to", "VALUE", id, block, resultBlock, maps);
convertNextStatement(id, block, resultBlock, maps);
}
/*----------------------------------------------------------------------
Convert operator blocks (both expressions and statements
----------------------------------------------------------------------*/
// Convert expression or statement blocks that only have expression input sockets (one set of which can be expandable)
function convertOperator(id, block, spec, resultBlock, maps) {
/* Example:
AI1:
<Block id="685" genus-name="string-equal" >
<Location><X>537</X><Y>1101</Y></Location>
<Label>text=</Label>
<Plug>
<BlockConnector connector-kind="plug" connector-type="poly" init-type="poly" label="" position-type="single" con-block-id="693" ></BlockConnector><
/Plug>
<Sockets num-sockets="2" >