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)
|
[](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:
|
||||||
|
|
||||||
|
|
4
index.js
4
index.js
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
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": {
|
"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",
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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