diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..e84613dd6
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.{js,html}]
+indent_style = space
diff --git a/.eslintrc b/.eslintrc
index 0d67347a8..fcb231522 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,11 +8,13 @@
"max-len": [2, 80, 4],
"semi": [2, "always"],
"strict": [2, "never"],
- "no-console": [2, {"allow": ["log", "warn", "error"]}]
+ "no-console": [2, {"allow": ["log", "warn", "error", "groupCollapsed", "groupEnd", "time", "timeEnd"]}],
+ "valid-jsdoc": ["error", {"requireReturn": false}]
},
"env": {
"node": true,
- "browser": true
+ "browser": true,
+ "worker": true
},
"extends": "eslint:recommended"
}
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..9ea236ba5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,15 @@
+### Expected behavior
+
+_Please describe what should happen_
+
+### Actual behavior
+
+_Describe what actually happens_
+
+### Steps to reproduce
+
+_Explain what someone needs to do in order to see what's described in *Actual behavior* above_
+
+### Operating system and browser
+
+_e.g. Mac OS 10.11.6 Safari 10.0_
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..696bd38d7
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+### Proposed changes
+
+_Describe what this Pull Request does_
+
+### Reason for changes
+
+_Explain why these changes should be made. Please include an issue # if applicable._
diff --git a/.gitignore b/.gitignore
index db98532d4..aad54ff5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,11 @@ npm-*
# Testing
/.nyc_output
/coverage
+/dist.js
+/vm.js
+/vm.min.js
+/playground/assets
+/playground/media
+/playground/vendor.js
+/playground/vm.js
+/playground/zenburn.css
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 000000000..bd86c446a
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,2 @@
+/.nyc_output
+/coverage
diff --git a/.travis.yml b/.travis.yml
index 00dd791d5..a44d53809 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,35 @@
language: node_js
node_js:
-- "4"
-- "stable"
+- '4'
+- stable
sudo: false
cache:
directories:
- node_modules
+install:
+- npm install
+- npm update
+after_script:
+- |
+ # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel
+ declare exitCode
+ $(npm bin)/travis-after-all
+ exitCode=$?
+ if [[
+ # Execute after all jobs finish successfully
+ $exitCode = 0 &&
+ # Only release on release branches
+ $RELEASE_BRANCHES =~ $TRAVIS_BRANCH &&
+ # Don't release on PR builds
+ $TRAVIS_PULL_REQUEST = "false"
+ ]]; then
+ # Authenticate NPM
+ echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
+ # Set version to timestamp
+ npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
+ npm publish
+ # Publish to gh-pages as most recent committer
+ git config --global user.email $(git log --pretty=format:"%ce" -n1)
+ git config --global user.name $(git log --pretty=format:"%cn" -n1)
+ ./node_modules/.bin/gh-pages -x -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -d playground -m "Build for $(git log --pretty=format:%H)"
+ fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..ddd2e36c3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,25 @@
+## Contributing
+The development of scratch-vm is an ongoing process,
+and we love to have people in the Scratch and open source communities help us along the way.
+
+If you're interested in contributing, please take a look at the
+[issues](https://github.com/LLK/scratch-vm/issues) on this repository.
+Two great ways of helping are by identifying bugs and documenting them as issues,
+or fixing issues and creating pull requests. When submitting pull requests please be patient
+-- it can take a while to find time to review them.
+The organization and class structures can't be radically changed without significant coordination
+and collaboration from the Scratch Team, so these types of changes should be avoided.
+
+It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch,
+but some think that estimate is a little low. While we welcome suggestions for new features in our
+[suggestions forum](https://scratch.mit.edu/discuss/1/) (especially ones that come with mockups), we are unlikely to accept PRs with
+new features that haven't been thought through and discussed as a group. Why? Because we have a strong belief
+in the value of keeping things simple for new users. To learn more about our design philosophy,
+see [the Scratch Developers page](https://scratch.mit.edu/developers), or
+[this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
+
+Beyond this repo, there are also some other resources that you might want to take a look at:
+* [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch)
+* [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch
+* [Suggestions forum](https://scratch.mit.edu/discuss/1/) on Scratch
+* [Bugs & Glitches forum](https://scratch.mit.edu/discuss/3/) on Scratch
diff --git a/Makefile b/Makefile
index 74886ded4..14a5870ba 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@ ESLINT=./node_modules/.bin/eslint
NODE=node
TAP=./node_modules/.bin/tap
WEBPACK=./node_modules/.bin/webpack --progress --colors
+WEBPACK_DEV_SERVER=./node_modules/.bin/webpack-dev-server
# ------------------------------------------------------------------------------
@@ -11,6 +12,9 @@ build:
watch:
$(WEBPACK) --watch
+serve:
+ $(WEBPACK_DEV_SERVER)
+
# ------------------------------------------------------------------------------
lint:
@@ -27,4 +31,4 @@ coverage:
# ------------------------------------------------------------------------------
-.PHONY: build lint test coverage benchmark
+.PHONY: build lint test coverage benchmark serve
diff --git a/README.md b/README.md
index 3d6fd7fa0..b701ca103 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,45 @@
## scratch-vm
#### Scratch VM is a library for representing, running, and maintaining the state of computer programs written using [Scratch Blocks](https://github.com/LLK/scratch-blocks).
-[![Build Status](https://travis-ci.com/LLK/scratch-vm.svg?token=xzzHj4ct3SyBTpeqxnx1&branch=develop)](https://travis-ci.com/LLK/scratch-vm)
+[![Build Status](https://travis-ci.org/LLK/scratch-vm.svg?branch=develop)](https://travis-ci.org/LLK/scratch-vm)
+[![Coverage Status](https://coveralls.io/repos/github/LLK/scratch-vm/badge.svg?branch=develop)](https://coveralls.io/github/LLK/scratch-vm?branch=develop)
+[![Dependency Status](https://david-dm.org/LLK/scratch-vm.svg)](https://david-dm.org/LLK/scratch-vm)
+[![devDependency Status](https://david-dm.org/LLK/scratch-vm/dev-status.svg)](https://david-dm.org/LLK/scratch-vm#info=devDependencies)
## Installation
+This requires you to have Git and Node.js installed.
+
+In your own node environment/application:
```bash
-npm install scratch-vm
+npm install https://github.com/LLK/scratch-vm.git
+```
+If you want to edit/play yourself:
+```bash
+git clone https://github.com/LLK/scratch-vm.git
+cd scratch-vm
+npm install
```
-## Setup
-```js
-var VirtualMachine = require('scratch-vm');
-var vm = new VirtualMachine();
+## Development Server
+This requires Node.js to be installed.
-// Block events
-workspace.addChangeListener(function(e) {
- // Handle "tapping" a block
- if (e instanceof Blockly.Events.Ui && e.element === 'click') {
- var stackBlock = workspace.getBlockById(e.blockId).getRootBlock().id;
- vm.runtime.toggleStack(stackBlock);
- // Otherwise, pass along to the block listener
- } else {
- vm.blockListener(e);
- }
-});
+For convenience, we've included a development server with the VM. This is sometimes useful when running in an environment that's loading remote resources (e.g., SVGs from the Scratch server).
-// Run threads
-vm.runtime.start();
+## Running the Development Server
+Open a Command Prompt or Terminal in the repository and run:
+```bash
+npm start
```
+Or on Windows:
+```bash
+StartServerWindows.bat
+```
+
+## Playground
+To run the Playground, make sure the dev server's running and go to [http://localhost:8080/](http://localhost:8080/) - you will be directed to the playground, which demonstrates various tools and internal state.
+
+![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif)
+
## Standalone Build
```bash
@@ -42,42 +54,51 @@ make build
```
+## How to include in a Node.js App
+For an extended setup example, check out the /playground directory, which includes a fully running VM instance.
+```js
+var VirtualMachine = require('scratch-vm');
+var vm = new VirtualMachine();
+
+// Block events
+workspace.addChangeListener(vm.blockListener);
+
+// Run threads
+vm.start();
+```
+
## Abstract Syntax Tree
#### Overview
-The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. At any time, the current state of the AST can be viewed by inspecting the `vm.runtime.blocks` object.
+The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST) by listening to events emitted by the [scratch-blocks](https://github.com/LLK/scratch-blocks) workspace via the `blockListener`. Each target (code-running object, for example, a sprite) keeps an AST for its blocks. At any time, the current state of an AST can be viewed by inspecting the `vm.runtime.targets[...].blocks` object.
#### Anatomy of a Block
+The VM's block representation contains all the important information for execution and storage. Here's an example representing the "when key pressed" script on a workspace:
```json
{
- "id": "^1r~63Gdl7;Dh?I*OP3_",
- "opcode": "wedo_motorclockwise",
- "next": null,
- "fields": {
- "DURATION": {
- "name": "DURATION",
- "value": null,
- "blocks": {
- "1?P=eV(OiDY3vMk!24Ip": {
- "id": "1?P=eV(OiDY3vMk!24Ip",
- "opcode": "math_number",
- "next": null,
- "fields": {
- "NUM": {
- "name": "NUM",
- "value": "10",
- "blocks": null
- }
- }
- }
- }
- },
- "SUBSTACK": {
- "name": "SUBSTACK",
- "value": "@1ln(HsUO4!]*2*%BrE|",
- "blocks": null
+ "_blocks": {
+ "Q]PK~yJ@BTV8Y~FfISeo": {
+ "id": "Q]PK~yJ@BTV8Y~FfISeo",
+ "opcode": "event_whenkeypressed",
+ "inputs": {
+ },
+ "fields": {
+ "KEY_OPTION": {
+ "name": "KEY_OPTION",
+ "value": "space"
}
+ },
+ "next": null,
+ "topLevel": true,
+ "parent": null,
+ "shadow": false,
+ "x": -69.333333333333,
+ "y": 174
}
+ },
+ "_scripts": [
+ "Q]PK~yJ@BTV8Y~FfISeo"
+ ]
}
```
@@ -90,5 +111,5 @@ make test
make coverage
```
-## Donation
-We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, community, and resource development efforts. Donations of any size are appreciated. Thank you!
+## Donate
+We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!
diff --git a/StartServerWindows.bat b/StartServerWindows.bat
new file mode 100644
index 000000000..73a4d84db
--- /dev/null
+++ b/StartServerWindows.bat
@@ -0,0 +1,2 @@
+@echo off
+node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .\playground
diff --git a/assets/scratch_cat.svg b/assets/scratch_cat.svg
new file mode 100644
index 000000000..823455468
--- /dev/null
+++ b/assets/scratch_cat.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/stage.png b/assets/stage.png
new file mode 100644
index 000000000..b13e9d1f5
Binary files /dev/null and b/assets/stage.png differ
diff --git a/package.json b/package.json
index 1fcd00ef3..15f2dd459 100644
--- a/package.json
+++ b/package.json
@@ -9,18 +9,32 @@
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
},
- "main": "./src/index.js",
+ "main": "./dist.js",
"scripts": {
- "test": "make test"
- },
- "dependencies": {
- "htmlparser2": "3.9.0",
- "memoizee": "0.3.10"
+ "prepublish": "./node_modules/.bin/webpack --bail",
+ "start": "webpack-dev-server",
+ "build": "webpack --colors --progress",
+ "test": "make test",
+ "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
},
"devDependencies": {
+ "copy-webpack-plugin": "3.0.1",
"eslint": "2.7.0",
+ "expose-loader": "0.7.1",
+ "gh-pages": "0.11.0",
+ "highlightjs": "8.7.0",
+ "htmlparser2": "3.9.0",
+ "json": "9.0.4",
"json-loader": "0.5.4",
+ "lodash.defaultsdeep": "4.6.0",
+ "promise": "7.1.1",
+ "scratch-blocks": "^0.1.0-prepublish",
+ "scratch-render": "^0.1.0-prepublish",
+ "script-loader": "0.7.0",
+ "stats.js": "0.16.0",
"tap": "5.7.1",
- "webpack": "1.13.0"
+ "travis-after-all": "1.4.4",
+ "webpack": "1.13.0",
+ "webpack-dev-server": "1.14.1"
}
}
diff --git a/playground/index.html b/playground/index.html
new file mode 100644
index 000000000..46fa57cb4
--- /dev/null
+++ b/playground/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ Scratch VM Playground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/playground.css b/playground/playground.css
new file mode 100644
index 000000000..3eccd2625
--- /dev/null
+++ b/playground/playground.css
@@ -0,0 +1,73 @@
+body {
+ background: rgb(36,36,36);
+}
+a {
+ color: rgb(217,217,217);
+}
+h2 {
+ font-size: 1em;
+}
+#blocks {
+ position: absolute;
+ left: 40%;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ font-family: "Helvetica Neue", Helvetica, sans-serif;
+}
+#vm-devtools {
+ color: rgb(217,217,217);
+ position: absolute;
+ left: 1%;
+ right: 60%;
+ top: 1%;
+ bottom: 0;
+ width: 35%;
+}
+#blockexplorer, #threadexplorer, #importexport {
+ position: absolute;
+ height: 75%;
+ overflow: scroll;
+ border: 1px solid #fff;
+ background: rgb(36,36,36);
+ color: rgb(217,217,217);
+ font-family: monospace;
+ font-size: 10pt;
+ width: 480px;
+ height: 360px;
+}
+#tab-blockexplorer, #tab-threadexplorer, #tab-importexport {
+ display: none;
+}
+#importExport {
+ width: 480px;
+ height: 360px;
+ background: rgb(36,36,36);
+ color: rgb(217,217,217);
+}
+#projectId {
+ background: rgb(36,36,36);
+ color: rgb(217,217,217);
+ font-family: monospace;
+ font-size: 10pt;
+}
+ul#playgroundLinks {
+ display: block;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ background-color: #333;
+}
+#playgroundLinks li {
+ float: left;
+}
+#playgroundLinks li a {
+ display: block;
+ color: white;
+ text-align: center;
+ padding: 5px 10px;
+}
+#playgroundLinks li a:hover {
+ background-color: #111;
+}
diff --git a/playground/playground.js b/playground/playground.js
new file mode 100644
index 000000000..577877ae1
--- /dev/null
+++ b/playground/playground.js
@@ -0,0 +1,312 @@
+var NEW_PROJECT_HASH = 'createEmptyProject';
+
+var loadProject = function () {
+ var id = location.hash.substring(1);
+ if (id === NEW_PROJECT_HASH) {
+ window.vm.createEmptyProject();
+ return;
+ }
+ if (id.length < 1 || !isFinite(id)) {
+ id = '119615668';
+ }
+ var url = 'https://projects.scratch.mit.edu/internalapi/project/' +
+ id + '/get/';
+ var r = new XMLHttpRequest();
+ r.onreadystatechange = function() {
+ if (this.readyState === 4) {
+ if (r.status === 200) {
+ window.vm.loadProject(this.responseText);
+ } else {
+ window.vm.createEmptyProject();
+ }
+ }
+ };
+ r.open('GET', url);
+ r.send();
+};
+
+window.onload = function() {
+ // Lots of global variables to make debugging easier
+ // Instantiate the VM.
+ var vm = new window.VirtualMachine();
+ window.vm = vm;
+
+ // Loading projects from the server.
+ document.getElementById('projectLoadButton').onclick = function () {
+ document.location = '#' + document.getElementById('projectId').value;
+ location.reload();
+ };
+ document.getElementById('createEmptyProject').addEventListener('click',
+ function() {
+ document.location = '#' + NEW_PROJECT_HASH;
+ location.reload();
+ });
+ loadProject();
+
+ // Instantiate the renderer and connect it to the VM.
+ var canvas = document.getElementById('scratch-stage');
+ var renderer = new window.RenderWebGL(canvas);
+ window.renderer = renderer;
+ vm.attachRenderer(renderer);
+
+ // Instantiate scratch-blocks and attach it to the DOM.
+ var workspace = window.Blockly.inject('blocks', {
+ media: './media/',
+ zoom: {
+ controls: true,
+ wheel: true,
+ startScale: 0.75
+ },
+ colours: {
+ workspace: '#334771',
+ flyout: '#283856',
+ scrollbar: '#24324D',
+ scrollbarHover: '#0C111A',
+ insertionMarker: '#FFFFFF',
+ insertionMarkerOpacity: 0.3,
+ fieldShadow: 'rgba(255, 255, 255, 0.3)',
+ dragShadowOpacity: 0.6
+ }
+ });
+ window.workspace = workspace;
+
+ // Attach scratch-blocks events to VM.
+ workspace.addChangeListener(vm.blockListener);
+ var flyoutWorkspace = workspace.getFlyout().getWorkspace();
+ flyoutWorkspace.addChangeListener(vm.flyoutBlockListener);
+
+ // Create FPS counter.
+ var stats = new window.Stats();
+ document.getElementById('tab-renderexplorer').appendChild(stats.dom);
+ stats.dom.style.position = 'relative';
+ stats.begin();
+
+ // Playground data tabs.
+ // Block representation tab.
+ var blockexplorer = document.getElementById('blockexplorer');
+ var updateBlockExplorer = function(blocks) {
+ blockexplorer.innerHTML = JSON.stringify(blocks, null, 2);
+ window.hljs.highlightBlock(blockexplorer);
+ };
+
+ // Thread representation tab.
+ var threadexplorer = document.getElementById('threadexplorer');
+ var cachedThreadJSON = '';
+ var updateThreadExplorer = function (newJSON) {
+ if (newJSON != cachedThreadJSON) {
+ cachedThreadJSON = newJSON;
+ threadexplorer.innerHTML = cachedThreadJSON;
+ window.hljs.highlightBlock(threadexplorer);
+ }
+ };
+
+ // Only request data from the VM thread if the appropriate tab is open.
+ window.exploreTabOpen = false;
+ var getPlaygroundData = function () {
+ vm.getPlaygroundData();
+ if (window.exploreTabOpen) {
+ window.requestAnimationFrame(getPlaygroundData);
+ }
+ };
+
+ // VM handlers.
+ // Receipt of new playground data (thread, block representations).
+ vm.on('playgroundData', function(data) {
+ updateThreadExplorer(data.threads);
+ updateBlockExplorer(data.blocks);
+ });
+
+ // Receipt of new block XML for the selected target.
+ vm.on('workspaceUpdate', function (data) {
+ workspace.clear();
+ var dom = window.Blockly.Xml.textToDom(data.xml);
+ window.Blockly.Xml.domToWorkspace(dom, workspace);
+ });
+
+ // Receipt of new list of targets, selected target update.
+ var selectedTarget = document.getElementById('selectedTarget');
+ vm.on('targetsUpdate', function (data) {
+ // Clear select box.
+ while (selectedTarget.firstChild) {
+ selectedTarget.removeChild(selectedTarget.firstChild);
+ }
+ // Generate new select box.
+ for (var i = 0; i < data.targetList.length; i++) {
+ var targetOption = document.createElement('option');
+ targetOption.setAttribute('value', data.targetList[i][0]);
+ // If target id matches editingTarget id, select it.
+ if (data.targetList[i][0] == data.editingTarget) {
+ targetOption.setAttribute('selected', 'selected');
+ }
+ targetOption.appendChild(
+ document.createTextNode(data.targetList[i][1])
+ );
+ selectedTarget.appendChild(targetOption);
+ }
+ });
+ selectedTarget.onchange = function () {
+ vm.setEditingTarget(this.value);
+ };
+
+ // Feedback for stacks and blocks running.
+ vm.on('STACK_GLOW_ON', function(data) {
+ workspace.glowStack(data.id, true);
+ });
+ vm.on('STACK_GLOW_OFF', function(data) {
+ workspace.glowStack(data.id, false);
+ });
+ vm.on('BLOCK_GLOW_ON', function(data) {
+ workspace.glowBlock(data.id, true);
+ });
+ vm.on('BLOCK_GLOW_OFF', function(data) {
+ workspace.glowBlock(data.id, false);
+ });
+ vm.on('VISUAL_REPORT', function(data) {
+ workspace.reportValue(data.id, data.value);
+ });
+
+ // Feed mouse events as VM I/O events.
+ document.addEventListener('mousemove', function (e) {
+ var rect = canvas.getBoundingClientRect();
+ var coordinates = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ canvasWidth: rect.width,
+ canvasHeight: rect.height
+ };
+ window.vm.postIOData('mouse', coordinates);
+ });
+ canvas.addEventListener('mousedown', function (e) {
+ var rect = canvas.getBoundingClientRect();
+ var data = {
+ isDown: true,
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ canvasWidth: rect.width,
+ canvasHeight: rect.height
+ };
+ window.vm.postIOData('mouse', data);
+ e.preventDefault();
+ });
+ canvas.addEventListener('mouseup', function (e) {
+ var rect = canvas.getBoundingClientRect();
+ var data = {
+ isDown: false,
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ canvasWidth: rect.width,
+ canvasHeight: rect.height
+ };
+ window.vm.postIOData('mouse', data);
+ e.preventDefault();
+ });
+
+ // Feed keyboard events as VM I/O events.
+ document.addEventListener('keydown', function (e) {
+ // Don't capture keys intended for Blockly inputs.
+ if (e.target != document && e.target != document.body) {
+ return;
+ }
+ window.vm.postIOData('keyboard', {
+ keyCode: e.keyCode,
+ isDown: true
+ });
+ e.preventDefault();
+ });
+ document.addEventListener('keyup', function(e) {
+ // Always capture up events,
+ // even those that have switched to other targets.
+ window.vm.postIOData('keyboard', {
+ keyCode: e.keyCode,
+ isDown: false
+ });
+ // E.g., prevent scroll.
+ if (e.target != document && e.target != document.body) {
+ e.preventDefault();
+ }
+ });
+
+ // Run threads
+ vm.start();
+
+ // Inform VM of animation frames.
+ var animate = function() {
+ stats.end();
+ stats.begin();
+ window.vm.animationFrame();
+ requestAnimationFrame(animate);
+ };
+ requestAnimationFrame(animate);
+
+ // Handlers for green flag and stop all.
+ document.getElementById('greenflag').addEventListener('click', function() {
+ vm.greenFlag();
+ });
+ document.getElementById('stopall').addEventListener('click', function() {
+ vm.stopAll();
+ });
+ document.getElementById('turbomode').addEventListener('change', function() {
+ var turboOn = document.getElementById('turbomode').checked;
+ vm.setTurboMode(turboOn);
+ });
+ document.getElementById('pausemode').addEventListener('change', function() {
+ var pauseOn = document.getElementById('pausemode').checked;
+ vm.setPauseMode(pauseOn);
+ });
+ document.getElementById('compatmode').addEventListener('change',
+ function() {
+ var compatibilityMode = document.getElementById('compatmode').checked;
+ vm.setCompatibilityMode(compatibilityMode);
+ });
+ document.getElementById('singlestepmode').addEventListener('change',
+ function() {
+ var singleStep = document.getElementById('singlestepmode').checked;
+ vm.setSingleSteppingMode(singleStep);
+ });
+ document.getElementById('singlestepspeed').addEventListener('input',
+ function() {
+ var speed = document.getElementById('singlestepspeed').value;
+ vm.setSingleSteppingSpeed(speed);
+ });
+
+ var tabBlockExplorer = document.getElementById('tab-blockexplorer');
+ var tabThreadExplorer = document.getElementById('tab-threadexplorer');
+ var tabRenderExplorer = document.getElementById('tab-renderexplorer');
+ var tabImportExport = document.getElementById('tab-importexport');
+
+ // Handlers to show different explorers.
+ document.getElementById('threadexplorer-link').addEventListener('click',
+ function () {
+ window.exploreTabOpen = true;
+ getPlaygroundData();
+ tabBlockExplorer.style.display = 'none';
+ tabRenderExplorer.style.display = 'none';
+ tabThreadExplorer.style.display = 'block';
+ tabImportExport.style.display = 'none';
+ });
+ document.getElementById('blockexplorer-link').addEventListener('click',
+ function () {
+ window.exploreTabOpen = true;
+ getPlaygroundData();
+ tabBlockExplorer.style.display = 'block';
+ tabRenderExplorer.style.display = 'none';
+ tabThreadExplorer.style.display = 'none';
+ tabImportExport.style.display = 'none';
+ });
+ document.getElementById('renderexplorer-link').addEventListener('click',
+ function () {
+ window.exploreTabOpen = false;
+ tabBlockExplorer.style.display = 'none';
+ tabRenderExplorer.style.display = 'block';
+ tabThreadExplorer.style.display = 'none';
+ tabImportExport.style.display = 'none';
+ });
+ document.getElementById('importexport-link').addEventListener('click',
+ function () {
+ window.exploreTabOpen = false;
+ tabBlockExplorer.style.display = 'none';
+ tabRenderExplorer.style.display = 'none';
+ tabThreadExplorer.style.display = 'none';
+ tabImportExport.style.display = 'block';
+ });
+};
diff --git a/src/blocks/scratch3.js b/src/blocks/scratch3.js
deleted file mode 100644
index 6bf327642..000000000
--- a/src/blocks/scratch3.js
+++ /dev/null
@@ -1,82 +0,0 @@
-function Scratch3Blocks(runtime) {
- /**
- * The runtime instantiating this block package.
- * @type {Runtime}
- */
- this.runtime = runtime;
-}
-
-/**
- * Retrieve the block primitives implemented by this package.
- * @return {Object.} Mapping of opcode to Function.
- */
-Scratch3Blocks.prototype.getPrimitives = function() {
- return {
- 'control_repeat': this.repeat,
- 'control_forever': this.forever,
- 'control_wait': this.wait,
- 'control_stop': this.stop,
- 'event_whenflagclicked': this.whenFlagClicked,
- 'event_whenbroadcastreceived': this.whenBroadcastReceived,
- 'event_broadcast': this.broadcast
- };
-};
-
-Scratch3Blocks.prototype.repeat = function(argValues, util) {
- console.log('Running: control_repeat');
- // Initialize loop
- if (util.stackFrame.loopCounter === undefined) {
- util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg
- }
- // Decrease counter
- util.stackFrame.loopCounter--;
- // If we still have some left, start the substack
- if (util.stackFrame.loopCounter >= 0) {
- util.startSubstack();
- }
-};
-
-Scratch3Blocks.prototype.forever = function(argValues, util) {
- console.log('Running: control_forever');
- util.startSubstack();
-};
-
-Scratch3Blocks.prototype.wait = function(argValues, util) {
- console.log('Running: control_wait');
- util.yield();
- util.timeout(function() {
- util.done();
- }, 1000 * parseFloat(argValues[0]));
-};
-
-Scratch3Blocks.prototype.stop = function() {
- console.log('Running: control_stop');
- // @todo - don't use this.runtime
- this.runtime.stopAll();
-};
-
-Scratch3Blocks.prototype.whenFlagClicked = function() {
- console.log('Running: event_whenflagclicked');
- // No-op
-};
-
-Scratch3Blocks.prototype.whenBroadcastReceived = function() {
- console.log('Running: event_whenbroadcastreceived');
- // No-op
-};
-
-Scratch3Blocks.prototype.broadcast = function(argValues, util) {
- console.log('Running: event_broadcast');
- util.startHats(function(hat) {
- if (hat.opcode === 'event_whenbroadcastreceived') {
- var shadows = hat.fields.CHOICE.blocks;
- for (var sb in shadows) {
- var shadowblock = shadows[sb];
- return shadowblock.fields.CHOICE.value === argValues[0];
- }
- }
- return false;
- });
-};
-
-module.exports = Scratch3Blocks;
diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js
new file mode 100644
index 000000000..571b1478f
--- /dev/null
+++ b/src/blocks/scratch3_control.js
@@ -0,0 +1,139 @@
+var Cast = require('../util/cast');
+var Timer = require('../util/timer');
+
+function Scratch3ControlBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3ControlBlocks.prototype.getPrimitives = function() {
+ return {
+ 'control_repeat': this.repeat,
+ 'control_repeat_until': this.repeatUntil,
+ 'control_forever': this.forever,
+ 'control_wait': this.wait,
+ 'control_wait_until': this.waitUntil,
+ 'control_if': this.if,
+ 'control_if_else': this.ifElse,
+ 'control_stop': this.stop,
+ 'control_create_clone_of': this.createClone,
+ 'control_delete_this_clone': this.deleteClone
+ };
+};
+
+Scratch3ControlBlocks.prototype.getHats = function () {
+ return {
+ 'control_start_as_clone': {
+ restartExistingThreads: false
+ }
+ };
+};
+
+Scratch3ControlBlocks.prototype.repeat = function(args, util) {
+ var times = Math.floor(Cast.toNumber(args.TIMES));
+ // Initialize loop
+ if (util.stackFrame.loopCounter === undefined) {
+ util.stackFrame.loopCounter = times;
+ }
+ // Only execute once per frame.
+ // When the branch finishes, `repeat` will be executed again and
+ // the second branch will be taken, yielding for the rest of the frame.
+ // Decrease counter
+ util.stackFrame.loopCounter--;
+ // If we still have some left, start the branch.
+ if (util.stackFrame.loopCounter >= 0) {
+ util.startBranch(1, true);
+ }
+};
+
+Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
+ var condition = Cast.toBoolean(args.CONDITION);
+ // If the condition is true, start the branch.
+ if (!condition) {
+ util.startBranch(1, true);
+ }
+};
+
+Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
+ var condition = Cast.toBoolean(args.CONDITION);
+ if (!condition) {
+ util.yield();
+ }
+};
+
+Scratch3ControlBlocks.prototype.forever = function(args, util) {
+ util.startBranch(1, true);
+};
+
+Scratch3ControlBlocks.prototype.wait = function(args, util) {
+ if (!util.stackFrame.timer) {
+ util.stackFrame.timer = new Timer();
+ util.stackFrame.timer.start();
+ util.yield();
+ this.runtime.requestRedraw();
+ } else {
+ var duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
+ if (util.stackFrame.timer.timeElapsed() < duration) {
+ util.yield();
+ }
+ }
+};
+
+Scratch3ControlBlocks.prototype.if = function(args, util) {
+ var condition = Cast.toBoolean(args.CONDITION);
+ if (condition) {
+ util.startBranch(1, false);
+ }
+};
+
+Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
+ var condition = Cast.toBoolean(args.CONDITION);
+ if (condition) {
+ util.startBranch(1, false);
+ } else {
+ util.startBranch(2, false);
+ }
+};
+
+Scratch3ControlBlocks.prototype.stop = function(args, util) {
+ var option = args.STOP_OPTION;
+ if (option == 'all') {
+ util.stopAll();
+ } else if (option == 'other scripts in sprite' ||
+ option == 'other scripts in stage') {
+ util.stopOtherTargetThreads();
+ } else if (option == 'this script') {
+ util.stopThread();
+ }
+};
+
+Scratch3ControlBlocks.prototype.createClone = function (args, util) {
+ var cloneTarget;
+ if (args.CLONE_OPTION == '_myself_') {
+ cloneTarget = util.target;
+ } else {
+ cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
+ }
+ if (!cloneTarget) {
+ return;
+ }
+ var newClone = cloneTarget.makeClone();
+ if (newClone) {
+ this.runtime.targets.push(newClone);
+ }
+};
+
+Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
+ if (util.target.isOriginal) return;
+ this.runtime.disposeTarget(util.target);
+ this.runtime.stopForTarget(util.target);
+};
+
+module.exports = Scratch3ControlBlocks;
diff --git a/src/blocks/scratch3_data.js b/src/blocks/scratch3_data.js
new file mode 100644
index 000000000..a40172427
--- /dev/null
+++ b/src/blocks/scratch3_data.js
@@ -0,0 +1,136 @@
+var Cast = require('../util/cast');
+
+function Scratch3DataBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3DataBlocks.prototype.getPrimitives = function () {
+ return {
+ 'data_variable': this.getVariable,
+ 'data_setvariableto': this.setVariableTo,
+ 'data_changevariableby': this.changeVariableBy,
+ 'data_listcontents': this.getListContents,
+ 'data_addtolist': this.addToList,
+ 'data_deleteoflist': this.deleteOfList,
+ 'data_insertatlist': this.insertAtList,
+ 'data_replaceitemoflist': this.replaceItemOfList,
+ 'data_itemoflist': this.getItemOfList,
+ 'data_lengthoflist': this.lengthOfList,
+ 'data_listcontainsitem': this.listContainsItem
+ };
+};
+
+Scratch3DataBlocks.prototype.getVariable = function (args, util) {
+ var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
+ return variable.value;
+};
+
+Scratch3DataBlocks.prototype.setVariableTo = function (args, util) {
+ var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
+ variable.value = args.VALUE;
+};
+
+Scratch3DataBlocks.prototype.changeVariableBy = function (args, util) {
+ var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
+ var castedValue = Cast.toNumber(variable.value);
+ var dValue = Cast.toNumber(args.VALUE);
+ variable.value = castedValue + dValue;
+};
+
+Scratch3DataBlocks.prototype.getListContents = function (args, util) {
+ var list = util.target.lookupOrCreateList(args.LIST);
+ // Determine if the list is all single letters.
+ // If it is, report contents joined together with no separator.
+ // If it's not, report contents joined together with a space.
+ var allSingleLetters = true;
+ for (var i = 0; i < list.contents.length; i++) {
+ var listItem = list.contents[i];
+ if (!((typeof listItem === 'string') &&
+ (listItem.length == 1))) {
+ allSingleLetters = false;
+ break;
+ }
+ }
+ if (allSingleLetters) {
+ return list.contents.join('');
+ } else {
+ return list.contents.join(' ');
+ }
+};
+
+Scratch3DataBlocks.prototype.addToList = function (args, util) {
+ var list = util.target.lookupOrCreateList(args.LIST);
+ list.contents.push(args.ITEM);
+};
+
+Scratch3DataBlocks.prototype.deleteOfList = function (args, util) {
+ var list = util.target.lookupOrCreateList(args.LIST);
+ var index = Cast.toListIndex(args.INDEX, list.contents.length);
+ if (index === Cast.LIST_INVALID) {
+ return;
+ } else if (index === Cast.LIST_ALL) {
+ list.contents = [];
+ return;
+ }
+ list.contents.splice(index - 1, 1);
+};
+
+Scratch3DataBlocks.prototype.insertAtList = function (args, util) {
+ var item = args.ITEM;
+ var list = util.target.lookupOrCreateList(args.LIST);
+ var index = Cast.toListIndex(args.INDEX, list.contents.length + 1);
+ if (index === Cast.LIST_INVALID) {
+ return;
+ }
+ list.contents.splice(index - 1, 0, item);
+};
+
+Scratch3DataBlocks.prototype.replaceItemOfList = function (args, util) {
+ var item = args.ITEM;
+ var list = util.target.lookupOrCreateList(args.LIST);
+ var index = Cast.toListIndex(args.INDEX, list.contents.length);
+ if (index === Cast.LIST_INVALID) {
+ return;
+ }
+ list.contents.splice(index - 1, 1, item);
+};
+
+Scratch3DataBlocks.prototype.getItemOfList = function (args, util) {
+ var list = util.target.lookupOrCreateList(args.LIST);
+ var index = Cast.toListIndex(args.INDEX, list.contents.length);
+ if (index === Cast.LIST_INVALID) {
+ return '';
+ }
+ return list.contents[index - 1];
+};
+
+Scratch3DataBlocks.prototype.lengthOfList = function (args, util) {
+ var list = util.target.lookupOrCreateList(args.LIST);
+ return list.contents.length;
+};
+
+Scratch3DataBlocks.prototype.listContainsItem = function (args, util) {
+ var item = args.ITEM;
+ var list = util.target.lookupOrCreateList(args.LIST);
+ if (list.contents.indexOf(item) >= 0) {
+ return true;
+ }
+ // Try using Scratch comparison operator on each item.
+ // (Scratch considers the string '123' equal to the number 123).
+ for (var i = 0; i < list.contents.length; i++) {
+ if (Cast.compare(list.contents[i], item) == 0) {
+ return true;
+ }
+ }
+ return false;
+};
+
+module.exports = Scratch3DataBlocks;
diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js
new file mode 100644
index 000000000..7f56e3baf
--- /dev/null
+++ b/src/blocks/scratch3_event.js
@@ -0,0 +1,89 @@
+var Cast = require('../util/cast');
+
+function Scratch3EventBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3EventBlocks.prototype.getPrimitives = function() {
+ return {
+ 'event_broadcast': this.broadcast,
+ 'event_broadcastandwait': this.broadcastAndWait,
+ 'event_whengreaterthan': this.hatGreaterThanPredicate
+ };
+};
+
+Scratch3EventBlocks.prototype.getHats = function () {
+ return {
+ 'event_whenflagclicked': {
+ restartExistingThreads: true
+ },
+ 'event_whenkeypressed': {
+ restartExistingThreads: false
+ },
+ 'event_whenthisspriteclicked': {
+ restartExistingThreads: true
+ },
+ 'event_whenbackdropswitchesto': {
+ restartExistingThreads: true
+ },
+ 'event_whengreaterthan': {
+ restartExistingThreads: false,
+ edgeActivated: true
+ },
+ 'event_whenbroadcastreceived': {
+ restartExistingThreads: true
+ }
+ };
+};
+
+Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) {
+ var option = Cast.toString(args.WHENGREATERTHANMENU).toLowerCase();
+ var value = Cast.toNumber(args.VALUE);
+ // @todo: Other cases :)
+ if (option == 'timer') {
+ return util.ioQuery('clock', 'projectTimer') > value;
+ }
+ return false;
+};
+
+Scratch3EventBlocks.prototype.broadcast = function(args, util) {
+ var broadcastOption = Cast.toString(args.BROADCAST_OPTION);
+ util.startHats('event_whenbroadcastreceived', {
+ 'BROADCAST_OPTION': broadcastOption
+ });
+};
+
+Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
+ var broadcastOption = Cast.toString(args.BROADCAST_OPTION);
+ // Have we run before, starting threads?
+ if (!util.stackFrame.startedThreads) {
+ // No - start hats for this broadcast.
+ util.stackFrame.startedThreads = util.startHats(
+ 'event_whenbroadcastreceived', {
+ 'BROADCAST_OPTION': broadcastOption
+ }
+ );
+ if (util.stackFrame.startedThreads.length == 0) {
+ // Nothing was started.
+ return;
+ }
+ }
+ // We've run before; check if the wait is still going on.
+ var instance = this;
+ var waiting = util.stackFrame.startedThreads.some(function(thread) {
+ return instance.runtime.isActiveThread(thread);
+ });
+ if (waiting) {
+ util.yield();
+ }
+};
+
+module.exports = Scratch3EventBlocks;
diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js
new file mode 100644
index 000000000..a1d433e94
--- /dev/null
+++ b/src/blocks/scratch3_looks.js
@@ -0,0 +1,221 @@
+var Cast = require('../util/cast');
+
+function Scratch3LooksBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3LooksBlocks.prototype.getPrimitives = function() {
+ return {
+ 'looks_say': this.say,
+ 'looks_sayforsecs': this.sayforsecs,
+ 'looks_think': this.think,
+ 'looks_thinkforsecs': this.sayforsecs,
+ 'looks_show': this.show,
+ 'looks_hide': this.hide,
+ 'looks_switchcostumeto': this.switchCostume,
+ 'looks_switchbackdropto': this.switchBackdrop,
+ 'looks_switchbackdroptoandwait': this.switchBackdropAndWait,
+ 'looks_nextcostume': this.nextCostume,
+ 'looks_nextbackdrop': this.nextBackdrop,
+ 'looks_changeeffectby': this.changeEffect,
+ 'looks_seteffectto': this.setEffect,
+ 'looks_cleargraphiceffects': this.clearEffects,
+ 'looks_changesizeby': this.changeSize,
+ 'looks_setsizeto': this.setSize,
+ 'looks_gotofront': this.goToFront,
+ 'looks_gobacklayers': this.goBackLayers,
+ 'looks_size': this.getSize,
+ 'looks_costumeorder': this.getCostumeIndex,
+ 'looks_backdroporder': this.getBackdropIndex,
+ 'looks_backdropname': this.getBackdropName
+ };
+};
+
+Scratch3LooksBlocks.prototype.say = function (args, util) {
+ util.target.setSay('say', args.MESSAGE);
+};
+
+Scratch3LooksBlocks.prototype.sayforsecs = function (args, util) {
+ util.target.setSay('say', args.MESSAGE);
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ // Clear say bubble and proceed.
+ util.target.setSay();
+ resolve();
+ }, 1000 * args.SECS);
+ });
+};
+
+Scratch3LooksBlocks.prototype.think = function (args, util) {
+ util.target.setSay('think', args.MESSAGE);
+};
+
+Scratch3LooksBlocks.prototype.thinkforsecs = function (args, util) {
+ util.target.setSay('think', args.MESSAGE);
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ // Clear say bubble and proceed.
+ util.target.setSay();
+ resolve();
+ }, 1000 * args.SECS);
+ });
+};
+
+Scratch3LooksBlocks.prototype.show = function (args, util) {
+ util.target.setVisible(true);
+};
+
+Scratch3LooksBlocks.prototype.hide = function (args, util) {
+ util.target.setVisible(false);
+};
+
+/**
+ * Utility function to set the costume or backdrop of a target.
+ * Matches the behavior of Scratch 2.0 for different types of arguments.
+ * @param {!Target} target Target to set costume/backdrop to.
+ * @param {Any} requestedCostume Costume requested, e.g., 0, 'name', etc.
+ * @param {boolean=} opt_zeroIndex Set to zero-index the requestedCostume.
+ * @return {Array.} Any threads started by this switch.
+ */
+Scratch3LooksBlocks.prototype._setCostumeOrBackdrop = function (target,
+ requestedCostume, opt_zeroIndex) {
+ if (typeof requestedCostume === 'number') {
+ target.setCostume(opt_zeroIndex ?
+ requestedCostume : requestedCostume - 1);
+ } else {
+ var costumeIndex = target.getCostumeIndexByName(requestedCostume);
+ if (costumeIndex > -1) {
+ target.setCostume(costumeIndex);
+ } else if (costumeIndex == 'previous costume' ||
+ costumeIndex == 'previous backdrop') {
+ target.setCostume(target.currentCostume - 1);
+ } else if (costumeIndex == 'next costume' ||
+ costumeIndex == 'next backdrop') {
+ target.setCostume(target.currentCostume + 1);
+ } else {
+ var forcedNumber = Cast.toNumber(requestedCostume);
+ if (!isNaN(forcedNumber)) {
+ target.setCostume(opt_zeroIndex ?
+ forcedNumber : forcedNumber - 1);
+ }
+ }
+ }
+ if (target == this.runtime.getTargetForStage()) {
+ // Target is the stage - start hats.
+ var newName = target.sprite.costumes[target.currentCostume].name;
+ return this.runtime.startHats('event_whenbackdropswitchesto', {
+ 'BACKDROP': newName
+ });
+ }
+ return [];
+};
+
+Scratch3LooksBlocks.prototype.switchCostume = function (args, util) {
+ this._setCostumeOrBackdrop(util.target, args.COSTUME);
+};
+
+Scratch3LooksBlocks.prototype.nextCostume = function (args, util) {
+ this._setCostumeOrBackdrop(
+ util.target, util.target.currentCostume + 1, true
+ );
+};
+
+Scratch3LooksBlocks.prototype.switchBackdrop = function (args) {
+ this._setCostumeOrBackdrop(this.runtime.getTargetForStage(), args.BACKDROP);
+};
+
+Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
+ // Have we run before, starting threads?
+ if (!util.stackFrame.startedThreads) {
+ // No - switch the backdrop.
+ util.stackFrame.startedThreads = (
+ this._setCostumeOrBackdrop(
+ this.runtime.getTargetForStage(),
+ args.BACKDROP
+ )
+ );
+ if (util.stackFrame.startedThreads.length == 0) {
+ // Nothing was started.
+ return;
+ }
+ }
+ // We've run before; check if the wait is still going on.
+ var instance = this;
+ var waiting = util.stackFrame.startedThreads.some(function(thread) {
+ return instance.runtime.isActiveThread(thread);
+ });
+ if (waiting) {
+ util.yield();
+ }
+};
+
+Scratch3LooksBlocks.prototype.nextBackdrop = function () {
+ var stage = this.runtime.getTargetForStage();
+ this._setCostumeOrBackdrop(
+ stage, stage.currentCostume + 1, true
+ );
+};
+
+Scratch3LooksBlocks.prototype.changeEffect = function (args, util) {
+ var effect = Cast.toString(args.EFFECT).toLowerCase();
+ var change = Cast.toNumber(args.CHANGE);
+ if (!util.target.effects.hasOwnProperty(effect)) return;
+ var newValue = change + util.target.effects[effect];
+ util.target.setEffect(effect, newValue);
+};
+
+Scratch3LooksBlocks.prototype.setEffect = function (args, util) {
+ var effect = Cast.toString(args.EFFECT).toLowerCase();
+ var value = Cast.toNumber(args.VALUE);
+ util.target.setEffect(effect, value);
+};
+
+Scratch3LooksBlocks.prototype.clearEffects = function (args, util) {
+ util.target.clearEffects();
+};
+
+Scratch3LooksBlocks.prototype.changeSize = function (args, util) {
+ var change = Cast.toNumber(args.CHANGE);
+ util.target.setSize(util.target.size + change);
+};
+
+Scratch3LooksBlocks.prototype.setSize = function (args, util) {
+ var size = Cast.toNumber(args.SIZE);
+ util.target.setSize(size);
+};
+
+Scratch3LooksBlocks.prototype.goToFront = function (args, util) {
+ util.target.goToFront();
+};
+
+Scratch3LooksBlocks.prototype.goBackLayers = function (args, util) {
+ util.target.goBackLayers(args.NUM);
+};
+
+Scratch3LooksBlocks.prototype.getSize = function (args, util) {
+ return util.target.size;
+};
+
+Scratch3LooksBlocks.prototype.getBackdropIndex = function () {
+ var stage = this.runtime.getTargetForStage();
+ return stage.currentCostume + 1;
+};
+
+Scratch3LooksBlocks.prototype.getBackdropName = function () {
+ var stage = this.runtime.getTargetForStage();
+ return stage.sprite.costumes[stage.currentCostume].name;
+};
+
+Scratch3LooksBlocks.prototype.getCostumeIndex = function (args, util) {
+ return util.target.currentCostume + 1;
+};
+
+module.exports = Scratch3LooksBlocks;
diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js
new file mode 100644
index 000000000..68078578b
--- /dev/null
+++ b/src/blocks/scratch3_motion.js
@@ -0,0 +1,234 @@
+var Cast = require('../util/cast');
+var MathUtil = require('../util/math-util');
+var Timer = require('../util/timer');
+
+function Scratch3MotionBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3MotionBlocks.prototype.getPrimitives = function() {
+ return {
+ 'motion_movesteps': this.moveSteps,
+ 'motion_gotoxy': this.goToXY,
+ 'motion_goto': this.goTo,
+ 'motion_turnright': this.turnRight,
+ 'motion_turnleft': this.turnLeft,
+ 'motion_pointindirection': this.pointInDirection,
+ 'motion_pointtowards': this.pointTowards,
+ 'motion_glidesecstoxy': this.glide,
+ 'motion_ifonedgebounce': this.ifOnEdgeBounce,
+ 'motion_setrotationstyle': this.setRotationStyle,
+ 'motion_changexby': this.changeX,
+ 'motion_setx': this.setX,
+ 'motion_changeyby': this.changeY,
+ 'motion_sety': this.setY,
+ 'motion_xposition': this.getX,
+ 'motion_yposition': this.getY,
+ 'motion_direction': this.getDirection
+ };
+};
+
+Scratch3MotionBlocks.prototype.moveSteps = function (args, util) {
+ var steps = Cast.toNumber(args.STEPS);
+ var radians = MathUtil.degToRad(90 - util.target.direction);
+ var dx = steps * Math.cos(radians);
+ var dy = steps * Math.sin(radians);
+ util.target.setXY(util.target.x + dx, util.target.y + dy);
+};
+
+Scratch3MotionBlocks.prototype.goToXY = function (args, util) {
+ var x = Cast.toNumber(args.X);
+ var y = Cast.toNumber(args.Y);
+ util.target.setXY(x, y);
+};
+
+Scratch3MotionBlocks.prototype.goTo = function (args, util) {
+ var targetX = 0;
+ var targetY = 0;
+ if (args.TO === '_mouse_') {
+ targetX = util.ioQuery('mouse', 'getX');
+ targetY = util.ioQuery('mouse', 'getY');
+ } else if (args.TO === '_random_') {
+ var stageWidth = this.runtime.constructor.STAGE_WIDTH;
+ var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
+ targetX = Math.round(stageWidth * (Math.random() - 0.5));
+ targetY = Math.round(stageHeight * (Math.random() - 0.5));
+ } else {
+ var goToTarget = this.runtime.getSpriteTargetByName(args.TO);
+ if (!goToTarget) return;
+ targetX = goToTarget.x;
+ targetY = goToTarget.y;
+ }
+ util.target.setXY(targetX, targetY);
+};
+
+Scratch3MotionBlocks.prototype.turnRight = function (args, util) {
+ var degrees = Cast.toNumber(args.DEGREES);
+ util.target.setDirection(util.target.direction + degrees);
+};
+
+Scratch3MotionBlocks.prototype.turnLeft = function (args, util) {
+ var degrees = Cast.toNumber(args.DEGREES);
+ util.target.setDirection(util.target.direction - degrees);
+};
+
+Scratch3MotionBlocks.prototype.pointInDirection = function (args, util) {
+ var direction = Cast.toNumber(args.DIRECTION);
+ util.target.setDirection(direction);
+};
+
+Scratch3MotionBlocks.prototype.pointTowards = function (args, util) {
+ var targetX = 0;
+ var targetY = 0;
+ if (args.TOWARDS === '_mouse_') {
+ targetX = util.ioQuery('mouse', 'getX');
+ targetY = util.ioQuery('mouse', 'getY');
+ } else {
+ var pointTarget = this.runtime.getSpriteTargetByName(args.TOWARDS);
+ if (!pointTarget) return;
+ targetX = pointTarget.x;
+ targetY = pointTarget.y;
+ }
+
+ var dx = targetX - util.target.x;
+ var dy = targetY - util.target.y;
+ var direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx));
+ util.target.setDirection(direction);
+};
+
+Scratch3MotionBlocks.prototype.glide = function (args, util) {
+ if (!util.stackFrame.timer) {
+ // First time: save data for future use.
+ util.stackFrame.timer = new Timer();
+ util.stackFrame.timer.start();
+ util.stackFrame.duration = Cast.toNumber(args.SECS);
+ util.stackFrame.startX = util.target.x;
+ util.stackFrame.startY = util.target.y;
+ util.stackFrame.endX = Cast.toNumber(args.X);
+ util.stackFrame.endY = Cast.toNumber(args.Y);
+ if (util.stackFrame.duration <= 0) {
+ // Duration too short to glide.
+ util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
+ return;
+ }
+ util.yield();
+ } else {
+ var timeElapsed = util.stackFrame.timer.timeElapsed();
+ if (timeElapsed < util.stackFrame.duration * 1000) {
+ // In progress: move to intermediate position.
+ var frac = timeElapsed / (util.stackFrame.duration * 1000);
+ var dx = frac * (util.stackFrame.endX - util.stackFrame.startX);
+ var dy = frac * (util.stackFrame.endY - util.stackFrame.startY);
+ util.target.setXY(
+ util.stackFrame.startX + dx,
+ util.stackFrame.startY + dy
+ );
+ util.yield();
+ } else {
+ // Finished: move to final position.
+ util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
+ }
+ }
+};
+
+Scratch3MotionBlocks.prototype.ifOnEdgeBounce = function (args, util) {
+ var bounds = util.target.getBounds();
+ if (!bounds) {
+ return;
+ }
+ // Measure distance to edges.
+ // Values are positive when the sprite is far away,
+ // and clamped to zero when the sprite is beyond.
+ var stageWidth = this.runtime.constructor.STAGE_WIDTH;
+ var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
+ var distLeft = Math.max(0, stageWidth / 2 + bounds.left);
+ var distTop = Math.max(0, stageHeight / 2 - bounds.top);
+ var distRight = Math.max(0, stageWidth / 2 - bounds.right);
+ var distBottom = Math.max(0, stageHeight / 2 + bounds.bottom);
+ // Find the nearest edge.
+ var nearestEdge = '';
+ var minDist = Infinity;
+ if (distLeft < minDist) {
+ minDist = distLeft;
+ nearestEdge = 'left';
+ }
+ if (distTop < minDist) {
+ minDist = distTop;
+ nearestEdge = 'top';
+ }
+ if (distRight < minDist) {
+ minDist = distRight;
+ nearestEdge = 'right';
+ }
+ if (distBottom < minDist) {
+ minDist = distBottom;
+ nearestEdge = 'bottom';
+ }
+ if (minDist > 0) {
+ return; // Not touching any edge.
+ }
+ // Point away from the nearest edge.
+ var radians = MathUtil.degToRad(90 - util.target.direction);
+ var dx = Math.cos(radians);
+ var dy = -Math.sin(radians);
+ if (nearestEdge == 'left') {
+ dx = Math.max(0.2, Math.abs(dx));
+ } else if (nearestEdge == 'top') {
+ dy = Math.max(0.2, Math.abs(dy));
+ } else if (nearestEdge == 'right') {
+ dx = 0 - Math.max(0.2, Math.abs(dx));
+ } else if (nearestEdge == 'bottom') {
+ dy = 0 - Math.max(0.2, Math.abs(dy));
+ }
+ var newDirection = MathUtil.radToDeg(Math.atan2(dy, dx)) + 90;
+ util.target.setDirection(newDirection);
+ // Keep within the stage.
+ var fencedPosition = util.target.keepInFence(util.target.x, util.target.y);
+ util.target.setXY(fencedPosition[0], fencedPosition[1]);
+};
+
+Scratch3MotionBlocks.prototype.setRotationStyle = function (args, util) {
+ util.target.setRotationStyle(args.STYLE);
+};
+
+Scratch3MotionBlocks.prototype.changeX = function (args, util) {
+ var dx = Cast.toNumber(args.DX);
+ util.target.setXY(util.target.x + dx, util.target.y);
+};
+
+Scratch3MotionBlocks.prototype.setX = function (args, util) {
+ var x = Cast.toNumber(args.X);
+ util.target.setXY(x, util.target.y);
+};
+
+Scratch3MotionBlocks.prototype.changeY = function (args, util) {
+ var dy = Cast.toNumber(args.DY);
+ util.target.setXY(util.target.x, util.target.y + dy);
+};
+
+Scratch3MotionBlocks.prototype.setY = function (args, util) {
+ var y = Cast.toNumber(args.Y);
+ util.target.setXY(util.target.x, y);
+};
+
+Scratch3MotionBlocks.prototype.getX = function (args, util) {
+ return util.target.x;
+};
+
+Scratch3MotionBlocks.prototype.getY = function (args, util) {
+ return util.target.y;
+};
+
+Scratch3MotionBlocks.prototype.getDirection = function (args, util) {
+ return util.target.direction;
+};
+
+module.exports = Scratch3MotionBlocks;
diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js
new file mode 100644
index 000000000..0d5531778
--- /dev/null
+++ b/src/blocks/scratch3_operators.js
@@ -0,0 +1,143 @@
+var Cast = require('../util/cast.js');
+
+function Scratch3OperatorsBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3OperatorsBlocks.prototype.getPrimitives = function() {
+ return {
+ 'operator_add': this.add,
+ 'operator_subtract': this.subtract,
+ 'operator_multiply': this.multiply,
+ 'operator_divide': this.divide,
+ 'operator_lt': this.lt,
+ 'operator_equals': this.equals,
+ 'operator_gt': this.gt,
+ 'operator_and': this.and,
+ 'operator_or': this.or,
+ 'operator_not': this.not,
+ 'operator_random': this.random,
+ 'operator_join': this.join,
+ 'operator_letter_of': this.letterOf,
+ 'operator_length': this.length,
+ 'operator_mod': this.mod,
+ 'operator_round': this.round,
+ 'operator_mathop': this.mathop
+ };
+};
+
+Scratch3OperatorsBlocks.prototype.add = function (args) {
+ return Cast.toNumber(args.NUM1) + Cast.toNumber(args.NUM2);
+};
+
+Scratch3OperatorsBlocks.prototype.subtract = function (args) {
+ return Cast.toNumber(args.NUM1) - Cast.toNumber(args.NUM2);
+};
+
+Scratch3OperatorsBlocks.prototype.multiply = function (args) {
+ return Cast.toNumber(args.NUM1) * Cast.toNumber(args.NUM2);
+};
+
+Scratch3OperatorsBlocks.prototype.divide = function (args) {
+ return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2);
+};
+
+Scratch3OperatorsBlocks.prototype.lt = function (args) {
+ return Cast.compare(args.OPERAND1, args.OPERAND2) < 0;
+};
+
+Scratch3OperatorsBlocks.prototype.equals = function (args) {
+ return Cast.compare(args.OPERAND1, args.OPERAND2) == 0;
+};
+
+Scratch3OperatorsBlocks.prototype.gt = function (args) {
+ return Cast.compare(args.OPERAND1, args.OPERAND2) > 0;
+};
+
+Scratch3OperatorsBlocks.prototype.and = function (args) {
+ return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2);
+};
+
+Scratch3OperatorsBlocks.prototype.or = function (args) {
+ return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2);
+};
+
+Scratch3OperatorsBlocks.prototype.not = function (args) {
+ return !Cast.toBoolean(args.OPERAND);
+};
+
+Scratch3OperatorsBlocks.prototype.random = function (args) {
+ var nFrom = Cast.toNumber(args.FROM);
+ var nTo = Cast.toNumber(args.TO);
+ var low = nFrom <= nTo ? nFrom : nTo;
+ var high = nFrom <= nTo ? nTo : nFrom;
+ if (low == high) return low;
+ // If both arguments are ints, truncate the result to an int.
+ if (Cast.isInt(args.FROM) && Cast.isInt(args.TO)) {
+ return low + parseInt(Math.random() * ((high + 1) - low));
+ }
+ return (Math.random() * (high - low)) + low;
+};
+
+Scratch3OperatorsBlocks.prototype.join = function (args) {
+ return Cast.toString(args.STRING1) + Cast.toString(args.STRING2);
+};
+
+Scratch3OperatorsBlocks.prototype.letterOf = function (args) {
+ var index = Cast.toNumber(args.LETTER) - 1;
+ var str = Cast.toString(args.STRING);
+ // Out of bounds?
+ if (index < 0 || index >= str.length) {
+ return '';
+ }
+ return str.charAt(index);
+};
+
+Scratch3OperatorsBlocks.prototype.length = function (args) {
+ return Cast.toString(args.STRING).length;
+};
+
+Scratch3OperatorsBlocks.prototype.mod = function (args) {
+ var n = Cast.toNumber(args.NUM1);
+ var modulus = Cast.toNumber(args.NUM2);
+ var result = n % modulus;
+ // Scratch mod is kept positive.
+ if (result / modulus < 0) result += modulus;
+ return result;
+};
+
+Scratch3OperatorsBlocks.prototype.round = function (args) {
+ return Math.round(Cast.toNumber(args.NUM));
+};
+
+Scratch3OperatorsBlocks.prototype.mathop = function (args) {
+ var operator = Cast.toString(args.OPERATOR).toLowerCase();
+ var n = Cast.toNumber(args.NUM);
+ switch (operator) {
+ case 'abs': return Math.abs(n);
+ case 'floor': return Math.floor(n);
+ case 'ceiling': return Math.ceil(n);
+ case 'sqrt': return Math.sqrt(n);
+ case 'sin': return Math.sin((Math.PI * n) / 180);
+ case 'cos': return Math.cos((Math.PI * n) / 180);
+ case 'tan': return Math.tan((Math.PI * n) / 180);
+ case 'asin': return (Math.asin(n) * 180) / Math.PI;
+ case 'acos': return (Math.acos(n) * 180) / Math.PI;
+ case 'atan': return (Math.atan(n) * 180) / Math.PI;
+ case 'ln': return Math.log(n);
+ case 'log': return Math.log(n) / Math.LN10;
+ case 'e ^': return Math.exp(n);
+ case '10 ^': return Math.pow(10, n);
+ }
+ return 0;
+};
+
+module.exports = Scratch3OperatorsBlocks;
diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js
new file mode 100644
index 000000000..19a6df370
--- /dev/null
+++ b/src/blocks/scratch3_procedures.js
@@ -0,0 +1,44 @@
+function Scratch3ProcedureBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
+ return {
+ 'procedures_defnoreturn': this.defNoReturn,
+ 'procedures_callnoreturn': this.callNoReturn,
+ 'procedures_param': this.param
+ };
+};
+
+Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
+ // No-op: execute the blocks.
+};
+
+Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
+ if (!util.stackFrame.executed) {
+ var procedureCode = args.mutation.proccode;
+ var paramNames = util.getProcedureParamNames(procedureCode);
+ for (var i = 0; i < paramNames.length; i++) {
+ if (args.hasOwnProperty('input' + i)) {
+ util.pushParam(paramNames[i], args['input' + i]);
+ }
+ }
+ util.stackFrame.executed = true;
+ util.startProcedure(procedureCode);
+ }
+};
+
+Scratch3ProcedureBlocks.prototype.param = function (args, util) {
+ var value = util.getParam(args.mutation.paramname);
+ return value;
+};
+
+module.exports = Scratch3ProcedureBlocks;
diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js
new file mode 100644
index 000000000..293273936
--- /dev/null
+++ b/src/blocks/scratch3_sensing.js
@@ -0,0 +1,128 @@
+var Cast = require('../util/cast');
+
+function Scratch3SensingBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3SensingBlocks.prototype.getPrimitives = function() {
+ return {
+ 'sensing_touchingobject': this.touchingObject,
+ 'sensing_touchingcolor': this.touchingColor,
+ 'sensing_coloristouchingcolor': this.colorTouchingColor,
+ 'sensing_distanceto': this.distanceTo,
+ 'sensing_timer': this.getTimer,
+ 'sensing_resettimer': this.resetTimer,
+ 'sensing_mousex': this.getMouseX,
+ 'sensing_mousey': this.getMouseY,
+ 'sensing_mousedown': this.getMouseDown,
+ 'sensing_keypressed': this.getKeyPressed,
+ 'sensing_current': this.current,
+ 'sensing_dayssince2000': this.daysSince2000
+ };
+};
+
+Scratch3SensingBlocks.prototype.touchingObject = function (args, util) {
+ var requestedObject = args.TOUCHINGOBJECTMENU;
+ if (requestedObject == '_mouse_') {
+ var mouseX = util.ioQuery('mouse', 'getX');
+ var mouseY = util.ioQuery('mouse', 'getY');
+ return util.target.isTouchingPoint(mouseX, mouseY);
+ } else if (requestedObject == '_edge_') {
+ return util.target.isTouchingEdge();
+ } else {
+ return util.target.isTouchingSprite(requestedObject);
+ }
+};
+
+Scratch3SensingBlocks.prototype.touchingColor = function (args, util) {
+ var color = Cast.toRgbColorList(args.COLOR);
+ return util.target.isTouchingColor(color);
+};
+
+Scratch3SensingBlocks.prototype.colorTouchingColor = function (args, util) {
+ var maskColor = Cast.toRgbColorList(args.COLOR);
+ var targetColor = Cast.toRgbColorList(args.COLOR2);
+ return util.target.colorIsTouchingColor(targetColor, maskColor);
+};
+
+Scratch3SensingBlocks.prototype.distanceTo = function (args, util) {
+ if (util.target.isStage) return 10000;
+
+ var targetX = 0;
+ var targetY = 0;
+ if (args.DISTANCETOMENU === '_mouse_') {
+ targetX = util.ioQuery('mouse', 'getX');
+ targetY = util.ioQuery('mouse', 'getY');
+ } else {
+ var distTarget = this.runtime.getSpriteTargetByName(
+ args.DISTANCETOMENU
+ );
+ if (!distTarget) return 10000;
+ targetX = distTarget.x;
+ targetY = distTarget.y;
+ }
+
+ var dx = util.target.x - targetX;
+ var dy = util.target.y - targetY;
+ return Math.sqrt((dx * dx) + (dy * dy));
+};
+
+Scratch3SensingBlocks.prototype.getTimer = function (args, util) {
+ return util.ioQuery('clock', 'projectTimer');
+};
+
+Scratch3SensingBlocks.prototype.resetTimer = function (args, util) {
+ util.ioQuery('clock', 'resetProjectTimer');
+};
+
+Scratch3SensingBlocks.prototype.getMouseX = function (args, util) {
+ return util.ioQuery('mouse', 'getX');
+};
+
+Scratch3SensingBlocks.prototype.getMouseY = function (args, util) {
+ return util.ioQuery('mouse', 'getY');
+};
+
+Scratch3SensingBlocks.prototype.getMouseDown = function (args, util) {
+ return util.ioQuery('mouse', 'getIsDown');
+};
+
+Scratch3SensingBlocks.prototype.current = function (args) {
+ var menuOption = Cast.toString(args.CURRENTMENU).toLowerCase();
+ var date = new Date();
+ switch (menuOption) {
+ case 'year': return date.getFullYear();
+ case 'month': return date.getMonth() + 1; // getMonth is zero-based
+ case 'date': return date.getDate();
+ case 'dayofweek': return date.getDay() + 1; // getDay is zero-based, Sun=0
+ case 'hour': return date.getHours();
+ case 'minute': return date.getMinutes();
+ case 'second': return date.getSeconds();
+ }
+ return 0;
+};
+
+Scratch3SensingBlocks.prototype.getKeyPressed = function (args, util) {
+ return util.ioQuery('keyboard', 'getKeyIsDown', args.KEY_OPTION);
+};
+
+Scratch3SensingBlocks.prototype.daysSince2000 = function()
+{
+ var msPerDay = 24 * 60 * 60 * 1000;
+ var start = new Date(2000, 1-1, 1);
+ var today = new Date();
+ var dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset();
+ var mSecsSinceStart = today.valueOf() - start.valueOf();
+ mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
+ return mSecsSinceStart / msPerDay;
+};
+
+module.exports = Scratch3SensingBlocks;
diff --git a/src/blocks/wedo2.js b/src/blocks/wedo2.js
deleted file mode 100644
index 86c73de07..000000000
--- a/src/blocks/wedo2.js
+++ /dev/null
@@ -1,154 +0,0 @@
-
-var YieldTimers = require('../util/yieldtimers.js');
-
-function WeDo2Blocks(runtime) {
- /**
- * The runtime instantiating this block package.
- * @type {Runtime}
- */
- this.runtime = runtime;
-
- /**
- * Current motor speed, as a percentage (100 = full speed).
- * @type {number}
- * @private
- */
- this._motorSpeed = 100;
-
- /**
- * The timeout ID for a pending motor action.
- * @type {?int}
- * @private
- */
- this._motorTimeout = null;
-}
-
-/**
- * Retrieve the block primitives implemented by this package.
- * @return {Object.} Mapping of opcode to Function.
- */
-WeDo2Blocks.prototype.getPrimitives = function() {
- return {
- 'wedo_motorclockwise': this.motorClockwise,
- 'wedo_motorcounterclockwise': this.motorCounterClockwise,
- 'wedo_motorspeed': this.motorSpeed,
- 'wedo_setcolor': this.setColor,
- 'wedo_whendistanceclose': this.whenDistanceClose,
- 'wedo_whentilt': this.whenTilt
- };
-};
-
-/**
- * Clamp a value between a minimum and maximum value.
- * @todo move this to a common utility class.
- * @param val The value to clamp.
- * @param min The minimum return value.
- * @param max The maximum return value.
- * @returns {number} The clamped value.
- * @private
- */
-WeDo2Blocks.prototype._clamp = function(val, min, max) {
- return Math.max(min, Math.min(val, max));
-};
-
-/**
- * Common implementation for motor blocks.
- * @param direction The direction to turn ('left' or 'right').
- * @param durationSeconds The number of seconds to run.
- * @param util The util instance to use for yielding and finishing.
- * @private
- */
-WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
- if (this._motorTimeout > 0) {
- // @todo maybe this should go through util
- YieldTimers.resolve(this._motorTimeout);
- this._motorTimeout = null;
- }
- if (window.native) {
- window.native.motorRun(direction, this._motorSpeed);
- }
-
- var instance = this;
- var myTimeout = this._motorTimeout = util.timeout(function() {
- if (instance._motorTimeout == myTimeout) {
- instance._motorTimeout = null;
- }
- if (window.native) {
- window.native.motorStop();
- }
- util.done();
- }, 1000 * durationSeconds);
-
- util.yield();
-};
-
-WeDo2Blocks.prototype.motorClockwise = function(argValues, util) {
- this._motorOnFor('right', parseFloat(argValues[0]), util);
-};
-
-WeDo2Blocks.prototype.motorCounterClockwise = function(argValues, util) {
- this._motorOnFor('left', parseFloat(argValues[0]), util);
-};
-
-WeDo2Blocks.prototype.motorSpeed = function(argValues) {
- var speed = argValues[0];
- switch (speed) {
- case 'slow':
- this._motorSpeed = 20;
- break;
- case 'medium':
- this._motorSpeed = 50;
- break;
- case 'fast':
- this._motorSpeed = 100;
- break;
- }
-};
-
-/**
- * Convert a color name to a WeDo color index.
- * Supports 'mystery' for a random hue.
- * @param colorName The color to retrieve.
- * @returns {number} The WeDo color index.
- * @private
- */
-WeDo2Blocks.prototype._getColor = function(colorName) {
- var colors = {
- 'yellow': 7,
- 'orange': 8,
- 'coral': 9,
- 'magenta': 1,
- 'purple': 2,
- 'blue': 3,
- 'green': 6,
- 'white': 10
- };
-
- if (colorName == 'mystery') {
- return Math.floor((Math.random() * 10) + 1);
- }
-
- return colors[colorName];
-};
-
-WeDo2Blocks.prototype.setColor = function(argValues, util) {
- if (window.native) {
- var colorIndex = this._getColor(argValues[0]);
- window.native.setLedColor(colorIndex);
- }
- // Pause for quarter second
- util.yield();
- util.timeout(function() {
- util.done();
- }, 250);
-};
-
-WeDo2Blocks.prototype.whenDistanceClose = function() {
- console.log('Running: wedo_whendistanceclose');
-};
-
-WeDo2Blocks.prototype.whenTilt = function() {
- console.log('Running: wedo_whentilt');
-};
-
-module.exports = WeDo2Blocks;
diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 222273c5c..327aae5e3 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -1,87 +1,147 @@
+var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2');
-var memoize = require('memoizee');
-var parseDOM = memoize(html.parseDOM, {
- length: 1,
- resolvers: [String],
- max: 200
-});
/**
* Adapter between block creation events and block representation which can be
* used by the Scratch runtime.
- *
- * @param {Object} `Blockly.events.create`
- *
- * @return {Object}
+ * @param {Object} e `Blockly.events.create`
+ * @return {Array.} List of blocks from this CREATE event.
*/
module.exports = function (e) {
// Validate input
if (typeof e !== 'object') return;
- if (typeof e.blockId !== 'string') return;
if (typeof e.xml !== 'object') return;
- // Storage object
- var obj = {
- id: e.blockId,
- opcode: null,
- next: null,
- fields: {}
- };
-
- // Set opcode
- if (typeof e.xml.attributes === 'object') {
- obj.opcode = e.xml.attributes.type.value;
- }
-
- // Extract fields from event's `innerHTML`
- if (typeof e.xml.innerHTML !== 'string') return obj;
- if (e.xml.innerHTML === '') return obj;
- obj.fields = extract(parseDOM(e.xml.innerHTML));
-
- return obj;
+ return domToBlocks(html.parseDOM(e.xml.outerHTML));
};
/**
- * Extracts fields from a block's innerHTML.
- * @todo Extend this to support vertical grammar / nested blocks.
- *
- * @param {Object} DOM representation of block's innerHTML
- *
- * @return {Object}
+ * Convert outer blocks DOM from a Blockly CREATE event
+ * to a usable form for the Scratch runtime.
+ * This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
+ * @param {Element} blocksDOM DOM tree for this event.
+ * @return {Array.} Usable list of blocks from this CREATE event.
*/
-function extract (dom) {
- // Storage object
- var fields = {};
-
- // Field
- var field = dom[0];
- var fieldName = field.attribs.name;
- fields[fieldName] = {
- name: fieldName,
- value: null,
- blocks: {}
- };
-
- // Shadow block
- var shadow = field.children[0];
- var shadowId = shadow.attribs.id;
- var shadowOpcode = shadow.attribs.type;
- fields[fieldName].blocks[shadowId] = {
- id: shadowId,
- opcode: shadowOpcode,
- next: null,
- fields: {}
- };
-
- // Primitive
- var primitive = shadow.children[0];
- var primitiveName = primitive.attribs.name;
- var primitiveValue = primitive.children[0].data;
- fields[fieldName].blocks[shadowId].fields[primitiveName] = {
- name: primitiveName,
- value: primitiveValue,
- blocks: null
- };
-
- return fields;
+function domToBlocks (blocksDOM) {
+ // At this level, there could be multiple blocks adjacent in the DOM tree.
+ var blocks = {};
+ for (var i = 0; i < blocksDOM.length; i++) {
+ var block = blocksDOM[i];
+ if (!block.name || !block.attribs) {
+ continue;
+ }
+ var tagName = block.name.toLowerCase();
+ if (tagName == 'block' || tagName == 'shadow') {
+ domToBlock(block, blocks, true, null);
+ }
+ }
+ // Flatten blocks object into a list.
+ var blocksList = [];
+ for (var b in blocks) {
+ blocksList.push(blocks[b]);
+ }
+ return blocksList;
+}
+
+/**
+ * Convert and an individual block DOM to the representation tree.
+ * Based on Blockly's `domToBlockHeadless_`.
+ * @param {Element} blockDOM DOM tree for an individual block.
+ * @param {Object} blocks Collection of blocks to add to.
+ * @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
+ * @param {?string} parent Parent block ID.
+ */
+function domToBlock (blockDOM, blocks, isTopBlock, parent) {
+ // Block skeleton.
+ var block = {
+ id: blockDOM.attribs.id, // Block ID
+ opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
+ inputs: {}, // Inputs to this block and the blocks they point to.
+ fields: {}, // Fields on this block and their values.
+ next: null, // Next block in the stack, if one exists.
+ topLevel: isTopBlock, // If this block starts a stack.
+ parent: parent, // Parent block ID, if available.
+ shadow: blockDOM.name == 'shadow', // If this represents a shadow/slot.
+ x: blockDOM.attribs.x, // X position of script, if top-level.
+ y: blockDOM.attribs.y // Y position of script, if top-level.
+ };
+
+ // Add the block to the representation tree.
+ blocks[block.id] = block;
+
+ // Process XML children and find enclosed blocks, fields, etc.
+ for (var i = 0; i < blockDOM.children.length; i++) {
+ var xmlChild = blockDOM.children[i];
+ // Enclosed blocks and shadows
+ var childBlockNode = null;
+ var childShadowNode = null;
+ for (var j = 0; j < xmlChild.children.length; j++) {
+ var grandChildNode = xmlChild.children[j];
+ if (!grandChildNode.name) {
+ // Non-XML tag node.
+ continue;
+ }
+ var grandChildNodeName = grandChildNode.name.toLowerCase();
+ if (grandChildNodeName == 'block') {
+ childBlockNode = grandChildNode;
+ } else if (grandChildNodeName == 'shadow') {
+ childShadowNode = grandChildNode;
+ }
+ }
+
+ // Use shadow block only if there's no real block node.
+ if (!childBlockNode && childShadowNode) {
+ childBlockNode = childShadowNode;
+ }
+
+ // Not all Blockly-type blocks are handled here,
+ // as we won't be using all of them for Scratch.
+ switch (xmlChild.name.toLowerCase()) {
+ case 'field':
+ // Add the field to this block.
+ var fieldName = xmlChild.attribs.name;
+ var fieldData = '';
+ if (xmlChild.children.length > 0 && xmlChild.children[0].data) {
+ fieldData = xmlChild.children[0].data;
+ } else {
+ // If the child of the field with a data property
+ // doesn't exist, set the data to an empty string.
+ fieldData = '';
+ }
+ block.fields[fieldName] = {
+ name: fieldName,
+ value: fieldData
+ };
+ break;
+ case 'value':
+ case 'statement':
+ // Recursively generate block structure for input block.
+ domToBlock(childBlockNode, blocks, false, block.id);
+ if (childShadowNode && childBlockNode != childShadowNode) {
+ // Also generate the shadow block.
+ domToBlock(childShadowNode, blocks, false, block.id);
+ }
+ // Link this block's input to the child block.
+ var inputName = xmlChild.attribs.name;
+ block.inputs[inputName] = {
+ name: inputName,
+ block: childBlockNode.attribs.id,
+ shadow: childShadowNode ? childShadowNode.attribs.id : null
+ };
+ break;
+ case 'next':
+ if (!childBlockNode || !childBlockNode.attribs) {
+ // Invalid child block.
+ continue;
+ }
+ // Recursively generate block structure for next block.
+ domToBlock(childBlockNode, blocks, false, block.id);
+ // Link next block to this block.
+ block.next = childBlockNode.attribs.id;
+ break;
+ case 'mutation':
+ block.mutation = mutationAdapter(xmlChild);
+ break;
+ }
+ }
}
diff --git a/src/engine/blocks.js b/src/engine/blocks.js
new file mode 100644
index 000000000..0e89025e9
--- /dev/null
+++ b/src/engine/blocks.js
@@ -0,0 +1,494 @@
+var adapter = require('./adapter');
+var mutationAdapter = require('./mutation-adapter');
+var xmlEscape = require('../util/xml-escape');
+
+/**
+ * @fileoverview
+ * Store and mutate the VM block representation,
+ * and handle updates from Scratch Blocks events.
+ */
+
+function Blocks () {
+ /**
+ * All blocks in the workspace.
+ * Keys are block IDs, values are metadata about the block.
+ * @type {Object.}
+ */
+ this._blocks = {};
+
+ /**
+ * All top-level scripts in the workspace.
+ * A list of block IDs that represent scripts (i.e., first block in script).
+ * @type {Array.}
+ */
+ this._scripts = [];
+}
+
+/**
+ * Blockly inputs that represent statements/branch.
+ * are prefixed with this string.
+ * @const{string}
+ */
+Blocks.BRANCH_INPUT_PREFIX = 'SUBSTACK';
+
+/**
+ * Provide an object with metadata for the requested block ID.
+ * @param {!string} blockId ID of block we have stored.
+ * @return {?Object} Metadata about the block, if it exists.
+ */
+Blocks.prototype.getBlock = function (blockId) {
+ return this._blocks[blockId];
+};
+
+/**
+ * Get all known top-level blocks that start scripts.
+ * @return {Array.} List of block IDs.
+ */
+Blocks.prototype.getScripts = function () {
+ return this._scripts;
+};
+
+ /**
+ * Get the next block for a particular block
+ * @param {?string} id ID of block to get the next block for
+ * @return {?string} ID of next block in the sequence
+ */
+Blocks.prototype.getNextBlock = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ return this._blocks[id].next;
+};
+
+/**
+ * Get the branch for a particular C-shaped block.
+ * @param {?string} id ID for block to get the branch for.
+ * @param {?number} branchNum Which branch to select (e.g. for if-else).
+ * @return {?string} ID of block in the branch.
+ */
+Blocks.prototype.getBranch = function (id, branchNum) {
+ var block = this._blocks[id];
+ if (typeof block === 'undefined') return null;
+ if (!branchNum) branchNum = 1;
+
+ var inputName = Blocks.BRANCH_INPUT_PREFIX;
+ if (branchNum > 1) {
+ inputName += branchNum;
+ }
+
+ // Empty C-block?
+ if (!(inputName in block.inputs)) return null;
+ return block.inputs[inputName].block;
+};
+
+/**
+ * Get the opcode for a particular block
+ * @param {?string} id ID of block to query
+ * @return {?string} the opcode corresponding to that block
+ */
+Blocks.prototype.getOpcode = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ return this._blocks[id].opcode;
+};
+
+/**
+ * Get all fields and their values for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} All fields and their values.
+ */
+Blocks.prototype.getFields = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ return this._blocks[id].fields;
+};
+
+/**
+ * Get all non-branch inputs for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} All non-branch inputs and their associated blocks.
+ */
+Blocks.prototype.getInputs = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ var inputs = {};
+ for (var input in this._blocks[id].inputs) {
+ // Ignore blocks prefixed with branch prefix.
+ if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length)
+ != Blocks.BRANCH_INPUT_PREFIX) {
+ inputs[input] = this._blocks[id].inputs[input];
+ }
+ }
+ return inputs;
+};
+
+/**
+ * Get mutation data for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} Mutation for the block.
+ */
+Blocks.prototype.getMutation = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ return this._blocks[id].mutation;
+};
+
+/**
+ * Get the top-level script for a given block.
+ * @param {?string} id ID of block to query.
+ * @return {?string} ID of top-level script block.
+ */
+Blocks.prototype.getTopLevelScript = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ var block = this._blocks[id];
+ while (block.parent !== null) {
+ block = this._blocks[block.parent];
+ }
+ return block.id;
+};
+
+/**
+ * Get the procedure definition for a given name.
+ * @param {?string} name Name of procedure to query.
+ * @return {?string} ID of procedure definition.
+ */
+Blocks.prototype.getProcedureDefinition = function (name) {
+ for (var id in this._blocks) {
+ var block = this._blocks[id];
+ if ((block.opcode == 'procedures_defnoreturn' ||
+ block.opcode == 'procedures_defreturn') &&
+ block.mutation.proccode == name) {
+ return id;
+ }
+ }
+ return null;
+};
+
+/**
+ * Get the procedure definition for a given name.
+ * @param {?string} name Name of procedure to query.
+ * @return {?string} ID of procedure definition.
+ */
+Blocks.prototype.getProcedureParamNames = function (name) {
+ for (var id in this._blocks) {
+ var block = this._blocks[id];
+ if ((block.opcode == 'procedures_defnoreturn' ||
+ block.opcode == 'procedures_defreturn') &&
+ block.mutation.proccode == name) {
+ return JSON.parse(block.mutation.argumentnames);
+ }
+ }
+ return null;
+};
+
+// ---------------------------------------------------------------------
+
+/**
+ * Create event listener for blocks. Handles validation and serves as a generic
+ * adapter between the blocks and the runtime interface.
+ * @param {Object} e Blockly "block" event
+ * @param {?Runtime} opt_runtime Optional runtime to forward click events to.
+ */
+
+Blocks.prototype.blocklyListen = function (e, opt_runtime) {
+ // Validate event
+ if (typeof e !== 'object') return;
+ if (typeof e.blockId !== 'string') return;
+
+ // UI event: clicked scripts toggle in the runtime.
+ if (e.element === 'stackclick') {
+ if (opt_runtime) {
+ opt_runtime.toggleScript(e.blockId);
+ }
+ return;
+ }
+
+ // Block create/update/destroy
+ switch (e.type) {
+ case 'create':
+ var newBlocks = adapter(e);
+ // A create event can create many blocks. Add them all.
+ for (var i = 0; i < newBlocks.length; i++) {
+ this.createBlock(newBlocks[i]);
+ }
+ break;
+ case 'change':
+ this.changeBlock({
+ id: e.blockId,
+ element: e.element,
+ name: e.name,
+ value: e.newValue
+ });
+ break;
+ case 'move':
+ this.moveBlock({
+ id: e.blockId,
+ oldParent: e.oldParentId,
+ oldInput: e.oldInputName,
+ newParent: e.newParentId,
+ newInput: e.newInputName,
+ newCoordinate: e.newCoordinate
+ });
+ break;
+ case 'delete':
+ // Don't accept delete events for missing blocks,
+ // or shadow blocks being obscured.
+ if (!this._blocks.hasOwnProperty(e.blockId) ||
+ this._blocks[e.blockId].shadow) {
+ return;
+ }
+ // Inform any runtime to forget about glows on this script.
+ if (opt_runtime && this._blocks[e.blockId].topLevel) {
+ opt_runtime.quietGlow(e.blockId);
+ }
+ this.deleteBlock({
+ id: e.blockId
+ });
+ break;
+ }
+};
+
+// ---------------------------------------------------------------------
+
+/**
+ * Block management: create blocks and scripts from a `create` event
+ * @param {!Object} block Blockly create event to be processed
+ */
+Blocks.prototype.createBlock = function (block) {
+ // Does the block already exist?
+ // Could happen, e.g., for an unobscured shadow.
+ if (this._blocks.hasOwnProperty(block.id)) {
+ return;
+ }
+ // Create new block.
+ this._blocks[block.id] = block;
+ // Push block id to scripts array.
+ // Blocks are added as a top-level stack if they are marked as a top-block
+ // (if they were top-level XML in the event).
+ if (block.topLevel) {
+ this._addScript(block.id);
+ }
+};
+
+/**
+ * Block management: change block field values
+ * @param {!Object} args Blockly change event to be processed
+ */
+Blocks.prototype.changeBlock = function (args) {
+ // Validate
+ if (args.element !== 'field' && args.element !== 'mutation') return;
+ if (typeof this._blocks[args.id] === 'undefined') return;
+
+ if (args.element == 'field') {
+ // Update block value
+ if (!this._blocks[args.id].fields[args.name]) return;
+ this._blocks[args.id].fields[args.name].value = args.value;
+ } else if (args.element == 'mutation') {
+ this._blocks[args.id].mutation = mutationAdapter(args.value);
+ }
+};
+
+/**
+ * Block management: move blocks from parent to parent
+ * @param {!Object} e Blockly move event to be processed
+ */
+Blocks.prototype.moveBlock = function (e) {
+ if (!this._blocks.hasOwnProperty(e.id)) {
+ return;
+ }
+
+ // Move coordinate changes.
+ if (e.newCoordinate) {
+ this._blocks[e.id].x = e.newCoordinate.x;
+ this._blocks[e.id].y = e.newCoordinate.y;
+ }
+
+ // Remove from any old parent.
+ if (e.oldParent !== undefined) {
+ var oldParent = this._blocks[e.oldParent];
+ if (e.oldInput !== undefined &&
+ oldParent.inputs[e.oldInput].block === e.id) {
+ // This block was connected to the old parent's input.
+ oldParent.inputs[e.oldInput].block = null;
+ } else if (oldParent.next === e.id) {
+ // This block was connected to the old parent's next connection.
+ oldParent.next = null;
+ }
+ this._blocks[e.id].parent = null;
+ }
+
+ // Has the block become a top-level block?
+ if (e.newParent === undefined) {
+ this._addScript(e.id);
+ } else {
+ // Remove script, if one exists.
+ this._deleteScript(e.id);
+ // Otherwise, try to connect it in its new place.
+ if (e.newInput !== undefined) {
+ // Moved to the new parent's input.
+ // Don't obscure the shadow block.
+ var oldShadow = null;
+ if (this._blocks[e.newParent].inputs.hasOwnProperty(e.newInput)) {
+ oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
+ }
+ this._blocks[e.newParent].inputs[e.newInput] = {
+ name: e.newInput,
+ block: e.id,
+ shadow: oldShadow
+ };
+ } else {
+ // Moved to the new parent's next connection.
+ this._blocks[e.newParent].next = e.id;
+ }
+ this._blocks[e.id].parent = e.newParent;
+ }
+};
+
+/**
+ * Block management: delete blocks and their associated scripts.
+ * @param {!Object} e Blockly delete event to be processed.
+ */
+Blocks.prototype.deleteBlock = function (e) {
+ // @todo In runtime, stop threads running on this script.
+
+ // Get block
+ var block = this._blocks[e.id];
+
+ // Delete children
+ if (block.next !== null) {
+ this.deleteBlock({id: block.next});
+ }
+
+ // Delete inputs (including branches)
+ for (var input in block.inputs) {
+ // If it's null, the block in this input moved away.
+ if (block.inputs[input].block !== null) {
+ this.deleteBlock({id: block.inputs[input].block});
+ }
+ // Delete obscured shadow blocks.
+ if (block.inputs[input].shadow !== null &&
+ block.inputs[input].shadow !== block.inputs[input].block) {
+ this.deleteBlock({id: block.inputs[input].shadow});
+ }
+ }
+
+ // Delete any script starting with this block.
+ this._deleteScript(e.id);
+
+ // Delete block itself.
+ delete this._blocks[e.id];
+};
+
+// ---------------------------------------------------------------------
+
+/**
+ * Encode all of `this._blocks` as an XML string usable
+ * by a Blockly/scratch-blocks workspace.
+ * @return {string} String of XML representing this object's blocks.
+ */
+Blocks.prototype.toXML = function () {
+ var xmlString = '';
+ for (var i = 0; i < this._scripts.length; i++) {
+ xmlString += this.blockToXML(this._scripts[i]);
+ }
+ return xmlString + ' ';
+};
+
+/**
+ * Recursively encode an individual block and its children
+ * into a Blockly/scratch-blocks XML string.
+ * @param {!string} blockId ID of block to encode.
+ * @return {string} String of XML representing this block and any children.
+ */
+Blocks.prototype.blockToXML = function (blockId) {
+ var block = this._blocks[blockId];
+ // Encode properties of this block.
+ var tagName = (block.shadow) ? 'shadow' : 'block';
+ var xy = (block.topLevel) ?
+ ' x="' + block.x +'"' + ' y="' + block.y +'"' :
+ '';
+ var xmlString = '';
+ xmlString += '<' + tagName +
+ ' id="' + block.id + '"' +
+ ' type="' + block.opcode + '"' +
+ xy +
+ '>';
+ // Add any mutation. Must come before inputs.
+ if (block.mutation) {
+ xmlString += this.mutationToXML(block.mutation);
+ }
+ // Add any inputs on this block.
+ for (var input in block.inputs) {
+ var blockInput = block.inputs[input];
+ // Only encode a value tag if the value input is occupied.
+ if (blockInput.block || blockInput.shadow) {
+ xmlString += '';
+ if (blockInput.block) {
+ xmlString += this.blockToXML(blockInput.block);
+ }
+ if (blockInput.shadow && blockInput.shadow != blockInput.block) {
+ // Obscured shadow.
+ xmlString += this.blockToXML(blockInput.shadow);
+ }
+ xmlString += ' ';
+ }
+ }
+ // Add any fields on this block.
+ for (var field in block.fields) {
+ var blockField = block.fields[field];
+ var value = blockField.value;
+ if (typeof value === 'string') {
+ value = xmlEscape(blockField.value);
+ }
+ xmlString += '' +
+ value + ' ';
+ }
+ // Add blocks connected to the next connection.
+ if (block.next) {
+ xmlString += '' + this.blockToXML(block.next) + ' ';
+ }
+ xmlString += '' + tagName + '>';
+ return xmlString;
+};
+
+/**
+ * Recursively encode a mutation object to XML.
+ * @param {!Object} mutation Object representing a mutation.
+ * @return {string} XML string representing a mutation.
+ */
+Blocks.prototype.mutationToXML = function (mutation) {
+ var mutationString = '<' + mutation.tagName;
+ for (var prop in mutation) {
+ if (prop == 'children' || prop == 'tagName') continue;
+ var mutationValue = (typeof mutation[prop] === 'string') ?
+ xmlEscape(mutation[prop]) : mutation[prop];
+ mutationString += ' ' + prop + '="' + mutationValue + '"';
+ }
+ mutationString += '>';
+ for (var i = 0; i < mutation.children.length; i++) {
+ mutationString += this.mutationToXML(mutation.children[i]);
+ }
+ mutationString += '' + mutation.tagName + '>';
+ return mutationString;
+};
+
+// ---------------------------------------------------------------------
+
+/**
+ * Helper to add a stack to `this._scripts`.
+ * @param {?string} topBlockId ID of block that starts the script.
+ */
+Blocks.prototype._addScript = function (topBlockId) {
+ var i = this._scripts.indexOf(topBlockId);
+ if (i > -1) return; // Already in scripts.
+ this._scripts.push(topBlockId);
+ // Update `topLevel` property on the top block.
+ this._blocks[topBlockId].topLevel = true;
+};
+
+/**
+ * Helper to remove a script from `this._scripts`.
+ * @param {?string} topBlockId ID of block that starts the script.
+ */
+Blocks.prototype._deleteScript = function (topBlockId) {
+ var i = this._scripts.indexOf(topBlockId);
+ if (i > -1) this._scripts.splice(i, 1);
+ // Update `topLevel` property on the top block.
+ if (this._blocks[topBlockId]) this._blocks[topBlockId].topLevel = false;
+};
+
+module.exports = Blocks;
diff --git a/src/engine/execute.js b/src/engine/execute.js
new file mode 100644
index 000000000..6ef0599de
--- /dev/null
+++ b/src/engine/execute.js
@@ -0,0 +1,248 @@
+var Thread = require('./thread');
+
+/**
+ * Utility function to determine if a value is a Promise.
+ * @param {*} value Value to check for a Promise.
+ * @return {Boolean} True if the value appears to be a Promise.
+ */
+var isPromise = function (value) {
+ return value && value.then && typeof value.then === 'function';
+};
+
+/**
+ * Execute a block.
+ * @param {!Sequencer} sequencer Which sequencer is executing.
+ * @param {!Thread} thread Thread which to read and execute.
+ */
+var execute = function (sequencer, thread) {
+ var runtime = sequencer.runtime;
+ var target = thread.target;
+
+ // Current block to execute is the one on the top of the stack.
+ var currentBlockId = thread.peekStack();
+ var currentStackFrame = thread.peekStackFrame();
+
+ // Check where the block lives: target blocks or flyout blocks.
+ var targetHasBlock = (
+ typeof target.blocks.getBlock(currentBlockId) !== 'undefined'
+ );
+ var flyoutHasBlock = (
+ typeof runtime.flyoutBlocks.getBlock(currentBlockId) !== 'undefined'
+ );
+
+ // Stop if block or target no longer exists.
+ if (!target || (!targetHasBlock && !flyoutHasBlock)) {
+ // No block found: stop the thread; script no longer exists.
+ sequencer.retireThread(thread);
+ return;
+ }
+
+ // Query info about the block.
+ var blockContainer = null;
+ if (targetHasBlock) {
+ blockContainer = target.blocks;
+ } else {
+ blockContainer = runtime.flyoutBlocks;
+ }
+ var opcode = blockContainer.getOpcode(currentBlockId);
+ var fields = blockContainer.getFields(currentBlockId);
+ var inputs = blockContainer.getInputs(currentBlockId);
+ var blockFunction = runtime.getOpcodeFunction(opcode);
+ var isHat = runtime.getIsHat(opcode);
+
+
+ if (!opcode) {
+ console.warn('Could not get opcode for block: ' + currentBlockId);
+ return;
+ }
+
+ /**
+ * Handle any reported value from the primitive, either directly returned
+ * or after a promise resolves.
+ * @param {*} resolvedValue Value eventually returned from the primitive.
+ */
+ var handleReport = function (resolvedValue) {
+ thread.pushReportedValue(resolvedValue);
+ if (isHat) {
+ // Hat predicate was evaluated.
+ if (runtime.getIsEdgeActivatedHat(opcode)) {
+ // If this is an edge-activated hat, only proceed if
+ // the value is true and used to be false.
+ var oldEdgeValue = runtime.updateEdgeActivatedValue(
+ currentBlockId,
+ resolvedValue
+ );
+ var edgeWasActivated = !oldEdgeValue && resolvedValue;
+ if (!edgeWasActivated) {
+ sequencer.retireThread(thread);
+ }
+ } else {
+ // Not an edge-activated hat: retire the thread
+ // if predicate was false.
+ if (!resolvedValue) {
+ sequencer.retireThread(thread);
+ }
+ }
+ } else {
+ // In a non-hat, report the value visually if necessary if
+ // at the top of the thread stack.
+ if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) {
+ runtime.visualReport(currentBlockId, resolvedValue);
+ }
+ // Finished any yields.
+ thread.status = Thread.STATUS_RUNNING;
+ }
+ };
+
+ // Hats and single-field shadows are implemented slightly differently
+ // from regular blocks.
+ // For hats: if they have an associated block function,
+ // it's treated as a predicate; if not, execution will proceed as a no-op.
+ // For single-field shadows: If the block has a single field, and no inputs,
+ // immediately return the value of the field.
+ if (!blockFunction) {
+ if (isHat) {
+ // Skip through the block (hat with no predicate).
+ return;
+ } else {
+ if (Object.keys(fields).length == 1 &&
+ Object.keys(inputs).length == 0) {
+ // One field and no inputs - treat as arg.
+ for (var fieldKey in fields) { // One iteration.
+ handleReport(fields[fieldKey].value);
+ }
+ } else {
+ console.warn('Could not get implementation for opcode: ' +
+ opcode);
+ }
+ thread.requestScriptGlowInFrame = true;
+ return;
+ }
+ }
+
+ // Generate values for arguments (inputs).
+ var argValues = {};
+
+ // Add all fields on this block to the argValues.
+ for (var fieldName in fields) {
+ argValues[fieldName] = fields[fieldName].value;
+ }
+
+ // Recursively evaluate input blocks.
+ for (var inputName in inputs) {
+ var input = inputs[inputName];
+ var inputBlockId = input.block;
+ // Is there no value for this input waiting in the stack frame?
+ if (typeof currentStackFrame.reported[inputName] === 'undefined'
+ && inputBlockId) {
+ // If there's not, we need to evaluate the block.
+ // Push to the stack to evaluate the reporter block.
+ thread.pushStack(inputBlockId);
+ // Save name of input for `Thread.pushReportedValue`.
+ currentStackFrame.waitingReporter = inputName;
+ // Actually execute the block.
+ execute(sequencer, thread);
+ if (thread.status === Thread.STATUS_PROMISE_WAIT) {
+ return;
+ } else {
+ // Execution returned immediately,
+ // and presumably a value was reported, so pop the stack.
+ currentStackFrame.waitingReporter = null;
+ thread.popStack();
+ }
+ }
+ argValues[inputName] = currentStackFrame.reported[inputName];
+ }
+
+ // Add any mutation to args (e.g., for procedures).
+ var mutation = blockContainer.getMutation(currentBlockId);
+ if (mutation) {
+ argValues.mutation = mutation;
+ }
+
+ // If we've gotten this far, all of the input blocks are evaluated,
+ // and `argValues` is fully populated. So, execute the block primitive.
+ // First, clear `currentStackFrame.reported`, so any subsequent execution
+ // (e.g., on return from a branch) gets fresh inputs.
+ currentStackFrame.reported = {};
+
+ var primitiveReportedValue = null;
+ primitiveReportedValue = blockFunction(argValues, {
+ stackFrame: currentStackFrame.executionContext,
+ target: target,
+ yield: function() {
+ thread.status = Thread.STATUS_YIELD;
+ },
+ startBranch: function (branchNum, isLoop) {
+ sequencer.stepToBranch(thread, branchNum, isLoop);
+ },
+ stopAll: function () {
+ runtime.stopAll();
+ },
+ stopOtherTargetThreads: function() {
+ runtime.stopForTarget(target, thread);
+ },
+ stopThread: function() {
+ sequencer.retireThread(thread);
+ },
+ startProcedure: function (procedureCode) {
+ sequencer.stepToProcedure(thread, procedureCode);
+ },
+ getProcedureParamNames: function (procedureCode) {
+ return blockContainer.getProcedureParamNames(procedureCode);
+ },
+ pushParam: function (paramName, paramValue) {
+ thread.pushParam(paramName, paramValue);
+ },
+ getParam: function (paramName) {
+ return thread.getParam(paramName);
+ },
+ startHats: function(requestedHat, opt_matchFields, opt_target) {
+ return (
+ runtime.startHats(requestedHat, opt_matchFields, opt_target)
+ );
+ },
+ ioQuery: function (device, func, args) {
+ // Find the I/O device and execute the query/function call.
+ if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
+ var devObject = runtime.ioDevices[device];
+ return devObject[func].call(devObject, args);
+ }
+ }
+ });
+
+ if (typeof primitiveReportedValue === 'undefined') {
+ // No value reported - potentially a command block.
+ // Edge-activated hats don't request a glow; all commands do.
+ thread.requestScriptGlowInFrame = true;
+ }
+
+ // If it's a promise, wait until promise resolves.
+ if (isPromise(primitiveReportedValue)) {
+ if (thread.status === Thread.STATUS_RUNNING) {
+ // Primitive returned a promise; automatically yield thread.
+ thread.status = Thread.STATUS_PROMISE_WAIT;
+ }
+ // Promise handlers
+ primitiveReportedValue.then(function(resolvedValue) {
+ handleReport(resolvedValue);
+ if (typeof resolvedValue !== 'undefined') {
+ thread.popStack();
+ } else {
+ var popped = thread.popStack();
+ var nextBlockId = thread.target.blocks.getNextBlock(popped);
+ thread.pushStack(nextBlockId);
+ }
+ }, function(rejectionReason) {
+ // Promise rejected: the primitive had some error.
+ // Log it and proceed.
+ console.warn('Primitive rejected promise: ', rejectionReason);
+ thread.status = Thread.STATUS_RUNNING;
+ thread.popStack();
+ });
+ } else if (thread.status === Thread.STATUS_RUNNING) {
+ handleReport(primitiveReportedValue);
+ }
+};
+
+module.exports = execute;
diff --git a/src/engine/list.js b/src/engine/list.js
new file mode 100644
index 000000000..8ef082cde
--- /dev/null
+++ b/src/engine/list.js
@@ -0,0 +1,16 @@
+/**
+ * @fileoverview
+ * Object representing a Scratch list.
+ */
+
+ /**
+ * @param {!string} name Name of the list.
+ * @param {Array} contents Contents of the list, as an array.
+ * @constructor
+ */
+function List (name, contents) {
+ this.name = name;
+ this.contents = contents;
+}
+
+module.exports = List;
diff --git a/src/engine/mutation-adapter.js b/src/engine/mutation-adapter.js
new file mode 100644
index 000000000..12dc123e1
--- /dev/null
+++ b/src/engine/mutation-adapter.js
@@ -0,0 +1,39 @@
+var html = require('htmlparser2');
+
+/**
+ * Adapter between mutator XML or DOM and block representation which can be
+ * used by the Scratch runtime.
+ * @param {(Object|string)} mutation Mutation XML string or DOM.
+ * @return {Object} Object representing the mutation.
+ */
+module.exports = function (mutation) {
+ var mutationParsed;
+ // Check if the mutation is already parsed; if not, parse it.
+ if (typeof mutation === 'object') {
+ mutationParsed = mutation;
+ } else {
+ mutationParsed = html.parseDOM(mutation)[0];
+ }
+ return mutatorTagToObject(mutationParsed);
+};
+
+/**
+ * Convert a part of a mutation DOM to a mutation VM object, recursively.
+ * @param {Object} dom DOM object for mutation tag.
+ * @return {Object} Object representing useful parts of this mutation.
+ */
+function mutatorTagToObject (dom) {
+ var obj = Object.create(null);
+ obj.tagName = dom.name;
+ obj.children = [];
+ for (var prop in dom.attribs) {
+ if (prop == 'xmlns') continue;
+ obj[prop] = dom.attribs[prop];
+ }
+ for (var i = 0; i < dom.children.length; i++) {
+ obj.children.push(
+ mutatorTagToObject(dom.children[i])
+ );
+ }
+ return obj;
+}
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 0507b96b5..14df35528 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -1,34 +1,37 @@
var EventEmitter = require('events');
var Sequencer = require('./sequencer');
+var Blocks = require('./blocks');
var Thread = require('./thread');
var util = require('util');
+// Virtual I/O devices.
+var Clock = require('../io/clock');
+var Keyboard = require('../io/keyboard');
+var Mouse = require('../io/mouse');
+
var defaultBlockPackages = {
- 'scratch3': require('../blocks/scratch3'),
- 'wedo2': require('../blocks/wedo2')
+ 'scratch3_control': require('../blocks/scratch3_control'),
+ 'scratch3_event': require('../blocks/scratch3_event'),
+ 'scratch3_looks': require('../blocks/scratch3_looks'),
+ 'scratch3_motion': require('../blocks/scratch3_motion'),
+ 'scratch3_operators': require('../blocks/scratch3_operators'),
+ 'scratch3_sensing': require('../blocks/scratch3_sensing'),
+ 'scratch3_data': require('../blocks/scratch3_data'),
+ 'scratch3_procedures': require('../blocks/scratch3_procedures')
};
/**
- * Manages blocks, stacks, and the sequencer.
+ * Manages targets, scripts, and the sequencer.
*/
function Runtime () {
// Bind event emitter
EventEmitter.call(this);
- // State for the runtime
/**
- * All blocks in the workspace.
- * Keys are block IDs, values are metadata about the block.
- * @type {Object.}
+ * Target management and storage.
+ * @type {Array.}
*/
- this.blocks = {};
-
- /**
- * All stacks in the workspace.
- * A list of block IDs that represent stacks (first block in stack).
- * @type {Array.}
- */
- this.stacks = [];
+ this.targets = [];
/**
* A list of threads that are currently running in the VM.
@@ -40,161 +43,188 @@ function Runtime () {
/** @type {!Sequencer} */
this.sequencer = new Sequencer(this);
+ /**
+ * Storage container for flyout blocks.
+ * These will execute on `_editingTarget.`
+ * @type {!Blocks}
+ */
+ this.flyoutBlocks = new Blocks();
+
+ /**
+ * Currently known editing target for the VM.
+ * @type {?Target}
+ */
+ this._editingTarget = null;
+
/**
* Map to look up a block primitive's implementation function by its opcode.
* This is a two-step lookup: package name first, then primitive name.
* @type {Object.}
*/
this._primitives = {};
+
+ /**
+ * Map to look up hat blocks' metadata.
+ * Keys are opcode for hat, values are metadata objects.
+ * @type {Object.}
+ */
+ this._hats = {};
+
+ /**
+ * Currently known values for edge-activated hats.
+ * Keys are block ID for the hat; values are the currently known values.
+ * @type {Object.}
+ */
+ this._edgeActivatedHatValues = {};
+
+ /**
+ * A list of script block IDs that were glowing during the previous frame.
+ * @type {!Array.}
+ */
+ this._scriptGlowsPreviousFrame = [];
+
+ /**
+ * A list of block IDs that were glowing during the previous frame.
+ * @type {!Array.}
+ */
+ this._blockGlowsPreviousFrame = [];
+
+ /**
+ * Currently known number of clones, used to enforce clone limit.
+ * @type {number}
+ */
+ this._cloneCounter = 0;
+
+ /**
+ * Whether the project is in "turbo mode."
+ * @type {Boolean}
+ */
+ this.turboMode = false;
+
+ /**
+ * Whether the project is in "pause mode."
+ * @type {Boolean}
+ */
+ this.pauseMode = false;
+
+ /**
+ * Whether the project is in "compatibility mode" (30 TPS).
+ * @type {Boolean}
+ */
+ this.compatibilityMode = false;
+
+ /**
+ * Whether the project is in "single stepping mode."
+ * @type {Boolean}
+ */
+ this.singleStepping = false;
+
+ /**
+ * How fast in ms "single stepping mode" should run, in ms.
+ * Can be updated dynamically.
+ * @type {!number}
+ */
+ this.singleStepInterval = 1000 / 10;
+
+ /**
+ * A reference to the current runtime stepping interval, set
+ * by a `setInterval`.
+ * @type {!number}
+ */
+ this._steppingInterval = null;
+
+ /**
+ * Current length of a step.
+ * Changes as mode switches, and used by the sequencer to calculate
+ * WORK_TIME.
+ * @type {!number}
+ */
+ this.currentStepTime = null;
+
+ /**
+ * Whether any primitive has requested a redraw.
+ * Affects whether `Sequencer.stepThreads` will yield
+ * after stepping each thread.
+ * Reset on every frame.
+ * @type {boolean}
+ */
+ this.redrawRequested = false;
+
+ // Register all given block packages.
this._registerBlockPackages();
+
+ // Register and initialize "IO devices", containers for processing
+ // I/O related data.
+ /** @type {Object.} */
+ this.ioDevices = {
+ 'clock': new Clock(),
+ 'keyboard': new Keyboard(this),
+ 'mouse': new Mouse(this)
+ };
}
-/**
- * Event name for glowing a stack
- * @const {string}
- */
-Runtime.STACK_GLOW_ON = 'STACK_GLOW_ON';
-
-/**
- * Event name for unglowing a stack
- * @const {string}
- */
-Runtime.STACK_GLOW_OFF = 'STACK_GLOW_OFF';
-
-/**
- * Event name for glowing a block
- * @const {string}
- */
-Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
-
-/**
- * Event name for unglowing a block
- * @const {string}
- */
-Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
-
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
- * How rapidly we try to step threads, in ms.
+ * Width of the stage, in pixels.
+ * @const {number}
*/
-Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
+Runtime.STAGE_WIDTH = 480;
/**
- * Block management: create blocks and stacks from a `create` event
- * @param {!Object} block Blockly create event to be processed
+ * Height of the stage, in pixels.
+ * @const {number}
*/
-Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
- // Create new block
- this.blocks[block.id] = block;
-
- // Walk each field and add any shadow blocks
- // @todo Expand this to cover vertical / nested blocks
- for (var i in block.fields) {
- var shadows = block.fields[i].blocks;
- for (var y in shadows) {
- var shadow = shadows[y];
- this.blocks[shadow.id] = shadow;
- }
- }
-
- // Push block id to stacks array. New blocks are always a stack even if only
- // momentary. If the new block is added to an existing stack this stack will
- // be removed by the `moveBlock` method below.
- if (!opt_isFlyoutBlock) {
- this.stacks.push(block.id);
- }
-};
+Runtime.STAGE_HEIGHT = 360;
/**
- * Block management: change block field values
- * @param {!Object} args Blockly change event to be processed
+ * Event name for glowing a script.
+ * @const {string}
*/
-Runtime.prototype.changeBlock = function (args) {
- // Validate
- if (args.element !== 'field') return;
- if (typeof this.blocks[args.id] === 'undefined') return;
- if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
-
- // Update block value
- this.blocks[args.id].fields[args.name].value = args.value;
-};
+Runtime.SCRIPT_GLOW_ON = 'STACK_GLOW_ON';
/**
- * Block management: move blocks from parent to parent
- * @param {!Object} e Blockly move event to be processed
+ * Event name for unglowing a script.
+ * @const {string}
*/
-Runtime.prototype.moveBlock = function (e) {
- var _this = this;
-
- // Block was removed from parent
- if (e.newParent === undefined && e.oldParent !== undefined) {
- // Add stack
- _this.stacks.push(e.id);
-
- // Update old parent
- if (e.oldField === undefined) {
- _this.blocks[e.oldParent].next = null;
- } else {
- delete _this.blocks[e.oldParent].fields[e.oldField];
- }
- } else if (e.newParent !== undefined) {
- // Block was moved to a new parent
- // Either happens because it was previously parentless
- // (e.oldParent === undefined)
- // or because a block was moved in front of it.
-
- // Remove stack
- _this._deleteStack(e.id);
-
- // Update new parent
- if (e.newField === undefined) {
- _this.blocks[e.newParent].next = e.id;
- } else {
- _this.blocks[e.newParent].fields[e.newField] = {
- name: e.newField,
- value: e.id,
- blocks: {}
- };
- }
- }
-};
+Runtime.SCRIPT_GLOW_OFF = 'STACK_GLOW_OFF';
/**
- * Block management: delete blocks and their associated stacks
- * @param {!Object} e Blockly delete event to be processed
+ * Event name for glowing a block.
+ * @const {string}
*/
-Runtime.prototype.deleteBlock = function (e) {
- // @todo Stop threads running on this stack
+Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
- // Get block
- var block = this.blocks[e.id];
+/**
+ * Event name for unglowing a block.
+ * @const {string}
+ */
+Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
- // Delete children
- if (block.next !== null) {
- this.deleteBlock({id: block.next});
- }
+/**
+ * Event name for visual value report.
+ * @const {string}
+ */
+Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
- // Delete substacks and fields
- for (var field in block.fields) {
- if (field === 'SUBSTACK') {
- this.deleteBlock({id: block.fields[field].value});
- } else {
- for (var shadow in block.fields[field].blocks) {
- this.deleteBlock({id: shadow});
- }
- }
- }
+/**
+ * How rapidly we try to step threads by default, in ms.
+ */
+Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
- // Delete stack
- this._deleteStack(e.id);
+/**
+ * In compatibility mode, how rapidly we try to step threads, in ms.
+ */
+Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30;
- // Delete block
- delete this.blocks[e.id];
-};
+/**
+ * How many clones can be created at a time.
+ * @const {number}
+ */
+Runtime.MAX_CLONES = 300;
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
@@ -209,11 +239,23 @@ Runtime.prototype._registerBlockPackages = function () {
if (defaultBlockPackages.hasOwnProperty(packageName)) {
// @todo pass a different runtime depending on package privilege?
var packageObject = new (defaultBlockPackages[packageName])(this);
- var packageContents = packageObject.getPrimitives();
- for (var op in packageContents) {
- if (packageContents.hasOwnProperty(op)) {
- this._primitives[op] =
- packageContents[op].bind(packageObject);
+ // Collect primitives from package.
+ if (packageObject.getPrimitives) {
+ var packagePrimitives = packageObject.getPrimitives();
+ for (var op in packagePrimitives) {
+ if (packagePrimitives.hasOwnProperty(op)) {
+ this._primitives[op] =
+ packagePrimitives[op].bind(packageObject);
+ }
+ }
+ }
+ // Collect hat metadata from package.
+ if (packageObject.getHats) {
+ var packageHats = packageObject.getHats();
+ for (var hatName in packageHats) {
+ if (packageHats.hasOwnProperty(hatName)) {
+ this._hats[hatName] = packageHats[hatName];
+ }
}
}
}
@@ -229,17 +271,67 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
return this._primitives[opcode];
};
+/**
+ * Return whether an opcode represents a hat block.
+ * @param {!string} opcode The opcode to look up.
+ * @return {Boolean} True if the op is known to be a hat.
+ */
+Runtime.prototype.getIsHat = function (opcode) {
+ return this._hats.hasOwnProperty(opcode);
+};
+
+/**
+ * Return whether an opcode represents an edge-activated hat block.
+ * @param {!string} opcode The opcode to look up.
+ * @return {Boolean} True if the op is known to be a edge-activated hat.
+ */
+Runtime.prototype.getIsEdgeActivatedHat = function (opcode) {
+ return this._hats.hasOwnProperty(opcode) &&
+ this._hats[opcode].edgeActivated;
+};
+
+/**
+ * Update an edge-activated hat block value.
+ * @param {!string} blockId ID of hat to store value for.
+ * @param {*} newValue Value to store for edge-activated hat.
+ * @return {*} The old value for the edge-activated hat.
+ */
+Runtime.prototype.updateEdgeActivatedValue = function (blockId, newValue) {
+ var oldValue = this._edgeActivatedHatValues[blockId];
+ this._edgeActivatedHatValues[blockId] = newValue;
+ return oldValue;
+};
+
+/**
+ * Clear all edge-activaed hat values.
+ */
+Runtime.prototype.clearEdgeActivatedValues = function () {
+ this._edgeActivatedHatValues = {};
+};
+
+/**
+ * Attach the renderer
+ * @param {!RenderWebGL} renderer The renderer to attach
+ */
+Runtime.prototype.attachRenderer = function (renderer) {
+ this.renderer = renderer;
+};
+
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Create a thread and push it to the list of threads.
- * @param {!string} id ID of block that starts the stack
+ * @param {!string} id ID of block that starts the stack.
+ * @param {!Target} target Target to run thread on.
+ * @return {!Thread} The newly created thread.
*/
-Runtime.prototype._pushThread = function (id) {
- this.emit(Runtime.STACK_GLOW_ON, id);
+Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
+ thread.target = target;
+ thread.pushStack(id);
this.threads.push(thread);
+ return thread;
};
/**
@@ -247,79 +339,201 @@ Runtime.prototype._pushThread = function (id) {
* @param {?Thread} thread Thread object to remove from actives
*/
Runtime.prototype._removeThread = function (thread) {
+ // Inform sequencer to stop executing that thread.
+ this.sequencer.retireThread(thread);
+ // Remove from the list.
var i = this.threads.indexOf(thread);
if (i > -1) {
- this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
this.threads.splice(i, 1);
}
};
/**
- * Toggle a stack
- * @param {!string} stackId ID of block that starts the stack
+ * Return whether a thread is currently active/running.
+ * @param {?Thread} thread Thread object to check.
+ * @return {Boolean} True if the thread is active/running.
*/
-Runtime.prototype.toggleStack = function (stackId) {
- // Remove any existing thread
+Runtime.prototype.isActiveThread = function (thread) {
+ return this.threads.indexOf(thread) > -1;
+};
+
+/**
+ * Toggle a script.
+ * @param {!string} topBlockId ID of block that starts the script.
+ */
+Runtime.prototype.toggleScript = function (topBlockId) {
+ // Remove any existing thread.
for (var i = 0; i < this.threads.length; i++) {
- if (this.threads[i].topBlock == stackId) {
+ if (this.threads[i].topBlock == topBlockId) {
this._removeThread(this.threads[i]);
return;
}
}
- // Otherwise add it
- this._pushThread(stackId);
+ // Otherwise add it.
+ this._pushThread(topBlockId, this._editingTarget);
};
/**
- * Green flag, which stops currently running threads
- * and adds all top-level stacks that start with the green flag
+ * Run a function `f` for all scripts in a workspace.
+ * `f` will be called with two parameters:
+ * - the top block ID of the script.
+ * - the target that owns the script.
+ * @param {!Function} f Function to call for each script.
+ * @param {Target=} opt_target Optionally, a target to restrict to.
*/
-Runtime.prototype.greenFlag = function () {
- // Remove all existing threads
- for (var i = 0; i < this.threads.length; i++) {
- this._removeThread(this.threads[i]);
+Runtime.prototype.allScriptsDo = function (f, opt_target) {
+ var targets = this.targets;
+ if (opt_target) {
+ targets = [opt_target];
}
- // Add all top stacks with green flag
- for (var j = 0; j < this.stacks.length; j++) {
- var topBlock = this.stacks[j];
- if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
- this._pushThread(this.stacks[j]);
+ for (var t = 0; t < targets.length; t++) {
+ var target = targets[t];
+ var scripts = target.blocks.getScripts();
+ for (var j = 0; j < scripts.length; j++) {
+ var topBlockId = scripts[j];
+ f(topBlockId, target);
}
}
};
/**
- * Distance sensor hack
+ * Start all relevant hats.
+ * @param {!string} requestedHatOpcode Opcode of hats to start.
+ * @param {Object=} opt_matchFields Optionally, fields to match on the hat.
+ * @param {Target=} opt_target Optionally, a target to restrict to.
+ * @return {Array.} List of threads started by this function.
*/
-Runtime.prototype.startDistanceSensors = function () {
- // Add all top stacks with distance sensor
- for (var j = 0; j < this.stacks.length; j++) {
- var topBlock = this.stacks[j];
- if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') {
- var alreadyRunning = false;
- for (var k = 0; k < this.threads.length; k++) {
- if (this.threads[k].topBlock === topBlock) {
- alreadyRunning = true;
+Runtime.prototype.startHats = function (requestedHatOpcode,
+ opt_matchFields, opt_target) {
+ if (!this._hats.hasOwnProperty(requestedHatOpcode)) {
+ // No known hat with this opcode.
+ return;
+ }
+ var instance = this;
+ var newThreads = [];
+ // Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
+ this.allScriptsDo(function(topBlockId, target) {
+ var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode;
+ if (potentialHatOpcode !== requestedHatOpcode) {
+ // Not the right hat.
+ return;
+ }
+ // Match any requested fields.
+ // For example: ensures that broadcasts match.
+ // This needs to happen before the block is evaluated
+ // (i.e., before the predicate can be run) because "broadcast and wait"
+ // needs to have a precise collection of started threads.
+ var hatFields = target.blocks.getFields(topBlockId);
+ if (opt_matchFields) {
+ for (var matchField in opt_matchFields) {
+ if (hatFields[matchField].value !==
+ opt_matchFields[matchField]) {
+ // Field mismatch.
+ return;
}
}
- if (!alreadyRunning) {
- this._pushThread(this.stacks[j]);
+ }
+ // Look up metadata for the relevant hat.
+ var hatMeta = instance._hats[requestedHatOpcode];
+ if (hatMeta.restartExistingThreads) {
+ // If `restartExistingThreads` is true, we should stop
+ // any existing threads starting with the top block.
+ for (var i = 0; i < instance.threads.length; i++) {
+ if (instance.threads[i].topBlock === topBlockId &&
+ instance.threads[i].target == target) {
+ instance._removeThread(instance.threads[i]);
+ }
}
+ } else {
+ // If `restartExistingThreads` is false, we should
+ // give up if any threads with the top block are running.
+ for (var j = 0; j < instance.threads.length; j++) {
+ if (instance.threads[j].topBlock === topBlockId &&
+ instance.threads[j].target == target) {
+ // Some thread is already running.
+ return;
+ }
+ }
+ }
+ // Start the thread with this top block.
+ newThreads.push(instance._pushThread(topBlockId, target));
+ }, opt_target);
+ return newThreads;
+};
+
+/**
+ * Dispose all targets. Return to clean state.
+ */
+Runtime.prototype.dispose = function () {
+ this.stopAll();
+ this.targets.map(this.disposeTarget, this);
+};
+
+/**
+ * Dispose of a target.
+ * @param {!Target} disposingTarget Target to dispose of.
+ */
+Runtime.prototype.disposeTarget = function (disposingTarget) {
+ this.targets = this.targets.filter(function (target) {
+ if (disposingTarget !== target) return true;
+ // Allow target to do dispose actions.
+ target.dispose();
+ // Remove from list of targets.
+ return false;
+ });
+};
+
+/**
+ * Stop any threads acting on the target.
+ * @param {!Target} target Target to stop threads for.
+ * @param {Thread=} opt_threadException Optional thread to skip.
+ */
+Runtime.prototype.stopForTarget = function (target, opt_threadException) {
+ // Stop any threads on the target.
+ for (var i = 0; i < this.threads.length; i++) {
+ if (this.threads[i] === opt_threadException) {
+ continue;
+ }
+ if (this.threads[i].target == target) {
+ this._removeThread(this.threads[i]);
}
}
};
/**
- * Stop "everything"
+ * Start all threads that start with the green flag.
+ */
+Runtime.prototype.greenFlag = function () {
+ this.stopAll();
+ this.ioDevices.clock.resetProjectTimer();
+ this.clearEdgeActivatedValues();
+ // Inform all targets of the green flag.
+ for (var i = 0; i < this.targets.length; i++) {
+ this.targets[i].onGreenFlag();
+ }
+ this.startHats('event_whenflagclicked');
+};
+
+/**
+ * Stop "everything."
*/
Runtime.prototype.stopAll = function () {
+ // Dispose all clones.
+ var newTargets = [];
+ for (var i = 0; i < this.targets.length; i++) {
+ if (this.targets[i].hasOwnProperty('isOriginal') &&
+ !this.targets[i].isOriginal) {
+ this.targets[i].dispose();
+ } else {
+ newTargets.push(this.targets[i]);
+ }
+ }
+ this.targets = newTargets;
+ // Dispose all threads.
var threadsCopy = this.threads.slice();
while (threadsCopy.length > 0) {
- this._removeThread(threadsCopy.pop());
- }
- // @todo call stop function in all extensions/packages/WeDo stub
- if (window.native) {
- window.native.motorStop();
+ var poppedThread = threadsCopy.pop();
+ this._removeThread(poppedThread);
}
};
@@ -328,9 +542,182 @@ Runtime.prototype.stopAll = function () {
* inactive threads after each iteration.
*/
Runtime.prototype._step = function () {
- var inactiveThreads = this.sequencer.stepThreads(this.threads);
- for (var i = 0; i < inactiveThreads.length; i++) {
- this._removeThread(inactiveThreads[i]);
+ if (this.pauseMode) {
+ // Don't do any execution while in pause mode.
+ return;
+ }
+ // Find all edge-activated hats, and add them to threads to be evaluated.
+ for (var hatType in this._hats) {
+ var hat = this._hats[hatType];
+ if (hat.edgeActivated) {
+ this.startHats(hatType);
+ }
+ }
+ this.redrawRequested = false;
+ var inactiveThreads = this.sequencer.stepThreads();
+ this._updateGlows(inactiveThreads);
+};
+
+/**
+ * Set the current editing target known by the runtime.
+ * @param {!Target} editingTarget New editing target.
+ */
+Runtime.prototype.setEditingTarget = function (editingTarget) {
+ this._editingTarget = editingTarget;
+ // Script glows must be cleared.
+ this._scriptGlowsPreviousFrame = [];
+ this._updateGlows();
+};
+
+/**
+ * Set whether we are in pause mode.
+ * @param {boolean} pauseModeOn True iff in pause mode.
+ */
+Runtime.prototype.setPauseMode = function (pauseModeOn) {
+ // Inform the project clock/timer to pause/resume its time.
+ if (this.ioDevices.clock) {
+ if (pauseModeOn && !this.pauseMode) {
+ this.ioDevices.clock.pause();
+ }
+ if (!pauseModeOn && this.pauseMode) {
+ this.ioDevices.clock.resume();
+ }
+ }
+ this.pauseMode = pauseModeOn;
+};
+
+/**
+ * Set whether we are in 30 TPS compatibility mode.
+ * @param {boolean} compatibilityModeOn True iff in compatibility mode.
+ */
+Runtime.prototype.setCompatibilityMode = function (compatibilityModeOn) {
+ this.compatibilityMode = compatibilityModeOn;
+ if (this._steppingInterval) {
+ self.clearInterval(this._steppingInterval);
+ this.start();
+ }
+};
+
+/**
+ * Set whether we are in single-stepping mode.
+ * @param {boolean} singleSteppingOn True iff in single-stepping mode.
+ */
+Runtime.prototype.setSingleSteppingMode = function (singleSteppingOn) {
+ this.singleStepping = singleSteppingOn;
+ if (this._steppingInterval) {
+ self.clearInterval(this._steppingInterval);
+ this.start();
+ }
+};
+
+/**
+ * Set the speed during single-stepping mode.
+ * @param {number} speed Interval length to step threads, in ms.
+ */
+Runtime.prototype.setSingleSteppingSpeed = function (speed) {
+ this.singleStepInterval = 1000 / speed;
+ if (this._steppingInterval) {
+ self.clearInterval(this._steppingInterval);
+ this.start();
+ }
+};
+
+/**
+ * Emit glows/glow clears for blocks and scripts after a single tick.
+ * Looks at `this.threads` and notices which have turned on/off new glows.
+ * @param {Array.=} opt_extraThreads Optional list of inactive threads.
+ */
+Runtime.prototype._updateGlows = function (opt_extraThreads) {
+ var searchThreads = [];
+ searchThreads.push.apply(searchThreads, this.threads);
+ if (opt_extraThreads) {
+ searchThreads.push.apply(searchThreads, opt_extraThreads);
+ }
+ // Set of scripts that request a glow this frame.
+ var requestedGlowsThisFrame = [];
+ var requestedBlockGlowsThisFrame = [];
+ // Final set of scripts glowing during this frame.
+ var finalScriptGlows = [];
+ var finalBlockGlows = [];
+ // Find all scripts that should be glowing.
+ for (var i = 0; i < searchThreads.length; i++) {
+ var thread = searchThreads[i];
+ var target = thread.target;
+ if (target == this._editingTarget) {
+ var blockForThread = thread.blockGlowInFrame;
+ if (thread.requestScriptGlowInFrame) {
+ var script = target.blocks.getTopLevelScript(blockForThread);
+ if (!script) {
+ // Attempt to find in flyout blocks.
+ script = this.flyoutBlocks.getTopLevelScript(
+ blockForThread
+ );
+ }
+ if (script) {
+ requestedGlowsThisFrame.push(script);
+ }
+ }
+ // Only show block glows in single-stepping mode.
+ if (this.singleStepping && blockForThread) {
+ requestedBlockGlowsThisFrame.push(blockForThread);
+ }
+ }
+ }
+ // Compare to previous frame.
+ for (var j = 0; j < this._scriptGlowsPreviousFrame.length; j++) {
+ var previousFrameGlow = this._scriptGlowsPreviousFrame[j];
+ if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) {
+ // Glow turned off.
+ this.glowScript(previousFrameGlow, false);
+ } else {
+ // Still glowing.
+ finalScriptGlows.push(previousFrameGlow);
+ }
+ }
+ for (var k = 0; k < requestedGlowsThisFrame.length; k++) {
+ var currentFrameGlow = requestedGlowsThisFrame[k];
+ if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) {
+ // Glow turned on.
+ this.glowScript(currentFrameGlow, true);
+ finalScriptGlows.push(currentFrameGlow);
+ }
+ }
+ for (var m = 0; m < this._blockGlowsPreviousFrame.length; m++) {
+ var previousBlockGlow = this._blockGlowsPreviousFrame[m];
+ if (requestedBlockGlowsThisFrame.indexOf(previousBlockGlow) < 0) {
+ // Glow turned off.
+ try {
+ this.glowBlock(previousBlockGlow, false);
+ } catch (e) {
+ // Block has been removed.
+ }
+ } else {
+ // Still glowing.
+ finalBlockGlows.push(previousBlockGlow);
+ }
+ }
+ for (var p = 0; p < requestedBlockGlowsThisFrame.length; p++) {
+ var currentBlockFrameGlow = requestedBlockGlowsThisFrame[p];
+ if (this._blockGlowsPreviousFrame.indexOf(currentBlockFrameGlow) < 0) {
+ // Glow turned on.
+ this.glowBlock(currentBlockFrameGlow, true);
+ finalBlockGlows.push(currentBlockFrameGlow);
+ }
+ }
+ this._scriptGlowsPreviousFrame = finalScriptGlows;
+ this._blockGlowsPreviousFrame = finalBlockGlows;
+};
+
+/**
+ * "Quiet" a script's glow: stop the VM from generating glow/unglow events
+ * about that script. Use when a script has just been deleted, but we may
+ * still be tracking glow data about it.
+ * @param {!string} scriptBlockId Id of top-level block in script to quiet.
+ */
+Runtime.prototype.quietGlow = function (scriptBlockId) {
+ var index = this._scriptGlowsPreviousFrame.indexOf(scriptBlockId);
+ if (index > -1) {
+ this._scriptGlowsPreviousFrame.splice(index, 1);
}
};
@@ -348,55 +735,116 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) {
};
/**
- * Set up timers to repeatedly step in a browser
+ * Emit feedback for script glowing.
+ * @param {?string} topBlockId ID for the top block to update glow
+ * @param {boolean} isGlowing True to turn on glow; false to turn off.
+ */
+Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
+ if (isGlowing) {
+ this.emit(Runtime.SCRIPT_GLOW_ON, topBlockId);
+ } else {
+ this.emit(Runtime.SCRIPT_GLOW_OFF, topBlockId);
+ }
+};
+
+/**
+ * Emit value for reporter to show in the blocks.
+ * @param {string} blockId ID for the block.
+ * @param {string} value Value to show associated with the block.
+ */
+Runtime.prototype.visualReport = function (blockId, value) {
+ this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
+};
+
+/**
+ * Get a target by its id.
+ * @param {string} targetId Id of target to find.
+ * @return {?Target} The target, if found.
+ */
+Runtime.prototype.getTargetById = function (targetId) {
+ for (var i = 0; i < this.targets.length; i++) {
+ var target = this.targets[i];
+ if (target.id == targetId) {
+ return target;
+ }
+ }
+};
+
+/**
+ * Get the first original (non-clone-block-created) sprite given a name.
+ * @param {string} spriteName Name of sprite to look for.
+ * @return {?Target} Target representing a sprite of the given name.
+ */
+Runtime.prototype.getSpriteTargetByName = function (spriteName) {
+ for (var i = 0; i < this.targets.length; i++) {
+ var target = this.targets[i];
+ if (target.sprite && target.sprite.name == spriteName) {
+ return target;
+ }
+ }
+};
+
+/**
+ * Update the clone counter to track how many clones are created.
+ * @param {number} changeAmount How many clones have been created/destroyed.
+ */
+Runtime.prototype.changeCloneCounter = function (changeAmount) {
+ this._cloneCounter += changeAmount;
+};
+
+/**
+ * Return whether there are clones available.
+ * @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
+ */
+Runtime.prototype.clonesAvailable = function () {
+ return this._cloneCounter < Runtime.MAX_CLONES;
+};
+
+/**
+ * Get a target representing the Scratch stage, if one exists.
+ * @return {?Target} The target, if found.
+ */
+Runtime.prototype.getTargetForStage = function () {
+ for (var i = 0; i < this.targets.length; i++) {
+ var target = this.targets[i];
+ if (target.isStage) {
+ return target;
+ }
+ }
+};
+
+/**
+ * Tell the runtime to request a redraw.
+ * Use after a clone/sprite has completed some visible operation on the stage.
+ */
+Runtime.prototype.requestRedraw = function () {
+ this.redrawRequested = true;
+};
+
+/**
+ * Handle an animation frame from the main thread.
+ */
+Runtime.prototype.animationFrame = function () {
+ if (this.renderer) {
+ // @todo: Only render when this.redrawRequested or clones rendered.
+ this.renderer.draw();
+ }
+};
+
+/**
+ * Set up timers to repeatedly step in a browser.
*/
Runtime.prototype.start = function () {
- if (!window.setInterval) return;
- window.setInterval(function() {
+ var interval = Runtime.THREAD_STEP_INTERVAL;
+ if (this.singleStepping) {
+ interval = this.singleStepInterval;
+ } else if (this.compatibilityMode) {
+ interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
+ }
+ this.currentStepTime = interval;
+ this._steppingInterval = self.setInterval(function() {
this._step();
- }.bind(this), Runtime.THREAD_STEP_INTERVAL);
-};
-
-// -----------------------------------------------------------------------------
-// -----------------------------------------------------------------------------
-
-/**
- * Helper to remove a stack from `this.stacks`
- * @param {?string} id ID of block that starts the stack
- */
-Runtime.prototype._deleteStack = function (id) {
- var i = this.stacks.indexOf(id);
- if (i > -1) this.stacks.splice(i, 1);
-};
-
-/**
- * Helper to get the next block for a particular block
- * @param {?string} id ID of block to get the next block for
- * @return {?string} ID of next block in the sequence
- */
-Runtime.prototype._getNextBlock = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].next;
-};
-
-/**
- * Helper to get the substack for a particular C-shaped block
- * @param {?string} id ID for block to get the substack for
- * @return {?string} ID of block in the substack
- */
-Runtime.prototype._getSubstack = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].fields['SUBSTACK'];
-};
-
-/**
- * Helper to get the opcode for a particular block
- * @param {?string} id ID of block to query
- * @return {?string} the opcode corresponding to that block
- */
-Runtime.prototype._getOpcode = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].opcode;
+ }.bind(this), interval);
};
module.exports = Runtime;
diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js
index f495264c8..182b9ee65 100644
--- a/src/engine/sequencer.js
+++ b/src/engine/sequencer.js
@@ -1,6 +1,6 @@
var Timer = require('../util/timer');
var Thread = require('./thread');
-var YieldTimers = require('../util/yieldtimers.js');
+var execute = require('./execute.js');
function Sequencer (runtime) {
/**
@@ -17,225 +17,238 @@ function Sequencer (runtime) {
}
/**
- * The sequencer does as much work as it can within WORK_TIME milliseconds,
- * then yields. This is essentially a rate-limiter for blocks.
- * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
- * @const {!number}
+ * Time to run a warp-mode thread, in ms.
+ * @type {number}
*/
-Sequencer.WORK_TIME = 10;
+Sequencer.WARP_TIME = 500;
/**
- * Step through all threads in `this.threads`, running them in order.
- * @return {Array.} All threads which have finished in this iteration.
+ * Step through all threads in `this.runtime.threads`, running them in order.
+ * @return {Array.} List of inactive threads after stepping.
*/
-Sequencer.prototype.stepThreads = function (threads) {
- // Start counting toward WORK_TIME
+Sequencer.prototype.stepThreads = function () {
+ // Work time is 75% of the thread stepping interval.
+ var WORK_TIME = 0.75 * this.runtime.currentStepTime;
+ // Start counting toward WORK_TIME.
this.timer.start();
- // List of threads which have been killed by this step.
+ // Count of active threads.
+ var numActiveThreads = Infinity;
+ // Whether `stepThreads` has run through a full single tick.
+ var ranFirstTick = false;
var inactiveThreads = [];
- // If all of the threads are yielding, we should yield.
- var numYieldingThreads = 0;
- // While there are still threads to run and we are within WORK_TIME,
- // continue executing threads.
- while (threads.length > 0 &&
- threads.length > numYieldingThreads &&
- this.timer.timeElapsed() < Sequencer.WORK_TIME) {
- // New threads at the end of the iteration.
- var newThreads = [];
- // Attempt to run each thread one time
- for (var i = 0; i < threads.length; i++) {
- var activeThread = threads[i];
- if (activeThread.status === Thread.STATUS_RUNNING) {
+ // Conditions for continuing to stepping threads:
+ // 1. We must have threads in the list, and some must be active.
+ // 2. Time elapsed must be less than WORK_TIME.
+ // 3. Either turbo mode, or no redraw has been requested by a primitive.
+ while (this.runtime.threads.length > 0 &&
+ numActiveThreads > 0 &&
+ this.timer.timeElapsed() < WORK_TIME &&
+ (this.runtime.turboMode || !this.runtime.redrawRequested)) {
+ numActiveThreads = 0;
+ // Inline copy of the threads, updated on each step.
+ var threadsCopy = this.runtime.threads.slice();
+ // Attempt to run each thread one time.
+ for (var i = 0; i < threadsCopy.length; i++) {
+ var activeThread = threadsCopy[i];
+ if (activeThread.stack.length === 0 ||
+ activeThread.status === Thread.STATUS_DONE) {
+ // Finished with this thread.
+ if (inactiveThreads.indexOf(activeThread) < 0) {
+ inactiveThreads.push(activeThread);
+ }
+ continue;
+ }
+ if (activeThread.status === Thread.STATUS_YIELD_TICK &&
+ !ranFirstTick) {
+ // Clear single-tick yield from the last call of `stepThreads`.
+ activeThread.status = Thread.STATUS_RUNNING;
+ }
+ if (activeThread.status === Thread.STATUS_RUNNING ||
+ activeThread.status === Thread.STATUS_YIELD) {
// Normal-mode thread: step.
this.stepThread(activeThread);
- } else if (activeThread.status === Thread.STATUS_YIELD) {
- // Yield-mode thread: check if the time has passed.
- YieldTimers.resolve(activeThread.yieldTimerId);
- numYieldingThreads++;
- } else if (activeThread.status === Thread.STATUS_DONE) {
- // Moved to a done state - finish up
- activeThread.status = Thread.STATUS_RUNNING;
- // @todo Deal with the return value
+ activeThread.warpTimer = null;
}
- // First attempt to pop from the stack
- if (activeThread.stack.length > 0 &&
- activeThread.nextBlock === null &&
- activeThread.status === Thread.STATUS_DONE) {
- activeThread.nextBlock = activeThread.stack.pop();
- // Don't pop stack frame - we need the data.
- // A new one won't be created when we execute.
- if (activeThread.nextBlock !== null) {
- activeThread.status === Thread.STATUS_RUNNING;
+ if (activeThread.status === Thread.STATUS_RUNNING) {
+ // After stepping, status is still running.
+ // If we're in single-stepping mode, mark the thread as
+ // a single-tick yield so it doesn't re-execute
+ // until the next frame.
+ if (this.runtime.singleStepping) {
+ activeThread.status = Thread.STATUS_YIELD_TICK;
}
- }
- if (activeThread.nextBlock === null &&
- activeThread.status === Thread.STATUS_DONE) {
- // Finished with this thread - tell runtime to clean it up.
- inactiveThreads.push(activeThread);
- } else {
- // Keep this thead in the loop.
- newThreads.push(activeThread);
+ numActiveThreads++;
}
}
- // Effectively filters out threads that have stopped.
- threads = newThreads;
+ // We successfully ticked once. Prevents running STATUS_YIELD_TICK
+ // threads on the next tick.
+ ranFirstTick = true;
}
+ // Filter inactive threads from `this.runtime.threads`.
+ this.runtime.threads = this.runtime.threads.filter(function(thread) {
+ if (inactiveThreads.indexOf(thread) > -1) {
+ return false;
+ }
+ return true;
+ });
return inactiveThreads;
};
/**
- * Step the requested thread
- * @param {!Thread} thread Thread object to step
+ * Step the requested thread for as long as necessary.
+ * @param {!Thread} thread Thread object to step.
*/
Sequencer.prototype.stepThread = function (thread) {
- // Save the yield timer ID, in case a primitive makes a new one
- // @todo hack - perhaps patch this to allow more than one timer per
- // primitive, for example...
- var oldYieldTimerId = YieldTimers.timerId;
+ var currentBlockId = thread.peekStack();
+ if (!currentBlockId) {
+ // A "null block" - empty branch.
+ thread.popStack();
+ }
+ while (thread.peekStack()) {
+ var isWarpMode = thread.peekStackFrame().warpMode;
+ if (isWarpMode && !thread.warpTimer) {
+ // Initialize warp-mode timer if it hasn't been already.
+ // This will start counting the thread toward `Sequencer.WARP_TIME`.
+ thread.warpTimer = new Timer();
+ thread.warpTimer.start();
+ }
+ // Execute the current block.
+ // Save the current block ID to notice if we did control flow.
+ currentBlockId = thread.peekStack();
+ execute(this, thread);
+ thread.blockGlowInFrame = currentBlockId;
+ // If the thread has yielded or is waiting, yield to other threads.
+ if (thread.status === Thread.STATUS_YIELD) {
+ // Mark as running for next iteration.
+ thread.status = Thread.STATUS_RUNNING;
+ // In warp mode, yielded blocks are re-executed immediately.
+ if (isWarpMode &&
+ thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
+ continue;
+ }
+ return;
+ } else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
+ // A promise was returned by the primitive. Yield the thread
+ // until the promise resolves. Promise resolution should reset
+ // thread.status to Thread.STATUS_RUNNING.
+ return;
+ }
+ // If no control flow has happened, switch to next block.
+ if (thread.peekStack() === currentBlockId) {
+ thread.goToNextBlock();
+ }
+ // If no next block has been found at this point, look on the stack.
+ while (!thread.peekStack()) {
+ thread.popStack();
+ if (thread.stack.length === 0) {
+ // No more stack to run!
+ thread.status = Thread.STATUS_DONE;
+ return;
+ }
+ if (thread.peekStackFrame().isLoop) {
+ // The current level of the stack is marked as a loop.
+ // Return to yield for the frame/tick in general.
+ // Unless we're in warp mode - then only return if the
+ // warp timer is up.
+ if (!isWarpMode ||
+ thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
+ // Don't do anything to the stack, since loops need
+ // to be re-executed.
+ return;
+ } else {
+ // Don't go to the next block for this level of the stack,
+ // since loops need to be re-executed.
+ continue;
+ }
+ } else if (thread.peekStackFrame().waitingReporter) {
+ // This level of the stack was waiting for a value.
+ // This means a reporter has just returned - so don't go
+ // to the next block for this level of the stack.
+ return;
+ }
+ // Get next block of existing block on the stack.
+ thread.goToNextBlock();
+ }
+ // In single-stepping mode, force `stepThread` to only run one block
+ // at a time.
+ if (this.runtime.singleStepping) {
+ return;
+ }
+ }
+};
- // Save the current block and set the nextBlock.
- // If the primitive would like to do control flow,
- // it can overwrite nextBlock.
- var currentBlock = thread.nextBlock;
- if (!currentBlock || !this.runtime.blocks[currentBlock]) {
- thread.status = Thread.STATUS_DONE;
+/**
+ * Step a thread into a block's branch.
+ * @param {!Thread} thread Thread object to step to branch.
+ * @param {Number} branchNum Which branch to step to (i.e., 1, 2).
+ * @param {Boolean} isLoop Whether this block is a loop.
+ */
+Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
+ if (!branchNum) {
+ branchNum = 1;
+ }
+ var currentBlockId = thread.peekStack();
+ var branchId = thread.target.blocks.getBranch(
+ currentBlockId,
+ branchNum
+ );
+ thread.peekStackFrame().isLoop = isLoop;
+ if (branchId) {
+ // Push branch ID to the thread's stack.
+ thread.pushStack(branchId);
+ } else {
+ thread.pushStack(null);
+ }
+};
+
+/**
+ * Step a procedure.
+ * @param {!Thread} thread Thread object to step to procedure.
+ * @param {!string} procedureCode Procedure code of procedure to step to.
+ */
+Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
+ var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
+ if (!definition) {
return;
}
- thread.nextBlock = this.runtime._getNextBlock(currentBlock);
-
- var opcode = this.runtime._getOpcode(currentBlock);
-
- // Push the current block to the stack
- thread.stack.push(currentBlock);
- // Push an empty stack frame, if we need one.
- // Might not, if we just popped the stack.
- if (thread.stack.length > thread.stackFrames.length) {
- thread.stackFrames.push({});
- }
- var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];
-
- /**
- * A callback for the primitive to indicate its thread should yield.
- * @type {Function}
- */
- var threadYieldCallback = function () {
+ // Check if the call is recursive.
+ // If so, set the thread to yield after pushing.
+ var isRecursive = thread.isRecursiveCall(procedureCode);
+ // To step to a procedure, we put its definition on the stack.
+ // Execution for the thread will proceed through the definition hat
+ // and on to the main definition of the procedure.
+ // When that set of blocks finishes executing, it will be popped
+ // from the stack by the sequencer, returning control to the caller.
+ thread.pushStack(definition);
+ // In known warp-mode threads, only yield when time is up.
+ if (thread.peekStackFrame().warpMode &&
+ thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
thread.status = Thread.STATUS_YIELD;
- };
-
- /**
- * A callback for the primitive to indicate its thread is finished
- * @type {Function}
- */
- var instance = this;
- var threadDoneCallback = function () {
- thread.status = Thread.STATUS_DONE;
- // Refresh nextBlock in case it has changed during a yield.
- thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
- // Pop the stack and stack frame
- thread.stack.pop();
- thread.stackFrames.pop();
- };
-
- /**
- * A callback for the primitive to start hats.
- * @todo very hacked...
- */
- var startHats = function(callback) {
- for (var i = 0; i < instance.runtime.stacks.length; i++) {
- var stack = instance.runtime.stacks[i];
- var stackBlock = instance.runtime.blocks[stack];
- var result = callback(stackBlock);
- if (result) {
- // Check if the stack is already running
- var stackRunning = false;
-
- for (var j = 0; j < instance.runtime.threads.length; j++) {
- if (instance.runtime.threads[j].topBlock == stack) {
- stackRunning = true;
- break;
- }
- }
- if (!stackRunning) {
- instance.runtime._pushThread(stack);
- }
- }
- }
- };
-
- /**
- * Record whether we have switched stack,
- * to avoid proceeding the thread automatically.
- * @type {boolean}
- */
- var switchedStack = false;
- /**
- * A callback for a primitive to start a substack.
- * @type {Function}
- */
- var threadStartSubstack = function () {
- // Set nextBlock to the start of the substack
- var substack = instance.runtime._getSubstack(currentBlock);
- if (substack && substack.value) {
- thread.nextBlock = substack.value;
+ } else {
+ // Look for warp-mode flag on definition, and set the thread
+ // to warp-mode if needed.
+ var definitionBlock = thread.target.blocks.getBlock(definition);
+ var doWarp = definitionBlock.mutation.warp;
+ if (doWarp) {
+ thread.peekStackFrame().warpMode = true;
} else {
- thread.nextBlock = null;
- }
- switchedStack = true;
- };
-
- // @todo extreme hack to get the single argument value for prototype
- var argValues = [];
- var blockInputs = this.runtime.blocks[currentBlock].fields;
- for (var bi in blockInputs) {
- var outer = blockInputs[bi];
- for (var b in outer.blocks) {
- var block = outer.blocks[b];
- var fields = block.fields;
- for (var f in fields) {
- var field = fields[f];
- argValues.push(field.value);
- }
- }
- }
-
- if (!opcode) {
- console.warn('Could not get opcode for block: ' + currentBlock);
- }
- else {
- var blockFunction = this.runtime.getOpcodeFunction(opcode);
- if (!blockFunction) {
- console.warn('Could not get implementation for opcode: ' + opcode);
- }
- else {
- try {
- // @todo deal with the return value
- blockFunction(argValues, {
- yield: threadYieldCallback,
- done: threadDoneCallback,
- timeout: YieldTimers.timeout,
- stackFrame: currentStackFrame,
- startSubstack: threadStartSubstack,
- startHats: startHats
- });
- }
- catch(e) {
- console.error(
- 'Exception calling block function for opcode: ' +
- opcode + '\n' + e);
- } finally {
- // Update if the thread has set a yield timer ID
- // @todo hack
- if (YieldTimers.timerId > oldYieldTimerId) {
- thread.yieldTimerId = YieldTimers.timerId;
- }
- if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
- // Thread executed without yielding - move to done
- threadDoneCallback();
- }
+ // In normal-mode threads, yield any time we have a recursive call.
+ if (isRecursive) {
+ thread.status = Thread.STATUS_YIELD;
}
}
}
+};
+/**
+ * Retire a thread in the middle, without considering further blocks.
+ * @param {!Thread} thread Thread object to retire.
+ */
+Sequencer.prototype.retireThread = function (thread) {
+ thread.stack = [];
+ thread.stackFrame = [];
+ thread.requestScriptGlowInFrame = false;
+ thread.status = Thread.STATUS_DONE;
};
module.exports = Sequencer;
diff --git a/src/engine/target.js b/src/engine/target.js
new file mode 100644
index 000000000..d5a59b47a
--- /dev/null
+++ b/src/engine/target.js
@@ -0,0 +1,116 @@
+var Blocks = require('./blocks');
+var Variable = require('../engine/variable');
+var List = require('../engine/list');
+var uid = require('../util/uid');
+
+/**
+ * @fileoverview
+ * A Target is an abstract "code-running" object for the Scratch VM.
+ * Examples include sprites/clones or potentially physical-world devices.
+ */
+
+/**
+ * @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
+ * @constructor
+ */
+function Target (blocks) {
+ if (!blocks) {
+ blocks = new Blocks(this);
+ }
+ /**
+ * A unique ID for this target.
+ * @type {string}
+ */
+ this.id = uid();
+ /**
+ * Blocks run as code for this target.
+ * @type {!Blocks}
+ */
+ this.blocks = blocks;
+ /**
+ * Dictionary of variables and their values for this target.
+ * Key is the variable name.
+ * @type {Object.}
+ */
+ this.variables = {};
+ /**
+ * Dictionary of lists and their contents for this target.
+ * Key is the list name.
+ * @type {Object.}
+ */
+ this.lists = {};
+}
+
+/**
+ * Called when the project receives a "green flag."
+ * @abstract
+ */
+Target.prototype.onGreenFlag = function () {};
+
+/**
+ * Return a human-readable name for this target.
+ * Target implementations should override this.
+ * @abstract
+ * @returns {string} Human-readable name for the target.
+ */
+Target.prototype.getName = function () {
+ return this.id;
+};
+
+/**
+ * Look up a variable object, and create it if one doesn't exist.
+ * Search begins for local variables; then look for globals.
+ * @param {!string} name Name of the variable.
+ * @return {!Variable} Variable object.
+ */
+Target.prototype.lookupOrCreateVariable = function (name) {
+ // If we have a local copy, return it.
+ if (this.variables.hasOwnProperty(name)) {
+ return this.variables[name];
+ }
+ // If the stage has a global copy, return it.
+ if (this.runtime && !this.isStage) {
+ var stage = this.runtime.getTargetForStage();
+ if (stage.variables.hasOwnProperty(name)) {
+ return stage.variables[name];
+ }
+ }
+ // No variable with this name exists - create it locally.
+ var newVariable = new Variable(name, 0, false);
+ this.variables[name] = newVariable;
+ return newVariable;
+};
+
+/**
+* Look up a list object for this target, and create it if one doesn't exist.
+* Search begins for local lists; then look for globals.
+* @param {!string} name Name of the list.
+* @return {!List} List object.
+ */
+Target.prototype.lookupOrCreateList = function (name) {
+ // If we have a local copy, return it.
+ if (this.lists.hasOwnProperty(name)) {
+ return this.lists[name];
+ }
+ // If the stage has a global copy, return it.
+ if (this.runtime && !this.isStage) {
+ var stage = this.runtime.getTargetForStage();
+ if (stage.lists.hasOwnProperty(name)) {
+ return stage.lists[name];
+ }
+ }
+ // No list with this name exists - create it locally.
+ var newList = new List(name, []);
+ this.lists[name] = newList;
+ return newList;
+};
+
+/**
+ * Call to destroy a target.
+ * @abstract
+ */
+Target.prototype.dispose = function () {
+
+};
+
+module.exports = Target;
diff --git a/src/engine/thread.js b/src/engine/thread.js
index 07ceaca35..9aeb559f3 100644
--- a/src/engine/thread.js
+++ b/src/engine/thread.js
@@ -9,11 +9,7 @@ function Thread (firstBlock) {
* @type {!string}
*/
this.topBlock = firstBlock;
- /**
- * ID of next block that the thread will execute, or null if none.
- * @type {?string}
- */
- this.nextBlock = firstBlock;
+
/**
* Stack for the thread. When the sequencer enters a control structure,
* the block is pushed onto the stack so we know where to exit.
@@ -34,32 +30,208 @@ function Thread (firstBlock) {
this.status = 0; /* Thread.STATUS_RUNNING */
/**
- * Yield timer ID (for checking when the thread should unyield).
- * @type {number}
+ * Target of this thread.
+ * @type {?Target}
*/
- this.yieldTimerId = -1;
+ this.target = null;
+
+ /**
+ * Whether the thread requests its script to glow during this frame.
+ * @type {boolean}
+ */
+ this.requestScriptGlowInFrame = false;
+
+ /**
+ * Which block ID should glow during this frame, if any.
+ * @type {?string}
+ */
+ this.blockGlowInFrame = null;
+
+ /**
+ * A timer for when the thread enters warp mode.
+ * Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
+ * @type {?Timer}
+ */
+ this.warpTimer = null;
}
/**
* Thread status for initialized or running thread.
- * Threads are in this state when the primitive is called for the first time.
+ * This is the default state for a thread - execution should run normally,
+ * stepping from block to block.
* @const
*/
Thread.STATUS_RUNNING = 0;
/**
- * Thread status for a yielded thread.
- * Threads are in this state when a primitive has yielded.
+ * Threads are in this state when a primitive is waiting on a promise;
+ * execution is paused until the promise changes thread status.
* @const
*/
-Thread.STATUS_YIELD = 1;
+Thread.STATUS_PROMISE_WAIT = 1;
+
+/**
+ * Thread status for yield.
+ * @const
+ */
+Thread.STATUS_YIELD = 2;
+
+/**
+ * Thread status for a single-tick yield. This will be cleared when the
+ * thread is resumed.
+ * @const
+ */
+Thread.STATUS_YIELD_TICK = 3;
/**
* Thread status for a finished/done thread.
- * Thread is moved to this state when the interpreter
- * can proceed with execution.
+ * Thread is in this state when there are no more blocks to execute.
* @const
*/
-Thread.STATUS_DONE = 2;
+Thread.STATUS_DONE = 4;
+
+/**
+ * Push stack and update stack frames appropriately.
+ * @param {string} blockId Block ID to push to stack.
+ */
+Thread.prototype.pushStack = function (blockId) {
+ this.stack.push(blockId);
+ // Push an empty stack frame, if we need one.
+ // Might not, if we just popped the stack.
+ if (this.stack.length > this.stackFrames.length) {
+ // Copy warp mode from any higher level.
+ var warpMode = false;
+ if (this.stackFrames[this.stackFrames.length - 1]) {
+ warpMode = this.stackFrames[this.stackFrames.length - 1].warpMode;
+ }
+ this.stackFrames.push({
+ isLoop: false, // Whether this level of the stack is a loop.
+ warpMode: warpMode, // Whether this level is in warp mode.
+ reported: {}, // Collects reported input values.
+ waitingReporter: null, // Name of waiting reporter.
+ params: {}, // Procedure parameters.
+ executionContext: {} // A context passed to block implementations.
+ });
+ }
+};
+
+/**
+ * Pop last block on the stack and its stack frame.
+ * @return {string} Block ID popped from the stack.
+ */
+Thread.prototype.popStack = function () {
+ this.stackFrames.pop();
+ return this.stack.pop();
+};
+
+/**
+ * Get top stack item.
+ * @return {?string} Block ID on top of stack.
+ */
+Thread.prototype.peekStack = function () {
+ return this.stack[this.stack.length - 1];
+};
+
+
+/**
+ * Get top stack frame.
+ * @return {?Object} Last stack frame stored on this thread.
+ */
+Thread.prototype.peekStackFrame = function () {
+ return this.stackFrames[this.stackFrames.length - 1];
+};
+
+/**
+ * Get stack frame above the current top.
+ * @return {?Object} Second to last stack frame stored on this thread.
+ */
+Thread.prototype.peekParentStackFrame = function () {
+ return this.stackFrames[this.stackFrames.length - 2];
+};
+
+/**
+ * Push a reported value to the parent of the current stack frame.
+ * @param {*} value Reported value to push.
+ */
+Thread.prototype.pushReportedValue = function (value) {
+ var parentStackFrame = this.peekParentStackFrame();
+ if (parentStackFrame) {
+ var waitingReporter = parentStackFrame.waitingReporter;
+ parentStackFrame.reported[waitingReporter] = value;
+ }
+};
+
+/**
+ * Add a parameter to the stack frame.
+ * Use when calling a procedure with parameter values.
+ * @param {!string} paramName Name of parameter.
+ * @param {*} value Value to set for parameter.
+ */
+Thread.prototype.pushParam = function (paramName, value) {
+ var stackFrame = this.peekStackFrame();
+ stackFrame.params[paramName] = value;
+};
+
+/**
+ * Get a parameter at the lowest possible level of the stack.
+ * @param {!string} paramName Name of parameter.
+ * @return {*} value Value for parameter.
+ */
+Thread.prototype.getParam = function (paramName) {
+ for (var i = this.stackFrames.length - 1; i >= 0; i--) {
+ var frame = this.stackFrames[i];
+ if (frame.params.hasOwnProperty(paramName)) {
+ return frame.params[paramName];
+ }
+ }
+ return null;
+};
+
+/**
+ * Whether the current execution of a thread is at the top of the stack.
+ * @return {Boolean} True if execution is at top of the stack.
+ */
+Thread.prototype.atStackTop = function () {
+ return this.peekStack() === this.topBlock;
+};
+
+
+/**
+ * Switch the thread to the next block at the current level of the stack.
+ * For example, this is used in a standard sequence of blocks,
+ * where execution proceeds from one block to the next.
+ */
+Thread.prototype.goToNextBlock = function () {
+ var nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
+ // Copy warp mode to next block.
+ var warpMode = this.peekStackFrame().warpMode;
+ // The current block is on the stack - pop it and push the next.
+ // Note that this could push `null` - that is handled by the sequencer.
+ this.popStack();
+ this.pushStack(nextBlockId);
+ if (this.peekStackFrame()) {
+ this.peekStackFrame().warpMode = warpMode;
+ }
+};
+
+/**
+ * Attempt to determine whether a procedure call is recursive,
+ * by examining the stack.
+ * @param {!string} procedureCode Procedure code of procedure being called.
+ * @return {boolean} True if the call appears recursive.
+ */
+Thread.prototype.isRecursiveCall = function (procedureCode) {
+ var callCount = 5; // Max number of enclosing procedure calls to examine.
+ var sp = this.stack.length - 1;
+ for (var i = sp - 1; i >= 0; i--) {
+ var block = this.target.blocks.getBlock(this.stack[i]);
+ if (block.opcode == 'procedures_callnoreturn' &&
+ block.mutation.proccode == procedureCode) {
+ return true;
+ }
+ if (--callCount < 0) return false;
+ }
+ return false;
+};
module.exports = Thread;
diff --git a/src/engine/variable.js b/src/engine/variable.js
new file mode 100644
index 000000000..4e5e5e6e3
--- /dev/null
+++ b/src/engine/variable.js
@@ -0,0 +1,18 @@
+/**
+ * @fileoverview
+ * Object representing a Scratch variable.
+ */
+
+/**
+ * @param {!string} name Name of the variable.
+ * @param {(string|Number)} value Value of the variable.
+ * @param {boolean} isCloud Whether the variable is stored in the cloud.
+ * @constructor
+ */
+function Variable (name, value, isCloud) {
+ this.name = name;
+ this.value = value;
+ this.isCloud = isCloud;
+}
+
+module.exports = Variable;
diff --git a/src/import/sb2import.js b/src/import/sb2import.js
new file mode 100644
index 000000000..b97144d20
--- /dev/null
+++ b/src/import/sb2import.js
@@ -0,0 +1,418 @@
+/**
+ * @fileoverview
+ * Partial implementation of an SB2 JSON importer.
+ * Parses provided JSON and then generates all needed
+ * scratch-vm runtime structures.
+ */
+
+var Blocks = require('../engine/blocks');
+var Clone = require('../sprites/clone');
+var Sprite = require('../sprites/sprite');
+var Color = require('../util/color.js');
+var uid = require('../util/uid');
+var specMap = require('./sb2specmap');
+var Variable = require('../engine/variable');
+var List = require('../engine/list');
+
+/**
+ * Top-level handler. Parse provided JSON,
+ * and process the top-level object (the stage object).
+ * @param {!string} json SB2-format JSON to load.
+ * @param {!Runtime} runtime Runtime object to load all structures into.
+ */
+function sb2import (json, runtime) {
+ parseScratchObject(
+ JSON.parse(json),
+ runtime,
+ true
+ );
+}
+
+/**
+ * Parse a single "Scratch object" and create all its in-memory VM objects.
+ * @param {!Object} object From-JSON "Scratch object:" sprite, stage, watcher.
+ * @param {!Runtime} runtime Runtime object to load all structures into.
+ * @param {boolean} topLevel Whether this is the top-level object (stage).
+ */
+function parseScratchObject (object, runtime, topLevel) {
+ if (!object.hasOwnProperty('objName')) {
+ // Watcher/monitor - skip this object until those are implemented in VM.
+ // @todo
+ return;
+ }
+ // Blocks container for this object.
+ var blocks = new Blocks();
+ // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
+ var sprite = new Sprite(blocks, runtime);
+ // Sprite/stage name from JSON.
+ if (object.hasOwnProperty('objName')) {
+ sprite.name = object.objName;
+ }
+ // Costumes from JSON.
+ if (object.hasOwnProperty('costumes')) {
+ for (var i = 0; i < object.costumes.length; i++) {
+ var costume = object.costumes[i];
+ // @todo: Make sure all the relevant metadata is being pulled out.
+ sprite.costumes.push({
+ skin: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/'
+ + costume.baseLayerMD5 + '/get/',
+ name: costume.costumeName,
+ bitmapResolution: costume.bitmapResolution,
+ rotationCenterX: costume.rotationCenterX,
+ rotationCenterY: costume.rotationCenterY
+ });
+ }
+ }
+ // If included, parse any and all scripts/blocks on the object.
+ if (object.hasOwnProperty('scripts')) {
+ parseScripts(object.scripts, blocks);
+ }
+ // Create the first clone, and load its run-state from JSON.
+ var target = sprite.createClone();
+ // Add it to the runtime's list of targets.
+ runtime.targets.push(target);
+ // Load target properties from JSON.
+ if (object.hasOwnProperty('variables')) {
+ for (var j = 0; j < object.variables.length; j++) {
+ var variable = object.variables[j];
+ target.variables[variable.name] = new Variable(
+ variable.name,
+ variable.value,
+ variable.isPersistent
+ );
+ }
+ }
+ if (object.hasOwnProperty('lists')) {
+ for (var k = 0; k < object.lists.length; k++) {
+ var list = object.lists[k];
+ // @todo: monitor properties.
+ target.lists[list.listName] = new List(
+ list.listName,
+ list.contents
+ );
+ }
+ }
+ if (object.hasOwnProperty('scratchX')) {
+ target.x = object.scratchX;
+ }
+ if (object.hasOwnProperty('scratchY')) {
+ target.y = object.scratchY;
+ }
+ if (object.hasOwnProperty('direction')) {
+ target.direction = object.direction;
+ }
+ if (object.hasOwnProperty('scale')) {
+ // SB2 stores as 1.0 = 100%; we use % in the VM.
+ target.size = object.scale * 100;
+ }
+ if (object.hasOwnProperty('visible')) {
+ target.visible = object.visible;
+ }
+ if (object.hasOwnProperty('currentCostumeIndex')) {
+ target.currentCostume = Math.round(object.currentCostumeIndex);
+ }
+ if (object.hasOwnProperty('rotationStyle')) {
+ if (object.rotationStyle == 'none') {
+ target.rotationStyle = Clone.ROTATION_STYLE_NONE;
+ } else if (object.rotationStyle == 'leftRight') {
+ target.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
+ } else if (object.rotationStyle == 'normal') {
+ target.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
+ }
+ }
+ target.isStage = topLevel;
+ target.updateAllDrawableProperties();
+ // The stage will have child objects; recursively process them.
+ if (object.children) {
+ for (var m = 0; m < object.children.length; m++) {
+ parseScratchObject(object.children[m], runtime, false);
+ }
+ }
+}
+
+/**
+ * Parse a Scratch object's scripts into VM blocks.
+ * This should only handle top-level scripts that include X, Y coordinates.
+ * @param {!Object} scripts Scripts object from SB2 JSON.
+ * @param {!Blocks} blocks Blocks object to load parsed blocks into.
+ */
+function parseScripts (scripts, blocks) {
+ for (var i = 0; i < scripts.length; i++) {
+ var script = scripts[i];
+ var scriptX = script[0];
+ var scriptY = script[1];
+ var blockList = script[2];
+ var parsedBlockList = parseBlockList(blockList);
+ if (parsedBlockList[0]) {
+ // Adjust script coordinates to account for
+ // larger block size in scratch-blocks.
+ // @todo: Determine more precisely the right formulas here.
+ parsedBlockList[0].x = scriptX * 1.1;
+ parsedBlockList[0].y = scriptY * 1.1;
+ parsedBlockList[0].topLevel = true;
+ parsedBlockList[0].parent = null;
+ }
+ // Flatten children and create add the blocks.
+ var convertedBlocks = flatten(parsedBlockList);
+ for (var j = 0; j < convertedBlocks.length; j++) {
+ blocks.createBlock(convertedBlocks[j]);
+ }
+ }
+}
+
+/**
+ * Parse any list of blocks from SB2 JSON into a list of VM-format blocks.
+ * Could be used to parse a top-level script,
+ * a list of blocks in a branch (e.g., in forever),
+ * or a list of blocks in an argument (e.g., move [pick random...]).
+ * @param {Array.} blockList SB2 JSON-format block list.
+ * @return {Array.} Scratch VM-format block list.
+ */
+function parseBlockList (blockList) {
+ var resultingList = [];
+ var previousBlock = null; // For setting next.
+ for (var i = 0; i < blockList.length; i++) {
+ var block = blockList[i];
+ var parsedBlock = parseBlock(block);
+ if (previousBlock) {
+ parsedBlock.parent = previousBlock.id;
+ previousBlock.next = parsedBlock.id;
+ }
+ previousBlock = parsedBlock;
+ resultingList.push(parsedBlock);
+ }
+ return resultingList;
+}
+
+/**
+ * Flatten a block tree into a block list.
+ * Children are temporarily stored on the `block.children` property.
+ * @param {Array.} blocks list generated by `parseBlockList`.
+ * @return {Array.} Flattened list to be passed to `blocks.createBlock`.
+ */
+function flatten (blocks) {
+ var finalBlocks = [];
+ for (var i = 0; i < blocks.length; i++) {
+ var block = blocks[i];
+ finalBlocks.push(block);
+ if (block.children) {
+ finalBlocks = finalBlocks.concat(flatten(block.children));
+ }
+ delete block.children;
+ }
+ return finalBlocks;
+}
+
+/**
+ * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
+ * into an argument map. This allows us to provide the expected inputs
+ * to a mutated procedure call.
+ * @param {string} procCode Scratch 2.0 procedure string.
+ * @return {Object} Argument map compatible with those in sb2specmap.
+ */
+function parseProcedureArgMap (procCode) {
+ var argMap = [
+ {} // First item in list is op string.
+ ];
+ var INPUT_PREFIX = 'input';
+ var inputCount = 0;
+ // Split by %n, %b, %s.
+ var parts = procCode.split(/(?=[^\\]\%[nbs])/);
+ for (var i = 0; i < parts.length; i++) {
+ var part = parts[i].trim();
+ if (part.substring(0, 1) == '%') {
+ var argType = part.substring(1, 2);
+ var arg = {
+ type: 'input',
+ inputName: INPUT_PREFIX + (inputCount++)
+ };
+ if (argType == 'n') {
+ arg.inputOp = 'math_number';
+ } else if (argType == 's') {
+ arg.inputOp = 'text';
+ }
+ argMap.push(arg);
+ }
+ }
+ return argMap;
+}
+
+/**
+ * Parse a single SB2 JSON-formatted block and its children.
+ * @param {!Object} sb2block SB2 JSON-formatted block.
+ * @return {Object} Scratch VM format block.
+ */
+function parseBlock (sb2block) {
+ // First item in block object is the old opcode (e.g., 'forward:').
+ var oldOpcode = sb2block[0];
+ // Convert the block using the specMap. See sb2specmap.js.
+ if (!oldOpcode || !specMap[oldOpcode]) {
+ console.warn('Couldn\'t find SB2 block: ', oldOpcode);
+ return;
+ }
+ var blockMetadata = specMap[oldOpcode];
+ // Block skeleton.
+ var activeBlock = {
+ id: uid(), // Generate a new block unique ID.
+ opcode: blockMetadata.opcode, // Converted, e.g. "motion_movesteps".
+ inputs: {}, // Inputs to this block and the blocks they point to.
+ fields: {}, // Fields on this block and their values.
+ next: null, // Next block.
+ shadow: false, // No shadow blocks in an SB2 by default.
+ children: [] // Store any generated children, flattened in `flatten`.
+ };
+ // For a procedure call, generate argument map from proc string.
+ if (oldOpcode == 'call') {
+ blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
+ }
+ // Look at the expected arguments in `blockMetadata.argMap.`
+ // The basic problem here is to turn positional SB2 arguments into
+ // non-positional named Scratch VM arguments.
+ for (var i = 0; i < blockMetadata.argMap.length; i++) {
+ var expectedArg = blockMetadata.argMap[i];
+ var providedArg = sb2block[i + 1]; // (i = 0 is opcode)
+ // Whether the input is obscuring a shadow.
+ var shadowObscured = false;
+ // Positional argument is an input.
+ if (expectedArg.type == 'input') {
+ // Create a new block and input metadata.
+ var inputUid = uid();
+ activeBlock.inputs[expectedArg.inputName] = {
+ name: expectedArg.inputName,
+ block: null,
+ shadow: null
+ };
+ if (typeof providedArg == 'object' && providedArg) {
+ // Block or block list occupies the input.
+ var innerBlocks;
+ if (typeof providedArg[0] == 'object' && providedArg[0]) {
+ // Block list occupies the input.
+ innerBlocks = parseBlockList(providedArg);
+ } else {
+ // Single block occupies the input.
+ innerBlocks = [parseBlock(providedArg)];
+ }
+ var previousBlock = null;
+ for (var j = 0; j < innerBlocks.length; j++) {
+ if (j == 0) {
+ innerBlocks[j].parent = activeBlock.id;
+ } else {
+ innerBlocks[j].parent = previousBlock;
+ }
+ previousBlock = innerBlocks[j].id;
+ }
+ // Obscures any shadow.
+ shadowObscured = true;
+ activeBlock.inputs[expectedArg.inputName].block = (
+ innerBlocks[0].id
+ );
+ activeBlock.children = (
+ activeBlock.children.concat(innerBlocks)
+ );
+ }
+ // Generate a shadow block to occupy the input.
+ if (!expectedArg.inputOp) {
+ // No editable shadow input; e.g., for a boolean.
+ continue;
+ }
+ // Each shadow has a field generated for it automatically.
+ // Value to be filled in the field.
+ var fieldValue = providedArg;
+ // Shadows' field names match the input name, except for these:
+ var fieldName = expectedArg.inputName;
+ if (expectedArg.inputOp == 'math_number' ||
+ expectedArg.inputOp == 'math_whole_number' ||
+ expectedArg.inputOp == 'math_positive_number' ||
+ expectedArg.inputOp == 'math_integer' ||
+ expectedArg.inputOp == 'math_angle') {
+ fieldName = 'NUM';
+ // Fields are given Scratch 2.0 default values if obscured.
+ if (shadowObscured) {
+ fieldValue = 10;
+ }
+ } else if (expectedArg.inputOp == 'text') {
+ fieldName = 'TEXT';
+ if (shadowObscured) {
+ fieldValue = '';
+ }
+ } else if (expectedArg.inputOp == 'colour_picker') {
+ // Convert SB2 color to hex.
+ fieldValue = Color.decimalToHex(providedArg);
+ fieldName = 'COLOUR';
+ if (shadowObscured) {
+ fieldValue = '#990000';
+ }
+ }
+ var fields = {};
+ fields[fieldName] = {
+ name: fieldName,
+ value: fieldValue
+ };
+ activeBlock.children.push({
+ id: inputUid,
+ opcode: expectedArg.inputOp,
+ inputs: {},
+ fields: fields,
+ next: null,
+ topLevel: false,
+ parent: activeBlock.id,
+ shadow: true
+ });
+ activeBlock.inputs[expectedArg.inputName].shadow = inputUid;
+ // If no block occupying the input, alias to the shadow.
+ if (!activeBlock.inputs[expectedArg.inputName].block) {
+ activeBlock.inputs[expectedArg.inputName].block = inputUid;
+ }
+ } else if (expectedArg.type == 'field') {
+ // Add as a field on this block.
+ activeBlock.fields[expectedArg.fieldName] = {
+ name: expectedArg.fieldName,
+ value: providedArg
+ };
+ }
+ }
+ // Special cases to generate mutations.
+ if (oldOpcode == 'stopScripts') {
+ // Mutation for stop block: if the argument is 'other scripts',
+ // the block needs a next connection.
+ if (sb2block[1] == 'other scripts in sprite' ||
+ sb2block[1] == 'other scripts in stage') {
+ activeBlock.mutation = {
+ tagName: 'mutation',
+ hasnext: 'true',
+ children: []
+ };
+ }
+ } else if (oldOpcode == 'procDef') {
+ // Mutation for procedure definition:
+ // store all 2.0 proc data.
+ var procData = sb2block.slice(1);
+ activeBlock.mutation = {
+ tagName: 'mutation',
+ proccode: procData[0], // e.g., "abc %n %b %s"
+ argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2']
+ argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc']
+ warp: procData[3], // Warp mode, e.g., true/false.
+ children: []
+ };
+ } else if (oldOpcode == 'call') {
+ // Mutation for procedure call:
+ // string for proc code (e.g., "abc %n %b %s").
+ activeBlock.mutation = {
+ tagName: 'mutation',
+ children: [],
+ proccode: sb2block[1]
+ };
+ } else if (oldOpcode == 'getParam') {
+ // Mutation for procedure parameter.
+ activeBlock.mutation = {
+ tagName: 'mutation',
+ children: [],
+ paramname: sb2block[1], // Name of parameter.
+ shape: sb2block[2] // Shape - in 2.0, 'r' or 'b'.
+ };
+ }
+ return activeBlock;
+}
+
+module.exports = sb2import;
diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js
new file mode 100644
index 000000000..1cf0620fc
--- /dev/null
+++ b/src/import/sb2specmap.js
@@ -0,0 +1,1388 @@
+/**
+ * @fileoverview
+ * The specMap below handles a few pieces of "translation" work between
+ * the SB2 JSON format and the data we need to run a project
+ * in the Scratch 3.0 VM.
+ * Notably:
+ * - Map 2.0 and 1.4 opcodes (forward:) into 3.0-format (motion_movesteps).
+ * - Map ordered, unnamed args to unordered, named inputs and fields.
+ * Keep this up-to-date as 3.0 blocks are renamed, changed, etc.
+ * Originally this was generated largely by a hand-guided scripting process.
+ * The relevant data lives here:
+ * https://github.com/LLK/scratch-flash/blob/master/src/Specs.as
+ * (for the old opcode and argument order).
+ * and here:
+ * https://github.com/LLK/scratch-blocks/tree/develop/blocks_vertical
+ * (for the new opcodes and argument names).
+ * and here:
+ * https://github.com/LLK/scratch-blocks/blob/develop/tests/
+ * (for the shadow blocks created for each block).
+ * I started with the `commands` array in Specs.as, and discarded irrelevant
+ * properties. By hand, I matched the opcode name to the 3.0 opcode.
+ * Finally, I filled in the expected arguments as below.
+ */
+var specMap = {
+ 'forward:':{
+ 'opcode':'motion_movesteps',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'STEPS'
+ }
+ ]
+ },
+ 'turnRight:':{
+ 'opcode':'motion_turnright',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'DEGREES'
+ }
+ ]
+ },
+ 'turnLeft:':{
+ 'opcode':'motion_turnleft',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'DEGREES'
+ }
+ ]
+ },
+ 'heading:':{
+ 'opcode':'motion_pointindirection',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_angle',
+ 'inputName':'DIRECTION'
+ }
+ ]
+ },
+ 'pointTowards:':{
+ 'opcode':'motion_pointtowards',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'motion_pointtowards_menu',
+ 'inputName':'TOWARDS'
+ }
+ ]
+ },
+ 'gotoX:y:':{
+ 'opcode':'motion_gotoxy',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'X'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'Y'
+ }
+ ]
+ },
+ 'gotoSpriteOrMouse:':{
+ 'opcode':'motion_goto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'motion_goto_menu',
+ 'inputName':'TO'
+ }
+ ]
+ },
+ 'glideSecs:toX:y:elapsed:from:':{
+ 'opcode':'motion_glidesecstoxy',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SECS'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'X'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'Y'
+ }
+ ]
+ },
+ 'changeXposBy:':{
+ 'opcode':'motion_changexby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'DX'
+ }
+ ]
+ },
+ 'xpos:':{
+ 'opcode':'motion_setx',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'X'
+ }
+ ]
+ },
+ 'changeYposBy:':{
+ 'opcode':'motion_changeyby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'DY'
+ }
+ ]
+ },
+ 'ypos:':{
+ 'opcode':'motion_sety',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'Y'
+ }
+ ]
+ },
+ 'bounceOffEdge':{
+ 'opcode':'motion_ifonedgebounce',
+ 'argMap':[
+ ]
+ },
+ 'setRotationStyle':{
+ 'opcode':'motion_setrotationstyle',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'motion_setrotationstyle_menu',
+ 'inputName':'STYLE'
+ }
+ ]
+ },
+ 'xpos':{
+ 'opcode':'motion_xposition',
+ 'argMap':[
+ ]
+ },
+ 'ypos':{
+ 'opcode':'motion_yposition',
+ 'argMap':[
+ ]
+ },
+ 'heading':{
+ 'opcode':'motion_direction',
+ 'argMap':[
+ ]
+ },
+ 'say:duration:elapsed:from:':{
+ 'opcode':'looks_sayforsecs',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'MESSAGE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SECS'
+ }
+ ]
+ },
+ 'say:':{
+ 'opcode':'looks_say',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'MESSAGE'
+ }
+ ]
+ },
+ 'think:duration:elapsed:from:':{
+ 'opcode':'looks_thinkforsecs',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'MESSAGE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SECS'
+ }
+ ]
+ },
+ 'think:':{
+ 'opcode':'looks_think',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'MESSAGE'
+ }
+ ]
+ },
+ 'show':{
+ 'opcode':'looks_show',
+ 'argMap':[
+ ]
+ },
+ 'hide':{
+ 'opcode':'looks_hide',
+ 'argMap':[
+ ]
+ },
+ 'lookLike:':{
+ 'opcode':'looks_switchcostumeto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'looks_costume',
+ 'inputName':'COSTUME'
+ }
+ ]
+ },
+ 'nextCostume':{
+ 'opcode':'looks_nextcostume',
+ 'argMap':[
+ ]
+ },
+ 'startScene':{
+ 'opcode':'looks_switchbackdropto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'looks_backdrops',
+ 'inputName':'BACKDROP'
+ }
+ ]
+ },
+ 'changeGraphicEffect:by:':{
+ 'opcode':'looks_changeeffectby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'looks_effectmenu',
+ 'inputName':'EFFECT'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'CHANGE'
+ }
+ ]
+ },
+ 'setGraphicEffect:to:':{
+ 'opcode':'looks_seteffectto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'looks_effectmenu',
+ 'inputName':'EFFECT'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'VALUE'
+ }
+ ]
+ },
+ 'filterReset':{
+ 'opcode':'looks_cleargraphiceffects',
+ 'argMap':[
+ ]
+ },
+ 'changeSizeBy:':{
+ 'opcode':'looks_changesizeby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'CHANGE'
+ }
+ ]
+ },
+ 'setSizeTo:':{
+ 'opcode':'looks_setsizeto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SIZE'
+ }
+ ]
+ },
+ 'comeToFront':{
+ 'opcode':'looks_gotofront',
+ 'argMap':[
+ ]
+ },
+ 'goBackByLayers:':{
+ 'opcode':'looks_gobacklayers',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_integer',
+ 'inputName':'NUM'
+ }
+ ]
+ },
+ 'costumeIndex':{
+ 'opcode':'looks_costumeorder',
+ 'argMap':[
+ ]
+ },
+ 'sceneName':{
+ 'opcode':'looks_backdropname',
+ 'argMap':[
+ ]
+ },
+ 'scale':{
+ 'opcode':'looks_size',
+ 'argMap':[
+ ]
+ },
+ 'startSceneAndWait':{
+ 'opcode':'looks_switchbackdroptoandwait',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'looks_backdrops',
+ 'inputName':'BACKDROP'
+ }
+ ]
+ },
+ 'nextScene':{
+ 'opcode':'looks_nextbackdrop',
+ 'argMap':[
+ ]
+ },
+ 'backgroundIndex':{
+ 'opcode':'looks_backdroporder',
+ 'argMap':[
+ ]
+ },
+ 'playSound:':{
+ 'opcode':'sound_play',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sound_sounds_option',
+ 'inputName':'SOUND_MENU'
+ }
+ ]
+ },
+ 'doPlaySoundAndWait':{
+ 'opcode':'sound_playuntildone',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sound_sounds_option',
+ 'inputName':'SOUND_MENU'
+ }
+ ]
+ },
+ 'stopAllSounds':{
+ 'opcode':'sound_stopallsounds',
+ 'argMap':[
+ ]
+ },
+ 'playDrum':{
+ 'opcode':'sound_playdrumforbeats',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'DRUMTYPE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'BEATS'
+ }
+ ]
+ },
+ 'rest:elapsed:from:':{
+ 'opcode':'sound_restforbeats',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'BEATS'
+ }
+ ]
+ },
+ 'noteOn:duration:elapsed:from:':{
+ 'opcode':'sound_playnoteforbeats',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NOTE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'BEATS'
+ }
+ ]
+ },
+ 'instrument:':{
+ 'opcode':'sound_setinstrumentto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'INSTRUMENT'
+ }
+ ]
+ },
+ 'changeVolumeBy:':{
+ 'opcode':'sound_changevolumeby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'VOLUME'
+ }
+ ]
+ },
+ 'setVolumeTo:':{
+ 'opcode':'sound_setvolumeto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'VOLUME'
+ }
+ ]
+ },
+ 'volume':{
+ 'opcode':'sound_volume',
+ 'argMap':[
+ ]
+ },
+ 'changeTempoBy:':{
+ 'opcode':'sound_changetempoby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'TEMPO'
+ }
+ ]
+ },
+ 'setTempoTo:':{
+ 'opcode':'sound_settempotobpm',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'TEMPO'
+ }
+ ]
+ },
+ 'tempo':{
+ 'opcode':'sound_tempo',
+ 'argMap':[
+ ]
+ },
+ 'clearPenTrails':{
+ 'opcode':'pen_clear',
+ 'argMap':[
+ ]
+ },
+ 'stampCostume':{
+ 'opcode':'pen_stamp',
+ 'argMap':[
+ ]
+ },
+ 'putPenDown':{
+ 'opcode':'pen_pendown',
+ 'argMap':[
+ ]
+ },
+ 'putPenUp':{
+ 'opcode':'pen_penup',
+ 'argMap':[
+ ]
+ },
+ 'penColor:':{
+ 'opcode':'pen_setpencolortocolor',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'colour_picker',
+ 'inputName':'COLOR'
+ }
+ ]
+ },
+ 'changePenHueBy:':{
+ 'opcode':'pen_changepencolorby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'COLOR'
+ }
+ ]
+ },
+ 'setPenHueTo:':{
+ 'opcode':'pen_setpencolortonum',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'COLOR'
+ }
+ ]
+ },
+ 'changePenShadeBy:':{
+ 'opcode':'pen_changepenshadeby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SHADE'
+ }
+ ]
+ },
+ 'setPenShadeTo:':{
+ 'opcode':'pen_changepenshadeby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SHADE'
+ }
+ ]
+ },
+ 'changePenSizeBy:':{
+ 'opcode':'pen_changepensizeby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SIZE'
+ }
+ ]
+ },
+ 'penSize:':{
+ 'opcode':'pen_setpensizeto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'SIZE'
+ }
+ ]
+ },
+ 'whenGreenFlag':{
+ 'opcode':'event_whenflagclicked',
+ 'argMap':[
+ ]
+ },
+ 'whenKeyPressed':{
+ 'opcode':'event_whenkeypressed',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'KEY_OPTION'
+ }
+ ]
+ },
+ 'whenClicked':{
+ 'opcode':'event_whenthisspriteclicked',
+ 'argMap':[
+ ]
+ },
+ 'whenSceneStarts':{
+ 'opcode':'event_whenbackdropswitchesto',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'BACKDROP'
+ }
+ ]
+ },
+ 'whenSensorGreaterThan':{
+ 'opcode':'event_whengreaterthan',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'WHENGREATERTHANMENU'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'VALUE'
+ }
+ ]
+ },
+ 'whenIReceive':{
+ 'opcode':'event_whenbroadcastreceived',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'BROADCAST_OPTION'
+ }
+ ]
+ },
+ 'broadcast:':{
+ 'opcode':'event_broadcast',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'event_broadcast_menu',
+ 'inputName':'BROADCAST_OPTION'
+ }
+ ]
+ },
+ 'doBroadcastAndWait':{
+ 'opcode':'event_broadcastandwait',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'event_broadcast_menu',
+ 'inputName':'BROADCAST_OPTION'
+ }
+ ]
+ },
+ 'wait:elapsed:from:':{
+ 'opcode':'control_wait',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_positive_number',
+ 'inputName':'DURATION'
+ }
+ ]
+ },
+ 'doRepeat':{
+ 'opcode':'control_repeat',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_whole_number',
+ 'inputName':'TIMES'
+ },
+ {
+ 'type':'input',
+ 'inputName': 'SUBSTACK'
+ }
+ ]
+ },
+ 'doForever':{
+ 'opcode':'control_forever',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'SUBSTACK'
+ }
+ ]
+ },
+ 'doIf':{
+ 'opcode':'control_if',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'CONDITION'
+ },
+ {
+ 'type':'input',
+ 'inputName':'SUBSTACK'
+ }
+ ]
+ },
+ 'doIfElse':{
+ 'opcode':'control_if_else',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'CONDITION'
+ },
+ {
+ 'type':'input',
+ 'inputName':'SUBSTACK'
+ },
+ {
+ 'type':'input',
+ 'inputName':'SUBSTACK2'
+ }
+ ]
+ },
+ 'doWaitUntil':{
+ 'opcode':'control_wait_until',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'CONDITION'
+ }
+ ]
+ },
+ 'doUntil':{
+ 'opcode':'control_repeat_until',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'CONDITION'
+ },
+ {
+ 'type':'input',
+ 'inputName':'SUBSTACK'
+ }
+ ]
+ },
+ 'stopScripts':{
+ 'opcode':'control_stop',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'STOP_OPTION'
+ }
+ ]
+ },
+ 'whenCloned':{
+ 'opcode':'control_start_as_clone',
+ 'argMap':[
+ ]
+ },
+ 'createCloneOf':{
+ 'opcode':'control_create_clone_of',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'control_create_clone_of_menu',
+ 'inputName':'CLONE_OPTION'
+ }
+ ]
+ },
+ 'deleteClone':{
+ 'opcode':'control_delete_this_clone',
+ 'argMap':[
+ ]
+ },
+ 'touching:':{
+ 'opcode':'sensing_touchingobject',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_touchingobjectmenu',
+ 'inputName':'TOUCHINGOBJECTMENU'
+ }
+ ]
+ },
+ 'touchingColor:':{
+ 'opcode':'sensing_touchingcolor',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'colour_picker',
+ 'inputName':'COLOR'
+ }
+ ]
+ },
+ 'color:sees:':{
+ 'opcode':'sensing_coloristouchingcolor',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'colour_picker',
+ 'inputName':'COLOR'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'colour_picker',
+ 'inputName':'COLOR2'
+ }
+ ]
+ },
+ 'distanceTo:':{
+ 'opcode':'sensing_distanceto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_distancetomenu',
+ 'inputName':'DISTANCETOMENU'
+ }
+ ]
+ },
+ 'doAsk':{
+ 'opcode':'sensing_askandwait',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'QUESTION'
+ }
+ ]
+ },
+ 'answer':{
+ 'opcode':'sensing_answer',
+ 'argMap':[
+ ]
+ },
+ 'keyPressed:':{
+ 'opcode':'sensing_keypressed',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_keyoptions',
+ 'inputName':'KEY_OPTION'
+ }
+ ]
+ },
+ 'mousePressed':{
+ 'opcode':'sensing_mousedown',
+ 'argMap':[
+ ]
+ },
+ 'mouseX':{
+ 'opcode':'sensing_mousex',
+ 'argMap':[
+ ]
+ },
+ 'mouseY':{
+ 'opcode':'sensing_mousey',
+ 'argMap':[
+ ]
+ },
+ 'soundLevel':{
+ 'opcode':'sensing_loudness',
+ 'argMap':[
+ ]
+ },
+ 'senseVideoMotion':{
+ 'opcode':'sensing_videoon',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_videoonmenuone',
+ 'inputName':'VIDEOONMENU1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'sensing_videoonmenutwo',
+ 'inputName':'VIDEOONMENU2'
+ }
+ ]
+ },
+ 'setVideoState':{
+ 'opcode':'sensing_videotoggle',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_videotogglemenu',
+ 'inputName':'VIDEOTOGGLEMENU'
+ }
+ ]
+ },
+ 'setVideoTransparency':{
+ 'opcode':'sensing_setvideotransparency',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'TRANSPARENCY'
+ }
+ ]
+ },
+ 'timer':{
+ 'opcode':'sensing_timer',
+ 'argMap':[
+ ]
+ },
+ 'timerReset':{
+ 'opcode':'sensing_resettimer',
+ 'argMap':[
+ ]
+ },
+ 'getAttribute:of:':{
+ 'opcode':'sensing_of',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_of_property_menu',
+ 'inputName':'PROPERTY'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'sensing_of_object_menu',
+ 'inputName':'OBJECT'
+ }
+ ]
+ },
+ 'timeAndDate':{
+ 'opcode':'sensing_current',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'sensing_currentmenu',
+ 'inputName':'CURRENTMENU'
+ }
+ ]
+ },
+ 'timestamp':{
+ 'opcode':'sensing_dayssince2000',
+ 'argMap':[
+ ]
+ },
+ 'getUserName':{
+ 'opcode':'sensing_username',
+ 'argMap':[
+ ]
+ },
+ '+':{
+ 'opcode':'operator_add',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM2'
+ }
+ ]
+ },
+ '-':{
+ 'opcode':'operator_subtract',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM2'
+ }
+ ]
+ },
+ '*':{
+ 'opcode':'operator_multiply',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM2'
+ }
+ ]
+ },
+ '/':{
+ 'opcode':'operator_divide',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM2'
+ }
+ ]
+ },
+ 'randomFrom:to:':{
+ 'opcode':'operator_random',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'FROM'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'TO'
+ }
+ ]
+ },
+ '<':{
+ 'opcode':'operator_lt',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND2'
+ }
+ ]
+ },
+ '=':{
+ 'opcode':'operator_equals',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND2'
+ }
+ ]
+ },
+ '>':{
+ 'opcode':'operator_gt',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'OPERAND2'
+ }
+ ]
+ },
+ '&':{
+ 'opcode':'operator_and',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'OPERAND1'
+ },
+ {
+ 'type':'input',
+ 'inputName':'OPERAND2'
+ }
+ ]
+ },
+ '|':{
+ 'opcode':'operator_or',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'OPERAND1'
+ },
+ {
+ 'type':'input',
+ 'inputName':'OPERAND2'
+ }
+ ]
+ },
+ 'not':{
+ 'opcode':'operator_not',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputName':'OPERAND'
+ }
+ ]
+ },
+ 'concatenate:with:':{
+ 'opcode':'operator_join',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'STRING1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'STRING2'
+ }
+ ]
+ },
+ 'letter:of:':{
+ 'opcode':'operator_letter_of',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_whole_number',
+ 'inputName':'LETTER'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'STRING'
+ }
+ ]
+ },
+ 'stringLength:':{
+ 'opcode':'operator_length',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'STRING'
+ }
+ ]
+ },
+ '%':{
+ 'opcode':'operator_mod',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM1'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM2'
+ }
+ ]
+ },
+ 'rounded':{
+ 'opcode':'operator_round',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM'
+ }
+ ]
+ },
+ 'computeFunction:of:':{
+ 'opcode':'operator_mathop',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'operator_mathop_menu',
+ 'inputName':'OPERATOR'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'NUM'
+ }
+ ]
+ },
+ 'readVariable':{
+ 'opcode':'data_variable',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'data_variablemenu',
+ 'inputName':'VARIABLE'
+ }
+ ]
+ },
+ 'setVar:to:':{
+ 'opcode':'data_setvariableto',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'data_variablemenu',
+ 'inputName':'VARIABLE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'VALUE'
+ }
+ ]
+ },
+ 'changeVar:by:':{
+ 'opcode':'data_changevariableby',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'data_variablemenu',
+ 'inputName':'VARIABLE'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_number',
+ 'inputName':'VALUE'
+ }
+ ]
+ },
+ 'showVariable:':{
+ 'opcode':'data_showvariable',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'data_variablemenu',
+ 'inputName':'VARIABLE'
+ }
+ ]
+ },
+ 'hideVariable:':{
+ 'opcode':'data_hidevariable',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'data_variablemenu',
+ 'inputName':'VARIABLE'
+ }
+ ]
+ },
+ 'contentsOfList:':{
+ 'opcode':'data_list',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'append:toList:':{
+ 'opcode':'data_addtolist',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'ITEM'
+ },
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'deleteLine:ofList:':{
+ 'opcode':'data_deleteoflist',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_integer',
+ 'inputName':'INDEX'
+ },
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'insert:at:ofList:':{
+ 'opcode':'data_insertatlist',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'ITEM'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'math_integer',
+ 'inputName':'INDEX'
+ },
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'setLine:ofList:to:':{
+ 'opcode':'data_replaceitemoflist',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_integer',
+ 'inputName':'INDEX'
+ },
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'ITEM'
+ }
+ ]
+ },
+ 'getLine:ofList:':{
+ 'opcode':'data_itemoflist',
+ 'argMap':[
+ {
+ 'type':'input',
+ 'inputOp':'math_integer',
+ 'inputName':'INDEX'
+ },
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'lineCountOfList:':{
+ 'opcode':'data_lengthoflist',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'list:contains:':{
+ 'opcode':'data_listcontainsitem',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ },
+ {
+ 'type':'input',
+ 'inputOp':'text',
+ 'inputName':'ITEM'
+ }
+ ]
+ },
+ 'showList:':{
+ 'opcode':'data_showlist',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'hideList:':{
+ 'opcode':'data_hidelist',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'LIST'
+ }
+ ]
+ },
+ 'procDef':{
+ 'opcode':'procedures_defnoreturn',
+ 'argMap':[]
+ },
+ 'getParam':{
+ 'opcode':'procedures_param',
+ 'argMap':[]
+ },
+ 'call':{
+ 'opcode':'procedures_callnoreturn',
+ 'argMap':[]
+ }
+};
+module.exports = specMap;
diff --git a/src/index.js b/src/index.js
index d6e05e970..014dc2b23 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,9 @@ var EventEmitter = require('events');
var util = require('util');
var Runtime = require('./engine/runtime');
-var adapter = require('./engine/adapter');
+var sb2import = require('./import/sb2import');
+var Sprite = require('./sprites/sprite');
+var Blocks = require('./engine/blocks');
/**
* Handles connections between blocks, stage, and extensions.
@@ -11,73 +13,38 @@ var adapter = require('./engine/adapter');
*/
function VirtualMachine () {
var instance = this;
-
// Bind event emitter and runtime to VM instance
- // @todo Post message (Web Worker) polyfill
EventEmitter.call(instance);
- instance.runtime = new Runtime();
-
/**
- * Event listener for blocks. Handles validation and serves as a generic
- * adapter between the blocks and the runtime interface.
- *
- * @param {Object} Blockly "block" event
+ * VM runtime, to store blocks, I/O devices, sprites/targets, etc.
+ * @type {!Runtime}
*/
- instance.blockListener = function (e) {
- // Validate event
- if (typeof e !== 'object') return;
- if (typeof e.blockId !== 'string') return;
+ instance.runtime = new Runtime();
+ /**
+ * The "currently editing"/selected target ID for the VM.
+ * Block events from any Blockly workspace are routed to this target.
+ * @type {!string}
+ */
+ instance.editingTarget = null;
+ // Runtime emits are passed along as VM emits.
+ instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) {
+ instance.emit(Runtime.SCRIPT_GLOW_ON, {id: id});
+ });
+ instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) {
+ instance.emit(Runtime.SCRIPT_GLOW_OFF, {id: id});
+ });
+ instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) {
+ instance.emit(Runtime.BLOCK_GLOW_ON, {id: id});
+ });
+ instance.runtime.on(Runtime.BLOCK_GLOW_OFF, function (id) {
+ instance.emit(Runtime.BLOCK_GLOW_OFF, {id: id});
+ });
+ instance.runtime.on(Runtime.VISUAL_REPORT, function (id, value) {
+ instance.emit(Runtime.VISUAL_REPORT, {id: id, value: value});
+ });
- // Blocks
- switch (e.type) {
- case 'create':
- instance.runtime.createBlock(adapter(e), false);
- break;
- case 'change':
- instance.runtime.changeBlock({
- id: e.blockId,
- element: e.element,
- name: e.name,
- value: e.newValue
- });
- break;
- case 'move':
- instance.runtime.moveBlock({
- id: e.blockId,
- oldParent: e.oldParentId,
- oldField: e.oldInputName,
- newParent: e.newParentId,
- newField: e.newInputName
- });
- break;
- case 'delete':
- instance.runtime.deleteBlock({
- id: e.blockId
- });
- break;
- }
- };
-
- instance.flyoutBlockListener = function (e) {
- switch (e.type) {
- case 'create':
- instance.runtime.createBlock(adapter(e), true);
- break;
- case 'change':
- instance.runtime.changeBlock({
- id: e.blockId,
- element: e.element,
- name: e.name,
- value: e.newValue
- });
- break;
- case 'delete':
- instance.runtime.deleteBlock({
- id: e.blockId
- });
- break;
- }
- };
+ this.blockListener = this.blockListener.bind(this);
+ this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
}
/**
@@ -86,7 +53,259 @@ function VirtualMachine () {
util.inherits(VirtualMachine, EventEmitter);
/**
- * Export and bind to `window`
+ * Start running the VM - do this before anything else.
*/
+VirtualMachine.prototype.start = function () {
+ this.runtime.start();
+};
+
+/**
+ * "Green flag" handler - start all threads starting with a green flag.
+ */
+VirtualMachine.prototype.greenFlag = function () {
+ this.runtime.greenFlag();
+};
+
+/**
+ * Set whether the VM is in "turbo mode."
+ * When true, loops don't yield to redraw.
+ * @param {Boolean} turboModeOn Whether turbo mode should be set.
+ */
+VirtualMachine.prototype.setTurboMode = function (turboModeOn) {
+ this.runtime.turboMode = !!turboModeOn;
+};
+
+/**
+ * Set whether the VM is in "pause mode."
+ * When true, nothing is stepped.
+ * @param {Boolean} pauseModeOn Whether pause mode should be set.
+ */
+VirtualMachine.prototype.setPauseMode = function (pauseModeOn) {
+ this.runtime.setPauseMode(!!pauseModeOn);
+};
+
+/**
+ * Set whether the VM is in 2.0 "compatibility mode."
+ * When true, ticks go at 2.0 speed (30 TPS).
+ * @param {Boolean} compatibilityModeOn Whether compatibility mode is set.
+ */
+VirtualMachine.prototype.setCompatibilityMode = function (compatibilityModeOn) {
+ this.runtime.setCompatibilityMode(!!compatibilityModeOn);
+};
+
+/**
+ * Set whether the VM is in "single stepping mode."
+ * When true, blocks execute slowly and are highlighted visually.
+ * @param {Boolean} singleSteppingOn Whether single-stepping mode is set.
+ */
+VirtualMachine.prototype.setSingleSteppingMode = function (singleSteppingOn) {
+ this.runtime.setSingleSteppingMode(!!singleSteppingOn);
+};
+
+
+/**
+ * Set single-stepping mode speed.
+ * When in single-stepping mode, adjusts the speed of execution.
+ * @param {Number} speed Interval length in ms.
+ */
+VirtualMachine.prototype.setSingleSteppingSpeed = function (speed) {
+ this.runtime.setSingleSteppingSpeed(speed);
+};
+
+
+/**
+ * Stop all threads and running activities.
+ */
+VirtualMachine.prototype.stopAll = function () {
+ this.runtime.stopAll();
+};
+
+/**
+ * Clear out current running project data.
+ */
+VirtualMachine.prototype.clear = function () {
+ this.runtime.dispose();
+ this.editingTarget = null;
+ this.emitTargetsUpdate();
+};
+
+/**
+ * Get data for playground. Data comes back in an emitted event.
+ */
+VirtualMachine.prototype.getPlaygroundData = function () {
+ var instance = this;
+ // Only send back thread data for the current editingTarget.
+ var threadData = this.runtime.threads.filter(function(thread) {
+ return thread.target == instance.editingTarget;
+ });
+ // Remove the target key, since it's a circular reference.
+ var filteredThreadData = JSON.stringify(threadData, function(key, value) {
+ if (key == 'target') return undefined;
+ return value;
+ }, 2);
+ this.emit('playgroundData', {
+ blocks: this.editingTarget.blocks,
+ threads: filteredThreadData
+ });
+};
+
+/**
+ * Handle an animation frame.
+ */
+VirtualMachine.prototype.animationFrame = function () {
+ this.runtime.animationFrame();
+};
+
+/**
+ * Post I/O data to the virtual devices.
+ * @param {?string} device Name of virtual I/O device.
+ * @param {Object} data Any data object to post to the I/O device.
+ */
+VirtualMachine.prototype.postIOData = function (device, data) {
+ if (this.runtime.ioDevices[device]) {
+ this.runtime.ioDevices[device].postData(data);
+ }
+};
+
+/**
+ * Load a project from a Scratch 2.0 JSON representation.
+ * @param {?string} json JSON string representing the project.
+ */
+VirtualMachine.prototype.loadProject = function (json) {
+ this.clear();
+ // @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0.
+ sb2import(json, this.runtime);
+ // Select the first target for editing, e.g., the stage.
+ this.editingTarget = this.runtime.targets[0];
+ // Update the VM user's knowledge of targets and blocks on the workspace.
+ this.emitTargetsUpdate();
+ this.emitWorkspaceUpdate();
+ this.runtime.setEditingTarget(this.editingTarget);
+};
+
+/**
+ * Temporary way to make an empty project, in case the desired project
+ * cannot be loaded from the online server.
+ */
+VirtualMachine.prototype.createEmptyProject = function () {
+ // Stage.
+ var blocks2 = new Blocks();
+ var stage = new Sprite(blocks2, this.runtime);
+ stage.name = 'Stage';
+ stage.costumes.push({
+ skin: './assets/stage.png',
+ name: 'backdrop1',
+ bitmapResolution: 2,
+ rotationCenterX: 480,
+ rotationCenterY: 360
+ });
+ var target2 = stage.createClone();
+ this.runtime.targets.push(target2);
+ target2.x = 0;
+ target2.y = 0;
+ target2.direction = 90;
+ target2.size = 200;
+ target2.visible = true;
+ target2.isStage = true;
+ // Sprite1 (cat).
+ var blocks1 = new Blocks();
+ var sprite = new Sprite(blocks1, this.runtime);
+ sprite.name = 'Sprite1';
+ sprite.costumes.push({
+ skin: './assets/scratch_cat.svg',
+ name: 'costume1',
+ bitmapResolution: 1,
+ rotationCenterX: 47,
+ rotationCenterY: 55
+ });
+ var target1 = sprite.createClone();
+ this.runtime.targets.push(target1);
+ target1.x = 0;
+ target1.y = 0;
+ target1.direction = 90;
+ target1.size = 100;
+ target1.visible = true;
+ this.editingTarget = this.runtime.targets[0];
+ this.emitTargetsUpdate();
+ this.emitWorkspaceUpdate();
+};
+
+/**
+ * Set the renderer for the VM/runtime
+ * @param {!RenderWebGL} renderer The renderer to attach
+ */
+VirtualMachine.prototype.attachRenderer = function (renderer) {
+ this.runtime.attachRenderer(renderer);
+};
+
+/**
+ * Handle a Blockly event for the current editing target.
+ * @param {!Blockly.Event} e Any Blockly event.
+ */
+VirtualMachine.prototype.blockListener = function (e) {
+ if (this.editingTarget) {
+ this.editingTarget.blocks.blocklyListen(e, this.runtime);
+ }
+};
+
+/**
+ * Handle a Blockly event for the flyout.
+ * @param {!Blockly.Event} e Any Blockly event.
+ */
+VirtualMachine.prototype.flyoutBlockListener = function (e) {
+ this.runtime.flyoutBlocks.blocklyListen(e, this.runtime);
+};
+
+/**
+ * Set an editing target. An editor UI can use this function to switch
+ * between editing different targets, sprites, etc.
+ * After switching the editing target, the VM may emit updates
+ * to the list of targets and any attached workspace blocks
+ * (see `emitTargetsUpdate` and `emitWorkspaceUpdate`).
+ * @param {string} targetId Id of target to set as editing.
+ */
+VirtualMachine.prototype.setEditingTarget = function (targetId) {
+ // Has the target id changed? If not, exit.
+ if (targetId == this.editingTarget.id) {
+ return;
+ }
+ var target = this.runtime.getTargetById(targetId);
+ if (target) {
+ this.editingTarget = target;
+ // Emit appropriate UI updates.
+ this.emitTargetsUpdate();
+ this.emitWorkspaceUpdate();
+ this.runtime.setEditingTarget(target);
+ }
+};
+
+/**
+ * Emit metadata about available targets.
+ * An editor UI could use this to display a list of targets and show
+ * the currently editing one.
+ */
+VirtualMachine.prototype.emitTargetsUpdate = function () {
+ this.emit('targetsUpdate', {
+ // [[target id, human readable target name], ...].
+ targetList: this.runtime.targets.filter(function (target) {
+ // Don't report clones.
+ return !target.hasOwnProperty('isOriginal') || target.isOriginal;
+ }).map(function(target) {
+ return [target.id, target.getName()];
+ }),
+ // Currently editing target id.
+ editingTarget: this.editingTarget ? this.editingTarget.id : null
+ });
+};
+
+/**
+ * Emit an Blockly/scratch-blocks compatible XML representation
+ * of the current editing target's blocks.
+ */
+VirtualMachine.prototype.emitWorkspaceUpdate = function () {
+ this.emit('workspaceUpdate', {
+ 'xml': this.editingTarget.blocks.toXML()
+ });
+};
+
module.exports = VirtualMachine;
-if (typeof window !== 'undefined') window.VirtualMachine = module.exports;
diff --git a/src/io/clock.js b/src/io/clock.js
new file mode 100644
index 000000000..ce4c8f553
--- /dev/null
+++ b/src/io/clock.js
@@ -0,0 +1,37 @@
+var Timer = require('../util/timer');
+
+function Clock (runtime) {
+ this._projectTimer = new Timer();
+ this._projectTimer.start();
+ this._pausedTime = null;
+ this._paused = false;
+ /**
+ * Reference to the owning Runtime.
+ * @type{!Runtime}
+ */
+ this.runtime = runtime;
+}
+
+Clock.prototype.projectTimer = function () {
+ if (this._paused) {
+ return this._pausedTime / 1000;
+ }
+ return this._projectTimer.timeElapsed() / 1000;
+};
+
+Clock.prototype.pause = function () {
+ this._paused = true;
+ this._pausedTime = this._projectTimer.timeElapsed();
+};
+
+Clock.prototype.resume = function () {
+ this._paused = false;
+ var dt = this._projectTimer.timeElapsed() - this._pausedTime;
+ this._projectTimer.startTime += dt;
+};
+
+Clock.prototype.resetProjectTimer = function () {
+ this._projectTimer.start();
+};
+
+module.exports = Clock;
diff --git a/src/io/keyboard.js b/src/io/keyboard.js
new file mode 100644
index 000000000..ea423fd40
--- /dev/null
+++ b/src/io/keyboard.js
@@ -0,0 +1,85 @@
+var Cast = require('../util/cast');
+
+function Keyboard (runtime) {
+ /**
+ * List of currently pressed keys.
+ * @type{Array.}
+ */
+ this._keysPressed = [];
+ /**
+ * Reference to the owning Runtime.
+ * Can be used, for example, to activate hats.
+ * @type{!Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Convert a Scratch key name to a DOM keyCode.
+ * @param {Any} keyName Scratch key argument.
+ * @return {number} Key code corresponding to a DOM event.
+ */
+Keyboard.prototype._scratchKeyToKeyCode = function (keyName) {
+ if (typeof keyName == 'number') {
+ // Key codes placed in with number blocks.
+ return keyName;
+ }
+ var keyString = Cast.toString(keyName);
+ switch (keyString) {
+ case 'space': return 32;
+ case 'left arrow': return 37;
+ case 'up arrow': return 38;
+ case 'right arrow': return 39;
+ case 'down arrow': return 40;
+ // @todo: Consider adding other special keys here.
+ }
+ // Keys reported by DOM keyCode are upper case.
+ return keyString.toUpperCase().charCodeAt(0);
+};
+
+Keyboard.prototype._keyCodeToScratchKey = function (keyCode) {
+ if (keyCode >= 48 && keyCode <= 90) {
+ // Standard letter.
+ return String.fromCharCode(keyCode).toLowerCase();
+ }
+ switch (keyCode) {
+ case 32: return 'space';
+ case 37: return 'left arrow';
+ case 38: return 'up arrow';
+ case 39: return 'right arrow';
+ case 40: return 'down arrow';
+ }
+ return null;
+};
+
+Keyboard.prototype.postData = function (data) {
+ if (data.keyCode) {
+ var index = this._keysPressed.indexOf(data.keyCode);
+ if (data.isDown) {
+ // If not already present, add to the list.
+ if (index < 0) {
+ this._keysPressed.push(data.keyCode);
+ }
+ // Always trigger hats, even if it was already pressed.
+ this.runtime.startHats('event_whenkeypressed', {
+ 'KEY_OPTION': this._keyCodeToScratchKey(data.keyCode)
+ });
+ this.runtime.startHats('event_whenkeypressed', {
+ 'KEY_OPTION': 'any'
+ });
+ } else if (index > -1) {
+ // If already present, remove from the list.
+ this._keysPressed.splice(index, 1);
+ }
+ }
+};
+
+Keyboard.prototype.getKeyIsDown = function (key) {
+ if (key == 'any') {
+ return this._keysPressed.length > 0;
+ }
+ var keyCode = this._scratchKeyToKeyCode(key);
+ return this._keysPressed.indexOf(keyCode) > -1;
+};
+
+module.exports = Keyboard;
diff --git a/src/io/mouse.js b/src/io/mouse.js
new file mode 100644
index 000000000..6b9860ee7
--- /dev/null
+++ b/src/io/mouse.js
@@ -0,0 +1,57 @@
+var MathUtil = require('../util/math-util');
+
+function Mouse (runtime) {
+ this._x = 0;
+ this._y = 0;
+ this._isDown = false;
+ /**
+ * Reference to the owning Runtime.
+ * Can be used, for example, to activate hats.
+ * @type{!Runtime}
+ */
+ this.runtime = runtime;
+}
+
+Mouse.prototype.postData = function(data) {
+ if (data.x) {
+ this._x = data.x - data.canvasWidth / 2;
+ }
+ if (data.y) {
+ this._y = data.y - data.canvasHeight / 2;
+ }
+ if (typeof data.isDown !== 'undefined') {
+ this._isDown = data.isDown;
+ if (this._isDown) {
+ this._activateClickHats(data.x, data.y);
+ }
+ }
+};
+
+Mouse.prototype._activateClickHats = function (x, y) {
+ if (this.runtime.renderer) {
+ var drawableID = this.runtime.renderer.pick(x, y);
+ for (var i = 0; i < this.runtime.targets.length; i++) {
+ var target = this.runtime.targets[i];
+ if (target.hasOwnProperty('drawableID') &&
+ target.drawableID == drawableID) {
+ this.runtime.startHats('event_whenthisspriteclicked',
+ null, target);
+ return;
+ }
+ }
+ }
+};
+
+Mouse.prototype.getX = function () {
+ return MathUtil.clamp(this._x, -240, 240);
+};
+
+Mouse.prototype.getY = function () {
+ return MathUtil.clamp(-this._y, -180, 180);
+};
+
+Mouse.prototype.getIsDown = function () {
+ return this._isDown;
+};
+
+module.exports = Mouse;
diff --git a/src/sprites/clone.js b/src/sprites/clone.js
new file mode 100644
index 000000000..13d9434a9
--- /dev/null
+++ b/src/sprites/clone.js
@@ -0,0 +1,588 @@
+var util = require('util');
+var MathUtil = require('../util/math-util');
+var Target = require('../engine/target');
+
+/**
+ * Clone (instance) of a sprite.
+ * @param {!Sprite} sprite Reference to the sprite.
+ * @param {Runtime} runtime Reference to the runtime.
+ * @constructor
+ */
+function Clone(sprite, runtime) {
+ Target.call(this, sprite.blocks);
+ this.runtime = runtime;
+ /**
+ * Reference to the sprite that this is a clone of.
+ * @type {!Sprite}
+ */
+ this.sprite = sprite;
+ /**
+ * Reference to the global renderer for this VM, if one exists.
+ * @type {?RenderWebGLWorker}
+ */
+ this.renderer = null;
+ if (this.runtime) {
+ this.renderer = this.runtime.renderer;
+ }
+ /**
+ * ID of the drawable for this clone returned by the renderer, if rendered.
+ * @type {?Number}
+ */
+ this.drawableID = null;
+
+ /**
+ * Map of current graphic effect values.
+ * @type {!Object.}
+ */
+ this.effects = {
+ 'color': 0,
+ 'fisheye': 0,
+ 'whirl': 0,
+ 'pixelate': 0,
+ 'mosaic': 0,
+ 'brightness': 0,
+ 'ghost': 0
+ };
+}
+util.inherits(Clone, Target);
+
+/**
+ * Create a clone's drawable with the this.renderer.
+ */
+Clone.prototype.initDrawable = function () {
+ if (this.renderer) {
+ this.drawableID = this.renderer.createDrawable();
+ }
+ // If we're a clone, start the hats.
+ if (!this.isOriginal) {
+ this.runtime.startHats(
+ 'control_start_as_clone', null, this
+ );
+ }
+};
+
+// Clone-level properties.
+/**
+ * Whether this represents an "original" clone, i.e., created by the editor
+ * and not clone blocks. In interface terms, this true for a "sprite."
+ * @type {boolean}
+ */
+Clone.prototype.isOriginal = true;
+
+/**
+ * Whether this clone represents the Scratch stage.
+ * @type {boolean}
+ */
+Clone.prototype.isStage = false;
+
+/**
+ * Scratch X coordinate. Currently should range from -240 to 240.
+ * @type {Number}
+ */
+Clone.prototype.x = 0;
+
+/**
+ * Scratch Y coordinate. Currently should range from -180 to 180.
+ * @type {number}
+ */
+Clone.prototype.y = 0;
+
+/**
+ * Scratch direction. Currently should range from -179 to 180.
+ * @type {number}
+ */
+Clone.prototype.direction = 90;
+
+/**
+ * Whether the clone is currently visible.
+ * @type {boolean}
+ */
+Clone.prototype.visible = true;
+
+/**
+ * Size of clone as a percent of costume size. Ranges from 5% to 535%.
+ * @type {number}
+ */
+Clone.prototype.size = 100;
+
+/**
+ * Currently selected costume index.
+ * @type {number}
+ */
+Clone.prototype.currentCostume = 0;
+
+/**
+ * Rotation style for "all around"/spinning.
+ * @enum
+ */
+Clone.ROTATION_STYLE_ALL_AROUND = 'all around';
+
+/**
+ * Rotation style for "left-right"/flipping.
+ * @enum
+ */
+Clone.ROTATION_STYLE_LEFT_RIGHT = 'left-right';
+
+/**
+ * Rotation style for "no rotation."
+ * @enum
+ */
+Clone.ROTATION_STYLE_NONE = 'don\'t rotate';
+
+/**
+ * Current rotation style.
+ * @type {!string}
+ */
+Clone.prototype.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
+
+// End clone-level properties.
+
+/**
+ * Set the X and Y coordinates of a clone.
+ * @param {!number} x New X coordinate of clone, in Scratch coordinates.
+ * @param {!number} y New Y coordinate of clone, in Scratch coordinates.
+ */
+Clone.prototype.setXY = function (x, y) {
+ if (this.isStage) {
+ return;
+ }
+ this.x = x;
+ this.y = y;
+ if (this.renderer) {
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ position: [this.x, this.y]
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Get the rendered direction and scale, after applying rotation style.
+ * @return {Object} Direction and scale to render.
+ */
+Clone.prototype._getRenderedDirectionAndScale = function () {
+ // Default: no changes to `this.direction` or `this.scale`.
+ var finalDirection = this.direction;
+ var finalScale = [this.size, this.size];
+ if (this.rotationStyle == Clone.ROTATION_STYLE_NONE) {
+ // Force rendered direction to be 90.
+ finalDirection = 90;
+ } else if (this.rotationStyle === Clone.ROTATION_STYLE_LEFT_RIGHT) {
+ // Force rendered direction to be 90, and flip drawable if needed.
+ finalDirection = 90;
+ var scaleFlip = (this.direction < 0) ? -1 : 1;
+ finalScale = [scaleFlip * this.size, this.size];
+ }
+ return {direction: finalDirection, scale: finalScale};
+};
+
+/**
+ * Set the direction of a clone.
+ * @param {!number} direction New direction of clone.
+ */
+Clone.prototype.setDirection = function (direction) {
+ if (this.isStage) {
+ return;
+ }
+ // Keep direction between -179 and +180.
+ this.direction = MathUtil.wrapClamp(direction, -179, 180);
+ if (this.renderer) {
+ var renderedDirectionScale = this._getRenderedDirectionAndScale();
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ direction: renderedDirectionScale.direction,
+ scale: renderedDirectionScale.scale
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Set a say bubble on this clone.
+ * @param {?string} type Type of say bubble: "say", "think", or null.
+ * @param {?string} message Message to put in say bubble.
+ */
+Clone.prototype.setSay = function (type, message) {
+ if (this.isStage) {
+ return;
+ }
+ // @todo: Render to stage.
+ if (!type || !message) {
+ console.log('Clearing say bubble');
+ return;
+ }
+ console.log('Setting say bubble:', type, message);
+};
+
+/**
+ * Set visibility of the clone; i.e., whether it's shown or hidden.
+ * @param {!boolean} visible True if the sprite should be shown.
+ */
+Clone.prototype.setVisible = function (visible) {
+ if (this.isStage) {
+ return;
+ }
+ this.visible = visible;
+ if (this.renderer) {
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ visible: this.visible
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Set size of the clone, as a percentage of the costume size.
+ * @param {!number} size Size of clone, from 5 to 535.
+ */
+Clone.prototype.setSize = function (size) {
+ if (this.isStage) {
+ return;
+ }
+ // Keep size between 5% and 535%.
+ this.size = MathUtil.clamp(size, 5, 535);
+ if (this.renderer) {
+ var renderedDirectionScale = this._getRenderedDirectionAndScale();
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ direction: renderedDirectionScale.direction,
+ scale: renderedDirectionScale.scale
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Set a particular graphic effect on this clone.
+ * @param {!string} effectName Name of effect (see `Clone.prototype.effects`).
+ * @param {!number} value Numerical magnitude of effect.
+ */
+Clone.prototype.setEffect = function (effectName, value) {
+ if (!this.effects.hasOwnProperty(effectName)) return;
+ this.effects[effectName] = value;
+ if (this.renderer) {
+ var props = {};
+ props[effectName] = this.effects[effectName];
+ this.renderer.updateDrawableProperties(this.drawableID, props);
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Clear all graphic effects on this clone.
+ */
+Clone.prototype.clearEffects = function () {
+ for (var effectName in this.effects) {
+ this.effects[effectName] = 0;
+ }
+ if (this.renderer) {
+ this.renderer.updateDrawableProperties(this.drawableID, this.effects);
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Set the current costume of this clone.
+ * @param {number} index New index of costume.
+ */
+Clone.prototype.setCostume = function (index) {
+ // Keep the costume index within possible values.
+ index = Math.round(index);
+ this.currentCostume = MathUtil.wrapClamp(
+ index, 0, this.sprite.costumes.length - 1
+ );
+ if (this.renderer) {
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ skin: this.sprite.costumes[this.currentCostume].skin
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Update the rotation style for this clone.
+ * @param {!string} rotationStyle New rotation style.
+ */
+Clone.prototype.setRotationStyle = function (rotationStyle) {
+ if (rotationStyle == Clone.ROTATION_STYLE_NONE) {
+ this.rotationStyle = Clone.ROTATION_STYLE_NONE;
+ } else if (rotationStyle == Clone.ROTATION_STYLE_ALL_AROUND) {
+ this.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
+ } else if (rotationStyle == Clone.ROTATION_STYLE_LEFT_RIGHT) {
+ this.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
+ }
+ if (this.renderer) {
+ var renderedDirectionScale = this._getRenderedDirectionAndScale();
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ direction: renderedDirectionScale.direction,
+ scale: renderedDirectionScale.scale
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Get a costume index of this clone, by name of the costume.
+ * @param {?string} costumeName Name of a costume.
+ * @return {number} Index of the named costume, or -1 if not present.
+ */
+Clone.prototype.getCostumeIndexByName = function (costumeName) {
+ for (var i = 0; i < this.sprite.costumes.length; i++) {
+ if (this.sprite.costumes[i].name == costumeName) {
+ return i;
+ }
+ }
+ return -1;
+};
+
+/**
+ * Update all drawable properties for this clone.
+ * Use when a batch has changed, e.g., when the drawable is first created.
+ */
+Clone.prototype.updateAllDrawableProperties = function () {
+ if (this.renderer) {
+ var renderedDirectionScale = this._getRenderedDirectionAndScale();
+ this.renderer.updateDrawableProperties(this.drawableID, {
+ position: [this.x, this.y],
+ direction: renderedDirectionScale.direction,
+ scale: renderedDirectionScale.scale,
+ visible: this.visible,
+ skin: this.sprite.costumes[this.currentCostume].skin
+ });
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+/**
+ * Return the human-readable name for this clone, i.e., the sprite's name.
+ * @override
+ * @returns {string} Human-readable name for the clone.
+ */
+Clone.prototype.getName = function () {
+ return this.sprite.name;
+};
+
+/**
+ * Return the clone's tight bounding box.
+ * Includes top, left, bottom, right attributes in Scratch coordinates.
+ * @return {?Object} Tight bounding box of clone, or null.
+ */
+Clone.prototype.getBounds = function () {
+ if (this.renderer) {
+ return this.runtime.renderer.getBounds(this.drawableID);
+ }
+ return null;
+};
+
+/**
+ * Return whether the clone is touching a point.
+ * @param {number} x X coordinate of test point.
+ * @param {number} y Y coordinate of test point.
+ * @return {Boolean} True iff the clone is touching the point.
+ */
+Clone.prototype.isTouchingPoint = function (x, y) {
+ if (this.renderer) {
+ // @todo: Update once pick is in Scratch coordinates.
+ // Limits test to this Drawable, so this will return true
+ // even if the clone is obscured by another Drawable.
+ var pickResult = this.runtime.renderer.pick(
+ x + this.runtime.constructor.STAGE_WIDTH / 2,
+ -y + this.runtime.constructor.STAGE_HEIGHT / 2,
+ null, null,
+ [this.drawableID]
+ );
+ return pickResult === this.drawableID;
+ }
+ return false;
+};
+
+/**
+ * Return whether the clone is touching a stage edge.
+ * @return {Boolean} True iff the clone is touching the stage edge.
+ */
+Clone.prototype.isTouchingEdge = function () {
+ if (this.renderer) {
+ var stageWidth = this.runtime.constructor.STAGE_WIDTH;
+ var stageHeight = this.runtime.constructor.STAGE_HEIGHT;
+ var bounds = this.getBounds();
+ if (bounds.left < -stageWidth / 2 ||
+ bounds.right > stageWidth / 2 ||
+ bounds.top > stageHeight / 2 ||
+ bounds.bottom < -stageHeight / 2) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Return whether the clone is touching a named sprite.
+ * @param {string} spriteName Name fo the sprite.
+ * @return {Boolean} True iff the clone is touching a clone of the sprite.
+ */
+Clone.prototype.isTouchingSprite = function (spriteName) {
+ var firstClone = this.runtime.getSpriteTargetByName(spriteName);
+ if (!firstClone || !this.renderer) {
+ return false;
+ }
+ var drawableCandidates = firstClone.sprite.clones.map(function(clone) {
+ return clone.drawableID;
+ });
+ return this.renderer.isTouchingDrawables(
+ this.drawableID, drawableCandidates);
+};
+
+/**
+ * Return whether the clone is touching a color.
+ * @param {Array.} rgb [r,g,b], values between 0-255.
+ * @return {Promise.} True iff the clone is touching the color.
+ */
+Clone.prototype.isTouchingColor = function (rgb) {
+ if (this.renderer) {
+ return this.renderer.isTouchingColor(this.drawableID, rgb);
+ }
+ return false;
+};
+
+/**
+ * Return whether the clone's color is touching a color.
+ * @param {Object} targetRgb {Array.} [r,g,b], values between 0-255.
+ * @param {Object} maskRgb {Array.} [r,g,b], values between 0-255.
+ * @return {Promise.} True iff the clone's color is touching the color.
+ */
+Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
+ if (this.renderer) {
+ return this.renderer.isTouchingColor(
+ this.drawableID,
+ targetRgb,
+ maskRgb
+ );
+ }
+ return false;
+};
+
+/**
+ * Move clone to the front layer.
+ */
+Clone.prototype.goToFront = function () {
+ if (this.renderer) {
+ this.renderer.setDrawableOrder(this.drawableID, Infinity);
+ }
+};
+
+/**
+ * Move clone back a number of layers.
+ * @param {number} nLayers How many layers to go back.
+ */
+Clone.prototype.goBackLayers = function (nLayers) {
+ if (this.renderer) {
+ this.renderer.setDrawableOrder(this.drawableID, -nLayers, true, 1);
+ }
+};
+
+/**
+ * Keep a desired position within a fence.
+ * @param {number} newX New desired X position.
+ * @param {number} newY New desired Y position.
+ * @param {Object=} opt_fence Optional fence with left, right, top bottom.
+ * @return {Array.} Fenced X and Y coordinates.
+ */
+Clone.prototype.keepInFence = function (newX, newY, opt_fence) {
+ var fence = opt_fence;
+ if (!fence) {
+ fence = {
+ left: -this.runtime.constructor.STAGE_WIDTH / 2,
+ right: this.runtime.constructor.STAGE_WIDTH / 2,
+ top: this.runtime.constructor.STAGE_HEIGHT / 2,
+ bottom: -this.runtime.constructor.STAGE_HEIGHT / 2
+ };
+ }
+ var bounds = this.getBounds();
+ if (!bounds) return;
+ // Adjust the known bounds to the target position.
+ bounds.left += (newX - this.x);
+ bounds.right += (newX - this.x);
+ bounds.top += (newY - this.y);
+ bounds.bottom += (newY - this.y);
+ // Find how far we need to move the target position.
+ var dx = 0;
+ var dy = 0;
+ if (bounds.left < fence.left) {
+ dx += fence.left - bounds.left;
+ }
+ if (bounds.right > fence.right) {
+ dx += fence.right - bounds.right;
+ }
+ if (bounds.top > fence.top) {
+ dy += fence.top - bounds.top;
+ }
+ if (bounds.bottom < fence.bottom) {
+ dy += fence.bottom - bounds.bottom;
+ }
+ return [newX + dx, newY + dy];
+};
+
+/**
+ * Make a clone of this clone, copying any run-time properties.
+ * If we've hit the global clone limit, returns null.
+ * @return {!Clone} New clone object.
+ */
+Clone.prototype.makeClone = function () {
+ if (!this.runtime.clonesAvailable()) {
+ return; // Hit max clone limit.
+ }
+ this.runtime.changeCloneCounter(1);
+ var newClone = this.sprite.createClone();
+ newClone.x = this.x;
+ newClone.y = this.y;
+ newClone.direction = this.direction;
+ newClone.visible = this.visible;
+ newClone.size = this.size;
+ newClone.currentCostume = this.currentCostume;
+ newClone.rotationStyle = this.rotationStyle;
+ newClone.effects = JSON.parse(JSON.stringify(this.effects));
+ newClone.variables = JSON.parse(JSON.stringify(this.variables));
+ newClone.lists = JSON.parse(JSON.stringify(this.lists));
+ newClone.initDrawable();
+ newClone.updateAllDrawableProperties();
+ return newClone;
+};
+
+/**
+ * Called when the project receives a "green flag."
+ * For a clone, this clears graphic effects.
+ */
+Clone.prototype.onGreenFlag = function () {
+ this.clearEffects();
+};
+
+/**
+ * Dispose of this clone, destroying any run-time properties.
+ */
+Clone.prototype.dispose = function () {
+ this.runtime.changeCloneCounter(-1);
+ if (this.renderer && this.drawableID !== null) {
+ this.renderer.destroyDrawable(this.drawableID);
+ if (this.visible) {
+ this.runtime.requestRedraw();
+ }
+ }
+};
+
+module.exports = Clone;
diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js
new file mode 100644
index 000000000..2afb25e5e
--- /dev/null
+++ b/src/sprites/sprite.js
@@ -0,0 +1,57 @@
+var Clone = require('./clone');
+var Blocks = require('../engine/blocks');
+
+/**
+ * Sprite to be used on the Scratch stage.
+ * All clones of a sprite have shared blocks, shared costumes, shared variables.
+ * @param {?Blocks} blocks Shared blocks object for all clones of sprite.
+ * @param {Runtime} runtime Reference to the runtime.
+ * @constructor
+ */
+function Sprite (blocks, runtime) {
+ this.runtime = runtime;
+ if (!blocks) {
+ // Shared set of blocks for all clones.
+ blocks = new Blocks();
+ }
+ this.blocks = blocks;
+ /**
+ * Human-readable name for this sprite (and all clones).
+ * @type {string}
+ */
+ this.name = '';
+ /**
+ * List of costumes for this sprite.
+ * Each entry is an object, e.g.,
+ * {
+ * skin: "costume.svg",
+ * name: "Costume Name",
+ * bitmapResolution: 2,
+ * rotationCenterX: 0,
+ * rotationCenterY: 0
+ * }
+ * @type {Array.}
+ */
+ this.costumes = [];
+ /**
+ * List of clones for this sprite, including the original.
+ * @type {Array.}
+ */
+ this.clones = [];
+}
+
+/**
+ * Create a clone of this sprite.
+ * @returns {!Clone} Newly created clone.
+ */
+Sprite.prototype.createClone = function () {
+ var newClone = new Clone(this, this.runtime);
+ newClone.isOriginal = this.clones.length == 0;
+ this.clones.push(newClone);
+ if (newClone.isOriginal) {
+ newClone.initDrawable();
+ }
+ return newClone;
+};
+
+module.exports = Sprite;
diff --git a/src/util/cast.js b/src/util/cast.js
new file mode 100644
index 000000000..dda55bf8e
--- /dev/null
+++ b/src/util/cast.js
@@ -0,0 +1,163 @@
+var Color = require('../util/color');
+
+function Cast () {}
+
+/**
+ * @fileoverview
+ * Utilities for casting and comparing Scratch data-types.
+ * Scratch behaves slightly differently from JavaScript in many respects,
+ * and these differences should be encapsulated below.
+ * For example, in Scratch, add(1, join("hello", world")) -> 1.
+ * This is because "hello world" is cast to 0.
+ * In JavaScript, 1 + Number("hello" + "world") would give you NaN.
+ * Use when coercing a value before computation.
+ */
+
+/**
+ * Scratch cast to number.
+ * Treats NaN as 0.
+ * In Scratch 2.0, this is captured by `interp.numArg.`
+ * @param {*} value Value to cast to number.
+ * @return {number} The Scratch-casted number value.
+ */
+Cast.toNumber = function (value) {
+ var n = Number(value);
+ if (isNaN(n)) {
+ // Scratch treats NaN as 0, when needed as a number.
+ // E.g., 0 + NaN -> 0.
+ return 0;
+ }
+ return n;
+};
+
+/**
+ * Scratch cast to boolean.
+ * In Scratch 2.0, this is captured by `interp.boolArg.`
+ * Treats some string values differently from JavaScript.
+ * @param {*} value Value to cast to boolean.
+ * @return {boolean} The Scratch-casted boolean value.
+ */
+Cast.toBoolean = function (value) {
+ // Already a boolean?
+ if (typeof value === 'boolean') {
+ return value;
+ }
+ if (typeof value === 'string') {
+ // These specific strings are treated as false in Scratch.
+ if ((value == '') ||
+ (value == '0') ||
+ (value.toLowerCase() == 'false')) {
+ return false;
+ }
+ // All other strings treated as true.
+ return true;
+ }
+ // Coerce other values and numbers.
+ return Boolean(value);
+};
+
+/**
+ * Scratch cast to string.
+ * @param {*} value Value to cast to string.
+ * @return {string} The Scratch-casted string value.
+ */
+Cast.toString = function (value) {
+ return String(value);
+};
+
+/**
+ * Cast any Scratch argument to an RGB color object to be used for the renderer.
+ * @param {*} value Value to convert to RGB color object.
+ * @return {Array.} [r,g,b], values between 0-255.
+ */
+Cast.toRgbColorList = function (value) {
+ var color;
+ if (typeof value == 'string' && value.substring(0, 1) == '#') {
+ color = Color.hexToRgb(value);
+ } else {
+ color = Color.decimalToRgb(Cast.toNumber(value));
+ }
+ return [color.r, color.g, color.b];
+};
+
+/**
+ * Compare two values, using Scratch cast, case-insensitive string compare, etc.
+ * In Scratch 2.0, this is captured by `interp.compare.`
+ * @param {*} v1 First value to compare.
+ * @param {*} v2 Second value to compare.
+ * @returns {Number} Negative number if v1 < v2; 0 if equal; positive otherwise.
+ */
+Cast.compare = function (v1, v2) {
+ var n1 = Number(v1);
+ var n2 = Number(v2);
+ if (isNaN(n1) || isNaN(n2)) {
+ // At least one argument can't be converted to a number.
+ // Scratch compares strings as case insensitive.
+ var s1 = String(v1).toLowerCase();
+ var s2 = String(v2).toLowerCase();
+ return s1.localeCompare(s2);
+ } else {
+ // Compare as numbers.
+ return n1 - n2;
+ }
+};
+
+/**
+ * Determine if a Scratch argument number represents a round integer.
+ * @param {*} val Value to check.
+ * @return {boolean} True if number looks like an integer.
+ */
+Cast.isInt = function (val) {
+ // Values that are already numbers.
+ if (typeof val === 'number') {
+ if (isNaN(val)) { // NaN is considered an integer.
+ return true;
+ }
+ // True if it's "round" (e.g., 2.0 and 2).
+ return val == parseInt(val);
+ } else if (typeof val === 'boolean') {
+ // `True` and `false` always represent integer after Scratch cast.
+ return true;
+ } else if (typeof val === 'string') {
+ // If it contains a decimal point, don't consider it an int.
+ return val.indexOf('.') < 0;
+ }
+ return false;
+};
+
+Cast.LIST_INVALID = 'INVALID';
+Cast.LIST_ALL = 'ALL';
+/**
+ * Compute a 1-based index into a list, based on a Scratch argument.
+ * Two special cases may be returned:
+ * LIST_ALL: if the block is referring to all of the items in the list.
+ * LIST_INVALID: if the index was invalid in any way.
+ * @param {*} index Scratch arg, including 1-based numbers or special cases.
+ * @param {number} length Length of the list.
+ * @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
+ */
+Cast.toListIndex = function (index, length) {
+ if (typeof index !== 'number') {
+ if (index == 'all') {
+ return Cast.LIST_ALL;
+ }
+ if (index == 'last') {
+ if (length > 0) {
+ return length;
+ }
+ return Cast.LIST_INVALID;
+ } else if (index == 'random' || index == 'any') {
+ if (length > 0) {
+ return 1 + Math.floor(Math.random() * length);
+ }
+ return Cast.LIST_INVALID;
+ }
+ }
+ index = Math.floor(Cast.toNumber(index));
+ if (index < 1 || index > length) {
+ return Cast.LIST_INVALID;
+ }
+ return index;
+};
+
+module.exports = Cast;
diff --git a/src/util/color.js b/src/util/color.js
new file mode 100644
index 000000000..2635c1b6c
--- /dev/null
+++ b/src/util/color.js
@@ -0,0 +1,76 @@
+function Color () {}
+
+/**
+ * Convert a Scratch decimal color to a hex string, #RRGGBB.
+ * @param {number} decimal RGB color as a decimal.
+ * @return {string} RGB color as #RRGGBB hex string.
+ */
+Color.decimalToHex = function (decimal) {
+ if (decimal < 0) {
+ decimal += 0xFFFFFF + 1;
+ }
+ var hex = Number(decimal).toString(16);
+ hex = '#' + '000000'.substr(0, 6 - hex.length) + hex;
+ return hex;
+};
+
+/**
+ * Convert a Scratch decimal color to an RGB color object.
+ * @param {number} decimal RGB color as decimal.
+ * @returns {Object} {r: R, g: G, b: B}, values between 0-255
+ */
+Color.decimalToRgb = function (decimal) {
+ var r = (decimal >> 16) & 0xFF;
+ var g = (decimal >> 8) & 0xFF;
+ var b = decimal & 0xFF;
+ return {r: r, g: g, b: b};
+};
+
+/**
+ * Convert a hex color (e.g., F00, #03F, #0033FF) to an RGB color object.
+ * CC-BY-SA Tim Down:
+ * https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
+ * @param {!string} hex Hex representation of the color.
+ * @return {Object} {r: R, g: G, b: B}, 0-255, or null.
+ */
+Color.hexToRgb = function (hex) {
+ var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ hex = hex.replace(shorthandRegex, function(m, r, g, b) {
+ return r + r + g + g + b + b;
+ });
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+};
+
+/**
+ * Convert an RGB color object to a hex color.
+ * @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
+ * @return {!string} Hex representation of the color.
+ */
+Color.rgbToHex = function (rgb) {
+ return Color.decimalToHex(Color.rgbToDecimal(rgb));
+};
+
+/**
+ * Convert an RGB color object to a Scratch decimal color.
+ * @param {Object} rgb {r: R, g: G, b: B}, values between 0-255.
+ * @return {!number} Number representing the color.
+ */
+Color.rgbToDecimal = function (rgb) {
+ return (rgb.r << 16) + (rgb.g << 8) + rgb.b;
+};
+
+/**
+* Convert a hex color (e.g., F00, #03F, #0033FF) to a decimal color number.
+* @param {!string} hex Hex representation of the color.
+* @return {!number} Number representing the color.
+*/
+Color.hexToDecimal = function (hex) {
+ return Color.rgbToDecimal(Color.hexToRgb(hex));
+};
+
+module.exports = Color;
diff --git a/src/util/math-util.js b/src/util/math-util.js
new file mode 100644
index 000000000..14ebb450e
--- /dev/null
+++ b/src/util/math-util.js
@@ -0,0 +1,48 @@
+function MathUtil () {}
+
+/**
+ * Convert a value from degrees to radians.
+ * @param {!number} deg Value in degrees.
+ * @return {!number} Equivalent value in radians.
+ */
+MathUtil.degToRad = function (deg) {
+ return deg * Math.PI / 180;
+};
+
+/**
+ * Convert a value from radians to degrees.
+ * @param {!number} rad Value in radians.
+ * @return {!number} Equivalent value in degrees.
+ */
+MathUtil.radToDeg = function (rad) {
+ return rad * 180 / Math.PI;
+};
+
+/**
+ * Clamp a number between two limits.
+ * If n < min, return min. If n > max, return max. Else, return n.
+ * @param {!number} n Number to clamp.
+ * @param {!number} min Minimum limit.
+ * @param {!number} max Maximum limit.
+ * @return {!number} Value of n clamped to min and max.
+ */
+MathUtil.clamp = function (n, min, max) {
+ return Math.min(Math.max(n, min), max);
+};
+
+/**
+ * Keep a number between two limits, wrapping "extra" into the range.
+ * e.g., wrapClamp(7, 1, 5) == 2
+ * wrapClamp(0, 1, 5) == 5
+ * wrapClamp(-11, -10, 6) == 6, etc.
+ * @param {!number} n Number to wrap.
+ * @param {!number} min Minimum limit.
+ * @param {!number} max Maximum limit.
+ * @return {!number} Value of n wrapped between min and max.
+ */
+MathUtil.wrapClamp = function (n, min, max) {
+ var range = (max - min) + 1;
+ return n - Math.floor((n - min) / range) * range;
+};
+
+module.exports = MathUtil;
diff --git a/src/util/timer.js b/src/util/timer.js
index 10a5b3241..48dd223d5 100644
--- a/src/util/timer.js
+++ b/src/util/timer.js
@@ -1,20 +1,70 @@
/**
- * Constructor
+ * @fileoverview
+ * A utility for accurately measuring time.
+ * To use:
+ * ---
+ * var timer = new Timer();
+ * timer.start();
+ * ... pass some time ...
+ * var timeDifference = timer.timeElapsed();
+ * ---
+ * Or, you can use the `time` and `relativeTime`
+ * to do some measurement yourself.
*/
-function Timer () {
- this.startTime = 0;
-}
+/**
+ * @constructor
+ */
+function Timer () {}
+
+/**
+ * Used to store the start time of a timer action.
+ * Updated when calling `timer.start`.
+ */
+Timer.prototype.startTime = 0;
+
+/**
+ * Return the currently known absolute time, in ms precision.
+ * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC.
+ */
Timer.prototype.time = function () {
- return Date.now();
+ if (Date.now) {
+ return Date.now();
+ } else {
+ return new Date().getTime();
+ }
};
+/**
+ * Returns a time accurate relative to other times produced by this function.
+ * If possible, will use sub-millisecond precision.
+ * If not, will use millisecond precision.
+ * Not guaranteed to produce the same absolute values per-system.
+ * @returns {number} ms-scale accurate time relative to other relative times.
+ */
+Timer.prototype.relativeTime = function () {
+ if (typeof self !== 'undefined' &&
+ self.performance && 'now' in self.performance) {
+ return self.performance.now();
+ } else {
+ return this.time();
+ }
+};
+
+/**
+ * Start a timer for measuring elapsed time,
+ * at the most accurate precision possible.
+ */
Timer.prototype.start = function () {
- this.startTime = this.time();
+ this.startTime = this.relativeTime();
};
+/**
+ * Check time elapsed since `timer.start` was called.
+ * @returns {number} Time elapsed, in ms (possibly sub-ms precision).
+ */
Timer.prototype.timeElapsed = function () {
- return this.time() - this.startTime;
+ return this.relativeTime() - this.startTime;
};
module.exports = Timer;
diff --git a/src/util/uid.js b/src/util/uid.js
new file mode 100644
index 000000000..532f9e9bf
--- /dev/null
+++ b/src/util/uid.js
@@ -0,0 +1,29 @@
+/**
+ * @fileoverview UID generator, from Blockly.
+ */
+
+/**
+ * Legal characters for the unique ID.
+ * Should be all on a US keyboard. No XML special characters or control codes.
+ * Removed $ due to issue 251.
+ * @private
+ */
+var soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' +
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+/**
+ * Generate a unique ID, from Blockly. This should be globally unique.
+ * 87 characters ^ 20 length > 128 bits (better than a UUID).
+ * @return {string} A globally unique ID string.
+ */
+var uid = function () {
+ var length = 20;
+ var soupLength = soup_.length;
+ var id = [];
+ for (var i = 0; i < length; i++) {
+ id[i] = soup_.charAt(Math.random() * soupLength);
+ }
+ return id.join('');
+};
+
+module.exports = uid;
diff --git a/src/util/xml-escape.js b/src/util/xml-escape.js
new file mode 100644
index 000000000..00ce5bff6
--- /dev/null
+++ b/src/util/xml-escape.js
@@ -0,0 +1,21 @@
+/**
+ * Escape a string to be safe to use in XML content.
+ * CC-BY-SA: hgoebl
+ * https://stackoverflow.com/questions/7918868/
+ * how-to-escape-xml-entities-in-javascript
+ * @param {!string} unsafe Unsafe string.
+ * @return {string} XML-escaped string, for use within an XML tag.
+ */
+var xmlEscape = function (unsafe) {
+ return unsafe.replace(/[<>&'"]/g, function (c) {
+ switch (c) {
+ case '<': return '<';
+ case '>': return '>';
+ case '&': return '&';
+ case '\'': return ''';
+ case '"': return '"';
+ }
+ });
+};
+
+module.exports = xmlEscape;
diff --git a/src/util/yieldtimers.js b/src/util/yieldtimers.js
deleted file mode 100644
index 45e244eaf..000000000
--- a/src/util/yieldtimers.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * @fileoverview Timers that are synchronized with the Scratch sequencer.
- */
-var Timer = require('./timer');
-
-function YieldTimers () {}
-
-/**
- * Shared collection of timers.
- * Each timer is a [Function, number] with the callback
- * and absolute time for it to run.
- * @type {Object.}
- */
-YieldTimers.timers = {};
-
-/**
- * Monotonically increasing timer ID.
- * @type {number}
- */
-YieldTimers.timerId = 0;
-
-/**
- * Utility for measuring time.
- * @type {!Timer}
- */
-YieldTimers.globalTimer = new Timer();
-
-/**
- * The timeout function is passed to primitives and is intended
- * as a convenient replacement for window.setTimeout.
- * The sequencer will attempt to resolve the timer every time
- * the yielded thread would have been stepped.
- * @param {!Function} callback To be called when the timer is done.
- * @param {number} timeDelta Time to wait, in ms.
- * @return {number} Timer ID to be used with other methods.
- */
-YieldTimers.timeout = function (callback, timeDelta) {
- var id = ++YieldTimers.timerId;
- YieldTimers.timers[id] = [
- callback,
- YieldTimers.globalTimer.time() + timeDelta
- ];
- return id;
-};
-
-/**
- * Attempt to resolve a timeout.
- * If the time has passed, call the callback.
- * Otherwise, do nothing.
- * @param {number} id Timer ID to resolve.
- * @return {boolean} True if the timer has resolved.
- */
-YieldTimers.resolve = function (id) {
- var timer = YieldTimers.timers[id];
- if (!timer) {
- // No such timer.
- return false;
- }
- var callback = timer[0];
- var time = timer[1];
- if (YieldTimers.globalTimer.time() < time) {
- // Not done yet.
- return false;
- }
- // Execute the callback and remove the timer.
- callback();
- delete YieldTimers.timers[id];
- return true;
-};
-
-/**
- * Reject a timer so the callback never executes.
- * @param {number} id Timer ID to reject.
- */
-YieldTimers.reject = function (id) {
- if (YieldTimers.timers[id]) {
- delete YieldTimers.timers[id];
- }
-};
-
-/**
- * Reject all timers currently stored.
- * Especially useful for a Scratch "stop."
- */
-YieldTimers.rejectAll = function () {
- YieldTimers.timers = {};
- YieldTimers.timerId = 0;
-};
-
-module.exports = YieldTimers;
diff --git a/test/fixtures/default.json b/test/fixtures/default.json
new file mode 100644
index 000000000..af478a4e2
--- /dev/null
+++ b/test/fixtures/default.json
@@ -0,0 +1,71 @@
+{
+ "objName": "Stage",
+ "sounds": [{
+ "soundName": "pop",
+ "soundID": -1,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "backdrop1",
+ "baseLayerID": -1,
+ "baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png",
+ "bitmapResolution": 1,
+ "rotationCenterX": 240,
+ "rotationCenterY": 180
+ }],
+ "currentCostumeIndex": 0,
+ "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png",
+ "penLayerID": -1,
+ "tempoBPM": 60,
+ "videoAlpha": 0.5,
+ "children": [{
+ "objName": "Sprite1",
+ "sounds": [{
+ "soundName": "meow",
+ "soundID": -1,
+ "md5": "83c36d806dc92327b9e7049a565c6bff.wav",
+ "sampleCount": 18688,
+ "rate": 22050,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "costume1",
+ "baseLayerID": -1,
+ "baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 47,
+ "rotationCenterY": 55
+ },
+ {
+ "costumeName": "costume2",
+ "baseLayerID": -1,
+ "baseLayerMD5": "3696356a03a8d938318876a593572843.svg",
+ "bitmapResolution": 1,
+ "rotationCenterX": 47,
+ "rotationCenterY": 55
+ }],
+ "currentCostumeIndex": 0,
+ "scratchX": 0,
+ "scratchY": 0,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "indexInLibrary": 1,
+ "visible": true,
+ "spriteInfo": {
+ }
+ }],
+ "info": {
+ "videoOn": false,
+ "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/50.0.2661.102 Safari\/537.36",
+ "swfVersion": "v446",
+ "scriptCount": 0,
+ "spriteCount": 1,
+ "hasCloudData": false,
+ "flashVersion": "MAC 21,0,0,242"
+ }
+}
diff --git a/test/fixtures/demo.json b/test/fixtures/demo.json
new file mode 100644
index 000000000..b10376ec7
--- /dev/null
+++ b/test/fixtures/demo.json
@@ -0,0 +1,359 @@
+{
+ "objName": "Stage",
+ "variables": [{
+ "name": "x",
+ "value": "1",
+ "isPersistent": false
+ },
+ {
+ "name": "y",
+ "value": "1",
+ "isPersistent": false
+ },
+ {
+ "name": "z",
+ "value": "1",
+ "isPersistent": false
+ },
+ {
+ "name": "d",
+ "value": "1",
+ "isPersistent": false
+ },
+ {
+ "name": "a",
+ "value": 4,
+ "isPersistent": false
+ }],
+ "lists": [{
+ "listName": "D# Minor Pentatonic",
+ "contents": ["78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "78",
+ "73",
+ "75",
+ "75",
+ "78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "75",
+ "78",
+ "73",
+ "75",
+ "78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "73",
+ "68",
+ "70",
+ "66",
+ "68",
+ "63"],
+ "isPersistent": false,
+ "x": 5,
+ "y": 32,
+ "width": 125,
+ "height": 206,
+ "visible": true
+ }],
+ "scripts": [[52,
+ 8,
+ [["whenIReceive", "start"],
+ ["setVar:to:", "a", "1"],
+ ["doRepeat",
+ ["lineCountOfList:", "D# Minor Pentatonic"],
+ [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "a"], "D# Minor Pentatonic"], 0.5], ["changeVar:by:", "a", 1]]]]],
+ [53,
+ 186,
+ [["whenIReceive", "start"],
+ ["setVar:to:", "x", "1"],
+ ["rest:elapsed:from:", 7.25],
+ ["doRepeat",
+ ["lineCountOfList:", "D# Minor Pentatonic"],
+ [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "x"], "D# Minor Pentatonic"], 0.25], ["changeVar:by:", "x", 1]]]]],
+ [48,
+ 557,
+ [["whenIReceive", "start"],
+ ["setVar:to:", "z", "1"],
+ ["rest:elapsed:from:", 13],
+ ["doRepeat",
+ ["lineCountOfList:", "D# Minor Pentatonic"],
+ [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "z"], "D# Minor Pentatonic"], 0.0625], ["changeVar:by:", "z", 1]]]]],
+ [49,
+ 368,
+ [["whenIReceive", "start"],
+ ["setVar:to:", "y", "1"],
+ ["rest:elapsed:from:", 11],
+ ["doRepeat",
+ ["lineCountOfList:", "D# Minor Pentatonic"],
+ [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "y"], "D# Minor Pentatonic"], 0.125], ["changeVar:by:", "y", 1]]]]],
+ [52,
+ 745,
+ [["whenIReceive", "start"],
+ ["setVar:to:", "d", "1"],
+ ["rest:elapsed:from:", 13.5],
+ ["doRepeat",
+ ["lineCountOfList:", "D# Minor Pentatonic"],
+ [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "d"], "D# Minor Pentatonic"], 0.03125], ["changeVar:by:", "d", 1]]]]]],
+ "sounds": [{
+ "soundName": "pop",
+ "soundID": 0,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "backdrop1",
+ "baseLayerID": 4,
+ "baseLayerMD5": "b61b1077b0ea1931abee9dbbfa7903ff.png",
+ "bitmapResolution": 2,
+ "rotationCenterX": 480,
+ "rotationCenterY": 360
+ }],
+ "currentCostumeIndex": 0,
+ "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png",
+ "penLayerID": 0,
+ "tempoBPM": 60,
+ "videoAlpha": 0.5,
+ "children": [{
+ "objName": "Indicator",
+ "scripts": [[247.85,
+ 32.8,
+ [["procDef", "foo %n", ["bar"], [1], false],
+ ["hide"],
+ ["clearPenTrails"],
+ ["penColor:", 5968094],
+ ["say:", ["getParam", "bar", "r"]],
+ ["stopScripts", "this script"]]],
+ [41, 36, [["whenGreenFlag"], ["call", "foo %n", 1]]]],
+ "sounds": [{
+ "soundName": "pop",
+ "soundID": 0,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "costume1",
+ "baseLayerID": 1,
+ "baseLayerMD5": "d36f6603ec293d2c2198d3ea05109fe0.png",
+ "bitmapResolution": 2,
+ "rotationCenterX": 0,
+ "rotationCenterY": 0
+ }],
+ "currentCostumeIndex": 0,
+ "scratchX": 22,
+ "scratchY": -26,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "indexInLibrary": 3,
+ "visible": false,
+ "spriteInfo": {
+ }
+ },
+ {
+ "target": "Stage",
+ "cmd": "timer",
+ "param": null,
+ "color": 2926050,
+ "label": "timer",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 5,
+ "y": 5,
+ "visible": false
+ },
+ {
+ "target": "Stage",
+ "cmd": "getVar:",
+ "param": "x",
+ "color": 15629590,
+ "label": "x",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 5,
+ "y": 268,
+ "visible": true
+ },
+ {
+ "target": "Stage",
+ "cmd": "getVar:",
+ "param": "y",
+ "color": 15629590,
+ "label": "y",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 5,
+ "y": 295,
+ "visible": true
+ },
+ {
+ "target": "Stage",
+ "cmd": "getVar:",
+ "param": "z",
+ "color": 15629590,
+ "label": "z",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 78,
+ "y": 268,
+ "visible": true
+ },
+ {
+ "objName": "Play",
+ "scripts": [[32, 33, [["whenClicked"], ["broadcast:", "start"]]]],
+ "sounds": [{
+ "soundName": "pop",
+ "soundID": 0,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "costume1",
+ "baseLayerID": 2,
+ "baseLayerMD5": "30f811366ae3a53e6447932cc7f0212d.png",
+ "bitmapResolution": 2,
+ "rotationCenterX": 68,
+ "rotationCenterY": 115
+ }],
+ "currentCostumeIndex": 0,
+ "scratchX": 2,
+ "scratchY": -48,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "indexInLibrary": 1,
+ "visible": true,
+ "spriteInfo": {
+ }
+ },
+ {
+ "target": "Stage",
+ "cmd": "getVar:",
+ "param": "d",
+ "color": 15629590,
+ "label": "d",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 5,
+ "y": 241,
+ "visible": true
+ },
+ {
+ "target": "Stage",
+ "cmd": "getVar:",
+ "param": "a",
+ "color": 15629590,
+ "label": "a",
+ "mode": 1,
+ "sliderMin": 0,
+ "sliderMax": 100,
+ "isDiscrete": true,
+ "x": 78,
+ "y": 241,
+ "visible": true
+ },
+ {
+ "objName": "Stop",
+ "scripts": [[45, 104, [["whenClicked"], ["stopScripts", "all"]]]],
+ "sounds": [{
+ "soundName": "pop",
+ "soundID": 0,
+ "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
+ "sampleCount": 258,
+ "rate": 11025,
+ "format": ""
+ }],
+ "costumes": [{
+ "costumeName": "costume1",
+ "baseLayerID": 3,
+ "baseLayerMD5": "3de406f265b8d664406adf7c70762514.png",
+ "bitmapResolution": 2,
+ "rotationCenterX": 68,
+ "rotationCenterY": 70
+ }],
+ "currentCostumeIndex": 0,
+ "scratchX": 121,
+ "scratchY": -33,
+ "scale": 1,
+ "direction": 90,
+ "rotationStyle": "normal",
+ "isDraggable": false,
+ "indexInLibrary": 2,
+ "visible": true,
+ "spriteInfo": {
+ }
+ },
+ {
+ "listName": "D# Minor Pentatonic",
+ "contents": ["78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "78",
+ "73",
+ "75",
+ "75",
+ "78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "75",
+ "78",
+ "73",
+ "75",
+ "78",
+ "75",
+ "73",
+ "75",
+ "70",
+ "73",
+ "68",
+ "70",
+ "66",
+ "68",
+ "63"],
+ "isPersistent": false,
+ "x": 5,
+ "y": 32,
+ "width": 125,
+ "height": 206,
+ "visible": true
+ }],
+ "info": {
+ "spriteCount": 3,
+ "projectID": "118381369",
+ "videoOn": false,
+ "hasCloudData": false,
+ "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/53.0.2785.143 Safari\/537.36",
+ "scriptCount": 9,
+ "flashVersion": "MAC 23,0,0,185",
+ "swfVersion": "v450.1"
+ }
+}
diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index fca8693ff..d91370f6e 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -1,20 +1,69 @@
{
"create": {
- "blockId": "z!+#Nqr,_(V=xz0y7a@d",
"workspaceId": "7Luws3lyb*Z98~Kk+IG|",
"group": ";OswyM#@%`%,xOrhOXC=",
"recordUndo": true,
+ "name": "block",
"xml": {
- "attributes": {
- "type": {
- "value": "wedo_motorclockwise"
- }
- },
- "innerHTML": "10 "
+ "outerHTML": "10 "
},
"ids": [
"z!+#Nqr,_(V=xz0y7a@d",
"!6Ahqg4f}Ljl}X5Hws?Z"
]
+ },
+ "createbranch": {
+ "name": "block",
+ "xml": {
+ "outerHTML": "1 "
+ }
+ },
+ "createtwobranches": {
+ "name": "block",
+ "xml": {
+ "outerHTML": " "
+ }
+ },
+ "createtoplevelshadow": {
+ "name": "shadow",
+ "xml": {
+ "outerHTML": "4 "
+ }
+ },
+ "createwithnext": {
+ "name": "block",
+ "xml": {
+ "outerHTML": " "
+ }
+ },
+ "createinvalid": {
+ "name": "whatever",
+ "xml": {
+ "outerHTML": " "
+ }
+ },
+ "createinvalidgrandchild": {
+ "name": "block",
+ "xml": {
+ "outerHTML": "xxx "
+ }
+ },
+ "createbadxml": {
+ "name": "whatever",
+ "xml": {
+ "outerHTML": ">"
+ }
+ },
+ "createemptyfield": {
+ "name": "block",
+ "xml": {
+ "outerHTML": " "
+ }
+ },
+ "createobscuredshadow": {
+ "name": "block",
+ "xml": {
+ "outerHTML": " "
+ }
}
}
diff --git a/test/integration/index.js b/test/integration/index.js
index c98e44651..06bdcf37b 100644
--- a/test/integration/index.js
+++ b/test/integration/index.js
@@ -6,7 +6,6 @@ test('spec', function (t) {
t.type(VirtualMachine, 'function');
t.type(vm, 'object');
- t.type(vm.blockListener, 'function');
t.end();
});
diff --git a/test/unit/adapter.js b/test/unit/adapter.js
deleted file mode 100644
index 547e0b9b0..000000000
--- a/test/unit/adapter.js
+++ /dev/null
@@ -1,20 +0,0 @@
-var test = require('tap').test;
-var adapter = require('../../src/engine/adapter');
-var events = require('../fixtures/events.json');
-
-test('spec', function (t) {
- t.type(adapter, 'function');
- t.end();
-});
-
-test('create event', function (t) {
- var result = adapter(events.create);
-
- t.type(result, 'object');
- t.type(result.id, 'string');
- t.type(result.opcode, 'string');
- t.type(result.fields, 'object');
- t.type(result.fields['DURATION'], 'object');
-
- t.end();
-});
diff --git a/test/unit/blocks_operators.js b/test/unit/blocks_operators.js
new file mode 100644
index 000000000..f7befa063
--- /dev/null
+++ b/test/unit/blocks_operators.js
@@ -0,0 +1,175 @@
+var test = require('tap').test;
+var Operators = require('../../src/blocks/scratch3_operators');
+
+var blocks = new Operators(null);
+
+test('getPrimitives', function (t) {
+ t.type(blocks.getPrimitives(), 'object');
+ t.end();
+});
+
+test('add', function (t) {
+ t.strictEqual(blocks.add({NUM1:'1', NUM2:'1'}), 2);
+ t.strictEqual(blocks.add({NUM1:'foo', NUM2:'bar'}), 0);
+ t.end();
+});
+
+test('subtract', function (t) {
+ t.strictEqual(blocks.subtract({NUM1:'1', NUM2:'1'}), 0);
+ t.strictEqual(blocks.subtract({NUM1:'foo', NUM2:'bar'}), 0);
+ t.end();
+});
+
+test('multiply', function (t) {
+ t.strictEqual(blocks.multiply({NUM1:'2', NUM2:'2'}), 4);
+ t.strictEqual(blocks.multiply({NUM1:'foo', NUM2:'bar'}), 0);
+ t.end();
+});
+
+test('divide', function (t) {
+ t.strictEqual(blocks.divide({NUM1:'2', NUM2:'2'}), 1);
+ t.strictEqual(blocks.divide({NUM1:'1', NUM2:'0'}), Infinity); // @todo
+ t.ok(isNaN(blocks.divide({NUM1:'foo', NUM2:'bar'}))); // @todo
+ t.end();
+});
+
+test('lt', function (t) {
+ t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'2'}), true);
+ t.strictEqual(blocks.lt({OPERAND1:'2', OPERAND2:'1'}), false);
+ t.strictEqual(blocks.lt({OPERAND1:'1', OPERAND2:'1'}), false);
+ t.end();
+});
+
+test('equals', function (t) {
+ t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'2'}), false);
+ t.strictEqual(blocks.equals({OPERAND1:'2', OPERAND2:'1'}), false);
+ t.strictEqual(blocks.equals({OPERAND1:'1', OPERAND2:'1'}), true);
+ t.end();
+});
+
+test('gt', function (t) {
+ t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'2'}), false);
+ t.strictEqual(blocks.gt({OPERAND1:'2', OPERAND2:'1'}), true);
+ t.strictEqual(blocks.gt({OPERAND1:'1', OPERAND2:'1'}), false);
+ t.end();
+});
+
+test('and', function (t) {
+ t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:true}), true);
+ t.strictEqual(blocks.and({OPERAND1:true, OPERAND2:false}), false);
+ t.strictEqual(blocks.and({OPERAND1:false, OPERAND2:false}), false);
+ t.end();
+});
+
+test('or', function (t) {
+ t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:true}), true);
+ t.strictEqual(blocks.or({OPERAND1:true, OPERAND2:false}), true);
+ t.strictEqual(blocks.or({OPERAND1:false, OPERAND2:false}), false);
+ t.end();
+});
+
+test('not', function (t) {
+ t.strictEqual(blocks.not({OPERAND:true}), false);
+ t.strictEqual(blocks.not({OPERAND:false}), true);
+ t.end();
+});
+
+test('random', function (t) {
+ var min = 0;
+ var max = 100;
+ var result = blocks.random({FROM:min, TO:max});
+ t.ok(result >= min);
+ t.ok(result <= max);
+ t.end();
+});
+
+test('random - equal', function (t) {
+ var min = 1;
+ var max = 1;
+ t.strictEqual(blocks.random({FROM:min, TO:max}), min);
+ t.end();
+});
+
+test('random - decimal', function (t) {
+ var min = 0.1;
+ var max = 10;
+ var result = blocks.random({FROM:min, TO:max});
+ t.ok(result >= min);
+ t.ok(result <= max);
+ t.end();
+});
+
+test('random - int', function (t) {
+ var min = 0;
+ var max = 10;
+ var result = blocks.random({FROM:min, TO:max});
+ t.ok(result >= min);
+ t.ok(result <= max);
+ t.end();
+});
+
+test('random - reverse', function (t) {
+ var min = 0;
+ var max = 10;
+ var result = blocks.random({FROM:max, TO:min});
+ t.ok(result >= min);
+ t.ok(result <= max);
+ t.end();
+});
+
+test('join', function (t) {
+ t.strictEqual(blocks.join({STRING1:'foo', STRING2:'bar'}), 'foobar');
+ t.strictEqual(blocks.join({STRING1:'1', STRING2:'2'}), '12');
+ t.end();
+});
+
+test('letterOf', function (t) {
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:0}), '');
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:1}), 'f');
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:2}), 'o');
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:3}), 'o');
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:4}), '');
+ t.strictEqual(blocks.letterOf({STRING:'foo', LETTER:'bar'}), '');
+ t.end();
+});
+
+test('length', function (t) {
+ t.strictEqual(blocks.length({STRING:''}), 0);
+ t.strictEqual(blocks.length({STRING:'foo'}), 3);
+ t.strictEqual(blocks.length({STRING:'1'}), 1);
+ t.strictEqual(blocks.length({STRING:'100'}), 3);
+ t.end();
+});
+
+test('mod', function (t) {
+ t.strictEqual(blocks.mod({NUM1:1, NUM2:1}), 0);
+ t.strictEqual(blocks.mod({NUM1:3, NUM2:6}), 3);
+ t.strictEqual(blocks.mod({NUM1:-3, NUM2:6}), 3);
+ t.end();
+});
+
+test('round', function (t) {
+ t.strictEqual(blocks.round({NUM:1}), 1);
+ t.strictEqual(blocks.round({NUM:1.1}), 1);
+ t.strictEqual(blocks.round({NUM:1.5}), 2);
+ t.end();
+});
+
+test('mathop', function (t) {
+ t.strictEqual(blocks.mathop({OPERATOR:'abs', NUM:-1}), 1);
+ t.strictEqual(blocks.mathop({OPERATOR:'floor', NUM:1.5}), 1);
+ t.strictEqual(blocks.mathop({OPERATOR:'ceiling', NUM:0.1}), 1);
+ t.strictEqual(blocks.mathop({OPERATOR:'sqrt', NUM:1}), 1);
+ t.strictEqual(blocks.mathop({OPERATOR:'sin', NUM:1}), 0.01745240643728351);
+ t.strictEqual(blocks.mathop({OPERATOR:'cos', NUM:1}), 0.9998476951563913);
+ t.strictEqual(blocks.mathop({OPERATOR:'tan', NUM:1}), 0.017455064928217585);
+ t.strictEqual(blocks.mathop({OPERATOR:'asin', NUM:1}), 90);
+ t.strictEqual(blocks.mathop({OPERATOR:'acos', NUM:1}), 0);
+ t.strictEqual(blocks.mathop({OPERATOR:'atan', NUM:1}), 45);
+ t.strictEqual(blocks.mathop({OPERATOR:'ln', NUM:1}), 0);
+ t.strictEqual(blocks.mathop({OPERATOR:'log', NUM:1}), 0);
+ t.strictEqual(blocks.mathop({OPERATOR:'e ^', NUM:1}), 2.718281828459045);
+ t.strictEqual(blocks.mathop({OPERATOR:'10 ^', NUM:1}), 10);
+ t.strictEqual(blocks.mathop({OPERATOR:'undefined', NUM:1}), 0);
+ t.end();
+});
diff --git a/test/unit/engine_adapter.js b/test/unit/engine_adapter.js
new file mode 100644
index 000000000..19289dacd
--- /dev/null
+++ b/test/unit/engine_adapter.js
@@ -0,0 +1,187 @@
+var test = require('tap').test;
+var adapter = require('../../src/engine/adapter');
+var events = require('../fixtures/events.json');
+
+test('spec', function (t) {
+ t.type(adapter, 'function');
+ t.end();
+});
+
+test('invalid inputs', function(t) {
+ var nothing = adapter('not an object');
+ t.type(nothing, 'undefined');
+ nothing = adapter({noxmlproperty:true});
+ t.type(nothing, 'undefined');
+ t.end();
+});
+
+test('create event', function (t) {
+ var result = adapter(events.create);
+
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 2);
+
+ // Outer block
+ t.type(result[0].id, 'string');
+ t.type(result[0].opcode, 'string');
+ t.type(result[0].fields, 'object');
+ t.type(result[0].inputs, 'object');
+ t.type(result[0].inputs['DURATION'], 'object');
+ t.type(result[0].topLevel, 'boolean');
+ t.equal(result[0].topLevel, true);
+
+ // Enclosed shadow block
+ t.type(result[1].id, 'string');
+ t.type(result[1].opcode, 'string');
+ t.type(result[1].fields, 'object');
+ t.type(result[1].inputs, 'object');
+ t.type(result[1].fields['NUM'], 'object');
+ t.type(result[1].fields['NUM'].value, '10');
+ t.type(result[1].topLevel, 'boolean');
+ t.equal(result[1].topLevel, false);
+
+ t.end();
+});
+
+test('create with branch', function (t) {
+ var result = adapter(events.createbranch);
+ // Outer block
+ t.type(result[0].id, 'string');
+ t.type(result[0].opcode, 'string');
+ t.type(result[0].fields, 'object');
+ t.type(result[0].inputs, 'object');
+ t.type(result[0].inputs['SUBSTACK'], 'object');
+ t.type(result[0].topLevel, 'boolean');
+ t.equal(result[0].topLevel, true);
+ // In branch
+ var branchBlockId = result[0].inputs['SUBSTACK']['block'];
+ var branchShadowId = result[0].inputs['SUBSTACK']['shadow'];
+ t.type(branchBlockId, 'string');
+ t.equal(branchShadowId, null);
+ // Find actual branch block
+ var branchBlock = null;
+ for (var i = 0; i < result.length; i++) {
+ if (result[i].id == branchBlockId) {
+ branchBlock = result[i];
+ }
+ }
+ t.type(branchBlock, 'object');
+ t.end();
+});
+
+test('create with two branches', function (t) {
+ var result = adapter(events.createtwobranches);
+ // Outer block
+ t.type(result[0].id, 'string');
+ t.type(result[0].opcode, 'string');
+ t.type(result[0].fields, 'object');
+ t.type(result[0].inputs, 'object');
+ t.type(result[0].inputs['SUBSTACK'], 'object');
+ t.type(result[0].inputs['SUBSTACK2'], 'object');
+ t.type(result[0].topLevel, 'boolean');
+ t.equal(result[0].topLevel, true);
+ // In branchs
+ var firstBranchBlockId = result[0].inputs['SUBSTACK']['block'];
+ var secondBranchBlockId = result[0].inputs['SUBSTACK2']['block'];
+ t.type(firstBranchBlockId, 'string');
+ t.type(secondBranchBlockId, 'string');
+ var firstBranchShadowBlockId = result[0].inputs['SUBSTACK']['shadow'];
+ var secondBranchShadowBlockId = result[0].inputs['SUBSTACK2']['shadow'];
+ t.equal(firstBranchShadowBlockId, null);
+ t.equal(secondBranchShadowBlockId, null);
+ // Find actual branch blocks
+ var firstBranchBlock = null;
+ var secondBranchBlock = null;
+ for (var i = 0; i < result.length; i++) {
+ if (result[i].id == firstBranchBlockId) {
+ firstBranchBlock = result[i];
+ }
+ if (result[i].id == secondBranchBlockId) {
+ secondBranchBlock = result[i];
+ }
+ }
+ t.type(firstBranchBlock, 'object');
+ t.type(secondBranchBlock, 'object');
+ t.end();
+});
+
+test('create with top-level shadow', function (t) {
+ var result = adapter(events.createtoplevelshadow);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 1);
+
+ // Outer block
+ t.type(result[0].id, 'string');
+ t.type(result[0].opcode, 'string');
+ t.type(result[0].fields, 'object');
+ t.type(result[0].inputs, 'object');
+ t.type(result[0].topLevel, 'boolean');
+ t.equal(result[0].topLevel, true);
+ t.end();
+});
+
+test('create with next connection', function (t) {
+ var result = adapter(events.createwithnext);
+
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 2);
+
+ // First block
+ t.type(result[0].id, 'string');
+ t.type(result[0].opcode, 'string');
+ t.type(result[0].fields, 'object');
+ t.type(result[0].inputs, 'object');
+ t.type(result[0].topLevel, 'boolean');
+ t.equal(result[0].topLevel, true);
+ t.type(result[0].next, 'string');
+ t.equal(result[0].next, result[1].id);
+
+ // Second block
+ t.type(result[1].id, 'string');
+ t.type(result[1].opcode, 'string');
+ t.type(result[1].fields, 'object');
+ t.type(result[1].inputs, 'object');
+ t.type(result[1].topLevel, 'boolean');
+ t.equal(result[1].topLevel, false);
+ t.equal(result[1].next, null);
+
+ t.end();
+});
+
+test('create with obscured shadow', function (t) {
+ var result = adapter(events.createobscuredshadow);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 4);
+ t.end();
+});
+
+test('create with invalid block xml', function (t) {
+ // Entirely invalid block XML
+ var result = adapter(events.createinvalid);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 0);
+
+ // Invalid grandchild tag
+ var result2 = adapter(events.createinvalidgrandchild);
+ t.ok(Array.isArray(result2));
+ t.equal(result2.length, 1);
+ t.type(result2[0].id, 'string');
+ t.equal(Object.keys(result2[0].inputs).length, 0);
+ t.equal(Object.keys(result2[0].fields).length, 0);
+
+ t.end();
+});
+
+test('create with invalid xml', function (t) {
+ var result = adapter(events.createbadxml);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 0);
+ t.end();
+});
+
+test('create with empty field', function (t) {
+ var result = adapter(events.createemptyfield);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 3);
+ t.end();
+});
diff --git a/test/unit/engine_blocks.js b/test/unit/engine_blocks.js
new file mode 100644
index 000000000..2c9f8f13e
--- /dev/null
+++ b/test/unit/engine_blocks.js
@@ -0,0 +1,544 @@
+var test = require('tap').test;
+var Blocks = require('../../src/engine/blocks');
+
+test('spec', function (t) {
+ var b = new Blocks();
+
+ t.type(Blocks, 'function');
+ t.type(b, 'object');
+ t.ok(b instanceof Blocks);
+
+ t.type(b._blocks, 'object');
+ t.type(b._scripts, 'object');
+ t.ok(Array.isArray(b._scripts));
+
+ t.type(b.createBlock, 'function');
+ t.type(b.moveBlock, 'function');
+ t.type(b.changeBlock, 'function');
+ t.type(b.deleteBlock, 'function');
+ t.type(b.getBlock, 'function');
+ t.type(b.getScripts, 'function');
+ t.type(b.getNextBlock, 'function');
+ t.type(b.getBranch, 'function');
+ t.type(b.getOpcode, 'function');
+
+
+ t.end();
+});
+
+// Getter tests
+test('getBlock', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ var block = b.getBlock('foo');
+ t.type(block, 'object');
+ var notBlock = b.getBlock('?');
+ t.type(notBlock, 'undefined');
+ t.end();
+});
+
+test('getScripts', function (t) {
+ var b = new Blocks();
+ var scripts = b.getScripts();
+ t.type(scripts, 'object');
+ t.equals(scripts.length, 0);
+ // Create two top-level blocks and one not.
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo3',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+
+ scripts = b.getScripts();
+ t.type(scripts, 'object');
+ t.equals(scripts.length, 2);
+ t.ok(scripts.indexOf('foo') > -1);
+ t.ok(scripts.indexOf('foo2') > -1);
+ t.equals(scripts.indexOf('foo3'), -1);
+ t.end();
+
+});
+
+test('getNextBlock', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+
+ var next = b.getNextBlock('foo');
+ t.equals(next, null);
+
+ // Add a block with "foo" as its next.
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: 'foo',
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+
+ next = b.getNextBlock('foo2');
+ t.equals(next, 'foo');
+
+ // Block that doesn't exist.
+ var noBlock = b.getNextBlock('?');
+ t.equals(noBlock, null);
+
+ t.end();
+});
+
+test('getBranch', function (t) {
+ var b = new Blocks();
+ // Single branch
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {
+ SUBSTACK: {
+ name: 'SUBSTACK',
+ block: 'foo2',
+ shadow: null
+ }
+ },
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+
+ var branch = b.getBranch('foo');
+ t.equals(branch, 'foo2');
+
+ var notBranch = b.getBranch('?');
+ t.equals(notBranch, null);
+
+ t.end();
+});
+
+test('getBranch2', function (t) {
+ var b = new Blocks();
+ // Second branch
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {
+ SUBSTACK: {
+ name: 'SUBSTACK',
+ block: 'foo2',
+ shadow: null
+ },
+ SUBSTACK2: {
+ name: 'SUBSTACK2',
+ block: 'foo3',
+ shadow: null
+ }
+ },
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.createBlock({
+ id: 'foo3',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+
+ var branch1 = b.getBranch('foo', 1);
+ var branch2 = b.getBranch('foo', 2);
+ t.equals(branch1, 'foo2');
+ t.equals(branch2, 'foo3');
+
+ t.end();
+});
+
+test('getBranch with none', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ var noBranch = b.getBranch('foo');
+ t.equals(noBranch, null);
+ t.end();
+});
+
+test('getOpcode', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ var opcode = b.getOpcode('foo');
+ t.equals(opcode, 'TEST_BLOCK');
+ var notOpcode = b.getOpcode('?');
+ t.equals(notOpcode, null);
+ t.end();
+});
+
+// Block events tests
+test('create', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+
+ t.type(b._blocks['foo'], 'object');
+ t.equal(b._blocks['foo'].opcode, 'TEST_BLOCK');
+ t.notEqual(b._scripts.indexOf('foo'), -1);
+ t.end();
+});
+
+test('move', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'bar',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+
+ // Attach 'bar' to the end of 'foo'
+ b.moveBlock({
+ id: 'bar',
+ newParent: 'foo'
+ });
+ t.equal(b._scripts.length, 1);
+ t.equal(Object.keys(b._blocks).length, 2);
+ t.equal(b._blocks['foo'].next, 'bar');
+
+ // Detach 'bar' from 'foo'
+ b.moveBlock({
+ id: 'bar',
+ oldParent: 'foo'
+ });
+ t.equal(b._scripts.length, 2);
+ t.equal(Object.keys(b._blocks).length, 2);
+ t.equal(b._blocks['foo'].next, null);
+
+ t.end();
+});
+
+test('move into empty', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'bar',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.moveBlock({
+ id: 'bar',
+ newInput: 'fooInput',
+ newParent: 'foo'
+ });
+ t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
+ t.end();
+});
+
+test('move no obscure shadow', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {
+ 'fooInput': {
+ name: 'fooInput',
+ block: 'x',
+ shadow: 'y'
+ }
+ },
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'bar',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.moveBlock({
+ id: 'bar',
+ newInput: 'fooInput',
+ newParent: 'foo'
+ });
+ t.equal(b._blocks['foo'].inputs['fooInput'].block, 'bar');
+ t.equal(b._blocks['foo'].inputs['fooInput'].shadow, 'y');
+ t.end();
+});
+
+test('change', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {
+ someField: {
+ name: 'someField',
+ value: 'initial-value'
+ }
+ },
+ inputs: {},
+ topLevel: true
+ });
+
+ // Test that the field is updated
+ t.equal(b._blocks['foo'].fields.someField.value, 'initial-value');
+
+ b.changeBlock({
+ element: 'field',
+ id: 'foo',
+ name: 'someField',
+ value: 'final-value'
+ });
+
+ t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+ // Invalid cases
+ // No `element`
+ b.changeBlock({
+ id: 'foo',
+ name: 'someField',
+ value: 'invalid-value'
+ });
+ t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+ // No block ID
+ b.changeBlock({
+ element: 'field',
+ name: 'someField',
+ value: 'invalid-value'
+ });
+ t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+ // No such field
+ b.changeBlock({
+ element: 'field',
+ id: 'foo',
+ name: 'someWrongField',
+ value: 'final-value'
+ });
+ t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
+
+ t.end();
+});
+
+test('delete', function (t) {
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.deleteBlock({
+ id: 'foo'
+ });
+
+ t.type(b._blocks['foo'], 'undefined');
+ t.equal(b._scripts.indexOf('foo'), -1);
+ t.end();
+});
+
+test('delete chain', function (t) {
+ // Create a chain of connected blocks and delete the top one.
+ // All of them should be deleted.
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: 'foo2',
+ fields: {},
+ inputs: {},
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: 'foo3',
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.createBlock({
+ id: 'foo3',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.deleteBlock({
+ id: 'foo'
+ });
+ t.type(b._blocks['foo'], 'undefined');
+ t.type(b._blocks['foo2'], 'undefined');
+ t.type(b._blocks['foo3'], 'undefined');
+ t.equal(b._scripts.indexOf('foo'), -1);
+ t.equal(Object.keys(b._blocks).length, 0);
+ t.equal(b._scripts.length, 0);
+ t.end();
+});
+
+test('delete inputs', function (t) {
+ // Create a block with two inputs, one of which has its own input.
+ // Delete the block - all of them should be deleted.
+ var b = new Blocks();
+ b.createBlock({
+ id: 'foo',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {
+ input1: {
+ name: 'input1',
+ block: 'foo2',
+ shadow: 'foo2'
+ },
+ SUBSTACK: {
+ name: 'SUBSTACK',
+ block: 'foo3',
+ shadow: null
+ }
+ },
+ topLevel: true
+ });
+ b.createBlock({
+ id: 'foo2',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.createBlock({
+ id: 'foo5',
+ opcode: 'TEST_OBSCURED_SHADOW',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.createBlock({
+ id: 'foo3',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {
+ subinput: {
+ name: 'subinput',
+ block: 'foo4',
+ shadow: 'foo5'
+ }
+ },
+ topLevel: false
+ });
+ b.createBlock({
+ id: 'foo4',
+ opcode: 'TEST_BLOCK',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
+ b.deleteBlock({
+ id: 'foo'
+ });
+ t.type(b._blocks['foo'], 'undefined');
+ t.type(b._blocks['foo2'], 'undefined');
+ t.type(b._blocks['foo3'], 'undefined');
+ t.type(b._blocks['foo4'], 'undefined');
+ t.type(b._blocks['foo5'], 'undefined');
+ t.equal(b._scripts.indexOf('foo'), -1);
+ t.equal(Object.keys(b._blocks).length, 0);
+ t.equal(b._scripts.length, 0);
+ t.end();
+});
diff --git a/test/unit/engine_runtime.js b/test/unit/engine_runtime.js
new file mode 100644
index 000000000..40757c054
--- /dev/null
+++ b/test/unit/engine_runtime.js
@@ -0,0 +1,12 @@
+var test = require('tap').test;
+var Runtime = require('../../src/engine/runtime');
+
+test('spec', function (t) {
+ var r = new Runtime();
+
+ t.type(Runtime, 'function');
+ t.type(r, 'object');
+ t.ok(r instanceof Runtime);
+
+ t.end();
+});
diff --git a/test/unit/sequencer.js b/test/unit/engine_sequencer.js
similarity index 100%
rename from test/unit/sequencer.js
rename to test/unit/engine_sequencer.js
diff --git a/test/unit/thread.js b/test/unit/engine_thread.js
similarity index 100%
rename from test/unit/thread.js
rename to test/unit/engine_thread.js
diff --git a/test/unit/import_sb2.js b/test/unit/import_sb2.js
new file mode 100644
index 000000000..be4d62864
--- /dev/null
+++ b/test/unit/import_sb2.js
@@ -0,0 +1,88 @@
+var fs = require('fs');
+var path = require('path');
+var test = require('tap').test;
+
+var clone = require('../../src/sprites/clone');
+var runtime = require('../../src/engine/runtime');
+var sb2 = require('../../src/import/sb2import');
+
+test('spec', function (t) {
+ t.type(sb2, 'function');
+ t.end();
+});
+
+test('default', function (t) {
+ // Get SB2 JSON (string)
+ var uri = path.resolve(__dirname, '../fixtures/default.json');
+ var file = fs.readFileSync(uri, 'utf8');
+
+ // Create runtime instance & load SB2 into it
+ var rt = new runtime();
+ sb2(file, rt);
+
+ // Test
+ t.type(file, 'string');
+ t.type(rt, 'object');
+ t.type(rt.targets, 'object');
+
+ t.ok(rt.targets[0] instanceof clone);
+ t.type(rt.targets[0].id, 'string');
+ t.type(rt.targets[0].blocks, 'object');
+ t.type(rt.targets[0].variables, 'object');
+ t.type(rt.targets[0].lists, 'object');
+
+ t.equal(rt.targets[0].isOriginal, true);
+ t.equal(rt.targets[0].currentCostume, 0);
+ t.equal(rt.targets[0].isOriginal, true);
+ t.equal(rt.targets[0].isStage, true);
+
+ t.ok(rt.targets[1] instanceof clone);
+ t.type(rt.targets[1].id, 'string');
+ t.type(rt.targets[1].blocks, 'object');
+ t.type(rt.targets[1].variables, 'object');
+ t.type(rt.targets[1].lists, 'object');
+
+ t.equal(rt.targets[1].isOriginal, true);
+ t.equal(rt.targets[1].currentCostume, 0);
+ t.equal(rt.targets[1].isOriginal, true);
+ t.equal(rt.targets[1].isStage, false);
+ t.end();
+});
+
+test('demo', function (t) {
+ // Get SB2 JSON (string)
+ var uri = path.resolve(__dirname, '../fixtures/demo.json');
+ var file = fs.readFileSync(uri, 'utf8');
+
+ // Create runtime instance & load SB2 into it
+ var rt = new runtime();
+ sb2(file, rt);
+
+ // Test
+ t.type(file, 'string');
+ t.type(rt, 'object');
+ t.type(rt.targets, 'object');
+
+ t.ok(rt.targets[0] instanceof clone);
+ t.type(rt.targets[0].id, 'string');
+ t.type(rt.targets[0].blocks, 'object');
+ t.type(rt.targets[0].variables, 'object');
+ t.type(rt.targets[0].lists, 'object');
+
+ t.equal(rt.targets[0].isOriginal, true);
+ t.equal(rt.targets[0].currentCostume, 0);
+ t.equal(rt.targets[0].isOriginal, true);
+ t.equal(rt.targets[0].isStage, true);
+
+ t.ok(rt.targets[1] instanceof clone);
+ t.type(rt.targets[1].id, 'string');
+ t.type(rt.targets[1].blocks, 'object');
+ t.type(rt.targets[1].variables, 'object');
+ t.type(rt.targets[1].lists, 'object');
+
+ t.equal(rt.targets[1].isOriginal, true);
+ t.equal(rt.targets[1].currentCostume, 0);
+ t.equal(rt.targets[1].isOriginal, true);
+ t.equal(rt.targets[1].isStage, false);
+ t.end();
+});
diff --git a/test/unit/runtime.js b/test/unit/runtime.js
deleted file mode 100644
index 0e41ed5be..000000000
--- a/test/unit/runtime.js
+++ /dev/null
@@ -1,89 +0,0 @@
-var test = require('tap').test;
-var Runtime = require('../../src/engine/runtime');
-
-test('spec', function (t) {
- var r = new Runtime();
-
- t.type(Runtime, 'function');
- t.type(r, 'object');
- t.ok(r instanceof Runtime);
-
- t.type(r.blocks, 'object');
- t.type(r.stacks, 'object');
- t.ok(Array.isArray(r.stacks));
-
- t.type(r.createBlock, 'function');
- t.type(r.moveBlock, 'function');
- t.type(r.changeBlock, 'function');
- t.type(r.deleteBlock, 'function');
-
- t.end();
-});
-
-test('create', function (t) {
- var r = new Runtime();
- r.createBlock({
- id: 'foo',
- opcode: 'TEST_BLOCK',
- next: null,
- fields: {}
- });
-
- t.type(r.blocks['foo'], 'object');
- t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
- t.notEqual(r.stacks.indexOf('foo'), -1);
- t.end();
-});
-
-test('move', function (t) {
- var r = new Runtime();
- r.createBlock({
- id: 'foo',
- opcode: 'TEST_BLOCK',
- next: null,
- fields: {}
- });
- r.createBlock({
- id: 'bar',
- opcode: 'TEST_BLOCK',
- next: null,
- fields: {}
- });
-
- // Attach 'bar' to the end of 'foo'
- r.moveBlock({
- id: 'bar',
- newParent: 'foo'
- });
- t.equal(r.stacks.length, 1);
- t.equal(Object.keys(r.blocks).length, 2);
- t.equal(r.blocks['foo'].next, 'bar');
-
- // Detach 'bar' from 'foo'
- r.moveBlock({
- id: 'bar',
- oldParent: 'foo'
- });
- t.equal(r.stacks.length, 2);
- t.equal(Object.keys(r.blocks).length, 2);
- t.equal(r.blocks['foo'].next, null);
-
- t.end();
-});
-
-test('delete', function (t) {
- var r = new Runtime();
- r.createBlock({
- id: 'foo',
- opcode: 'TEST_BLOCK',
- next: null,
- fields: {}
- });
- r.deleteBlock({
- id: 'foo'
- });
-
- t.type(r.blocks['foo'], 'undefined');
- t.equal(r.stacks.indexOf('foo'), -1);
- t.end();
-});
diff --git a/test/unit/sprites_clone.js b/test/unit/sprites_clone.js
new file mode 100644
index 000000000..246e1b955
--- /dev/null
+++ b/test/unit/sprites_clone.js
@@ -0,0 +1,13 @@
+var test = require('tap').test;
+var Clone = require('../../src/sprites/clone');
+var Sprite = require('../../src/sprites/sprite');
+
+test('clone effects', function (t) {
+ // Create two clones and ensure they have different graphic effect objects.
+ // Regression test for Github issue #224
+ var spr = new Sprite();
+ var a = new Clone(spr, null);
+ var b = new Clone(spr, null);
+ t.ok(a.effects !== b.effects);
+ t.end();
+});
diff --git a/test/unit/util_cast.js b/test/unit/util_cast.js
new file mode 100644
index 000000000..1d372213e
--- /dev/null
+++ b/test/unit/util_cast.js
@@ -0,0 +1,179 @@
+var test = require('tap').test;
+var cast = require('../../src/util/cast');
+
+test('toNumber', function (t) {
+ // Numeric
+ t.strictEqual(cast.toNumber(0), 0);
+ t.strictEqual(cast.toNumber(1), 1);
+ t.strictEqual(cast.toNumber(3.14), 3.14);
+
+ // String
+ t.strictEqual(cast.toNumber('0'), 0);
+ t.strictEqual(cast.toNumber('1'), 1);
+ t.strictEqual(cast.toNumber('3.14'), 3.14);
+ t.strictEqual(cast.toNumber('0.1e10'), 1000000000);
+ t.strictEqual(cast.toNumber('foobar'), 0);
+
+ // Boolean
+ t.strictEqual(cast.toNumber(true), 1);
+ t.strictEqual(cast.toNumber(false), 0);
+ t.strictEqual(cast.toNumber('true'), 0);
+ t.strictEqual(cast.toNumber('false'), 0);
+
+ // Undefined & object
+ t.strictEqual(cast.toNumber(undefined), 0);
+ t.strictEqual(cast.toNumber({}), 0);
+ t.strictEqual(cast.toNumber(NaN), 0);
+ t.end();
+});
+
+test('toBoolean', function (t) {
+ // Numeric
+ t.strictEqual(cast.toBoolean(0), false);
+ t.strictEqual(cast.toBoolean(1), true);
+ t.strictEqual(cast.toBoolean(3.14), true);
+
+ // String
+ t.strictEqual(cast.toBoolean('0'), false);
+ t.strictEqual(cast.toBoolean('1'), true);
+ t.strictEqual(cast.toBoolean('3.14'), true);
+ t.strictEqual(cast.toBoolean('0.1e10'), true);
+ t.strictEqual(cast.toBoolean('foobar'), true);
+
+ // Boolean
+ t.strictEqual(cast.toBoolean(true), true);
+ t.strictEqual(cast.toBoolean(false), false);
+
+ // Undefined & object
+ t.strictEqual(cast.toBoolean(undefined), false);
+ t.strictEqual(cast.toBoolean({}), true);
+ t.end();
+});
+
+test('toString', function (t) {
+ // Numeric
+ t.strictEqual(cast.toString(0), '0');
+ t.strictEqual(cast.toString(1), '1');
+ t.strictEqual(cast.toString(3.14), '3.14');
+
+ // String
+ t.strictEqual(cast.toString('0'), '0');
+ t.strictEqual(cast.toString('1'), '1');
+ t.strictEqual(cast.toString('3.14'), '3.14');
+ t.strictEqual(cast.toString('0.1e10'), '0.1e10');
+ t.strictEqual(cast.toString('foobar'), 'foobar');
+
+ // Boolean
+ t.strictEqual(cast.toString(true), 'true');
+ t.strictEqual(cast.toString(false), 'false');
+
+ // Undefined & object
+ t.strictEqual(cast.toString(undefined), 'undefined');
+ t.strictEqual(cast.toString({}), '[object Object]');
+ t.end();
+});
+
+test('toRbgColorList', function (t) {
+ // Hex (minimal, see "color" util tests)
+ t.deepEqual(cast.toRgbColorList('#000'), [0,0,0]);
+ t.deepEqual(cast.toRgbColorList('#000000'), [0,0,0]);
+ t.deepEqual(cast.toRgbColorList('#fff'), [255,255,255]);
+ t.deepEqual(cast.toRgbColorList('#ffffff'), [255,255,255]);
+
+ // Decimal (minimal, see "color" util tests)
+ t.deepEqual(cast.toRgbColorList(0), [0,0,0]);
+ t.deepEqual(cast.toRgbColorList(1), [0,0,1]);
+ t.deepEqual(cast.toRgbColorList(16777215), [255,255,255]);
+
+ // Malformed
+ t.deepEqual(cast.toRgbColorList('ffffff'), [0,0,0]);
+ t.deepEqual(cast.toRgbColorList('foobar'), [0,0,0]);
+ t.end();
+});
+
+test('compare', function (t) {
+ // Numeric
+ t.strictEqual(cast.compare(0, 0), 0);
+ t.strictEqual(cast.compare(1, 0), 1);
+ t.strictEqual(cast.compare(0, 1), -1);
+ t.strictEqual(cast.compare(1, 1), 0);
+
+ // String
+ t.strictEqual(cast.compare('0', '0'), 0);
+ t.strictEqual(cast.compare('0.1e10', '1000000000'), 0);
+ t.strictEqual(cast.compare('foobar', 'FOOBAR'), 0);
+ t.ok(cast.compare('dog', 'cat') > 0);
+
+ // Boolean
+ t.strictEqual(cast.compare(true, true), 0);
+ t.strictEqual(cast.compare(true, false), 1);
+ t.strictEqual(cast.compare(false, true), -1);
+ t.strictEqual(cast.compare(true, true), 0);
+
+ // Undefined & object
+ t.strictEqual(cast.compare(undefined, undefined), 0);
+ t.strictEqual(cast.compare(undefined, 'undefined'), 0);
+ t.strictEqual(cast.compare({}, {}), 0);
+ t.strictEqual(cast.compare({}, '[object Object]'), 0);
+ t.end();
+});
+
+test('isInt', function (t) {
+ // Numeric
+ t.strictEqual(cast.isInt(0), true);
+ t.strictEqual(cast.isInt(1), true);
+ t.strictEqual(cast.isInt(0.0), true);
+ t.strictEqual(cast.isInt(3.14), false);
+ t.strictEqual(cast.isInt(NaN), true);
+
+ // String
+ t.strictEqual(cast.isInt('0'), true);
+ t.strictEqual(cast.isInt('1'), true);
+ t.strictEqual(cast.isInt('0.0'), false);
+ t.strictEqual(cast.isInt('0.1e10'), false);
+ t.strictEqual(cast.isInt('3.14'), false);
+
+ // Boolean
+ t.strictEqual(cast.isInt(true), true);
+ t.strictEqual(cast.isInt(false), true);
+
+ // Undefined & object
+ t.strictEqual(cast.isInt(undefined), false);
+ t.strictEqual(cast.isInt({}), false);
+ t.end();
+});
+
+test('toListIndex', function (t) {
+ var list = [0,1,2,3,4,5];
+ var empty = [];
+
+ // Valid
+ t.strictEqual(cast.toListIndex(1, list.length), 1);
+ t.strictEqual(cast.toListIndex(6, list.length), 6);
+
+ // Invalid
+ t.strictEqual(cast.toListIndex(-1, list.length), cast.LIST_INVALID);
+ t.strictEqual(cast.toListIndex(0.1, list.length), cast.LIST_INVALID);
+ t.strictEqual(cast.toListIndex(0, list.length), cast.LIST_INVALID);
+ t.strictEqual(cast.toListIndex(7, list.length), cast.LIST_INVALID);
+
+ // "all"
+ t.strictEqual(cast.toListIndex('all', list.length), cast.LIST_ALL);
+
+ // "last"
+ t.strictEqual(cast.toListIndex('last', list.length), list.length);
+ t.strictEqual(cast.toListIndex('last', empty.length), cast.LIST_INVALID);
+
+ // "random"
+ var random = cast.toListIndex('random', list.length);
+ t.ok(random <= list.length);
+ t.ok(random > 0);
+ t.strictEqual(cast.toListIndex('random', empty.length), cast.LIST_INVALID);
+
+ // "any" (alias for "random")
+ var any = cast.toListIndex('any', list.length);
+ t.ok(any <= list.length);
+ t.ok(any > 0);
+ t.strictEqual(cast.toListIndex('any', empty.length), cast.LIST_INVALID);
+ t.end();
+});
diff --git a/test/unit/util_color.js b/test/unit/util_color.js
new file mode 100644
index 000000000..ba7fa059c
--- /dev/null
+++ b/test/unit/util_color.js
@@ -0,0 +1,62 @@
+var test = require('tap').test;
+var color = require('../../src/util/color');
+
+test('decimalToHex', function (t) {
+ t.strictEqual(color.decimalToHex(0), '#000000');
+ t.strictEqual(color.decimalToHex(1), '#000001');
+ t.strictEqual(color.decimalToHex(16777215), '#ffffff');
+ t.strictEqual(color.decimalToHex(-16777215), '#000001');
+ t.strictEqual(color.decimalToHex(99999999), '#5f5e0ff');
+ t.end();
+});
+
+test('decimalToRgb', function (t) {
+ t.deepEqual(color.decimalToRgb(0), {r:0,g:0,b:0});
+ t.deepEqual(color.decimalToRgb(1), {r:0,g:0,b:1});
+ t.deepEqual(color.decimalToRgb(16777215), {r:255,g:255,b:255});
+ t.deepEqual(color.decimalToRgb(-16777215), {r:0,g:0,b:1});
+ t.deepEqual(color.decimalToRgb(99999999), {r:245,g:224,b:255});
+ t.end();
+});
+
+test('hexToRgb', function (t) {
+ t.deepEqual(color.hexToRgb('#000'), {r:0,g:0,b:0});
+ t.deepEqual(color.hexToRgb('#000000'), {r:0,g:0,b:0});
+ t.deepEqual(color.hexToRgb('#fff'), {r:255,g:255,b:255});
+ t.deepEqual(color.hexToRgb('#ffffff'), {r:255,g:255,b:255});
+ t.deepEqual(color.hexToRgb('#0fa'), {r:0,g:255,b:170});
+ t.deepEqual(color.hexToRgb('#00ffaa'), {r:0,g:255,b:170});
+
+ t.deepEqual(color.hexToRgb('000'), {r:0,g:0,b:0});
+ t.deepEqual(color.hexToRgb('fff'), {r:255,g:255,b:255});
+ t.deepEqual(color.hexToRgb('00ffaa'), {r:0,g:255,b:170});
+
+ t.deepEqual(color.hexToRgb('0'), null);
+ t.deepEqual(color.hexToRgb('hello world'), null);
+
+ t.end();
+});
+
+test('rgbToHex', function (t) {
+ t.strictEqual(color.rgbToHex({r:0,g:0,b:0}), '#000000');
+ t.strictEqual(color.rgbToHex({r:255,g:255,b:255}), '#ffffff');
+ t.strictEqual(color.rgbToHex({r:0,g:255,b:170}), '#00ffaa');
+ t.end();
+});
+
+test('rgbToDecimal', function (t) {
+ t.strictEqual(color.rgbToDecimal({r:0,g:0,b:0}), 0);
+ t.strictEqual(color.rgbToDecimal({r:255,g:255,b:255}), 16777215);
+ t.strictEqual(color.rgbToDecimal({r:0,g:255,b:170}), 65450);
+ t.end();
+});
+
+test('hexToDecimal', function (t) {
+ t.strictEqual(color.hexToDecimal('#000'), 0);
+ t.strictEqual(color.hexToDecimal('#000000'), 0);
+ t.strictEqual(color.hexToDecimal('#fff'), 16777215);
+ t.strictEqual(color.hexToDecimal('#ffffff'), 16777215);
+ t.strictEqual(color.hexToDecimal('#0fa'), 65450);
+ t.strictEqual(color.hexToDecimal('#00ffaa'), 65450);
+ t.end();
+});
diff --git a/test/unit/util_math.js b/test/unit/util_math.js
new file mode 100644
index 000000000..b198ef1d3
--- /dev/null
+++ b/test/unit/util_math.js
@@ -0,0 +1,36 @@
+var test = require('tap').test;
+var math = require('../../src/util/math-util');
+
+test('degToRad', function (t) {
+ t.strictEqual(math.degToRad(0), 0);
+ t.strictEqual(math.degToRad(1), 0.017453292519943295);
+ t.strictEqual(math.degToRad(180), Math.PI);
+ t.strictEqual(math.degToRad(360), 2 * Math.PI);
+ t.strictEqual(math.degToRad(720), 4 * Math.PI);
+ t.end();
+});
+
+test('radToDeg', function (t) {
+ t.strictEqual(math.radToDeg(0), 0);
+ t.strictEqual(math.radToDeg(1), 57.29577951308232);
+ t.strictEqual(math.radToDeg(180), 10313.240312354817);
+ t.strictEqual(math.radToDeg(360), 20626.480624709635);
+ t.strictEqual(math.radToDeg(720), 41252.96124941927);
+ t.end();
+});
+
+test('clamp', function (t) {
+ t.strictEqual(math.clamp(0, 0, 10), 0);
+ t.strictEqual(math.clamp(1, 0, 10), 1);
+ t.strictEqual(math.clamp(-10, 0, 10), 0);
+ t.strictEqual(math.clamp(100, 0, 10), 10);
+ t.end();
+});
+
+test('wrapClamp', function (t) {
+ t.strictEqual(math.wrapClamp(0, 0, 10), 0);
+ t.strictEqual(math.wrapClamp(1, 0, 10), 1);
+ t.strictEqual(math.wrapClamp(-10, 0, 10), 1);
+ t.strictEqual(math.wrapClamp(100, 0, 10), 1);
+ t.end();
+});
diff --git a/test/unit/timer.js b/test/unit/util_timer.js
similarity index 100%
rename from test/unit/timer.js
rename to test/unit/util_timer.js
diff --git a/test/unit/util_xml.js b/test/unit/util_xml.js
new file mode 100644
index 000000000..1906a2ab6
--- /dev/null
+++ b/test/unit/util_xml.js
@@ -0,0 +1,9 @@
+var test = require('tap').test;
+var xml = require('../../src/util/xml-escape');
+
+test('escape', function (t) {
+ var input = ' ';
+ var output = '<foo bar="he & llo '"></foo>';
+ t.strictEqual(xml(input), output);
+ t.end();
+});
diff --git a/vm.js b/vm.js
deleted file mode 100644
index fb2523c87..000000000
--- a/vm.js
+++ /dev/null
@@ -1,13805 +0,0 @@
-/******/ (function(modules) { // webpackBootstrap
-/******/ // The module cache
-/******/ var installedModules = {};
-
-/******/ // The require function
-/******/ function __webpack_require__(moduleId) {
-
-/******/ // Check if module is in cache
-/******/ if(installedModules[moduleId])
-/******/ return installedModules[moduleId].exports;
-
-/******/ // Create a new module (and put it into the cache)
-/******/ var module = installedModules[moduleId] = {
-/******/ exports: {},
-/******/ id: moduleId,
-/******/ loaded: false
-/******/ };
-
-/******/ // Execute the module function
-/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-
-/******/ // Flag the module as loaded
-/******/ module.loaded = true;
-
-/******/ // Return the exports of the module
-/******/ return module.exports;
-/******/ }
-
-
-/******/ // expose the modules object (__webpack_modules__)
-/******/ __webpack_require__.m = modules;
-
-/******/ // expose the module cache
-/******/ __webpack_require__.c = installedModules;
-
-/******/ // __webpack_public_path__
-/******/ __webpack_require__.p = "";
-
-/******/ // Load entry module and return exports
-/******/ return __webpack_require__(0);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ function(module, exports, __webpack_require__) {
-
- var EventEmitter = __webpack_require__(1);
- var util = __webpack_require__(2);
-
- var Runtime = __webpack_require__(6);
- var adapter = __webpack_require__(13);
-
- /**
- * Handles connections between blocks, stage, and extensions.
- *
- * @author Andrew Sliwinski
- */
- function VirtualMachine () {
- var instance = this;
-
- // Bind event emitter and runtime to VM instance
- // @todo Post message (Web Worker) polyfill
- EventEmitter.call(instance);
- instance.runtime = new Runtime();
-
- /**
- * Event listener for blocks. Handles validation and serves as a generic
- * adapter between the blocks and the runtime interface.
- *
- * @param {Object} Blockly "block" event
- */
- instance.blockListener = function (e) {
- // Validate event
- if (typeof e !== 'object') return;
- if (typeof e.blockId !== 'string') return;
-
- // Blocks
- switch (e.type) {
- case 'create':
- instance.runtime.createBlock(adapter(e), false);
- break;
- case 'change':
- instance.runtime.changeBlock({
- id: e.blockId,
- element: e.element,
- name: e.name,
- value: e.newValue
- });
- break;
- case 'move':
- instance.runtime.moveBlock({
- id: e.blockId,
- oldParent: e.oldParentId,
- oldField: e.oldInputName,
- newParent: e.newParentId,
- newField: e.newInputName
- });
- break;
- case 'delete':
- instance.runtime.deleteBlock({
- id: e.blockId
- });
- break;
- }
- };
-
- instance.flyoutBlockListener = function (e) {
- switch (e.type) {
- case 'create':
- instance.runtime.createBlock(adapter(e), true);
- break;
- case 'change':
- instance.runtime.changeBlock({
- id: e.blockId,
- element: e.element,
- name: e.name,
- value: e.newValue
- });
- break;
- case 'delete':
- instance.runtime.deleteBlock({
- id: e.blockId
- });
- break;
- }
- };
- }
-
- /**
- * Inherit from EventEmitter
- */
- util.inherits(VirtualMachine, EventEmitter);
-
- /**
- * Export and bind to `window`
- */
- module.exports = VirtualMachine;
- if (typeof window !== 'undefined') window.VirtualMachine = module.exports;
-
-
-/***/ },
-/* 1 */
-/***/ function(module, exports) {
-
- // Copyright Joyent, Inc. and other Node contributors.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a
- // copy of this software and associated documentation files (the
- // "Software"), to deal in the Software without restriction, including
- // without limitation the rights to use, copy, modify, merge, publish,
- // distribute, sublicense, and/or sell copies of the Software, and to permit
- // persons to whom the Software is furnished to do so, subject to the
- // following conditions:
- //
- // The above copyright notice and this permission notice shall be included
- // in all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
- // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
- // USE OR OTHER DEALINGS IN THE SOFTWARE.
-
- function EventEmitter() {
- this._events = this._events || {};
- this._maxListeners = this._maxListeners || undefined;
- }
- module.exports = EventEmitter;
-
- // Backwards-compat with node 0.10.x
- EventEmitter.EventEmitter = EventEmitter;
-
- EventEmitter.prototype._events = undefined;
- EventEmitter.prototype._maxListeners = undefined;
-
- // By default EventEmitters will print a warning if more than 10 listeners are
- // added to it. This is a useful default which helps finding memory leaks.
- EventEmitter.defaultMaxListeners = 10;
-
- // Obviously not all Emitters should be limited to 10. This function allows
- // that to be increased. Set to zero for unlimited.
- EventEmitter.prototype.setMaxListeners = function(n) {
- if (!isNumber(n) || n < 0 || isNaN(n))
- throw TypeError('n must be a positive number');
- this._maxListeners = n;
- return this;
- };
-
- EventEmitter.prototype.emit = function(type) {
- var er, handler, len, args, i, listeners;
-
- if (!this._events)
- this._events = {};
-
- // If there is no 'error' event listener then throw.
- if (type === 'error') {
- if (!this._events.error ||
- (isObject(this._events.error) && !this._events.error.length)) {
- er = arguments[1];
- if (er instanceof Error) {
- throw er; // Unhandled 'error' event
- }
- throw TypeError('Uncaught, unspecified "error" event.');
- }
- }
-
- handler = this._events[type];
-
- if (isUndefined(handler))
- return false;
-
- if (isFunction(handler)) {
- switch (arguments.length) {
- // fast cases
- case 1:
- handler.call(this);
- break;
- case 2:
- handler.call(this, arguments[1]);
- break;
- case 3:
- handler.call(this, arguments[1], arguments[2]);
- break;
- // slower
- default:
- args = Array.prototype.slice.call(arguments, 1);
- handler.apply(this, args);
- }
- } else if (isObject(handler)) {
- args = Array.prototype.slice.call(arguments, 1);
- listeners = handler.slice();
- len = listeners.length;
- for (i = 0; i < len; i++)
- listeners[i].apply(this, args);
- }
-
- return true;
- };
-
- EventEmitter.prototype.addListener = function(type, listener) {
- var m;
-
- if (!isFunction(listener))
- throw TypeError('listener must be a function');
-
- if (!this._events)
- this._events = {};
-
- // To avoid recursion in the case that type === "newListener"! Before
- // adding it to the listeners, first emit "newListener".
- if (this._events.newListener)
- this.emit('newListener', type,
- isFunction(listener.listener) ?
- listener.listener : listener);
-
- if (!this._events[type])
- // Optimize the case of one listener. Don't need the extra array object.
- this._events[type] = listener;
- else if (isObject(this._events[type]))
- // If we've already got an array, just append.
- this._events[type].push(listener);
- else
- // Adding the second element, need to change to array.
- this._events[type] = [this._events[type], listener];
-
- // Check for listener leak
- if (isObject(this._events[type]) && !this._events[type].warned) {
- if (!isUndefined(this._maxListeners)) {
- m = this._maxListeners;
- } else {
- m = EventEmitter.defaultMaxListeners;
- }
-
- if (m && m > 0 && this._events[type].length > m) {
- this._events[type].warned = true;
- console.error('(node) warning: possible EventEmitter memory ' +
- 'leak detected. %d listeners added. ' +
- 'Use emitter.setMaxListeners() to increase limit.',
- this._events[type].length);
- if (typeof console.trace === 'function') {
- // not supported in IE 10
- console.trace();
- }
- }
- }
-
- return this;
- };
-
- EventEmitter.prototype.on = EventEmitter.prototype.addListener;
-
- EventEmitter.prototype.once = function(type, listener) {
- if (!isFunction(listener))
- throw TypeError('listener must be a function');
-
- var fired = false;
-
- function g() {
- this.removeListener(type, g);
-
- if (!fired) {
- fired = true;
- listener.apply(this, arguments);
- }
- }
-
- g.listener = listener;
- this.on(type, g);
-
- return this;
- };
-
- // emits a 'removeListener' event iff the listener was removed
- EventEmitter.prototype.removeListener = function(type, listener) {
- var list, position, length, i;
-
- if (!isFunction(listener))
- throw TypeError('listener must be a function');
-
- if (!this._events || !this._events[type])
- return this;
-
- list = this._events[type];
- length = list.length;
- position = -1;
-
- if (list === listener ||
- (isFunction(list.listener) && list.listener === listener)) {
- delete this._events[type];
- if (this._events.removeListener)
- this.emit('removeListener', type, listener);
-
- } else if (isObject(list)) {
- for (i = length; i-- > 0;) {
- if (list[i] === listener ||
- (list[i].listener && list[i].listener === listener)) {
- position = i;
- break;
- }
- }
-
- if (position < 0)
- return this;
-
- if (list.length === 1) {
- list.length = 0;
- delete this._events[type];
- } else {
- list.splice(position, 1);
- }
-
- if (this._events.removeListener)
- this.emit('removeListener', type, listener);
- }
-
- return this;
- };
-
- EventEmitter.prototype.removeAllListeners = function(type) {
- var key, listeners;
-
- if (!this._events)
- return this;
-
- // not listening for removeListener, no need to emit
- if (!this._events.removeListener) {
- if (arguments.length === 0)
- this._events = {};
- else if (this._events[type])
- delete this._events[type];
- return this;
- }
-
- // emit removeListener for all listeners on all events
- if (arguments.length === 0) {
- for (key in this._events) {
- if (key === 'removeListener') continue;
- this.removeAllListeners(key);
- }
- this.removeAllListeners('removeListener');
- this._events = {};
- return this;
- }
-
- listeners = this._events[type];
-
- if (isFunction(listeners)) {
- this.removeListener(type, listeners);
- } else if (listeners) {
- // LIFO order
- while (listeners.length)
- this.removeListener(type, listeners[listeners.length - 1]);
- }
- delete this._events[type];
-
- return this;
- };
-
- EventEmitter.prototype.listeners = function(type) {
- var ret;
- if (!this._events || !this._events[type])
- ret = [];
- else if (isFunction(this._events[type]))
- ret = [this._events[type]];
- else
- ret = this._events[type].slice();
- return ret;
- };
-
- EventEmitter.prototype.listenerCount = function(type) {
- if (this._events) {
- var evlistener = this._events[type];
-
- if (isFunction(evlistener))
- return 1;
- else if (evlistener)
- return evlistener.length;
- }
- return 0;
- };
-
- EventEmitter.listenerCount = function(emitter, type) {
- return emitter.listenerCount(type);
- };
-
- function isFunction(arg) {
- return typeof arg === 'function';
- }
-
- function isNumber(arg) {
- return typeof arg === 'number';
- }
-
- function isObject(arg) {
- return typeof arg === 'object' && arg !== null;
- }
-
- function isUndefined(arg) {
- return arg === void 0;
- }
-
-
-/***/ },
-/* 2 */
-/***/ function(module, exports, __webpack_require__) {
-
- /* WEBPACK VAR INJECTION */(function(global, process) {// Copyright Joyent, Inc. and other Node contributors.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a
- // copy of this software and associated documentation files (the
- // "Software"), to deal in the Software without restriction, including
- // without limitation the rights to use, copy, modify, merge, publish,
- // distribute, sublicense, and/or sell copies of the Software, and to permit
- // persons to whom the Software is furnished to do so, subject to the
- // following conditions:
- //
- // The above copyright notice and this permission notice shall be included
- // in all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
- // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
- // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
- // USE OR OTHER DEALINGS IN THE SOFTWARE.
-
- var formatRegExp = /%[sdj%]/g;
- exports.format = function(f) {
- if (!isString(f)) {
- var objects = [];
- for (var i = 0; i < arguments.length; i++) {
- objects.push(inspect(arguments[i]));
- }
- return objects.join(' ');
- }
-
- var i = 1;
- var args = arguments;
- var len = args.length;
- var str = String(f).replace(formatRegExp, function(x) {
- if (x === '%%') return '%';
- if (i >= len) return x;
- switch (x) {
- case '%s': return String(args[i++]);
- case '%d': return Number(args[i++]);
- case '%j':
- try {
- return JSON.stringify(args[i++]);
- } catch (_) {
- return '[Circular]';
- }
- default:
- return x;
- }
- });
- for (var x = args[i]; i < len; x = args[++i]) {
- if (isNull(x) || !isObject(x)) {
- str += ' ' + x;
- } else {
- str += ' ' + inspect(x);
- }
- }
- return str;
- };
-
-
- // Mark that a method should not be used.
- // Returns a modified function which warns once by default.
- // If --no-deprecation is set, then it is a no-op.
- exports.deprecate = function(fn, msg) {
- // Allow for deprecating things in the process of starting up.
- if (isUndefined(global.process)) {
- return function() {
- return exports.deprecate(fn, msg).apply(this, arguments);
- };
- }
-
- if (process.noDeprecation === true) {
- return fn;
- }
-
- var warned = false;
- function deprecated() {
- if (!warned) {
- if (process.throwDeprecation) {
- throw new Error(msg);
- } else if (process.traceDeprecation) {
- console.trace(msg);
- } else {
- console.error(msg);
- }
- warned = true;
- }
- return fn.apply(this, arguments);
- }
-
- return deprecated;
- };
-
-
- var debugs = {};
- var debugEnviron;
- exports.debuglog = function(set) {
- if (isUndefined(debugEnviron))
- debugEnviron = process.env.NODE_DEBUG || '';
- set = set.toUpperCase();
- if (!debugs[set]) {
- if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
- var pid = process.pid;
- debugs[set] = function() {
- var msg = exports.format.apply(exports, arguments);
- console.error('%s %d: %s', set, pid, msg);
- };
- } else {
- debugs[set] = function() {};
- }
- }
- return debugs[set];
- };
-
-
- /**
- * Echos the value of a value. Trys to print the value out
- * in the best way possible given the different types.
- *
- * @param {Object} obj The object to print out.
- * @param {Object} opts Optional options object that alters the output.
- */
- /* legacy: obj, showHidden, depth, colors*/
- function inspect(obj, opts) {
- // default options
- var ctx = {
- seen: [],
- stylize: stylizeNoColor
- };
- // legacy...
- if (arguments.length >= 3) ctx.depth = arguments[2];
- if (arguments.length >= 4) ctx.colors = arguments[3];
- if (isBoolean(opts)) {
- // legacy...
- ctx.showHidden = opts;
- } else if (opts) {
- // got an "options" object
- exports._extend(ctx, opts);
- }
- // set default options
- if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
- if (isUndefined(ctx.depth)) ctx.depth = 2;
- if (isUndefined(ctx.colors)) ctx.colors = false;
- if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
- if (ctx.colors) ctx.stylize = stylizeWithColor;
- return formatValue(ctx, obj, ctx.depth);
- }
- exports.inspect = inspect;
-
-
- // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
- inspect.colors = {
- 'bold' : [1, 22],
- 'italic' : [3, 23],
- 'underline' : [4, 24],
- 'inverse' : [7, 27],
- 'white' : [37, 39],
- 'grey' : [90, 39],
- 'black' : [30, 39],
- 'blue' : [34, 39],
- 'cyan' : [36, 39],
- 'green' : [32, 39],
- 'magenta' : [35, 39],
- 'red' : [31, 39],
- 'yellow' : [33, 39]
- };
-
- // Don't use 'blue' not visible on cmd.exe
- inspect.styles = {
- 'special': 'cyan',
- 'number': 'yellow',
- 'boolean': 'yellow',
- 'undefined': 'grey',
- 'null': 'bold',
- 'string': 'green',
- 'date': 'magenta',
- // "name": intentionally not styling
- 'regexp': 'red'
- };
-
-
- function stylizeWithColor(str, styleType) {
- var style = inspect.styles[styleType];
-
- if (style) {
- return '\u001b[' + inspect.colors[style][0] + 'm' + str +
- '\u001b[' + inspect.colors[style][1] + 'm';
- } else {
- return str;
- }
- }
-
-
- function stylizeNoColor(str, styleType) {
- return str;
- }
-
-
- function arrayToHash(array) {
- var hash = {};
-
- array.forEach(function(val, idx) {
- hash[val] = true;
- });
-
- return hash;
- }
-
-
- function formatValue(ctx, value, recurseTimes) {
- // Provide a hook for user-specified inspect functions.
- // Check that value is an object with an inspect function on it
- if (ctx.customInspect &&
- value &&
- isFunction(value.inspect) &&
- // Filter out the util module, it's inspect function is special
- value.inspect !== exports.inspect &&
- // Also filter out any prototype objects using the circular check.
- !(value.constructor && value.constructor.prototype === value)) {
- var ret = value.inspect(recurseTimes, ctx);
- if (!isString(ret)) {
- ret = formatValue(ctx, ret, recurseTimes);
- }
- return ret;
- }
-
- // Primitive types cannot have properties
- var primitive = formatPrimitive(ctx, value);
- if (primitive) {
- return primitive;
- }
-
- // Look up the keys of the object.
- var keys = Object.keys(value);
- var visibleKeys = arrayToHash(keys);
-
- if (ctx.showHidden) {
- keys = Object.getOwnPropertyNames(value);
- }
-
- // IE doesn't make error fields non-enumerable
- // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
- if (isError(value)
- && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
- return formatError(value);
- }
-
- // Some type of object without properties can be shortcutted.
- if (keys.length === 0) {
- if (isFunction(value)) {
- var name = value.name ? ': ' + value.name : '';
- return ctx.stylize('[Function' + name + ']', 'special');
- }
- if (isRegExp(value)) {
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
- }
- if (isDate(value)) {
- return ctx.stylize(Date.prototype.toString.call(value), 'date');
- }
- if (isError(value)) {
- return formatError(value);
- }
- }
-
- var base = '', array = false, braces = ['{', '}'];
-
- // Make Array say that they are Array
- if (isArray(value)) {
- array = true;
- braces = ['[', ']'];
- }
-
- // Make functions say that they are functions
- if (isFunction(value)) {
- var n = value.name ? ': ' + value.name : '';
- base = ' [Function' + n + ']';
- }
-
- // Make RegExps say that they are RegExps
- if (isRegExp(value)) {
- base = ' ' + RegExp.prototype.toString.call(value);
- }
-
- // Make dates with properties first say the date
- if (isDate(value)) {
- base = ' ' + Date.prototype.toUTCString.call(value);
- }
-
- // Make error with message first say the error
- if (isError(value)) {
- base = ' ' + formatError(value);
- }
-
- if (keys.length === 0 && (!array || value.length == 0)) {
- return braces[0] + base + braces[1];
- }
-
- if (recurseTimes < 0) {
- if (isRegExp(value)) {
- return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
- } else {
- return ctx.stylize('[Object]', 'special');
- }
- }
-
- ctx.seen.push(value);
-
- var output;
- if (array) {
- output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
- } else {
- output = keys.map(function(key) {
- return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
- });
- }
-
- ctx.seen.pop();
-
- return reduceToSingleString(output, base, braces);
- }
-
-
- function formatPrimitive(ctx, value) {
- if (isUndefined(value))
- return ctx.stylize('undefined', 'undefined');
- if (isString(value)) {
- var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
- .replace(/'/g, "\\'")
- .replace(/\\"/g, '"') + '\'';
- return ctx.stylize(simple, 'string');
- }
- if (isNumber(value))
- return ctx.stylize('' + value, 'number');
- if (isBoolean(value))
- return ctx.stylize('' + value, 'boolean');
- // For some reason typeof null is "object", so special case here.
- if (isNull(value))
- return ctx.stylize('null', 'null');
- }
-
-
- function formatError(value) {
- return '[' + Error.prototype.toString.call(value) + ']';
- }
-
-
- function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
- var output = [];
- for (var i = 0, l = value.length; i < l; ++i) {
- if (hasOwnProperty(value, String(i))) {
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
- String(i), true));
- } else {
- output.push('');
- }
- }
- keys.forEach(function(key) {
- if (!key.match(/^\d+$/)) {
- output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
- key, true));
- }
- });
- return output;
- }
-
-
- function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
- var name, str, desc;
- desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
- if (desc.get) {
- if (desc.set) {
- str = ctx.stylize('[Getter/Setter]', 'special');
- } else {
- str = ctx.stylize('[Getter]', 'special');
- }
- } else {
- if (desc.set) {
- str = ctx.stylize('[Setter]', 'special');
- }
- }
- if (!hasOwnProperty(visibleKeys, key)) {
- name = '[' + key + ']';
- }
- if (!str) {
- if (ctx.seen.indexOf(desc.value) < 0) {
- if (isNull(recurseTimes)) {
- str = formatValue(ctx, desc.value, null);
- } else {
- str = formatValue(ctx, desc.value, recurseTimes - 1);
- }
- if (str.indexOf('\n') > -1) {
- if (array) {
- str = str.split('\n').map(function(line) {
- return ' ' + line;
- }).join('\n').substr(2);
- } else {
- str = '\n' + str.split('\n').map(function(line) {
- return ' ' + line;
- }).join('\n');
- }
- }
- } else {
- str = ctx.stylize('[Circular]', 'special');
- }
- }
- if (isUndefined(name)) {
- if (array && key.match(/^\d+$/)) {
- return str;
- }
- name = JSON.stringify('' + key);
- if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
- name = name.substr(1, name.length - 2);
- name = ctx.stylize(name, 'name');
- } else {
- name = name.replace(/'/g, "\\'")
- .replace(/\\"/g, '"')
- .replace(/(^"|"$)/g, "'");
- name = ctx.stylize(name, 'string');
- }
- }
-
- return name + ': ' + str;
- }
-
-
- function reduceToSingleString(output, base, braces) {
- var numLinesEst = 0;
- var length = output.reduce(function(prev, cur) {
- numLinesEst++;
- if (cur.indexOf('\n') >= 0) numLinesEst++;
- return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
- }, 0);
-
- if (length > 60) {
- return braces[0] +
- (base === '' ? '' : base + '\n ') +
- ' ' +
- output.join(',\n ') +
- ' ' +
- braces[1];
- }
-
- return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
- }
-
-
- // NOTE: These type checking functions intentionally don't use `instanceof`
- // because it is fragile and can be easily faked with `Object.create()`.
- function isArray(ar) {
- return Array.isArray(ar);
- }
- exports.isArray = isArray;
-
- function isBoolean(arg) {
- return typeof arg === 'boolean';
- }
- exports.isBoolean = isBoolean;
-
- function isNull(arg) {
- return arg === null;
- }
- exports.isNull = isNull;
-
- function isNullOrUndefined(arg) {
- return arg == null;
- }
- exports.isNullOrUndefined = isNullOrUndefined;
-
- function isNumber(arg) {
- return typeof arg === 'number';
- }
- exports.isNumber = isNumber;
-
- function isString(arg) {
- return typeof arg === 'string';
- }
- exports.isString = isString;
-
- function isSymbol(arg) {
- return typeof arg === 'symbol';
- }
- exports.isSymbol = isSymbol;
-
- function isUndefined(arg) {
- return arg === void 0;
- }
- exports.isUndefined = isUndefined;
-
- function isRegExp(re) {
- return isObject(re) && objectToString(re) === '[object RegExp]';
- }
- exports.isRegExp = isRegExp;
-
- function isObject(arg) {
- return typeof arg === 'object' && arg !== null;
- }
- exports.isObject = isObject;
-
- function isDate(d) {
- return isObject(d) && objectToString(d) === '[object Date]';
- }
- exports.isDate = isDate;
-
- function isError(e) {
- return isObject(e) &&
- (objectToString(e) === '[object Error]' || e instanceof Error);
- }
- exports.isError = isError;
-
- function isFunction(arg) {
- return typeof arg === 'function';
- }
- exports.isFunction = isFunction;
-
- function isPrimitive(arg) {
- return arg === null ||
- typeof arg === 'boolean' ||
- typeof arg === 'number' ||
- typeof arg === 'string' ||
- typeof arg === 'symbol' || // ES6 symbol
- typeof arg === 'undefined';
- }
- exports.isPrimitive = isPrimitive;
-
- exports.isBuffer = __webpack_require__(4);
-
- function objectToString(o) {
- return Object.prototype.toString.call(o);
- }
-
-
- function pad(n) {
- return n < 10 ? '0' + n.toString(10) : n.toString(10);
- }
-
-
- var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
- 'Oct', 'Nov', 'Dec'];
-
- // 26 Feb 16:19:34
- function timestamp() {
- var d = new Date();
- var time = [pad(d.getHours()),
- pad(d.getMinutes()),
- pad(d.getSeconds())].join(':');
- return [d.getDate(), months[d.getMonth()], time].join(' ');
- }
-
-
- // log is just a thin wrapper to console.log that prepends a timestamp
- exports.log = function() {
- console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
- };
-
-
- /**
- * Inherit the prototype methods from one constructor into another.
- *
- * The Function.prototype.inherits from lang.js rewritten as a standalone
- * function (not on Function.prototype). NOTE: If this file is to be loaded
- * during bootstrapping this function needs to be rewritten using some native
- * functions as prototype setup using normal JavaScript does not work as
- * expected during bootstrapping (see mirror.js in r114903).
- *
- * @param {function} ctor Constructor function which needs to inherit the
- * prototype.
- * @param {function} superCtor Constructor function to inherit prototype from.
- */
- exports.inherits = __webpack_require__(5);
-
- exports._extend = function(origin, add) {
- // Don't do anything if add isn't an object
- if (!add || !isObject(add)) return origin;
-
- var keys = Object.keys(add);
- var i = keys.length;
- while (i--) {
- origin[keys[i]] = add[keys[i]];
- }
- return origin;
- };
-
- function hasOwnProperty(obj, prop) {
- return Object.prototype.hasOwnProperty.call(obj, prop);
- }
-
- /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(3)))
-
-/***/ },
-/* 3 */
-/***/ function(module, exports) {
-
- // shim for using process in browser
-
- var process = module.exports = {};
- var queue = [];
- var draining = false;
- var currentQueue;
- var queueIndex = -1;
-
- function cleanUpNextTick() {
- draining = false;
- if (currentQueue.length) {
- queue = currentQueue.concat(queue);
- } else {
- queueIndex = -1;
- }
- if (queue.length) {
- drainQueue();
- }
- }
-
- function drainQueue() {
- if (draining) {
- return;
- }
- var timeout = setTimeout(cleanUpNextTick);
- draining = true;
-
- var len = queue.length;
- while(len) {
- currentQueue = queue;
- queue = [];
- while (++queueIndex < len) {
- if (currentQueue) {
- currentQueue[queueIndex].run();
- }
- }
- queueIndex = -1;
- len = queue.length;
- }
- currentQueue = null;
- draining = false;
- clearTimeout(timeout);
- }
-
- process.nextTick = function (fun) {
- var args = new Array(arguments.length - 1);
- if (arguments.length > 1) {
- for (var i = 1; i < arguments.length; i++) {
- args[i - 1] = arguments[i];
- }
- }
- queue.push(new Item(fun, args));
- if (queue.length === 1 && !draining) {
- setTimeout(drainQueue, 0);
- }
- };
-
- // v8 likes predictible objects
- function Item(fun, array) {
- this.fun = fun;
- this.array = array;
- }
- Item.prototype.run = function () {
- this.fun.apply(null, this.array);
- };
- process.title = 'browser';
- process.browser = true;
- process.env = {};
- process.argv = [];
- process.version = ''; // empty string to avoid regexp issues
- process.versions = {};
-
- function noop() {}
-
- process.on = noop;
- process.addListener = noop;
- process.once = noop;
- process.off = noop;
- process.removeListener = noop;
- process.removeAllListeners = noop;
- process.emit = noop;
-
- process.binding = function (name) {
- throw new Error('process.binding is not supported');
- };
-
- process.cwd = function () { return '/' };
- process.chdir = function (dir) {
- throw new Error('process.chdir is not supported');
- };
- process.umask = function() { return 0; };
-
-
-/***/ },
-/* 4 */
-/***/ function(module, exports) {
-
- module.exports = function isBuffer(arg) {
- return arg && typeof arg === 'object'
- && typeof arg.copy === 'function'
- && typeof arg.fill === 'function'
- && typeof arg.readUInt8 === 'function';
- }
-
-/***/ },
-/* 5 */
-/***/ function(module, exports) {
-
- if (typeof Object.create === 'function') {
- // implementation from standard node.js 'util' module
- module.exports = function inherits(ctor, superCtor) {
- ctor.super_ = superCtor
- ctor.prototype = Object.create(superCtor.prototype, {
- constructor: {
- value: ctor,
- enumerable: false,
- writable: true,
- configurable: true
- }
- });
- };
- } else {
- // old school shim for old browsers
- module.exports = function inherits(ctor, superCtor) {
- ctor.super_ = superCtor
- var TempCtor = function () {}
- TempCtor.prototype = superCtor.prototype
- ctor.prototype = new TempCtor()
- ctor.prototype.constructor = ctor
- }
- }
-
-
-/***/ },
-/* 6 */
-/***/ function(module, exports, __webpack_require__) {
-
- var EventEmitter = __webpack_require__(1);
- var Sequencer = __webpack_require__(7);
- var Thread = __webpack_require__(9);
- var util = __webpack_require__(2);
-
- var defaultBlockPackages = {
- 'scratch3': __webpack_require__(11),
- 'wedo2': __webpack_require__(12)
- };
-
- /**
- * Manages blocks, stacks, and the sequencer.
- */
- function Runtime () {
- // Bind event emitter
- EventEmitter.call(this);
-
- // State for the runtime
- /**
- * All blocks in the workspace.
- * Keys are block IDs, values are metadata about the block.
- * @type {Object.}
- */
- this.blocks = {};
-
- /**
- * All stacks in the workspace.
- * A list of block IDs that represent stacks (first block in stack).
- * @type {Array.}
- */
- this.stacks = [];
-
- /**
- * A list of threads that are currently running in the VM.
- * Threads are added when execution starts and pruned when execution ends.
- * @type {Array.}
- */
- this.threads = [];
-
- /** @type {!Sequencer} */
- this.sequencer = new Sequencer(this);
-
- /**
- * Map to look up a block primitive's implementation function by its opcode.
- * This is a two-step lookup: package name first, then primitive name.
- * @type {Object.}
- */
- this._primitives = {};
- this._registerBlockPackages();
- }
-
- /**
- * Event name for glowing a stack
- * @const {string}
- */
- Runtime.STACK_GLOW_ON = 'STACK_GLOW_ON';
-
- /**
- * Event name for unglowing a stack
- * @const {string}
- */
- Runtime.STACK_GLOW_OFF = 'STACK_GLOW_OFF';
-
- /**
- * Event name for glowing a block
- * @const {string}
- */
- Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
-
- /**
- * Event name for unglowing a block
- * @const {string}
- */
- Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
-
- /**
- * Inherit from EventEmitter
- */
- util.inherits(Runtime, EventEmitter);
-
- /**
- * How rapidly we try to step threads, in ms.
- */
- Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
-
- /**
- * Block management: create blocks and stacks from a `create` event
- * @param {!Object} block Blockly create event to be processed
- */
- Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
- // Create new block
- this.blocks[block.id] = block;
-
- // Walk each field and add any shadow blocks
- // @todo Expand this to cover vertical / nested blocks
- for (var i in block.fields) {
- var shadows = block.fields[i].blocks;
- for (var y in shadows) {
- var shadow = shadows[y];
- this.blocks[shadow.id] = shadow;
- }
- }
-
- // Push block id to stacks array. New blocks are always a stack even if only
- // momentary. If the new block is added to an existing stack this stack will
- // be removed by the `moveBlock` method below.
- if (!opt_isFlyoutBlock) {
- this.stacks.push(block.id);
- }
- };
-
- /**
- * Block management: change block field values
- * @param {!Object} args Blockly change event to be processed
- */
- Runtime.prototype.changeBlock = function (args) {
- // Validate
- if (args.element !== 'field') return;
- if (typeof this.blocks[args.id] === 'undefined') return;
- if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
-
- // Update block value
- this.blocks[args.id].fields[args.name].value = args.value;
- };
-
- /**
- * Block management: move blocks from parent to parent
- * @param {!Object} e Blockly move event to be processed
- */
- Runtime.prototype.moveBlock = function (e) {
- var _this = this;
-
- // Block was removed from parent
- if (e.newParent === undefined && e.oldParent !== undefined) {
- // Add stack
- _this.stacks.push(e.id);
-
- // Update old parent
- if (e.oldField === undefined) {
- _this.blocks[e.oldParent].next = null;
- } else {
- delete _this.blocks[e.oldParent].fields[e.oldField];
- }
- } else if (e.newParent !== undefined) {
- // Block was moved to a new parent
- // Either happens because it was previously parentless
- // (e.oldParent === undefined)
- // or because a block was moved in front of it.
-
- // Remove stack
- _this._deleteStack(e.id);
-
- // Update new parent
- if (e.newField === undefined) {
- _this.blocks[e.newParent].next = e.id;
- } else {
- _this.blocks[e.newParent].fields[e.newField] = {
- name: e.newField,
- value: e.id,
- blocks: {}
- };
- }
- }
- };
-
- /**
- * Block management: delete blocks and their associated stacks
- * @param {!Object} e Blockly delete event to be processed
- */
- Runtime.prototype.deleteBlock = function (e) {
- // @todo Stop threads running on this stack
-
- // Get block
- var block = this.blocks[e.id];
-
- // Delete children
- if (block.next !== null) {
- this.deleteBlock({id: block.next});
- }
-
- // Delete substacks and fields
- for (var field in block.fields) {
- if (field === 'SUBSTACK') {
- this.deleteBlock({id: block.fields[field].value});
- } else {
- for (var shadow in block.fields[field].blocks) {
- this.deleteBlock({id: shadow});
- }
- }
- }
-
- // Delete stack
- this._deleteStack(e.id);
-
- // Delete block
- delete this.blocks[e.id];
- };
-
- // -----------------------------------------------------------------------------
- // -----------------------------------------------------------------------------
-
- /**
- * Register default block packages with this runtime.
- * @todo Prefix opcodes with package name.
- * @private
- */
- Runtime.prototype._registerBlockPackages = function () {
- for (var packageName in defaultBlockPackages) {
- if (defaultBlockPackages.hasOwnProperty(packageName)) {
- // @todo pass a different runtime depending on package privilege?
- var packageObject = new (defaultBlockPackages[packageName])(this);
- var packageContents = packageObject.getPrimitives();
- for (var op in packageContents) {
- if (packageContents.hasOwnProperty(op)) {
- this._primitives[op] =
- packageContents[op].bind(packageObject);
- }
- }
- }
- }
- };
-
- /**
- * Retrieve the function associated with the given opcode.
- * @param {!string} opcode The opcode to look up.
- * @return {Function} The function which implements the opcode.
- */
- Runtime.prototype.getOpcodeFunction = function (opcode) {
- return this._primitives[opcode];
- };
-
- // -----------------------------------------------------------------------------
- // -----------------------------------------------------------------------------
-
- /**
- * Create a thread and push it to the list of threads.
- * @param {!string} id ID of block that starts the stack
- */
- Runtime.prototype._pushThread = function (id) {
- this.emit(Runtime.STACK_GLOW_ON, id);
- var thread = new Thread(id);
- this.threads.push(thread);
- };
-
- /**
- * Remove a thread from the list of threads.
- * @param {?Thread} thread Thread object to remove from actives
- */
- Runtime.prototype._removeThread = function (thread) {
- var i = this.threads.indexOf(thread);
- if (i > -1) {
- this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
- this.threads.splice(i, 1);
- }
- };
-
- /**
- * Toggle a stack
- * @param {!string} stackId ID of block that starts the stack
- */
- Runtime.prototype.toggleStack = function (stackId) {
- // Remove any existing thread
- for (var i = 0; i < this.threads.length; i++) {
- if (this.threads[i].topBlock == stackId) {
- this._removeThread(this.threads[i]);
- return;
- }
- }
- // Otherwise add it
- this._pushThread(stackId);
- };
-
- /**
- * Green flag, which stops currently running threads
- * and adds all top-level stacks that start with the green flag
- */
- Runtime.prototype.greenFlag = function () {
- // Remove all existing threads
- for (var i = 0; i < this.threads.length; i++) {
- this._removeThread(this.threads[i]);
- }
- // Add all top stacks with green flag
- for (var j = 0; j < this.stacks.length; j++) {
- var topBlock = this.stacks[j];
- if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
- this._pushThread(this.stacks[j]);
- }
- }
- };
-
- /**
- * Distance sensor hack
- */
- Runtime.prototype.startDistanceSensors = function () {
- // Add all top stacks with distance sensor
- for (var j = 0; j < this.stacks.length; j++) {
- var topBlock = this.stacks[j];
- if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') {
- var alreadyRunning = false;
- for (var k = 0; k < this.threads.length; k++) {
- if (this.threads[k].topBlock === topBlock) {
- alreadyRunning = true;
- }
- }
- if (!alreadyRunning) {
- this._pushThread(this.stacks[j]);
- }
- }
- }
- };
-
- /**
- * Stop "everything"
- */
- Runtime.prototype.stopAll = function () {
- var threadsCopy = this.threads.slice();
- while (threadsCopy.length > 0) {
- this._removeThread(threadsCopy.pop());
- }
- // @todo call stop function in all extensions/packages/WeDo stub
- if (window.native) {
- window.native.motorStop();
- }
- };
-
- /**
- * Repeatedly run `sequencer.stepThreads` and filter out
- * inactive threads after each iteration.
- */
- Runtime.prototype._step = function () {
- var inactiveThreads = this.sequencer.stepThreads(this.threads);
- for (var i = 0; i < inactiveThreads.length; i++) {
- this._removeThread(inactiveThreads[i]);
- }
- };
-
- /**
- * Emit feedback for block glowing (used in the sequencer).
- * @param {?string} blockId ID for the block to update glow
- * @param {boolean} isGlowing True to turn on glow; false to turn off.
- */
- Runtime.prototype.glowBlock = function (blockId, isGlowing) {
- if (isGlowing) {
- this.emit(Runtime.BLOCK_GLOW_ON, blockId);
- } else {
- this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
- }
- };
-
- /**
- * Set up timers to repeatedly step in a browser
- */
- Runtime.prototype.start = function () {
- if (!window.setInterval) return;
- window.setInterval(function() {
- this._step();
- }.bind(this), Runtime.THREAD_STEP_INTERVAL);
- };
-
- // -----------------------------------------------------------------------------
- // -----------------------------------------------------------------------------
-
- /**
- * Helper to remove a stack from `this.stacks`
- * @param {?string} id ID of block that starts the stack
- */
- Runtime.prototype._deleteStack = function (id) {
- var i = this.stacks.indexOf(id);
- if (i > -1) this.stacks.splice(i, 1);
- };
-
- /**
- * Helper to get the next block for a particular block
- * @param {?string} id ID of block to get the next block for
- * @return {?string} ID of next block in the sequence
- */
- Runtime.prototype._getNextBlock = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].next;
- };
-
- /**
- * Helper to get the substack for a particular C-shaped block
- * @param {?string} id ID for block to get the substack for
- * @return {?string} ID of block in the substack
- */
- Runtime.prototype._getSubstack = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].fields['SUBSTACK'];
- };
-
- /**
- * Helper to get the opcode for a particular block
- * @param {?string} id ID of block to query
- * @return {?string} the opcode corresponding to that block
- */
- Runtime.prototype._getOpcode = function (id) {
- if (typeof this.blocks[id] === 'undefined') return null;
- return this.blocks[id].opcode;
- };
-
- module.exports = Runtime;
-
-
-/***/ },
-/* 7 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Timer = __webpack_require__(8);
- var Thread = __webpack_require__(9);
- var YieldTimers = __webpack_require__(10);
-
- function Sequencer (runtime) {
- /**
- * A utility timer for timing thread sequencing.
- * @type {!Timer}
- */
- this.timer = new Timer();
-
- /**
- * Reference to the runtime owning this sequencer.
- * @type {!Runtime}
- */
- this.runtime = runtime;
- }
-
- /**
- * The sequencer does as much work as it can within WORK_TIME milliseconds,
- * then yields. This is essentially a rate-limiter for blocks.
- * In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
- * @const {!number}
- */
- Sequencer.WORK_TIME = 10;
-
- /**
- * Step through all threads in `this.threads`, running them in order.
- * @return {Array.} All threads which have finished in this iteration.
- */
- Sequencer.prototype.stepThreads = function (threads) {
- // Start counting toward WORK_TIME
- this.timer.start();
- // List of threads which have been killed by this step.
- var inactiveThreads = [];
- // If all of the threads are yielding, we should yield.
- var numYieldingThreads = 0;
- // While there are still threads to run and we are within WORK_TIME,
- // continue executing threads.
- while (threads.length > 0 &&
- threads.length > numYieldingThreads &&
- this.timer.timeElapsed() < Sequencer.WORK_TIME) {
- // New threads at the end of the iteration.
- var newThreads = [];
- // Attempt to run each thread one time
- for (var i = 0; i < threads.length; i++) {
- var activeThread = threads[i];
- if (activeThread.status === Thread.STATUS_RUNNING) {
- // Normal-mode thread: step.
- this.stepThread(activeThread);
- } else if (activeThread.status === Thread.STATUS_YIELD) {
- // Yield-mode thread: check if the time has passed.
- YieldTimers.resolve(activeThread.yieldTimerId);
- numYieldingThreads++;
- } else if (activeThread.status === Thread.STATUS_DONE) {
- // Moved to a done state - finish up
- activeThread.status = Thread.STATUS_RUNNING;
- // @todo Deal with the return value
- }
- // First attempt to pop from the stack
- if (activeThread.stack.length > 0 &&
- activeThread.nextBlock === null &&
- activeThread.status === Thread.STATUS_DONE) {
- activeThread.nextBlock = activeThread.stack.pop();
- // Don't pop stack frame - we need the data.
- // A new one won't be created when we execute.
- if (activeThread.nextBlock !== null) {
- activeThread.status === Thread.STATUS_RUNNING;
- }
- }
- if (activeThread.nextBlock === null &&
- activeThread.status === Thread.STATUS_DONE) {
- // Finished with this thread - tell runtime to clean it up.
- inactiveThreads.push(activeThread);
- } else {
- // Keep this thead in the loop.
- newThreads.push(activeThread);
- }
- }
- // Effectively filters out threads that have stopped.
- threads = newThreads;
- }
- return inactiveThreads;
- };
-
- /**
- * Step the requested thread
- * @param {!Thread} thread Thread object to step
- */
- Sequencer.prototype.stepThread = function (thread) {
- // Save the yield timer ID, in case a primitive makes a new one
- // @todo hack - perhaps patch this to allow more than one timer per
- // primitive, for example...
- var oldYieldTimerId = YieldTimers.timerId;
-
- // Save the current block and set the nextBlock.
- // If the primitive would like to do control flow,
- // it can overwrite nextBlock.
- var currentBlock = thread.nextBlock;
- if (!currentBlock || !this.runtime.blocks[currentBlock]) {
- thread.status = Thread.STATUS_DONE;
- return;
- }
- thread.nextBlock = this.runtime._getNextBlock(currentBlock);
-
- var opcode = this.runtime._getOpcode(currentBlock);
-
- // Push the current block to the stack
- thread.stack.push(currentBlock);
- // Push an empty stack frame, if we need one.
- // Might not, if we just popped the stack.
- if (thread.stack.length > thread.stackFrames.length) {
- thread.stackFrames.push({});
- }
- var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];
-
- /**
- * A callback for the primitive to indicate its thread should yield.
- * @type {Function}
- */
- var threadYieldCallback = function () {
- thread.status = Thread.STATUS_YIELD;
- };
-
- /**
- * A callback for the primitive to indicate its thread is finished
- * @type {Function}
- */
- var instance = this;
- var threadDoneCallback = function () {
- thread.status = Thread.STATUS_DONE;
- // Refresh nextBlock in case it has changed during a yield.
- thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
- // Pop the stack and stack frame
- thread.stack.pop();
- thread.stackFrames.pop();
- };
-
- /**
- * A callback for the primitive to start hats.
- * @todo very hacked...
- */
- var startHats = function(callback) {
- for (var i = 0; i < instance.runtime.stacks.length; i++) {
- var stack = instance.runtime.stacks[i];
- var stackBlock = instance.runtime.blocks[stack];
- var result = callback(stackBlock);
- if (result) {
- // Check if the stack is already running
- var stackRunning = false;
-
- for (var j = 0; j < instance.runtime.threads.length; j++) {
- if (instance.runtime.threads[j].topBlock == stack) {
- stackRunning = true;
- break;
- }
- }
- if (!stackRunning) {
- instance.runtime._pushThread(stack);
- }
- }
- }
- };
-
- /**
- * Record whether we have switched stack,
- * to avoid proceeding the thread automatically.
- * @type {boolean}
- */
- var switchedStack = false;
- /**
- * A callback for a primitive to start a substack.
- * @type {Function}
- */
- var threadStartSubstack = function () {
- // Set nextBlock to the start of the substack
- var substack = instance.runtime._getSubstack(currentBlock);
- if (substack && substack.value) {
- thread.nextBlock = substack.value;
- } else {
- thread.nextBlock = null;
- }
- switchedStack = true;
- };
-
- // @todo extreme hack to get the single argument value for prototype
- var argValues = [];
- var blockInputs = this.runtime.blocks[currentBlock].fields;
- for (var bi in blockInputs) {
- var outer = blockInputs[bi];
- for (var b in outer.blocks) {
- var block = outer.blocks[b];
- var fields = block.fields;
- for (var f in fields) {
- var field = fields[f];
- argValues.push(field.value);
- }
- }
- }
-
- if (!opcode) {
- console.warn('Could not get opcode for block: ' + currentBlock);
- }
- else {
- var blockFunction = this.runtime.getOpcodeFunction(opcode);
- if (!blockFunction) {
- console.warn('Could not get implementation for opcode: ' + opcode);
- }
- else {
- try {
- // @todo deal with the return value
- blockFunction(argValues, {
- yield: threadYieldCallback,
- done: threadDoneCallback,
- timeout: YieldTimers.timeout,
- stackFrame: currentStackFrame,
- startSubstack: threadStartSubstack,
- startHats: startHats
- });
- }
- catch(e) {
- console.error(
- 'Exception calling block function for opcode: ' +
- opcode + '\n' + e);
- } finally {
- // Update if the thread has set a yield timer ID
- // @todo hack
- if (YieldTimers.timerId > oldYieldTimerId) {
- thread.yieldTimerId = YieldTimers.timerId;
- }
- if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
- // Thread executed without yielding - move to done
- threadDoneCallback();
- }
- }
- }
- }
-
- };
-
- module.exports = Sequencer;
-
-
-/***/ },
-/* 8 */
-/***/ function(module, exports) {
-
- /**
- * Constructor
- */
- function Timer () {
- this.startTime = 0;
- }
-
- Timer.prototype.time = function () {
- return Date.now();
- };
-
- Timer.prototype.start = function () {
- this.startTime = this.time();
- };
-
- Timer.prototype.timeElapsed = function () {
- return this.time() - this.startTime;
- };
-
- module.exports = Timer;
-
-
-/***/ },
-/* 9 */
-/***/ function(module, exports) {
-
- /**
- * A thread is a running stack context and all the metadata needed.
- * @param {?string} firstBlock First block to execute in the thread.
- * @constructor
- */
- function Thread (firstBlock) {
- /**
- * ID of top block of the thread
- * @type {!string}
- */
- this.topBlock = firstBlock;
- /**
- * ID of next block that the thread will execute, or null if none.
- * @type {?string}
- */
- this.nextBlock = firstBlock;
- /**
- * Stack for the thread. When the sequencer enters a control structure,
- * the block is pushed onto the stack so we know where to exit.
- * @type {Array.}
- */
- this.stack = [];
-
- /**
- * Stack frames for the thread. Store metadata for the executing blocks.
- * @type {Array.}
- */
- this.stackFrames = [];
-
- /**
- * Status of the thread, one of three states (below)
- * @type {number}
- */
- this.status = 0; /* Thread.STATUS_RUNNING */
-
- /**
- * Yield timer ID (for checking when the thread should unyield).
- * @type {number}
- */
- this.yieldTimerId = -1;
- }
-
- /**
- * Thread status for initialized or running thread.
- * Threads are in this state when the primitive is called for the first time.
- * @const
- */
- Thread.STATUS_RUNNING = 0;
-
- /**
- * Thread status for a yielded thread.
- * Threads are in this state when a primitive has yielded.
- * @const
- */
- Thread.STATUS_YIELD = 1;
-
- /**
- * Thread status for a finished/done thread.
- * Thread is moved to this state when the interpreter
- * can proceed with execution.
- * @const
- */
- Thread.STATUS_DONE = 2;
-
- module.exports = Thread;
-
-
-/***/ },
-/* 10 */
-/***/ function(module, exports, __webpack_require__) {
-
- /**
- * @fileoverview Timers that are synchronized with the Scratch sequencer.
- */
- var Timer = __webpack_require__(8);
-
- function YieldTimers () {}
-
- /**
- * Shared collection of timers.
- * Each timer is a [Function, number] with the callback
- * and absolute time for it to run.
- * @type {Object.}
- */
- YieldTimers.timers = {};
-
- /**
- * Monotonically increasing timer ID.
- * @type {number}
- */
- YieldTimers.timerId = 0;
-
- /**
- * Utility for measuring time.
- * @type {!Timer}
- */
- YieldTimers.globalTimer = new Timer();
-
- /**
- * The timeout function is passed to primitives and is intended
- * as a convenient replacement for window.setTimeout.
- * The sequencer will attempt to resolve the timer every time
- * the yielded thread would have been stepped.
- * @param {!Function} callback To be called when the timer is done.
- * @param {number} timeDelta Time to wait, in ms.
- * @return {number} Timer ID to be used with other methods.
- */
- YieldTimers.timeout = function (callback, timeDelta) {
- var id = ++YieldTimers.timerId;
- YieldTimers.timers[id] = [
- callback,
- YieldTimers.globalTimer.time() + timeDelta
- ];
- return id;
- };
-
- /**
- * Attempt to resolve a timeout.
- * If the time has passed, call the callback.
- * Otherwise, do nothing.
- * @param {number} id Timer ID to resolve.
- * @return {boolean} True if the timer has resolved.
- */
- YieldTimers.resolve = function (id) {
- var timer = YieldTimers.timers[id];
- if (!timer) {
- // No such timer.
- return false;
- }
- var callback = timer[0];
- var time = timer[1];
- if (YieldTimers.globalTimer.time() < time) {
- // Not done yet.
- return false;
- }
- // Execute the callback and remove the timer.
- callback();
- delete YieldTimers.timers[id];
- return true;
- };
-
- /**
- * Reject a timer so the callback never executes.
- * @param {number} id Timer ID to reject.
- */
- YieldTimers.reject = function (id) {
- if (YieldTimers.timers[id]) {
- delete YieldTimers.timers[id];
- }
- };
-
- /**
- * Reject all timers currently stored.
- * Especially useful for a Scratch "stop."
- */
- YieldTimers.rejectAll = function () {
- YieldTimers.timers = {};
- YieldTimers.timerId = 0;
- };
-
- module.exports = YieldTimers;
-
-
-/***/ },
-/* 11 */
-/***/ function(module, exports) {
-
- function Scratch3Blocks(runtime) {
- /**
- * The runtime instantiating this block package.
- * @type {Runtime}
- */
- this.runtime = runtime;
- }
-
- /**
- * Retrieve the block primitives implemented by this package.
- * @return {Object.} Mapping of opcode to Function.
- */
- Scratch3Blocks.prototype.getPrimitives = function() {
- return {
- 'control_repeat': this.repeat,
- 'control_forever': this.forever,
- 'control_wait': this.wait,
- 'control_stop': this.stop,
- 'event_whenflagclicked': this.whenFlagClicked,
- 'event_whenbroadcastreceived': this.whenBroadcastReceived,
- 'event_broadcast': this.broadcast
- };
- };
-
- Scratch3Blocks.prototype.repeat = function(argValues, util) {
- console.log('Running: control_repeat');
- // Initialize loop
- if (util.stackFrame.loopCounter === undefined) {
- util.stackFrame.loopCounter = parseInt(argValues[0]); // @todo arg
- }
- // Decrease counter
- util.stackFrame.loopCounter--;
- // If we still have some left, start the substack
- if (util.stackFrame.loopCounter >= 0) {
- util.startSubstack();
- }
- };
-
- Scratch3Blocks.prototype.forever = function(argValues, util) {
- console.log('Running: control_forever');
- util.startSubstack();
- };
-
- Scratch3Blocks.prototype.wait = function(argValues, util) {
- console.log('Running: control_wait');
- util.yield();
- util.timeout(function() {
- util.done();
- }, 1000 * parseFloat(argValues[0]));
- };
-
- Scratch3Blocks.prototype.stop = function() {
- console.log('Running: control_stop');
- // @todo - don't use this.runtime
- this.runtime.stopAll();
- };
-
- Scratch3Blocks.prototype.whenFlagClicked = function() {
- console.log('Running: event_whenflagclicked');
- // No-op
- };
-
- Scratch3Blocks.prototype.whenBroadcastReceived = function() {
- console.log('Running: event_whenbroadcastreceived');
- // No-op
- };
-
- Scratch3Blocks.prototype.broadcast = function(argValues, util) {
- console.log('Running: event_broadcast');
- util.startHats(function(hat) {
- if (hat.opcode === 'event_whenbroadcastreceived') {
- var shadows = hat.fields.CHOICE.blocks;
- for (var sb in shadows) {
- var shadowblock = shadows[sb];
- return shadowblock.fields.CHOICE.value === argValues[0];
- }
- }
- return false;
- });
- };
-
- module.exports = Scratch3Blocks;
-
-
-/***/ },
-/* 12 */
-/***/ function(module, exports, __webpack_require__) {
-
-
- var YieldTimers = __webpack_require__(10);
-
- function WeDo2Blocks(runtime) {
- /**
- * The runtime instantiating this block package.
- * @type {Runtime}
- */
- this.runtime = runtime;
-
- /**
- * Current motor speed, as a percentage (100 = full speed).
- * @type {number}
- * @private
- */
- this._motorSpeed = 100;
-
- /**
- * The timeout ID for a pending motor action.
- * @type {?int}
- * @private
- */
- this._motorTimeout = null;
- }
-
- /**
- * Retrieve the block primitives implemented by this package.
- * @return {Object.} Mapping of opcode to Function.
- */
- WeDo2Blocks.prototype.getPrimitives = function() {
- return {
- 'wedo_motorclockwise': this.motorClockwise,
- 'wedo_motorcounterclockwise': this.motorCounterClockwise,
- 'wedo_motorspeed': this.motorSpeed,
- 'wedo_setcolor': this.setColor,
- 'wedo_whendistanceclose': this.whenDistanceClose,
- 'wedo_whentilt': this.whenTilt
- };
- };
-
- /**
- * Clamp a value between a minimum and maximum value.
- * @todo move this to a common utility class.
- * @param val The value to clamp.
- * @param min The minimum return value.
- * @param max The maximum return value.
- * @returns {number} The clamped value.
- * @private
- */
- WeDo2Blocks.prototype._clamp = function(val, min, max) {
- return Math.max(min, Math.min(val, max));
- };
-
- /**
- * Common implementation for motor blocks.
- * @param direction The direction to turn ('left' or 'right').
- * @param durationSeconds The number of seconds to run.
- * @param util The util instance to use for yielding and finishing.
- * @private
- */
- WeDo2Blocks.prototype._motorOnFor = function(direction, durationSeconds, util) {
- if (this._motorTimeout > 0) {
- // @todo maybe this should go through util
- YieldTimers.resolve(this._motorTimeout);
- this._motorTimeout = null;
- }
- if (window.native) {
- window.native.motorRun(direction, this._motorSpeed);
- }
-
- var instance = this;
- var myTimeout = this._motorTimeout = util.timeout(function() {
- if (instance._motorTimeout == myTimeout) {
- instance._motorTimeout = null;
- }
- if (window.native) {
- window.native.motorStop();
- }
- util.done();
- }, 1000 * durationSeconds);
-
- util.yield();
- };
-
- WeDo2Blocks.prototype.motorClockwise = function(argValues, util) {
- this._motorOnFor('right', parseFloat(argValues[0]), util);
- };
-
- WeDo2Blocks.prototype.motorCounterClockwise = function(argValues, util) {
- this._motorOnFor('left', parseFloat(argValues[0]), util);
- };
-
- WeDo2Blocks.prototype.motorSpeed = function(argValues) {
- var speed = argValues[0];
- switch (speed) {
- case 'slow':
- this._motorSpeed = 20;
- break;
- case 'medium':
- this._motorSpeed = 50;
- break;
- case 'fast':
- this._motorSpeed = 100;
- break;
- }
- };
-
- /**
- * Convert a color name to a WeDo color index.
- * Supports 'mystery' for a random hue.
- * @param colorName The color to retrieve.
- * @returns {number} The WeDo color index.
- * @private
- */
- WeDo2Blocks.prototype._getColor = function(colorName) {
- var colors = {
- 'yellow': 7,
- 'orange': 8,
- 'coral': 9,
- 'magenta': 1,
- 'purple': 2,
- 'blue': 3,
- 'green': 6,
- 'white': 10
- };
-
- if (colorName == 'mystery') {
- return Math.floor((Math.random() * 10) + 1);
- }
-
- return colors[colorName];
- };
-
- WeDo2Blocks.prototype.setColor = function(argValues, util) {
- if (window.native) {
- var colorIndex = this._getColor(argValues[0]);
- window.native.setLedColor(colorIndex);
- }
- // Pause for quarter second
- util.yield();
- util.timeout(function() {
- util.done();
- }, 250);
- };
-
- WeDo2Blocks.prototype.whenDistanceClose = function() {
- console.log('Running: wedo_whendistanceclose');
- };
-
- WeDo2Blocks.prototype.whenTilt = function() {
- console.log('Running: wedo_whentilt');
- };
-
- module.exports = WeDo2Blocks;
-
-
-/***/ },
-/* 13 */
-/***/ function(module, exports, __webpack_require__) {
-
- var html = __webpack_require__(14);
- var memoize = __webpack_require__(65);
- var parseDOM = memoize(html.parseDOM, {
- length: 1,
- resolvers: [String],
- max: 200
- });
-
- /**
- * Adapter between block creation events and block representation which can be
- * used by the Scratch runtime.
- *
- * @param {Object} `Blockly.events.create`
- *
- * @return {Object}
- */
- module.exports = function (e) {
- // Validate input
- if (typeof e !== 'object') return;
- if (typeof e.blockId !== 'string') return;
- if (typeof e.xml !== 'object') return;
-
- // Storage object
- var obj = {
- id: e.blockId,
- opcode: null,
- next: null,
- fields: {}
- };
-
- // Set opcode
- if (typeof e.xml.attributes === 'object') {
- obj.opcode = e.xml.attributes.type.value;
- }
-
- // Extract fields from event's `innerHTML`
- if (typeof e.xml.innerHTML !== 'string') return obj;
- if (e.xml.innerHTML === '') return obj;
- obj.fields = extract(parseDOM(e.xml.innerHTML));
-
- return obj;
- };
-
- /**
- * Extracts fields from a block's innerHTML.
- * @todo Extend this to support vertical grammar / nested blocks.
- *
- * @param {Object} DOM representation of block's innerHTML
- *
- * @return {Object}
- */
- function extract (dom) {
- // Storage object
- var fields = {};
-
- // Field
- var field = dom[0];
- var fieldName = field.attribs.name;
- fields[fieldName] = {
- name: fieldName,
- value: null,
- blocks: {}
- };
-
- // Shadow block
- var shadow = field.children[0];
- var shadowId = shadow.attribs.id;
- var shadowOpcode = shadow.attribs.type;
- fields[fieldName].blocks[shadowId] = {
- id: shadowId,
- opcode: shadowOpcode,
- next: null,
- fields: {}
- };
-
- // Primitive
- var primitive = shadow.children[0];
- var primitiveName = primitive.attribs.name;
- var primitiveValue = primitive.children[0].data;
- fields[fieldName].blocks[shadowId].fields[primitiveName] = {
- name: primitiveName,
- value: primitiveValue,
- blocks: null
- };
-
- return fields;
- }
-
-
-/***/ },
-/* 14 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Parser = __webpack_require__(15),
- DomHandler = __webpack_require__(22);
-
- function defineProp(name, value){
- delete module.exports[name];
- module.exports[name] = value;
- return value;
- }
-
- module.exports = {
- Parser: Parser,
- Tokenizer: __webpack_require__(16),
- ElementType: __webpack_require__(23),
- DomHandler: DomHandler,
- get FeedHandler(){
- return defineProp("FeedHandler", __webpack_require__(26));
- },
- get Stream(){
- return defineProp("Stream", __webpack_require__(27));
- },
- get WritableStream(){
- return defineProp("WritableStream", __webpack_require__(28));
- },
- get ProxyHandler(){
- return defineProp("ProxyHandler", __webpack_require__(51));
- },
- get DomUtils(){
- return defineProp("DomUtils", __webpack_require__(52));
- },
- get CollectingHandler(){
- return defineProp("CollectingHandler", __webpack_require__(64));
- },
- // For legacy support
- DefaultHandler: DomHandler,
- get RssHandler(){
- return defineProp("RssHandler", this.FeedHandler);
- },
- //helper methods
- parseDOM: function(data, options){
- var handler = new DomHandler(options);
- new Parser(handler, options).end(data);
- return handler.dom;
- },
- parseFeed: function(feed, options){
- var handler = new module.exports.FeedHandler(options);
- new Parser(handler, options).end(feed);
- return handler.dom;
- },
- createDomStream: function(cb, options, elementCb){
- var handler = new DomHandler(cb, options, elementCb);
- return new Parser(handler, options);
- },
- // List of all events that the parser emits
- EVENTS: { /* Format: eventname: number of arguments */
- attribute: 2,
- cdatastart: 0,
- cdataend: 0,
- text: 1,
- processinginstruction: 2,
- comment: 1,
- commentend: 0,
- closetag: 1,
- opentag: 2,
- opentagname: 1,
- error: 1,
- end: 0
- }
- };
-
-
-/***/ },
-/* 15 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Tokenizer = __webpack_require__(16);
-
- /*
- Options:
-
- xmlMode: Disables the special behavior for script/style tags (false by default)
- lowerCaseAttributeNames: call .toLowerCase for each attribute name (true if xmlMode is `false`)
- lowerCaseTags: call .toLowerCase for each tag name (true if xmlMode is `false`)
- */
-
- /*
- Callbacks:
-
- oncdataend,
- oncdatastart,
- onclosetag,
- oncomment,
- oncommentend,
- onerror,
- onopentag,
- onprocessinginstruction,
- onreset,
- ontext
- */
-
- var formTags = {
- input: true,
- option: true,
- optgroup: true,
- select: true,
- button: true,
- datalist: true,
- textarea: true
- };
-
- var openImpliesClose = {
- tr : { tr:true, th:true, td:true },
- th : { th:true },
- td : { thead:true, th:true, td:true },
- body : { head:true, link:true, script:true },
- li : { li:true },
- p : { p:true },
- h1 : { p:true },
- h2 : { p:true },
- h3 : { p:true },
- h4 : { p:true },
- h5 : { p:true },
- h6 : { p:true },
- select : formTags,
- input : formTags,
- output : formTags,
- button : formTags,
- datalist: formTags,
- textarea: formTags,
- option : { option:true },
- optgroup: { optgroup:true }
- };
-
- var voidElements = {
- __proto__: null,
- area: true,
- base: true,
- basefont: true,
- br: true,
- col: true,
- command: true,
- embed: true,
- frame: true,
- hr: true,
- img: true,
- input: true,
- isindex: true,
- keygen: true,
- link: true,
- meta: true,
- param: true,
- source: true,
- track: true,
- wbr: true,
-
- //common self closing svg elements
- path: true,
- circle: true,
- ellipse: true,
- line: true,
- rect: true,
- use: true,
- stop: true,
- polyline: true,
- polygon: true
- };
-
- var re_nameEnd = /\s|\//;
-
- function Parser(cbs, options){
- this._options = options || {};
- this._cbs = cbs || {};
-
- this._tagname = "";
- this._attribname = "";
- this._attribvalue = "";
- this._attribs = null;
- this._stack = [];
-
- this.startIndex = 0;
- this.endIndex = null;
-
- this._lowerCaseTagNames = "lowerCaseTags" in this._options ?
- !!this._options.lowerCaseTags :
- !this._options.xmlMode;
- this._lowerCaseAttributeNames = "lowerCaseAttributeNames" in this._options ?
- !!this._options.lowerCaseAttributeNames :
- !this._options.xmlMode;
- if(!!this._options.Tokenizer) {
- Tokenizer = this._options.Tokenizer;
- }
- this._tokenizer = new Tokenizer(this._options, this);
-
- if(this._cbs.onparserinit) this._cbs.onparserinit(this);
- }
-
- __webpack_require__(2).inherits(Parser, __webpack_require__(1).EventEmitter);
-
- Parser.prototype._updatePosition = function(initialOffset){
- if(this.endIndex === null){
- if(this._tokenizer._sectionStart <= initialOffset){
- this.startIndex = 0;
- } else {
- this.startIndex = this._tokenizer._sectionStart - initialOffset;
- }
- }
- else this.startIndex = this.endIndex + 1;
- this.endIndex = this._tokenizer.getAbsoluteIndex();
- };
-
- //Tokenizer event handlers
- Parser.prototype.ontext = function(data){
- this._updatePosition(1);
- this.endIndex--;
-
- if(this._cbs.ontext) this._cbs.ontext(data);
- };
-
- Parser.prototype.onopentagname = function(name){
- if(this._lowerCaseTagNames){
- name = name.toLowerCase();
- }
-
- this._tagname = name;
-
- if(!this._options.xmlMode && name in openImpliesClose) {
- for(
- var el;
- (el = this._stack[this._stack.length - 1]) in openImpliesClose[name];
- this.onclosetag(el)
- );
- }
-
- if(this._options.xmlMode || !(name in voidElements)){
- this._stack.push(name);
- }
-
- if(this._cbs.onopentagname) this._cbs.onopentagname(name);
- if(this._cbs.onopentag) this._attribs = {};
- };
-
- Parser.prototype.onopentagend = function(){
- this._updatePosition(1);
-
- if(this._attribs){
- if(this._cbs.onopentag) this._cbs.onopentag(this._tagname, this._attribs);
- this._attribs = null;
- }
-
- if(!this._options.xmlMode && this._cbs.onclosetag && this._tagname in voidElements){
- this._cbs.onclosetag(this._tagname);
- }
-
- this._tagname = "";
- };
-
- Parser.prototype.onclosetag = function(name){
- this._updatePosition(1);
-
- if(this._lowerCaseTagNames){
- name = name.toLowerCase();
- }
-
- if(this._stack.length && (!(name in voidElements) || this._options.xmlMode)){
- var pos = this._stack.lastIndexOf(name);
- if(pos !== -1){
- if(this._cbs.onclosetag){
- pos = this._stack.length - pos;
- while(pos--) this._cbs.onclosetag(this._stack.pop());
- }
- else this._stack.length = pos;
- } else if(name === "p" && !this._options.xmlMode){
- this.onopentagname(name);
- this._closeCurrentTag();
- }
- } else if(!this._options.xmlMode && (name === "br" || name === "p")){
- this.onopentagname(name);
- this._closeCurrentTag();
- }
- };
-
- Parser.prototype.onselfclosingtag = function(){
- if(this._options.xmlMode || this._options.recognizeSelfClosing){
- this._closeCurrentTag();
- } else {
- this.onopentagend();
- }
- };
-
- Parser.prototype._closeCurrentTag = function(){
- var name = this._tagname;
-
- this.onopentagend();
-
- //self-closing tags will be on the top of the stack
- //(cheaper check than in onclosetag)
- if(this._stack[this._stack.length - 1] === name){
- if(this._cbs.onclosetag){
- this._cbs.onclosetag(name);
- }
- this._stack.pop();
- }
- };
-
- Parser.prototype.onattribname = function(name){
- if(this._lowerCaseAttributeNames){
- name = name.toLowerCase();
- }
- this._attribname = name;
- };
-
- Parser.prototype.onattribdata = function(value){
- this._attribvalue += value;
- };
-
- Parser.prototype.onattribend = function(){
- if(this._cbs.onattribute) this._cbs.onattribute(this._attribname, this._attribvalue);
- if(
- this._attribs &&
- !Object.prototype.hasOwnProperty.call(this._attribs, this._attribname)
- ){
- this._attribs[this._attribname] = this._attribvalue;
- }
- this._attribname = "";
- this._attribvalue = "";
- };
-
- Parser.prototype._getInstructionName = function(value){
- var idx = value.search(re_nameEnd),
- name = idx < 0 ? value : value.substr(0, idx);
-
- if(this._lowerCaseTagNames){
- name = name.toLowerCase();
- }
-
- return name;
- };
-
- Parser.prototype.ondeclaration = function(value){
- if(this._cbs.onprocessinginstruction){
- var name = this._getInstructionName(value);
- this._cbs.onprocessinginstruction("!" + name, "!" + value);
- }
- };
-
- Parser.prototype.onprocessinginstruction = function(value){
- if(this._cbs.onprocessinginstruction){
- var name = this._getInstructionName(value);
- this._cbs.onprocessinginstruction("?" + name, "?" + value);
- }
- };
-
- Parser.prototype.oncomment = function(value){
- this._updatePosition(4);
-
- if(this._cbs.oncomment) this._cbs.oncomment(value);
- if(this._cbs.oncommentend) this._cbs.oncommentend();
- };
-
- Parser.prototype.oncdata = function(value){
- this._updatePosition(1);
-
- if(this._options.xmlMode || this._options.recognizeCDATA){
- if(this._cbs.oncdatastart) this._cbs.oncdatastart();
- if(this._cbs.ontext) this._cbs.ontext(value);
- if(this._cbs.oncdataend) this._cbs.oncdataend();
- } else {
- this.oncomment("[CDATA[" + value + "]]");
- }
- };
-
- Parser.prototype.onerror = function(err){
- if(this._cbs.onerror) this._cbs.onerror(err);
- };
-
- Parser.prototype.onend = function(){
- if(this._cbs.onclosetag){
- for(
- var i = this._stack.length;
- i > 0;
- this._cbs.onclosetag(this._stack[--i])
- );
- }
- if(this._cbs.onend) this._cbs.onend();
- };
-
-
- //Resets the parser to a blank state, ready to parse a new HTML document
- Parser.prototype.reset = function(){
- if(this._cbs.onreset) this._cbs.onreset();
- this._tokenizer.reset();
-
- this._tagname = "";
- this._attribname = "";
- this._attribs = null;
- this._stack = [];
-
- if(this._cbs.onparserinit) this._cbs.onparserinit(this);
- };
-
- //Parses a complete HTML document and pushes it to the handler
- Parser.prototype.parseComplete = function(data){
- this.reset();
- this.end(data);
- };
-
- Parser.prototype.write = function(chunk){
- this._tokenizer.write(chunk);
- };
-
- Parser.prototype.end = function(chunk){
- this._tokenizer.end(chunk);
- };
-
- Parser.prototype.pause = function(){
- this._tokenizer.pause();
- };
-
- Parser.prototype.resume = function(){
- this._tokenizer.resume();
- };
-
- //alias for backwards compat
- Parser.prototype.parseChunk = Parser.prototype.write;
- Parser.prototype.done = Parser.prototype.end;
-
- module.exports = Parser;
-
-
-/***/ },
-/* 16 */
-/***/ function(module, exports, __webpack_require__) {
-
- module.exports = Tokenizer;
-
- var decodeCodePoint = __webpack_require__(17),
- entityMap = __webpack_require__(19),
- legacyMap = __webpack_require__(20),
- xmlMap = __webpack_require__(21),
-
- i = 0,
-
- TEXT = i++,
- BEFORE_TAG_NAME = i++, //after <
- IN_TAG_NAME = i++,
- IN_SELF_CLOSING_TAG = i++,
- BEFORE_CLOSING_TAG_NAME = i++,
- IN_CLOSING_TAG_NAME = i++,
- AFTER_CLOSING_TAG_NAME = i++,
-
- //attributes
- BEFORE_ATTRIBUTE_NAME = i++,
- IN_ATTRIBUTE_NAME = i++,
- AFTER_ATTRIBUTE_NAME = i++,
- BEFORE_ATTRIBUTE_VALUE = i++,
- IN_ATTRIBUTE_VALUE_DQ = i++, // "
- IN_ATTRIBUTE_VALUE_SQ = i++, // '
- IN_ATTRIBUTE_VALUE_NQ = i++,
-
- //declarations
- BEFORE_DECLARATION = i++, // !
- IN_DECLARATION = i++,
-
- //processing instructions
- IN_PROCESSING_INSTRUCTION = i++, // ?
-
- //comments
- BEFORE_COMMENT = i++,
- IN_COMMENT = i++,
- AFTER_COMMENT_1 = i++,
- AFTER_COMMENT_2 = i++,
-
- //cdata
- BEFORE_CDATA_1 = i++, // [
- BEFORE_CDATA_2 = i++, // C
- BEFORE_CDATA_3 = i++, // D
- BEFORE_CDATA_4 = i++, // A
- BEFORE_CDATA_5 = i++, // T
- BEFORE_CDATA_6 = i++, // A
- IN_CDATA = i++, // [
- AFTER_CDATA_1 = i++, // ]
- AFTER_CDATA_2 = i++, // ]
-
- //special tags
- BEFORE_SPECIAL = i++, //S
- BEFORE_SPECIAL_END = i++, //S
-
- BEFORE_SCRIPT_1 = i++, //C
- BEFORE_SCRIPT_2 = i++, //R
- BEFORE_SCRIPT_3 = i++, //I
- BEFORE_SCRIPT_4 = i++, //P
- BEFORE_SCRIPT_5 = i++, //T
- AFTER_SCRIPT_1 = i++, //C
- AFTER_SCRIPT_2 = i++, //R
- AFTER_SCRIPT_3 = i++, //I
- AFTER_SCRIPT_4 = i++, //P
- AFTER_SCRIPT_5 = i++, //T
-
- BEFORE_STYLE_1 = i++, //T
- BEFORE_STYLE_2 = i++, //Y
- BEFORE_STYLE_3 = i++, //L
- BEFORE_STYLE_4 = i++, //E
- AFTER_STYLE_1 = i++, //T
- AFTER_STYLE_2 = i++, //Y
- AFTER_STYLE_3 = i++, //L
- AFTER_STYLE_4 = i++, //E
-
- BEFORE_ENTITY = i++, //&
- BEFORE_NUMERIC_ENTITY = i++, //#
- IN_NAMED_ENTITY = i++,
- IN_NUMERIC_ENTITY = i++,
- IN_HEX_ENTITY = i++, //X
-
- j = 0,
-
- SPECIAL_NONE = j++,
- SPECIAL_SCRIPT = j++,
- SPECIAL_STYLE = j++;
-
- function whitespace(c){
- return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r";
- }
-
- function characterState(char, SUCCESS){
- return function(c){
- if(c === char) this._state = SUCCESS;
- };
- }
-
- function ifElseState(upper, SUCCESS, FAILURE){
- var lower = upper.toLowerCase();
-
- if(upper === lower){
- return function(c){
- if(c === lower){
- this._state = SUCCESS;
- } else {
- this._state = FAILURE;
- this._index--;
- }
- };
- } else {
- return function(c){
- if(c === lower || c === upper){
- this._state = SUCCESS;
- } else {
- this._state = FAILURE;
- this._index--;
- }
- };
- }
- }
-
- function consumeSpecialNameChar(upper, NEXT_STATE){
- var lower = upper.toLowerCase();
-
- return function(c){
- if(c === lower || c === upper){
- this._state = NEXT_STATE;
- } else {
- this._state = IN_TAG_NAME;
- this._index--; //consume the token again
- }
- };
- }
-
- function Tokenizer(options, cbs){
- this._state = TEXT;
- this._buffer = "";
- this._sectionStart = 0;
- this._index = 0;
- this._bufferOffset = 0; //chars removed from _buffer
- this._baseState = TEXT;
- this._special = SPECIAL_NONE;
- this._cbs = cbs;
- this._running = true;
- this._ended = false;
- this._xmlMode = !!(options && options.xmlMode);
- this._decodeEntities = !!(options && options.decodeEntities);
- }
-
- Tokenizer.prototype._stateText = function(c){
- if(c === "<"){
- if(this._index > this._sectionStart){
- this._cbs.ontext(this._getSection());
- }
- this._state = BEFORE_TAG_NAME;
- this._sectionStart = this._index;
- } else if(this._decodeEntities && this._special === SPECIAL_NONE && c === "&"){
- if(this._index > this._sectionStart){
- this._cbs.ontext(this._getSection());
- }
- this._baseState = TEXT;
- this._state = BEFORE_ENTITY;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateBeforeTagName = function(c){
- if(c === "/"){
- this._state = BEFORE_CLOSING_TAG_NAME;
- } else if(c === ">" || this._special !== SPECIAL_NONE || whitespace(c)) {
- this._state = TEXT;
- } else if(c === "!"){
- this._state = BEFORE_DECLARATION;
- this._sectionStart = this._index + 1;
- } else if(c === "?"){
- this._state = IN_PROCESSING_INSTRUCTION;
- this._sectionStart = this._index + 1;
- } else if(c === "<"){
- this._cbs.ontext(this._getSection());
- this._sectionStart = this._index;
- } else {
- this._state = (!this._xmlMode && (c === "s" || c === "S")) ?
- BEFORE_SPECIAL : IN_TAG_NAME;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateInTagName = function(c){
- if(c === "/" || c === ">" || whitespace(c)){
- this._emitToken("onopentagname");
- this._state = BEFORE_ATTRIBUTE_NAME;
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateBeforeCloseingTagName = function(c){
- if(whitespace(c));
- else if(c === ">"){
- this._state = TEXT;
- } else if(this._special !== SPECIAL_NONE){
- if(c === "s" || c === "S"){
- this._state = BEFORE_SPECIAL_END;
- } else {
- this._state = TEXT;
- this._index--;
- }
- } else {
- this._state = IN_CLOSING_TAG_NAME;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateInCloseingTagName = function(c){
- if(c === ">" || whitespace(c)){
- this._emitToken("onclosetag");
- this._state = AFTER_CLOSING_TAG_NAME;
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateAfterCloseingTagName = function(c){
- //skip everything until ">"
- if(c === ">"){
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- }
- };
-
- Tokenizer.prototype._stateBeforeAttributeName = function(c){
- if(c === ">"){
- this._cbs.onopentagend();
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- } else if(c === "/"){
- this._state = IN_SELF_CLOSING_TAG;
- } else if(!whitespace(c)){
- this._state = IN_ATTRIBUTE_NAME;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateInSelfClosingTag = function(c){
- if(c === ">"){
- this._cbs.onselfclosingtag();
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- } else if(!whitespace(c)){
- this._state = BEFORE_ATTRIBUTE_NAME;
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateInAttributeName = function(c){
- if(c === "=" || c === "/" || c === ">" || whitespace(c)){
- this._cbs.onattribname(this._getSection());
- this._sectionStart = -1;
- this._state = AFTER_ATTRIBUTE_NAME;
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateAfterAttributeName = function(c){
- if(c === "="){
- this._state = BEFORE_ATTRIBUTE_VALUE;
- } else if(c === "/" || c === ">"){
- this._cbs.onattribend();
- this._state = BEFORE_ATTRIBUTE_NAME;
- this._index--;
- } else if(!whitespace(c)){
- this._cbs.onattribend();
- this._state = IN_ATTRIBUTE_NAME;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateBeforeAttributeValue = function(c){
- if(c === "\""){
- this._state = IN_ATTRIBUTE_VALUE_DQ;
- this._sectionStart = this._index + 1;
- } else if(c === "'"){
- this._state = IN_ATTRIBUTE_VALUE_SQ;
- this._sectionStart = this._index + 1;
- } else if(!whitespace(c)){
- this._state = IN_ATTRIBUTE_VALUE_NQ;
- this._sectionStart = this._index;
- this._index--; //reconsume token
- }
- };
-
- Tokenizer.prototype._stateInAttributeValueDoubleQuotes = function(c){
- if(c === "\""){
- this._emitToken("onattribdata");
- this._cbs.onattribend();
- this._state = BEFORE_ATTRIBUTE_NAME;
- } else if(this._decodeEntities && c === "&"){
- this._emitToken("onattribdata");
- this._baseState = this._state;
- this._state = BEFORE_ENTITY;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateInAttributeValueSingleQuotes = function(c){
- if(c === "'"){
- this._emitToken("onattribdata");
- this._cbs.onattribend();
- this._state = BEFORE_ATTRIBUTE_NAME;
- } else if(this._decodeEntities && c === "&"){
- this._emitToken("onattribdata");
- this._baseState = this._state;
- this._state = BEFORE_ENTITY;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateInAttributeValueNoQuotes = function(c){
- if(whitespace(c) || c === ">"){
- this._emitToken("onattribdata");
- this._cbs.onattribend();
- this._state = BEFORE_ATTRIBUTE_NAME;
- this._index--;
- } else if(this._decodeEntities && c === "&"){
- this._emitToken("onattribdata");
- this._baseState = this._state;
- this._state = BEFORE_ENTITY;
- this._sectionStart = this._index;
- }
- };
-
- Tokenizer.prototype._stateBeforeDeclaration = function(c){
- this._state = c === "[" ? BEFORE_CDATA_1 :
- c === "-" ? BEFORE_COMMENT :
- IN_DECLARATION;
- };
-
- Tokenizer.prototype._stateInDeclaration = function(c){
- if(c === ">"){
- this._cbs.ondeclaration(this._getSection());
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- }
- };
-
- Tokenizer.prototype._stateInProcessingInstruction = function(c){
- if(c === ">"){
- this._cbs.onprocessinginstruction(this._getSection());
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- }
- };
-
- Tokenizer.prototype._stateBeforeComment = function(c){
- if(c === "-"){
- this._state = IN_COMMENT;
- this._sectionStart = this._index + 1;
- } else {
- this._state = IN_DECLARATION;
- }
- };
-
- Tokenizer.prototype._stateInComment = function(c){
- if(c === "-") this._state = AFTER_COMMENT_1;
- };
-
- Tokenizer.prototype._stateAfterComment1 = function(c){
- if(c === "-"){
- this._state = AFTER_COMMENT_2;
- } else {
- this._state = IN_COMMENT;
- }
- };
-
- Tokenizer.prototype._stateAfterComment2 = function(c){
- if(c === ">"){
- //remove 2 trailing chars
- this._cbs.oncomment(this._buffer.substring(this._sectionStart, this._index - 2));
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- } else if(c !== "-"){
- this._state = IN_COMMENT;
- }
- // else: stay in AFTER_COMMENT_2 (`--->`)
- };
-
- Tokenizer.prototype._stateBeforeCdata1 = ifElseState("C", BEFORE_CDATA_2, IN_DECLARATION);
- Tokenizer.prototype._stateBeforeCdata2 = ifElseState("D", BEFORE_CDATA_3, IN_DECLARATION);
- Tokenizer.prototype._stateBeforeCdata3 = ifElseState("A", BEFORE_CDATA_4, IN_DECLARATION);
- Tokenizer.prototype._stateBeforeCdata4 = ifElseState("T", BEFORE_CDATA_5, IN_DECLARATION);
- Tokenizer.prototype._stateBeforeCdata5 = ifElseState("A", BEFORE_CDATA_6, IN_DECLARATION);
-
- Tokenizer.prototype._stateBeforeCdata6 = function(c){
- if(c === "["){
- this._state = IN_CDATA;
- this._sectionStart = this._index + 1;
- } else {
- this._state = IN_DECLARATION;
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateInCdata = function(c){
- if(c === "]") this._state = AFTER_CDATA_1;
- };
-
- Tokenizer.prototype._stateAfterCdata1 = characterState("]", AFTER_CDATA_2);
-
- Tokenizer.prototype._stateAfterCdata2 = function(c){
- if(c === ">"){
- //remove 2 trailing chars
- this._cbs.oncdata(this._buffer.substring(this._sectionStart, this._index - 2));
- this._state = TEXT;
- this._sectionStart = this._index + 1;
- } else if(c !== "]") {
- this._state = IN_CDATA;
- }
- //else: stay in AFTER_CDATA_2 (`]]]>`)
- };
-
- Tokenizer.prototype._stateBeforeSpecial = function(c){
- if(c === "c" || c === "C"){
- this._state = BEFORE_SCRIPT_1;
- } else if(c === "t" || c === "T"){
- this._state = BEFORE_STYLE_1;
- } else {
- this._state = IN_TAG_NAME;
- this._index--; //consume the token again
- }
- };
-
- Tokenizer.prototype._stateBeforeSpecialEnd = function(c){
- if(this._special === SPECIAL_SCRIPT && (c === "c" || c === "C")){
- this._state = AFTER_SCRIPT_1;
- } else if(this._special === SPECIAL_STYLE && (c === "t" || c === "T")){
- this._state = AFTER_STYLE_1;
- }
- else this._state = TEXT;
- };
-
- Tokenizer.prototype._stateBeforeScript1 = consumeSpecialNameChar("R", BEFORE_SCRIPT_2);
- Tokenizer.prototype._stateBeforeScript2 = consumeSpecialNameChar("I", BEFORE_SCRIPT_3);
- Tokenizer.prototype._stateBeforeScript3 = consumeSpecialNameChar("P", BEFORE_SCRIPT_4);
- Tokenizer.prototype._stateBeforeScript4 = consumeSpecialNameChar("T", BEFORE_SCRIPT_5);
-
- Tokenizer.prototype._stateBeforeScript5 = function(c){
- if(c === "/" || c === ">" || whitespace(c)){
- this._special = SPECIAL_SCRIPT;
- }
- this._state = IN_TAG_NAME;
- this._index--; //consume the token again
- };
-
- Tokenizer.prototype._stateAfterScript1 = ifElseState("R", AFTER_SCRIPT_2, TEXT);
- Tokenizer.prototype._stateAfterScript2 = ifElseState("I", AFTER_SCRIPT_3, TEXT);
- Tokenizer.prototype._stateAfterScript3 = ifElseState("P", AFTER_SCRIPT_4, TEXT);
- Tokenizer.prototype._stateAfterScript4 = ifElseState("T", AFTER_SCRIPT_5, TEXT);
-
- Tokenizer.prototype._stateAfterScript5 = function(c){
- if(c === ">" || whitespace(c)){
- this._special = SPECIAL_NONE;
- this._state = IN_CLOSING_TAG_NAME;
- this._sectionStart = this._index - 6;
- this._index--; //reconsume the token
- }
- else this._state = TEXT;
- };
-
- Tokenizer.prototype._stateBeforeStyle1 = consumeSpecialNameChar("Y", BEFORE_STYLE_2);
- Tokenizer.prototype._stateBeforeStyle2 = consumeSpecialNameChar("L", BEFORE_STYLE_3);
- Tokenizer.prototype._stateBeforeStyle3 = consumeSpecialNameChar("E", BEFORE_STYLE_4);
-
- Tokenizer.prototype._stateBeforeStyle4 = function(c){
- if(c === "/" || c === ">" || whitespace(c)){
- this._special = SPECIAL_STYLE;
- }
- this._state = IN_TAG_NAME;
- this._index--; //consume the token again
- };
-
- Tokenizer.prototype._stateAfterStyle1 = ifElseState("Y", AFTER_STYLE_2, TEXT);
- Tokenizer.prototype._stateAfterStyle2 = ifElseState("L", AFTER_STYLE_3, TEXT);
- Tokenizer.prototype._stateAfterStyle3 = ifElseState("E", AFTER_STYLE_4, TEXT);
-
- Tokenizer.prototype._stateAfterStyle4 = function(c){
- if(c === ">" || whitespace(c)){
- this._special = SPECIAL_NONE;
- this._state = IN_CLOSING_TAG_NAME;
- this._sectionStart = this._index - 5;
- this._index--; //reconsume the token
- }
- else this._state = TEXT;
- };
-
- Tokenizer.prototype._stateBeforeEntity = ifElseState("#", BEFORE_NUMERIC_ENTITY, IN_NAMED_ENTITY);
- Tokenizer.prototype._stateBeforeNumericEntity = ifElseState("X", IN_HEX_ENTITY, IN_NUMERIC_ENTITY);
-
- //for entities terminated with a semicolon
- Tokenizer.prototype._parseNamedEntityStrict = function(){
- //offset = 1
- if(this._sectionStart + 1 < this._index){
- var entity = this._buffer.substring(this._sectionStart + 1, this._index),
- map = this._xmlMode ? xmlMap : entityMap;
-
- if(map.hasOwnProperty(entity)){
- this._emitPartial(map[entity]);
- this._sectionStart = this._index + 1;
- }
- }
- };
-
-
- //parses legacy entities (without trailing semicolon)
- Tokenizer.prototype._parseLegacyEntity = function(){
- var start = this._sectionStart + 1,
- limit = this._index - start;
-
- if(limit > 6) limit = 6; //the max length of legacy entities is 6
-
- while(limit >= 2){ //the min length of legacy entities is 2
- var entity = this._buffer.substr(start, limit);
-
- if(legacyMap.hasOwnProperty(entity)){
- this._emitPartial(legacyMap[entity]);
- this._sectionStart += limit + 1;
- return;
- } else {
- limit--;
- }
- }
- };
-
- Tokenizer.prototype._stateInNamedEntity = function(c){
- if(c === ";"){
- this._parseNamedEntityStrict();
- if(this._sectionStart + 1 < this._index && !this._xmlMode){
- this._parseLegacyEntity();
- }
- this._state = this._baseState;
- } else if((c < "a" || c > "z") && (c < "A" || c > "Z") && (c < "0" || c > "9")){
- if(this._xmlMode);
- else if(this._sectionStart + 1 === this._index);
- else if(this._baseState !== TEXT){
- if(c !== "="){
- this._parseNamedEntityStrict();
- }
- } else {
- this._parseLegacyEntity();
- }
-
- this._state = this._baseState;
- this._index--;
- }
- };
-
- Tokenizer.prototype._decodeNumericEntity = function(offset, base){
- var sectionStart = this._sectionStart + offset;
-
- if(sectionStart !== this._index){
- //parse entity
- var entity = this._buffer.substring(sectionStart, this._index);
- var parsed = parseInt(entity, base);
-
- this._emitPartial(decodeCodePoint(parsed));
- this._sectionStart = this._index;
- } else {
- this._sectionStart--;
- }
-
- this._state = this._baseState;
- };
-
- Tokenizer.prototype._stateInNumericEntity = function(c){
- if(c === ";"){
- this._decodeNumericEntity(2, 10);
- this._sectionStart++;
- } else if(c < "0" || c > "9"){
- if(!this._xmlMode){
- this._decodeNumericEntity(2, 10);
- } else {
- this._state = this._baseState;
- }
- this._index--;
- }
- };
-
- Tokenizer.prototype._stateInHexEntity = function(c){
- if(c === ";"){
- this._decodeNumericEntity(3, 16);
- this._sectionStart++;
- } else if((c < "a" || c > "f") && (c < "A" || c > "F") && (c < "0" || c > "9")){
- if(!this._xmlMode){
- this._decodeNumericEntity(3, 16);
- } else {
- this._state = this._baseState;
- }
- this._index--;
- }
- };
-
- Tokenizer.prototype._cleanup = function (){
- if(this._sectionStart < 0){
- this._buffer = "";
- this._index = 0;
- this._bufferOffset += this._index;
- } else if(this._running){
- if(this._state === TEXT){
- if(this._sectionStart !== this._index){
- this._cbs.ontext(this._buffer.substr(this._sectionStart));
- }
- this._buffer = "";
- this._index = 0;
- this._bufferOffset += this._index;
- } else if(this._sectionStart === this._index){
- //the section just started
- this._buffer = "";
- this._index = 0;
- this._bufferOffset += this._index;
- } else {
- //remove everything unnecessary
- this._buffer = this._buffer.substr(this._sectionStart);
- this._index -= this._sectionStart;
- this._bufferOffset += this._sectionStart;
- }
-
- this._sectionStart = 0;
- }
- };
-
- //TODO make events conditional
- Tokenizer.prototype.write = function(chunk){
- if(this._ended) this._cbs.onerror(Error(".write() after done!"));
-
- this._buffer += chunk;
- this._parse();
- };
-
- Tokenizer.prototype._parse = function(){
- while(this._index < this._buffer.length && this._running){
- var c = this._buffer.charAt(this._index);
- if(this._state === TEXT) {
- this._stateText(c);
- } else if(this._state === BEFORE_TAG_NAME){
- this._stateBeforeTagName(c);
- } else if(this._state === IN_TAG_NAME) {
- this._stateInTagName(c);
- } else if(this._state === BEFORE_CLOSING_TAG_NAME){
- this._stateBeforeCloseingTagName(c);
- } else if(this._state === IN_CLOSING_TAG_NAME){
- this._stateInCloseingTagName(c);
- } else if(this._state === AFTER_CLOSING_TAG_NAME){
- this._stateAfterCloseingTagName(c);
- } else if(this._state === IN_SELF_CLOSING_TAG){
- this._stateInSelfClosingTag(c);
- }
-
- /*
- * attributes
- */
- else if(this._state === BEFORE_ATTRIBUTE_NAME){
- this._stateBeforeAttributeName(c);
- } else if(this._state === IN_ATTRIBUTE_NAME){
- this._stateInAttributeName(c);
- } else if(this._state === AFTER_ATTRIBUTE_NAME){
- this._stateAfterAttributeName(c);
- } else if(this._state === BEFORE_ATTRIBUTE_VALUE){
- this._stateBeforeAttributeValue(c);
- } else if(this._state === IN_ATTRIBUTE_VALUE_DQ){
- this._stateInAttributeValueDoubleQuotes(c);
- } else if(this._state === IN_ATTRIBUTE_VALUE_SQ){
- this._stateInAttributeValueSingleQuotes(c);
- } else if(this._state === IN_ATTRIBUTE_VALUE_NQ){
- this._stateInAttributeValueNoQuotes(c);
- }
-
- /*
- * declarations
- */
- else if(this._state === BEFORE_DECLARATION){
- this._stateBeforeDeclaration(c);
- } else if(this._state === IN_DECLARATION){
- this._stateInDeclaration(c);
- }
-
- /*
- * processing instructions
- */
- else if(this._state === IN_PROCESSING_INSTRUCTION){
- this._stateInProcessingInstruction(c);
- }
-
- /*
- * comments
- */
- else if(this._state === BEFORE_COMMENT){
- this._stateBeforeComment(c);
- } else if(this._state === IN_COMMENT){
- this._stateInComment(c);
- } else if(this._state === AFTER_COMMENT_1){
- this._stateAfterComment1(c);
- } else if(this._state === AFTER_COMMENT_2){
- this._stateAfterComment2(c);
- }
-
- /*
- * cdata
- */
- else if(this._state === BEFORE_CDATA_1){
- this._stateBeforeCdata1(c);
- } else if(this._state === BEFORE_CDATA_2){
- this._stateBeforeCdata2(c);
- } else if(this._state === BEFORE_CDATA_3){
- this._stateBeforeCdata3(c);
- } else if(this._state === BEFORE_CDATA_4){
- this._stateBeforeCdata4(c);
- } else if(this._state === BEFORE_CDATA_5){
- this._stateBeforeCdata5(c);
- } else if(this._state === BEFORE_CDATA_6){
- this._stateBeforeCdata6(c);
- } else if(this._state === IN_CDATA){
- this._stateInCdata(c);
- } else if(this._state === AFTER_CDATA_1){
- this._stateAfterCdata1(c);
- } else if(this._state === AFTER_CDATA_2){
- this._stateAfterCdata2(c);
- }
-
- /*
- * special tags
- */
- else if(this._state === BEFORE_SPECIAL){
- this._stateBeforeSpecial(c);
- } else if(this._state === BEFORE_SPECIAL_END){
- this._stateBeforeSpecialEnd(c);
- }
-
- /*
- * script
- */
- else if(this._state === BEFORE_SCRIPT_1){
- this._stateBeforeScript1(c);
- } else if(this._state === BEFORE_SCRIPT_2){
- this._stateBeforeScript2(c);
- } else if(this._state === BEFORE_SCRIPT_3){
- this._stateBeforeScript3(c);
- } else if(this._state === BEFORE_SCRIPT_4){
- this._stateBeforeScript4(c);
- } else if(this._state === BEFORE_SCRIPT_5){
- this._stateBeforeScript5(c);
- }
-
- else if(this._state === AFTER_SCRIPT_1){
- this._stateAfterScript1(c);
- } else if(this._state === AFTER_SCRIPT_2){
- this._stateAfterScript2(c);
- } else if(this._state === AFTER_SCRIPT_3){
- this._stateAfterScript3(c);
- } else if(this._state === AFTER_SCRIPT_4){
- this._stateAfterScript4(c);
- } else if(this._state === AFTER_SCRIPT_5){
- this._stateAfterScript5(c);
- }
-
- /*
- * style
- */
- else if(this._state === BEFORE_STYLE_1){
- this._stateBeforeStyle1(c);
- } else if(this._state === BEFORE_STYLE_2){
- this._stateBeforeStyle2(c);
- } else if(this._state === BEFORE_STYLE_3){
- this._stateBeforeStyle3(c);
- } else if(this._state === BEFORE_STYLE_4){
- this._stateBeforeStyle4(c);
- }
-
- else if(this._state === AFTER_STYLE_1){
- this._stateAfterStyle1(c);
- } else if(this._state === AFTER_STYLE_2){
- this._stateAfterStyle2(c);
- } else if(this._state === AFTER_STYLE_3){
- this._stateAfterStyle3(c);
- } else if(this._state === AFTER_STYLE_4){
- this._stateAfterStyle4(c);
- }
-
- /*
- * entities
- */
- else if(this._state === BEFORE_ENTITY){
- this._stateBeforeEntity(c);
- } else if(this._state === BEFORE_NUMERIC_ENTITY){
- this._stateBeforeNumericEntity(c);
- } else if(this._state === IN_NAMED_ENTITY){
- this._stateInNamedEntity(c);
- } else if(this._state === IN_NUMERIC_ENTITY){
- this._stateInNumericEntity(c);
- } else if(this._state === IN_HEX_ENTITY){
- this._stateInHexEntity(c);
- }
-
- else {
- this._cbs.onerror(Error("unknown _state"), this._state);
- }
-
- this._index++;
- }
-
- this._cleanup();
- };
-
- Tokenizer.prototype.pause = function(){
- this._running = false;
- };
- Tokenizer.prototype.resume = function(){
- this._running = true;
-
- if(this._index < this._buffer.length){
- this._parse();
- }
- if(this._ended){
- this._finish();
- }
- };
-
- Tokenizer.prototype.end = function(chunk){
- if(this._ended) this._cbs.onerror(Error(".end() after done!"));
- if(chunk) this.write(chunk);
-
- this._ended = true;
-
- if(this._running) this._finish();
- };
-
- Tokenizer.prototype._finish = function(){
- //if there is remaining data, emit it in a reasonable way
- if(this._sectionStart < this._index){
- this._handleTrailingData();
- }
-
- this._cbs.onend();
- };
-
- Tokenizer.prototype._handleTrailingData = function(){
- var data = this._buffer.substr(this._sectionStart);
-
- if(this._state === IN_CDATA || this._state === AFTER_CDATA_1 || this._state === AFTER_CDATA_2){
- this._cbs.oncdata(data);
- } else if(this._state === IN_COMMENT || this._state === AFTER_COMMENT_1 || this._state === AFTER_COMMENT_2){
- this._cbs.oncomment(data);
- } else if(this._state === IN_NAMED_ENTITY && !this._xmlMode){
- this._parseLegacyEntity();
- if(this._sectionStart < this._index){
- this._state = this._baseState;
- this._handleTrailingData();
- }
- } else if(this._state === IN_NUMERIC_ENTITY && !this._xmlMode){
- this._decodeNumericEntity(2, 10);
- if(this._sectionStart < this._index){
- this._state = this._baseState;
- this._handleTrailingData();
- }
- } else if(this._state === IN_HEX_ENTITY && !this._xmlMode){
- this._decodeNumericEntity(3, 16);
- if(this._sectionStart < this._index){
- this._state = this._baseState;
- this._handleTrailingData();
- }
- } else if(
- this._state !== IN_TAG_NAME &&
- this._state !== BEFORE_ATTRIBUTE_NAME &&
- this._state !== BEFORE_ATTRIBUTE_VALUE &&
- this._state !== AFTER_ATTRIBUTE_NAME &&
- this._state !== IN_ATTRIBUTE_NAME &&
- this._state !== IN_ATTRIBUTE_VALUE_SQ &&
- this._state !== IN_ATTRIBUTE_VALUE_DQ &&
- this._state !== IN_ATTRIBUTE_VALUE_NQ &&
- this._state !== IN_CLOSING_TAG_NAME
- ){
- this._cbs.ontext(data);
- }
- //else, ignore remaining data
- //TODO add a way to remove current tag
- };
-
- Tokenizer.prototype.reset = function(){
- Tokenizer.call(this, {xmlMode: this._xmlMode, decodeEntities: this._decodeEntities}, this._cbs);
- };
-
- Tokenizer.prototype.getAbsoluteIndex = function(){
- return this._bufferOffset + this._index;
- };
-
- Tokenizer.prototype._getSection = function(){
- return this._buffer.substring(this._sectionStart, this._index);
- };
-
- Tokenizer.prototype._emitToken = function(name){
- this._cbs[name](this._getSection());
- this._sectionStart = -1;
- };
-
- Tokenizer.prototype._emitPartial = function(value){
- if(this._baseState !== TEXT){
- this._cbs.onattribdata(value); //TODO implement the new event
- } else {
- this._cbs.ontext(value);
- }
- };
-
-
-/***/ },
-/* 17 */
-/***/ function(module, exports, __webpack_require__) {
-
- var decodeMap = __webpack_require__(18);
-
- module.exports = decodeCodePoint;
-
- // modified version of https://github.com/mathiasbynens/he/blob/master/src/he.js#L94-L119
- function decodeCodePoint(codePoint){
-
- if((codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint > 0x10FFFF){
- return "\uFFFD";
- }
-
- if(codePoint in decodeMap){
- codePoint = decodeMap[codePoint];
- }
-
- var output = "";
-
- if(codePoint > 0xFFFF){
- codePoint -= 0x10000;
- output += String.fromCharCode(codePoint >>> 10 & 0x3FF | 0xD800);
- codePoint = 0xDC00 | codePoint & 0x3FF;
- }
-
- output += String.fromCharCode(codePoint);
- return output;
- }
-
-
-/***/ },
-/* 18 */
-/***/ function(module, exports) {
-
- module.exports = {
- "0": 65533,
- "128": 8364,
- "130": 8218,
- "131": 402,
- "132": 8222,
- "133": 8230,
- "134": 8224,
- "135": 8225,
- "136": 710,
- "137": 8240,
- "138": 352,
- "139": 8249,
- "140": 338,
- "142": 381,
- "145": 8216,
- "146": 8217,
- "147": 8220,
- "148": 8221,
- "149": 8226,
- "150": 8211,
- "151": 8212,
- "152": 732,
- "153": 8482,
- "154": 353,
- "155": 8250,
- "156": 339,
- "158": 382,
- "159": 376
- };
-
-/***/ },
-/* 19 */
-/***/ function(module, exports) {
-
- module.exports = {
- "Aacute": "Á",
- "aacute": "á",
- "Abreve": "Ă",
- "abreve": "ă",
- "ac": "∾",
- "acd": "∿",
- "acE": "∾̳",
- "Acirc": "Â",
- "acirc": "â",
- "acute": "´",
- "Acy": "А",
- "acy": "а",
- "AElig": "Æ",
- "aelig": "æ",
- "af": "",
- "Afr": "𝔄",
- "afr": "𝔞",
- "Agrave": "À",
- "agrave": "à",
- "alefsym": "ℵ",
- "aleph": "ℵ",
- "Alpha": "Α",
- "alpha": "α",
- "Amacr": "Ā",
- "amacr": "ā",
- "amalg": "⨿",
- "amp": "&",
- "AMP": "&",
- "andand": "⩕",
- "And": "⩓",
- "and": "∧",
- "andd": "⩜",
- "andslope": "⩘",
- "andv": "⩚",
- "ang": "∠",
- "ange": "⦤",
- "angle": "∠",
- "angmsdaa": "⦨",
- "angmsdab": "⦩",
- "angmsdac": "⦪",
- "angmsdad": "⦫",
- "angmsdae": "⦬",
- "angmsdaf": "⦭",
- "angmsdag": "⦮",
- "angmsdah": "⦯",
- "angmsd": "∡",
- "angrt": "∟",
- "angrtvb": "⊾",
- "angrtvbd": "⦝",
- "angsph": "∢",
- "angst": "Å",
- "angzarr": "⍼",
- "Aogon": "Ą",
- "aogon": "ą",
- "Aopf": "𝔸",
- "aopf": "𝕒",
- "apacir": "⩯",
- "ap": "≈",
- "apE": "⩰",
- "ape": "≊",
- "apid": "≋",
- "apos": "'",
- "ApplyFunction": "",
- "approx": "≈",
- "approxeq": "≊",
- "Aring": "Å",
- "aring": "å",
- "Ascr": "𝒜",
- "ascr": "𝒶",
- "Assign": "≔",
- "ast": "*",
- "asymp": "≈",
- "asympeq": "≍",
- "Atilde": "Ã",
- "atilde": "ã",
- "Auml": "Ä",
- "auml": "ä",
- "awconint": "∳",
- "awint": "⨑",
- "backcong": "≌",
- "backepsilon": "϶",
- "backprime": "‵",
- "backsim": "∽",
- "backsimeq": "⋍",
- "Backslash": "∖",
- "Barv": "⫧",
- "barvee": "⊽",
- "barwed": "⌅",
- "Barwed": "⌆",
- "barwedge": "⌅",
- "bbrk": "⎵",
- "bbrktbrk": "⎶",
- "bcong": "≌",
- "Bcy": "Б",
- "bcy": "б",
- "bdquo": "„",
- "becaus": "∵",
- "because": "∵",
- "Because": "∵",
- "bemptyv": "⦰",
- "bepsi": "϶",
- "bernou": "ℬ",
- "Bernoullis": "ℬ",
- "Beta": "Β",
- "beta": "β",
- "beth": "ℶ",
- "between": "≬",
- "Bfr": "𝔅",
- "bfr": "𝔟",
- "bigcap": "⋂",
- "bigcirc": "◯",
- "bigcup": "⋃",
- "bigodot": "⨀",
- "bigoplus": "⨁",
- "bigotimes": "⨂",
- "bigsqcup": "⨆",
- "bigstar": "★",
- "bigtriangledown": "▽",
- "bigtriangleup": "△",
- "biguplus": "⨄",
- "bigvee": "⋁",
- "bigwedge": "⋀",
- "bkarow": "⤍",
- "blacklozenge": "⧫",
- "blacksquare": "▪",
- "blacktriangle": "▴",
- "blacktriangledown": "▾",
- "blacktriangleleft": "◂",
- "blacktriangleright": "▸",
- "blank": "␣",
- "blk12": "▒",
- "blk14": "░",
- "blk34": "▓",
- "block": "█",
- "bne": "=⃥",
- "bnequiv": "≡⃥",
- "bNot": "⫭",
- "bnot": "⌐",
- "Bopf": "𝔹",
- "bopf": "𝕓",
- "bot": "⊥",
- "bottom": "⊥",
- "bowtie": "⋈",
- "boxbox": "⧉",
- "boxdl": "┐",
- "boxdL": "╕",
- "boxDl": "╖",
- "boxDL": "╗",
- "boxdr": "┌",
- "boxdR": "╒",
- "boxDr": "╓",
- "boxDR": "╔",
- "boxh": "─",
- "boxH": "═",
- "boxhd": "┬",
- "boxHd": "╤",
- "boxhD": "╥",
- "boxHD": "╦",
- "boxhu": "┴",
- "boxHu": "╧",
- "boxhU": "╨",
- "boxHU": "╩",
- "boxminus": "⊟",
- "boxplus": "⊞",
- "boxtimes": "⊠",
- "boxul": "┘",
- "boxuL": "╛",
- "boxUl": "╜",
- "boxUL": "╝",
- "boxur": "└",
- "boxuR": "╘",
- "boxUr": "╙",
- "boxUR": "╚",
- "boxv": "│",
- "boxV": "║",
- "boxvh": "┼",
- "boxvH": "╪",
- "boxVh": "╫",
- "boxVH": "╬",
- "boxvl": "┤",
- "boxvL": "╡",
- "boxVl": "╢",
- "boxVL": "╣",
- "boxvr": "├",
- "boxvR": "╞",
- "boxVr": "╟",
- "boxVR": "╠",
- "bprime": "‵",
- "breve": "˘",
- "Breve": "˘",
- "brvbar": "¦",
- "bscr": "𝒷",
- "Bscr": "ℬ",
- "bsemi": "⁏",
- "bsim": "∽",
- "bsime": "⋍",
- "bsolb": "⧅",
- "bsol": "\\",
- "bsolhsub": "⟈",
- "bull": "•",
- "bullet": "•",
- "bump": "≎",
- "bumpE": "⪮",
- "bumpe": "≏",
- "Bumpeq": "≎",
- "bumpeq": "≏",
- "Cacute": "Ć",
- "cacute": "ć",
- "capand": "⩄",
- "capbrcup": "⩉",
- "capcap": "⩋",
- "cap": "∩",
- "Cap": "⋒",
- "capcup": "⩇",
- "capdot": "⩀",
- "CapitalDifferentialD": "ⅅ",
- "caps": "∩︀",
- "caret": "⁁",
- "caron": "ˇ",
- "Cayleys": "ℭ",
- "ccaps": "⩍",
- "Ccaron": "Č",
- "ccaron": "č",
- "Ccedil": "Ç",
- "ccedil": "ç",
- "Ccirc": "Ĉ",
- "ccirc": "ĉ",
- "Cconint": "∰",
- "ccups": "⩌",
- "ccupssm": "⩐",
- "Cdot": "Ċ",
- "cdot": "ċ",
- "cedil": "¸",
- "Cedilla": "¸",
- "cemptyv": "⦲",
- "cent": "¢",
- "centerdot": "·",
- "CenterDot": "·",
- "cfr": "𝔠",
- "Cfr": "ℭ",
- "CHcy": "Ч",
- "chcy": "ч",
- "check": "✓",
- "checkmark": "✓",
- "Chi": "Χ",
- "chi": "χ",
- "circ": "ˆ",
- "circeq": "≗",
- "circlearrowleft": "↺",
- "circlearrowright": "↻",
- "circledast": "⊛",
- "circledcirc": "⊚",
- "circleddash": "⊝",
- "CircleDot": "⊙",
- "circledR": "®",
- "circledS": "Ⓢ",
- "CircleMinus": "⊖",
- "CirclePlus": "⊕",
- "CircleTimes": "⊗",
- "cir": "○",
- "cirE": "⧃",
- "cire": "≗",
- "cirfnint": "⨐",
- "cirmid": "⫯",
- "cirscir": "⧂",
- "ClockwiseContourIntegral": "∲",
- "CloseCurlyDoubleQuote": "”",
- "CloseCurlyQuote": "’",
- "clubs": "♣",
- "clubsuit": "♣",
- "colon": ":",
- "Colon": "∷",
- "Colone": "⩴",
- "colone": "≔",
- "coloneq": "≔",
- "comma": ",",
- "commat": "@",
- "comp": "∁",
- "compfn": "∘",
- "complement": "∁",
- "complexes": "ℂ",
- "cong": "≅",
- "congdot": "⩭",
- "Congruent": "≡",
- "conint": "∮",
- "Conint": "∯",
- "ContourIntegral": "∮",
- "copf": "𝕔",
- "Copf": "ℂ",
- "coprod": "∐",
- "Coproduct": "∐",
- "copy": "©",
- "COPY": "©",
- "copysr": "℗",
- "CounterClockwiseContourIntegral": "∳",
- "crarr": "↵",
- "cross": "✗",
- "Cross": "⨯",
- "Cscr": "𝒞",
- "cscr": "𝒸",
- "csub": "⫏",
- "csube": "⫑",
- "csup": "⫐",
- "csupe": "⫒",
- "ctdot": "⋯",
- "cudarrl": "⤸",
- "cudarrr": "⤵",
- "cuepr": "⋞",
- "cuesc": "⋟",
- "cularr": "↶",
- "cularrp": "⤽",
- "cupbrcap": "⩈",
- "cupcap": "⩆",
- "CupCap": "≍",
- "cup": "∪",
- "Cup": "⋓",
- "cupcup": "⩊",
- "cupdot": "⊍",
- "cupor": "⩅",
- "cups": "∪︀",
- "curarr": "↷",
- "curarrm": "⤼",
- "curlyeqprec": "⋞",
- "curlyeqsucc": "⋟",
- "curlyvee": "⋎",
- "curlywedge": "⋏",
- "curren": "¤",
- "curvearrowleft": "↶",
- "curvearrowright": "↷",
- "cuvee": "⋎",
- "cuwed": "⋏",
- "cwconint": "∲",
- "cwint": "∱",
- "cylcty": "⌭",
- "dagger": "†",
- "Dagger": "‡",
- "daleth": "ℸ",
- "darr": "↓",
- "Darr": "↡",
- "dArr": "⇓",
- "dash": "‐",
- "Dashv": "⫤",
- "dashv": "⊣",
- "dbkarow": "⤏",
- "dblac": "˝",
- "Dcaron": "Ď",
- "dcaron": "ď",
- "Dcy": "Д",
- "dcy": "д",
- "ddagger": "‡",
- "ddarr": "⇊",
- "DD": "ⅅ",
- "dd": "ⅆ",
- "DDotrahd": "⤑",
- "ddotseq": "⩷",
- "deg": "°",
- "Del": "∇",
- "Delta": "Δ",
- "delta": "δ",
- "demptyv": "⦱",
- "dfisht": "⥿",
- "Dfr": "𝔇",
- "dfr": "𝔡",
- "dHar": "⥥",
- "dharl": "⇃",
- "dharr": "⇂",
- "DiacriticalAcute": "´",
- "DiacriticalDot": "˙",
- "DiacriticalDoubleAcute": "˝",
- "DiacriticalGrave": "`",
- "DiacriticalTilde": "˜",
- "diam": "⋄",
- "diamond": "⋄",
- "Diamond": "⋄",
- "diamondsuit": "♦",
- "diams": "♦",
- "die": "¨",
- "DifferentialD": "ⅆ",
- "digamma": "ϝ",
- "disin": "⋲",
- "div": "÷",
- "divide": "÷",
- "divideontimes": "⋇",
- "divonx": "⋇",
- "DJcy": "Ђ",
- "djcy": "ђ",
- "dlcorn": "⌞",
- "dlcrop": "⌍",
- "dollar": "$",
- "Dopf": "𝔻",
- "dopf": "𝕕",
- "Dot": "¨",
- "dot": "˙",
- "DotDot": "⃜",
- "doteq": "≐",
- "doteqdot": "≑",
- "DotEqual": "≐",
- "dotminus": "∸",
- "dotplus": "∔",
- "dotsquare": "⊡",
- "doublebarwedge": "⌆",
- "DoubleContourIntegral": "∯",
- "DoubleDot": "¨",
- "DoubleDownArrow": "⇓",
- "DoubleLeftArrow": "⇐",
- "DoubleLeftRightArrow": "⇔",
- "DoubleLeftTee": "⫤",
- "DoubleLongLeftArrow": "⟸",
- "DoubleLongLeftRightArrow": "⟺",
- "DoubleLongRightArrow": "⟹",
- "DoubleRightArrow": "⇒",
- "DoubleRightTee": "⊨",
- "DoubleUpArrow": "⇑",
- "DoubleUpDownArrow": "⇕",
- "DoubleVerticalBar": "∥",
- "DownArrowBar": "⤓",
- "downarrow": "↓",
- "DownArrow": "↓",
- "Downarrow": "⇓",
- "DownArrowUpArrow": "⇵",
- "DownBreve": "̑",
- "downdownarrows": "⇊",
- "downharpoonleft": "⇃",
- "downharpoonright": "⇂",
- "DownLeftRightVector": "⥐",
- "DownLeftTeeVector": "⥞",
- "DownLeftVectorBar": "⥖",
- "DownLeftVector": "↽",
- "DownRightTeeVector": "⥟",
- "DownRightVectorBar": "⥗",
- "DownRightVector": "⇁",
- "DownTeeArrow": "↧",
- "DownTee": "⊤",
- "drbkarow": "⤐",
- "drcorn": "⌟",
- "drcrop": "⌌",
- "Dscr": "𝒟",
- "dscr": "𝒹",
- "DScy": "Ѕ",
- "dscy": "ѕ",
- "dsol": "⧶",
- "Dstrok": "Đ",
- "dstrok": "đ",
- "dtdot": "⋱",
- "dtri": "▿",
- "dtrif": "▾",
- "duarr": "⇵",
- "duhar": "⥯",
- "dwangle": "⦦",
- "DZcy": "Џ",
- "dzcy": "џ",
- "dzigrarr": "⟿",
- "Eacute": "É",
- "eacute": "é",
- "easter": "⩮",
- "Ecaron": "Ě",
- "ecaron": "ě",
- "Ecirc": "Ê",
- "ecirc": "ê",
- "ecir": "≖",
- "ecolon": "≕",
- "Ecy": "Э",
- "ecy": "э",
- "eDDot": "⩷",
- "Edot": "Ė",
- "edot": "ė",
- "eDot": "≑",
- "ee": "ⅇ",
- "efDot": "≒",
- "Efr": "𝔈",
- "efr": "𝔢",
- "eg": "⪚",
- "Egrave": "È",
- "egrave": "è",
- "egs": "⪖",
- "egsdot": "⪘",
- "el": "⪙",
- "Element": "∈",
- "elinters": "⏧",
- "ell": "ℓ",
- "els": "⪕",
- "elsdot": "⪗",
- "Emacr": "Ē",
- "emacr": "ē",
- "empty": "∅",
- "emptyset": "∅",
- "EmptySmallSquare": "◻",
- "emptyv": "∅",
- "EmptyVerySmallSquare": "▫",
- "emsp13": " ",
- "emsp14": " ",
- "emsp": " ",
- "ENG": "Ŋ",
- "eng": "ŋ",
- "ensp": " ",
- "Eogon": "Ę",
- "eogon": "ę",
- "Eopf": "𝔼",
- "eopf": "𝕖",
- "epar": "⋕",
- "eparsl": "⧣",
- "eplus": "⩱",
- "epsi": "ε",
- "Epsilon": "Ε",
- "epsilon": "ε",
- "epsiv": "ϵ",
- "eqcirc": "≖",
- "eqcolon": "≕",
- "eqsim": "≂",
- "eqslantgtr": "⪖",
- "eqslantless": "⪕",
- "Equal": "⩵",
- "equals": "=",
- "EqualTilde": "≂",
- "equest": "≟",
- "Equilibrium": "⇌",
- "equiv": "≡",
- "equivDD": "⩸",
- "eqvparsl": "⧥",
- "erarr": "⥱",
- "erDot": "≓",
- "escr": "ℯ",
- "Escr": "ℰ",
- "esdot": "≐",
- "Esim": "⩳",
- "esim": "≂",
- "Eta": "Η",
- "eta": "η",
- "ETH": "Ð",
- "eth": "ð",
- "Euml": "Ë",
- "euml": "ë",
- "euro": "€",
- "excl": "!",
- "exist": "∃",
- "Exists": "∃",
- "expectation": "ℰ",
- "exponentiale": "ⅇ",
- "ExponentialE": "ⅇ",
- "fallingdotseq": "≒",
- "Fcy": "Ф",
- "fcy": "ф",
- "female": "♀",
- "ffilig": "ffi",
- "fflig": "ff",
- "ffllig": "ffl",
- "Ffr": "𝔉",
- "ffr": "𝔣",
- "filig": "fi",
- "FilledSmallSquare": "◼",
- "FilledVerySmallSquare": "▪",
- "fjlig": "fj",
- "flat": "♭",
- "fllig": "fl",
- "fltns": "▱",
- "fnof": "ƒ",
- "Fopf": "𝔽",
- "fopf": "𝕗",
- "forall": "∀",
- "ForAll": "∀",
- "fork": "⋔",
- "forkv": "⫙",
- "Fouriertrf": "ℱ",
- "fpartint": "⨍",
- "frac12": "½",
- "frac13": "⅓",
- "frac14": "¼",
- "frac15": "⅕",
- "frac16": "⅙",
- "frac18": "⅛",
- "frac23": "⅔",
- "frac25": "⅖",
- "frac34": "¾",
- "frac35": "⅗",
- "frac38": "⅜",
- "frac45": "⅘",
- "frac56": "⅚",
- "frac58": "⅝",
- "frac78": "⅞",
- "frasl": "⁄",
- "frown": "⌢",
- "fscr": "𝒻",
- "Fscr": "ℱ",
- "gacute": "ǵ",
- "Gamma": "Γ",
- "gamma": "γ",
- "Gammad": "Ϝ",
- "gammad": "ϝ",
- "gap": "⪆",
- "Gbreve": "Ğ",
- "gbreve": "ğ",
- "Gcedil": "Ģ",
- "Gcirc": "Ĝ",
- "gcirc": "ĝ",
- "Gcy": "Г",
- "gcy": "г",
- "Gdot": "Ġ",
- "gdot": "ġ",
- "ge": "≥",
- "gE": "≧",
- "gEl": "⪌",
- "gel": "⋛",
- "geq": "≥",
- "geqq": "≧",
- "geqslant": "⩾",
- "gescc": "⪩",
- "ges": "⩾",
- "gesdot": "⪀",
- "gesdoto": "⪂",
- "gesdotol": "⪄",
- "gesl": "⋛︀",
- "gesles": "⪔",
- "Gfr": "𝔊",
- "gfr": "𝔤",
- "gg": "≫",
- "Gg": "⋙",
- "ggg": "⋙",
- "gimel": "ℷ",
- "GJcy": "Ѓ",
- "gjcy": "ѓ",
- "gla": "⪥",
- "gl": "≷",
- "glE": "⪒",
- "glj": "⪤",
- "gnap": "⪊",
- "gnapprox": "⪊",
- "gne": "⪈",
- "gnE": "≩",
- "gneq": "⪈",
- "gneqq": "≩",
- "gnsim": "⋧",
- "Gopf": "𝔾",
- "gopf": "𝕘",
- "grave": "`",
- "GreaterEqual": "≥",
- "GreaterEqualLess": "⋛",
- "GreaterFullEqual": "≧",
- "GreaterGreater": "⪢",
- "GreaterLess": "≷",
- "GreaterSlantEqual": "⩾",
- "GreaterTilde": "≳",
- "Gscr": "𝒢",
- "gscr": "ℊ",
- "gsim": "≳",
- "gsime": "⪎",
- "gsiml": "⪐",
- "gtcc": "⪧",
- "gtcir": "⩺",
- "gt": ">",
- "GT": ">",
- "Gt": "≫",
- "gtdot": "⋗",
- "gtlPar": "⦕",
- "gtquest": "⩼",
- "gtrapprox": "⪆",
- "gtrarr": "⥸",
- "gtrdot": "⋗",
- "gtreqless": "⋛",
- "gtreqqless": "⪌",
- "gtrless": "≷",
- "gtrsim": "≳",
- "gvertneqq": "≩︀",
- "gvnE": "≩︀",
- "Hacek": "ˇ",
- "hairsp": " ",
- "half": "½",
- "hamilt": "ℋ",
- "HARDcy": "Ъ",
- "hardcy": "ъ",
- "harrcir": "⥈",
- "harr": "↔",
- "hArr": "⇔",
- "harrw": "↭",
- "Hat": "^",
- "hbar": "ℏ",
- "Hcirc": "Ĥ",
- "hcirc": "ĥ",
- "hearts": "♥",
- "heartsuit": "♥",
- "hellip": "…",
- "hercon": "⊹",
- "hfr": "𝔥",
- "Hfr": "ℌ",
- "HilbertSpace": "ℋ",
- "hksearow": "⤥",
- "hkswarow": "⤦",
- "hoarr": "⇿",
- "homtht": "∻",
- "hookleftarrow": "↩",
- "hookrightarrow": "↪",
- "hopf": "𝕙",
- "Hopf": "ℍ",
- "horbar": "―",
- "HorizontalLine": "─",
- "hscr": "𝒽",
- "Hscr": "ℋ",
- "hslash": "ℏ",
- "Hstrok": "Ħ",
- "hstrok": "ħ",
- "HumpDownHump": "≎",
- "HumpEqual": "≏",
- "hybull": "⁃",
- "hyphen": "‐",
- "Iacute": "Í",
- "iacute": "í",
- "ic": "",
- "Icirc": "Î",
- "icirc": "î",
- "Icy": "И",
- "icy": "и",
- "Idot": "İ",
- "IEcy": "Е",
- "iecy": "е",
- "iexcl": "¡",
- "iff": "⇔",
- "ifr": "𝔦",
- "Ifr": "ℑ",
- "Igrave": "Ì",
- "igrave": "ì",
- "ii": "ⅈ",
- "iiiint": "⨌",
- "iiint": "∭",
- "iinfin": "⧜",
- "iiota": "℩",
- "IJlig": "IJ",
- "ijlig": "ij",
- "Imacr": "Ī",
- "imacr": "ī",
- "image": "ℑ",
- "ImaginaryI": "ⅈ",
- "imagline": "ℐ",
- "imagpart": "ℑ",
- "imath": "ı",
- "Im": "ℑ",
- "imof": "⊷",
- "imped": "Ƶ",
- "Implies": "⇒",
- "incare": "℅",
- "in": "∈",
- "infin": "∞",
- "infintie": "⧝",
- "inodot": "ı",
- "intcal": "⊺",
- "int": "∫",
- "Int": "∬",
- "integers": "ℤ",
- "Integral": "∫",
- "intercal": "⊺",
- "Intersection": "⋂",
- "intlarhk": "⨗",
- "intprod": "⨼",
- "InvisibleComma": "",
- "InvisibleTimes": "",
- "IOcy": "Ё",
- "iocy": "ё",
- "Iogon": "Į",
- "iogon": "į",
- "Iopf": "𝕀",
- "iopf": "𝕚",
- "Iota": "Ι",
- "iota": "ι",
- "iprod": "⨼",
- "iquest": "¿",
- "iscr": "𝒾",
- "Iscr": "ℐ",
- "isin": "∈",
- "isindot": "⋵",
- "isinE": "⋹",
- "isins": "⋴",
- "isinsv": "⋳",
- "isinv": "∈",
- "it": "",
- "Itilde": "Ĩ",
- "itilde": "ĩ",
- "Iukcy": "І",
- "iukcy": "і",
- "Iuml": "Ï",
- "iuml": "ï",
- "Jcirc": "Ĵ",
- "jcirc": "ĵ",
- "Jcy": "Й",
- "jcy": "й",
- "Jfr": "𝔍",
- "jfr": "𝔧",
- "jmath": "ȷ",
- "Jopf": "𝕁",
- "jopf": "𝕛",
- "Jscr": "𝒥",
- "jscr": "𝒿",
- "Jsercy": "Ј",
- "jsercy": "ј",
- "Jukcy": "Є",
- "jukcy": "є",
- "Kappa": "Κ",
- "kappa": "κ",
- "kappav": "ϰ",
- "Kcedil": "Ķ",
- "kcedil": "ķ",
- "Kcy": "К",
- "kcy": "к",
- "Kfr": "𝔎",
- "kfr": "𝔨",
- "kgreen": "ĸ",
- "KHcy": "Х",
- "khcy": "х",
- "KJcy": "Ќ",
- "kjcy": "ќ",
- "Kopf": "𝕂",
- "kopf": "𝕜",
- "Kscr": "𝒦",
- "kscr": "𝓀",
- "lAarr": "⇚",
- "Lacute": "Ĺ",
- "lacute": "ĺ",
- "laemptyv": "⦴",
- "lagran": "ℒ",
- "Lambda": "Λ",
- "lambda": "λ",
- "lang": "⟨",
- "Lang": "⟪",
- "langd": "⦑",
- "langle": "⟨",
- "lap": "⪅",
- "Laplacetrf": "ℒ",
- "laquo": "«",
- "larrb": "⇤",
- "larrbfs": "⤟",
- "larr": "←",
- "Larr": "↞",
- "lArr": "⇐",
- "larrfs": "⤝",
- "larrhk": "↩",
- "larrlp": "↫",
- "larrpl": "⤹",
- "larrsim": "⥳",
- "larrtl": "↢",
- "latail": "⤙",
- "lAtail": "⤛",
- "lat": "⪫",
- "late": "⪭",
- "lates": "⪭︀",
- "lbarr": "⤌",
- "lBarr": "⤎",
- "lbbrk": "❲",
- "lbrace": "{",
- "lbrack": "[",
- "lbrke": "⦋",
- "lbrksld": "⦏",
- "lbrkslu": "⦍",
- "Lcaron": "Ľ",
- "lcaron": "ľ",
- "Lcedil": "Ļ",
- "lcedil": "ļ",
- "lceil": "⌈",
- "lcub": "{",
- "Lcy": "Л",
- "lcy": "л",
- "ldca": "⤶",
- "ldquo": "“",
- "ldquor": "„",
- "ldrdhar": "⥧",
- "ldrushar": "⥋",
- "ldsh": "↲",
- "le": "≤",
- "lE": "≦",
- "LeftAngleBracket": "⟨",
- "LeftArrowBar": "⇤",
- "leftarrow": "←",
- "LeftArrow": "←",
- "Leftarrow": "⇐",
- "LeftArrowRightArrow": "⇆",
- "leftarrowtail": "↢",
- "LeftCeiling": "⌈",
- "LeftDoubleBracket": "⟦",
- "LeftDownTeeVector": "⥡",
- "LeftDownVectorBar": "⥙",
- "LeftDownVector": "⇃",
- "LeftFloor": "⌊",
- "leftharpoondown": "↽",
- "leftharpoonup": "↼",
- "leftleftarrows": "⇇",
- "leftrightarrow": "↔",
- "LeftRightArrow": "↔",
- "Leftrightarrow": "⇔",
- "leftrightarrows": "⇆",
- "leftrightharpoons": "⇋",
- "leftrightsquigarrow": "↭",
- "LeftRightVector": "⥎",
- "LeftTeeArrow": "↤",
- "LeftTee": "⊣",
- "LeftTeeVector": "⥚",
- "leftthreetimes": "⋋",
- "LeftTriangleBar": "⧏",
- "LeftTriangle": "⊲",
- "LeftTriangleEqual": "⊴",
- "LeftUpDownVector": "⥑",
- "LeftUpTeeVector": "⥠",
- "LeftUpVectorBar": "⥘",
- "LeftUpVector": "↿",
- "LeftVectorBar": "⥒",
- "LeftVector": "↼",
- "lEg": "⪋",
- "leg": "⋚",
- "leq": "≤",
- "leqq": "≦",
- "leqslant": "⩽",
- "lescc": "⪨",
- "les": "⩽",
- "lesdot": "⩿",
- "lesdoto": "⪁",
- "lesdotor": "⪃",
- "lesg": "⋚︀",
- "lesges": "⪓",
- "lessapprox": "⪅",
- "lessdot": "⋖",
- "lesseqgtr": "⋚",
- "lesseqqgtr": "⪋",
- "LessEqualGreater": "⋚",
- "LessFullEqual": "≦",
- "LessGreater": "≶",
- "lessgtr": "≶",
- "LessLess": "⪡",
- "lesssim": "≲",
- "LessSlantEqual": "⩽",
- "LessTilde": "≲",
- "lfisht": "⥼",
- "lfloor": "⌊",
- "Lfr": "𝔏",
- "lfr": "𝔩",
- "lg": "≶",
- "lgE": "⪑",
- "lHar": "⥢",
- "lhard": "↽",
- "lharu": "↼",
- "lharul": "⥪",
- "lhblk": "▄",
- "LJcy": "Љ",
- "ljcy": "љ",
- "llarr": "⇇",
- "ll": "≪",
- "Ll": "⋘",
- "llcorner": "⌞",
- "Lleftarrow": "⇚",
- "llhard": "⥫",
- "lltri": "◺",
- "Lmidot": "Ŀ",
- "lmidot": "ŀ",
- "lmoustache": "⎰",
- "lmoust": "⎰",
- "lnap": "⪉",
- "lnapprox": "⪉",
- "lne": "⪇",
- "lnE": "≨",
- "lneq": "⪇",
- "lneqq": "≨",
- "lnsim": "⋦",
- "loang": "⟬",
- "loarr": "⇽",
- "lobrk": "⟦",
- "longleftarrow": "⟵",
- "LongLeftArrow": "⟵",
- "Longleftarrow": "⟸",
- "longleftrightarrow": "⟷",
- "LongLeftRightArrow": "⟷",
- "Longleftrightarrow": "⟺",
- "longmapsto": "⟼",
- "longrightarrow": "⟶",
- "LongRightArrow": "⟶",
- "Longrightarrow": "⟹",
- "looparrowleft": "↫",
- "looparrowright": "↬",
- "lopar": "⦅",
- "Lopf": "𝕃",
- "lopf": "𝕝",
- "loplus": "⨭",
- "lotimes": "⨴",
- "lowast": "∗",
- "lowbar": "_",
- "LowerLeftArrow": "↙",
- "LowerRightArrow": "↘",
- "loz": "◊",
- "lozenge": "◊",
- "lozf": "⧫",
- "lpar": "(",
- "lparlt": "⦓",
- "lrarr": "⇆",
- "lrcorner": "⌟",
- "lrhar": "⇋",
- "lrhard": "⥭",
- "lrm": "",
- "lrtri": "⊿",
- "lsaquo": "‹",
- "lscr": "𝓁",
- "Lscr": "ℒ",
- "lsh": "↰",
- "Lsh": "↰",
- "lsim": "≲",
- "lsime": "⪍",
- "lsimg": "⪏",
- "lsqb": "[",
- "lsquo": "‘",
- "lsquor": "‚",
- "Lstrok": "Ł",
- "lstrok": "ł",
- "ltcc": "⪦",
- "ltcir": "⩹",
- "lt": "<",
- "LT": "<",
- "Lt": "≪",
- "ltdot": "⋖",
- "lthree": "⋋",
- "ltimes": "⋉",
- "ltlarr": "⥶",
- "ltquest": "⩻",
- "ltri": "◃",
- "ltrie": "⊴",
- "ltrif": "◂",
- "ltrPar": "⦖",
- "lurdshar": "⥊",
- "luruhar": "⥦",
- "lvertneqq": "≨︀",
- "lvnE": "≨︀",
- "macr": "¯",
- "male": "♂",
- "malt": "✠",
- "maltese": "✠",
- "Map": "⤅",
- "map": "↦",
- "mapsto": "↦",
- "mapstodown": "↧",
- "mapstoleft": "↤",
- "mapstoup": "↥",
- "marker": "▮",
- "mcomma": "⨩",
- "Mcy": "М",
- "mcy": "м",
- "mdash": "—",
- "mDDot": "∺",
- "measuredangle": "∡",
- "MediumSpace": " ",
- "Mellintrf": "ℳ",
- "Mfr": "𝔐",
- "mfr": "𝔪",
- "mho": "℧",
- "micro": "µ",
- "midast": "*",
- "midcir": "⫰",
- "mid": "∣",
- "middot": "·",
- "minusb": "⊟",
- "minus": "−",
- "minusd": "∸",
- "minusdu": "⨪",
- "MinusPlus": "∓",
- "mlcp": "⫛",
- "mldr": "…",
- "mnplus": "∓",
- "models": "⊧",
- "Mopf": "𝕄",
- "mopf": "𝕞",
- "mp": "∓",
- "mscr": "𝓂",
- "Mscr": "ℳ",
- "mstpos": "∾",
- "Mu": "Μ",
- "mu": "μ",
- "multimap": "⊸",
- "mumap": "⊸",
- "nabla": "∇",
- "Nacute": "Ń",
- "nacute": "ń",
- "nang": "∠⃒",
- "nap": "≉",
- "napE": "⩰̸",
- "napid": "≋̸",
- "napos": "ʼn",
- "napprox": "≉",
- "natural": "♮",
- "naturals": "ℕ",
- "natur": "♮",
- "nbsp": " ",
- "nbump": "≎̸",
- "nbumpe": "≏̸",
- "ncap": "⩃",
- "Ncaron": "Ň",
- "ncaron": "ň",
- "Ncedil": "Ņ",
- "ncedil": "ņ",
- "ncong": "≇",
- "ncongdot": "⩭̸",
- "ncup": "⩂",
- "Ncy": "Н",
- "ncy": "н",
- "ndash": "–",
- "nearhk": "⤤",
- "nearr": "↗",
- "neArr": "⇗",
- "nearrow": "↗",
- "ne": "≠",
- "nedot": "≐̸",
- "NegativeMediumSpace": "",
- "NegativeThickSpace": "",
- "NegativeThinSpace": "",
- "NegativeVeryThinSpace": "",
- "nequiv": "≢",
- "nesear": "⤨",
- "nesim": "≂̸",
- "NestedGreaterGreater": "≫",
- "NestedLessLess": "≪",
- "NewLine": "\n",
- "nexist": "∄",
- "nexists": "∄",
- "Nfr": "𝔑",
- "nfr": "𝔫",
- "ngE": "≧̸",
- "nge": "≱",
- "ngeq": "≱",
- "ngeqq": "≧̸",
- "ngeqslant": "⩾̸",
- "nges": "⩾̸",
- "nGg": "⋙̸",
- "ngsim": "≵",
- "nGt": "≫⃒",
- "ngt": "≯",
- "ngtr": "≯",
- "nGtv": "≫̸",
- "nharr": "↮",
- "nhArr": "⇎",
- "nhpar": "⫲",
- "ni": "∋",
- "nis": "⋼",
- "nisd": "⋺",
- "niv": "∋",
- "NJcy": "Њ",
- "njcy": "њ",
- "nlarr": "↚",
- "nlArr": "⇍",
- "nldr": "‥",
- "nlE": "≦̸",
- "nle": "≰",
- "nleftarrow": "↚",
- "nLeftarrow": "⇍",
- "nleftrightarrow": "↮",
- "nLeftrightarrow": "⇎",
- "nleq": "≰",
- "nleqq": "≦̸",
- "nleqslant": "⩽̸",
- "nles": "⩽̸",
- "nless": "≮",
- "nLl": "⋘̸",
- "nlsim": "≴",
- "nLt": "≪⃒",
- "nlt": "≮",
- "nltri": "⋪",
- "nltrie": "⋬",
- "nLtv": "≪̸",
- "nmid": "∤",
- "NoBreak": "",
- "NonBreakingSpace": " ",
- "nopf": "𝕟",
- "Nopf": "ℕ",
- "Not": "⫬",
- "not": "¬",
- "NotCongruent": "≢",
- "NotCupCap": "≭",
- "NotDoubleVerticalBar": "∦",
- "NotElement": "∉",
- "NotEqual": "≠",
- "NotEqualTilde": "≂̸",
- "NotExists": "∄",
- "NotGreater": "≯",
- "NotGreaterEqual": "≱",
- "NotGreaterFullEqual": "≧̸",
- "NotGreaterGreater": "≫̸",
- "NotGreaterLess": "≹",
- "NotGreaterSlantEqual": "⩾̸",
- "NotGreaterTilde": "≵",
- "NotHumpDownHump": "≎̸",
- "NotHumpEqual": "≏̸",
- "notin": "∉",
- "notindot": "⋵̸",
- "notinE": "⋹̸",
- "notinva": "∉",
- "notinvb": "⋷",
- "notinvc": "⋶",
- "NotLeftTriangleBar": "⧏̸",
- "NotLeftTriangle": "⋪",
- "NotLeftTriangleEqual": "⋬",
- "NotLess": "≮",
- "NotLessEqual": "≰",
- "NotLessGreater": "≸",
- "NotLessLess": "≪̸",
- "NotLessSlantEqual": "⩽̸",
- "NotLessTilde": "≴",
- "NotNestedGreaterGreater": "⪢̸",
- "NotNestedLessLess": "⪡̸",
- "notni": "∌",
- "notniva": "∌",
- "notnivb": "⋾",
- "notnivc": "⋽",
- "NotPrecedes": "⊀",
- "NotPrecedesEqual": "⪯̸",
- "NotPrecedesSlantEqual": "⋠",
- "NotReverseElement": "∌",
- "NotRightTriangleBar": "⧐̸",
- "NotRightTriangle": "⋫",
- "NotRightTriangleEqual": "⋭",
- "NotSquareSubset": "⊏̸",
- "NotSquareSubsetEqual": "⋢",
- "NotSquareSuperset": "⊐̸",
- "NotSquareSupersetEqual": "⋣",
- "NotSubset": "⊂⃒",
- "NotSubsetEqual": "⊈",
- "NotSucceeds": "⊁",
- "NotSucceedsEqual": "⪰̸",
- "NotSucceedsSlantEqual": "⋡",
- "NotSucceedsTilde": "≿̸",
- "NotSuperset": "⊃⃒",
- "NotSupersetEqual": "⊉",
- "NotTilde": "≁",
- "NotTildeEqual": "≄",
- "NotTildeFullEqual": "≇",
- "NotTildeTilde": "≉",
- "NotVerticalBar": "∤",
- "nparallel": "∦",
- "npar": "∦",
- "nparsl": "⫽⃥",
- "npart": "∂̸",
- "npolint": "⨔",
- "npr": "⊀",
- "nprcue": "⋠",
- "nprec": "⊀",
- "npreceq": "⪯̸",
- "npre": "⪯̸",
- "nrarrc": "⤳̸",
- "nrarr": "↛",
- "nrArr": "⇏",
- "nrarrw": "↝̸",
- "nrightarrow": "↛",
- "nRightarrow": "⇏",
- "nrtri": "⋫",
- "nrtrie": "⋭",
- "nsc": "⊁",
- "nsccue": "⋡",
- "nsce": "⪰̸",
- "Nscr": "𝒩",
- "nscr": "𝓃",
- "nshortmid": "∤",
- "nshortparallel": "∦",
- "nsim": "≁",
- "nsime": "≄",
- "nsimeq": "≄",
- "nsmid": "∤",
- "nspar": "∦",
- "nsqsube": "⋢",
- "nsqsupe": "⋣",
- "nsub": "⊄",
- "nsubE": "⫅̸",
- "nsube": "⊈",
- "nsubset": "⊂⃒",
- "nsubseteq": "⊈",
- "nsubseteqq": "⫅̸",
- "nsucc": "⊁",
- "nsucceq": "⪰̸",
- "nsup": "⊅",
- "nsupE": "⫆̸",
- "nsupe": "⊉",
- "nsupset": "⊃⃒",
- "nsupseteq": "⊉",
- "nsupseteqq": "⫆̸",
- "ntgl": "≹",
- "Ntilde": "Ñ",
- "ntilde": "ñ",
- "ntlg": "≸",
- "ntriangleleft": "⋪",
- "ntrianglelefteq": "⋬",
- "ntriangleright": "⋫",
- "ntrianglerighteq": "⋭",
- "Nu": "Ν",
- "nu": "ν",
- "num": "#",
- "numero": "№",
- "numsp": " ",
- "nvap": "≍⃒",
- "nvdash": "⊬",
- "nvDash": "⊭",
- "nVdash": "⊮",
- "nVDash": "⊯",
- "nvge": "≥⃒",
- "nvgt": ">⃒",
- "nvHarr": "⤄",
- "nvinfin": "⧞",
- "nvlArr": "⤂",
- "nvle": "≤⃒",
- "nvlt": "<⃒",
- "nvltrie": "⊴⃒",
- "nvrArr": "⤃",
- "nvrtrie": "⊵⃒",
- "nvsim": "∼⃒",
- "nwarhk": "⤣",
- "nwarr": "↖",
- "nwArr": "⇖",
- "nwarrow": "↖",
- "nwnear": "⤧",
- "Oacute": "Ó",
- "oacute": "ó",
- "oast": "⊛",
- "Ocirc": "Ô",
- "ocirc": "ô",
- "ocir": "⊚",
- "Ocy": "О",
- "ocy": "о",
- "odash": "⊝",
- "Odblac": "Ő",
- "odblac": "ő",
- "odiv": "⨸",
- "odot": "⊙",
- "odsold": "⦼",
- "OElig": "Œ",
- "oelig": "œ",
- "ofcir": "⦿",
- "Ofr": "𝔒",
- "ofr": "𝔬",
- "ogon": "˛",
- "Ograve": "Ò",
- "ograve": "ò",
- "ogt": "⧁",
- "ohbar": "⦵",
- "ohm": "Ω",
- "oint": "∮",
- "olarr": "↺",
- "olcir": "⦾",
- "olcross": "⦻",
- "oline": "‾",
- "olt": "⧀",
- "Omacr": "Ō",
- "omacr": "ō",
- "Omega": "Ω",
- "omega": "ω",
- "Omicron": "Ο",
- "omicron": "ο",
- "omid": "⦶",
- "ominus": "⊖",
- "Oopf": "𝕆",
- "oopf": "𝕠",
- "opar": "⦷",
- "OpenCurlyDoubleQuote": "“",
- "OpenCurlyQuote": "‘",
- "operp": "⦹",
- "oplus": "⊕",
- "orarr": "↻",
- "Or": "⩔",
- "or": "∨",
- "ord": "⩝",
- "order": "ℴ",
- "orderof": "ℴ",
- "ordf": "ª",
- "ordm": "º",
- "origof": "⊶",
- "oror": "⩖",
- "orslope": "⩗",
- "orv": "⩛",
- "oS": "Ⓢ",
- "Oscr": "𝒪",
- "oscr": "ℴ",
- "Oslash": "Ø",
- "oslash": "ø",
- "osol": "⊘",
- "Otilde": "Õ",
- "otilde": "õ",
- "otimesas": "⨶",
- "Otimes": "⨷",
- "otimes": "⊗",
- "Ouml": "Ö",
- "ouml": "ö",
- "ovbar": "⌽",
- "OverBar": "‾",
- "OverBrace": "⏞",
- "OverBracket": "⎴",
- "OverParenthesis": "⏜",
- "para": "¶",
- "parallel": "∥",
- "par": "∥",
- "parsim": "⫳",
- "parsl": "⫽",
- "part": "∂",
- "PartialD": "∂",
- "Pcy": "П",
- "pcy": "п",
- "percnt": "%",
- "period": ".",
- "permil": "‰",
- "perp": "⊥",
- "pertenk": "‱",
- "Pfr": "𝔓",
- "pfr": "𝔭",
- "Phi": "Φ",
- "phi": "φ",
- "phiv": "ϕ",
- "phmmat": "ℳ",
- "phone": "☎",
- "Pi": "Π",
- "pi": "π",
- "pitchfork": "⋔",
- "piv": "ϖ",
- "planck": "ℏ",
- "planckh": "ℎ",
- "plankv": "ℏ",
- "plusacir": "⨣",
- "plusb": "⊞",
- "pluscir": "⨢",
- "plus": "+",
- "plusdo": "∔",
- "plusdu": "⨥",
- "pluse": "⩲",
- "PlusMinus": "±",
- "plusmn": "±",
- "plussim": "⨦",
- "plustwo": "⨧",
- "pm": "±",
- "Poincareplane": "ℌ",
- "pointint": "⨕",
- "popf": "𝕡",
- "Popf": "ℙ",
- "pound": "£",
- "prap": "⪷",
- "Pr": "⪻",
- "pr": "≺",
- "prcue": "≼",
- "precapprox": "⪷",
- "prec": "≺",
- "preccurlyeq": "≼",
- "Precedes": "≺",
- "PrecedesEqual": "⪯",
- "PrecedesSlantEqual": "≼",
- "PrecedesTilde": "≾",
- "preceq": "⪯",
- "precnapprox": "⪹",
- "precneqq": "⪵",
- "precnsim": "⋨",
- "pre": "⪯",
- "prE": "⪳",
- "precsim": "≾",
- "prime": "′",
- "Prime": "″",
- "primes": "ℙ",
- "prnap": "⪹",
- "prnE": "⪵",
- "prnsim": "⋨",
- "prod": "∏",
- "Product": "∏",
- "profalar": "⌮",
- "profline": "⌒",
- "profsurf": "⌓",
- "prop": "∝",
- "Proportional": "∝",
- "Proportion": "∷",
- "propto": "∝",
- "prsim": "≾",
- "prurel": "⊰",
- "Pscr": "𝒫",
- "pscr": "𝓅",
- "Psi": "Ψ",
- "psi": "ψ",
- "puncsp": " ",
- "Qfr": "𝔔",
- "qfr": "𝔮",
- "qint": "⨌",
- "qopf": "𝕢",
- "Qopf": "ℚ",
- "qprime": "⁗",
- "Qscr": "𝒬",
- "qscr": "𝓆",
- "quaternions": "ℍ",
- "quatint": "⨖",
- "quest": "?",
- "questeq": "≟",
- "quot": "\"",
- "QUOT": "\"",
- "rAarr": "⇛",
- "race": "∽̱",
- "Racute": "Ŕ",
- "racute": "ŕ",
- "radic": "√",
- "raemptyv": "⦳",
- "rang": "⟩",
- "Rang": "⟫",
- "rangd": "⦒",
- "range": "⦥",
- "rangle": "⟩",
- "raquo": "»",
- "rarrap": "⥵",
- "rarrb": "⇥",
- "rarrbfs": "⤠",
- "rarrc": "⤳",
- "rarr": "→",
- "Rarr": "↠",
- "rArr": "⇒",
- "rarrfs": "⤞",
- "rarrhk": "↪",
- "rarrlp": "↬",
- "rarrpl": "⥅",
- "rarrsim": "⥴",
- "Rarrtl": "⤖",
- "rarrtl": "↣",
- "rarrw": "↝",
- "ratail": "⤚",
- "rAtail": "⤜",
- "ratio": "∶",
- "rationals": "ℚ",
- "rbarr": "⤍",
- "rBarr": "⤏",
- "RBarr": "⤐",
- "rbbrk": "❳",
- "rbrace": "}",
- "rbrack": "]",
- "rbrke": "⦌",
- "rbrksld": "⦎",
- "rbrkslu": "⦐",
- "Rcaron": "Ř",
- "rcaron": "ř",
- "Rcedil": "Ŗ",
- "rcedil": "ŗ",
- "rceil": "⌉",
- "rcub": "}",
- "Rcy": "Р",
- "rcy": "р",
- "rdca": "⤷",
- "rdldhar": "⥩",
- "rdquo": "”",
- "rdquor": "”",
- "rdsh": "↳",
- "real": "ℜ",
- "realine": "ℛ",
- "realpart": "ℜ",
- "reals": "ℝ",
- "Re": "ℜ",
- "rect": "▭",
- "reg": "®",
- "REG": "®",
- "ReverseElement": "∋",
- "ReverseEquilibrium": "⇋",
- "ReverseUpEquilibrium": "⥯",
- "rfisht": "⥽",
- "rfloor": "⌋",
- "rfr": "𝔯",
- "Rfr": "ℜ",
- "rHar": "⥤",
- "rhard": "⇁",
- "rharu": "⇀",
- "rharul": "⥬",
- "Rho": "Ρ",
- "rho": "ρ",
- "rhov": "ϱ",
- "RightAngleBracket": "⟩",
- "RightArrowBar": "⇥",
- "rightarrow": "→",
- "RightArrow": "→",
- "Rightarrow": "⇒",
- "RightArrowLeftArrow": "⇄",
- "rightarrowtail": "↣",
- "RightCeiling": "⌉",
- "RightDoubleBracket": "⟧",
- "RightDownTeeVector": "⥝",
- "RightDownVectorBar": "⥕",
- "RightDownVector": "⇂",
- "RightFloor": "⌋",
- "rightharpoondown": "⇁",
- "rightharpoonup": "⇀",
- "rightleftarrows": "⇄",
- "rightleftharpoons": "⇌",
- "rightrightarrows": "⇉",
- "rightsquigarrow": "↝",
- "RightTeeArrow": "↦",
- "RightTee": "⊢",
- "RightTeeVector": "⥛",
- "rightthreetimes": "⋌",
- "RightTriangleBar": "⧐",
- "RightTriangle": "⊳",
- "RightTriangleEqual": "⊵",
- "RightUpDownVector": "⥏",
- "RightUpTeeVector": "⥜",
- "RightUpVectorBar": "⥔",
- "RightUpVector": "↾",
- "RightVectorBar": "⥓",
- "RightVector": "⇀",
- "ring": "˚",
- "risingdotseq": "≓",
- "rlarr": "⇄",
- "rlhar": "⇌",
- "rlm": "",
- "rmoustache": "⎱",
- "rmoust": "⎱",
- "rnmid": "⫮",
- "roang": "⟭",
- "roarr": "⇾",
- "robrk": "⟧",
- "ropar": "⦆",
- "ropf": "𝕣",
- "Ropf": "ℝ",
- "roplus": "⨮",
- "rotimes": "⨵",
- "RoundImplies": "⥰",
- "rpar": ")",
- "rpargt": "⦔",
- "rppolint": "⨒",
- "rrarr": "⇉",
- "Rrightarrow": "⇛",
- "rsaquo": "›",
- "rscr": "𝓇",
- "Rscr": "ℛ",
- "rsh": "↱",
- "Rsh": "↱",
- "rsqb": "]",
- "rsquo": "’",
- "rsquor": "’",
- "rthree": "⋌",
- "rtimes": "⋊",
- "rtri": "▹",
- "rtrie": "⊵",
- "rtrif": "▸",
- "rtriltri": "⧎",
- "RuleDelayed": "⧴",
- "ruluhar": "⥨",
- "rx": "℞",
- "Sacute": "Ś",
- "sacute": "ś",
- "sbquo": "‚",
- "scap": "⪸",
- "Scaron": "Š",
- "scaron": "š",
- "Sc": "⪼",
- "sc": "≻",
- "sccue": "≽",
- "sce": "⪰",
- "scE": "⪴",
- "Scedil": "Ş",
- "scedil": "ş",
- "Scirc": "Ŝ",
- "scirc": "ŝ",
- "scnap": "⪺",
- "scnE": "⪶",
- "scnsim": "⋩",
- "scpolint": "⨓",
- "scsim": "≿",
- "Scy": "С",
- "scy": "с",
- "sdotb": "⊡",
- "sdot": "⋅",
- "sdote": "⩦",
- "searhk": "⤥",
- "searr": "↘",
- "seArr": "⇘",
- "searrow": "↘",
- "sect": "§",
- "semi": ";",
- "seswar": "⤩",
- "setminus": "∖",
- "setmn": "∖",
- "sext": "✶",
- "Sfr": "𝔖",
- "sfr": "𝔰",
- "sfrown": "⌢",
- "sharp": "♯",
- "SHCHcy": "Щ",
- "shchcy": "щ",
- "SHcy": "Ш",
- "shcy": "ш",
- "ShortDownArrow": "↓",
- "ShortLeftArrow": "←",
- "shortmid": "∣",
- "shortparallel": "∥",
- "ShortRightArrow": "→",
- "ShortUpArrow": "↑",
- "shy": "",
- "Sigma": "Σ",
- "sigma": "σ",
- "sigmaf": "ς",
- "sigmav": "ς",
- "sim": "∼",
- "simdot": "⩪",
- "sime": "≃",
- "simeq": "≃",
- "simg": "⪞",
- "simgE": "⪠",
- "siml": "⪝",
- "simlE": "⪟",
- "simne": "≆",
- "simplus": "⨤",
- "simrarr": "⥲",
- "slarr": "←",
- "SmallCircle": "∘",
- "smallsetminus": "∖",
- "smashp": "⨳",
- "smeparsl": "⧤",
- "smid": "∣",
- "smile": "⌣",
- "smt": "⪪",
- "smte": "⪬",
- "smtes": "⪬︀",
- "SOFTcy": "Ь",
- "softcy": "ь",
- "solbar": "⌿",
- "solb": "⧄",
- "sol": "/",
- "Sopf": "𝕊",
- "sopf": "𝕤",
- "spades": "♠",
- "spadesuit": "♠",
- "spar": "∥",
- "sqcap": "⊓",
- "sqcaps": "⊓︀",
- "sqcup": "⊔",
- "sqcups": "⊔︀",
- "Sqrt": "√",
- "sqsub": "⊏",
- "sqsube": "⊑",
- "sqsubset": "⊏",
- "sqsubseteq": "⊑",
- "sqsup": "⊐",
- "sqsupe": "⊒",
- "sqsupset": "⊐",
- "sqsupseteq": "⊒",
- "square": "□",
- "Square": "□",
- "SquareIntersection": "⊓",
- "SquareSubset": "⊏",
- "SquareSubsetEqual": "⊑",
- "SquareSuperset": "⊐",
- "SquareSupersetEqual": "⊒",
- "SquareUnion": "⊔",
- "squarf": "▪",
- "squ": "□",
- "squf": "▪",
- "srarr": "→",
- "Sscr": "𝒮",
- "sscr": "𝓈",
- "ssetmn": "∖",
- "ssmile": "⌣",
- "sstarf": "⋆",
- "Star": "⋆",
- "star": "☆",
- "starf": "★",
- "straightepsilon": "ϵ",
- "straightphi": "ϕ",
- "strns": "¯",
- "sub": "⊂",
- "Sub": "⋐",
- "subdot": "⪽",
- "subE": "⫅",
- "sube": "⊆",
- "subedot": "⫃",
- "submult": "⫁",
- "subnE": "⫋",
- "subne": "⊊",
- "subplus": "⪿",
- "subrarr": "⥹",
- "subset": "⊂",
- "Subset": "⋐",
- "subseteq": "⊆",
- "subseteqq": "⫅",
- "SubsetEqual": "⊆",
- "subsetneq": "⊊",
- "subsetneqq": "⫋",
- "subsim": "⫇",
- "subsub": "⫕",
- "subsup": "⫓",
- "succapprox": "⪸",
- "succ": "≻",
- "succcurlyeq": "≽",
- "Succeeds": "≻",
- "SucceedsEqual": "⪰",
- "SucceedsSlantEqual": "≽",
- "SucceedsTilde": "≿",
- "succeq": "⪰",
- "succnapprox": "⪺",
- "succneqq": "⪶",
- "succnsim": "⋩",
- "succsim": "≿",
- "SuchThat": "∋",
- "sum": "∑",
- "Sum": "∑",
- "sung": "♪",
- "sup1": "¹",
- "sup2": "²",
- "sup3": "³",
- "sup": "⊃",
- "Sup": "⋑",
- "supdot": "⪾",
- "supdsub": "⫘",
- "supE": "⫆",
- "supe": "⊇",
- "supedot": "⫄",
- "Superset": "⊃",
- "SupersetEqual": "⊇",
- "suphsol": "⟉",
- "suphsub": "⫗",
- "suplarr": "⥻",
- "supmult": "⫂",
- "supnE": "⫌",
- "supne": "⊋",
- "supplus": "⫀",
- "supset": "⊃",
- "Supset": "⋑",
- "supseteq": "⊇",
- "supseteqq": "⫆",
- "supsetneq": "⊋",
- "supsetneqq": "⫌",
- "supsim": "⫈",
- "supsub": "⫔",
- "supsup": "⫖",
- "swarhk": "⤦",
- "swarr": "↙",
- "swArr": "⇙",
- "swarrow": "↙",
- "swnwar": "⤪",
- "szlig": "ß",
- "Tab": "\t",
- "target": "⌖",
- "Tau": "Τ",
- "tau": "τ",
- "tbrk": "⎴",
- "Tcaron": "Ť",
- "tcaron": "ť",
- "Tcedil": "Ţ",
- "tcedil": "ţ",
- "Tcy": "Т",
- "tcy": "т",
- "tdot": "⃛",
- "telrec": "⌕",
- "Tfr": "𝔗",
- "tfr": "𝔱",
- "there4": "∴",
- "therefore": "∴",
- "Therefore": "∴",
- "Theta": "Θ",
- "theta": "θ",
- "thetasym": "ϑ",
- "thetav": "ϑ",
- "thickapprox": "≈",
- "thicksim": "∼",
- "ThickSpace": " ",
- "ThinSpace": " ",
- "thinsp": " ",
- "thkap": "≈",
- "thksim": "∼",
- "THORN": "Þ",
- "thorn": "þ",
- "tilde": "˜",
- "Tilde": "∼",
- "TildeEqual": "≃",
- "TildeFullEqual": "≅",
- "TildeTilde": "≈",
- "timesbar": "⨱",
- "timesb": "⊠",
- "times": "×",
- "timesd": "⨰",
- "tint": "∭",
- "toea": "⤨",
- "topbot": "⌶",
- "topcir": "⫱",
- "top": "⊤",
- "Topf": "𝕋",
- "topf": "𝕥",
- "topfork": "⫚",
- "tosa": "⤩",
- "tprime": "‴",
- "trade": "™",
- "TRADE": "™",
- "triangle": "▵",
- "triangledown": "▿",
- "triangleleft": "◃",
- "trianglelefteq": "⊴",
- "triangleq": "≜",
- "triangleright": "▹",
- "trianglerighteq": "⊵",
- "tridot": "◬",
- "trie": "≜",
- "triminus": "⨺",
- "TripleDot": "⃛",
- "triplus": "⨹",
- "trisb": "⧍",
- "tritime": "⨻",
- "trpezium": "⏢",
- "Tscr": "𝒯",
- "tscr": "𝓉",
- "TScy": "Ц",
- "tscy": "ц",
- "TSHcy": "Ћ",
- "tshcy": "ћ",
- "Tstrok": "Ŧ",
- "tstrok": "ŧ",
- "twixt": "≬",
- "twoheadleftarrow": "↞",
- "twoheadrightarrow": "↠",
- "Uacute": "Ú",
- "uacute": "ú",
- "uarr": "↑",
- "Uarr": "↟",
- "uArr": "⇑",
- "Uarrocir": "⥉",
- "Ubrcy": "Ў",
- "ubrcy": "ў",
- "Ubreve": "Ŭ",
- "ubreve": "ŭ",
- "Ucirc": "Û",
- "ucirc": "û",
- "Ucy": "У",
- "ucy": "у",
- "udarr": "⇅",
- "Udblac": "Ű",
- "udblac": "ű",
- "udhar": "⥮",
- "ufisht": "⥾",
- "Ufr": "𝔘",
- "ufr": "𝔲",
- "Ugrave": "Ù",
- "ugrave": "ù",
- "uHar": "⥣",
- "uharl": "↿",
- "uharr": "↾",
- "uhblk": "▀",
- "ulcorn": "⌜",
- "ulcorner": "⌜",
- "ulcrop": "⌏",
- "ultri": "◸",
- "Umacr": "Ū",
- "umacr": "ū",
- "uml": "¨",
- "UnderBar": "_",
- "UnderBrace": "⏟",
- "UnderBracket": "⎵",
- "UnderParenthesis": "⏝",
- "Union": "⋃",
- "UnionPlus": "⊎",
- "Uogon": "Ų",
- "uogon": "ų",
- "Uopf": "𝕌",
- "uopf": "𝕦",
- "UpArrowBar": "⤒",
- "uparrow": "↑",
- "UpArrow": "↑",
- "Uparrow": "⇑",
- "UpArrowDownArrow": "⇅",
- "updownarrow": "↕",
- "UpDownArrow": "↕",
- "Updownarrow": "⇕",
- "UpEquilibrium": "⥮",
- "upharpoonleft": "↿",
- "upharpoonright": "↾",
- "uplus": "⊎",
- "UpperLeftArrow": "↖",
- "UpperRightArrow": "↗",
- "upsi": "υ",
- "Upsi": "ϒ",
- "upsih": "ϒ",
- "Upsilon": "Υ",
- "upsilon": "υ",
- "UpTeeArrow": "↥",
- "UpTee": "⊥",
- "upuparrows": "⇈",
- "urcorn": "⌝",
- "urcorner": "⌝",
- "urcrop": "⌎",
- "Uring": "Ů",
- "uring": "ů",
- "urtri": "◹",
- "Uscr": "𝒰",
- "uscr": "𝓊",
- "utdot": "⋰",
- "Utilde": "Ũ",
- "utilde": "ũ",
- "utri": "▵",
- "utrif": "▴",
- "uuarr": "⇈",
- "Uuml": "Ü",
- "uuml": "ü",
- "uwangle": "⦧",
- "vangrt": "⦜",
- "varepsilon": "ϵ",
- "varkappa": "ϰ",
- "varnothing": "∅",
- "varphi": "ϕ",
- "varpi": "ϖ",
- "varpropto": "∝",
- "varr": "↕",
- "vArr": "⇕",
- "varrho": "ϱ",
- "varsigma": "ς",
- "varsubsetneq": "⊊︀",
- "varsubsetneqq": "⫋︀",
- "varsupsetneq": "⊋︀",
- "varsupsetneqq": "⫌︀",
- "vartheta": "ϑ",
- "vartriangleleft": "⊲",
- "vartriangleright": "⊳",
- "vBar": "⫨",
- "Vbar": "⫫",
- "vBarv": "⫩",
- "Vcy": "В",
- "vcy": "в",
- "vdash": "⊢",
- "vDash": "⊨",
- "Vdash": "⊩",
- "VDash": "⊫",
- "Vdashl": "⫦",
- "veebar": "⊻",
- "vee": "∨",
- "Vee": "⋁",
- "veeeq": "≚",
- "vellip": "⋮",
- "verbar": "|",
- "Verbar": "‖",
- "vert": "|",
- "Vert": "‖",
- "VerticalBar": "∣",
- "VerticalLine": "|",
- "VerticalSeparator": "❘",
- "VerticalTilde": "≀",
- "VeryThinSpace": " ",
- "Vfr": "𝔙",
- "vfr": "𝔳",
- "vltri": "⊲",
- "vnsub": "⊂⃒",
- "vnsup": "⊃⃒",
- "Vopf": "𝕍",
- "vopf": "𝕧",
- "vprop": "∝",
- "vrtri": "⊳",
- "Vscr": "𝒱",
- "vscr": "𝓋",
- "vsubnE": "⫋︀",
- "vsubne": "⊊︀",
- "vsupnE": "⫌︀",
- "vsupne": "⊋︀",
- "Vvdash": "⊪",
- "vzigzag": "⦚",
- "Wcirc": "Ŵ",
- "wcirc": "ŵ",
- "wedbar": "⩟",
- "wedge": "∧",
- "Wedge": "⋀",
- "wedgeq": "≙",
- "weierp": "℘",
- "Wfr": "𝔚",
- "wfr": "𝔴",
- "Wopf": "𝕎",
- "wopf": "𝕨",
- "wp": "℘",
- "wr": "≀",
- "wreath": "≀",
- "Wscr": "𝒲",
- "wscr": "𝓌",
- "xcap": "⋂",
- "xcirc": "◯",
- "xcup": "⋃",
- "xdtri": "▽",
- "Xfr": "𝔛",
- "xfr": "𝔵",
- "xharr": "⟷",
- "xhArr": "⟺",
- "Xi": "Ξ",
- "xi": "ξ",
- "xlarr": "⟵",
- "xlArr": "⟸",
- "xmap": "⟼",
- "xnis": "⋻",
- "xodot": "⨀",
- "Xopf": "𝕏",
- "xopf": "𝕩",
- "xoplus": "⨁",
- "xotime": "⨂",
- "xrarr": "⟶",
- "xrArr": "⟹",
- "Xscr": "𝒳",
- "xscr": "𝓍",
- "xsqcup": "⨆",
- "xuplus": "⨄",
- "xutri": "△",
- "xvee": "⋁",
- "xwedge": "⋀",
- "Yacute": "Ý",
- "yacute": "ý",
- "YAcy": "Я",
- "yacy": "я",
- "Ycirc": "Ŷ",
- "ycirc": "ŷ",
- "Ycy": "Ы",
- "ycy": "ы",
- "yen": "¥",
- "Yfr": "𝔜",
- "yfr": "𝔶",
- "YIcy": "Ї",
- "yicy": "ї",
- "Yopf": "𝕐",
- "yopf": "𝕪",
- "Yscr": "𝒴",
- "yscr": "𝓎",
- "YUcy": "Ю",
- "yucy": "ю",
- "yuml": "ÿ",
- "Yuml": "Ÿ",
- "Zacute": "Ź",
- "zacute": "ź",
- "Zcaron": "Ž",
- "zcaron": "ž",
- "Zcy": "З",
- "zcy": "з",
- "Zdot": "Ż",
- "zdot": "ż",
- "zeetrf": "ℨ",
- "ZeroWidthSpace": "",
- "Zeta": "Ζ",
- "zeta": "ζ",
- "zfr": "𝔷",
- "Zfr": "ℨ",
- "ZHcy": "Ж",
- "zhcy": "ж",
- "zigrarr": "⇝",
- "zopf": "𝕫",
- "Zopf": "ℤ",
- "Zscr": "𝒵",
- "zscr": "𝓏",
- "zwj": "",
- "zwnj": ""
- };
-
-/***/ },
-/* 20 */
-/***/ function(module, exports) {
-
- module.exports = {
- "Aacute": "Á",
- "aacute": "á",
- "Acirc": "Â",
- "acirc": "â",
- "acute": "´",
- "AElig": "Æ",
- "aelig": "æ",
- "Agrave": "À",
- "agrave": "à",
- "amp": "&",
- "AMP": "&",
- "Aring": "Å",
- "aring": "å",
- "Atilde": "Ã",
- "atilde": "ã",
- "Auml": "Ä",
- "auml": "ä",
- "brvbar": "¦",
- "Ccedil": "Ç",
- "ccedil": "ç",
- "cedil": "¸",
- "cent": "¢",
- "copy": "©",
- "COPY": "©",
- "curren": "¤",
- "deg": "°",
- "divide": "÷",
- "Eacute": "É",
- "eacute": "é",
- "Ecirc": "Ê",
- "ecirc": "ê",
- "Egrave": "È",
- "egrave": "è",
- "ETH": "Ð",
- "eth": "ð",
- "Euml": "Ë",
- "euml": "ë",
- "frac12": "½",
- "frac14": "¼",
- "frac34": "¾",
- "gt": ">",
- "GT": ">",
- "Iacute": "Í",
- "iacute": "í",
- "Icirc": "Î",
- "icirc": "î",
- "iexcl": "¡",
- "Igrave": "Ì",
- "igrave": "ì",
- "iquest": "¿",
- "Iuml": "Ï",
- "iuml": "ï",
- "laquo": "«",
- "lt": "<",
- "LT": "<",
- "macr": "¯",
- "micro": "µ",
- "middot": "·",
- "nbsp": " ",
- "not": "¬",
- "Ntilde": "Ñ",
- "ntilde": "ñ",
- "Oacute": "Ó",
- "oacute": "ó",
- "Ocirc": "Ô",
- "ocirc": "ô",
- "Ograve": "Ò",
- "ograve": "ò",
- "ordf": "ª",
- "ordm": "º",
- "Oslash": "Ø",
- "oslash": "ø",
- "Otilde": "Õ",
- "otilde": "õ",
- "Ouml": "Ö",
- "ouml": "ö",
- "para": "¶",
- "plusmn": "±",
- "pound": "£",
- "quot": "\"",
- "QUOT": "\"",
- "raquo": "»",
- "reg": "®",
- "REG": "®",
- "sect": "§",
- "shy": "",
- "sup1": "¹",
- "sup2": "²",
- "sup3": "³",
- "szlig": "ß",
- "THORN": "Þ",
- "thorn": "þ",
- "times": "×",
- "Uacute": "Ú",
- "uacute": "ú",
- "Ucirc": "Û",
- "ucirc": "û",
- "Ugrave": "Ù",
- "ugrave": "ù",
- "uml": "¨",
- "Uuml": "Ü",
- "uuml": "ü",
- "Yacute": "Ý",
- "yacute": "ý",
- "yen": "¥",
- "yuml": "ÿ"
- };
-
-/***/ },
-/* 21 */
-/***/ function(module, exports) {
-
- module.exports = {
- "amp": "&",
- "apos": "'",
- "gt": ">",
- "lt": "<",
- "quot": "\""
- };
-
-/***/ },
-/* 22 */
-/***/ function(module, exports, __webpack_require__) {
-
- var ElementType = __webpack_require__(23);
-
- var re_whitespace = /\s+/g;
- var NodePrototype = __webpack_require__(24);
- var ElementPrototype = __webpack_require__(25);
-
- function DomHandler(callback, options, elementCB){
- if(typeof callback === "object"){
- elementCB = options;
- options = callback;
- callback = null;
- } else if(typeof options === "function"){
- elementCB = options;
- options = defaultOpts;
- }
- this._callback = callback;
- this._options = options || defaultOpts;
- this._elementCB = elementCB;
- this.dom = [];
- this._done = false;
- this._tagStack = [];
- this._parser = this._parser || null;
- }
-
- //default options
- var defaultOpts = {
- normalizeWhitespace: false, //Replace all whitespace with single spaces
- withStartIndices: false, //Add startIndex properties to nodes
- };
-
- DomHandler.prototype.onparserinit = function(parser){
- this._parser = parser;
- };
-
- //Resets the handler back to starting state
- DomHandler.prototype.onreset = function(){
- DomHandler.call(this, this._callback, this._options, this._elementCB);
- };
-
- //Signals the handler that parsing is done
- DomHandler.prototype.onend = function(){
- if(this._done) return;
- this._done = true;
- this._parser = null;
- this._handleCallback(null);
- };
-
- DomHandler.prototype._handleCallback =
- DomHandler.prototype.onerror = function(error){
- if(typeof this._callback === "function"){
- this._callback(error, this.dom);
- } else {
- if(error) throw error;
- }
- };
-
- DomHandler.prototype.onclosetag = function(){
- //if(this._tagStack.pop().name !== name) this._handleCallback(Error("Tagname didn't match!"));
- var elem = this._tagStack.pop();
- if(this._elementCB) this._elementCB(elem);
- };
-
- DomHandler.prototype._addDomElement = function(element){
- var parent = this._tagStack[this._tagStack.length - 1];
- var siblings = parent ? parent.children : this.dom;
- var previousSibling = siblings[siblings.length - 1];
-
- element.next = null;
-
- if(this._options.withStartIndices){
- element.startIndex = this._parser.startIndex;
- }
-
- if (this._options.withDomLvl1) {
- element.__proto__ = element.type === "tag" ? ElementPrototype : NodePrototype;
- }
-
- if(previousSibling){
- element.prev = previousSibling;
- previousSibling.next = element;
- } else {
- element.prev = null;
- }
-
- siblings.push(element);
- element.parent = parent || null;
- };
-
- DomHandler.prototype.onopentag = function(name, attribs){
- var element = {
- type: name === "script" ? ElementType.Script : name === "style" ? ElementType.Style : ElementType.Tag,
- name: name,
- attribs: attribs,
- children: []
- };
-
- this._addDomElement(element);
-
- this._tagStack.push(element);
- };
-
- DomHandler.prototype.ontext = function(data){
- //the ignoreWhitespace is officially dropped, but for now,
- //it's an alias for normalizeWhitespace
- var normalize = this._options.normalizeWhitespace || this._options.ignoreWhitespace;
-
- var lastTag;
-
- if(!this._tagStack.length && this.dom.length && (lastTag = this.dom[this.dom.length-1]).type === ElementType.Text){
- if(normalize){
- lastTag.data = (lastTag.data + data).replace(re_whitespace, " ");
- } else {
- lastTag.data += data;
- }
- } else {
- if(
- this._tagStack.length &&
- (lastTag = this._tagStack[this._tagStack.length - 1]) &&
- (lastTag = lastTag.children[lastTag.children.length - 1]) &&
- lastTag.type === ElementType.Text
- ){
- if(normalize){
- lastTag.data = (lastTag.data + data).replace(re_whitespace, " ");
- } else {
- lastTag.data += data;
- }
- } else {
- if(normalize){
- data = data.replace(re_whitespace, " ");
- }
-
- this._addDomElement({
- data: data,
- type: ElementType.Text
- });
- }
- }
- };
-
- DomHandler.prototype.oncomment = function(data){
- var lastTag = this._tagStack[this._tagStack.length - 1];
-
- if(lastTag && lastTag.type === ElementType.Comment){
- lastTag.data += data;
- return;
- }
-
- var element = {
- data: data,
- type: ElementType.Comment
- };
-
- this._addDomElement(element);
- this._tagStack.push(element);
- };
-
- DomHandler.prototype.oncdatastart = function(){
- var element = {
- children: [{
- data: "",
- type: ElementType.Text
- }],
- type: ElementType.CDATA
- };
-
- this._addDomElement(element);
- this._tagStack.push(element);
- };
-
- DomHandler.prototype.oncommentend = DomHandler.prototype.oncdataend = function(){
- this._tagStack.pop();
- };
-
- DomHandler.prototype.onprocessinginstruction = function(name, data){
- this._addDomElement({
- name: name,
- data: data,
- type: ElementType.Directive
- });
- };
-
- module.exports = DomHandler;
-
-
-/***/ },
-/* 23 */
-/***/ function(module, exports) {
-
- //Types of elements found in the DOM
- module.exports = {
- Text: "text", //Text
- Directive: "directive", // ... ?>
- Comment: "comment", //
- Script: "script", //