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 + + + + +
+

Scratch VM Playground

+ +
+ + +
+
+ Turbo: +
+
+ Pause: +
+
+ Compatibility (30 TPS): +
+
+ Single stepping: + +
+
+
+
+ Renderer
+
+
+
+ Thread explorer +

+        
+
+ Block explorer +

+        
+
+ Import/Export
+ Project ID: + +
+

+ +   + +

+ +

+
+
+ +
+ + + + + + + + + + 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 += ''; + 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 += ''; + 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", //