scratch-sb1-converter
Version:
Scratch 1 (.sb) to Scratch 2 (.sb2) conversion library for Scratch 3.0
159 lines (145 loc) • 4.96 kB
JavaScript
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';
/**
* Consume values for the byte stream with a iterator-like interface.
*/
class Consumer {
/**
* @param {object} options - Define the consumer.
* @param {function} [options.type=Value] - The {@link Field} type to
* create.
* @param {BytePrimitive} options.read - How to read the third Field
* argument. The third field argument is the value the field represented in
* the `.sb` file. It is either the Value's value, the Reference's index,
* or the Header's field size.
* @param {function} [options.value] - How to produce the third Field
* argument from a stream. Defaults to `stream =>
* stream.read(options.read)`.
*/
constructor ({
type = Value,
read,
value = read ? (stream => stream.read(read)) : null
}) {
this.type = type;
this.value = value;
}
/**
* @param {ByteStream} stream - Stream to read from.
* @param {TYPES} classId - FieldObject TYPES identifying the value to read.
* @param {number} position - Position in the stream the classId was read
* from.
* @returns {{value: *, done: boolean}} - An iterator.next() return value.
*/
next (stream, classId, position) {
return {
value: new this.type(classId, position, this.value(stream)),
done: false
};
}
}
/**
* @const CONSUMER_PROTOS
* @type {Object.<number, {type, read, value}>}
*/
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
* @type {Array.<Consumer|null>}
*/
const CONSUMERS = Array.from(
{length: 256},
(_, i) => {
if (CONSUMER_PROTOS[i]) return new Consumer(CONSUMER_PROTOS[i]);
return null;
}
);
const builtinConsumer = new Consumer({
type: BuiltinObjectHeader,
value: () => null
});
/**
* Field iterator.
*/
class FieldIterator {
/**
* @param {ArrayBuffer} buffer - Buffer to read from.
* @param {number} position - Position in buffer to start at.
*/
constructor (buffer, position) {
this.buffer = buffer;
this.stream = new ByteStream(buffer, position);
}
/**
* @returns {FieldIterator} - Returns this.
*/
[Symbol.iterator] () {
return this;
}
/**
* @returns {{value: *, done: boolean}} - An iterator.next() value.
*/
next () {
if (this.stream.position >= this.stream.uint8a.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};