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:
Andrew Sliwinski 2018-03-24 11:19:01 -04:00
parent e8369be7f5
commit f7cb29dff6
8 changed files with 6 additions and 284 deletions

View file

@ -6,7 +6,7 @@
[![devDependencies Status](https://david-dm.org/llk/scratch-parser/dev-status.svg)](https://david-dm.org/llk/scratch-parser?type=dev) [![devDependencies Status](https://david-dm.org/llk/scratch-parser/dev-status.svg)](https://david-dm.org/llk/scratch-parser?type=dev)
## Overview ## 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 ## 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" #### "Info"
In addition to the `_meta` data described above, Scratch projects include an attribute called `info` that *may* include the following: In addition to the `_meta` data described above, Scratch projects include an attribute called `info` that *may* include the following:

View file

@ -3,7 +3,6 @@ var async = require('async');
var unpack = require('./lib/unpack'); var unpack = require('./lib/unpack');
var parse = require('./lib/parse'); var parse = require('./lib/parse');
var validate = require('./lib/validate'); var validate = require('./lib/validate');
var analyze = require('./lib/analyze');
/** /**
* Unpacks, parses, validates, and analyzes Scratch projects. If successful, * Unpacks, parses, validates, and analyzes Scratch projects. If successful,
@ -17,7 +16,6 @@ module.exports = function (input, callback) {
unpack(input, cb); unpack(input, cb);
}, },
parse, parse,
validate, validate
analyze
], callback); ], callback);
}; };

View file

@ -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);
};

View file

@ -12,7 +12,7 @@
"scripts": { "scripts": {
"test:lint": "eslint . --ext=js", "test:lint": "eslint . --ext=js",
"test:unit": "tap test/unit/*.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:coverage": "tap test/{unit,integration}/*.js --coverage --coverage-report=lcov",
"test:benchmark": "node test/benchmark/performance.js", "test:benchmark": "node test/benchmark/performance.js",
"test": "npm run test:lint && npm run test:unit && npm run test:integration", "test": "npm run test:lint && npm run test:unit && npm run test:integration",

View file

@ -14,7 +14,6 @@ test('sb2', function (t) {
parser(data.empty.sb2, function (err, res) { parser(data.empty.sb2, function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });
@ -24,7 +23,6 @@ test('json', function (t) {
parser(data.empty.json, function (err, res) { parser(data.empty.json, function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });
@ -34,7 +32,6 @@ test('json string', function (t) {
parser(data.empty.json.toString('utf-8'), function (err, res) { parser(data.empty.json.toString('utf-8'), function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });

View file

@ -14,7 +14,6 @@ test('sb2', function (t) {
parser(data.example.sb2, function (err, res) { parser(data.example.sb2, function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });
@ -24,7 +23,6 @@ test('json', function (t) {
parser(data.example.json, function (err, res) { parser(data.example.json, function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });
@ -34,7 +32,6 @@ test('json string', function (t) {
parser(data.example.json.toString('utf-8'), function (err, res) { parser(data.example.json.toString('utf-8'), function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
t.end(); t.end();
}); });

View file

@ -15,12 +15,11 @@ test('sb', function (t) {
test('sb2', function (t) { test('sb2', function (t) {
var set = data.sb2; var set = data.sb2;
t.plan(set.length * 4); t.plan(set.length * 3);
for (var i in data.sb2) { for (var i in data.sb2) {
parser(data.sb2[i], function (err, res) { parser(data.sb2[i], function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
}); });
} }
@ -28,12 +27,11 @@ test('sb2', function (t) {
test('json', function (t) { test('json', function (t) {
var set = data.json; var set = data.json;
t.plan(set.length * 4); t.plan(set.length * 3);
for (var i in data.json) { for (var i in data.json) {
parser(data.json[i], function (err, res) { parser(data.json[i], function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
}); });
} }
@ -41,12 +39,11 @@ test('json', function (t) {
test('json string', function (t) { test('json string', function (t) {
var set = data.json; var set = data.json;
t.plan(set.length * 4); t.plan(set.length * 3);
for (var i in data.json) { for (var i in data.json) {
parser(data.json[i].toString('utf-8'), function (err, res) { parser(data.json[i].toString('utf-8'), function (err, res) {
t.equal(err, null); t.equal(err, null);
t.type(res, 'object'); t.type(res, 'object');
t.type(res._meta, 'object');
t.type(res.info, 'object'); t.type(res.info, 'object');
}); });
} }

View file

@ -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();
});
});