mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 22:42:27 -05:00
Merge pull request #1180 from kchadha/parse-sb2-comments
Import Comments from 2.0 Projects
This commit is contained in:
commit
ac91d8af65
12 changed files with 345 additions and 41 deletions
|
@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record');
|
||||||
const Clone = require('../util/clone');
|
const Clone = require('../util/clone');
|
||||||
const {Map} = require('immutable');
|
const {Map} = require('immutable');
|
||||||
const BlocksExecuteCache = require('./blocks-execute-cache');
|
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||||
|
const log = require('../util/log');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -734,19 +735,21 @@ class Blocks {
|
||||||
/**
|
/**
|
||||||
* Encode all of `this._blocks` as an XML string usable
|
* Encode all of `this._blocks` as an XML string usable
|
||||||
* by a Blockly/scratch-blocks workspace.
|
* by a Blockly/scratch-blocks workspace.
|
||||||
|
* @param {object<string, Comment>} comments Map of comments referenced by id
|
||||||
* @return {string} String of XML representing this object's blocks.
|
* @return {string} String of XML representing this object's blocks.
|
||||||
*/
|
*/
|
||||||
toXML () {
|
toXML (comments) {
|
||||||
return this._scripts.map(script => this.blockToXML(script)).join();
|
return this._scripts.map(script => this.blockToXML(script, comments)).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively encode an individual block and its children
|
* Recursively encode an individual block and its children
|
||||||
* into a Blockly/scratch-blocks XML string.
|
* into a Blockly/scratch-blocks XML string.
|
||||||
* @param {!string} blockId ID of block to encode.
|
* @param {!string} blockId ID of block to encode.
|
||||||
|
* @param {object<string, Comment>} comments Map of comments referenced by id
|
||||||
* @return {string} String of XML representing this block and any children.
|
* @return {string} String of XML representing this block and any children.
|
||||||
*/
|
*/
|
||||||
blockToXML (blockId) {
|
blockToXML (blockId, comments) {
|
||||||
const block = this._blocks[blockId];
|
const block = this._blocks[blockId];
|
||||||
// Encode properties of this block.
|
// Encode properties of this block.
|
||||||
const tagName = (block.shadow) ? 'shadow' : 'block';
|
const tagName = (block.shadow) ? 'shadow' : 'block';
|
||||||
|
@ -756,6 +759,18 @@ class Blocks {
|
||||||
type="${block.opcode}"
|
type="${block.opcode}"
|
||||||
${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''}
|
${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''}
|
||||||
>`;
|
>`;
|
||||||
|
const commentId = block.comment;
|
||||||
|
if (commentId) {
|
||||||
|
if (comments) {
|
||||||
|
if (comments.hasOwnProperty(commentId)) {
|
||||||
|
xmlString += comments[commentId].toXML();
|
||||||
|
} else {
|
||||||
|
log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Add any mutation. Must come before inputs.
|
// Add any mutation. Must come before inputs.
|
||||||
if (block.mutation) {
|
if (block.mutation) {
|
||||||
xmlString += this.mutationToXML(block.mutation);
|
xmlString += this.mutationToXML(block.mutation);
|
||||||
|
@ -768,11 +783,11 @@ class Blocks {
|
||||||
if (blockInput.block || blockInput.shadow) {
|
if (blockInput.block || blockInput.shadow) {
|
||||||
xmlString += `<value name="${blockInput.name}">`;
|
xmlString += `<value name="${blockInput.name}">`;
|
||||||
if (blockInput.block) {
|
if (blockInput.block) {
|
||||||
xmlString += this.blockToXML(blockInput.block);
|
xmlString += this.blockToXML(blockInput.block, comments);
|
||||||
}
|
}
|
||||||
if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
|
if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
|
||||||
// Obscured shadow.
|
// Obscured shadow.
|
||||||
xmlString += this.blockToXML(blockInput.shadow);
|
xmlString += this.blockToXML(blockInput.shadow, comments);
|
||||||
}
|
}
|
||||||
xmlString += '</value>';
|
xmlString += '</value>';
|
||||||
}
|
}
|
||||||
|
@ -798,7 +813,7 @@ class Blocks {
|
||||||
}
|
}
|
||||||
// Add blocks connected to the next connection.
|
// Add blocks connected to the next connection.
|
||||||
if (block.next) {
|
if (block.next) {
|
||||||
xmlString += `<next>${this.blockToXML(block.next)}</next>`;
|
xmlString += `<next>${this.blockToXML(block.next, comments)}</next>`;
|
||||||
}
|
}
|
||||||
xmlString += `</${tagName}>`;
|
xmlString += `</${tagName}>`;
|
||||||
return xmlString;
|
return xmlString;
|
||||||
|
|
56
src/engine/comment.js
Normal file
56
src/engine/comment.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* Object representing a Scratch Comment (block or workspace).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const uid = require('../util/uid');
|
||||||
|
const Cast = require('../util/cast');
|
||||||
|
|
||||||
|
class Comment {
|
||||||
|
/**
|
||||||
|
* @param {string} id Id of the comment.
|
||||||
|
* @param {string} text Text content of the comment.
|
||||||
|
* @param {number} x X position of the comment on the workspace.
|
||||||
|
* @param {number} y Y position of the comment on the workspace.
|
||||||
|
* @param {number} width The width of the comment when it is full size.
|
||||||
|
* @param {number} height The height of the comment when it is full size.
|
||||||
|
* @param {boolean} minimized Whether the comment is minimized.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor (id, text, x, y, width, height, minimized) {
|
||||||
|
this.id = id || uid();
|
||||||
|
this.text = text;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.width = Math.max(Cast.toNumber(width), Comment.MIN_WIDTH);
|
||||||
|
this.height = Math.max(Cast.toNumber(height), Comment.MIN_HEIGHT);
|
||||||
|
this.minimized = minimized || false;
|
||||||
|
this.blockId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toXML () {
|
||||||
|
return `<comment id="${this.id}" x="${this.x}" y="${
|
||||||
|
this.y}" w="${this.width}" h="${this.height}" pinned="${
|
||||||
|
this.blockId !== null}" minimized="${this.minimized}">${this.text}</comment>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO choose min and defaults for width and height
|
||||||
|
static get MIN_WIDTH () {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get MIN_HEIGHT () {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DEFAULT_WIDTH () {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get DEFAULT_HEIGHT () {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Comment;
|
|
@ -43,16 +43,16 @@ class Target extends EventEmitter {
|
||||||
this.blocks = blocks;
|
this.blocks = blocks;
|
||||||
/**
|
/**
|
||||||
* Dictionary of variables and their values for this target.
|
* Dictionary of variables and their values for this target.
|
||||||
* Key is the variable name.
|
* Key is the variable id.
|
||||||
* @type {Object.<string,*>}
|
* @type {Object.<string,*>}
|
||||||
*/
|
*/
|
||||||
this.variables = {};
|
this.variables = {};
|
||||||
/**
|
/**
|
||||||
* Dictionary of lists and their contents for this target.
|
* Dictionary of comments for this target.
|
||||||
* Key is the list name.
|
* Key is the comment id.
|
||||||
* @type {Object.<string,*>}
|
* @type {Object.<string,*>}
|
||||||
*/
|
*/
|
||||||
this.lists = {};
|
this.comments = {};
|
||||||
/**
|
/**
|
||||||
* Dictionary of custom state for this target.
|
* Dictionary of custom state for this target.
|
||||||
* This can be used to store target-specific custom state for blocks which need it.
|
* This can be used to store target-specific custom state for blocks which need it.
|
||||||
|
|
|
@ -13,6 +13,7 @@ const log = require('../util/log');
|
||||||
const uid = require('../util/uid');
|
const uid = require('../util/uid');
|
||||||
const StringUtil = require('../util/string-util');
|
const StringUtil = require('../util/string-util');
|
||||||
const specMap = require('./sb2_specmap');
|
const specMap = require('./sb2_specmap');
|
||||||
|
const Comment = require('../engine/comment');
|
||||||
const Variable = require('../engine/variable');
|
const Variable = require('../engine/variable');
|
||||||
const MonitorRecord = require('../engine/monitor-record');
|
const MonitorRecord = require('../engine/monitor-record');
|
||||||
const StageLayering = require('../engine/stage-layering');
|
const StageLayering = require('../engine/stage-layering');
|
||||||
|
@ -36,6 +37,12 @@ const CORE_EXTENSIONS = [
|
||||||
'sound'
|
'sound'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Adjust script coordinates to account for
|
||||||
|
// larger block size in scratch-blocks.
|
||||||
|
// @todo: Determine more precisely the right formulas here.
|
||||||
|
const WORKSPACE_X_SCALE = 1.5;
|
||||||
|
const WORKSPACE_Y_SCALE = 2.2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
|
* 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
|
* into an argument map. This allows us to provide the expected inputs
|
||||||
|
@ -110,15 +117,26 @@ const flatten = function (blocks) {
|
||||||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
* @return {Array.<object>} Scratch VM-format block list.
|
* @param {object<int, Comment>} comments - Comments from sb2 project that need to be attached to blocks.
|
||||||
|
* They are indexed in this object by the sb2 flattened block list index indicating
|
||||||
|
* which block they should attach to.
|
||||||
|
* @param {int} commentIndex The current index of the top block in this list if it were in a flattened
|
||||||
|
* list of all blocks for the target
|
||||||
|
* @return {[Array.<object>, int]} Tuple where first item is the Scratch VM-format block list, and
|
||||||
|
* second item is the updated comment index
|
||||||
*/
|
*/
|
||||||
const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions) {
|
const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) {
|
||||||
const resultingList = [];
|
const resultingList = [];
|
||||||
let previousBlock = null; // For setting next.
|
let previousBlock = null; // For setting next.
|
||||||
for (let i = 0; i < blockList.length; i++) {
|
for (let i = 0; i < blockList.length; i++) {
|
||||||
const block = blockList[i];
|
const block = blockList[i];
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
const parsedBlock = parseBlock(block, addBroadcastMsg, getVariableId, extensions);
|
const parsedBlockAndComments = parseBlock(block, addBroadcastMsg, getVariableId,
|
||||||
|
extensions, comments, commentIndex);
|
||||||
|
const parsedBlock = parsedBlockAndComments[0];
|
||||||
|
// Update commentIndex
|
||||||
|
commentIndex = parsedBlockAndComments[1];
|
||||||
|
|
||||||
if (typeof parsedBlock === 'undefined') continue;
|
if (typeof parsedBlock === 'undefined') continue;
|
||||||
if (previousBlock) {
|
if (previousBlock) {
|
||||||
parsedBlock.parent = previousBlock.id;
|
parsedBlock.parent = previousBlock.id;
|
||||||
|
@ -127,7 +145,7 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte
|
||||||
previousBlock = parsedBlock;
|
previousBlock = parsedBlock;
|
||||||
resultingList.push(parsedBlock);
|
resultingList.push(parsedBlock);
|
||||||
}
|
}
|
||||||
return resultingList;
|
return [resultingList, commentIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -138,20 +156,24 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte
|
||||||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
|
* @param {object} comments Comments that need to be attached to the blocks that need to be parsed
|
||||||
*/
|
*/
|
||||||
const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions) {
|
const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions, comments) {
|
||||||
|
// Keep track of the index of the current script being
|
||||||
|
// parsed in order to attach block comments correctly
|
||||||
|
let scriptIndexForComment = 0;
|
||||||
|
|
||||||
for (let i = 0; i < scripts.length; i++) {
|
for (let i = 0; i < scripts.length; i++) {
|
||||||
const script = scripts[i];
|
const script = scripts[i];
|
||||||
const scriptX = script[0];
|
const scriptX = script[0];
|
||||||
const scriptY = script[1];
|
const scriptY = script[1];
|
||||||
const blockList = script[2];
|
const blockList = script[2];
|
||||||
const parsedBlockList = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions);
|
const [parsedBlockList, newCommentIndex] = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions,
|
||||||
|
comments, scriptIndexForComment);
|
||||||
|
scriptIndexForComment = newCommentIndex;
|
||||||
if (parsedBlockList[0]) {
|
if (parsedBlockList[0]) {
|
||||||
// Adjust script coordinates to account for
|
parsedBlockList[0].x = scriptX * WORKSPACE_X_SCALE;
|
||||||
// larger block size in scratch-blocks.
|
parsedBlockList[0].y = scriptY * WORKSPACE_Y_SCALE;
|
||||||
// @todo: Determine more precisely the right formulas here.
|
|
||||||
parsedBlockList[0].x = scriptX * 1.5;
|
|
||||||
parsedBlockList[0].y = scriptY * 2.2;
|
|
||||||
parsedBlockList[0].topLevel = true;
|
parsedBlockList[0].topLevel = true;
|
||||||
parsedBlockList[0].parent = null;
|
parsedBlockList[0].parent = null;
|
||||||
}
|
}
|
||||||
|
@ -242,11 +264,13 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
|
||||||
// Create var id getter to make block naming/parsing easier, variables already created.
|
// Create var id getter to make block naming/parsing easier, variables already created.
|
||||||
const getVariableId = generateVariableIdGetter(target.id, false);
|
const getVariableId = generateVariableIdGetter(target.id, false);
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
const block = parseBlock(
|
const [block, _] = parseBlock(
|
||||||
[object.cmd, object.param], // Scratch 2 monitor blocks only have one param.
|
[object.cmd, object.param], // Scratch 2 monitor blocks only have one param.
|
||||||
null, // `addBroadcastMsg`, not needed for monitor blocks.
|
null, // `addBroadcastMsg`, not needed for monitor blocks.
|
||||||
getVariableId,
|
getVariableId,
|
||||||
extensions
|
extensions,
|
||||||
|
null, // `comments`, not needed for monitor blocks
|
||||||
|
null // `commentIndex`, not needed for monitor blocks
|
||||||
);
|
);
|
||||||
|
|
||||||
// Monitor blocks have special IDs to match the toolbox obtained from the getId
|
// Monitor blocks have special IDs to match the toolbox obtained from the getId
|
||||||
|
@ -423,9 +447,63 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If included, parse any and all comments on the object (this includes top-level
|
||||||
|
// workspace comments as well as comments attached to specific blocks)
|
||||||
|
const blockComments = {};
|
||||||
|
if (object.hasOwnProperty('scriptComments')) {
|
||||||
|
const comments = object.scriptComments.map(commentDesc => {
|
||||||
|
const [
|
||||||
|
commentX,
|
||||||
|
commentY,
|
||||||
|
commentWidth,
|
||||||
|
commentHeight,
|
||||||
|
commentFullSize,
|
||||||
|
flattenedBlockIndex,
|
||||||
|
commentText
|
||||||
|
] = commentDesc;
|
||||||
|
const isBlockComment = commentDesc[5] >= 0;
|
||||||
|
const newComment = new Comment(
|
||||||
|
null, // generate a new id for this comment
|
||||||
|
commentText, // text content of sb2 comment
|
||||||
|
// Only serialize x & y position of comment if it's a workspace comment
|
||||||
|
// If it's a block comment, we'll let scratch-blocks handle positioning
|
||||||
|
isBlockComment ? null : commentX * WORKSPACE_X_SCALE,
|
||||||
|
isBlockComment ? null : commentY * WORKSPACE_Y_SCALE,
|
||||||
|
commentWidth * WORKSPACE_X_SCALE,
|
||||||
|
commentHeight * WORKSPACE_Y_SCALE,
|
||||||
|
!commentFullSize
|
||||||
|
);
|
||||||
|
if (isBlockComment) {
|
||||||
|
// commentDesc[5] refers to the index of the block that this
|
||||||
|
// comment is attached to -- in a flattened version of the
|
||||||
|
// scripts array.
|
||||||
|
// If commentDesc[5] is -1, this is a workspace comment (we don't need to do anything
|
||||||
|
// extra at this point), otherwise temporarily save the flattened script array
|
||||||
|
// index as the blockId property of the new comment. We will
|
||||||
|
// change this to refer to the actual block id of the corresponding
|
||||||
|
// block when that block gets created
|
||||||
|
newComment.blockId = flattenedBlockIndex;
|
||||||
|
// Add this comment to the block comments object with its script index
|
||||||
|
// as the key
|
||||||
|
if (blockComments.hasOwnProperty(flattenedBlockIndex)) {
|
||||||
|
blockComments[flattenedBlockIndex].push(newComment);
|
||||||
|
} else {
|
||||||
|
blockComments[flattenedBlockIndex] = [newComment];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newComment;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add all the comments that were just created to the target.comments,
|
||||||
|
// referenced by id
|
||||||
|
comments.forEach(comment => {
|
||||||
|
target.comments[comment.id] = comment;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If included, parse any and all scripts/blocks on the object.
|
// If included, parse any and all scripts/blocks on the object.
|
||||||
if (object.hasOwnProperty('scripts')) {
|
if (object.hasOwnProperty('scripts')) {
|
||||||
parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions);
|
parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stage specific blocks (e.g. sprite clicked <=> stage clicked)
|
// Update stage specific blocks (e.g. sprite clicked <=> stage clicked)
|
||||||
|
@ -619,9 +697,15 @@ const specMapBlock = function (block) {
|
||||||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||||
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
||||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||||
* @return {object} Scratch VM format block, or null if unsupported object.
|
* @param {object<int, Comment>} comments - Comments from sb2 project that need to be attached to blocks.
|
||||||
|
* They are indexed in this object by the sb2 flattened block list index indicating
|
||||||
|
* which block they should attach to.
|
||||||
|
* @param {int} commentIndex The comment index for the block to be parsed if it were in a flattened
|
||||||
|
* list of all blocks for the target
|
||||||
|
* @return {[object, int]} Tuple where first item is the Scratch VM-format block (or null if unsupported object),
|
||||||
|
* and second item is the updated comment index (after this block and its children are parsed)
|
||||||
*/
|
*/
|
||||||
const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions) {
|
const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions, comments, commentIndex) {
|
||||||
const blockMetadata = specMapBlock(sb2block);
|
const blockMetadata = specMapBlock(sb2block);
|
||||||
if (!blockMetadata) {
|
if (!blockMetadata) {
|
||||||
return;
|
return;
|
||||||
|
@ -645,6 +729,21 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
||||||
shadow: false, // No shadow blocks in an SB2 by default.
|
shadow: false, // No shadow blocks in an SB2 by default.
|
||||||
children: [] // Store any generated children, flattened in `flatten`.
|
children: [] // Store any generated children, flattened in `flatten`.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Attach any comments to this block..
|
||||||
|
const commentsForParsedBlock = (comments && typeof commentIndex === 'number' && !isNaN(commentIndex)) ?
|
||||||
|
comments[commentIndex] : null;
|
||||||
|
if (commentsForParsedBlock) {
|
||||||
|
// TODO currently only attaching the last comment to the block if there are multiple...
|
||||||
|
// not sure what to do here.. concatenate all the messages in all the comments and only
|
||||||
|
// keep one around?
|
||||||
|
activeBlock.comment = commentsForParsedBlock[commentsForParsedBlock.length - 1].id;
|
||||||
|
commentsForParsedBlock.forEach(comment => {
|
||||||
|
comment.blockId = activeBlock.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
commentIndex++;
|
||||||
|
|
||||||
// For a procedure call, generate argument map from proc string.
|
// For a procedure call, generate argument map from proc string.
|
||||||
if (oldOpcode === 'call') {
|
if (oldOpcode === 'call') {
|
||||||
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
|
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
|
||||||
|
@ -671,10 +770,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
||||||
let innerBlocks;
|
let innerBlocks;
|
||||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||||
// Block list occupies the input.
|
// Block list occupies the input.
|
||||||
innerBlocks = parseBlockList(providedArg, addBroadcastMsg, getVariableId, extensions);
|
[innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId,
|
||||||
|
extensions, comments, commentIndex);
|
||||||
} else {
|
} else {
|
||||||
// Single block occupies the input.
|
// Single block occupies the input.
|
||||||
innerBlocks = [parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions)];
|
const parsedBlockDesc = parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions,
|
||||||
|
comments, commentIndex);
|
||||||
|
innerBlocks = [parsedBlockDesc[0]];
|
||||||
|
// Update commentIndex
|
||||||
|
commentIndex = parsedBlockDesc[1];
|
||||||
}
|
}
|
||||||
let previousBlock = null;
|
let previousBlock = null;
|
||||||
for (let j = 0; j < innerBlocks.length; j++) {
|
for (let j = 0; j < innerBlocks.length; j++) {
|
||||||
|
@ -914,7 +1018,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return activeBlock;
|
return [activeBlock, commentIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -930,7 +930,6 @@ class RenderedTarget extends Target {
|
||||||
newClone.rotationStyle = this.rotationStyle;
|
newClone.rotationStyle = this.rotationStyle;
|
||||||
newClone.effects = JSON.parse(JSON.stringify(this.effects));
|
newClone.effects = JSON.parse(JSON.stringify(this.effects));
|
||||||
newClone.variables = JSON.parse(JSON.stringify(this.variables));
|
newClone.variables = JSON.parse(JSON.stringify(this.variables));
|
||||||
newClone.lists = JSON.parse(JSON.stringify(this.lists));
|
|
||||||
newClone.initDrawable(StageLayering.SPRITE_LAYER);
|
newClone.initDrawable(StageLayering.SPRITE_LAYER);
|
||||||
newClone.updateAllDrawableProperties();
|
newClone.updateAllDrawableProperties();
|
||||||
// Place behind the current target.
|
// Place behind the current target.
|
||||||
|
@ -957,7 +956,6 @@ class RenderedTarget extends Target {
|
||||||
newTarget.rotationStyle = this.rotationStyle;
|
newTarget.rotationStyle = this.rotationStyle;
|
||||||
newTarget.effects = JSON.parse(JSON.stringify(this.effects));
|
newTarget.effects = JSON.parse(JSON.stringify(this.effects));
|
||||||
newTarget.variables = JSON.parse(JSON.stringify(this.variables));
|
newTarget.variables = JSON.parse(JSON.stringify(this.variables));
|
||||||
newTarget.lists = JSON.parse(JSON.stringify(this.lists));
|
|
||||||
newTarget.updateAllDrawableProperties();
|
newTarget.updateAllDrawableProperties();
|
||||||
newTarget.goBehindOther(this);
|
newTarget.goBehindOther(this);
|
||||||
return newTarget;
|
return newTarget;
|
||||||
|
@ -1049,7 +1047,6 @@ class RenderedTarget extends Target {
|
||||||
rotationStyle: this.rotationStyle,
|
rotationStyle: this.rotationStyle,
|
||||||
blocks: this.blocks._blocks,
|
blocks: this.blocks._blocks,
|
||||||
variables: this.variables,
|
variables: this.variables,
|
||||||
lists: this.lists,
|
|
||||||
costumes: costumes,
|
costumes: costumes,
|
||||||
sounds: this.getSounds(),
|
sounds: this.getSounds(),
|
||||||
tempo: this.tempo,
|
tempo: this.tempo,
|
||||||
|
|
|
@ -1005,12 +1005,16 @@ class VirtualMachine extends EventEmitter {
|
||||||
);
|
);
|
||||||
|
|
||||||
const variables = Object.keys(variableMap).map(k => variableMap[k]);
|
const variables = Object.keys(variableMap).map(k => variableMap[k]);
|
||||||
|
const workspaceComments = Object.keys(this.editingTarget.comments)
|
||||||
|
.map(k => this.editingTarget.comments[k])
|
||||||
|
.filter(c => c.blockId === null);
|
||||||
|
|
||||||
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<variables>
|
<variables>
|
||||||
${variables.map(v => v.toXML()).join()}
|
${variables.map(v => v.toXML()).join()}
|
||||||
</variables>
|
</variables>
|
||||||
${this.editingTarget.blocks.toXML()}
|
${workspaceComments.map(c => c.toXML()).join()}
|
||||||
|
${this.editingTarget.blocks.toXML(this.editingTarget.comments)}
|
||||||
</xml>`;
|
</xml>`;
|
||||||
|
|
||||||
this.emit('workspaceUpdate', {xml: xmlString});
|
this.emit('workspaceUpdate', {xml: xmlString});
|
||||||
|
|
BIN
test/fixtures/comments.sb2
vendored
Normal file
BIN
test/fixtures/comments.sb2
vendored
Normal file
Binary file not shown.
91
test/integration/comments.js
Normal file
91
test/integration/comments.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
const path = require('path');
|
||||||
|
const test = require('tap').test;
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
test('importing sb2 project with comments', t => {
|
||||||
|
const vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(makeTestStorage());
|
||||||
|
|
||||||
|
// Evaluate playground data and exit
|
||||||
|
vm.on('playgroundData', e => {
|
||||||
|
const threads = JSON.parse(e.threads);
|
||||||
|
t.equal(threads.length, 0);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
const target = vm.runtime.targets[1];
|
||||||
|
|
||||||
|
const stageComments = Object.values(stage.comments);
|
||||||
|
|
||||||
|
// Stage has 1 comment, and it is minimized.
|
||||||
|
t.equal(stageComments.length, 1);
|
||||||
|
t.equal(stageComments[0].minimized, true);
|
||||||
|
t.equal(stageComments[0].text, 'A minimized stage comment.');
|
||||||
|
// The stage comment is a workspace comment
|
||||||
|
t.equal(stageComments[0].blockId, null);
|
||||||
|
|
||||||
|
// Sprite 1 has 6 Comments, 1 workspace comment, and 5 block comments
|
||||||
|
const targetComments = Object.values(target.comments);
|
||||||
|
t.equal(targetComments.length, 6);
|
||||||
|
const spriteWorkspaceComments = targetComments.filter(comment => comment.blockId === null);
|
||||||
|
t.equal(spriteWorkspaceComments.length, 1);
|
||||||
|
t.equal(spriteWorkspaceComments[0].minimized, false);
|
||||||
|
t.equal(spriteWorkspaceComments[0].text, 'This is a workspace comment.');
|
||||||
|
|
||||||
|
// Test the sprite block comments
|
||||||
|
const blockComments = targetComments.filter(comment => !!comment.blockId);
|
||||||
|
t.equal(blockComments.length, 5);
|
||||||
|
|
||||||
|
t.equal(blockComments[0].minimized, true);
|
||||||
|
t.equal(blockComments[0].text, '1. Green Flag Comment.');
|
||||||
|
const greenFlagBlock = target.blocks.getBlock(blockComments[0].blockId);
|
||||||
|
t.equal(greenFlagBlock.comment, blockComments[0].id);
|
||||||
|
t.equal(greenFlagBlock.opcode, 'event_whenflagclicked');
|
||||||
|
|
||||||
|
t.equal(blockComments[1].minimized, true);
|
||||||
|
t.equal(blockComments[1].text, '2. Turn 15 Degrees Comment.');
|
||||||
|
const turnRightBlock = target.blocks.getBlock(blockComments[1].blockId);
|
||||||
|
t.equal(turnRightBlock.comment, blockComments[1].id);
|
||||||
|
t.equal(turnRightBlock.opcode, 'motion_turnright');
|
||||||
|
|
||||||
|
t.equal(blockComments[2].minimized, false);
|
||||||
|
t.equal(blockComments[2].text, '3. Comment for a loop.');
|
||||||
|
const repeatBlock = target.blocks.getBlock(blockComments[2].blockId);
|
||||||
|
t.equal(repeatBlock.comment, blockComments[2].id);
|
||||||
|
t.equal(repeatBlock.opcode, 'control_repeat');
|
||||||
|
|
||||||
|
t.equal(blockComments[3].minimized, false);
|
||||||
|
t.equal(blockComments[3].text, '4. Comment for a block nested in a loop.');
|
||||||
|
const changeColorBlock = target.blocks.getBlock(blockComments[3].blockId);
|
||||||
|
t.equal(changeColorBlock.comment, blockComments[3].id);
|
||||||
|
t.equal(changeColorBlock.opcode, 'looks_changeeffectby');
|
||||||
|
|
||||||
|
t.equal(blockComments[4].minimized, false);
|
||||||
|
t.equal(blockComments[4].text, '5. Comment for a block outside of a loop.');
|
||||||
|
const stopAllBlock = target.blocks.getBlock(blockComments[4].blockId);
|
||||||
|
t.equal(stopAllBlock.comment, blockComments[4].id);
|
||||||
|
t.equal(stopAllBlock.opcode, 'control_stop');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start VM, load project, and run
|
||||||
|
t.doesNotThrow(() => {
|
||||||
|
vm.start();
|
||||||
|
vm.clear();
|
||||||
|
vm.setCompatibilityMode(false);
|
||||||
|
vm.setTurboMode(false);
|
||||||
|
vm.loadProject(project).then(() => {
|
||||||
|
vm.greenFlag();
|
||||||
|
setTimeout(() => {
|
||||||
|
vm.getPlaygroundData();
|
||||||
|
vm.stopAll();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -30,7 +30,7 @@ test('default', t => {
|
||||||
t.type(targets[0].id, 'string');
|
t.type(targets[0].id, 'string');
|
||||||
t.type(targets[0].blocks, 'object');
|
t.type(targets[0].blocks, 'object');
|
||||||
t.type(targets[0].variables, 'object');
|
t.type(targets[0].variables, 'object');
|
||||||
t.type(targets[0].lists, 'object');
|
t.type(targets[0].comments, 'object');
|
||||||
|
|
||||||
t.equal(targets[0].isOriginal, true);
|
t.equal(targets[0].isOriginal, true);
|
||||||
t.equal(targets[0].currentCostume, 0);
|
t.equal(targets[0].currentCostume, 0);
|
||||||
|
@ -41,7 +41,7 @@ test('default', t => {
|
||||||
t.type(targets[1].id, 'string');
|
t.type(targets[1].id, 'string');
|
||||||
t.type(targets[1].blocks, 'object');
|
t.type(targets[1].blocks, 'object');
|
||||||
t.type(targets[1].variables, 'object');
|
t.type(targets[1].variables, 'object');
|
||||||
t.type(targets[1].lists, 'object');
|
t.type(targets[1].comments, 'object');
|
||||||
|
|
||||||
t.equal(targets[1].isOriginal, true);
|
t.equal(targets[1].isOriginal, true);
|
||||||
t.equal(targets[1].currentCostume, 0);
|
t.equal(targets[1].currentCostume, 0);
|
||||||
|
|
|
@ -12,7 +12,7 @@ test('spec', t => {
|
||||||
t.type(target.id, 'string');
|
t.type(target.id, 'string');
|
||||||
t.type(target.blocks, 'object');
|
t.type(target.blocks, 'object');
|
||||||
t.type(target.variables, 'object');
|
t.type(target.variables, 'object');
|
||||||
t.type(target.lists, 'object');
|
t.type(target.comments, 'object');
|
||||||
t.type(target._customState, 'object');
|
t.type(target._customState, 'object');
|
||||||
|
|
||||||
t.type(target.createVariable, 'function');
|
t.type(target.createVariable, 'function');
|
||||||
|
|
|
@ -29,7 +29,7 @@ test('default', t => {
|
||||||
t.type(targets[0].id, 'string');
|
t.type(targets[0].id, 'string');
|
||||||
t.type(targets[0].blocks, 'object');
|
t.type(targets[0].blocks, 'object');
|
||||||
t.type(targets[0].variables, 'object');
|
t.type(targets[0].variables, 'object');
|
||||||
t.type(targets[0].lists, 'object');
|
t.type(targets[0].comments, 'object');
|
||||||
|
|
||||||
t.equal(targets[0].isOriginal, true);
|
t.equal(targets[0].isOriginal, true);
|
||||||
t.equal(targets[0].currentCostume, 0);
|
t.equal(targets[0].currentCostume, 0);
|
||||||
|
@ -40,7 +40,7 @@ test('default', t => {
|
||||||
t.type(targets[1].id, 'string');
|
t.type(targets[1].id, 'string');
|
||||||
t.type(targets[1].blocks, 'object');
|
t.type(targets[1].blocks, 'object');
|
||||||
t.type(targets[1].variables, 'object');
|
t.type(targets[1].variables, 'object');
|
||||||
t.type(targets[1].lists, 'object');
|
t.type(targets[1].comments, 'object');
|
||||||
|
|
||||||
t.equal(targets[1].isOriginal, true);
|
t.equal(targets[1].isOriginal, true);
|
||||||
t.equal(targets[1].currentCostume, 0);
|
t.equal(targets[1].currentCostume, 0);
|
||||||
|
|
|
@ -307,6 +307,17 @@ test('duplicateSprite assigns duplicated sprite a fresh name', t => {
|
||||||
|
|
||||||
test('emitWorkspaceUpdate', t => {
|
test('emitWorkspaceUpdate', t => {
|
||||||
const vm = new VirtualMachine();
|
const vm = new VirtualMachine();
|
||||||
|
const blocksToXML = comments => {
|
||||||
|
let blockString = 'blocks\n';
|
||||||
|
if (comments) {
|
||||||
|
for (const commentId in comments) {
|
||||||
|
const comment = comments[commentId];
|
||||||
|
blockString += `A Block Comment: ${comment.toXML()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return blockString;
|
||||||
|
};
|
||||||
vm.runtime.targets = [
|
vm.runtime.targets = [
|
||||||
{
|
{
|
||||||
isStage: true,
|
isStage: true,
|
||||||
|
@ -316,7 +327,13 @@ test('emitWorkspaceUpdate', t => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
toXML: () => 'blocks'
|
toXML: blocksToXML
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
aStageComment: {
|
||||||
|
toXML: () => 'aStageComment',
|
||||||
|
blockId: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -325,7 +342,13 @@ test('emitWorkspaceUpdate', t => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
toXML: () => 'blocks'
|
toXML: blocksToXML
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
someBlockComment: {
|
||||||
|
toXML: () => 'someBlockComment',
|
||||||
|
blockId: 'someBlockId'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -334,7 +357,17 @@ test('emitWorkspaceUpdate', t => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
blocks: {
|
blocks: {
|
||||||
toXML: () => 'blocks'
|
toXML: blocksToXML
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
someOtherComment: {
|
||||||
|
toXML: () => 'someOtherComment',
|
||||||
|
blockId: null
|
||||||
|
},
|
||||||
|
aBlockComment: {
|
||||||
|
toXML: () => 'aBlockComment',
|
||||||
|
blockId: 'a block'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -347,6 +380,10 @@ test('emitWorkspaceUpdate', t => {
|
||||||
t.notEqual(xml.indexOf('local'), -1);
|
t.notEqual(xml.indexOf('local'), -1);
|
||||||
t.equal(xml.indexOf('unused'), -1);
|
t.equal(xml.indexOf('unused'), -1);
|
||||||
t.notEqual(xml.indexOf('blocks'), -1);
|
t.notEqual(xml.indexOf('blocks'), -1);
|
||||||
|
t.equal(xml.indexOf('aStageComment'), -1);
|
||||||
|
t.equal(xml.indexOf('someBlockComment'), -1);
|
||||||
|
t.notEqual(xml.indexOf('someOtherComment'), -1);
|
||||||
|
t.notEqual(xml.indexOf('A Block Comment: aBlockComment'), -1);
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue