mirror of
https://github.com/scratchfoundation/scratch-analysis.git
synced 2025-06-17 16:21:27 -04:00
Merge pull request #4 from thisandagain/feature/cloud
Add support for cloud variables
This commit is contained in:
commit
84d97ed46e
12 changed files with 236 additions and 6 deletions
|
@ -26,7 +26,8 @@ The `scratch-analysis` module will return an object containing high-level summar
|
|||
| `scripts` | `count` |
|
||||
| `blocks` | `count`, `unique`, `list`, `frequency` |
|
||||
| `sprites` | `count` |
|
||||
| `variables` | `count` |
|
||||
| `variables` | `count`, `id` |
|
||||
| `cloud` | `count`, `id` |
|
||||
| `lists` | `count` |
|
||||
| `costumes` | `count`, `list`, `hash` |
|
||||
| `sounds` | `count`, `list`, `hash` |
|
||||
|
|
62
lib/sb2.js
62
lib/sb2.js
|
@ -92,6 +92,24 @@ const blocks = function (project) {
|
|||
// Storage objects
|
||||
const result = [];
|
||||
|
||||
/**
|
||||
* Determine if a argument is the name of a known cloud variable.
|
||||
* @param {string} arg Argument (variable name)
|
||||
* @return {boolean} Is cloud variable?
|
||||
*/
|
||||
const isArgCloudVar = function (arg) {
|
||||
// Validate argument
|
||||
// @note "Hacked" inputs here could be objects (arrays)
|
||||
if (typeof arg !== 'string') return false;
|
||||
|
||||
// Iterate over global variables and check to see if arg matches
|
||||
for (let i in project.variables) {
|
||||
const variable = project.variables[i];
|
||||
if (variable.name === arg && variable.isPersistent) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk scripts array(s) and build block list.
|
||||
* @param {array} stack Stack of blocks
|
||||
|
@ -108,11 +126,20 @@ const blocks = function (project) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Get opcode and check variable manipulation for the presence of
|
||||
// cloud variables
|
||||
let opcode = stack[i][0];
|
||||
if (opcode === 'setVar:to:' || opcode === 'changeVar:by:') {
|
||||
if (isArgCloudVar(stack[i][1])) {
|
||||
opcode += 'cloud:';
|
||||
}
|
||||
}
|
||||
|
||||
// Add to block list
|
||||
result.push(stack[i][0]);
|
||||
result.push(opcode);
|
||||
|
||||
// Don't pull in params from procedures
|
||||
if (stack[i][0] === 'procDef') continue;
|
||||
if (opcode === 'procDef') continue;
|
||||
|
||||
// Move to next item and walk
|
||||
walk(stack[i].slice(1));
|
||||
|
@ -154,6 +181,35 @@ const extensions = function (project) {
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts cloud variable information.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @param {array} names Names of all variables in project
|
||||
* @return {object} Cloud variable information
|
||||
*/
|
||||
const cloud = function (project, names) {
|
||||
const obj = [];
|
||||
|
||||
// Extract "isPersistent" parameter from all variables in project
|
||||
const cloudyness = extract(project, 'variables', 'isPersistent').id;
|
||||
|
||||
// Ensure that variable names and isPersistent parameter list are the same
|
||||
// length
|
||||
if (names.length !== cloudyness.length) return -1;
|
||||
|
||||
// Iterate over isPersistent values, and extract names of any that are true
|
||||
for (let i in cloudyness) {
|
||||
if (cloudyness[i]) {
|
||||
obj.push(names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
count: obj.length,
|
||||
id: obj
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyzes a project and returns summary information about the project.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
|
@ -171,6 +227,8 @@ module.exports = function (project, callback) {
|
|||
costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5')
|
||||
};
|
||||
|
||||
meta.cloud = cloud(project, meta.variables.id);
|
||||
|
||||
// Sprites
|
||||
meta.sprites = sprites(project);
|
||||
|
||||
|
|
35
lib/sb3.js
35
lib/sb3.js
|
@ -21,9 +21,14 @@ const variables = function (targets, attribute) {
|
|||
let occurrences = 0;
|
||||
let idList = [];
|
||||
|
||||
// Cloud variables are a type of variable
|
||||
const isCloud = (attribute === 'cloud');
|
||||
if (isCloud) attribute = 'variables';
|
||||
|
||||
for (let t in targets) {
|
||||
for (let a in targets[t][attribute]) {
|
||||
const variable = targets[t][attribute][a];
|
||||
if (isCloud && (variable.length !== 3 || !variable[2])) continue;
|
||||
occurrences++;
|
||||
idList.push(variable[0]);
|
||||
}
|
||||
|
@ -67,10 +72,37 @@ const blocks = function (targets) {
|
|||
// Storage object
|
||||
let result = [];
|
||||
|
||||
/**
|
||||
* Determine if a argument is the name of a known cloud variable.
|
||||
* @param {string} arg Argument (variable name)
|
||||
* @return {boolean} Is cloud variable?
|
||||
*/
|
||||
const isArgCloudVar = function (arg) {
|
||||
// Validate argument
|
||||
if (typeof arg !== 'string') return false;
|
||||
|
||||
// Check first target (stage) to determine if arg is a cloud variable id
|
||||
const stage = targets[0];
|
||||
if (typeof stage.variables[arg] !== 'undefined') {
|
||||
return stage.variables[arg].length === 3 && stage.variables[arg][2];
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over all targets and push block opcodes to storage object
|
||||
for (let t in targets) {
|
||||
for (let a in targets[t].blocks) {
|
||||
const block = targets[t].blocks[a];
|
||||
if (!block.shadow) result.push(block.opcode);
|
||||
|
||||
// Get opcode and check variable manipulation for the presence of
|
||||
// cloud variables
|
||||
let opcode = block.opcode;
|
||||
if (opcode === 'data_setvariableto' || opcode === 'data_changevariableby') {
|
||||
if (isArgCloudVar(block.fields.VARIABLE[1])) {
|
||||
opcode += '_cloud';
|
||||
}
|
||||
}
|
||||
|
||||
if (!block.shadow) result.push(opcode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,6 +129,7 @@ module.exports = function (project, callback) {
|
|||
const meta = {
|
||||
scripts: scripts(project.targets),
|
||||
variables: variables(project.targets, 'variables'),
|
||||
cloud: variables(project.targets, 'cloud'),
|
||||
lists: variables(project.targets, 'lists'),
|
||||
comments: extract(project.targets, 'comments'),
|
||||
sounds: extract(project.targets, 'sounds', 'name', 'md5ext'),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "scratch-analysis",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.0",
|
||||
"description": "Analysis tool for summarizing the structure, composition, and complexity of Scratch programs.",
|
||||
"main": "lib/index.js",
|
||||
"directories": {
|
||||
|
@ -17,7 +17,7 @@
|
|||
"author": "Scratch Foundation",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"scratch-parser": "4.3.3"
|
||||
"scratch-parser": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
|
BIN
test/fixtures/sb2/cloud.sb2
vendored
Normal file
BIN
test/fixtures/sb2/cloud.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb2/cloud_complex.sb2
vendored
Normal file
BIN
test/fixtures/sb2/cloud_complex.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb2/cloud_opcodes.sb2
vendored
Normal file
BIN
test/fixtures/sb2/cloud_opcodes.sb2
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb3/cloud.sb3
vendored
Normal file
BIN
test/fixtures/sb3/cloud.sb3
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb3/cloud_complex.sb3
vendored
Normal file
BIN
test/fixtures/sb3/cloud_complex.sb3
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/sb3/cloud_opcodes.sb3
vendored
Normal file
BIN
test/fixtures/sb3/cloud_opcodes.sb3
vendored
Normal file
Binary file not shown.
79
test/unit/cloud.js
Normal file
79
test/unit/cloud.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const analysis = require('../../lib/index');
|
||||
|
||||
const sb2 = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb2/cloud.sb2')
|
||||
);
|
||||
const sb3 = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb3/cloud.sb3')
|
||||
);
|
||||
const sb2Complex = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb2/cloud_complex.sb2')
|
||||
);
|
||||
const sb3Complex = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb3/cloud_complex.sb3')
|
||||
);
|
||||
|
||||
test('sb2', t => {
|
||||
analysis(sb2, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.cloud, 'object');
|
||||
t.equals(result.cloud.count, 1);
|
||||
t.deepEquals(result.cloud.id, ['☁ baz']);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb3', t => {
|
||||
analysis(sb3, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.cloud, 'object');
|
||||
t.equals(result.cloud.count, 1);
|
||||
t.deepEquals(result.cloud.id, ['☁ baz']);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb2 complex', t => {
|
||||
analysis(sb2Complex, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.cloud, 'object');
|
||||
t.equals(result.cloud.count, 8);
|
||||
t.deepEquals(result.cloud.id, [
|
||||
'☁ Player_1',
|
||||
'☁ Player_2',
|
||||
'☁ Player_3',
|
||||
'☁ Player_4',
|
||||
'☁ Player_5',
|
||||
'☁ GameData',
|
||||
'☁ Player_6',
|
||||
'☁ SAVE_DATA2'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb3 complex', t => {
|
||||
analysis(sb3Complex, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.cloud, 'object');
|
||||
t.equals(result.cloud.count, 8);
|
||||
t.deepEquals(result.cloud.id, [
|
||||
'☁ Player_1',
|
||||
'☁ Player_2',
|
||||
'☁ Player_3',
|
||||
'☁ Player_4',
|
||||
'☁ Player_5',
|
||||
'☁ GameData',
|
||||
'☁ Player_6',
|
||||
'☁ SAVE_DATA2'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
59
test/unit/cloud_opcodes.js
Normal file
59
test/unit/cloud_opcodes.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const test = require('tap').test;
|
||||
const analysis = require('../../lib/index');
|
||||
|
||||
const sb2 = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb2/cloud_opcodes.sb2')
|
||||
);
|
||||
const sb3 = fs.readFileSync(
|
||||
path.resolve(__dirname, '../fixtures/sb3/cloud_opcodes.sb3')
|
||||
);
|
||||
|
||||
test('sb2', t => {
|
||||
analysis(sb2, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.blocks, 'object');
|
||||
t.type(result.blocks.id, 'object');
|
||||
t.deepEquals(result.blocks.id, [
|
||||
'whenGreenFlag',
|
||||
'doForever',
|
||||
'setVar:to:',
|
||||
'randomFrom:to:',
|
||||
'changeVar:by:',
|
||||
'setVar:to:',
|
||||
'randomFrom:to:',
|
||||
'changeVar:by:',
|
||||
'setVar:to:cloud:',
|
||||
'randomFrom:to:',
|
||||
'changeVar:by:cloud:',
|
||||
'wait:elapsed:from:'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('sb3', t => {
|
||||
analysis(sb3, (err, result) => {
|
||||
t.true(typeof err === 'undefined' || err === null);
|
||||
t.type(result, 'object');
|
||||
t.type(result.blocks, 'object');
|
||||
t.type(result.blocks.id, 'object');
|
||||
t.deepEquals(result.blocks.id, [
|
||||
'event_whenflagclicked',
|
||||
'control_forever',
|
||||
'control_wait',
|
||||
'data_setvariableto',
|
||||
'data_setvariableto',
|
||||
'data_setvariableto_cloud',
|
||||
'operator_random',
|
||||
'operator_random',
|
||||
'operator_random',
|
||||
'data_changevariableby',
|
||||
'data_changevariableby',
|
||||
'data_changevariableby_cloud'
|
||||
]);
|
||||
t.end();
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue