mirror of
https://github.com/scratchfoundation/scratch-analysis.git
synced 2025-05-04 18:03:29 -04:00
Initial working version
This commit is contained in:
parent
4f28eeef52
commit
590b71a0aa
22 changed files with 1414 additions and 0 deletions
25
lib/index.js
Normal file
25
lib/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const parser = require('scratch-parser');
|
||||
|
||||
const sb2 = require('./sb2');
|
||||
const sb3 = require('./sb3');
|
||||
|
||||
module.exports = function (buffer, callback) {
|
||||
parser(buffer, false, (err, project) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Flatten array
|
||||
project = project[0];
|
||||
|
||||
// Push project object to the appropriate analysis handler
|
||||
switch (project.projectVersion) {
|
||||
case 2:
|
||||
sb2(project, callback);
|
||||
break;
|
||||
case 3:
|
||||
sb3(project, callback);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported project version');
|
||||
}
|
||||
});
|
||||
};
|
200
lib/sb2.js
Normal file
200
lib/sb2.js
Normal file
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* Returns an array of items matching the specified attribute.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @param {string} attribute Attribute to extract and flatten
|
||||
* @return {array} Array of specified attribute
|
||||
*/
|
||||
const flatten = function (project, attribute) {
|
||||
// Storage object
|
||||
let result = [];
|
||||
|
||||
// If attribute exists at the top level of the project, append it
|
||||
if (typeof project[attribute] !== 'undefined') {
|
||||
result = project[attribute];
|
||||
}
|
||||
|
||||
// Iterate over child elements and append to result array
|
||||
for (let i in project.children) {
|
||||
const child = project.children[i];
|
||||
if (typeof child[attribute] !== 'undefined') {
|
||||
result = result.concat(child[attribute]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract summary information from a specific project attribute. Will attempt
|
||||
* to concatinate all children when generating summary.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @param {string} attribute Attribute key
|
||||
* @param {string} id "id" key
|
||||
* @param {string} hash "hash" key
|
||||
* @return {object} Summary information
|
||||
*/
|
||||
const extract = function (project, attribute, id, hash) {
|
||||
// Create storage objects and flatten project
|
||||
let idList = null;
|
||||
let hashList = null;
|
||||
let elements = flatten(project, attribute);
|
||||
|
||||
// Extract ids if specified
|
||||
if (typeof id !== 'undefined') {
|
||||
idList = [];
|
||||
for (var x in elements) {
|
||||
idList.push(elements[x][id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract hashes if specified
|
||||
if (typeof hash !== 'undefined') {
|
||||
hashList = [];
|
||||
for (var y in elements) {
|
||||
hashList.push(elements[y][hash]);
|
||||
}
|
||||
}
|
||||
|
||||
// Build result and return
|
||||
var result = {
|
||||
count: elements.length
|
||||
};
|
||||
if (idList !== null) result.id = idList;
|
||||
if (hashList !== null) result.hash = hashList;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract number of sprites from a project object. Will attempt to ignore
|
||||
* "children" which are not sprites.
|
||||
* @param {object} input Project object (SB2 format)
|
||||
* @return {object} Sprite information
|
||||
*/
|
||||
const sprites = function (input) {
|
||||
let result = 0;
|
||||
|
||||
for (let i in input.children) {
|
||||
if (input.children[i].hasOwnProperty('spriteInfo')) result++;
|
||||
}
|
||||
|
||||
return {count: result};
|
||||
};
|
||||
|
||||
/**
|
||||
* Tallys term frequency from an array of strings.
|
||||
* @param {array} input Array of strings
|
||||
* @return {object} Frequency information
|
||||
*/
|
||||
const frequency = function (input) {
|
||||
const result = Object.create(null);
|
||||
|
||||
for (let i in input) {
|
||||
var term = input[i];
|
||||
if (typeof result[term] === 'undefined') result[term] = 0;
|
||||
result[term]++;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts all blocks and generates a frequency count.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @return {object} Block information
|
||||
*/
|
||||
const blocks = function (project) {
|
||||
// Storage objects
|
||||
const result = [];
|
||||
|
||||
/**
|
||||
* Walk scripts array(s) and build block list.
|
||||
* @param {array} stack Stack of blocks
|
||||
* @return {void}
|
||||
*/
|
||||
const walk = function (stack) {
|
||||
for (let i in stack) {
|
||||
// Skip if item is not array
|
||||
if (!Array.isArray(stack[i])) continue;
|
||||
|
||||
// Recurse if first item is not a string
|
||||
if (typeof stack[i][0] !== 'string') {
|
||||
walk(stack[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to block list
|
||||
result.push(stack[i][0]);
|
||||
|
||||
// Don't pull in params from procedures
|
||||
if (stack[i][0] === 'procDef') continue;
|
||||
|
||||
// Move to next item and walk
|
||||
walk(stack[i].slice(1));
|
||||
}
|
||||
};
|
||||
walk(flatten(project, 'scripts'));
|
||||
|
||||
// Generate frequency count
|
||||
const freq = frequency(result);
|
||||
|
||||
// Build result and return
|
||||
return {
|
||||
count: result.length,
|
||||
unique: Object.keys(freq).length,
|
||||
id: result,
|
||||
frequency: freq
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts extension information.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @return {object} Extension information
|
||||
*/
|
||||
const extensions = function (project) {
|
||||
const result = {count: 0, id: []};
|
||||
|
||||
// Check to ensure project includes any extensions
|
||||
if (typeof project.info.savedExtensions === 'undefined') return result;
|
||||
|
||||
// Iterate over extensions and build list
|
||||
var ext = project.info.savedExtensions;
|
||||
for (let i in ext) {
|
||||
result.id.push(ext[i].extensionName);
|
||||
}
|
||||
|
||||
// Count and return
|
||||
result.count = result.id.length;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyzes a project and returns summary information about the project.
|
||||
* @param {object} project Project object (SB2 format)
|
||||
* @param {Function} callback Callback function
|
||||
* @return {void}
|
||||
*/
|
||||
module.exports = function (project, callback) {
|
||||
// Create metadata object
|
||||
const meta = {
|
||||
scripts: extract(project, 'scripts'),
|
||||
variables: extract(project, 'variables', 'name'),
|
||||
lists: extract(project, 'lists', 'listName'),
|
||||
comments: extract(project, 'scriptComments'),
|
||||
sounds: extract(project, 'sounds', 'soundName', 'md5'),
|
||||
costumes: extract(project, 'costumes', 'costumeName', 'baseLayerMD5')
|
||||
};
|
||||
|
||||
// Sprites
|
||||
meta.sprites = sprites(project);
|
||||
|
||||
// Blocks
|
||||
meta.blocks = blocks(project);
|
||||
|
||||
// Extensions
|
||||
meta.extensions = extensions(project);
|
||||
|
||||
// Return all metadata
|
||||
return callback(null, meta);
|
||||
};
|
110
lib/sb3.js
Normal file
110
lib/sb3.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
const utility = require('./utility');
|
||||
|
||||
const scripts = function (targets) {
|
||||
// Storage objects
|
||||
let occurances = 0;
|
||||
|
||||
// Iterate over all blocks in each target, and look for "top level" blocks
|
||||
for (let t in targets) {
|
||||
for (let b in targets[t].blocks) {
|
||||
if (targets[t].blocks[b].topLevel) occurances++;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
count: occurances
|
||||
};
|
||||
};
|
||||
|
||||
const variables = function (targets, attribute) {
|
||||
// Storage objects
|
||||
let occurances = 0;
|
||||
let idList = [];
|
||||
|
||||
for (let t in targets) {
|
||||
for (let a in targets[t][attribute]) {
|
||||
const variable = targets[t][attribute][a];
|
||||
occurances++;
|
||||
idList.push(variable[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
count: occurances,
|
||||
id: idList
|
||||
};
|
||||
};
|
||||
|
||||
// Iterate over targets, extract attribute, and aggregate results
|
||||
const extract = function (targets, attribute, id, hash) {
|
||||
// Storage objects
|
||||
let occurances = 0;
|
||||
let idList = [];
|
||||
let hashList = [];
|
||||
|
||||
for (let t in targets) {
|
||||
for (let a in targets[t][attribute]) {
|
||||
const asset = targets[t][attribute][a];
|
||||
occurances++;
|
||||
if (typeof id !== 'undefined') idList.push(asset[id]);
|
||||
if (typeof hash !== 'undefined') hashList.push(asset[hash]);
|
||||
}
|
||||
};
|
||||
|
||||
const result = {count: occurances};
|
||||
if (typeof id !== 'undefined') result.id = idList;
|
||||
if (typeof hash !== 'undefined') result.hash = hashList;
|
||||
return result;
|
||||
};
|
||||
|
||||
const sprites = function (targets) {
|
||||
return {
|
||||
count: targets.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
const blocks = function (targets) {
|
||||
// Storage object
|
||||
let result = [];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate block frequency
|
||||
const freq = utility.frequency(result);
|
||||
|
||||
// Return summary
|
||||
return {
|
||||
count: result.length,
|
||||
unique: Object.keys(freq).length,
|
||||
id: result,
|
||||
frequency: freq
|
||||
};
|
||||
};
|
||||
|
||||
const extensions = function (extensions) {
|
||||
return {
|
||||
count: extensions.length,
|
||||
id: extensions
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = function (project, callback) {
|
||||
const meta = {
|
||||
scripts: scripts(project.targets),
|
||||
variables: variables(project.targets, 'variables'),
|
||||
lists: variables(project.targets, 'lists'),
|
||||
comments: extract(project.targets, 'comments'),
|
||||
sounds: extract(project.targets, 'sounds', 'name', 'md5ext'),
|
||||
costumes: extract(project.targets, 'costumes', 'name', 'md5ext'),
|
||||
sprites: sprites(project.targets),
|
||||
blocks: blocks(project.targets),
|
||||
extensions: extensions(project.extensions)
|
||||
};
|
||||
|
||||
callback(null, meta);
|
||||
};
|
23
lib/utility.js
Normal file
23
lib/utility.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Utility methods for costructing Scratch project summaries.
|
||||
*/
|
||||
class Utility {
|
||||
/**
|
||||
* Tallys term frequency from an array of strings.
|
||||
* @param {array} input Array of strings
|
||||
* @return {object} Frequency information
|
||||
*/
|
||||
static frequency (input) {
|
||||
const result = Object.create(null);
|
||||
|
||||
for (let i in input) {
|
||||
var term = input[i];
|
||||
if (typeof result[term] === 'undefined') result[term] = 0;
|
||||
result[term]++;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Utility;
|
Loading…
Add table
Add a link
Reference in a new issue