@scratch/scratch-vm 11.0.0-UEPR-176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +120 -0
  3. package/dist/node/extension-worker.js +2 -0
  4. package/dist/node/extension-worker.js.map +1 -0
  5. package/dist/node/scratch-vm.js +3 -0
  6. package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
  7. package/dist/node/scratch-vm.js.map +1 -0
  8. package/dist/web/extension-worker.js +2 -0
  9. package/dist/web/extension-worker.js.map +1 -0
  10. package/dist/web/scratch-vm.js +3 -0
  11. package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
  12. package/dist/web/scratch-vm.js.map +1 -0
  13. package/package.json +108 -0
  14. package/src/.eslintrc.js +7 -0
  15. package/src/blocks/scratch3_control.js +206 -0
  16. package/src/blocks/scratch3_core_example.js +69 -0
  17. package/src/blocks/scratch3_data.js +257 -0
  18. package/src/blocks/scratch3_event.js +137 -0
  19. package/src/blocks/scratch3_looks.js +612 -0
  20. package/src/blocks/scratch3_motion.js +288 -0
  21. package/src/blocks/scratch3_operators.js +154 -0
  22. package/src/blocks/scratch3_procedures.js +79 -0
  23. package/src/blocks/scratch3_sensing.js +336 -0
  24. package/src/blocks/scratch3_sound.js +350 -0
  25. package/src/dispatch/central-dispatch.js +143 -0
  26. package/src/dispatch/shared-dispatch.js +235 -0
  27. package/src/dispatch/worker-dispatch.js +110 -0
  28. package/src/engine/adapter.js +176 -0
  29. package/src/engine/block-utility.js +242 -0
  30. package/src/engine/blocks-execute-cache.js +19 -0
  31. package/src/engine/blocks-runtime-cache.js +78 -0
  32. package/src/engine/blocks.js +1299 -0
  33. package/src/engine/comment.js +56 -0
  34. package/src/engine/execute.js +580 -0
  35. package/src/engine/monitor-record.js +23 -0
  36. package/src/engine/mutation-adapter.js +48 -0
  37. package/src/engine/profiler.js +390 -0
  38. package/src/engine/runtime.js +2686 -0
  39. package/src/engine/scratch-blocks-constants.js +27 -0
  40. package/src/engine/sequencer.js +361 -0
  41. package/src/engine/stage-layering.js +29 -0
  42. package/src/engine/target.js +799 -0
  43. package/src/engine/thread.js +404 -0
  44. package/src/engine/variable.js +70 -0
  45. package/src/extension-support/argument-type.js +47 -0
  46. package/src/extension-support/block-type.js +50 -0
  47. package/src/extension-support/define-messages.js +18 -0
  48. package/src/extension-support/extension-manager.js +440 -0
  49. package/src/extension-support/extension-metadata.js +64 -0
  50. package/src/extension-support/extension-worker.js +59 -0
  51. package/src/extension-support/reporter-scope.js +18 -0
  52. package/src/extension-support/target-type.js +17 -0
  53. package/src/extensions/scratch3_boost/index.js +2113 -0
  54. package/src/extensions/scratch3_ev3/index.js +1355 -0
  55. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  56. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  57. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  58. package/src/extensions/scratch3_microbit/index.js +984 -0
  59. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  116. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  117. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  118. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  119. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  120. package/src/extensions/scratch3_music/index.js +1333 -0
  121. package/src/extensions/scratch3_music/manifest.js +63 -0
  122. package/src/extensions/scratch3_pen/index.js +770 -0
  123. package/src/extensions/scratch3_speech2text/index.js +700 -0
  124. package/src/extensions/scratch3_text2speech/index.js +766 -0
  125. package/src/extensions/scratch3_translate/index.js +286 -0
  126. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  127. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  128. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  129. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  130. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  131. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  132. package/src/import/load-costume.js +416 -0
  133. package/src/import/load-sound.js +116 -0
  134. package/src/index.js +10 -0
  135. package/src/io/ble.js +256 -0
  136. package/src/io/bt.js +202 -0
  137. package/src/io/clock.js +39 -0
  138. package/src/io/cloud.js +169 -0
  139. package/src/io/keyboard.js +146 -0
  140. package/src/io/mouse.js +145 -0
  141. package/src/io/mouseWheel.js +28 -0
  142. package/src/io/userData.js +24 -0
  143. package/src/io/video.js +210 -0
  144. package/src/playground/benchmark.css +83 -0
  145. package/src/playground/benchmark.js +753 -0
  146. package/src/playground/index.html +111 -0
  147. package/src/playground/suite.css +78 -0
  148. package/src/playground/suite.html +25 -0
  149. package/src/playground/suite.js +544 -0
  150. package/src/playground/video-sensing.html +18 -0
  151. package/src/playground/video-sensing.js +133 -0
  152. package/src/serialization/deserialize-assets.js +178 -0
  153. package/src/serialization/sb2.js +1295 -0
  154. package/src/serialization/sb2_specmap.js +1818 -0
  155. package/src/serialization/sb3.js +1320 -0
  156. package/src/serialization/serialize-assets.js +60 -0
  157. package/src/sprites/rendered-target.js +1115 -0
  158. package/src/sprites/sprite.js +177 -0
  159. package/src/util/base64-util.js +48 -0
  160. package/src/util/cast.js +218 -0
  161. package/src/util/clone.js +17 -0
  162. package/src/util/color.js +204 -0
  163. package/src/util/fetch-with-timeout.js +53 -0
  164. package/src/util/get-monitor-id.js +33 -0
  165. package/src/util/jsonrpc.js +114 -0
  166. package/src/util/log.js +4 -0
  167. package/src/util/math-util.js +122 -0
  168. package/src/util/maybe-format-message.js +18 -0
  169. package/src/util/new-block-ids.js +33 -0
  170. package/src/util/rateLimiter.js +73 -0
  171. package/src/util/scratch-link-websocket.js +133 -0
  172. package/src/util/string-util.js +94 -0
  173. package/src/util/task-queue.js +203 -0
  174. package/src/util/timer.js +103 -0
  175. package/src/util/uid.js +29 -0
  176. package/src/util/variable-util.js +48 -0
  177. package/src/util/xml-escape.js +33 -0
  178. package/src/virtual-machine.js +1586 -0
@@ -0,0 +1,440 @@
1
+ const dispatch = require('../dispatch/central-dispatch');
2
+ const log = require('../util/log');
3
+ const maybeFormatMessage = require('../util/maybe-format-message');
4
+
5
+ const BlockType = require('./block-type');
6
+
7
+ // These extensions are currently built into the VM repository but should not be loaded at startup.
8
+ // TODO: move these out into a separate repository?
9
+ // TODO: change extension spec so that library info, including extension ID, can be collected through static methods
10
+
11
+ const builtinExtensions = {
12
+ // This is an example that isn't loaded with the other core blocks,
13
+ // but serves as a reference for loading core blocks as extensions.
14
+ coreExample: () => require('../blocks/scratch3_core_example'),
15
+ // These are the non-core built-in extensions.
16
+ pen: () => require('../extensions/scratch3_pen'),
17
+ wedo2: () => require('../extensions/scratch3_wedo2'),
18
+ music: () => require('../extensions/scratch3_music'),
19
+ microbit: () => require('../extensions/scratch3_microbit'),
20
+ text2speech: () => require('../extensions/scratch3_text2speech'),
21
+ translate: () => require('../extensions/scratch3_translate'),
22
+ videoSensing: () => require('../extensions/scratch3_video_sensing'),
23
+ ev3: () => require('../extensions/scratch3_ev3'),
24
+ makeymakey: () => require('../extensions/scratch3_makeymakey'),
25
+ boost: () => require('../extensions/scratch3_boost'),
26
+ gdxfor: () => require('../extensions/scratch3_gdx_for')
27
+ };
28
+
29
+ /**
30
+ * @typedef {object} ArgumentInfo - Information about an extension block argument
31
+ * @property {ArgumentType} type - the type of value this argument can take
32
+ * @property {*|undefined} default - the default value of this argument (default: blank)
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks
37
+ * @property {ExtensionBlockMetadata} info - the raw block info
38
+ * @property {object} json - the scratch-blocks JSON definition for this block
39
+ * @property {string} xml - the scratch-blocks XML definition for this block
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} CategoryInfo - Information about a block category
44
+ * @property {string} id - the unique ID of this category
45
+ * @property {string} name - the human-readable name of this category
46
+ * @property {string|undefined} blockIconURI - optional URI for the block icon image
47
+ * @property {string} color1 - the primary color for this category, in '#rrggbb' format
48
+ * @property {string} color2 - the secondary color for this category, in '#rrggbb' format
49
+ * @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
50
+ * @property {Array.<ConvertedBlockInfo>} blocks - the blocks, separators, etc. in this category
51
+ * @property {Array.<object>} menus - the menus provided by this category
52
+ */
53
+
54
+ /**
55
+ * @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
56
+ * @property {string} extensionURL - the URL of the extension to be loaded by this worker
57
+ * @property {Function} resolve - function to call on successful worker startup
58
+ * @property {Function} reject - function to call on failed worker startup
59
+ */
60
+
61
+ class ExtensionManager {
62
+ constructor (runtime) {
63
+ /**
64
+ * The ID number to provide to the next extension worker.
65
+ * @type {int}
66
+ */
67
+ this.nextExtensionWorker = 0;
68
+
69
+ /**
70
+ * FIFO queue of extensions which have been requested but not yet loaded in a worker,
71
+ * along with promise resolution functions to call once the worker is ready or failed.
72
+ *
73
+ * @type {Array.<PendingExtensionWorker>}
74
+ */
75
+ this.pendingExtensions = [];
76
+
77
+ /**
78
+ * Map of worker ID to workers which have been allocated but have not yet finished initialization.
79
+ * @type {Array.<PendingExtensionWorker>}
80
+ */
81
+ this.pendingWorkers = [];
82
+
83
+ /**
84
+ * Map of loaded extension URLs/IDs (equivalent for built-in extensions) to service name.
85
+ * @type {Map.<string,string>}
86
+ * @private
87
+ */
88
+ this._loadedExtensions = new Map();
89
+
90
+ /**
91
+ * Keep a reference to the runtime so we can construct internal extension objects.
92
+ * TODO: remove this in favor of extensions accessing the runtime as a service.
93
+ * @type {Runtime}
94
+ */
95
+ this.runtime = runtime;
96
+
97
+ dispatch.setService('extensions', this).catch(e => {
98
+ log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Check whether an extension is registered or is in the process of loading. This is intended to control loading or
104
+ * adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
105
+ * `loadExtensionURL` if you need to wait until the extension is truly ready.
106
+ * @param {string} extensionID - the ID of the extension.
107
+ * @returns {boolean} - true if loaded, false otherwise.
108
+ */
109
+ isExtensionLoaded (extensionID) {
110
+ return this._loadedExtensions.has(extensionID);
111
+ }
112
+
113
+ /**
114
+ * Synchronously load an internal extension (core or non-core) by ID. This call will
115
+ * fail if the provided id is not does not match an internal extension.
116
+ * @param {string} extensionId - the ID of an internal extension
117
+ */
118
+ loadExtensionIdSync (extensionId) {
119
+ if (!Object.prototype.hasOwnProperty.call(builtinExtensions, extensionId)) {
120
+ log.warn(`Could not find extension ${extensionId} in the built in extensions.`);
121
+ return;
122
+ }
123
+
124
+ /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
125
+ if (this.isExtensionLoaded(extensionId)) {
126
+ const message = `Rejecting attempt to load a second extension with ID ${extensionId}`;
127
+ log.warn(message);
128
+ return;
129
+ }
130
+
131
+ const extension = builtinExtensions[extensionId]();
132
+ const extensionInstance = new extension(this.runtime);
133
+ const serviceName = this._registerInternalExtension(extensionInstance);
134
+ this._loadedExtensions.set(extensionId, serviceName);
135
+ }
136
+
137
+ /**
138
+ * Load an extension by URL or internal extension ID
139
+ * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
140
+ * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
141
+ */
142
+ loadExtensionURL (extensionURL) {
143
+ if (Object.prototype.hasOwnProperty.call(builtinExtensions, extensionURL)) {
144
+ /** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
145
+ if (this.isExtensionLoaded(extensionURL)) {
146
+ const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
147
+ log.warn(message);
148
+ return Promise.resolve();
149
+ }
150
+
151
+ const extension = builtinExtensions[extensionURL]();
152
+ const extensionInstance = new extension(this.runtime);
153
+ const serviceName = this._registerInternalExtension(extensionInstance);
154
+ this._loadedExtensions.set(extensionURL, serviceName);
155
+ return Promise.resolve();
156
+ }
157
+
158
+ return new Promise((resolve, reject) => {
159
+ // If we `require` this at the global level it breaks non-webpack targets, including tests
160
+ const worker = new Worker('./extension-worker.js');
161
+
162
+ this.pendingExtensions.push({extensionURL, resolve, reject});
163
+ dispatch.addWorker(worker);
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Regenerate blockinfo for any loaded extensions
169
+ * @returns {Promise} resolved once all the extensions have been reinitialized
170
+ */
171
+ refreshBlocks () {
172
+ const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName =>
173
+ dispatch.call(serviceName, 'getInfo')
174
+ .then(info => {
175
+ info = this._prepareExtensionInfo(serviceName, info);
176
+ dispatch.call('runtime', '_refreshExtensionPrimitives', info);
177
+ })
178
+ .catch(e => {
179
+ log.error(`Failed to refresh built-in extension primitives: ${JSON.stringify(e)}`);
180
+ })
181
+ );
182
+ return Promise.all(allPromises);
183
+ }
184
+
185
+ allocateWorker () {
186
+ const id = this.nextExtensionWorker++;
187
+ const workerInfo = this.pendingExtensions.shift();
188
+ this.pendingWorkers[id] = workerInfo;
189
+ return [id, workerInfo.extensionURL];
190
+ }
191
+
192
+ /**
193
+ * Synchronously collect extension metadata from the specified service and begin the extension registration process.
194
+ * @param {string} serviceName - the name of the service hosting the extension.
195
+ */
196
+ registerExtensionServiceSync (serviceName) {
197
+ const info = dispatch.callSync(serviceName, 'getInfo');
198
+ this._registerExtensionInfo(serviceName, info);
199
+ }
200
+
201
+ /**
202
+ * Collect extension metadata from the specified service and begin the extension registration process.
203
+ * @param {string} serviceName - the name of the service hosting the extension.
204
+ */
205
+ registerExtensionService (serviceName) {
206
+ dispatch.call(serviceName, 'getInfo').then(info => {
207
+ this._registerExtensionInfo(serviceName, info);
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Called by an extension worker to indicate that the worker has finished initialization.
213
+ * @param {int} id - the worker ID.
214
+ * @param {*?} e - the error encountered during initialization, if any.
215
+ */
216
+ onWorkerInit (id, e) {
217
+ const workerInfo = this.pendingWorkers[id];
218
+ delete this.pendingWorkers[id];
219
+ if (e) {
220
+ workerInfo.reject(e);
221
+ } else {
222
+ workerInfo.resolve(id);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Register an internal (non-Worker) extension object
228
+ * @param {object} extensionObject - the extension object to register
229
+ * @returns {string} The name of the registered extension service
230
+ */
231
+ _registerInternalExtension (extensionObject) {
232
+ const extensionInfo = extensionObject.getInfo();
233
+ const fakeWorkerId = this.nextExtensionWorker++;
234
+ const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
235
+ dispatch.setServiceSync(serviceName, extensionObject);
236
+ dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName);
237
+ return serviceName;
238
+ }
239
+
240
+ /**
241
+ * Sanitize extension info then register its primitives with the VM.
242
+ * @param {string} serviceName - the name of the service hosting the extension
243
+ * @param {ExtensionInfo} extensionInfo - the extension's metadata
244
+ * @private
245
+ */
246
+ _registerExtensionInfo (serviceName, extensionInfo) {
247
+ extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
248
+ dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
249
+ log.error(`Failed to register primitives for extension on service ${serviceName}:`, e);
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
255
+ * @param {string} text - the text to be sanitized
256
+ * @returns {string} - the sanitized text
257
+ * @private
258
+ */
259
+ _sanitizeID (text) {
260
+ return text.toString().replace(/[<"&]/, '_');
261
+ }
262
+
263
+ /**
264
+ * Apply minor cleanup and defaults for optional extension fields.
265
+ * TODO: make the ID unique in cases where two copies of the same extension are loaded.
266
+ * @param {string} serviceName - the name of the service hosting this extension block
267
+ * @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
268
+ * @returns {ExtensionInfo} - a new extension info object with cleaned-up values
269
+ * @private
270
+ */
271
+ _prepareExtensionInfo (serviceName, extensionInfo) {
272
+ extensionInfo = Object.assign({}, extensionInfo);
273
+ if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) {
274
+ throw new Error('Invalid extension id');
275
+ }
276
+ extensionInfo.name = extensionInfo.name || extensionInfo.id;
277
+ extensionInfo.blocks = extensionInfo.blocks || [];
278
+ extensionInfo.targetTypes = extensionInfo.targetTypes || [];
279
+ extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => {
280
+ try {
281
+ let result;
282
+ switch (blockInfo) {
283
+ case '---': // separator
284
+ result = '---';
285
+ break;
286
+ default: // an ExtensionBlockMetadata object
287
+ result = this._prepareBlockInfo(serviceName, blockInfo);
288
+ break;
289
+ }
290
+ results.push(result);
291
+ } catch (e) {
292
+ // TODO: more meaningful error reporting
293
+ log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
294
+ }
295
+ return results;
296
+ }, []);
297
+ extensionInfo.menus = extensionInfo.menus || {};
298
+ extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
299
+ return extensionInfo;
300
+ }
301
+
302
+ /**
303
+ * Prepare extension menus. e.g. setup binding for dynamic menu functions.
304
+ * @param {string} serviceName - the name of the service hosting this extension block
305
+ * @param {Array.<MenuInfo>} menus - the menu defined by the extension.
306
+ * @returns {Array.<MenuInfo>} - a menuInfo object with all preprocessing done.
307
+ * @private
308
+ */
309
+ _prepareMenuInfo (serviceName, menus) {
310
+ const menuNames = Object.getOwnPropertyNames(menus);
311
+ for (let i = 0; i < menuNames.length; i++) {
312
+ const menuName = menuNames[i];
313
+ let menuInfo = menus[menuName];
314
+
315
+ // If the menu description is in short form (items only) then normalize it to general form: an object with
316
+ // its items listed in an `items` property.
317
+ if (!menuInfo.items) {
318
+ menuInfo = {
319
+ items: menuInfo
320
+ };
321
+ menus[menuName] = menuInfo;
322
+ }
323
+ // If `items` is a string, it should be the name of a function in the extension object. Calling the
324
+ // function should return an array of items to populate the menu when it is opened.
325
+ if (typeof menuInfo.items === 'string') {
326
+ const menuItemFunctionName = menuInfo.items;
327
+ const serviceObject = dispatch.services[serviceName];
328
+ // Bind the function here so we can pass a simple item generation function to Scratch Blocks later.
329
+ menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName);
330
+ }
331
+ }
332
+ return menus;
333
+ }
334
+
335
+ /**
336
+ * Fetch the items for a particular extension menu, providing the target ID for context.
337
+ * @param {object} extensionObject - the extension object providing the menu.
338
+ * @param {string} menuItemFunctionName - the name of the menu function to call.
339
+ * @returns {Array} menu items ready for scratch-blocks.
340
+ * @private
341
+ */
342
+ _getExtensionMenuItems (extensionObject, menuItemFunctionName) {
343
+ // Fetch the items appropriate for the target currently being edited. This assumes that menus only
344
+ // collect items when opened by the user while editing a particular target.
345
+ const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
346
+ const editingTargetID = editingTarget ? editingTarget.id : null;
347
+ const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
348
+
349
+ // TODO: Fix this to use dispatch.call when extensions are running in workers.
350
+ const menuFunc = extensionObject[menuItemFunctionName];
351
+ const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
352
+ item => {
353
+ item = maybeFormatMessage(item, extensionMessageContext);
354
+ switch (typeof item) {
355
+ case 'object':
356
+ return [
357
+ maybeFormatMessage(item.text, extensionMessageContext),
358
+ item.value
359
+ ];
360
+ case 'string':
361
+ return [item, item];
362
+ default:
363
+ return item;
364
+ }
365
+ });
366
+
367
+ if (!menuItems || menuItems.length < 1) {
368
+ throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`);
369
+ }
370
+ return menuItems;
371
+ }
372
+
373
+ /**
374
+ * Apply defaults for optional block fields.
375
+ * @param {string} serviceName - the name of the service hosting this extension block
376
+ * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension
377
+ * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields.
378
+ * @private
379
+ */
380
+ _prepareBlockInfo (serviceName, blockInfo) {
381
+ blockInfo = Object.assign({}, {
382
+ blockType: BlockType.COMMAND,
383
+ terminal: false,
384
+ blockAllThreads: false,
385
+ arguments: {}
386
+ }, blockInfo);
387
+ blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode);
388
+ blockInfo.text = blockInfo.text || blockInfo.opcode;
389
+
390
+ switch (blockInfo.blockType) {
391
+ case BlockType.EVENT:
392
+ if (blockInfo.func) {
393
+ log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
394
+ }
395
+ break;
396
+ case BlockType.BUTTON:
397
+ if (blockInfo.opcode) {
398
+ log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
399
+ }
400
+ break;
401
+ default: {
402
+ if (!blockInfo.opcode) {
403
+ throw new Error('Missing opcode for block');
404
+ }
405
+
406
+ const funcName = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
407
+
408
+ const getBlockInfo = blockInfo.isDynamic ?
409
+ args => args && args.mutation && args.mutation.blockInfo :
410
+ () => blockInfo;
411
+ const callBlockFunc = (() => {
412
+ if (dispatch._isRemoteService(serviceName)) {
413
+ return (args, util, realBlockInfo) =>
414
+ dispatch.call(serviceName, funcName, args, util, realBlockInfo);
415
+ }
416
+
417
+ // avoid promise latency if we can call direct
418
+ const serviceObject = dispatch.services[serviceName];
419
+ if (!serviceObject[funcName]) {
420
+ // The function might show up later as a dynamic property of the service object
421
+ log.warn(`Could not find extension block function called ${funcName}`);
422
+ }
423
+ return (args, util, realBlockInfo) =>
424
+ serviceObject[funcName](args, util, realBlockInfo);
425
+ })();
426
+
427
+ blockInfo.func = (args, util) => {
428
+ const realBlockInfo = getBlockInfo(args);
429
+ // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed?
430
+ return callBlockFunc(args, util, realBlockInfo);
431
+ };
432
+ break;
433
+ }
434
+ }
435
+
436
+ return blockInfo;
437
+ }
438
+ }
439
+
440
+ module.exports = ExtensionManager;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @typedef {object} ExtensionMetadata
3
+ * All the metadata needed to register an extension.
4
+ * @property {string} id - a unique alphanumeric identifier for this extension. No special characters allowed.
5
+ * @property {string} [name] - the human-readable name of this extension.
6
+ * @property {string} [blockIconURI] - URI for an image to be placed on each block in this extension. Data URI ok.
7
+ * @property {string} [menuIconURI] - URI for an image to be placed on this extension's category menu item. Data URI ok.
8
+ * @property {string} [docsURI] - link to documentation content for this extension.
9
+ * @property {Array.<ExtensionBlockMetadata|string>} blocks - the blocks provided by this extension, plus separators.
10
+ * @property {Object.<ExtensionMenuMetadata>} [menus] - map of menu name to metadata for each of this extension's menus.
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} ExtensionBlockMetadata
15
+ * All the metadata needed to register an extension block.
16
+ * @property {string} opcode - a unique alphanumeric identifier for this block. No special characters allowed.
17
+ * @property {string} [func] - the name of the function implementing this block. Can be shared by other blocks/opcodes.
18
+ * @property {BlockType} blockType - the type of block (command, reporter, etc.) being described.
19
+ * @property {string} text - the text on the block, with [PLACEHOLDERS] for arguments.
20
+ * @property {Boolean} [hideFromPalette] - true if this block should not appear in the block palette.
21
+ * @property {Boolean} [isTerminal] - true if the block ends a stack - no blocks can be connected after it.
22
+ * @property {Boolean} [disableMonitor] - true if this block is a reporter but should not allow a monitor.
23
+ * @property {ReporterScope} [reporterScope] - if this block is a reporter, this is the scope/context for its value.
24
+ * @property {Boolean} [isEdgeActivated] - sets whether a hat block is edge-activated.
25
+ * @property {Boolean} [shouldRestartExistingThreads] - sets whether a hat/event block should restart existing threads.
26
+ * @property {int} [branchCount] - for flow control blocks, the number of branches/substacks for this block.
27
+ * @property {Object.<ExtensionArgumentMetadata>} [arguments] - map of argument placeholder to metadata about each arg.
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} ExtensionArgumentMetadata
32
+ * All the metadata needed to register an argument for an extension block.
33
+ * @property {ArgumentType} type - the type of the argument (number, string, etc.)
34
+ * @property {*} [defaultValue] - the default value of this argument.
35
+ * @property {string} [menu] - the name of the menu to use for this argument, if any.
36
+ */
37
+
38
+ /**
39
+ * @typedef {ExtensionDynamicMenu|ExtensionMenuItems} ExtensionMenuMetadata
40
+ * All the metadata needed to register an extension drop-down menu.
41
+ */
42
+
43
+ /**
44
+ * @typedef {string} ExtensionDynamicMenu
45
+ * The string name of a function which returns menu items.
46
+ * @see {ExtensionMenuItems} - the type of data expected to be returned by the specified function.
47
+ */
48
+
49
+ /**
50
+ * @typedef {Array.<ExtensionMenuItemSimple|ExtensionMenuItemComplex>} ExtensionMenuItems
51
+ * Items in an extension menu.
52
+ */
53
+
54
+ /**
55
+ * @typedef {string} ExtensionMenuItemSimple
56
+ * A menu item for which the label and value are identical strings.
57
+ */
58
+
59
+ /**
60
+ * @typedef {object} ExtensionMenuItemComplex
61
+ * A menu item for which the label and value can differ.
62
+ * @property {*} value - the value of the block argument when this menu item is selected.
63
+ * @property {string} text - the human-readable label of this menu item in the menu.
64
+ */
@@ -0,0 +1,59 @@
1
+ /* eslint-env worker */
2
+
3
+ const ArgumentType = require('../extension-support/argument-type');
4
+ const BlockType = require('../extension-support/block-type');
5
+ const dispatch = require('../dispatch/worker-dispatch');
6
+ const TargetType = require('../extension-support/target-type');
7
+
8
+ class ExtensionWorker {
9
+ constructor () {
10
+ this.nextExtensionId = 0;
11
+
12
+ this.initialRegistrations = [];
13
+
14
+ dispatch.waitForConnection.then(() => {
15
+ dispatch.call('extensions', 'allocateWorker').then(x => {
16
+ const [id, extension] = x;
17
+ this.workerId = id;
18
+
19
+ try {
20
+ importScripts(extension);
21
+
22
+ const initialRegistrations = this.initialRegistrations;
23
+ this.initialRegistrations = null;
24
+
25
+ Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id));
26
+ } catch (e) {
27
+ dispatch.call('extensions', 'onWorkerInit', id, e);
28
+ }
29
+ });
30
+ });
31
+
32
+ this.extensions = [];
33
+ }
34
+
35
+ register (extensionObject) {
36
+ const extensionId = this.nextExtensionId++;
37
+ this.extensions.push(extensionObject);
38
+ const serviceName = `extension.${this.workerId}.${extensionId}`;
39
+ const promise = dispatch.setService(serviceName, extensionObject)
40
+ .then(() => dispatch.call('extensions', 'registerExtensionService', serviceName));
41
+ if (this.initialRegistrations) {
42
+ this.initialRegistrations.push(promise);
43
+ }
44
+ return promise;
45
+ }
46
+ }
47
+
48
+ global.Scratch = global.Scratch || {};
49
+ global.Scratch.ArgumentType = ArgumentType;
50
+ global.Scratch.BlockType = BlockType;
51
+ global.Scratch.TargetType = TargetType;
52
+
53
+ /**
54
+ * Expose only specific parts of the worker to extensions.
55
+ */
56
+ const extensionWorker = new ExtensionWorker();
57
+ global.Scratch.extensions = {
58
+ register: extensionWorker.register.bind(extensionWorker)
59
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Indicate the scope for a reporter's value.
3
+ * @enum {string}
4
+ */
5
+ const ReporterScope = {
6
+ /**
7
+ * This reporter's value is global and does not depend on context.
8
+ */
9
+ GLOBAL: 'global',
10
+
11
+ /**
12
+ * This reporter's value is specific to a particular target/sprite.
13
+ * Another target may have a different value or may not even have a value.
14
+ */
15
+ TARGET: 'target'
16
+ };
17
+
18
+ module.exports = ReporterScope;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Default types of Target supported by the VM
3
+ * @enum {string}
4
+ */
5
+ const TargetType = {
6
+ /**
7
+ * Rendered target which can move, change costumes, etc.
8
+ */
9
+ SPRITE: 'sprite',
10
+
11
+ /**
12
+ * Rendered target which cannot move but can change backdrops
13
+ */
14
+ STAGE: 'stage'
15
+ };
16
+
17
+ module.exports = TargetType;