mirror of
https://github.com/scratchfoundation/scratch-sb1-converter.git
synced 2025-08-28 22:31:12 -04:00
decode and preview individual fields in .sb files
This commit is contained in:
parent
c999f2bc31
commit
61a611b979
27 changed files with 3965 additions and 2826 deletions
3
.babelrc
Normal file
3
.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
|
@ -6,8 +6,8 @@ env:
|
|||
global:
|
||||
- NODE_ENV=production
|
||||
matrix:
|
||||
- NPM_SCRIPT="test:unit -- --jobs=4"
|
||||
- NPM_SCRIPT="test:integration -- --jobs=4"
|
||||
- NPM_SCRIPT="test:ci:unit -- --jobs=4"
|
||||
- NPM_SCRIPT="test:ci:integration -- --jobs=4"
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.npm"
|
||||
|
|
3
index.js
3
index.js
|
@ -0,0 +1,3 @@
|
|||
const {SB1File} = require('./src');
|
||||
|
||||
exports.SB1File = SB1File;
|
5396
package-lock.json
generated
5396
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -18,25 +18,32 @@
|
|||
"lint": "eslint .",
|
||||
"prepublish": "in-publish && npm run build || not-in-publish",
|
||||
"start": "webpack-dev-server",
|
||||
"test:unit": "tap ./test/unit/*.js",
|
||||
"test:integration": "tap ./test/integration/*.js",
|
||||
"test:ci:build": "babel --presets @babel/preset-env . --out-dir dist/test-tmp --only src,test,index.js --source-maps",
|
||||
"test:ci:unit": "npm run test:ci:build && tap ./dist/test-tmp/test/unit",
|
||||
"test:ci:integration": "npm run test:ci:build && tap ./dist/test-tmp/test/integration",
|
||||
"test:ci": "npm run test:ci:build && tap ./dist/test-tmp/test",
|
||||
"test:unit": "tap --node-arg=--require --node-arg=@babel/register ./test/unit",
|
||||
"test:integration": "tap --node-arg=--require --node-arg=@babel/register ./test/integration",
|
||||
"test": "npm run lint && npm run docs && npm run test:unit && npm run test:integration",
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
|
||||
},
|
||||
"dependencies": {
|
||||
"minilog": "3.1.0",
|
||||
"text-encoding": "0.6.4"
|
||||
"text-encoding": "^0.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.0",
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/register": "^7.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.4",
|
||||
"docdash": "^1.0.0",
|
||||
"eslint": "^5.3.0",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"in-publish": "^2.0.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"json": "^9.0.4",
|
||||
|
|
57
src/coders/byte-packets.js
Normal file
57
src/coders/byte-packets.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
class Packet {
|
||||
constructor (uint8 = new Uint8Array(this.size), offset = 0) {
|
||||
this.uint8 = uint8;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
equals (other) {
|
||||
for (const key in other) {
|
||||
if (this[key] !== other[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
view () {
|
||||
const className = this.constructor.name;
|
||||
const obj = {
|
||||
toString () {
|
||||
return className;
|
||||
}
|
||||
};
|
||||
for (const key in this.shape) {
|
||||
obj[key] = this[key];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static initConstructor (PacketConstructor) {
|
||||
PacketConstructor.size = PacketConstructor.prototype.size;
|
||||
return PacketConstructor;
|
||||
}
|
||||
|
||||
static extend (shape) {
|
||||
const DefinedPacket = class extends Packet {
|
||||
get shape () {
|
||||
return shape;
|
||||
}
|
||||
};
|
||||
|
||||
let position = 0;
|
||||
Object.keys(shape).forEach(key => {
|
||||
Object.defineProperty(DefinedPacket.prototype, key, shape[key].asPropertyObject(position));
|
||||
if (shape[key].size === 0) {
|
||||
throw new Error('Packet cannot be defined with variable sized members.');
|
||||
}
|
||||
position += shape[key].size;
|
||||
});
|
||||
|
||||
DefinedPacket.prototype.size = position;
|
||||
DefinedPacket.size = position;
|
||||
|
||||
return DefinedPacket;
|
||||
}
|
||||
}
|
||||
|
||||
export {Packet};
|
262
src/coders/byte-primitives.js
Normal file
262
src/coders/byte-primitives.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
import {assert} from '../util/assert';
|
||||
|
||||
const notImplemented = () => {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
const IS_HOST_LITTLE_ENDIAN = (() => {
|
||||
const ab16 = new Uint16Array(1);
|
||||
const ab8 = new Uint8Array(ab16.buffer);
|
||||
ab16[0] = 0xaabb;
|
||||
return ab8[0] === 0xbb;
|
||||
})();
|
||||
|
||||
class BytePrimitive {
|
||||
constructor ({
|
||||
size = 0,
|
||||
sizeOf = () => size,
|
||||
writeSizeOf = notImplemented,
|
||||
toBytes = new Uint8Array(1),
|
||||
read,
|
||||
write = notImplemented
|
||||
}) {
|
||||
this.size = size;
|
||||
this.sizeOf = sizeOf;
|
||||
this.writeSizeOf = writeSizeOf;
|
||||
|
||||
this.toBytes = toBytes;
|
||||
this.bytes = new Uint8Array(toBytes.buffer);
|
||||
|
||||
this.read = read;
|
||||
this.write = write;
|
||||
}
|
||||
|
||||
asPropertyObject (position) {
|
||||
const _this = this;
|
||||
|
||||
return {
|
||||
get () {
|
||||
return _this.read(this.uint8, position + this.offset);
|
||||
},
|
||||
|
||||
set (value) {
|
||||
return _this.write(this.uint8, position + this.offset, value);
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const Uint8 = new BytePrimitive({
|
||||
size: 1,
|
||||
read (uint8, position) {
|
||||
return uint8[position];
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
uint8[position] = value;
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
const HOSTLE_BE16 = {
|
||||
size: 2,
|
||||
// toBytes: Defined by instance.
|
||||
read (uint8, position) {
|
||||
this.bytes[1] = uint8[position + 0];
|
||||
this.bytes[0] = uint8[position + 1];
|
||||
return this.toBytes[0];
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
this.toBytes[0] = value;
|
||||
uint8[position + 0] = this.bytes[1];
|
||||
uint8[position + 1] = this.bytes[0];
|
||||
return value;
|
||||
}
|
||||
};
|
||||
const HOSTBE_BE16 = {
|
||||
size: 2,
|
||||
// toBytes: Defined by instance.
|
||||
read (uint8, position) {
|
||||
this.bytes[0] = uint8[position + 0];
|
||||
this.bytes[1] = uint8[position + 1];
|
||||
return this.toBytes[0];
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
this.toBytes[0] = value;
|
||||
uint8[position + 0] = this.bytes[0];
|
||||
uint8[position + 1] = this.bytes[1];
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
let BE16;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
BE16 = HOSTLE_BE16;
|
||||
} else {
|
||||
BE16 = HOSTBE_BE16;
|
||||
}
|
||||
|
||||
const Uint16BE = new BytePrimitive(Object.assign({}, BE16, {
|
||||
toBytes: new Uint16Array(1)
|
||||
}));
|
||||
|
||||
const Int16BE = new BytePrimitive(Object.assign({}, BE16, {
|
||||
toBytes: new Int16Array(1)
|
||||
}));
|
||||
|
||||
const HOSTLE_BE32 = {
|
||||
size: 4,
|
||||
// toBytes: Defined by instance.
|
||||
read (uint8, position) {
|
||||
this.bytes[3] = uint8[position + 0];
|
||||
this.bytes[2] = uint8[position + 1];
|
||||
this.bytes[1] = uint8[position + 2];
|
||||
this.bytes[0] = uint8[position + 3];
|
||||
return this.toBytes[0];
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
this.toBytes[0] = value;
|
||||
uint8[position + 0] = this.bytes[3];
|
||||
uint8[position + 1] = this.bytes[2];
|
||||
uint8[position + 2] = this.bytes[1];
|
||||
uint8[position + 3] = this.bytes[0];
|
||||
return value;
|
||||
}
|
||||
};
|
||||
const HOSTBE_BE32 = {
|
||||
size: 4,
|
||||
// toBytes: Defined by instance.
|
||||
read (uint8, position) {
|
||||
this.bytes[0] = uint8[position + 0];
|
||||
this.bytes[1] = uint8[position + 1];
|
||||
this.bytes[2] = uint8[position + 2];
|
||||
this.bytes[3] = uint8[position + 3];
|
||||
return this.toBytes[0];
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
this.toBytes[0] = value;
|
||||
uint8[position + 0] = this.bytes[0];
|
||||
uint8[position + 1] = this.bytes[1];
|
||||
uint8[position + 2] = this.bytes[2];
|
||||
uint8[position + 3] = this.bytes[3];
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
let BE32;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
BE32 = HOSTLE_BE32;
|
||||
} else {
|
||||
BE32 = HOSTBE_BE32;
|
||||
}
|
||||
|
||||
const Int32BE = new BytePrimitive(Object.assign({}, BE32, {
|
||||
toBytes: new Int32Array(1)
|
||||
}));
|
||||
|
||||
const Uint32BE = new BytePrimitive(Object.assign({}, BE32, {
|
||||
toBytes: new Uint32Array(1)
|
||||
}));
|
||||
|
||||
let LE16;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
LE16 = HOSTBE_BE16;
|
||||
} else {
|
||||
LE16 = HOSTLE_BE16;
|
||||
}
|
||||
|
||||
const Uint16LE = new BytePrimitive(Object.assign({}, LE16, {
|
||||
toBytes: new Uint16Array(1)
|
||||
}));
|
||||
|
||||
let LE32;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
LE32 = HOSTBE_BE32;
|
||||
} else {
|
||||
LE32 = HOSTLE_BE32;
|
||||
}
|
||||
|
||||
const Uint32LE = new BytePrimitive(Object.assign({}, LE32, {
|
||||
toBytes: new Uint32Array(1)
|
||||
}));
|
||||
|
||||
const HOSTLE_BEDOUBLE = {
|
||||
size: 8,
|
||||
read (uint8, position) {
|
||||
this.bytes[7] = uint8[position + 0];
|
||||
this.bytes[6] = uint8[position + 1];
|
||||
this.bytes[5] = uint8[position + 2];
|
||||
this.bytes[4] = uint8[position + 3];
|
||||
this.bytes[3] = uint8[position + 4];
|
||||
this.bytes[2] = uint8[position + 5];
|
||||
this.bytes[1] = uint8[position + 6];
|
||||
this.bytes[0] = uint8[position + 7];
|
||||
return this.toBytes[0];
|
||||
}
|
||||
};
|
||||
|
||||
const HOSTBE_BEDOUBLE = {
|
||||
size: 8,
|
||||
read (uint8, position) {
|
||||
this.bytes[7] = uint8[position + 0];
|
||||
this.bytes[6] = uint8[position + 1];
|
||||
this.bytes[5] = uint8[position + 2];
|
||||
this.bytes[4] = uint8[position + 3];
|
||||
this.bytes[3] = uint8[position + 4];
|
||||
this.bytes[2] = uint8[position + 5];
|
||||
this.bytes[1] = uint8[position + 6];
|
||||
this.bytes[0] = uint8[position + 7];
|
||||
return this.toBytes[0];
|
||||
}
|
||||
};
|
||||
|
||||
let BEDOUBLE;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
BEDOUBLE = HOSTLE_BEDOUBLE;
|
||||
} else {
|
||||
BEDOUBLE = HOSTBE_BEDOUBLE;
|
||||
}
|
||||
|
||||
const DoubleBE = new BytePrimitive(Object.assign({}, BEDOUBLE, {
|
||||
toBytes: new Float64Array(1)
|
||||
}));
|
||||
|
||||
class FixedAsciiString extends BytePrimitive {
|
||||
constructor (size) {
|
||||
super({
|
||||
size,
|
||||
read (uint8, position) {
|
||||
let str = '';
|
||||
for (let i = 0; i < size; i++) {
|
||||
const code = uint8[position + i];
|
||||
assert(code <= 127, 'Non-ascii character in FixedAsciiString');
|
||||
str += String.fromCharCode(code);
|
||||
}
|
||||
return str;
|
||||
},
|
||||
write (uint8, position, value) {
|
||||
for (let i = 0; i < size; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
assert(code <= 127, 'Non-ascii character in FixedAsciiString');
|
||||
uint8[position + i] = code;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
IS_HOST_LITTLE_ENDIAN,
|
||||
BytePrimitive,
|
||||
Uint8,
|
||||
Uint16BE,
|
||||
Int16BE,
|
||||
Int32BE,
|
||||
Uint32BE,
|
||||
Uint16LE,
|
||||
Uint32LE,
|
||||
DoubleBE,
|
||||
FixedAsciiString
|
||||
};
|
75
src/coders/byte-stream.js
Normal file
75
src/coders/byte-stream.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {assert} from '../util/assert';
|
||||
|
||||
class ByteStream {
|
||||
constructor (buffer, position = 0) {
|
||||
this.buffer = buffer;
|
||||
this.position = position;
|
||||
|
||||
this.uint8 = new Uint8Array(this.buffer);
|
||||
}
|
||||
|
||||
read (member) {
|
||||
const value = member.read(this.uint8, this.position);
|
||||
if (member.size === 0) {
|
||||
this.position += member.sizeOf(this.uint8, this.position);
|
||||
} else {
|
||||
this.position += member.size;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
readStruct (StructType) {
|
||||
const obj = new StructType(this.uint8, this.position);
|
||||
this.position += StructType.size;
|
||||
return obj;
|
||||
}
|
||||
|
||||
resize (needed) {
|
||||
if (this.buffer.byteLength < needed) {
|
||||
const uint8 = this.uint8;
|
||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log(needed) / Math.log(2)));
|
||||
this.buffer = new ArrayBuffer(nextPowerOf2);
|
||||
|
||||
this.uint8 = new Uint8Array(this.buffer);
|
||||
this.uint8.set(uint8);
|
||||
}
|
||||
}
|
||||
|
||||
write (member, value) {
|
||||
if (member.size === 0) {
|
||||
this.resize(this.position + member.writeSizeOf(value));
|
||||
} else {
|
||||
this.resize(this.position + member.size);
|
||||
}
|
||||
|
||||
member.write(this.uint8, this.position, value);
|
||||
if (member.size === 0) {
|
||||
this.position += member.writeSizeOf(this.uint8, this.position);
|
||||
} else {
|
||||
this.position += member.size;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
writeStruct (StructType, data) {
|
||||
this.resize(this.position + StructType.size);
|
||||
|
||||
const st = Object.assign(new StructType(this.uint8, this.position), data);
|
||||
this.position += StructType.size;
|
||||
return st;
|
||||
}
|
||||
|
||||
writeBytes (bytes, start = 0, end = bytes.length) {
|
||||
assert(bytes instanceof Uint8Array, 'writeBytes must be passed an Uint8Array');
|
||||
|
||||
this.resize(this.position + (end - start));
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
this.uint8[this.position + i - start] = bytes[i];
|
||||
}
|
||||
this.position += end - start;
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
export {ByteStream};
|
1
src/index.js
Normal file
1
src/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export {SB1File} from './sb1-file';
|
84
src/playground/array.js
Normal file
84
src/playground/array.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import {assert} from '../util/assert';
|
||||
|
||||
class SB1ArrayAbstractView {
|
||||
constructor (array, start, end) {
|
||||
this.array = array instanceof SB1ArrayAbstractView ? array.array : array;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.end - this.start;
|
||||
}
|
||||
|
||||
get name () {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
map (fn) {
|
||||
const out = [];
|
||||
for (let i = this.start; i < this.end; i++) {
|
||||
out.push(fn(this.array[i], i, this));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
class SB1ArrayFullView extends SB1ArrayAbstractView {
|
||||
constructor (array) {
|
||||
super(array, array.start || 0, array.end || array.length);
|
||||
}
|
||||
|
||||
get name () {
|
||||
return 'all';
|
||||
}
|
||||
}
|
||||
|
||||
class SB1ArraySubView extends SB1ArrayAbstractView {
|
||||
get name () {
|
||||
return `${this.start + 1} - ${this.end}`;
|
||||
}
|
||||
|
||||
static views (array) {
|
||||
if (array instanceof SB1ArrayFullView) {
|
||||
return array;
|
||||
}
|
||||
if (array.length > 100) {
|
||||
const scale = Math.pow(10, Math.ceil(Math.log(array.length) / Math.log(10)));
|
||||
const increment = scale / 10;
|
||||
|
||||
const views = [];
|
||||
for (let i = (array.start || 0); i < (array.end || array.length); i += increment) {
|
||||
views.push(new SB1ArraySubView(array, i, Math.min(i + increment, array.end || array.length)));
|
||||
assert(views.length <= 10, 'Too many subviews');
|
||||
}
|
||||
views.push(new SB1ArrayFullView(array));
|
||||
return views;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayRenderer {
|
||||
static check (data) {
|
||||
return Array.isArray(data) || data instanceof SB1ArrayAbstractView;
|
||||
}
|
||||
|
||||
render (data, view) {
|
||||
if (data.length) view.renderArrow();
|
||||
view.renderTitle(`Array (${data.length})`);
|
||||
if (data.length) {
|
||||
view.renderExpand(() => (
|
||||
SB1ArraySubView.views(data)
|
||||
.map((field, index) => (
|
||||
view.child(
|
||||
field,
|
||||
field instanceof SB1ArrayAbstractView ? field.name : index + 1, `[${index}]`
|
||||
)
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {ArrayRenderer};
|
29
src/playground/field.js
Normal file
29
src/playground/field.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Field, Header, Reference, Value} from '../squeak/fields';
|
||||
import {TYPES, TYPE_NAMES} from '../squeak/ids';
|
||||
|
||||
class FieldRenderer {
|
||||
static check (data) {
|
||||
return data instanceof Field;
|
||||
}
|
||||
|
||||
render (data, view) {
|
||||
if (data instanceof Reference) {
|
||||
view.renderTitle(`Reference { index: ${data.index} }`);
|
||||
} else if (data instanceof Header) {
|
||||
view.renderTitle(`Header { classId: ${data.classId} (${TYPE_NAMES[data.classId]}), size: ${data.size} }`);
|
||||
} else if ((data instanceof Value) && (
|
||||
data.classId === TYPES.COLOR ||
|
||||
data.classId === TYPES.TRANSLUCENT_COLOR
|
||||
)) {
|
||||
view.renderTitle((+data).toString(16).padStart(8, '0')).style.fontFamily = 'monospace';
|
||||
} else if (data instanceof Value) {
|
||||
if (data.value && data.value.buffer) {
|
||||
view.renderTitle(`${data.value.constructor.name} (${data.value.length})`);
|
||||
} else {
|
||||
view.renderTitle(String(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {FieldRenderer};
|
1
src/playground/index.html
Normal file
1
src/playground/index.html
Normal file
|
@ -0,0 +1 @@
|
|||
<input type="file" class="file" />
|
49
src/playground/index.js
Normal file
49
src/playground/index.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {SB1File} from '../..';
|
||||
import {SB1View} from './view';
|
||||
|
||||
import {ArrayRenderer} from './array';
|
||||
import {FieldRenderer} from './field';
|
||||
import {JSPrimitiveRenderer} from './js-primitive';
|
||||
import {ObjectRenderer} from './object';
|
||||
import {ViewableRenderer} from './viewable';
|
||||
|
||||
SB1View.register(ArrayRenderer);
|
||||
SB1View.register(FieldRenderer);
|
||||
SB1View.register(JSPrimitiveRenderer);
|
||||
SB1View.register(ObjectRenderer);
|
||||
SB1View.register(ViewableRenderer);
|
||||
|
||||
let last = null;
|
||||
const readFile = f => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
if (last) {
|
||||
last.forEach(document.body.removeChild, document.body);
|
||||
}
|
||||
const sb1 = new SB1File(event.target.result);
|
||||
last = [
|
||||
new SB1View(sb1, 'file').element,
|
||||
new SB1View(Array.from(sb1.infoRaw()), 'raw - info').element,
|
||||
new SB1View(Array.from(sb1.dataRaw()), 'raw - data').element
|
||||
];
|
||||
last.forEach(document.body.appendChild, document.body);
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
};
|
||||
|
||||
Array.from(document.getElementsByClassName('file')).forEach(el => {
|
||||
el.addEventListener('change', () => {
|
||||
Array.from(el.files).forEach(readFile);
|
||||
});
|
||||
});
|
||||
|
||||
document.body.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
document.getElementsByClassName('file')[0].files = e.dataTransfer.files;
|
||||
});
|
||||
|
||||
document.body.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
});
|
13
src/playground/js-primitive.js
Normal file
13
src/playground/js-primitive.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const PRIMITIVE_TYPEOFS = ['undefined', 'string', 'number', 'boolean'];
|
||||
|
||||
class JSPrimitiveRenderer {
|
||||
static check (data) {
|
||||
return PRIMITIVE_TYPEOFS.includes(typeof data) || data === null;
|
||||
}
|
||||
|
||||
render (data, view) {
|
||||
view.renderTitle(String(data));
|
||||
}
|
||||
}
|
||||
|
||||
export {JSPrimitiveRenderer};
|
28
src/playground/object.js
Normal file
28
src/playground/object.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const log = require('../util/log');
|
||||
|
||||
class ObjectRenderer {
|
||||
static check (data) {
|
||||
return data && data.constructor === Object;
|
||||
}
|
||||
|
||||
render (dataOrFn, view) {
|
||||
view.renderArrow();
|
||||
view.renderTitle(String(dataOrFn) === '[object Object]' ? 'Object' : String(dataOrFn));
|
||||
view.renderExpand(() => {
|
||||
const data = typeof dataOrFn === 'function' ? dataOrFn() : dataOrFn;
|
||||
return Object.keys(data)
|
||||
.map(key => {
|
||||
try {
|
||||
if (typeof data[key] === 'function') return null;
|
||||
return view.child(data[key], key, `.${key}`);
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
return view.child('An error occured rendering view data.', key, `.${key}`);
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {ObjectRenderer};
|
171
src/playground/view.js
Normal file
171
src/playground/view.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
const log = require('../util/log');
|
||||
|
||||
const _expanded = {};
|
||||
|
||||
const _registry = [];
|
||||
|
||||
class DefaultRenderer {
|
||||
static check () {
|
||||
return true;
|
||||
}
|
||||
|
||||
render (data, view) {
|
||||
if (data instanceof HTMLElement) {
|
||||
view.content.appendChild(data);
|
||||
} else {
|
||||
view.renderTitle(`Unknown Structure(${data ? data.classId || data.constructor.name : ''})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SB1View {
|
||||
constructor (data, prefix = '', path = prefix) {
|
||||
this._elements = {};
|
||||
|
||||
this.element = document.createElement('div');
|
||||
this.element.style.position = 'relative';
|
||||
this.element.style.top = '0';
|
||||
this.element.style.left = '0';
|
||||
// this.element.style.overflow = 'hidden';
|
||||
|
||||
this.content = this.element;
|
||||
|
||||
this.data = data;
|
||||
this.prefix = prefix;
|
||||
this.path = path;
|
||||
this.expanded = !!_expanded[this.path];
|
||||
this.canExpand = false;
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
|
||||
this.element.addEventListener('click', this.toggle);
|
||||
|
||||
this.renderer = SB1View.createRenderer(this.data, this);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
toggle (event) {
|
||||
if (!this.canExpand) return;
|
||||
|
||||
if (event.target !== this._elements.arrow && event.target !== this._elements.title) return;
|
||||
|
||||
_expanded[this.path] = this.expanded = !this.expanded;
|
||||
this.render();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
createElement (type, name) {
|
||||
if (!this._elements[name]) {
|
||||
this._elements[name] = document.createElement(type);
|
||||
}
|
||||
this._elements[name].innerHTML = '';
|
||||
return this._elements[name];
|
||||
}
|
||||
|
||||
child (value, key, path) {
|
||||
return new SB1View(value, key, `${this.path}${path}`);
|
||||
}
|
||||
|
||||
renderClear () {
|
||||
this.canExpand = false;
|
||||
while (this.element.children.length) {
|
||||
this.element.removeChild(this.element.children[0]);
|
||||
}
|
||||
this.content = this.element;
|
||||
}
|
||||
|
||||
renderArrow () {
|
||||
this.canExpand = true;
|
||||
const arrowDiv = this.createElement('div', 'arrow');
|
||||
arrowDiv.innerHTML = '▶';
|
||||
arrowDiv.style.position = 'absolute';
|
||||
arrowDiv.style.left = '0';
|
||||
arrowDiv.style.width = '1em';
|
||||
arrowDiv.style.transform = this.expanded ? 'rotateZ(90deg)' : '';
|
||||
arrowDiv.style.transition = 'transform 3s';
|
||||
this.element.appendChild(arrowDiv);
|
||||
|
||||
const contentDiv = this.createElement('div', 'arrowContent');
|
||||
contentDiv.style.position = 'relative';
|
||||
contentDiv.style.left = '1em';
|
||||
contentDiv.style.right = '0';
|
||||
this.element.appendChild(contentDiv);
|
||||
this.content = contentDiv;
|
||||
}
|
||||
|
||||
renderTitle (title) {
|
||||
const titleDiv = this.createElement('div', 'title');
|
||||
const fullTitle = (this.prefix ? `${this.prefix}: ` : '') + title;
|
||||
if (['\n', '\r', '<br>'].some(str => fullTitle.indexOf(str) !== -1) || fullTitle.length > 80) {
|
||||
this.renderArrow();
|
||||
if (this.expanded) {
|
||||
titleDiv.innerText = fullTitle;
|
||||
} else {
|
||||
const maxLength = Math.min(
|
||||
fullTitle.lastIndexOf(' ', 80),
|
||||
['\n', '\r', '<br>'].reduce((value, str) => {
|
||||
if (fullTitle.indexOf(str) !== -1) {
|
||||
return Math.min(value, fullTitle.indexOf(str));
|
||||
}
|
||||
return value;
|
||||
}, Infinity)
|
||||
);
|
||||
titleDiv.innerText = `${fullTitle.substring(0, maxLength)} ...`;
|
||||
}
|
||||
} else {
|
||||
titleDiv.innerText = fullTitle;
|
||||
}
|
||||
this.content.appendChild(titleDiv);
|
||||
return titleDiv;
|
||||
}
|
||||
|
||||
expand (fn, elsefn) {
|
||||
if (this.expanded) {
|
||||
elsefn();
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
renderExpand (fn) {
|
||||
if (this.expanded) {
|
||||
try {
|
||||
const div = this.createElement('div', 'expanded');
|
||||
fn.call(this, div)
|
||||
.forEach(view => this.content.appendChild(view.element));
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
const divError = this.createElement('div', 'expanded-error');
|
||||
divError.innerText = 'Error rendering expanded area ...';
|
||||
this.content.appendChild(divError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
this.renderClear();
|
||||
this.renderer.render(this.data, this);
|
||||
}
|
||||
|
||||
static register (Class) {
|
||||
_registry.push([Class.check, Class]);
|
||||
}
|
||||
|
||||
static findRenderer (data, view) {
|
||||
return _registry.reduce((carry, [check, Class]) => {
|
||||
if (check(data, view)) return Class;
|
||||
return carry;
|
||||
}, DefaultRenderer);
|
||||
}
|
||||
|
||||
static createRenderer (data, view) {
|
||||
const Renderer = SB1View.findRenderer(data, view);
|
||||
return new Renderer(view);
|
||||
}
|
||||
}
|
||||
|
||||
export {SB1View};
|
17
src/playground/viewable.js
Normal file
17
src/playground/viewable.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {ObjectRenderer} from './object';
|
||||
|
||||
class ViewableRenderer {
|
||||
static check (data) {
|
||||
return data && typeof data.view === 'function';
|
||||
}
|
||||
|
||||
render (data, view) {
|
||||
new ObjectRenderer().render(Object.assign(() => data.view(), {
|
||||
toString () {
|
||||
return data.constructor.name;
|
||||
}
|
||||
}), view);
|
||||
}
|
||||
}
|
||||
|
||||
export {ViewableRenderer};
|
43
src/sb1-file-packets.js
Normal file
43
src/sb1-file-packets.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import {assert} from './util/assert';
|
||||
|
||||
import {Packet} from './coders/byte-packets';
|
||||
import {FixedAsciiString, Uint8, Uint32BE} from './coders/byte-primitives';
|
||||
|
||||
class SB1Signature extends Packet.extend({
|
||||
version: new FixedAsciiString(10),
|
||||
infoByteLength: Uint32BE
|
||||
}) {
|
||||
validate () {
|
||||
assert(
|
||||
this.equals({version: 'ScratchV01'}) ||
|
||||
this.equals({version: 'ScratchV02'}),
|
||||
'Invalid Scratch file signature.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Packet.initConstructor(SB1Signature);
|
||||
|
||||
class SB1Header extends Packet.extend({
|
||||
ObjS: new FixedAsciiString(4),
|
||||
ObjSValue: Uint8,
|
||||
Stch: new FixedAsciiString(4),
|
||||
StchValue: Uint8,
|
||||
numObjects: Uint32BE
|
||||
}) {
|
||||
validate () {
|
||||
assert(
|
||||
this.equals({
|
||||
ObjS: 'ObjS',
|
||||
ObjSValue: 1,
|
||||
Stch: 'Stch',
|
||||
StchValue: 1
|
||||
}),
|
||||
'Invalid Scratch file info packet header.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Packet.initConstructor(SB1Header);
|
||||
|
||||
export {SB1Signature, SB1Header};
|
51
src/sb1-file.js
Normal file
51
src/sb1-file.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {ByteStream} from './coders/byte-stream';
|
||||
|
||||
import {ByteTakeIterator} from './squeak/byte-take-iterator';
|
||||
import {FieldIterator} from './squeak/field-iterator';
|
||||
|
||||
import {SB1Header, SB1Signature} from './sb1-file-packets';
|
||||
|
||||
class SB1File {
|
||||
constructor (buffer) {
|
||||
this.buffer = buffer;
|
||||
this.stream = new ByteStream(buffer);
|
||||
|
||||
this.signature = this.stream.readStruct(SB1Signature);
|
||||
this.signature.validate();
|
||||
|
||||
this.infoHeader = this.stream.readStruct(SB1Header);
|
||||
this.infoHeader.validate();
|
||||
|
||||
this.stream.position += this.signature.infoByteLength - SB1Header.size;
|
||||
|
||||
this.dataHeader = this.stream.readStruct(SB1Header);
|
||||
this.dataHeader.validate();
|
||||
}
|
||||
|
||||
view () {
|
||||
return {
|
||||
signature: this.signature,
|
||||
infoHeader: this.infoHeader,
|
||||
dataHeader: this.dataHeader,
|
||||
toString () {
|
||||
return 'SB1File';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
infoRaw () {
|
||||
return new ByteTakeIterator(
|
||||
new FieldIterator(this.buffer, this.infoHeader.offset + SB1Header.size),
|
||||
this.signature.infoByteLength + SB1Signature.size
|
||||
);
|
||||
}
|
||||
|
||||
dataRaw () {
|
||||
return new ByteTakeIterator(
|
||||
new FieldIterator(this.buffer, this.dataHeader.offset + SB1Header.size),
|
||||
this.stream.uint8.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {SB1File};
|
173
src/squeak/byte-primitives.js
Normal file
173
src/squeak/byte-primitives.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
import {TextDecoder as JSTextDecoder} from 'text-encoding';
|
||||
|
||||
import {assert} from '../util/assert';
|
||||
|
||||
import {IS_HOST_LITTLE_ENDIAN, Int16BE, BytePrimitive, Uint8, Uint32BE} from '../coders/byte-primitives';
|
||||
|
||||
const BUFFER_TOO_BIG = 10 * 1024 * 1024;
|
||||
|
||||
let ReferenceBE;
|
||||
if (IS_HOST_LITTLE_ENDIAN) {
|
||||
ReferenceBE = new BytePrimitive({
|
||||
size: 3,
|
||||
read (uint8, position) {
|
||||
return (
|
||||
(uint8[position + 0] << 16) |
|
||||
(uint8[position + 1] << 8) |
|
||||
uint8[position + 2]
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ReferenceBE = new BytePrimitive({
|
||||
size: 3,
|
||||
read (uint8, position) {
|
||||
return (
|
||||
(uint8[position + 2] << 16) |
|
||||
(uint8[position + 1] << 8) |
|
||||
uint8[position + 0]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const LargeInt = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
const count = Int16BE.read(uint8, position);
|
||||
return Int16BE.size + count;
|
||||
},
|
||||
read (uint8, position) {
|
||||
let num = 0;
|
||||
let multiplier = 0;
|
||||
const count = Int16BE.read(uint8, position);
|
||||
for (let i = 0; i < count; i++) {
|
||||
num = num + (multiplier * Uint8.read(uint8, position++));
|
||||
multiplier *= 256;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
});
|
||||
|
||||
const AsciiString = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
const count = Uint32BE.read(uint8, position);
|
||||
return Uint32BE.size + count;
|
||||
},
|
||||
read (uint8, position) {
|
||||
const count = Uint32BE.read(uint8, position);
|
||||
assert(count < BUFFER_TOO_BIG, 'asciiString too big');
|
||||
position += 4;
|
||||
let str = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
str += String.fromCharCode(uint8[position++]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
});
|
||||
|
||||
const Bytes = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
return Uint32BE.size + Uint32BE.read(uint8, position);
|
||||
},
|
||||
read (uint8, position) {
|
||||
const count = Uint32BE.read(uint8, position);
|
||||
assert(count < BUFFER_TOO_BIG, 'bytes too big');
|
||||
position += Uint32BE.size;
|
||||
|
||||
assert(count < BUFFER_TOO_BIG, 'uint8 array too big');
|
||||
return new Uint8Array(uint8.buffer, position, count);
|
||||
}
|
||||
});
|
||||
|
||||
const SoundBytes = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
return Uint32BE.size + (Uint32BE.read(uint8, position) * 2);
|
||||
},
|
||||
read (uint8, position) {
|
||||
const samples = Uint32BE.read(uint8, position);
|
||||
assert(samples < BUFFER_TOO_BIG, 'sound too big');
|
||||
position += Uint32BE.size;
|
||||
|
||||
const count = samples * 2;
|
||||
assert(count < BUFFER_TOO_BIG, 'uint8 array too big');
|
||||
return new Uint8Array(uint8.buffer, position, count);
|
||||
}
|
||||
});
|
||||
|
||||
const Bitmap32BE = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
return Uint32BE.size + (Uint32BE.read(uint8, position) * Uint32BE.size);
|
||||
},
|
||||
read (uint8, position) {
|
||||
const count = Uint32BE.read(uint8, position);
|
||||
assert(count < BUFFER_TOO_BIG, 'bitmap too big');
|
||||
position += Uint32BE.size;
|
||||
|
||||
assert(count < BUFFER_TOO_BIG, 'uint8 array too big');
|
||||
const value = new Uint32Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
value[i] = Uint32BE.read(uint8, position);
|
||||
position += Uint32BE.size;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
let decoder;
|
||||
/* global TextDecoder:true */
|
||||
if (typeof TextDecoder === 'undefined') {
|
||||
decoder = new JSTextDecoder();
|
||||
} else {
|
||||
decoder = new TextDecoder();
|
||||
}
|
||||
|
||||
const UTF8 = new BytePrimitive({
|
||||
sizeOf (uint8, position) {
|
||||
return Uint32BE.size + Uint32BE.read(uint8, position);
|
||||
},
|
||||
read (uint8, position) {
|
||||
const count = Uint32BE.read(uint8, position);
|
||||
assert(count < BUFFER_TOO_BIG, 'utf8 too big');
|
||||
position += Uint32BE.size;
|
||||
|
||||
assert(count < BUFFER_TOO_BIG, 'uint8 array too big');
|
||||
return decoder.decode(new Uint8Array(uint8.buffer, position, count));
|
||||
}
|
||||
});
|
||||
|
||||
const OpaqueColor = new BytePrimitive({
|
||||
size: 4,
|
||||
read (uint8, position) {
|
||||
const rgb = Uint32BE.read(uint8, position);
|
||||
const a = 0xff;
|
||||
const r = (rgb >> 22) & 0xff;
|
||||
const g = (rgb >> 12) & 0xff;
|
||||
const b = (rgb >> 2) & 0xff;
|
||||
return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
||||
}
|
||||
});
|
||||
|
||||
const TranslucentColor = new BytePrimitive({
|
||||
size: 5,
|
||||
read (uint8, position) {
|
||||
const rgb = Uint32BE.read(uint8, position);
|
||||
const a = Uint8.read(uint8, position);
|
||||
const r = (rgb >> 22) & 0xff;
|
||||
const g = (rgb >> 12) & 0xff;
|
||||
const b = (rgb >> 2) & 0xff;
|
||||
return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
BUFFER_TOO_BIG,
|
||||
ReferenceBE,
|
||||
LargeInt,
|
||||
AsciiString,
|
||||
Bytes,
|
||||
SoundBytes,
|
||||
Bitmap32BE,
|
||||
UTF8,
|
||||
OpaqueColor,
|
||||
TranslucentColor
|
||||
};
|
23
src/squeak/byte-take-iterator.js
Normal file
23
src/squeak/byte-take-iterator.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
class ByteTakeIterator {
|
||||
constructor (iter, maxPosition = Infinity) {
|
||||
this.iter = iter;
|
||||
this.maxPosition = maxPosition;
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this;
|
||||
}
|
||||
|
||||
next () {
|
||||
if (this.iter.stream.position >= this.maxPosition) {
|
||||
return {
|
||||
value: null,
|
||||
done: true
|
||||
};
|
||||
}
|
||||
|
||||
return this.iter.next();
|
||||
}
|
||||
}
|
||||
|
||||
export {ByteTakeIterator};
|
114
src/squeak/field-iterator.js
Normal file
114
src/squeak/field-iterator.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
import {Uint8, Int16BE, Int32BE, DoubleBE} from '../coders/byte-primitives';
|
||||
import {ByteStream} from '../coders/byte-stream';
|
||||
|
||||
import {
|
||||
ReferenceBE, LargeInt, AsciiString, UTF8, Bytes, SoundBytes, Bitmap32BE, OpaqueColor, TranslucentColor
|
||||
} from './byte-primitives';
|
||||
import {BuiltinObjectHeader, FieldObjectHeader, Header, Reference, Value} from './fields';
|
||||
import {TYPES} from './ids';
|
||||
|
||||
class Consumer {
|
||||
constructor ({
|
||||
type = Value,
|
||||
read,
|
||||
value = read ? (stream => stream.read(read)) : null
|
||||
}) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
next (stream, classId, position) {
|
||||
return {
|
||||
value: new this.type(classId, position, this.value(stream)),
|
||||
done: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const CONSUMER_PROTOS = {
|
||||
[TYPES.NULL]: {value: () => null},
|
||||
[TYPES.TRUE]: {value: () => true},
|
||||
[TYPES.FALSE]: {value: () => false},
|
||||
[TYPES.SMALL_INT]: {read: Int32BE},
|
||||
[TYPES.SMALL_INT_16]: {read: Int16BE},
|
||||
[TYPES.LARGE_INT_POSITIVE]: {read: LargeInt},
|
||||
[TYPES.LARGE_INT_NEGATIVE]: {read: LargeInt},
|
||||
[TYPES.FLOATING]: {read: DoubleBE},
|
||||
[TYPES.STRING]: {read: AsciiString},
|
||||
[TYPES.SYMBOL]: {read: AsciiString},
|
||||
[TYPES.BYTES]: {read: Bytes},
|
||||
[TYPES.SOUND]: {read: SoundBytes},
|
||||
[TYPES.BITMAP]: {read: Bitmap32BE},
|
||||
[TYPES.UTF8]: {read: UTF8},
|
||||
[TYPES.ARRAY]: {type: Header, read: Int32BE},
|
||||
[TYPES.ORDERED_COLLECTION]: {type: Header, read: Int32BE},
|
||||
[TYPES.SET]: {type: Header, read: Int32BE},
|
||||
[TYPES.IDENTITY_SET]: {type: Header, read: Int32BE},
|
||||
[TYPES.DICTIONARY]: {
|
||||
type: Header,
|
||||
value: stream => stream.read(Int32BE) * 2
|
||||
},
|
||||
[TYPES.IDENTITY_DICTIONARY]: {
|
||||
type: Header,
|
||||
value: stream => stream.read(Int32BE) * 2
|
||||
},
|
||||
[TYPES.COLOR]: {read: OpaqueColor},
|
||||
[TYPES.TRANSLUCENT_COLOR]: {read: TranslucentColor},
|
||||
[TYPES.POINT]: {type: Header, value: () => 2},
|
||||
[TYPES.RECTANGLE]: {type: Header, value: () => 4},
|
||||
[TYPES.FORM]: {type: Header, value: () => 5},
|
||||
[TYPES.SQUEAK]: {type: Header, value: () => 6},
|
||||
[TYPES.OBJECT_REF]: {type: Reference, read: ReferenceBE}
|
||||
};
|
||||
|
||||
const CONSUMERS = new Array(256).fill(null);
|
||||
for (const index of Object.values(TYPES)) {
|
||||
if (CONSUMER_PROTOS[index]) {
|
||||
CONSUMERS[index] = new Consumer(CONSUMER_PROTOS[index]);
|
||||
}
|
||||
}
|
||||
|
||||
const builtinConsumer = new Consumer({
|
||||
type: BuiltinObjectHeader,
|
||||
value: () => null
|
||||
});
|
||||
|
||||
class FieldIterator {
|
||||
constructor (buffer, position) {
|
||||
this.buffer = buffer;
|
||||
this.stream = new ByteStream(buffer, position);
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return this;
|
||||
}
|
||||
|
||||
next () {
|
||||
if (this.stream.position >= this.stream.uint8.length) {
|
||||
return {
|
||||
value: null,
|
||||
done: true
|
||||
};
|
||||
}
|
||||
|
||||
const position = this.stream.position;
|
||||
const classId = this.stream.read(Uint8);
|
||||
|
||||
const consumer = CONSUMERS[classId];
|
||||
|
||||
if (consumer !== null) {
|
||||
return consumer.next(this.stream, classId, position);
|
||||
} else if (classId < TYPES.OBJECT_REF) {
|
||||
// TODO: Does this ever happen?
|
||||
return builtinConsumer.next(this.stream, classId, position);
|
||||
}
|
||||
const classVersion = this.stream.read(Uint8);
|
||||
const size = this.stream.read(Uint8);
|
||||
return {
|
||||
value: new FieldObjectHeader(classId, position, classVersion, size),
|
||||
done: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {FieldIterator};
|
80
src/squeak/fields.js
Normal file
80
src/squeak/fields.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {TYPES} from './ids';
|
||||
|
||||
class Field {
|
||||
constructor (classId, position) {
|
||||
this.classId = classId;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
const valueOf = obj => {
|
||||
if (typeof obj === 'object' && obj) return obj.valueOf();
|
||||
return obj;
|
||||
};
|
||||
|
||||
class Value extends Field {
|
||||
constructor (classId, position, value) {
|
||||
super(classId, position);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
valueOf () {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
if (
|
||||
this.classId === TYPES.TRANSLUCENT_COLOR ||
|
||||
this.classId === TYPES.COLOR
|
||||
) {
|
||||
// TODO: Can colors be 32 bit in scratch-packets?
|
||||
return this.value & 0xffffff;
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
class Header extends Field {
|
||||
constructor (classId, position, size) {
|
||||
super(classId, position);
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
class Reference extends Field {
|
||||
constructor (classId, position, index) {
|
||||
super(classId, position);
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
valueOf () {
|
||||
return `Ref(${this.index})`;
|
||||
}
|
||||
}
|
||||
|
||||
class BuiltinObjectHeader extends Header {
|
||||
constructor (classId, position) {
|
||||
super(classId, position, 0);
|
||||
}
|
||||
}
|
||||
|
||||
class FieldObjectHeader extends Header {
|
||||
constructor (classId, position, version, size) {
|
||||
super(classId, position, size);
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
valueOf as value,
|
||||
Value,
|
||||
Header,
|
||||
Reference,
|
||||
BuiltinObjectHeader,
|
||||
FieldObjectHeader
|
||||
};
|
54
src/squeak/ids.js
Normal file
54
src/squeak/ids.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const TYPES = {
|
||||
NULL: 1,
|
||||
TRUE: 2,
|
||||
FALSE: 3,
|
||||
SMALL_INT: 4,
|
||||
SMALL_INT_16: 5,
|
||||
LARGE_INT_POSITIVE: 6,
|
||||
LARGE_INT_NEGATIVE: 7,
|
||||
FLOATING: 8,
|
||||
STRING: 9,
|
||||
SYMBOL: 10,
|
||||
BYTES: 11,
|
||||
SOUND: 12,
|
||||
BITMAP: 13,
|
||||
UTF8: 14,
|
||||
ARRAY: 20,
|
||||
ORDERED_COLLECTION: 21,
|
||||
SET: 22,
|
||||
IDENTITY_SET: 23,
|
||||
DICTIONARY: 24,
|
||||
IDENTITY_DICTIONARY: 25,
|
||||
COLOR: 30,
|
||||
TRANSLUCENT_COLOR: 31,
|
||||
POINT: 32,
|
||||
RECTANGLE: 33,
|
||||
FORM: 34,
|
||||
SQUEAK: 35,
|
||||
OBJECT_REF: 99,
|
||||
MORPH: 100,
|
||||
ALIGNMENT: 104,
|
||||
// Called String in Scratch 2. To reduce confusion this is called
|
||||
// STATIC_STRING to differentiate it from STRING in this codebase.
|
||||
STATIC_STRING: 105,
|
||||
UPDATING_STRING: 106,
|
||||
SAMPLED_SOUND: 109,
|
||||
IMAGE_MORPH: 110,
|
||||
SPRITE: 124,
|
||||
STAGE: 125,
|
||||
WATCHER: 155,
|
||||
IMAGE_MEDIA: 162,
|
||||
SOUND_MEDIA: 164,
|
||||
MULTILINE_STRING: 171,
|
||||
WATCHER_READOUT_FRAME: 173,
|
||||
WATCHER_SLIDER: 174,
|
||||
LIST_WATCHER: 175
|
||||
};
|
||||
|
||||
const TYPE_NAMES = Object.entries(TYPES)
|
||||
.reduce((carry, [key, value]) => {
|
||||
carry[value] = key;
|
||||
return carry;
|
||||
}, {});
|
||||
|
||||
export {TYPES, TYPE_NAMES};
|
10
src/util/assert.js
Normal file
10
src/util/assert.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
class AssertionError extends Error {}
|
||||
|
||||
const assert = function (test, message) {
|
||||
if (!test) throw new AssertionError(message);
|
||||
};
|
||||
|
||||
export {
|
||||
AssertionError,
|
||||
assert
|
||||
};
|
3
src/util/log.js
Normal file
3
src/util/log.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
const minilog = require('minilog');
|
||||
|
||||
module.exports = new minilog('sb1-converter');
|
|
@ -1,3 +1,35 @@
|
|||
const path = require('path');
|
||||
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './index.js'
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
devServer: {
|
||||
contentBase: false,
|
||||
host: '0.0.0.0',
|
||||
port: process.env.PORT || 8093
|
||||
},
|
||||
entry: {
|
||||
main: './index.js',
|
||||
view: './src/playground/index.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'playground')
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: path.resolve(__dirname, 'src'),
|
||||
query: {
|
||||
presets: [['@babel/preset-env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]]
|
||||
}
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/playground/index.html',
|
||||
chunks: ['view']
|
||||
})
|
||||
]
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue