mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-08-10 21:39:57 -04:00
Parse SB2 comments and attach block comments to the blocks they belong to. Send comment xml on workspace update so they can be rendered.
This commit is contained in:
parent
057e89b1e0
commit
1401d54add
5 changed files with 183 additions and 25 deletions
src
|
@ -5,6 +5,7 @@ const MonitorRecord = require('./monitor-record');
|
|||
const Clone = require('../util/clone');
|
||||
const {Map} = require('immutable');
|
||||
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
|
@ -736,8 +737,8 @@ class Blocks {
|
|||
* by a Blockly/scratch-blocks workspace.
|
||||
* @return {string} String of XML representing this object's blocks.
|
||||
*/
|
||||
toXML () {
|
||||
return this._scripts.map(script => this.blockToXML(script)).join();
|
||||
toXML (comments) {
|
||||
return this._scripts.map(script => this.blockToXML(script, comments)).join();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -746,7 +747,7 @@ class Blocks {
|
|||
* @param {!string} blockId ID of block to encode.
|
||||
* @return {string} String of XML representing this block and any children.
|
||||
*/
|
||||
blockToXML (blockId) {
|
||||
blockToXML (blockId, comments) {
|
||||
const block = this._blocks[blockId];
|
||||
// Encode properties of this block.
|
||||
const tagName = (block.shadow) ? 'shadow' : 'block';
|
||||
|
@ -756,6 +757,18 @@ class Blocks {
|
|||
type="${block.opcode}"
|
||||
${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.
|
||||
if (block.mutation) {
|
||||
xmlString += this.mutationToXML(block.mutation);
|
||||
|
@ -768,11 +781,11 @@ class Blocks {
|
|||
if (blockInput.block || blockInput.shadow) {
|
||||
xmlString += `<value name="${blockInput.name}">`;
|
||||
if (blockInput.block) {
|
||||
xmlString += this.blockToXML(blockInput.block);
|
||||
xmlString += this.blockToXML(blockInput.block, comments);
|
||||
}
|
||||
if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
|
||||
// Obscured shadow.
|
||||
xmlString += this.blockToXML(blockInput.shadow);
|
||||
xmlString += this.blockToXML(blockInput.shadow, comments);
|
||||
}
|
||||
xmlString += '</value>';
|
||||
}
|
||||
|
@ -798,7 +811,7 @@ class Blocks {
|
|||
}
|
||||
// Add blocks connected to the next connection.
|
||||
if (block.next) {
|
||||
xmlString += `<next>${this.blockToXML(block.next)}</next>`;
|
||||
xmlString += `<next>${this.blockToXML(block.next, comments)}</next>`;
|
||||
}
|
||||
xmlString += `</${tagName}>`;
|
||||
return xmlString;
|
||||
|
|
52
src/engine/comment.js
Normal file
52
src/engine/comment.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @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 variable.
|
||||
* @param {string} name Name of the variable.
|
||||
* @param {string} type Type of the variable, one of '' or 'list'
|
||||
* @param {boolean} isCloud Whether the variable is stored in the cloud.
|
||||
* @constructor
|
||||
*/ /* TODO should the comment constructor take in an id? will we need this for sb3? */
|
||||
constructor (id, text, x, y, width, height, minimized) {
|
||||
this.id = id || uid();
|
||||
this.text = text;
|
||||
this.x = cast.toNumber(x);
|
||||
this.y = cast.toNumber(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}">${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;
|
||||
/**
|
||||
* Dictionary of variables and their values for this target.
|
||||
* Key is the variable name.
|
||||
* Key is the variable id.
|
||||
* @type {Object.<string,*>}
|
||||
*/
|
||||
this.variables = {};
|
||||
/**
|
||||
* Dictionary of lists and their contents for this target.
|
||||
* Key is the list name.
|
||||
* Dictionary of comments for this target.
|
||||
* Key is the comment id.
|
||||
* @type {Object.<string,*>}
|
||||
*/
|
||||
this.lists = {};
|
||||
this.comments = {};
|
||||
/**
|
||||
* Dictionary of custom state for this target.
|
||||
* 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 StringUtil = require('../util/string-util');
|
||||
const specMap = require('./sb2_specmap');
|
||||
const Comment = require('../engine/comment');
|
||||
const Variable = require('../engine/variable');
|
||||
const MonitorRecord = require('../engine/monitor-record');
|
||||
const StageLayering = require('../engine/stage-layering');
|
||||
|
@ -110,15 +111,26 @@ const flatten = function (blocks) {
|
|||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||
* @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.
|
||||
* @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 = [];
|
||||
let previousBlock = null; // For setting next.
|
||||
for (let i = 0; i < blockList.length; i++) {
|
||||
const block = blockList[i];
|
||||
// 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 (previousBlock) {
|
||||
parsedBlock.parent = previousBlock.id;
|
||||
|
@ -127,7 +139,7 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte
|
|||
previousBlock = parsedBlock;
|
||||
resultingList.push(parsedBlock);
|
||||
}
|
||||
return resultingList;
|
||||
return [resultingList, commentIndex];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -138,14 +150,21 @@ const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, exte
|
|||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||
* @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 {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++) {
|
||||
const script = scripts[i];
|
||||
const scriptX = script[0];
|
||||
const scriptY = script[1];
|
||||
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]) {
|
||||
// Adjust script coordinates to account for
|
||||
// larger block size in scratch-blocks.
|
||||
|
@ -242,11 +261,13 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
|
|||
// Create var id getter to make block naming/parsing easier, variables already created.
|
||||
const getVariableId = generateVariableIdGetter(target.id, false);
|
||||
// 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.
|
||||
null, // `addBroadcastMsg`, not needed for monitor blocks.
|
||||
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
|
||||
|
@ -423,9 +444,51 @@ 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 newComment = new Comment(
|
||||
null, // generate a new id for this comment
|
||||
commentDesc[6], // text content of sb2 comment
|
||||
commentDesc[0] * 1.5, // x position of comment
|
||||
commentDesc[1] * 2.2, // y position of comment
|
||||
commentDesc[2] * 1.5, // width of comment
|
||||
commentDesc[3] * 2.2, // height of comment
|
||||
!commentDesc[4] // commentDesc[4] -- false means minimized, true means full screen
|
||||
);
|
||||
if (commentDesc[5] >= 0) {
|
||||
// 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 = commentDesc[5];
|
||||
// Add this comment to the block comments object with its script index
|
||||
// as the key
|
||||
if (blockComments.hasOwnProperty(commentDesc[5])) {
|
||||
blockComments[commentDesc[5]].push(newComment);
|
||||
} else {
|
||||
blockComments[commentDesc[5]] = [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 (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)
|
||||
|
@ -619,9 +682,15 @@ const specMapBlock = function (block) {
|
|||
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
||||
* @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.
|
||||
* @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);
|
||||
if (!blockMetadata) {
|
||||
return;
|
||||
|
@ -645,6 +714,21 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
|||
shadow: false, // No shadow blocks in an SB2 by default.
|
||||
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.
|
||||
if (oldOpcode === 'call') {
|
||||
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
|
||||
|
@ -671,10 +755,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
|||
let innerBlocks;
|
||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||
// Block list occupies the input.
|
||||
innerBlocks = parseBlockList(providedArg, addBroadcastMsg, getVariableId, extensions);
|
||||
[innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId,
|
||||
extensions, comments, commentIndex);
|
||||
} else {
|
||||
// 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;
|
||||
for (let j = 0; j < innerBlocks.length; j++) {
|
||||
|
@ -914,7 +1003,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
|
|||
break;
|
||||
}
|
||||
}
|
||||
return activeBlock;
|
||||
return [activeBlock, commentIndex];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -1005,12 +1005,16 @@ class VirtualMachine extends EventEmitter {
|
|||
);
|
||||
|
||||
const variables = Object.keys(variableMap).map(k => variableMap[k]);
|
||||
const wkspcComments = 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">
|
||||
<variables>
|
||||
${variables.map(v => v.toXML()).join()}
|
||||
</variables>
|
||||
${this.editingTarget.blocks.toXML()}
|
||||
${wkspcComments.map(c => c.toXML()).join()}
|
||||
${this.editingTarget.blocks.toXML(this.editingTarget.comments)}
|
||||
</xml>`;
|
||||
|
||||
this.emit('workspaceUpdate', {xml: xmlString});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue