mirror of
https://github.com/scratchfoundation/scratch-parser.git
synced 2025-08-28 22:18:45 -04:00
refactor: Extricates analysis from the scope of this project
Removes the ./lib/analysis module from the scope of this project along with all related documentation and test coverage. BREAKING CHANGE: Module no longer provides a _meta object with returned project object.
This commit is contained in:
parent
e8369be7f5
commit
f7cb29dff6
8 changed files with 6 additions and 284 deletions
17
README.md
17
README.md
|
@ -6,7 +6,7 @@
|
|||
[](https://david-dm.org/llk/scratch-parser?type=dev)
|
||||
|
||||
## Overview
|
||||
The Scratch Parser is a [Node.js](https://nodejs.org) module that parses and validates [Scratch](https://scratch.mit.edu) projects. Internally, this utility is used for validation of Scratch projects as well as for extracting metadata from projects for research and search purposes.
|
||||
The Scratch Parser is a [Node.js](https://nodejs.org) module that parses and validates [Scratch](https://scratch.mit.edu) projects.
|
||||
|
||||
## API
|
||||
|
||||
|
@ -27,21 +27,6 @@ parser(buffer, function (err, project) {
|
|||
});
|
||||
```
|
||||
|
||||
## Metadata
|
||||
The `scratch-parser` module will append metadata about the project should validation and parsing be successful. The `_meta` object includes:
|
||||
|
||||
| Key | Attributes |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| `scripts` | `count` |
|
||||
| `blocks` | `count`, `unique`, `list`, `frequency` |
|
||||
| `sprites` | `count` |
|
||||
| `variables` | `count` |
|
||||
| `lists` | `count` |
|
||||
| `costumes` | `count`, `list`, `hash` |
|
||||
| `sounds` | `count`, `list`, `hash` |
|
||||
| `extensions` | `count`, `list` |
|
||||
| `comments` | `count` |
|
||||
|
||||
#### "Info"
|
||||
In addition to the `_meta` data described above, Scratch projects include an attribute called `info` that *may* include the following:
|
||||
|
||||
|
|
4
index.js
4
index.js
|
@ -3,7 +3,6 @@ var async = require('async');
|
|||
var unpack = require('./lib/unpack');
|
||||
var parse = require('./lib/parse');
|
||||
var validate = require('./lib/validate');
|
||||
var analyze = require('./lib/analyze');
|
||||
|
||||
/**
|
||||
* Unpacks, parses, validates, and analyzes Scratch projects. If successful,
|
||||
|
@ -17,7 +16,6 @@ module.exports = function (input, callback) {
|
|||
unpack(input, cb);
|
||||
},
|
||||
parse,
|
||||
validate,
|
||||
analyze
|
||||
validate
|
||||
], callback);
|
||||
};
|
||||
|
|
210
lib/analyze.js
210
lib/analyze.js
|
@ -1,210 +0,0 @@
|
|||
/**
|
||||
* Returns an array of items matching the specified attribute.
|
||||
*
|
||||
* @param {Object} Project
|
||||
* @param {String} Attribute to extract and flatten
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
function flatten (project, attribute) {
|
||||
// Storage object
|
||||
var 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 (var i in project.children) {
|
||||
var 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
|
||||
* @param {String} Attribute key
|
||||
* @param {String, Optional} "id" key
|
||||
* @param {String, Optional} "hash" key
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function extract (project, attribute, id, hash) {
|
||||
// Create storage objects and flatten project
|
||||
var idList = null;
|
||||
var hashList = null;
|
||||
var 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
|
||||
*
|
||||
* @return {Object} Sprite information
|
||||
*/
|
||||
function sprites (input) {
|
||||
var result = 0;
|
||||
|
||||
for (var i in input.children) {
|
||||
if (input.children[i].hasOwnProperty('spriteInfo')) result++;
|
||||
}
|
||||
|
||||
return { count: result };
|
||||
}
|
||||
|
||||
/**
|
||||
* Tallys term frequency from an array of strings.
|
||||
*
|
||||
* @param {Array} Array of strings
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function frequency (input) {
|
||||
var result = Object.create(null);
|
||||
|
||||
for (var i in input) {
|
||||
var term = input[i];
|
||||
if (typeof result[term] === 'undefined') result[term] = 0;
|
||||
result[term]++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract blocks and generate frequency count.
|
||||
*
|
||||
* @param {Object} Project
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function blocks (project) {
|
||||
// Storage objects
|
||||
var blocks = [];
|
||||
|
||||
// Walk scripts array(s) and build block list
|
||||
function walk (stack) {
|
||||
for (var 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
|
||||
blocks.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
|
||||
var freq = frequency(blocks);
|
||||
|
||||
// Build result and return
|
||||
return {
|
||||
count: blocks.length,
|
||||
unique: Object.keys(freq).length,
|
||||
id: blocks,
|
||||
frequency: freq
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract extensions list.
|
||||
*
|
||||
* @param {Object} Project
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function extensions (project) {
|
||||
var 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 (var i in ext) {
|
||||
result.id.push(ext[i].extensionName);
|
||||
}
|
||||
|
||||
// Count and return
|
||||
result.count = result.id.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes project and appends metadata to the project object.
|
||||
*
|
||||
* @param {Object} Project
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
module.exports = function (project, callback) {
|
||||
// Create metadata object
|
||||
var 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);
|
||||
|
||||
// Bind metadata to project and return
|
||||
project._meta = meta;
|
||||
callback(null, project);
|
||||
};
|
|
@ -12,7 +12,7 @@
|
|||
"scripts": {
|
||||
"test:lint": "eslint . --ext=js",
|
||||
"test:unit": "tap test/unit/*.js",
|
||||
"test:integration": "tap test/unit/*.js",
|
||||
"test:integration": "tap test/integration/*.js",
|
||||
"test:coverage": "tap test/{unit,integration}/*.js --coverage --coverage-report=lcov",
|
||||
"test:benchmark": "node test/benchmark/performance.js",
|
||||
"test": "npm run test:lint && npm run test:unit && npm run test:integration",
|
||||
|
|
|
@ -14,7 +14,6 @@ test('sb2', function (t) {
|
|||
parser(data.empty.sb2, function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
@ -24,7 +23,6 @@ test('json', function (t) {
|
|||
parser(data.empty.json, function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
@ -34,7 +32,6 @@ test('json string', function (t) {
|
|||
parser(data.empty.json.toString('utf-8'), function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -14,7 +14,6 @@ test('sb2', function (t) {
|
|||
parser(data.example.sb2, function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
@ -24,7 +23,6 @@ test('json', function (t) {
|
|||
parser(data.example.json, function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
@ -34,7 +32,6 @@ test('json string', function (t) {
|
|||
parser(data.example.json.toString('utf-8'), function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
t.end();
|
||||
});
|
||||
|
|
|
@ -15,12 +15,11 @@ test('sb', function (t) {
|
|||
|
||||
test('sb2', function (t) {
|
||||
var set = data.sb2;
|
||||
t.plan(set.length * 4);
|
||||
t.plan(set.length * 3);
|
||||
for (var i in data.sb2) {
|
||||
parser(data.sb2[i], function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
});
|
||||
}
|
||||
|
@ -28,12 +27,11 @@ test('sb2', function (t) {
|
|||
|
||||
test('json', function (t) {
|
||||
var set = data.json;
|
||||
t.plan(set.length * 4);
|
||||
t.plan(set.length * 3);
|
||||
for (var i in data.json) {
|
||||
parser(data.json[i], function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
});
|
||||
}
|
||||
|
@ -41,12 +39,11 @@ test('json', function (t) {
|
|||
|
||||
test('json string', function (t) {
|
||||
var set = data.json;
|
||||
t.plan(set.length * 4);
|
||||
t.plan(set.length * 3);
|
||||
for (var i in data.json) {
|
||||
parser(data.json[i].toString('utf-8'), function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res.info, 'object');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
var test = require('tap').test;
|
||||
var data = require('../fixtures/data');
|
||||
var analyze = require('../../lib/analyze');
|
||||
|
||||
test('spec', function (t) {
|
||||
t.type(analyze, 'function');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('empty project', function (t) {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('example project', function (t) {
|
||||
analyze(JSON.parse(data.example.json.toString()), function (err, res) {
|
||||
t.equal(err, null);
|
||||
t.type(res, 'object');
|
||||
t.type(res._meta, 'object');
|
||||
t.type(res._meta.sprites, 'object');
|
||||
t.type(res._meta.scripts, 'object');
|
||||
t.type(res._meta.variables, 'object');
|
||||
t.type(res._meta.lists, 'object');
|
||||
t.type(res._meta.comments, 'object');
|
||||
t.type(res._meta.sounds, 'object');
|
||||
t.type(res._meta.costumes, 'object');
|
||||
t.type(res._meta.blocks, 'object');
|
||||
t.type(res._meta.extensions, 'object');
|
||||
|
||||
t.equal(res._meta.sprites.count, 2, 'expected number of sprites');
|
||||
t.equal(res._meta.scripts.count, 5, 'expected number of scripts');
|
||||
t.equal(res._meta.variables.count, 2, 'expected number of variables');
|
||||
t.equal(res._meta.lists.count, 2, 'expected number of lists');
|
||||
t.equal(res._meta.comments.count, 1, 'expected number of comments');
|
||||
t.equal(res._meta.sounds.count, 4, 'expected number of sounds');
|
||||
t.equal(res._meta.costumes.count, 16, 'expected number of costumes');
|
||||
t.equal(res._meta.blocks.count, 16, 'expected number of blocks');
|
||||
t.equal(res._meta.blocks.unique, 11, 'exepected number of blocks');
|
||||
t.equal(res._meta.extensions.count, 1, 'expected number of extensions');
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue