From bcda700818a29e16d46cca6a31a61f902cbc08ac Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 5 Apr 2019 13:50:24 -0700 Subject: [PATCH 1/5] add existing extension docs in `docs` directory --- docs/extension-translation-straw-cat.md | 189 ++++++++++++++ docs/extensions.md | 318 ++++++++++++++++++++++++ 2 files changed, 507 insertions(+) create mode 100644 docs/extension-translation-straw-cat.md create mode 100644 docs/extensions.md diff --git a/docs/extension-translation-straw-cat.md b/docs/extension-translation-straw-cat.md new file mode 100644 index 000000000..29981bb44 --- /dev/null +++ b/docs/extension-translation-straw-cat.md @@ -0,0 +1,189 @@ +Not really a proposal, more a starting point for discussion. Currently only considering extensions. + +Essentially strings turn into an array of `['default string', 'translation-key']`, and `translation_map` has a dictionary for each language code supported. When someone switches language, the dictionary for the corresponding language code should be used if available. + +# Example + +```js +class SomeBlocks { + constructor (runtimeProxy) { + /** + * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. + * @type {Runtime} + */ + this.runtime = runtimeProxy; + } + + /** + * @return {object} This extension's metadata. + */ + getInfo () { + return { + // Required: the machine-readable name of this extension. + // Will be used as the extension's namespace. + id: 'someBlocks', + + // Optional: the human-readable name of this extension as string or array. + // If not present, use the ID. + // If an array, first element is default text, second is the id + // of the string in the translation map + name: ['Some Blocks', 'someBlocks'], + + // Optional: URI for an icon for this extension. Data URI OK. + // If not present, use a generic icon. + // TODO: what file types are OK? All web images? Just PNG? + iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + + // Optional: Link to documentation content for this extension. + // If not present, offer no link. + docsURI: 'https://....', + + // Required: the list of blocks implemented by this extension, + // in the order intended for display. + blocks: [ + { + // Required: the machine-readable name of this operation. + // This will appear in project JSON. + opcode: 'myReporter', // becomes 'someBlocks.myReporter' + + // Required: the kind of block we're defining, from a predefined list: + // 'command' - a normal command block, like "move {} steps" + // 'reporter' - returns a value, like "direction" + // 'Boolean' - same as 'reporter' but returns a Boolean value + // 'hat' - starts a stack if its value is truthy + // 'conditional' - control flow, like "if {}" or "repeat {}" + // A 'conditional' block may return the one-based index of a branch + // to run, or it may return zero/falsy to run no branch. Each time a + // child branch finishes, the block is called again. This is only a + // slight change to the current model for control flow blocks, and is + // also compatible with returning true/false for an "if" or "repeat" + // block. + // TODO: Consider Blockly-like nextStatement, previousStatement, and + // output attributes as an alternative. Those are more flexible, but + // allow bad combinations. + blockType: 'reporter', + + // Required for conditional blocks, ignored for others: the number of + // child branches this block controls. An "if" or "repeat" block would + // specify a branch count of 1; an "if-else" block would specify a + // branch count of 2. + // TODO: should we support dynamic branch count for "switch"-likes? + branchCount: 0, + + // Optional, default false: whether or not this block ends a stack. + // The "forever" and "stop all" blocks would specify true here. + terminal: true, + + // Optional, default false: whether or not to block all threads while + // this block is busy. This is for things like the "touching color" + // block in compatibility mode, and is only needed if the VM runs in a + // worker. We might even consider omitting it from extension docs... + blockAllThreads: false, + + // Required: the human-readable text on this block, as a map from IETF + // language tags to strings in ICU message format. Note that + // '{MY_ARG,string}' tells the extension system the name and type of + // this argument. If the type is found in the `menus` object below, + // then this is a menu. If the extension only supplies one language, + // this may be a single string instead of a map. + // I make no claims about the correctness of my German grammar... + text: ['give {MY_ARG} to my reporter', 'myReporter_text'] + + // Optional: each argument may specify a default value. + // If absent, the default is a blank/empty field. + defaults: { + MY_ARG: ['some string', 'MY_ARG_default'] + } + + // Required: the function implementing this block. + func: this.myReporter, + + // Optional: list of target types for which this block should appear. + // If absent, assume it applies to all builtin targets -- that is: + // ['sprite', 'stage'] + filter: ['someBlocks.wedo2', 'sprite', 'stage'] + }, + { + // Another block... + } + ], + + // Optional: define extension-specific menus here. + menus: { + // Required: an identifier for this menu, unique within this extension. + menuA: [ + // Static menu: list items which should appear in the menu. + { + // Required: the value of the menu item when it is chosen. + value: 'itemId1', + + // Optional: the human-readable label for this item, in ICU format. + // Use `value` as the text if this is absent. + text: ['Item One', 'menuA_item1'] + }, + + // The simplest form of a list item is a string which will be used as + // both value and text. + 'itemId2' + ], + + // Dynamic menu: returns an array as above. + // Called each time the menu is opened. + menuB: this.getItemsForMenuB + }, + + // Optional: translations + translation_map: { + 'de' : { + 'someBlocks' : 'Einige Blöcke', + 'myReporter_text' : 'Gib {MY_ARG} zu meinem Reporter', + 'myReporter_val' : '{my_arg} ist mein wert' + 'MY_ARG_default' : 'etwas inhalt', + 'menuA_item1' : 'Artikel eins', + 'menuB_default' : 'Beispiel' //default to use for getItemsForMenuB + }, + 'it' : { + ... + } + } + + // Optional: list new target type(s) provided by this extension. + targetTypes: [ + 'wedo2', // automatically transformed to 'someBlocks.wedo2' + 'speech' // automatically transformed to 'someBlocks.speech' + ] + }; + } + + /** + * Implement myReporter. + * @param {object} args - the block's arguments. + * @property {string} MY_ARG - the string value of the argument. + * @returns {string} a string which includes the block argument value. + */ + myReporter (args) { + //return `${args.MY_ARG} is my argument`; + return _trans(['{my_arg} is my argument', myReporter_val], {'my_arg' : args.MY_ARG}) + } +} +``` + +# Questions and Issues +* What about collisions? If two extensions have 'MY_ARG_default', they shouldn't collide. Should the keys include the extension id: 'someBlocks.MY_ARG_default', or should we assume that the strings will get loaded into a map that is name-spaced by extension: +``` +'someBlocks' : { + ... + 'MY_ARG_default' : 'something', + ... +}, +'otherExt' : { + ... + 'MY_ARG_default' : 'something else', + ... +} +``` +* Should we insist that the default is English? +* If people want to provide additional translations, how are the original key:strings extracted, and how are new translations included? Do we expect ongoing translation of extensions? +* Should we provide translation for the extension functions as in `_trans` in the myReporter function? How likely are extensions to actually return strings? +* Using ICU formatted strings, by default arguments are only typed if they are number, date or time. We may want to know what type of argument is allowed (e.g. Boolean) to determine the look of the block, but that probably doesn't belong in the translation string. +* This assumes that we'll do some sort of mapping from ICU format `'give {MY_ARG} to my reporter'` to Blockly `'give %1 to my reporter'`. An alternative would be to use Blockly type string format in extensions. diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 000000000..72a085661 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,318 @@ +# Scratch 3.0 Extensions + +This document describes technical topics related to Scratch 3.0 extension development, including the Scratch 3.0 +extension specification. + +For documentation regarding other aspects of Scratch 3.0 extensions see [this Extensions page on the +wiki](https://github.com/LLK/docs/wiki/Extensions). + +## Types of Extensions + +There are four types of extensions that can define everything from the Scratch's core library (such as the "Looks" and +"Operators" categories) to unofficial extensions that can be loaded from a remote URL. Unofficial extensions are not +yet supported. + +For more details, see [this Extensions page on the wiki](https://github.com/LLK/docs/wiki/Extensions). + +| | Core | Team | Official | Unofficial | +| ------------------------------ | ---- | ---- | -------- | ---------- | +| Developed by Scratch Team | √ | √ | O | X | +| Maintained by Scratch Team | √ | √ | O | X | +| Shown in Library | X | √ | √ | X | +| Sandboxed | X | X | √ | √ | +| Can save projects to community | √ | √ | √ | X | + +## Translation + +Scratch extensions use the [ICU message format](http://userguide.icu-project.org/formatparse/messages) to handle +translation across languages. For **core, team, and official** extensions, the function `formatMessage` is used to +wrap any ICU messages that need to be exported to the [Scratch Transifex group](https://www.transifex.com/llk/public/) +for translation. + +**All extensions** may additionally define a `translation_map` object within the `getInfo` function which can provide +translations within an extension itself. The "Annotated Example" below provides a more complete illustration of how +translation within an extension can be managed. + +## Backwards Compatibility + +Scratch is designed to be fully backwards compatible. Because of this, block definitions and opcodes should *never* +change in a way that could cause previously saved projects to fail to load or to act in unexpected / inconsistent +ways. + +## Defining an Extension + +Scratch extensions are defined as a single Javascript class which accepts either a reference to the Scratch +[VM](https://github.com/llk/scratch-vm) runtime or a "runtime proxy" which handles communication with the Scratch VM +across a well defined worker boundary (i.e. the sandbox). + +```js +var SomeBlocks = function (runtimeProxy) { + /** + * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. + * @type {Runtime} + */ + this.runtime = runtimeProxy; +} +``` + +All extensions must define a function called `getInfo` which returns an object that contains the information needed to +render both the blocks and the extension itself. + +```js +SomeBlocks.prototype.getInfo = function () { + return { + id: 'someBlocks', + name: 'Some Blocks', + blocks: [ + { + opcode: 'myReporter', + blockType: BlockType.REPORTER, + text: 'letter [LETTER_NUM] of [TEXT]', + arguments: { + LETTER_NUM: { + type: ArgumentType.STRING, + defaultValue: '1' + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'text' + } + } + } + ] + }; +}; +``` + +Finally the extension must define a function for any "opcode" defined in the blocks. For example: + +```js +SomeBlocks.prototype.myReporter = function (args) { + return args.TEXT.charAt(args.LETTER_NUM); +}; +``` + +## Annotated Example + +```js +var SomeBlocks = function (runtimeProxy) { + /** + * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. + * @type {Runtime} + */ + this.runtime = runtimeProxy; +}; + +/** + * @return {object} This extension's metadata. + */ +SomeBlocks.prototype.getInfo = function () { + return { + // Required: the machine-readable name of this extension. + // Will be used as the extension's namespace. + id: 'someBlocks', + + // Optional: the human-readable name of this extension as string. + // This and any other string to be displayed in the Scratch UI may either be + // a string or a call to `formatMessage`; a plain string will not be + // translated whereas a call to `formatMessage` will connect the string + // to the translation map (see below). The `formatMessage` call is + // similar to `formatMessage` from `react-intl` in form, but will actually + // call some extension support code to do its magic. For example, we will + // internally namespace the messages such that two extensions could have + // messages with the same ID without colliding. + // See also: https://github.com/yahoo/react-intl/wiki/API#formatmessage + name: formatMessage({ + id: 'extensionName', + defaultMessage: 'Some Blocks', + description: 'Extension name' + }), + + // Optional: URI for a block icon, to display at the edge of each block for this + // extension. Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + blockIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + + // Optional: URI for an icon to be displayed in the blocks category menu. + // If not present, the menu will display the block icon, if one is present. + // Otherwise, the category menu shows its default filled circle. + // Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + menuIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + + // Optional: Link to documentation content for this extension. + // If not present, offer no link. + docsURI: 'https://....', + + // Required: the list of blocks implemented by this extension, + // in the order intended for display. + blocks: [ + { + // Required: the machine-readable name of this operation. + // This will appear in project JSON. + opcode: 'myReporter', // becomes 'someBlocks.myReporter' + + // Required: the kind of block we're defining, from a predefined list: + // 'command' - a normal command block, like "move {} steps" + // 'reporter' - returns a value, like "direction" + // 'Boolean' - same as 'reporter' but returns a Boolean value + // 'hat' - starts a stack if its value is truthy + // 'conditional' - control flow, like "if {}" or "if {} else {}" + // A 'conditional' block may return the one-based index of a branch to + // run, or it may return zero/falsy to run no branch. + // 'loop' - control flow, like "repeat {} {}" or "forever {}" + // A 'loop' block is like a conditional block with two differences: + // - the block is assumed to have exactly one child branch, and + // - each time a child branch finishes, the loop block is called again. + blockType: 'reporter', + + // Required for conditional blocks, ignored for others: the number of + // child branches this block controls. An "if" or "repeat" block would + // specify a branch count of 1; an "if-else" block would specify a + // branch count of 2. + // TODO: should we support dynamic branch count for "switch"-likes? + branchCount: 0, + + // Optional, default false: whether or not this block ends a stack. + // The "forever" and "stop all" blocks would specify true here. + terminal: true, + + // Optional, default false: whether or not to block all threads while + // this block is busy. This is for things like the "touching color" + // block in compatibility mode, and is only needed if the VM runs in a + // worker. We might even consider omitting it from extension docs... + blockAllThreads: false, + + // Required: the human-readable text on this block, including argument + // placeholders. Argument placeholders should be in [MACRO_CASE] and + // must be [ENCLOSED_WITHIN_SQUARE_BRACKETS]. + text: formatMessage({ + id: 'myReporter', + defaultMessage: 'letter [LETTER_NUM] of [TEXT]', + description: 'Label on the "myReporter" block' + }), + + // Required: describe each argument. + // Argument order may change during translation, so arguments are + // identified by their placeholder name. In those situations where + // arguments must be ordered or assigned an ordinal, such as interaction + // with Scratch Blocks, arguments are ordered as they are in the default + // translation (probably English). + arguments: { + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + LETTER_NUM: { + // Required: type of the argument / shape of the block input + type: 'number', + + // Optional: the default value of the argument + default: 1 + }, + + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + TEXT: { + // Required: type of the argument / shape of the block input + type: 'string', + + // Optional: the default value of the argument + default: formatMessage({ + id: 'myReporter.TEXT_default', + defaultMessage: 'text', + description: 'Default for "TEXT" argument of "myReporter"' + }) + } + }, + + // Required: the function implementing this block. + func: 'myReporter', + + // Optional: list of target types for which this block should appear. + // If absent, assume it applies to all builtin targets -- that is: + // ['sprite', 'stage'] + filter: ['someBlocks.wedo2', 'sprite', 'stage'] + }, + { + // Another block... + } + ], + + // Optional: define extension-specific menus here. + menus: { + // Required: an identifier for this menu, unique within this extension. + menuA: [ + // Static menu: list items which should appear in the menu. + { + // Required: the value of the menu item when it is chosen. + value: 'itemId1', + + // Optional: the human-readable label for this item. + // Use `value` as the text if this is absent. + text: formatMessage({ + id: 'menuA_item1', + defaultMessage: 'Item One', + description: 'Label for item 1 of menu A' + }) + }, + + // The simplest form of a list item is a string which will be used as + // both value and text. + 'itemId2' + ], + + // Dynamic menu: returns an array as above. + // Called each time the menu is opened. + menuB: 'getItemsForMenuB' + }, + + // Optional: translations + translation_map: { + de: { + 'extensionName': 'Einige Blöcke', + 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', + 'myReporter.TEXT_default': 'Text', + 'menuA_item1': 'Artikel eins', + + // Dynamic menus can be translated too + 'menuB_example': 'Beispiel', + + // This message contains ICU placeholders (see `myReporter()` below) + 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' + }, + it: { + // ... + } + }, + + // Optional: list new target type(s) provided by this extension. + targetTypes: [ + 'wedo2', // automatically transformed to 'someBlocks.wedo2' + 'speech' // automatically transformed to 'someBlocks.speech' + ] + }; +}; + +/** + * Implement myReporter. + * @param {object} args - the block's arguments. + * @property {string} MY_ARG - the string value of the argument. + * @returns {string} a string which includes the block argument value. + */ +SomeBlocks.prototype.myReporter = function (args) { + // This message contains ICU placeholders, not Scratch placeholders + const message = formatMessage({ + id: 'myReporter.result', + defaultMessage: 'Letter {LETTER_NUM} of {TEXT} is {LETTER}.', + description: 'The text template for the "myReporter" block result' + }); + + // Note: this implementation is not Unicode-clean; it's just here as an example. + const result = args.TEXT.charAt(args.LETTER_NUM); + + return message.format({ + LETTER_NUM: args.LETTER_NUM, + TEXT: args.TEXT, + LETTER: result + }); +}; +``` From 2a5abe028fccacce61eccbf8e759c655c05e4070 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 5 Apr 2019 14:13:20 -0700 Subject: [PATCH 2/5] archive (delete) "Extension translation straw cat" --- docs/extension-translation-straw-cat.md | 189 ------------------------ 1 file changed, 189 deletions(-) delete mode 100644 docs/extension-translation-straw-cat.md diff --git a/docs/extension-translation-straw-cat.md b/docs/extension-translation-straw-cat.md deleted file mode 100644 index 29981bb44..000000000 --- a/docs/extension-translation-straw-cat.md +++ /dev/null @@ -1,189 +0,0 @@ -Not really a proposal, more a starting point for discussion. Currently only considering extensions. - -Essentially strings turn into an array of `['default string', 'translation-key']`, and `translation_map` has a dictionary for each language code supported. When someone switches language, the dictionary for the corresponding language code should be used if available. - -# Example - -```js -class SomeBlocks { - constructor (runtimeProxy) { - /** - * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. - * @type {Runtime} - */ - this.runtime = runtimeProxy; - } - - /** - * @return {object} This extension's metadata. - */ - getInfo () { - return { - // Required: the machine-readable name of this extension. - // Will be used as the extension's namespace. - id: 'someBlocks', - - // Optional: the human-readable name of this extension as string or array. - // If not present, use the ID. - // If an array, first element is default text, second is the id - // of the string in the translation map - name: ['Some Blocks', 'someBlocks'], - - // Optional: URI for an icon for this extension. Data URI OK. - // If not present, use a generic icon. - // TODO: what file types are OK? All web images? Just PNG? - iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', - - // Optional: Link to documentation content for this extension. - // If not present, offer no link. - docsURI: 'https://....', - - // Required: the list of blocks implemented by this extension, - // in the order intended for display. - blocks: [ - { - // Required: the machine-readable name of this operation. - // This will appear in project JSON. - opcode: 'myReporter', // becomes 'someBlocks.myReporter' - - // Required: the kind of block we're defining, from a predefined list: - // 'command' - a normal command block, like "move {} steps" - // 'reporter' - returns a value, like "direction" - // 'Boolean' - same as 'reporter' but returns a Boolean value - // 'hat' - starts a stack if its value is truthy - // 'conditional' - control flow, like "if {}" or "repeat {}" - // A 'conditional' block may return the one-based index of a branch - // to run, or it may return zero/falsy to run no branch. Each time a - // child branch finishes, the block is called again. This is only a - // slight change to the current model for control flow blocks, and is - // also compatible with returning true/false for an "if" or "repeat" - // block. - // TODO: Consider Blockly-like nextStatement, previousStatement, and - // output attributes as an alternative. Those are more flexible, but - // allow bad combinations. - blockType: 'reporter', - - // Required for conditional blocks, ignored for others: the number of - // child branches this block controls. An "if" or "repeat" block would - // specify a branch count of 1; an "if-else" block would specify a - // branch count of 2. - // TODO: should we support dynamic branch count for "switch"-likes? - branchCount: 0, - - // Optional, default false: whether or not this block ends a stack. - // The "forever" and "stop all" blocks would specify true here. - terminal: true, - - // Optional, default false: whether or not to block all threads while - // this block is busy. This is for things like the "touching color" - // block in compatibility mode, and is only needed if the VM runs in a - // worker. We might even consider omitting it from extension docs... - blockAllThreads: false, - - // Required: the human-readable text on this block, as a map from IETF - // language tags to strings in ICU message format. Note that - // '{MY_ARG,string}' tells the extension system the name and type of - // this argument. If the type is found in the `menus` object below, - // then this is a menu. If the extension only supplies one language, - // this may be a single string instead of a map. - // I make no claims about the correctness of my German grammar... - text: ['give {MY_ARG} to my reporter', 'myReporter_text'] - - // Optional: each argument may specify a default value. - // If absent, the default is a blank/empty field. - defaults: { - MY_ARG: ['some string', 'MY_ARG_default'] - } - - // Required: the function implementing this block. - func: this.myReporter, - - // Optional: list of target types for which this block should appear. - // If absent, assume it applies to all builtin targets -- that is: - // ['sprite', 'stage'] - filter: ['someBlocks.wedo2', 'sprite', 'stage'] - }, - { - // Another block... - } - ], - - // Optional: define extension-specific menus here. - menus: { - // Required: an identifier for this menu, unique within this extension. - menuA: [ - // Static menu: list items which should appear in the menu. - { - // Required: the value of the menu item when it is chosen. - value: 'itemId1', - - // Optional: the human-readable label for this item, in ICU format. - // Use `value` as the text if this is absent. - text: ['Item One', 'menuA_item1'] - }, - - // The simplest form of a list item is a string which will be used as - // both value and text. - 'itemId2' - ], - - // Dynamic menu: returns an array as above. - // Called each time the menu is opened. - menuB: this.getItemsForMenuB - }, - - // Optional: translations - translation_map: { - 'de' : { - 'someBlocks' : 'Einige Blöcke', - 'myReporter_text' : 'Gib {MY_ARG} zu meinem Reporter', - 'myReporter_val' : '{my_arg} ist mein wert' - 'MY_ARG_default' : 'etwas inhalt', - 'menuA_item1' : 'Artikel eins', - 'menuB_default' : 'Beispiel' //default to use for getItemsForMenuB - }, - 'it' : { - ... - } - } - - // Optional: list new target type(s) provided by this extension. - targetTypes: [ - 'wedo2', // automatically transformed to 'someBlocks.wedo2' - 'speech' // automatically transformed to 'someBlocks.speech' - ] - }; - } - - /** - * Implement myReporter. - * @param {object} args - the block's arguments. - * @property {string} MY_ARG - the string value of the argument. - * @returns {string} a string which includes the block argument value. - */ - myReporter (args) { - //return `${args.MY_ARG} is my argument`; - return _trans(['{my_arg} is my argument', myReporter_val], {'my_arg' : args.MY_ARG}) - } -} -``` - -# Questions and Issues -* What about collisions? If two extensions have 'MY_ARG_default', they shouldn't collide. Should the keys include the extension id: 'someBlocks.MY_ARG_default', or should we assume that the strings will get loaded into a map that is name-spaced by extension: -``` -'someBlocks' : { - ... - 'MY_ARG_default' : 'something', - ... -}, -'otherExt' : { - ... - 'MY_ARG_default' : 'something else', - ... -} -``` -* Should we insist that the default is English? -* If people want to provide additional translations, how are the original key:strings extracted, and how are new translations included? Do we expect ongoing translation of extensions? -* Should we provide translation for the extension functions as in `_trans` in the myReporter function? How likely are extensions to actually return strings? -* Using ICU formatted strings, by default arguments are only typed if they are number, date or time. We may want to know what type of argument is allowed (e.g. Boolean) to determine the look of the block, but that probably doesn't belong in the translation string. -* This assumes that we'll do some sort of mapping from ICU format `'give {MY_ARG} to my reporter'` to Blockly `'give %1 to my reporter'`. An alternative would be to use Blockly type string format in extensions. From 18f3307ed0fe82057e49e9bc89c1b46fff1af5f0 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 5 Apr 2019 15:00:09 -0700 Subject: [PATCH 3/5] Use ES6 for code in extension docs --- docs/extensions.md | 474 ++++++++++++++++++++++++--------------------- 1 file changed, 250 insertions(+), 224 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 72a085661..d55071c99 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -9,8 +9,9 @@ wiki](https://github.com/LLK/docs/wiki/Extensions). ## Types of Extensions There are four types of extensions that can define everything from the Scratch's core library (such as the "Looks" and -"Operators" categories) to unofficial extensions that can be loaded from a remote URL. Unofficial extensions are not -yet supported. +"Operators" categories) to unofficial extensions that can be loaded from a remote URL. + +**Scratch 3.0 does not yet support unofficial extensions.** For more details, see [this Extensions page on the wiki](https://github.com/LLK/docs/wiki/Extensions). @@ -22,6 +23,15 @@ For more details, see [this Extensions page on the wiki](https://github.com/LLK/ | Sandboxed | X | X | √ | √ | | Can save projects to community | √ | √ | √ | X | +## JavaScript Environment + +Most Scratch 3.0 is written using JavaScript features not yet commonly supported by browsers. For compatibility we +transpile the code to ES5 before publishing or deploying. Any extension included in the `scratch-vm` repository may +use ES6+ features and may use `require` to reference other code within the `scratch-vm` repository. + +Unofficial extensions must be self-contained. Authors of unofficial extensions are responsible for ensuring browser +compatibility for those extensions, including transpiling if necessary. + ## Translation Scratch extensions use the [ICU message format](http://userguide.icu-project.org/formatparse/messages) to handle @@ -46,12 +56,17 @@ Scratch extensions are defined as a single Javascript class which accepts either across a well defined worker boundary (i.e. the sandbox). ```js -var SomeBlocks = function (runtimeProxy) { - /** - * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. - * @type {Runtime} - */ - this.runtime = runtimeProxy; +class SomeBlocks { + constructor (runtime) { + /** + * Store this for later communication with the Scratch VM runtime. + * If this extension is running in a sandbox then `runtime` is an async proxy object. + * @type {Runtime} + */ + this.runtime = runtime; + } + + // ... } ``` @@ -59,260 +74,271 @@ All extensions must define a function called `getInfo` which returns an object t render both the blocks and the extension itself. ```js -SomeBlocks.prototype.getInfo = function () { - return { - id: 'someBlocks', - name: 'Some Blocks', - blocks: [ - { - opcode: 'myReporter', - blockType: BlockType.REPORTER, - text: 'letter [LETTER_NUM] of [TEXT]', - arguments: { - LETTER_NUM: { - type: ArgumentType.STRING, - defaultValue: '1' - }, - TEXT: { - type: ArgumentType.STRING, - defaultValue: 'text' +class SomeBlocks { + // ... + getInfo () { + return { + id: 'someBlocks', + name: 'Some Blocks', + blocks: [ + { + opcode: 'myReporter', + blockType: BlockType.REPORTER, + text: 'letter [LETTER_NUM] of [TEXT]', + arguments: { + LETTER_NUM: { + type: ArgumentType.STRING, + defaultValue: '1' + }, + TEXT: { + type: ArgumentType.STRING, + defaultValue: 'text' + } } } - } - ] - }; -}; + ] + }; + } + // ... +} ``` Finally the extension must define a function for any "opcode" defined in the blocks. For example: ```js -SomeBlocks.prototype.myReporter = function (args) { - return args.TEXT.charAt(args.LETTER_NUM); -}; +class SomeBlocks { + // ... + myReporter (args) { + return args.TEXT.charAt(args.LETTER_NUM); + }; + // ... +} ``` ## Annotated Example ```js -var SomeBlocks = function (runtimeProxy) { +class SomeBlocks { + constructor (runtime) { + /** + * Store this for later communication with the Scratch VM runtime. + * If this extension is running in a sandbox then `runtime` is an async proxy object. + * @type {Runtime} + */ + this.runtime = runtime; + } + /** - * A proxy to communicate with the Scratch 3.0 runtime across a worker boundary. - * @type {Runtime} + * @return {object} This extension's metadata. */ - this.runtime = runtimeProxy; -}; + getInfo () { + return { + // Required: the machine-readable name of this extension. + // Will be used as the extension's namespace. + id: 'someBlocks', -/** - * @return {object} This extension's metadata. - */ -SomeBlocks.prototype.getInfo = function () { - return { - // Required: the machine-readable name of this extension. - // Will be used as the extension's namespace. - id: 'someBlocks', + // Optional: the human-readable name of this extension as string. + // This and any other string to be displayed in the Scratch UI may either be + // a string or a call to `formatMessage`; a plain string will not be + // translated whereas a call to `formatMessage` will connect the string + // to the translation map (see below). The `formatMessage` call is + // similar to `formatMessage` from `react-intl` in form, but will actually + // call some extension support code to do its magic. For example, we will + // internally namespace the messages such that two extensions could have + // messages with the same ID without colliding. + // See also: https://github.com/yahoo/react-intl/wiki/API#formatmessage + name: formatMessage({ + id: 'extensionName', + defaultMessage: 'Some Blocks', + description: 'Extension name' + }), - // Optional: the human-readable name of this extension as string. - // This and any other string to be displayed in the Scratch UI may either be - // a string or a call to `formatMessage`; a plain string will not be - // translated whereas a call to `formatMessage` will connect the string - // to the translation map (see below). The `formatMessage` call is - // similar to `formatMessage` from `react-intl` in form, but will actually - // call some extension support code to do its magic. For example, we will - // internally namespace the messages such that two extensions could have - // messages with the same ID without colliding. - // See also: https://github.com/yahoo/react-intl/wiki/API#formatmessage - name: formatMessage({ - id: 'extensionName', - defaultMessage: 'Some Blocks', - description: 'Extension name' - }), + // Optional: URI for a block icon, to display at the edge of each block for this + // extension. Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + blockIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', - // Optional: URI for a block icon, to display at the edge of each block for this - // extension. Data URI OK. - // TODO: what file types are OK? All web images? Just PNG? - blockIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + // Optional: URI for an icon to be displayed in the blocks category menu. + // If not present, the menu will display the block icon, if one is present. + // Otherwise, the category menu shows its default filled circle. + // Data URI OK. + // TODO: what file types are OK? All web images? Just PNG? + menuIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', - // Optional: URI for an icon to be displayed in the blocks category menu. - // If not present, the menu will display the block icon, if one is present. - // Otherwise, the category menu shows its default filled circle. - // Data URI OK. - // TODO: what file types are OK? All web images? Just PNG? - menuIconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DEUIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==', + // Optional: Link to documentation content for this extension. + // If not present, offer no link. + docsURI: 'https://....', - // Optional: Link to documentation content for this extension. - // If not present, offer no link. - docsURI: 'https://....', + // Required: the list of blocks implemented by this extension, + // in the order intended for display. + blocks: [ + { + // Required: the machine-readable name of this operation. + // This will appear in project JSON. + opcode: 'myReporter', // becomes 'someBlocks.myReporter' - // Required: the list of blocks implemented by this extension, - // in the order intended for display. - blocks: [ - { - // Required: the machine-readable name of this operation. - // This will appear in project JSON. - opcode: 'myReporter', // becomes 'someBlocks.myReporter' + // Required: the kind of block we're defining, from a predefined list: + // 'command' - a normal command block, like "move {} steps" + // 'reporter' - returns a value, like "direction" + // 'Boolean' - same as 'reporter' but returns a Boolean value + // 'hat' - starts a stack if its value is truthy + // 'conditional' - control flow, like "if {}" or "if {} else {}" + // A 'conditional' block may return the one-based index of a branch to + // run, or it may return zero/falsy to run no branch. + // 'loop' - control flow, like "repeat {} {}" or "forever {}" + // A 'loop' block is like a conditional block with two differences: + // - the block is assumed to have exactly one child branch, and + // - each time a child branch finishes, the loop block is called again. + blockType: 'reporter', - // Required: the kind of block we're defining, from a predefined list: - // 'command' - a normal command block, like "move {} steps" - // 'reporter' - returns a value, like "direction" - // 'Boolean' - same as 'reporter' but returns a Boolean value - // 'hat' - starts a stack if its value is truthy - // 'conditional' - control flow, like "if {}" or "if {} else {}" - // A 'conditional' block may return the one-based index of a branch to - // run, or it may return zero/falsy to run no branch. - // 'loop' - control flow, like "repeat {} {}" or "forever {}" - // A 'loop' block is like a conditional block with two differences: - // - the block is assumed to have exactly one child branch, and - // - each time a child branch finishes, the loop block is called again. - blockType: 'reporter', + // Required for conditional blocks, ignored for others: the number of + // child branches this block controls. An "if" or "repeat" block would + // specify a branch count of 1; an "if-else" block would specify a + // branch count of 2. + // TODO: should we support dynamic branch count for "switch"-likes? + branchCount: 0, - // Required for conditional blocks, ignored for others: the number of - // child branches this block controls. An "if" or "repeat" block would - // specify a branch count of 1; an "if-else" block would specify a - // branch count of 2. - // TODO: should we support dynamic branch count for "switch"-likes? - branchCount: 0, + // Optional, default false: whether or not this block ends a stack. + // The "forever" and "stop all" blocks would specify true here. + terminal: true, - // Optional, default false: whether or not this block ends a stack. - // The "forever" and "stop all" blocks would specify true here. - terminal: true, + // Optional, default false: whether or not to block all threads while + // this block is busy. This is for things like the "touching color" + // block in compatibility mode, and is only needed if the VM runs in a + // worker. We might even consider omitting it from extension docs... + blockAllThreads: false, - // Optional, default false: whether or not to block all threads while - // this block is busy. This is for things like the "touching color" - // block in compatibility mode, and is only needed if the VM runs in a - // worker. We might even consider omitting it from extension docs... - blockAllThreads: false, + // Required: the human-readable text on this block, including argument + // placeholders. Argument placeholders should be in [MACRO_CASE] and + // must be [ENCLOSED_WITHIN_SQUARE_BRACKETS]. + text: formatMessage({ + id: 'myReporter', + defaultMessage: 'letter [LETTER_NUM] of [TEXT]', + description: 'Label on the "myReporter" block' + }), - // Required: the human-readable text on this block, including argument - // placeholders. Argument placeholders should be in [MACRO_CASE] and - // must be [ENCLOSED_WITHIN_SQUARE_BRACKETS]. - text: formatMessage({ - id: 'myReporter', - defaultMessage: 'letter [LETTER_NUM] of [TEXT]', - description: 'Label on the "myReporter" block' - }), - - // Required: describe each argument. - // Argument order may change during translation, so arguments are - // identified by their placeholder name. In those situations where - // arguments must be ordered or assigned an ordinal, such as interaction - // with Scratch Blocks, arguments are ordered as they are in the default - // translation (probably English). - arguments: { - // Required: the ID of the argument, which will be the name in the - // args object passed to the implementation function. - LETTER_NUM: { - // Required: type of the argument / shape of the block input - type: 'number', - - // Optional: the default value of the argument - default: 1 - }, - - // Required: the ID of the argument, which will be the name in the - // args object passed to the implementation function. - TEXT: { - // Required: type of the argument / shape of the block input - type: 'string', + // Required: describe each argument. + // Argument order may change during translation, so arguments are + // identified by their placeholder name. In those situations where + // arguments must be ordered or assigned an ordinal, such as interaction + // with Scratch Blocks, arguments are ordered as they are in the default + // translation (probably English). + arguments: { + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + LETTER_NUM: { + // Required: type of the argument / shape of the block input + type: 'number', // Optional: the default value of the argument - default: formatMessage({ - id: 'myReporter.TEXT_default', - defaultMessage: 'text', - description: 'Default for "TEXT" argument of "myReporter"' - }) - } + default: 1 + }, + + // Required: the ID of the argument, which will be the name in the + // args object passed to the implementation function. + TEXT: { + // Required: type of the argument / shape of the block input + type: 'string', + + // Optional: the default value of the argument + default: formatMessage({ + id: 'myReporter.TEXT_default', + defaultMessage: 'text', + description: 'Default for "TEXT" argument of "myReporter"' + }) + } + }, + + // Required: the function implementing this block. + func: 'myReporter', + + // Optional: list of target types for which this block should appear. + // If absent, assume it applies to all builtin targets -- that is: + // ['sprite', 'stage'] + filter: ['someBlocks.wedo2', 'sprite', 'stage'] }, - - // Required: the function implementing this block. - func: 'myReporter', - - // Optional: list of target types for which this block should appear. - // If absent, assume it applies to all builtin targets -- that is: - // ['sprite', 'stage'] - filter: ['someBlocks.wedo2', 'sprite', 'stage'] - }, - { - // Another block... - } - ], - - // Optional: define extension-specific menus here. - menus: { - // Required: an identifier for this menu, unique within this extension. - menuA: [ - // Static menu: list items which should appear in the menu. { - // Required: the value of the menu item when it is chosen. - value: 'itemId1', - - // Optional: the human-readable label for this item. - // Use `value` as the text if this is absent. - text: formatMessage({ - id: 'menuA_item1', - defaultMessage: 'Item One', - description: 'Label for item 1 of menu A' - }) - }, - - // The simplest form of a list item is a string which will be used as - // both value and text. - 'itemId2' + // Another block... + } ], - // Dynamic menu: returns an array as above. - // Called each time the menu is opened. - menuB: 'getItemsForMenuB' - }, + // Optional: define extension-specific menus here. + menus: { + // Required: an identifier for this menu, unique within this extension. + menuA: [ + // Static menu: list items which should appear in the menu. + { + // Required: the value of the menu item when it is chosen. + value: 'itemId1', - // Optional: translations - translation_map: { - de: { - 'extensionName': 'Einige Blöcke', - 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', - 'myReporter.TEXT_default': 'Text', - 'menuA_item1': 'Artikel eins', + // Optional: the human-readable label for this item. + // Use `value` as the text if this is absent. + text: formatMessage({ + id: 'menuA_item1', + defaultMessage: 'Item One', + description: 'Label for item 1 of menu A' + }) + }, - // Dynamic menus can be translated too - 'menuB_example': 'Beispiel', + // The simplest form of a list item is a string which will be used as + // both value and text. + 'itemId2' + ], - // This message contains ICU placeholders (see `myReporter()` below) - 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' + // Dynamic menu: returns an array as above. + // Called each time the menu is opened. + menuB: 'getItemsForMenuB' }, - it: { - // ... - } - }, - // Optional: list new target type(s) provided by this extension. - targetTypes: [ - 'wedo2', // automatically transformed to 'someBlocks.wedo2' - 'speech' // automatically transformed to 'someBlocks.speech' - ] + // Optional: translations + translation_map: { + de: { + 'extensionName': 'Einige Blöcke', + 'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]', + 'myReporter.TEXT_default': 'Text', + 'menuA_item1': 'Artikel eins', + + // Dynamic menus can be translated too + 'menuB_example': 'Beispiel', + + // This message contains ICU placeholders (see `myReporter()` below) + 'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.' + }, + it: { + // ... + } + }, + + // Optional: list new target type(s) provided by this extension. + targetTypes: [ + 'wedo2', // automatically transformed to 'someBlocks.wedo2' + 'speech' // automatically transformed to 'someBlocks.speech' + ] + }; }; -}; -/** - * Implement myReporter. - * @param {object} args - the block's arguments. - * @property {string} MY_ARG - the string value of the argument. - * @returns {string} a string which includes the block argument value. - */ -SomeBlocks.prototype.myReporter = function (args) { - // This message contains ICU placeholders, not Scratch placeholders - const message = formatMessage({ - id: 'myReporter.result', - defaultMessage: 'Letter {LETTER_NUM} of {TEXT} is {LETTER}.', - description: 'The text template for the "myReporter" block result' - }); + /** + * Implement myReporter. + * @param {object} args - the block's arguments. + * @property {string} MY_ARG - the string value of the argument. + * @returns {string} a string which includes the block argument value. + */ + myReporter (args) { + // This message contains ICU placeholders, not Scratch placeholders + const message = formatMessage({ + id: 'myReporter.result', + defaultMessage: 'Letter {LETTER_NUM} of {TEXT} is {LETTER}.', + description: 'The text template for the "myReporter" block result' + }); - // Note: this implementation is not Unicode-clean; it's just here as an example. - const result = args.TEXT.charAt(args.LETTER_NUM); + // Note: this implementation is not Unicode-clean; it's just here as an example. + const result = args.TEXT.charAt(args.LETTER_NUM); - return message.format({ - LETTER_NUM: args.LETTER_NUM, - TEXT: args.TEXT, - LETTER: result - }); -}; + return message.format({ + LETTER_NUM: args.LETTER_NUM, + TEXT: args.TEXT, + LETTER: result + }); + }; +} ``` From 99f6c7b6a093d6d54bc764234c96ececaac42076 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 5 Apr 2019 16:05:42 -0700 Subject: [PATCH 4/5] Update extension docs to reflect current implementation state --- docs/extensions.md | 72 ++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index d55071c99..7e500b03b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -41,7 +41,8 @@ for translation. **All extensions** may additionally define a `translation_map` object within the `getInfo` function which can provide translations within an extension itself. The "Annotated Example" below provides a more complete illustration of how -translation within an extension can be managed. +translation within an extension can be managed. **WARNING:** the `translation_map` feature is currently in the +proposal phase and may change before implementation. ## Backwards Compatibility @@ -118,6 +119,16 @@ class SomeBlocks { ## Annotated Example ```js +// Core, Team, and Official extensions can `require` VM code: +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); +const TargetType = require('../../extension-support/target-type'); + +// ...or VM dependencies: +const formatMessage = require('format-message'); + +// Core, Team, and Official extension classes should be registered statically with the Extension Manager. +// See: scratch-vm/src/extension-support/extension-manager.js class SomeBlocks { constructor (runtime) { /** @@ -150,7 +161,7 @@ class SomeBlocks { name: formatMessage({ id: 'extensionName', defaultMessage: 'Some Blocks', - description: 'Extension name' + description: 'The name of the "Some Blocks" extension' }), // Optional: URI for a block icon, to display at the edge of each block for this @@ -177,21 +188,25 @@ class SomeBlocks { // This will appear in project JSON. opcode: 'myReporter', // becomes 'someBlocks.myReporter' - // Required: the kind of block we're defining, from a predefined list: - // 'command' - a normal command block, like "move {} steps" - // 'reporter' - returns a value, like "direction" - // 'Boolean' - same as 'reporter' but returns a Boolean value - // 'hat' - starts a stack if its value is truthy - // 'conditional' - control flow, like "if {}" or "if {} else {}" - // A 'conditional' block may return the one-based index of a branch to - // run, or it may return zero/falsy to run no branch. - // 'loop' - control flow, like "repeat {} {}" or "forever {}" - // A 'loop' block is like a conditional block with two differences: - // - the block is assumed to have exactly one child branch, and - // - each time a child branch finishes, the loop block is called again. - blockType: 'reporter', + // Required: the kind of block we're defining, from a predefined list. + // Fully supported block types: + // BlockType.BOOLEAN - same as REPORTER but returns a Boolean value + // BlockType.COMMAND - a normal command block, like "move {} steps" + // BlockType.HAT - starts a stack if its value changes from falsy to truthy ("edge triggered") + // BlockType.REPORTER - returns a value, like "direction" + // Block types in development or for internal use only: + // BlockType.BUTTON - place a button in the block palette + // BlockType.CONDITIONAL - control flow, like "if {}" or "if {} else {}" + // A CONDITIONAL block may return the one-based index of a branch to + // run, or it may return zero/falsy to run no branch. + // BlockType.EVENT - starts a stack in response to an event (full spec TBD) + // BlockType.LOOP - control flow, like "repeat {} {}" or "forever {}" + // A LOOP block is like a CONDITIONAL block with two differences: + // - the block is assumed to have exactly one child branch, and + // - each time a child branch finishes, the loop block is called again. + blockType: BlockType.REPORTER, - // Required for conditional blocks, ignored for others: the number of + // Required for CONDITIONAL blocks, ignored for others: the number of // child branches this block controls. An "if" or "repeat" block would // specify a branch count of 1; an "if-else" block would specify a // branch count of 2. @@ -228,7 +243,7 @@ class SomeBlocks { // args object passed to the implementation function. LETTER_NUM: { // Required: type of the argument / shape of the block input - type: 'number', + type: ArgumentType.NUMBER, // Optional: the default value of the argument default: 1 @@ -238,24 +253,25 @@ class SomeBlocks { // args object passed to the implementation function. TEXT: { // Required: type of the argument / shape of the block input - type: 'string', + type: ArgumentType.STRING, // Optional: the default value of the argument default: formatMessage({ id: 'myReporter.TEXT_default', defaultMessage: 'text', - description: 'Default for "TEXT" argument of "myReporter"' + description: 'Default for "TEXT" argument of "someBlocks.myReporter"' }) } }, - // Required: the function implementing this block. + // Optional: the function implementing this block. + // If absent, assume `func` is the same as `opcode`. func: 'myReporter', // Optional: list of target types for which this block should appear. // If absent, assume it applies to all builtin targets -- that is: - // ['sprite', 'stage'] - filter: ['someBlocks.wedo2', 'sprite', 'stage'] + // [TargetType.SPRITE, TargetType.STAGE] + filter: [TargetType.SPRITE] }, { // Another block... @@ -276,7 +292,7 @@ class SomeBlocks { text: formatMessage({ id: 'menuA_item1', defaultMessage: 'Item One', - description: 'Label for item 1 of menu A' + description: 'Label for item 1 of menu A in "Some Blocks" extension' }) }, @@ -290,7 +306,7 @@ class SomeBlocks { menuB: 'getItemsForMenuB' }, - // Optional: translations + // Optional: translations (UNSTABLE - NOT YET SUPPORTED) translation_map: { de: { 'extensionName': 'Einige Blöcke', @@ -307,13 +323,7 @@ class SomeBlocks { it: { // ... } - }, - - // Optional: list new target type(s) provided by this extension. - targetTypes: [ - 'wedo2', // automatically transformed to 'someBlocks.wedo2' - 'speech' // automatically transformed to 'someBlocks.speech' - ] + } }; }; From 519a37bb00d564bab143ae9ea8b78c49a6d83020 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 16 Apr 2019 14:13:49 -0700 Subject: [PATCH 5/5] add `require` lines for arg/block types in `getInfo` intro snippet --- docs/extensions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/extensions.md b/docs/extensions.md index 7e500b03b..6cb632e1c 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -75,6 +75,10 @@ All extensions must define a function called `getInfo` which returns an object t render both the blocks and the extension itself. ```js +// Core, Team, and Official extensions can `require` VM code: +const ArgumentType = require('../../extension-support/argument-type'); +const BlockType = require('../../extension-support/block-type'); + class SomeBlocks { // ... getInfo () {