diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 000000000..6cb632e1c --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,358 @@ +# 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. + +**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). + +| | 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 | + +## 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 +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. **WARNING:** the `translation_map` feature is currently in the +proposal phase and may change before implementation. + +## 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 +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; + } + + // ... +} +``` + +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 +// 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 () { + 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 +class SomeBlocks { + // ... + myReporter (args) { + return args.TEXT.charAt(args.LETTER_NUM); + }; + // ... +} +``` + +## 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) { + /** + * 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; + } + + /** + * @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. + // 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: 'The name of the "Some Blocks" extension' + }), + + // 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. + // 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 + // 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: ArgumentType.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: ArgumentType.STRING, + + // Optional: the default value of the argument + default: formatMessage({ + id: 'myReporter.TEXT_default', + defaultMessage: 'text', + description: 'Default for "TEXT" argument of "someBlocks.myReporter"' + }) + } + }, + + // 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: + // [TargetType.SPRITE, TargetType.STAGE] + filter: [TargetType.SPRITE] + }, + { + // 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 in "Some Blocks" extension' + }) + }, + + // 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 (UNSTABLE - NOT YET SUPPORTED) + 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: { + // ... + } + } + }; + }; + + /** + * 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); + + return message.format({ + LETTER_NUM: args.LETTER_NUM, + TEXT: args.TEXT, + LETTER: result + }); + }; +} +```