From 21a79e79c8771a8676f2ed5e2ee75e437ae36b51 Mon Sep 17 00:00:00 2001 From: FurryR Date: Tue, 23 Jan 2024 21:18:46 +0800 Subject: [PATCH] :recycle: refactor Blockly hacks using new framework --- src/core/context.ts | 37 +- src/core/global.ts | 30 ++ src/core/index.ts | 3 + src/core/type.ts | 34 +- src/impl/block.ts | 776 ++++++++++++++++++++++++++++ src/impl/blockly/definition.ts | 867 -------------------------------- src/impl/blockly/extension.ts | 180 +++++++ src/impl/blockly/index.ts | 7 + src/impl/blockly/input.ts | 109 ++++ src/impl/blockly/middleware.ts | 102 ++++ src/impl/blockly/typing.ts | 36 ++ src/impl/context.ts | 12 +- src/impl/l10n.ts | 3 + src/impl/metadata.ts | 3 + src/impl/serialization/index.ts | 47 ++ src/impl/traceback/dialog.ts | 68 ++- src/impl/traceback/index.ts | 14 +- src/impl/traceback/inspector.ts | 39 +- src/impl/typing/index.ts | 39 +- src/impl/wrapper.ts | 19 + src/index.ts | 453 +++-------------- tsconfig.json | 2 +- 22 files changed, 1547 insertions(+), 1333 deletions(-) create mode 100644 src/impl/block.ts delete mode 100644 src/impl/blockly/definition.ts create mode 100644 src/impl/blockly/extension.ts create mode 100644 src/impl/blockly/index.ts create mode 100644 src/impl/blockly/input.ts create mode 100644 src/impl/blockly/middleware.ts create mode 100644 src/impl/blockly/typing.ts diff --git a/src/core/context.ts b/src/core/context.ts index a045529..7bdc694 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -7,12 +7,7 @@ export class LppReturn { * Construct a new LppReturn instance. * @param value Result. */ - constructor( - /** - * Result. - */ - public value: LppValue - ) { + constructor(public value: LppValue) { this.value = value } } @@ -35,16 +30,14 @@ export class LppException { * Construct a new LppException instance. * @param value Result. */ - constructor( - /** - * Result. - */ - public value: LppValue - ) { + constructor(public value: LppValue) { this.stack = [] } } export namespace LppTraceback { + /** + * Abstract base class of traceback. + */ export abstract class Base { abstract toString(): string } @@ -59,11 +52,8 @@ export namespace LppTraceback { * @param args Arguments. */ constructor( - /** Called function. */ public fn: LppFunction, - /** Self. */ public self: LppValue, - /** Arguments. */ public args: LppValue[] ) { super() @@ -73,6 +63,9 @@ export namespace LppTraceback { } } } +/** + * Closure. + */ export class LppClosure extends LppValue { value: Map = new Map() /** @@ -127,7 +120,7 @@ export class LppClosure extends LppValue { } } /** - * Lpp Scope. + * Context. */ export class LppContext { /** @@ -182,17 +175,8 @@ export class LppContext { * @param exceptionCallback Callback if function throws. */ constructor( - /** - * Parent closure. - */ public parent: LppContext | undefined, - /** - * Callback if function returns. - */ private _returnCallback: (value: LppReturn) => void, - /** - * Callback if function throws. - */ private _exceptionCallback: (value: LppException) => void ) { this.closure = new LppClosure() @@ -218,9 +202,6 @@ export class LppFunctionContext extends LppContext { */ constructor( parent: LppContext | undefined, - /** - * @type Self object. - */ public self: LppValue, returnCallback: (value: LppReturn) => void, exceptionCallback: (value: LppException) => void diff --git a/src/core/global.ts b/src/core/global.ts index 22c232e..4b17a7c 100644 --- a/src/core/global.ts +++ b/src/core/global.ts @@ -19,10 +19,16 @@ import { */ export const global = new Map() export namespace Global { + /** + * lpp builtin `Boolean` -- constructor of boolean types. + */ export const Boolean = LppFunction.native((_, args) => { if (args.length < 1) return new LppReturn(new LppConstant(false)) return new LppReturn(new LppConstant(asBoolean(args[0]))) }, new LppObject(new Map())) + /** + * lpp builtin `Number` -- constructor of number types. + */ export const Number = LppFunction.native((_, args) => { /** * Convert args to number. @@ -51,6 +57,9 @@ export namespace Global { } return new LppReturn(convertToNumber(args)) }, new LppObject(new Map())) + /** + * lpp builtin `String` -- constructor of string types. + */ export const String = LppFunction.native( (_, args) => { /** @@ -85,6 +94,9 @@ export namespace Global { ]) ) ) + /** + * lpp builtin `Array` -- constructor of array types. + */ export const Array = LppFunction.native( (_, args) => { /** @@ -121,6 +133,9 @@ export namespace Global { ]) ) ) + /** + * lpp builtin `Object` -- constructor of object types. + */ export const Object = LppFunction.native((_, args) => { /** * Convert args to object. @@ -133,6 +148,9 @@ export namespace Global { } return new LppReturn(convertToObject(args)) }, new LppObject(new Map())) + /** + * lpp builtin `Function` -- constructor of function types. + */ export const Function = LppFunction.native( (_, args) => { if (args.length < 1) @@ -197,6 +215,9 @@ export namespace Global { ]) ) ) + /** + * lpp builtin `Promise` -- represents the eventual completion (or failure) of an asynchronous operation and its resulting value. + */ export const Promise = LppFunction.native( (self, args) => { if ( @@ -276,6 +297,9 @@ export namespace Global { ]) ) ) + /** + * lpp builtin `Error` -- `Error` objects are thrown when runtime errors occur. + */ export const Error = LppFunction.native((self, args) => { if (self.instanceof(Error)) { self.set('value', args[0] ?? new LppConstant(null)) @@ -291,6 +315,9 @@ export namespace Global { return new LppException(res.value) } }, new LppObject(new Map())) + /** + * Lpp builtin `IllegalInvocationError` -- represents an error when trying to called a function with illegal arguments / context. + */ export const IllegalInvocationError = LppFunction.native( (self, args) => { if (self.instanceof(IllegalInvocationError)) { @@ -313,6 +340,9 @@ export namespace Global { }, new LppObject(new Map([['prototype', ensureValue(Error.get('prototype'))]])) ) + /** + * Lpp builtin `SyntaxError` -- represents an error when trying to interpret syntactically invalid code. + */ export const SyntaxError = LppFunction.native( (self, args) => { if (self.instanceof(SyntaxError)) { diff --git a/src/core/index.ts b/src/core/index.ts index f86c234..3be2be3 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,6 @@ +/** + * Lpp core (standard) implementation. + */ export { Global, global } from './global' export { LppClosure, diff --git a/src/core/type.ts b/src/core/type.ts index a4b5b9f..afcb84f 100644 --- a/src/core/type.ts +++ b/src/core/type.ts @@ -12,12 +12,7 @@ export class LppError extends Error { * Construct a new Lpp error. * @param id Error ID. */ - constructor( - /** - * Error ID. - */ - public id: string - ) { + constructor(public id: string) { super(`lpp: Error ${id}`) } } @@ -189,13 +184,7 @@ export class LppReference implements LppValue { */ constructor( parent: LppValue, - /** - * Key name. - */ public name: string, - /** - * Current object. - */ public value: LppValue ) { this.parent = new WeakRef(parent) @@ -512,15 +501,10 @@ export class LppConstant extends LppValue { throw new Error('lpp: unknown operand') } /** - * Constructs a value. - * @param value The value. + * Construct a value. + * @param _value The stored value. */ - constructor( - /** - * The stored value. - */ - private _value: T - ) { + constructor(private _value: T) { super() } } @@ -896,12 +880,7 @@ export class LppArray extends LppValue { * Construct an array object. * @param value Array of values. */ - constructor( - /** - * Array of values. - */ - public value: (LppValue | undefined)[] = [] - ) { + constructor(public value: (LppValue | undefined)[] = []) { super() } } @@ -1084,9 +1063,6 @@ export class LppFunction extends LppObject { * @param prototype Function prototype. */ constructor( - /** - * Function to execute. - */ private execute: ( self: LppValue, args: LppValue[] diff --git a/src/impl/block.ts b/src/impl/block.ts new file mode 100644 index 0000000..b4844b5 --- /dev/null +++ b/src/impl/block.ts @@ -0,0 +1,776 @@ +import type * as ScratchBlocks from 'blockly/core' +import { + BlocklyInstance, + Block, + Category, + Extension, + Reporter, + Input, + Command +} from './blockly' + +/** + * Compatible interface for FieldImageButton. + */ +interface FieldImageButton { + new ( + src: string, + width: number, + height: number, + callback: () => void, + opt_alt: string, + flip_rtl: boolean, + noPadding: boolean + ): ScratchBlocks.Field +} +/** + * Interface for mutable blocks. + */ +interface MutableBlock extends Block { + length: number +} +/** + * Detect if the specified block is mutable. + * @param block Block to detect. + * @returns True if the block is mutable, false otherwise. + */ +function isMutableBlock(block: Block): block is MutableBlock { + const v = block as MutableBlock + return typeof v.length === 'number' +} + +/** + * Generate FieldImageButton. + * @param Blockly Blockly instance. + * @returns FieldImageButton instance. + * @warning This section is ported from raw JavaScript. + * @author CST1229 + */ +const initalizeField = (() => { + let cache: FieldImageButton + return function initalizeField(Blockly: BlocklyInstance) { + interface Size { + height: number + width: number + } + type SizeConstructor = { + new (width: number, height: number): Size + } + const scratchBlocks = Blockly as BlocklyInstance & { + bindEventWithChecks_( + a: unknown, + b: unknown, + c: unknown, + d: unknown + ): unknown + BlockSvg: { + SEP_SPACE_X: number + } + } + const v = Blockly.FieldImage as unknown as ScratchBlocks.FieldImage & { + new (...args: unknown[]): { + fieldGroup_: unknown + mouseDownWrapper_: unknown + onMouseDown_: unknown + init(): void + getSvgRoot(): SVGGElement | null + render_(): void + size_: Size + } + } + // TODO: hover effect just like legacy one + /** + * @author CST1229 + */ + return ( + cache ?? + (cache = class FieldImageButton extends v { + /** + * Construct a FieldImageButton field. + * @param src Image source URL. + * @param width Image width. + * @param height Image height. + * @param callback Click callback. + * @param opt_alt Alternative text of the image. + * @param flip_rtl Whether to filp the image horizontally when in RTL mode. + * @param padding Whether the field has padding. + */ + constructor( + src: string, + width: number, + height: number, + private callback: () => void, + opt_alt: string, + flip_rtl: boolean, + private padding: boolean + ) { + super(src, width, height, opt_alt, flip_rtl) + } + /** + * Initalize the field. + */ + init() { + if (this.fieldGroup_) { + // Image has already been initialized once. + return + } + super.init() + this.mouseDownWrapper_ = scratchBlocks.bindEventWithChecks_( + this.getSvgRoot(), + 'mousedown', + this, + this.onMouseDown_ + ) + const svgRoot = this.getSvgRoot() + if (svgRoot) { + svgRoot.style.cursor = 'pointer' + } + } + /** + * Click handler. + */ + showEditor_() { + if (this.callback) { + this.callback() + } + } + /** + * Calculates the size of the field. + * @returns Size of the field. + */ + getSize(): Size { + if (!this.size_.width) { + this.render_() + } + if (this.padding) return this.size_ + return new (this.size_.constructor as SizeConstructor)( + Math.max(1, this.size_.width - scratchBlocks.BlockSvg.SEP_SPACE_X), + this.size_.height + ) + } + EDITABLE = true + } as unknown as FieldImageButton) + ) + } +})() + +/// Image from https://github.com/google/blockly-samples/tree/master/plugins/block-plus-minus + +/** + * Image of plus icon. + */ +const plusImage = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC' + + '9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPSJNMT' + + 'ggMTBoLTR2LTRjMC0xLjEwNC0uODk2LTItMi0ycy0yIC44OTYtMiAybC4wNzEgNGgtNC4wNz' + + 'FjLTEuMTA0IDAtMiAuODk2LTIgMnMuODk2IDIgMiAybDQuMDcxLS4wNzEtLjA3MSA0LjA3MW' + + 'MwIDEuMTA0Ljg5NiAyIDIgMnMyLS44OTYgMi0ydi00LjA3MWw0IC4wNzFjMS4xMDQgMCAyLS' + + '44OTYgMi0ycy0uODk2LTItMi0yeiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+Cg==' +/** + * Image of minus icon. + */ +const minusImage = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAw' + + 'MC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBkPS' + + 'JNMTggMTFoLTEyYy0xLjEwNCAwLTIgLjg5Ni0yIDJzLjg5NiAyIDIgMmgxMmMxLjEwNCAw' + + 'IDItLjg5NiAyLTJzLS44OTYtMi0yLTJ6IiBmaWxsPSJ3aGl0ZSIgLz48L3N2Zz4K' + +/** + * Defines extension. + * @param color Extension color. + * @param runtime Runtime instance. + * @param formatMessage Function to format message. + * @returns Extension. + */ +export function defineExtension( + color: string, + runtime: VM.Runtime, + formatMessage: (id: string) => string +): Extension { + /// code from https://github.com/google/blockly-samples/blob/master/plugins/block-plus-minus & Open Roberta Lab + /** + * Generates plus icon. + * @param Blockly Blockly instance. + * @param block Target block. + * @returns Field. + */ + const Plus = ( + Blockly: BlocklyInstance, + block: MutableBlock + ): ScratchBlocks.Field => { + const FieldImageButton = initalizeField(Blockly) + return new FieldImageButton( + plusImage, + 15, + 15, + () => { + Blockly.Events.setGroup(true) + if (block.mutationToDom && block.domToMutation) { + const state = block.mutationToDom() + const oldExtraState = Blockly.Xml.domToText(state) + const length = state.getAttribute('length') + if (length !== null) { + state.setAttribute('length', String(parseInt(length, 10) + 1)) + } + block.domToMutation(state) + block.initSvg() + block.render() + Blockly.Events.fire( + new Blockly.Events.BlockChange( + block, + 'mutation', + 'length', + oldExtraState, + Blockly.Xml.domToText(block.mutationToDom()) + ) + ) + } + Blockly.Events.setGroup(false) + }, + '+', + false, + true + ) + } + /** + * Generates minus icon. + * @param Blockly Blockly instance. + * @param block Target block. + * @returns Field. + */ + const Minus = ( + Blockly: BlocklyInstance, + block: MutableBlock + ): ScratchBlocks.Field => { + const FieldImageButton = initalizeField(Blockly) + return new FieldImageButton( + minusImage, + 15, + 15, + () => { + Blockly.Events.setGroup(true) + if (block.mutationToDom && block.domToMutation) { + const state = block.mutationToDom() + const oldExtraState = Blockly.Xml.domToText(state) + const length = state.getAttribute('length') + if (length !== null) { + state.setAttribute( + 'length', + String(Math.max(0, parseInt(length, 10) - 1)) + ) + } + block.domToMutation(state) + block.initSvg() + block.render() + Blockly.Events.fire( + new Blockly.Events.BlockChange( + block, + 'mutation', + 'length', + oldExtraState, + Blockly.Xml.domToText(block.mutationToDom()) + ) + ) + } + Blockly.Events.setGroup(false) + }, + '-', + false, + true + ) + } + /** + * Update buttons for mutable blocks. + * @param Blockly Blockly instance. + * @param block Target block. + */ + const updateButton = (Blockly: BlocklyInstance, block: MutableBlock) => { + if (block.length !== undefined) { + block.removeInput('MINUS', true) + block.removeInput('PLUS', true) + const start = block.inputList[0]?.name + block.appendDummyInput('PLUS').appendField(Plus(Blockly, block)) + if (start) block.moveInputBefore('PLUS', start) + if (block.length > 0) { + block.appendDummyInput('MINUS').appendField(Minus(Blockly, block)) + if (start) block.moveInputBefore('MINUS', start) + } + } + } + // TODO: optimization: only delete changed inputs for performance + /** + * Clean unused argument from vm. + * @param block Specified block. + * @author CST1229 + */ + const cleanInputs = (block: ScratchBlocks.BlockSvg) => { + const target = runtime.getEditingTarget() + const vmBlock = target?.blocks.getBlock(block.id) + if (!vmBlock || !target) return + + const usedInputs = new Set(block.inputList.map(i => i.name)) + + const inputs = vmBlock.inputs + for (const name of Object.keys(inputs)) { + const input = inputs[name] + if (!usedInputs.has(name)) { + const blocks = target.blocks as unknown as { + deleteBlock(input: unknown): unknown + } + blocks.deleteBlock(input.block) + blocks.deleteBlock(input.shadow) + delete inputs[name] + } + } + } + return new Extension('lpp', color) + .register( + /// Builtin + new Category(() => `#️⃣ ${formatMessage('lpp.category.builtin')}`) + .register( + 'builtinType', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.builtin.type')) + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['Boolean', 'Boolean'], + ['Number', 'Number'], + ['String', 'String'], + ['Array', 'Array'], + ['Object', 'Object'], + ['Function', 'Function'], + ['Promise', 'Promise'], + ['Generator', 'Generator'], + ['AsyncGenerator', 'AsyncGenerator'] + ]) as ScratchBlocks.Field, + 'value' + ) + }) + ) + .register( + 'builtinError', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.builtin.error')) + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['Error', 'Error'], + ['IllegalInvocationError', 'IllegalInvocationError'], + ['SyntaxError', 'SyntaxError'] + ]) as ScratchBlocks.Field, + 'value' + ) + }) + ) + .register( + 'builtinUtility', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.builtin.error')) + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['JSON', 'JSON'], + ['Math', 'Math'] + ]) as ScratchBlocks.Field, + 'value' + ) + }) + ) + ) + .register( + /// Construct + new Category(() => `🚧 ${formatMessage('lpp.category.construct')}`) + .register( + 'constructLiteral', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.literal')) + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['null', 'null'], + ['true', 'true'], + ['false', 'false'], + ['NaN', 'NaN'], + ['Infinity', 'Infinity'] + ]) as ScratchBlocks.Field, + 'value' + ) + }) + ) + .register( + 'constructNumber', + Reporter.Square((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.Number')) + Input.Text(block, 'BEGIN', [ + formatMessage('lpp.block.construct.Number'), + '(' + ]) + Input.String(block, 'value', '10') + Input.Text(block, 'END', `)`) + }) + ) + .register( + 'constructString', + Reporter.Square((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.String')) + Input.Text(block, 'BEGIN', [ + formatMessage('lpp.block.construct.String'), + '(' + ]) + Input.String(block, 'value', '🌟') + Input.Text(block, 'END', `)`) + }) + ) + .register('constructArray', { + init: Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.Array')) + const property = block as MutableBlock + Input.Text(block, 'BEGIN', '[') + /// Member + Input.Text(block, 'END', ']') + property.length = 0 + updateButton(instance, property) + }), + mutationToDom(_, block) { + const elem = document.createElement('mutation') + if (isMutableBlock(block)) { + elem.setAttribute('length', String(block.length)) + } + return elem + }, + domToMutation(instance, block, mutation: HTMLElement) { + const length = parseInt(mutation.getAttribute('length') ?? '0', 10) + if (isMutableBlock(block)) { + if (length > block.length) { + for (let i = block.length; i < length; i++) { + if (i > 0) { + block.appendDummyInput(`COMMA_${i}`).appendField(',') + block.moveInputBefore(`COMMA_${i}`, 'END') + } + Input.Any(block, `ARG_${i}`) + block.moveInputBefore(`ARG_${i}`, 'END') + } + } else { + for (let i = length; i < block.length; i++) { + block.removeInput(`ARG_${i}`, true) + block.removeInput(`COMMA_${i}`, true) + } + cleanInputs(block) + } + block.length = length + updateButton(instance, block) + } + } + }) + .register('constructObject', { + init: Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.Object')) + const property = block as MutableBlock + Input.Text(block, 'BEGIN', '{') + /// Member + Input.Text(block, 'END', '}') + property.length = 0 + updateButton(instance, property) + }), + mutationToDom(_, block) { + const elem = document.createElement('mutation') + if (isMutableBlock(block)) { + elem.setAttribute('length', String(block.length)) + } + return elem + }, + domToMutation(instance, block, mutation: HTMLElement) { + const length = parseInt(mutation.getAttribute('length') ?? '0', 10) + if (isMutableBlock(block)) { + if (length > block.length) { + for (let i = block.length; i < length; i++) { + if (i > 0) { + block.appendDummyInput(`COMMA_${i}`).appendField(',') + block.moveInputBefore(`COMMA_${i}`, 'END') + } + Input.String(block, `KEY_${i}`, '') + Input.Text(block, `COLON_${i}`, ':') + Input.Any(block, `VALUE_${i}`) + block.moveInputBefore(`VALUE_${i}`, 'END') + block.moveInputBefore(`COLON_${i}`, `VALUE_${i}`) + block.moveInputBefore(`KEY_${i}`, `COLON_${i}`) + } + } else { + for (let i = length; i < block.length; i++) { + block.removeInput(`KEY_${i}`, true) + block.removeInput(`COLON_${i}`, true) + block.removeInput(`VALUE_${i}`, true) + block.removeInput(`COMMA_${i}`, true) + } + cleanInputs(block) + } + block.length = length + updateButton(instance, block) + } + } + }) + .register('constructFunction', { + init: Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.construct.Function')) + const property = block as MutableBlock + Input.Text(block, 'BEGIN', [ + formatMessage('lpp.block.construct.Function'), + '(' + ]) + /// Signature + Input.Text(block, 'END', ')') + Input.Statement(block, 'SUBSTACK') + property.length = 0 + updateButton(instance, property) + }), + mutationToDom(_, block) { + const elem = document.createElement('mutation') + if (isMutableBlock(block)) { + elem.setAttribute('length', String(block.length)) + } + return elem + }, + domToMutation(instance, block, mutation: HTMLElement) { + const length = parseInt(mutation.getAttribute('length') ?? '0', 10) + if (isMutableBlock(block)) { + if (length > block.length) { + for (let i = block.length; i < length; i++) { + if (i > 0) { + block.appendDummyInput(`COMMA_${i}`).appendField(',') + block.moveInputBefore(`COMMA_${i}`, 'END') + } + Input.String(block, `ARG_${i}`, '') + block.moveInputBefore(`ARG_${i}`, 'END') + } + } else { + for (let i = length; i < block.length; i++) { + block.removeInput(`ARG_${i}`, true) + block.removeInput(`COMMA_${i}`, true) + } + cleanInputs(block) + } + block.length = length + updateButton(instance, block) + } + } + }) + ) + .register( + new Category(() => `🔢 ${formatMessage('lpp.category.operator')}`) + .register( + 'binaryOp', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.binaryOp')) + Input.Any(block, 'lhs') + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['=', '='], + ['.', '.'], + ['+', '+'], + ['-', '-'], + ['*', '*'], + ['/', '/'], + ['%', '%'], + ['==', '=='], + ['!=', '!='], + ['>', '>'], + ['<', '<'], + ['>=', '>='], + ['<=', '<='], + ['&&', '&&'], + ['||', '||'], + ['<<', '<<'], + ['>>', '>>'], + ['>>>', '>>>'], + ['&', '&'], + ['|', '|'], + ['^', '^'], + ['instanceof', 'instanceof'], + ['in', 'in'] + ]) as ScratchBlocks.Field, + 'op' + ) + Input.Any(block, 'rhs') + }) + ) + .register( + 'unaryOp', + Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.unaryOp')) + block.appendDummyInput().appendField( + new instance.FieldDropdown([ + ['+', '+'], + ['-', '-'], + ['!', '!'], + ['~', '~'], + ['delete', 'delete'], + ['await', 'await'], + ['yield', 'yield'], + ['yield*', 'yield*'] + ]) as ScratchBlocks.Field, + 'op' + ) + Input.Any(block, 'value') + }) + ) + .register('new', { + init: Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.new')) + const property = block as MutableBlock + Input.Text(block, 'LABEL', 'new') + Input.Any(block, 'fn') + Input.Text(block, 'BEGIN', '(') + /// Arguments + Input.Text(block, 'END', ')') + property.length = 0 + updateButton(instance, property) + }), + mutationToDom(_, block) { + const elem = document.createElement('mutation') + if (isMutableBlock(block)) { + elem.setAttribute('length', String(block.length)) + } + return elem + }, + domToMutation(instance, block, mutation: HTMLElement) { + const length = parseInt(mutation.getAttribute('length') ?? '0', 10) + if (isMutableBlock(block)) { + if (length > block.length) { + for (let i = block.length; i < length; i++) { + if (i > 0) { + block.appendDummyInput(`COMMA_${i}`).appendField(',') + block.moveInputBefore(`COMMA_${i}`, 'END') + } + Input.Any(block, `ARG_${i}`) + block.moveInputBefore(`ARG_${i}`, 'END') + } + } else { + for (let i = length; i < block.length; i++) { + block.removeInput(`ARG_${i}`, true) + block.removeInput(`COMMA_${i}`, true) + } + cleanInputs(block) + } + block.length = length + updateButton(instance, block) + } + } + }) + .register('call', { + init: Reporter.Square((instance, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.call')) + const property = block as MutableBlock + Input.Any(block, 'fn') + Input.Text(block, 'BEGIN', '(') + /// Arguments + Input.Text(block, 'END', ')') + property.length = 0 + updateButton(instance, property) + }), + mutationToDom(_, block) { + const elem = document.createElement('mutation') + if (isMutableBlock(block)) { + elem.setAttribute('length', String(block.length)) + } + return elem + }, + domToMutation(instance, block, mutation: HTMLElement) { + const length = parseInt(mutation.getAttribute('length') ?? '0', 10) + if (isMutableBlock(block)) { + if (length > block.length) { + for (let i = block.length; i < length; i++) { + if (i > 0) { + block.appendDummyInput(`COMMA_${i}`).appendField(',') + block.moveInputBefore(`COMMA_${i}`, 'END') + } + Input.Any(block, `ARG_${i}`) + block.moveInputBefore(`ARG_${i}`, 'END') + } + } else { + for (let i = length; i < block.length; i++) { + block.removeInput(`ARG_${i}`, true) + block.removeInput(`COMMA_${i}`, true) + } + cleanInputs(block) + } + block.length = length + updateButton(instance, block) + } + } + }) + .register( + 'self', + Reporter.Square((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.self')) + block.setCheckboxInFlyout(false) + Input.Text(block, 'LABEL', formatMessage('lpp.block.operator.self')) + }) + ) + .register( + 'var', + Reporter.Square((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.operator.var')) + Input.Text(block, 'LABEL', formatMessage('lpp.block.operator.var')) + Input.String(block, 'name', '🐺') + }) + ) + ) + .register( + new Category(() => `🤖 ${formatMessage('lpp.category.statement')}`) + .register( + 'return', + Command((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.statement.return')) + Input.Text( + block, + 'LABEL', + formatMessage('lpp.block.statement.return') + ) + Input.Any(block, 'value') + }) + ) + .register( + 'throw', + Command((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.statement.throw')) + Input.Text( + block, + 'LABEL', + formatMessage('lpp.block.statement.throw') + ) + Input.Any(block, 'value') + }) + ) + .register( + 'scope', + Command((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.statement.scope')) + Input.Text( + block, + 'LABEL', + formatMessage('lpp.block.statement.scope') + ) + Input.Statement(block, 'SUBSTACK') + }) + ) + .register( + 'try', + Command((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.statement.try')) + Input.Text(block, 'TRY', formatMessage('lpp.block.statement.try.1')) + Input.Statement(block, 'SUBSTACK') + Input.Text( + block, + 'CATCH', + formatMessage('lpp.block.statement.try.2') + ) + Input.Any(block, 'var') + Input.Statement(block, 'SUBSTACK_2') + }) + ) + .register( + 'nop', + Command((_, block) => { + block.setTooltip(formatMessage('lpp.tooltip.statement.nop')) + Input.Any(block, 'value') + }) + ) + ) +} diff --git a/src/impl/blockly/definition.ts b/src/impl/blockly/definition.ts deleted file mode 100644 index 5a77643..0000000 --- a/src/impl/blockly/definition.ts +++ /dev/null @@ -1,867 +0,0 @@ -import type * as ScratchBlocks from 'blockly/core' -export type BlocklyInstance = typeof ScratchBlocks -import type VM from 'scratch-vm' -export interface LppCompatibleBlock extends ScratchBlocks.BlockSvg { - plus?(): void - minus?(): void - setCategory(category: string): void -} -export interface LppCompatibleBlockly extends BlocklyInstance { - MutatorPlus?: { - new (): object - } - MutatorMinus?: { - new (): object - } - Mutator: { - new (_: null): ScratchBlocks.IIcon & { - block_: LppCompatibleBlock - createIcon(): void - } - } - utils: { - createSvgElement(a: string, b: unknown, c: unknown): unknown - isRightButton(a: unknown): boolean - } & typeof ScratchBlocks.utils - Colours: { - valueReportBackground: string - valueReportBorder: string - } - OUTPUT_SHAPE_SQUARE: number - OUTPUT_SHAPE_ROUND: number -} -export function defineBlocks( - Blockly: LppCompatibleBlockly, - color: string, - state: { mutatorClick: boolean }, - vm: VM, - formatMessage: (id: string) => string -) { - const simpleBlock = (fn: (this: LppCompatibleBlock) => unknown) => ({ - get: () => ({ init: fn }), - set: () => void 0 - }) - const advancedBlock = (v: Record) => ({ - get: () => v, - set: () => void 0 - }) - /// code from https://github.com/google/blockly-samples/blob/master/plugins/block-plus-minus & Open Roberta Lab - function getExtraBlockState(block: ScratchBlocks.Block) { - return block.mutationToDom - ? Blockly.Xml.domToText(block.mutationToDom()) - : '' - } - class MutatorPlus extends Blockly.Mutator { - drawIcon_(a: unknown) { - Blockly.utils.createSvgElement( - 'path', - { - class: 'blocklyIconSymbol', - height: '15', - width: '15', - opacity: '1', - d: 'M18 10h-4v-4c0-1.104-.896-2-2-2s-2 .896-2 2l.071 4h-4.071c-1.104 0-2 .896-2 2s.896 2 2 2l4.071-.071-.071 4.071c0 1.104.896 2 2 2s2-.896 2-2v-4.071l4 .071c1.104 0 2-.896 2-2s-.896-2-2-2z', - transform: 'scale(0.67) translate(0,8.45)' // 0.67 - }, - a - ) - } - iconClick_(a: unknown) { - if ( - !( - ( - this.block_.workspace as ScratchBlocks.Workspace & { - isDragging(): boolean - } - ).isDragging() || Blockly.utils.isRightButton(a) - ) - ) { - const block = this.block_ - state.mutatorClick = true - Blockly.Events.setGroup(true) - const oldExtraState = getExtraBlockState(block) - if (block.plus) block.plus() - const newExtraState = getExtraBlockState(block) - if (oldExtraState != newExtraState) { - Blockly.Events.fire( - new Blockly.Events.BlockChange( - block, - 'mutation', - null, - oldExtraState, - newExtraState - ) - ) - } - Blockly.Events.setGroup(false) - } - } - constructor() { - super(null) - } - } - class MutatorMinus extends Blockly.Mutator { - drawIcon_(a: unknown) { - Blockly.utils.createSvgElement( - 'path', - { - class: 'blocklyIconSymbol', - height: '15', - width: '15', - opacity: '1', - d: 'M18 11h-12c-1.104 0-2 .896-2 2s.896 2 2 2h12c1.104 0 2-.896 2-2s-.896-2-2-2z', - transform: 'scale(0.67) translate(0,8.45)' // 0.67 - }, - a - ) - } - iconClick_(a: unknown) { - if ( - !( - ( - this.block_.workspace as ScratchBlocks.Workspace & { - isDragging(): boolean - } - ).isDragging() || Blockly.utils.isRightButton(a) - ) - ) { - const block = this.block_ - state.mutatorClick = true - Blockly.Events.setGroup(true) - const oldExtraState = getExtraBlockState(block) - if (block.minus) block.minus() - const newExtraState = getExtraBlockState(block) - if (oldExtraState != newExtraState) { - Blockly.Events.fire( - new Blockly.Events.BlockChange( - block, - 'mutation', - null, - oldExtraState, - newExtraState - ) - ) - } - Blockly.Events.setGroup(false) - } - } - constructor() { - super(null) - } - } - Blockly.MutatorPlus = MutatorPlus - Blockly.MutatorMinus = MutatorMinus - type PatchedBlock = LppCompatibleBlock & { - setMutatorPlus(a?: MutatorPlus): void - setMutatorMinus(a?: MutatorMinus): void - mutatorPlus?: MutatorPlus - mutatorMinus?: MutatorMinus - length: number - } - ;(Blockly.BlockSvg.prototype as PatchedBlock).setMutatorPlus = function ( - a?: MutatorPlus - ) { - if (this.mutatorPlus && this.mutatorPlus !== a) { - this.mutatorPlus.dispose() - } - if ((this.mutatorPlus = a)) { - this.mutatorPlus.block_ = this - if (this.rendered) this.mutatorPlus.createIcon() - } - } - ;(Blockly.BlockSvg.prototype as PatchedBlock).setMutatorMinus = function ( - a?: MutatorMinus - ) { - if (this.mutatorMinus && this.mutatorMinus !== a) { - this.mutatorMinus.dispose() - } - if ((this.mutatorMinus = a)) { - this.mutatorMinus.block_ = this - if (this.rendered) this.mutatorMinus.createIcon() - } - } - const _update = Blockly.InsertionMarkerManager.prototype.update - Blockly.InsertionMarkerManager.prototype.update = function (a, b) { - const firstMarker = (this as unknown as { firstMarker_: PatchedBlock }) - .firstMarker_ - if (firstMarker.mutatorPlus || firstMarker.mutatorMinus) { - try { - return _update.call(this, a, b) - } catch (e) { - Blockly.Events.enable() - return - } - } - return _update.call(this, a, b) - } - const _getIcons = Blockly.BlockSvg.prototype.getIcons - Blockly.BlockSvg.prototype.getIcons = function () { - const res = _getIcons.call(this) - const self = this as PatchedBlock - self.mutatorPlus && - res.push(self.mutatorPlus as unknown as ScratchBlocks.IIcon) - self.mutatorMinus && res.push(self.mutatorMinus) - return res - } - /** - * Clean unused argument from vm. - * @author CST1229 - */ - function cleanInputs(this: ScratchBlocks.BlockSvg) { - const target = vm.editingTarget - if (!target) return - const vmBlock = target.blocks.getBlock(this.id) - if (!vmBlock) return - - const usedInputs = new Set(this.inputList.map(i => i?.name)) - - const inputs = vmBlock.inputs - for (const name of Object.keys(inputs)) { - const input = inputs[name] - if (!usedInputs.has(name)) { - const blocks = target.blocks as unknown as { - deleteBlock(input: unknown): unknown - } - blocks.deleteBlock(input.block) - blocks.deleteBlock(input.shadow) - delete inputs[name] - } - } - } - type Shadowable = ScratchBlocks.Input & { - connection: { setShadowDom(a: unknown): void; respawnShadow_(): void } - } - /** - * Append the shadow to the field. - * @param field Blockly field. - * @param defaultValue default value. - * @returns Field. - */ - function addShadow(field: Shadowable, defaultValue?: string): Shadowable { - const elem = document.createElement('shadow') - const child = document.createElement('field') - elem.setAttribute('type', 'text') - child.setAttribute('name', 'TEXT') - child.textContent = defaultValue ?? '' - elem.appendChild(child) - field.connection.setShadowDom(elem) - field.connection.respawnShadow_() - return field - } - /** - * Append null shadow to the field. - * @param field Blockly field. - * @returns Field. - */ - function addNullShadow(field: Shadowable) { - field.connection.setShadowDom(null) - field.connection.respawnShadow_() - return field - } - Object.defineProperties(Blockly.Blocks, { - // Builtins - lpp_builtinType: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_builtinType', - inputsInline: true, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - args0: [ - { - type: 'field_dropdown', - name: 'value', - options: [ - ['Boolean', 'Boolean'], - ['Number', 'Number'], - ['String', 'String'], - ['Array', 'Array'], - ['Object', 'Object'], - ['Function', 'Function'], - ['Promise', 'Promise'], - ['Generator', 'Generator'], - ['AsyncGenerator', 'AsyncGenerator'] - ] - } - ], - message0: '%1', - tooltip: formatMessage('lpp.tooltip.builtin.type') - }) - }), - lpp_builtinError: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_builtinError', - inputsInline: true, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - args0: [ - { - type: 'field_dropdown', - name: 'value', - options: [ - ['Error', 'Error'], - ['IllegalInvocationError', 'IllegalInvocationError'], - ['SyntaxError', 'SyntaxError'] - ] - } - ], - message0: '%1', - tooltip: formatMessage('lpp.tooltip.builtin.error') - }) - }), - lpp_builtinUtility: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_builtinError', - inputsInline: true, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - args0: [ - { - type: 'field_dropdown', - name: 'value', - options: [ - ['JSON', 'JSON'], - ['Math', 'Math'] - ] - } - ], - message0: '%1', - tooltip: formatMessage('lpp.tooltip.builtin.utility') - }) - }), - // Constructors - lpp_constructLiteral: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_constructLiteral', - inputsInline: true, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - args0: [ - { - type: 'field_dropdown', - name: 'value', - options: [ - ['null', 'null'], - ['true', 'true'], - ['false', 'false'], - ['NaN', 'NaN'], - ['Infinity', 'Infinity'] - ] - } - ], - message0: '%1', - tooltip: formatMessage('lpp.tooltip.construct.literal') - }) - }), - lpp_constructNumber: simpleBlock(function () { - this.setCategory('lpp') - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput() - .appendField(formatMessage('lpp.block.construct.Number')) - .appendField('(') - this.appendValueInput('value') - this.appendDummyInput().appendField(')') - this.setTooltip(formatMessage('lpp.tooltip.construct.Number')) - }), - lpp_constructString: simpleBlock(function () { - this.setCategory('lpp') - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput() - .appendField(formatMessage('lpp.block.construct.String')) - .appendField('(') - this.appendValueInput('value') - this.appendDummyInput().appendField(')') - this.setTooltip(formatMessage('lpp.tooltip.construct.String')) - }), - lpp_constructArray: advancedBlock({ - init: function (this: PatchedBlock) { - this.setCategory('lpp') - this.setMutatorPlus(new MutatorPlus()) - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput().appendField('[') - this.appendDummyInput('ENDBRACE').appendField(']') - this.setTooltip(formatMessage('lpp.tooltip.construct.Array')) - this.length = 0 - }, - mutationToDom: function () { - const elem = document.createElement('mutation') - elem.setAttribute('length', String(this.length)) - return elem - }, - domToMutation: function (this: PatchedBlock, mutation: HTMLElement) { - const attr = parseInt(mutation.getAttribute('length') ?? '0', 10) - // The following line also detects that attr is a number -- NaN > 0 will return false. - if (attr > 0) { - this.setMutatorMinus(new MutatorMinus()) - for (let i = 0; i < attr; i++) { - const input = this.appendValueInput(`ARG_${i}`) - if (!this.isInsertionMarker()) addNullShadow(input as Shadowable) - this.moveInputBefore(`ARG_${i}`, 'ENDBRACE') - if (i < attr - 1) { - this.appendDummyInput(`COMMA_${i}`).appendField(',') - this.moveInputBefore(`COMMA_${i}`, 'ENDBRACE') - } - } - } - this.length = attr - }, - plus: function (this: PatchedBlock) { - const arg = this.length++ - this.mutatorMinus ?? this.setMutatorMinus(new MutatorMinus()) - if (arg != 0) { - this.appendDummyInput(`COMMA_${arg}`).appendField(',') - this.moveInputBefore(`COMMA_${arg}`, 'ENDBRACE') - } - addNullShadow(this.appendValueInput(`ARG_${arg}`) as Shadowable) - this.moveInputBefore(`ARG_${arg}`, 'ENDBRACE') - this.initSvg() - this.render() - }, - minus: function (this: PatchedBlock) { - if (this.length > 0) { - this.length-- - if (this.length != 0) this.removeInput(`COMMA_${this.length}`) - else this.setMutatorMinus() - this.removeInput(`ARG_${this.length}`) - cleanInputs.call(this) - this.initSvg() - this.render() - } - } - }), - lpp_constructObject: advancedBlock({ - init: function (this: PatchedBlock) { - this.setCategory('lpp') - this.setMutatorPlus(new MutatorPlus()) - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput().appendField('{') - this.length = 0 - this.appendDummyInput('ENDBRACE').appendField('}') - this.setTooltip(formatMessage('lpp.tooltip.construct.Object')) - }, - mutationToDom: function (this: PatchedBlock) { - const elem = document.createElement('mutation') - elem.setAttribute('length', String(this.length)) - return elem - }, - domToMutation: function (this: PatchedBlock, mutation: HTMLElement) { - const attr = parseInt(mutation.getAttribute('length') ?? '0', 10) - // The following line also detects that attr is a number -- NaN > 0 will return false. - if (attr > 0) { - this.setMutatorMinus(new MutatorMinus()) - for (let i = 0; i < attr; i++) { - const key = this.appendValueInput(`KEY_${i}`) - const value = this.appendValueInput(`VALUE_${i}`) - this.appendDummyInput(`COLON_${i}`).appendField(':') - if (!this.isInsertionMarker()) { - addShadow(key as Shadowable) - addNullShadow(value as Shadowable) - } - this.moveInputBefore(`VALUE_${i}`, 'ENDBRACE') - this.moveInputBefore(`COLON_${i}`, `VALUE_${i}`) - this.moveInputBefore(`KEY_${i}`, `COLON_${i}`) - if (i < attr - 1) { - this.appendDummyInput(`COMMA_${i}`).appendField(',') - this.moveInputBefore(`COMMA_${i}`, 'ENDBRACE') - } - } - } - this.length = attr - }, - plus: function (this: PatchedBlock) { - const arg = this.length++ - this.mutatorMinus ?? this.setMutatorMinus(new MutatorMinus()) - if (arg != 0) { - this.appendDummyInput(`COMMA_${arg}`).appendField(',') - this.moveInputBefore(`COMMA_${arg}`, 'ENDBRACE') - } - this.appendDummyInput(`COLON_${arg}`).appendField(':') - addShadow(this.appendValueInput(`KEY_${arg}`) as Shadowable) - addNullShadow(this.appendValueInput(`VALUE_${arg}`) as Shadowable) - this.moveInputBefore(`VALUE_${arg}`, 'ENDBRACE') - this.moveInputBefore(`COLON_${arg}`, `VALUE_${arg}`) - this.moveInputBefore(`KEY_${arg}`, `COLON_${arg}`) - this.initSvg() - this.render() - }, - minus: function (this: PatchedBlock) { - if (this.length > 0) { - this.length-- - if (this.length != 0) this.removeInput(`COMMA_${this.length}`) - else this.setMutatorMinus() - this.removeInput(`KEY_${this.length}`) - this.removeInput(`COLON_${this.length}`) - this.removeInput(`VALUE_${this.length}`) - cleanInputs.call(this) - this.initSvg() - this.render() - } - } - }), - lpp_constructFunction: advancedBlock({ - init: function (this: PatchedBlock) { - this.setCategory('lpp') - this.setMutatorPlus(new MutatorPlus()) - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput() - .appendField(formatMessage('lpp.block.construct.Function')) - .appendField('(') - this.appendDummyInput('ENDBRACE').appendField(')') - this.appendStatementInput('SUBSTACK') - this.setTooltip(formatMessage('lpp.tooltip.construct.Function')) - this.length = 0 - }, - mutationToDom: function (this: PatchedBlock) { - const elem = document.createElement('mutation') - elem.setAttribute('length', String(this.length)) - return elem - }, - domToMutation: function (this: PatchedBlock, mutation: HTMLElement) { - const attr = parseInt(mutation.getAttribute('length') ?? '0', 10) - // The following line also detects that attr is a number -- NaN > 0 will return false. - if (attr > 0) { - this.setMutatorMinus(new MutatorMinus()) - for (let i = 0; i < attr; i++) { - const input = this.appendValueInput(`ARG_${i}`) - if (!this.isInsertionMarker()) addShadow(input as Shadowable) - this.moveInputBefore(`ARG_${i}`, 'ENDBRACE') - if (i < attr - 1) { - this.appendDummyInput(`COMMA_${i}`).appendField(',') - this.moveInputBefore(`COMMA_${i}`, 'ENDBRACE') - } - } - } - this.length = attr - }, - plus: function (this: PatchedBlock) { - const arg = this.length++ - this.mutatorMinus ?? this.setMutatorMinus(new MutatorMinus()) - if (arg != 0) { - this.appendDummyInput(`COMMA_${arg}`).appendField(',') - this.moveInputBefore(`COMMA_${arg}`, 'ENDBRACE') - } - addShadow(this.appendValueInput(`ARG_${arg}`) as Shadowable) - this.moveInputBefore(`ARG_${arg}`, 'ENDBRACE') - this.initSvg() - this.render() - }, - minus: function (this: PatchedBlock) { - if (this.length > 0) { - this.length-- - if (this.length != 0) this.removeInput(`COMMA_${this.length}`) - else this.setMutatorMinus() - this.removeInput(`ARG_${this.length}`) - cleanInputs.call(this) - this.initSvg() - this.render() - } - } - }), - // Operators - lpp_var: simpleBlock(function () { - this.setCategory('lpp') - this.setInputsInline(true) - this.setColour(color) - this.setOutput(true, 'String') - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setTooltip(formatMessage('lpp.tooltip.operator.var')) - this.appendDummyInput().appendField( - formatMessage('lpp.block.operator.var') - ) - this.appendValueInput('name') - }), - lpp_binaryOp: simpleBlock(function () { - this.setCategory('lpp') - this.setInputsInline(true) - this.setColour(color) - this.setOutput(true, 'String') - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setTooltip(formatMessage('lpp.tooltip.operator.binaryOp')) - this.appendValueInput('lhs') - this.appendDummyInput().appendField( - new Blockly.FieldDropdown([ - ['=', '='], - ['.', '.'], - ['+', '+'], - ['-', '-'], - ['*', '*'], - ['/', '/'], - ['%', '%'], - ['==', '=='], - ['!=', '!='], - ['>', '>'], - ['<', '<'], - ['>=', '>='], - ['<=', '<='], - ['&&', '&&'], - ['||', '||'], - ['<<', '<<'], - ['>>', '>>'], - ['>>>', '>>>'], - ['&', '&'], - ['|', '|'], - ['^', '^'], - ['instanceof', 'instanceof'], - ['in', 'in'] - ]) as ScratchBlocks.Field, - 'op' - ) - this.appendValueInput('rhs') - }), - lpp_unaryOp: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_unaryOp', - inputsInline: true, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - args0: [ - { - type: 'field_dropdown', - name: 'op', - options: [ - ['+', '+'], - ['-', '-'], - ['!', '!'], - ['~', '~'], - ['delete', 'delete'], - ['await', 'await'], - ['yield', 'yield'], - ['yield*', 'yield*'] - ] - }, - { - type: 'input_value', - name: 'value' - } - ], - message0: '%1%2', - tooltip: formatMessage('lpp.tooltip.operator.unaryOp') - }) - }), - lpp_self: simpleBlock(function () { - this.jsonInit({ - type: 'lpp_self', - inputsInline: true, - checkboxInFlyout: false, - category: 'lpp', - colour: color, - output: 'String', - outputShape: Blockly.OUTPUT_SHAPE_SQUARE, - message0: formatMessage('lpp.block.operator.self'), - tooltip: formatMessage('lpp.tooltip.operator.self') - }) - }), - lpp_call: advancedBlock({ - init: function (this: PatchedBlock) { - this.setCategory('lpp') - this.setMutatorPlus(new MutatorPlus()) - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendValueInput('fn') - this.appendDummyInput().appendField('(') - this.length = 0 - this.appendDummyInput('ENDBRACE').appendField(')') - this.setTooltip(formatMessage('lpp.tooltip.operator.call')) - }, - mutationToDom: function (this: PatchedBlock) { - const elem = document.createElement('mutation') - elem.setAttribute('length', String(this.length)) - return elem - }, - domToMutation: function (this: PatchedBlock, mutation: HTMLElement) { - const attr = parseInt(mutation.getAttribute('length') ?? '0', 10) - // The following line also detects that attr is a number -- NaN > 0 will return false. - if (attr > 0) { - this.setMutatorMinus(new MutatorMinus()) - for (let i = 0; i < attr; i++) { - if (!this.isInsertionMarker()) - addNullShadow(this.appendValueInput(`ARG_${i}`) as Shadowable) - this.moveInputBefore(`ARG_${i}`, 'ENDBRACE') - if (i < attr - 1) { - this.appendDummyInput(`COMMA_${i}`).appendField(',') - this.moveInputBefore(`COMMA_${i}`, 'ENDBRACE') - } - } - } - this.length = attr - }, - plus: function (this: PatchedBlock) { - const arg = this.length++ - this.mutatorMinus ?? this.setMutatorMinus(new MutatorMinus()) - if (arg != 0) { - this.appendDummyInput(`COMMA_${arg}`).appendField(',') - this.moveInputBefore(`COMMA_${arg}`, 'ENDBRACE') - } - addNullShadow(this.appendValueInput(`ARG_${arg}`) as Shadowable) - this.moveInputBefore(`ARG_${arg}`, 'ENDBRACE') - this.initSvg() - this.render() - }, - minus: function (this: PatchedBlock) { - if (this.length > 0) { - this.length-- - if (this.length != 0) this.removeInput(`COMMA_${this.length}`) - else this.setMutatorMinus() - this.removeInput(`ARG_${this.length}`) - cleanInputs.call(this) - this.initSvg() - this.render() - } - } - }), - lpp_new: advancedBlock({ - init: function (this: PatchedBlock) { - this.setCategory('lpp') - this.setMutatorPlus(new MutatorPlus()) - this.setColour(color) - this.setOutput(true, 'String') - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.appendDummyInput().appendField('new ') - this.appendValueInput('fn') - this.appendDummyInput().appendField('(') - this.length = 0 - this.appendDummyInput('ENDBRACE').appendField(')') - this.setTooltip(formatMessage('lpp.tooltip.operator.new')) - }, - mutationToDom: function (this: PatchedBlock) { - const elem = document.createElement('mutation') - elem.setAttribute('length', String(this.length)) - return elem - }, - domToMutation: function (this: PatchedBlock, mutation: HTMLElement) { - const attr = parseInt(mutation.getAttribute('length') ?? '0', 10) - // The following line also detects that attr is a number -- NaN > 0 will return false. - if (attr > 0) { - this.setMutatorMinus(new MutatorMinus()) - for (let i = 0; i < attr; i++) { - if (!this.isInsertionMarker()) - addNullShadow(this.appendValueInput(`ARG_${i}`) as Shadowable) - this.moveInputBefore(`ARG_${i}`, 'ENDBRACE') - if (i < attr - 1) { - this.appendDummyInput(`COMMA_${i}`).appendField(',') - this.moveInputBefore(`COMMA_${i}`, 'ENDBRACE') - } - } - } - this.length = attr - }, - plus: function (this: PatchedBlock) { - const arg = this.length++ - this.mutatorMinus ?? this.setMutatorMinus(new MutatorMinus()) - if (arg != 0) { - this.appendDummyInput(`COMMA_${arg}`).appendField(',') - this.moveInputBefore(`COMMA_${arg}`, 'ENDBRACE') - } - addNullShadow(this.appendValueInput(`ARG_${arg}`) as Shadowable) - this.moveInputBefore(`ARG_${arg}`, 'ENDBRACE') - this.initSvg() - this.render() - }, - minus: function (this: PatchedBlock) { - if (this.length > 0) { - this.length-- - if (this.length != 0) this.removeInput(`COMMA_${this.length}`) - else this.setMutatorMinus() - this.removeInput(`ARG_${this.length}`) - cleanInputs.call(this) - this.initSvg() - this.render() - } - } - }), - // Statements - lpp_return: simpleBlock(function () { - this.setCategory('lpp') - this.setInputsInline(true) - this.setColour(color) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setTooltip(formatMessage('lpp.tooltip.statement.return')) - this.setPreviousStatement(true, null) - this.appendDummyInput().appendField( - formatMessage('lpp.block.statement.return') - ) - this.appendValueInput('value') - }), - lpp_throw: simpleBlock(function () { - this.setCategory('lpp') - this.setInputsInline(true) - this.setColour(color) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setTooltip(formatMessage('lpp.tooltip.statement.throw')) - this.setPreviousStatement(true, null) - this.appendDummyInput().appendField( - formatMessage('lpp.block.statement.throw') - ) - this.appendValueInput('value') - }), - lpp_scope: simpleBlock(function () { - this.setCategory('lpp') - this.setColour(color) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setInputsInline(true) - this.setPreviousStatement(true, null) - this.setNextStatement(true, null) - this.appendDummyInput().appendField( - formatMessage('lpp.block.statement.scope') - ) - this.appendStatementInput('SUBSTACK') - this.setTooltip(formatMessage('lpp.tooltip.statement.scope')) - }), - lpp_try: simpleBlock(function () { - this.setCategory('lpp') - this.setColour(color) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setInputsInline(true) - this.setPreviousStatement(true, null) - this.setNextStatement(true, null) - this.appendDummyInput().appendField( - formatMessage('lpp.block.statement.try.1') - ) - this.appendStatementInput('SUBSTACK') - this.appendDummyInput().appendField( - formatMessage('lpp.block.statement.try.2') - ) - this.appendValueInput('var') - this.appendStatementInput('SUBSTACK_2') - this.setTooltip(formatMessage('lpp.tooltip.statement.try')) - }), - lpp_nop: simpleBlock(function () { - this.setCategory('lpp') - this.setInputsInline(true) - this.setColour(color) - this.setInputsInline(true) - this.setOutputShape(Blockly.OUTPUT_SHAPE_SQUARE) - this.setTooltip(formatMessage('lpp.tooltip.statement.nop')) - this.setPreviousStatement(true, null) - this.setNextStatement(true, null) - this.appendValueInput('value') - }) - }) -} diff --git a/src/impl/blockly/extension.ts b/src/impl/blockly/extension.ts new file mode 100644 index 0000000..61c49cf --- /dev/null +++ b/src/impl/blockly/extension.ts @@ -0,0 +1,180 @@ +import { Block, BlocklyInstance } from './typing' + +/** + * getInfo() metadata map. + */ +interface MetadataMap { + label: { + text: string + } + reporter: { + opcode: string + text: string + arguments: Record + } + button: { + text: string + onClick: () => void + } +} +/** + * Metadata for Scratch of a block. + */ +export type BlockMetadata = { + blockType: T +} & MetadataMap[T] +/** + * Metadata for Blockly block. + */ +export interface BlockDescriptor { + [key: string]: ( + this: BlockDescriptor, + Blockly: BlocklyInstance, + block: Block, + ...args: never[] + ) => unknown +} +/** + * Insertable block. + */ +export interface ExtensionBlock { + inject(Blockly: BlocklyInstance, extension: Extension): void + export(): BlockMetadata[] +} +/** + * Button (for documentation, etc.) + */ +export class Button implements ExtensionBlock { + inject() {} + export(): BlockMetadata[] { + return [ + { + blockType: 'button', + text: this.lazyText(), + onClick: this.onClick + } + ] + } + /** + * Construct a button. + * @param lazyText A function that returns button text. + * @param onClick click handler. + */ + constructor( + public lazyText: () => string, + public onClick: () => void + ) {} +} +/** + * Block category. + */ +export class Category { + private block: Map + /** + * Inject blocks to Blockly. Can be called multiple times for mixin. + * @param Blockly Blockly instance. + * @param extension Parent extension. + */ + inject(Blockly: BlocklyInstance, extension: Extension) { + for (const [key, value] of this.block.entries()) { + const res: Record = {} + for (const [key, originalFn] of Object.entries(value)) { + res[key] = function (this: Block, ...args: never[]) { + if (key === 'init') { + // Prepatch (color, icon, etc.) + this.setCategory(extension.id) + this.setInputsInline(true) + this.setColour(extension.color) + } + return originalFn.call(value, Blockly, this, ...args) + } + } + Reflect.defineProperty(Blockly.Blocks, `${extension.id}_${key}`, { + get() { + return res + }, + set() {}, + configurable: true + }) + } + } + /** + * Register a block under a category. + * @param name Block name (ID). + * @param block Block descriptor. + * @returns This for chaining. + */ + register( + name: string, + block: BlockDescriptor | BlockDescriptor[string] + ): this { + this.block.set(name, block instanceof Function ? { init: block } : block) + return this + } + /** + * Export blocks as Scratch metadata. + * @returns Scratch metadata. + */ + export(): BlockMetadata[] { + return [ + { + blockType: 'label', + text: this.lazyLabel() + } + ].concat( + Array.from(this.block.keys()).map(opcode => ({ + blockType: 'reporter', + opcode, + text: '', + arguments: {} + })) + ) as BlockMetadata[] + } + /** + * Construct a category. + * @param lazyLabel A function that returns category label. + */ + constructor(public lazyLabel: () => string) { + this.block = new Map() + } +} +/** + * Extension. + */ +export class Extension { + private blocks: ExtensionBlock[] = [] + /** + * Register an button, category, etc. + * @param block Object to register. + * @returns This for chaining. + */ + register(block: ExtensionBlock): this { + this.blocks.push(block) + return this + } + /** + * Inject blocks to Blockly. Can be called multiple times for mixin. + * @param Blockly + */ + inject(Blockly: BlocklyInstance) { + for (const v of this.blocks) { + v.inject(Blockly, this) + } + } + /** + * Export blocks as Scratch metadata. + * @returns Scratch metadata. + */ + export(): BlockMetadata[] { + return this.blocks.map(v => v.export()).flat(1) + } + /** + * Construct an extension. + * @param id Extension id. + * @param color Block color (experimental). + */ + constructor( + public id: string, + public color: string + ) {} +} diff --git a/src/impl/blockly/index.ts b/src/impl/blockly/index.ts new file mode 100644 index 0000000..ff36879 --- /dev/null +++ b/src/impl/blockly/index.ts @@ -0,0 +1,7 @@ +/** + * Standalone blockly helper utilities for creating extensions. + */ +export * from './typing' +export * from './extension' +export * from './middleware' +export * as Input from './input' diff --git a/src/impl/blockly/input.ts b/src/impl/blockly/input.ts new file mode 100644 index 0000000..f341a33 --- /dev/null +++ b/src/impl/blockly/input.ts @@ -0,0 +1,109 @@ +import type * as ScratchBlocks from 'blockly/core' + +type Shadowable = ScratchBlocks.Input & { + connection: { setShadowDom(a: unknown): void; respawnShadow_(): void } +} +type Gesture = ScratchBlocks.Workspace & { + currentGesture_?: { + isDraggingBlock_: boolean + targetBlock_?: ScratchBlocks.Block + } +} +/** + * Append string shadow to the field. + * @param field Blockly field. + * @param value Value. + * @returns Field. + */ +function addShadow(field: Shadowable, value: string): Shadowable { + const elem = document.createElement('shadow') + const child = document.createElement('field') + elem.setAttribute('type', 'text') + child.setAttribute('name', 'TEXT') + child.textContent = value + elem.appendChild(child) + field.connection.setShadowDom(elem) + field.connection.respawnShadow_() + return field +} +/** + * Append null shadow to the field. + * @param field Blockly field. + * @returns Field. + */ +function addNullShadow(field: Shadowable) { + field.connection.setShadowDom(null) + field.connection.respawnShadow_() + return field +} +/** + * Generate an input that allows string. + * @param block Target block. + * @param name Input name. + * @param value Input (default) value. + * @returns Input. + */ +export function String( + block: ScratchBlocks.Block, + name: string, + value: string +): ScratchBlocks.Input { + const field = block.appendValueInput(name) as Shadowable + const workspace = block.workspace as Gesture + if ( + block.isInsertionMarker() || + (workspace.currentGesture_?.isDraggingBlock_ && + workspace.currentGesture_?.targetBlock_?.type === block.type) + ) + return field + return addShadow(field, value) +} +/** + * Generate an input that allows anything (not directly). + * @param block Target block. + * @param name Input name. + * @returns Input. + */ +export function Any( + block: ScratchBlocks.Block, + name: string +): ScratchBlocks.Input { + const field = block.appendValueInput(name) as Shadowable + const workspace = block.workspace as Gesture + if ( + block.isInsertionMarker() || + (workspace.currentGesture_?.isDraggingBlock_ && + workspace.currentGesture_?.targetBlock_?.type === block.type) + ) + return field + return addNullShadow(field) +} +/** + * Generate text. + * @param block Target text. + * @param name Input name. + * @param value Text value. + * @returns Input. + */ +export function Text( + block: ScratchBlocks.Block, + name: string, + value: string | string[] +): ScratchBlocks.Input { + if (typeof value === 'string') return Text(block, name, [value]) + const input = block.appendDummyInput(name) + value.forEach(value => input.appendField(value)) + return input +} +/** + * Generate a statement input. + * @param block Target block. + * @param name Input name. + * @returns Input. + */ +export function Statement( + block: ScratchBlocks.Block, + name: string +): ScratchBlocks.Input { + return block.appendStatementInput(name) +} diff --git a/src/impl/blockly/middleware.ts b/src/impl/blockly/middleware.ts new file mode 100644 index 0000000..e34ee58 --- /dev/null +++ b/src/impl/blockly/middleware.ts @@ -0,0 +1,102 @@ +import { BlockDescriptor } from './extension' +import { Block, BlocklyInstance } from './typing' + +function _ReporterBase( + fn: ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void, + type: 'square' | 'round' +): ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] +) => void { + return function ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) { + block.setOutput(true, 'String') + block.setOutputShape( + type === 'square' + ? instance.OUTPUT_SHAPE_SQUARE + : instance.OUTPUT_SHAPE_ROUND + ) + return fn.call(this, instance, block, ...args) + } +} +/** + * Middleware to set a block as command. + * @param fn Function. + * @returns Processed function. + */ +export function Command( + fn: ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void +): ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] +) => void { + return function ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) { + block.setNextStatement(true) + block.setPreviousStatement(true) + block.setOutputShape(instance.OUTPUT_SHAPE_SQUARE) + return fn.call(this, instance, block, ...args) + } +} +/** + * Middlewares to set a block as reporter. + */ +export namespace Reporter { + export function Square( + fn: ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void + ): ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void { + return _ReporterBase(fn, 'square') + } + /** + * Middleware to set a block as reporter with round shape. + * @param fn Function. + * @returns Processed function. + */ + export function Round( + fn: ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void + ): ( + this: BlockDescriptor, + instance: BlocklyInstance, + block: Block, + ...args: never[] + ) => void { + return _ReporterBase(fn, 'round') + } +} diff --git a/src/impl/blockly/typing.ts b/src/impl/blockly/typing.ts new file mode 100644 index 0000000..47ac038 --- /dev/null +++ b/src/impl/blockly/typing.ts @@ -0,0 +1,36 @@ +import type * as ScratchBlocks from 'blockly/core' +type _BlocklyInstance = typeof ScratchBlocks +/** + * Blockly instance type. + */ +export interface BlocklyInstance extends _BlocklyInstance { + MutatorPlus?: { + new (): object + } + MutatorMinus?: { + new (): object + } + Mutator: { + new (_: null): ScratchBlocks.IIcon & { + block_: Block + createIcon(): void + } + } + utils: { + createSvgElement(a: string, b: unknown, c: unknown): unknown + isRightButton(a: unknown): boolean + } & typeof ScratchBlocks.utils + Colours: { + valueReportBackground: string + valueReportBorder: string + } + OUTPUT_SHAPE_SQUARE: number + OUTPUT_SHAPE_ROUND: number +} +/** + * extended ScratchBlocks.BlockSvg interface. + */ +export interface Block extends ScratchBlocks.BlockSvg { + setCategory(category: string): void + setCheckboxInFlyout(isInFlyout: boolean): void +} diff --git a/src/impl/context.ts b/src/impl/context.ts index fd4e5f0..cd853ae 100644 --- a/src/impl/context.ts +++ b/src/impl/context.ts @@ -1,21 +1,23 @@ import { LppContext, LppTraceback as CoreTraceback } from '../core' +/** + * Extended traceback namespace. + */ export namespace LppTraceback { export import Base = CoreTraceback.Base export import NativeFn = CoreTraceback.NativeFn + /** + * Block traceback. + */ export class Block extends CoreTraceback.Base { /** * Construct a traceback object. + * @param target Target ID. * @param block Block ID. * @param context Context. */ constructor( - /** - * Target ID. - */ public target: string, - /** Block ID. */ public block: string, - /** Context. */ public context?: LppContext ) { super() diff --git a/src/impl/l10n.ts b/src/impl/l10n.ts index 302d069..85705e2 100644 --- a/src/impl/l10n.ts +++ b/src/impl/l10n.ts @@ -1,3 +1,6 @@ +/** + * Localization of lpp extension. + */ export const locale = { en: { // Name diff --git a/src/impl/metadata.ts b/src/impl/metadata.ts index df1df12..6b594c1 100644 --- a/src/impl/metadata.ts +++ b/src/impl/metadata.ts @@ -1,6 +1,9 @@ import { LppFunction, LppReference, LppValue, ensureValue } from 'src/core' import { Global } from '../core/global' import { attachTypehint } from './serialization' +/** + * Attach type hint to builtin functions. + */ export function attachType() { function attachType(fn: LppValue | LppReference, signature: string[]) { const v = ensureValue(fn) diff --git a/src/impl/serialization/index.ts b/src/impl/serialization/index.ts index 3b5cfc8..15ac2ea 100644 --- a/src/impl/serialization/index.ts +++ b/src/impl/serialization/index.ts @@ -37,6 +37,14 @@ export interface SerializeMetadata extends LppFunction { */ isTypehint: boolean } +/** + * Attaches metadata (for serialization) to specified function. + * @param originalFn Function to attach. + * @param target The target which function belongs to. + * @param blocks Block container of the function. + * @param block Function block. + * @param signature Signature. + */ export function attachMetadata( originalFn: LppFunction, target: string | undefined, @@ -51,6 +59,11 @@ export function attachMetadata( v.signature = signature v.isTypehint = false } +/** + * Attach type hint to specified function. + * @param originalFn Function to attach. + * @param signature Signature. + */ export function attachTypehint(originalFn: LppFunction, signature: string[]) { const v = originalFn as SerializeMetadata v.signature = signature @@ -66,6 +79,12 @@ export function hasMetadata(fn: LppFunction): fn is SerializeMetadata { typeof v.isTypehint === 'boolean' ) } +/** + * Serialize all blocks related to specified block, including the block itself. + * @param container Block container. + * @param block Specified block. + * @returns Block list. + */ export function serializeBlock( container: VM.Blocks, block: VM.Block @@ -92,13 +111,26 @@ export function serializeBlock( res[block.id].parent = null return res } +/** + * Deserialize blocks to a container. + * @param container Block container. + * @param blocks Blocks to deserialize. + */ export function deserializeBlock( container: VM.Blocks, blocks: Record ) { container._blocks = blocks } +/** + * Validator of serialized block JSON. + */ export namespace Validator { + /** + * Check if value is a Field. + * @param value Value to check. + * @returns True if value is a Field, false otherwise. + */ export function isField(value: unknown): value is VM.Field { if (!(value instanceof Object)) return false const v = value as Record @@ -107,6 +139,11 @@ export namespace Validator { if (typeof v.value !== 'string') return false return true } + /** + * Check if value is an Input. + * @param value Value to check. + * @returns True if value is an Input, false otherwise. + */ export function isInput( container: Record, value: unknown @@ -118,6 +155,11 @@ export namespace Validator { if (typeof v.block !== 'string' || !(v.block in container)) return false return true } + /** + * Check if value is a Block. + * @param value Value to check. + * @returns True if value is a Block, false otherwise. + */ export function isBlock( container: Record, id: string, @@ -155,6 +197,11 @@ export namespace Validator { return false return true } + /** + * Check if value is valid serialized data. + * @param value Value to check. + * @returns True if value is valid, false otherwise. + */ export function isInfo(value: unknown): value is SerializationInfo { if (!(value instanceof Object)) return false const v = value as Record diff --git a/src/impl/traceback/dialog.ts b/src/impl/traceback/dialog.ts index 9e0f437..95f8b9b 100644 --- a/src/impl/traceback/dialog.ts +++ b/src/impl/traceback/dialog.ts @@ -1,8 +1,16 @@ import type ScratchBlocks from 'blockly/core' -import { LppCompatibleBlockly } from '../blockly/definition' +import { BlocklyInstance } from '../blockly' +/** + * Show an advanced visualReport with HTML elements. + * @param Blockly Blockly instance. + * @param id Block ID. + * @param value HTML Nodes. + * @param textAlign Text alignment. + * @returns Returns visualReport box element if available. + */ export function show( - Blockly: LppCompatibleBlockly, + Blockly: BlocklyInstance, id: string, value: (string | Node)[], textAlign: string @@ -31,14 +39,25 @@ export function show( ) return elem } +/** + * Generate an icon group. + * @param icons Icon nodes. + * @returns Element. + */ export function IconGroup(icons: (string | Node)[]): HTMLDivElement { const iconGroup = document.createElement('div') iconGroup.style.float = 'right' iconGroup.append(...icons) return iconGroup } +/** + * Generate a close icon. + * @param Blockly Blockly instance. + * @param title Alternative hint of the close icon. + * @returns Element. + */ export function CloseIcon( - Blockly: LppCompatibleBlockly, + Blockly: BlocklyInstance, title: string ): HTMLSpanElement { const icon = document.createElement('span') @@ -50,12 +69,20 @@ export function CloseIcon( icon.textContent = '❌' return icon } +/** + * Generate a help icon. + * @param title Alternative hint of the show icon. + * @param hideTitle Alternative hint of the hide icon. + * @param onShow Handles show behavior. + * @param onHide Handles hide behavior. + * @returns Element. + */ export function HelpIcon( title: string, - closeTitle: string, - onOpen: () => void, - onClose: () => void -) { + hideTitle: string, + onShow: () => void, + onHide: () => void +): HTMLSpanElement { let state = false const icon = document.createElement('span') icon.classList.add('lpp-traceback-icon') @@ -65,16 +92,21 @@ export function HelpIcon( if (state) { icon.textContent = '❓' icon.title = `❓ ${title}` - onClose() + onHide() } else { icon.textContent = '➖' - icon.title = `➖ ${closeTitle}` - onOpen() + icon.title = `➖ ${hideTitle}` + onShow() } state = !state }) return icon } +/** + * Generate title of visualReport window. + * @param value Title text. + * @returns Element. + */ export function Title(value: string): HTMLDivElement { const text = document.createElement('div') text.style.textAlign = 'left' @@ -84,12 +116,24 @@ export function Title(value: string): HTMLDivElement { text.title = text.textContent = value return text } +/** + * Generate a text element. + * @param value Text value. + * @param className Element class name. + * @returns Element. + */ export function Text(value: string, className?: string): HTMLSpanElement { const text = document.createElement('span') if (className) text.className = className text.textContent = value return text } +/** + * Generate an element group (aka div). + * @param value Nodes. + * @param className Group class name. + * @returns Element. + */ export function Div( value: (Node | string)[], className?: string @@ -99,7 +143,11 @@ export function Div( div.append(...value) return div } +/** + * Global style for traceback module. + */ export const globalStyle = document.createElement('style') +globalStyle.id = 'lpp-traceback-style' globalStyle.textContent = ` .lpp-traceback-icon { transition: text-shadow 0.25s ease-out; diff --git a/src/impl/traceback/index.ts b/src/impl/traceback/index.ts index 3e94a84..fed522b 100644 --- a/src/impl/traceback/index.ts +++ b/src/impl/traceback/index.ts @@ -1,6 +1,10 @@ -import type VM from 'scratch-vm' +/** + * Blockly traceback implementation. + */ + +import type { VM } from '../typing' import { LppException } from 'src/core' -import { LppCompatibleBlockly } from '../blockly/definition' +import { BlocklyInstance } from '../blockly' import * as Dialog from './dialog' import type ScratchBlocks from 'blockly/core' import { LppTraceback } from '../context' @@ -112,7 +116,7 @@ export function showTraceback(svgRoot: SVGAElement) { * @param target Target ID. */ export function warnError( - Blockly: LppCompatibleBlockly | undefined, + Blockly: BlocklyInstance | undefined, vm: VM, formatMessage: (id: string) => string, error: string, @@ -208,11 +212,11 @@ export function warnError( * Warn exception. * @param Blockly Blockly global instance. * @param vm VM instance. - * @param formatMessage formatMessage. + * @param formatMessage Function to format message. * @param exception Exception instance. */ export function warnException( - Blockly: LppCompatibleBlockly | undefined, + Blockly: BlocklyInstance | undefined, vm: VM, formatMessage: (id: string) => string, exception: LppException diff --git a/src/impl/traceback/inspector.ts b/src/impl/traceback/inspector.ts index 463c326..f80177e 100644 --- a/src/impl/traceback/inspector.ts +++ b/src/impl/traceback/inspector.ts @@ -7,23 +7,39 @@ import { Global } from 'src/core' import { Dialog } from '.' -import { BlocklyInstance } from '../blockly/definition' +import { BlocklyInstance } from '../blockly' import { hasMetadata } from '../serialization' -import type VM from 'scratch-vm' +import type { VM } from '../typing' import type ScratchBlocks from 'blockly/core' +/** + * Generate an inspector of specified object. + * @param Blockly Blockly instance. + * @param vm VM instance. + * @param formatMessage Function to format message. + * @param value The value to be inspected. + * @returns Element. + */ export function Inspector( Blockly: BlocklyInstance, vm: VM, formatMessage: (id: string) => string, value: LppValue ): HTMLSpanElement { + /** + * Generate an extend icon. + * @param title Alternative hint of the show icon. + * @param hideTitle Alternative hint of the hide icon. + * @param onShow Handles show behavior. + * @param onHide Handles hide behavior. + * @returns Element. + */ function ExtendIcon( title: string, - closeTitle: string, - onOpen: () => void, - onClose: () => void - ) { + hideTitle: string, + onShow: () => void, + onHide: () => void + ): HTMLSpanElement { let state = false const icon = document.createElement('span') icon.classList.add('lpp-traceback-icon') @@ -33,16 +49,21 @@ export function Inspector( if (state) { icon.textContent = '➕' icon.title = `➕ ${title}` - onClose() + onHide() } else { icon.textContent = '➖' - icon.title = `➖ ${closeTitle}` - onOpen() + icon.title = `➖ ${hideTitle}` + onShow() } state = !state }) return icon } + /** + * Internal function for member list. + * @param value Object. + * @returns List element. + */ function objView( value: LppArray | LppObject | LppFunction ): HTMLUListElement { diff --git a/src/impl/typing/index.ts b/src/impl/typing/index.ts index 3573f12..55ac24e 100644 --- a/src/impl/typing/index.ts +++ b/src/impl/typing/index.ts @@ -1,4 +1,4 @@ -import type VM from 'scratch-vm' +import type OriginalVM from 'scratch-vm' import type { Message } from 'format-message' import type { LppArray, @@ -16,13 +16,22 @@ import type { import type * as Serialization from '../serialization' import { Wrapper } from '../wrapper' +/** + * Definition of Scratch extension. + */ export interface ScratchExtension { getInfo(): object } +/** + * Definition of Scratch.translate(). + */ export interface TranslateFn { (message: Message, args?: object | undefined): string setup(newTranslations: object | Message | null): void } +/** + * Definition of Scratch object. + */ export interface ScratchContext { extensions: { register(ext: ScratchExtension): void @@ -31,6 +40,9 @@ export interface ScratchContext { translate: TranslateFn vm?: VM } +/** + * Definition of runtime (with compiler support, for Turbowarp). + */ export interface LppCompatibleRuntime extends VM.Runtime { lpp?: { LppValue: typeof LppValue @@ -55,6 +67,9 @@ export interface LppCompatibleRuntime extends VM.Runtime { ((...args: unknown[]) => unknown) | ((...args: unknown[]) => unknown)[] > } +/** + * Definition of lpp compatible thread. + */ export interface LppCompatibleThread extends VM.Thread { lpp?: LppContext isCompiled?: boolean @@ -63,13 +78,19 @@ export interface LppCompatibleThread extends VM.Thread { } tryCompile?(): void } -export interface LppCompatibleVM extends VM { +/** + * Definition of VM. + */ +export interface VM extends OriginalVM { _events: Record< keyof VM.RuntimeAndVirtualMachineEventMap, ((...args: unknown[]) => unknown) | ((...args: unknown[]) => unknown)[] > } -export interface LppCompatibleBlocks extends VM.Blocks { +/** + * Definition of Block container. + */ +export interface Blocks extends VM.Blocks { _cache: { _executeCached: Record< string, @@ -77,17 +98,29 @@ export interface LppCompatibleBlocks extends VM.Blocks { > } } +/** + * VM.Target constructor. + */ export interface TargetConstructor { new ({ blocks }: { blocks: VM.Blocks }): VM.Target } +/** + * VM.Sequencer constructor. + */ export interface SequencerConstructor { new (runtime: VM.Runtime): VM.Sequencer } +/** + * VM.Thread constructor. + */ export interface ThreadConstructor { new (id: string): VM.Thread STATUS_DONE: number STATUS_RUNNING: number } +/** + * VM.Blocks constructor. + */ export interface BlocksConstructor { new (runtime: VM.Runtime, optNoGlow?: boolean /** = false */): VM.Blocks } diff --git a/src/impl/wrapper.ts b/src/impl/wrapper.ts index 8da5652..2e4e378 100644 --- a/src/impl/wrapper.ts +++ b/src/impl/wrapper.ts @@ -1,13 +1,32 @@ +/** + * Wrapable object interface. + */ export interface Wrapable { toString(): string } +/** + * Wrapper for Scratch monitors. + */ export class Wrapper extends String { + /** + * Unwraps a wrapped object. + * @param value Wrapped object. + * @returns Unwrapped object. + */ static unwrap(value: unknown): unknown { return value instanceof Wrapper ? value.value : value } + /** + * toString method for Scratch monitors. + * @returns String display. + */ toString() { return this.value.toString() } + /** + * Construct a wrapped value. + * @param value Value to wrap. + */ constructor(public value: T) { super(value.toString()) } diff --git a/src/index.ts b/src/index.ts index bcfed8d..1b5ba5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,15 +5,15 @@ import { BlocksConstructor, - LppCompatibleBlocks, + Blocks, LppCompatibleRuntime, LppCompatibleThread, - LppCompatibleVM, + VM, ScratchContext, TargetConstructor, ThreadConstructor } from './impl/typing' -import type VM from 'scratch-vm' +import type * as ScratchBlocks from 'blockly/core' import { locale } from './impl/l10n' import { global, @@ -36,12 +36,14 @@ import { deserializeObject } from './core' import { Dialog, Inspector } from './impl/traceback' -import { LppCompatibleBlockly, defineBlocks } from './impl/blockly/definition' +import { BlocklyInstance } from './impl/blockly' +import { defineExtension } from './impl/block' import { LppTraceback } from './impl/context' import { warnError, warnException } from './impl/traceback' import * as Serialization from './impl/serialization' import { Wrapper } from './impl/wrapper' import { attachType } from './impl/metadata' +import { Extension } from './impl/blockly' declare let Scratch: ScratchContext ;(function (Scratch: ScratchContext) { @@ -78,35 +80,29 @@ declare let Scratch: ScratchContext /** * Virtual machine instance. */ - vm: LppCompatibleVM + vm: VM /** * ScratchBlocks instance. */ - Blockly?: LppCompatibleBlockly + Blockly?: BlocklyInstance /** - * Whether the extension is initalized. + * Blockly extension. */ - initalized: boolean - /** - * Shared isMutatorClick state. - */ - mutatorClick: boolean + extension: Extension /** * Scratch util. */ util?: VM.BlockUtility /** - * Constructs a new instance of lpp. + * Construct a new instance of lpp. * @param runtime Scratch runtime. */ constructor(runtime: VM.Runtime) { - this.initalized = false this.runtime = runtime as LppCompatibleRuntime this.Blockly = undefined - this.mutatorClick = false Scratch.translate.setup(locale) // step 1: get virtual machine instance - let virtualMachine: LppCompatibleVM | undefined + let virtualMachine: VM | undefined if (this.runtime._events['QUESTION'] instanceof Array) { for (const value of this.runtime._events['QUESTION']) { const v = hijack(value) as @@ -117,7 +113,7 @@ declare let Scratch: ScratchContext } | undefined if (v?.props?.vm) { - virtualMachine = v?.props?.vm as LppCompatibleVM | undefined + virtualMachine = v?.props?.vm as VM | undefined break } } @@ -130,17 +126,22 @@ declare let Scratch: ScratchContext } } | undefined - )?.props?.vm as LppCompatibleVM | undefined + )?.props?.vm as VM | undefined } if (!virtualMachine) throw new Error('lpp cannot get Virtual Machine instance.') this.vm = virtualMachine + this.extension = defineExtension( + color, + this.runtime, + this.formatMessage.bind(this) + ) // step 2: get ScratchBlocks instance if (this.vm._events['EXTENSION_ADDED'] instanceof Array) { for (const value of this.vm._events['EXTENSION_ADDED']) { const v = hijack(value) as | { - ScratchBlocks?: LppCompatibleBlockly + ScratchBlocks?: BlocklyInstance } | undefined if (v?.ScratchBlocks) { @@ -152,26 +153,63 @@ declare let Scratch: ScratchContext this.Blockly = ( hijack(this.vm._events['EXTENSION_ADDED']) as | { - ScratchBlocks?: LppCompatibleBlockly + ScratchBlocks?: BlocklyInstance } | undefined )?.ScratchBlocks } if (this.Blockly) { - console.groupCollapsed( - '❗ Undo/Redo feature is disabled in order to avoid undo bug.' - ) - console.log( - '🔗 Reference: https://github.com/FurryR/lpp-scratch/issues/1' - ) - console.groupEnd() - ;( - this.Blockly.Events as unknown as { recordUndo: boolean } - ).recordUndo = false + const Blockly = this.Blockly + const Events = Blockly.Events as unknown as { + Change: BlocklyInstance['Events']['Abstract'] + Create: BlocklyInstance['Events']['Abstract'] + Move: BlocklyInstance['Events']['Abstract'] + } + // Patch: redo bug -- force re-render the block after change + const _Change = Events.Change.prototype.run + Events.Change.prototype.run = function (_forward: boolean) { + _Change.call(this, _forward) + const self = this as unknown as { + blockId: string + } + const block = this.getEventWorkspace_().getBlockById( + self.blockId + ) as ScratchBlocks.BlockSvg | null + if (block instanceof Blockly.BlockSvg) { + block.initSvg() + block.render() + } + } + // Patch: undo bug -- silent fail if block is not exist + const _Move = Events.Move.prototype.run + Events.Move.prototype.run = function (_forward: boolean) { + // pre-check before run + const self = this as unknown as { + blockId: string + } + const block = this.getEventWorkspace_().getBlockById( + self.blockId + ) as ScratchBlocks.BlockSvg | null + if (block) _Move.call(this, _forward) + } + const _Create = Events.Create.prototype.run + Events.Create.prototype.run = function (_forward: boolean) { + // patch before run + const self = this as unknown as { + ids: string[] + } + const res: string[] = [] + const workspace = this.getEventWorkspace_() + for (const id of self.ids) { + if (workspace.getBlockById(id)) res.push(id) + } + self.ids = res + _Create.call(this, _forward) + } } // Ignore SAY and QUESTION calls on dummy target. const _emit = this.runtime.emit - this.runtime.emit = (event: string, ...args: unknown[]): void => { + this.runtime.emit = (event: string, ...args: unknown[]) => { const blacklist = ['SAY', 'QUESTION'] if ( blacklist.includes(event) && @@ -195,7 +233,7 @@ declare let Scratch: ScratchContext this.Blockly ) { Dialog.show( - this.Blockly as LppCompatibleBlockly, + this.Blockly as BlocklyInstance, blockId, [ Inspector( @@ -407,218 +445,12 @@ declare let Scratch: ScratchContext * @returns Extension info. */ getInfo() { - // Sometimes getInfo() is called multiple times due to engine defects. - if (!this.initalized) { - this.initalized = true - if (this.Blockly) { - defineBlocks( - this.Blockly, - color, - this, - this.vm, - this.formatMessage.bind(this) - ) - } - } + if (this.Blockly) this.extension.inject(this.Blockly) return { id: 'lpp', name: this.formatMessage('lpp.name'), color1: color, - blocks: [ - { - blockType: 'label', - text: `#️⃣ ${this.formatMessage('lpp.category.builtin')}` - }, - { - opcode: 'builtinType', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - menu: 'dummy' - } - } - }, - { - opcode: 'builtinError', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - menu: 'dummy' - } - } - }, - { - opcode: 'builtinUtility', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - menu: 'dummy' - } - } - }, - { - blockType: 'label', - text: `🚧 ${this.formatMessage('lpp.category.construct')}` - }, - { - opcode: 'constructLiteral', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - menu: 'dummy' - } - } - }, - { - opcode: 'constructNumber', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - defaultValue: '10' - } - } - }, - { - opcode: 'constructString', - blockType: 'reporter', - text: '[value]', - arguments: { - value: { - type: 'string', - defaultValue: '🌟' - } - } - }, - { - opcode: 'constructArray', - blockType: 'reporter', - text: '' - }, - { - opcode: 'constructObject', - blockType: 'reporter', - text: '' - }, - { - opcode: 'constructFunction', - blockType: 'reporter', - text: '' - }, - { - blockType: 'label', - text: `🔢 ${this.formatMessage('lpp.category.operator')}` - }, - { - opcode: 'binaryOp', - blockType: 'reporter', - text: '[lhs][op][rhs]', - arguments: { - lhs: { type: 'string' }, - op: { - type: 'string', - menu: 'dummy' - }, - rhs: { type: 'string' } - } - }, - { - opcode: 'unaryOp', - blockType: 'reporter', - text: '[op][value]', - arguments: { - op: { - type: 'string', - menu: 'dummy' - }, - value: { type: 'any' } - } - }, - { - opcode: 'call', - blockType: 'reporter', - text: '[fn]', - arguments: { - fn: { type: 'any' } - } - }, - { - opcode: 'new', - blockType: 'reporter', - text: '[fn]', - arguments: { - fn: { type: 'any' } - } - }, - { - opcode: 'self', - blockType: 'reporter', - text: '' - }, - { - opcode: 'var', - blockType: 'reporter', - text: '[name]', - arguments: { - name: { - type: 'string', - defaultValue: '🐺' - } - } - }, - { - blockType: 'label', - text: `🤖 ${this.formatMessage('lpp.category.statement')}` - }, - { - opcode: 'return', - isTerminal: true, - blockType: 'command', - text: '[value]', - arguments: { - value: { type: 'any' } - } - }, - { - opcode: 'throw', - isTerminal: true, - blockType: 'command', - text: '[value]', - arguments: { - value: { type: 'any' } - } - }, - { - opcode: 'scope', - blockType: 'command', - text: '' - }, - { - opcode: 'try', - blockType: 'command', - text: '[var]', - arguments: { - var: { type: 'any' } - } - }, - { - opcode: 'nop', - blockType: 'command', - text: '[value]', - arguments: { - value: { type: 'any' } - } - } - ], + blocks: this.extension.export(), menus: { dummy: { acceptReporters: false, @@ -636,15 +468,7 @@ declare let Scratch: ScratchContext args: { value: string }, util: VM.BlockUtility ): Wrapper | void { - const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const instance = global.get(args.value) if (instance) { return new Wrapper(instance) @@ -685,15 +509,7 @@ declare let Scratch: ScratchContext args: { value: unknown }, util: VM.BlockUtility ): Wrapper | void { - const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const res = (() => { switch (args.value) { case 'null': @@ -721,16 +537,8 @@ declare let Scratch: ScratchContext args: { lhs: unknown; op: string | number; rhs: unknown }, util: VM.BlockUtility ): Wrapper | Wrapper | void { - const { thread } = util this.util = util try { - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const lhs = Wrapper.unwrap(args.lhs) const rhs = Wrapper.unwrap(args.rhs) const res = (() => { @@ -820,16 +628,8 @@ declare let Scratch: ScratchContext ['yield', 'yield'], ['yield*', 'yield*'] */ - const { thread } = util this.util = util try { - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const value = Wrapper.unwrap(args.value) if (!(value instanceof LppValue || value instanceof LppReference)) throw new LppError('syntaxError') @@ -913,13 +713,6 @@ declare let Scratch: ScratchContext fn = Wrapper.unwrap(fn) const actualArgs: LppValue[] = [] // runtime hack by @FurryR. - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const block = this.getActiveBlockInstance(args, thread) const len = parseInt(this.getMutation(block)?.length ?? '0', 10) for (let i = 0; i < len; i++) { @@ -978,13 +771,6 @@ declare let Scratch: ScratchContext // runtime hack by @FurryR. const actualArgs: LppValue[] = [] // runtime hack by @FurryR. - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const block = this.getActiveBlockInstance(args, thread) const len = parseInt(this.getMutation(block)?.length ?? '0', 10) for (let i = 0; i < len; i++) { @@ -1026,13 +812,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const lppThread = thread as LppCompatibleThread if (lppThread.lpp) { const unwind = lppThread.lpp.unwind() @@ -1088,13 +867,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const arr = new LppArray() const block = this.getActiveBlockInstance(args, thread) const len = parseInt(this.getMutation(block)?.length ?? '0', 10) @@ -1122,13 +894,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const obj = new LppObject() const block = this.getActiveBlockInstance(args, thread) const len = parseInt(this.getMutation(block)?.length ?? '0', 10) @@ -1169,13 +934,6 @@ declare let Scratch: ScratchContext this.util = util const Target = target.constructor as TargetConstructor // runtime hack by @FurryR. - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const block = this.getActiveBlockInstance(args, thread) const signature: string[] = [] const len = parseInt( @@ -1270,13 +1028,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } const lppThread = thread as LppCompatibleThread if (lppThread.lpp) { const v = lppThread.lpp.get(args.name) @@ -1296,13 +1047,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } value = Wrapper.unwrap(value) if (!(value instanceof LppValue || value instanceof LppReference)) throw new LppError('syntaxError') @@ -1329,13 +1073,6 @@ declare let Scratch: ScratchContext try { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } value = Wrapper.unwrap(value) if (!(value instanceof LppValue || value instanceof LppReference)) throw new LppError('syntaxError') @@ -1371,13 +1108,6 @@ declare let Scratch: ScratchContext try { const { thread, target } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } // runtime hack by @FurryR. const block = this.getActiveBlockInstance(args, thread) const id = block.inputs.SUBSTACK?.block @@ -1432,13 +1162,6 @@ declare let Scratch: ScratchContext try { const { thread, target } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } // runtime hack by @FurryR. const block = this.getActiveBlockInstance(args, thread) const dest = Wrapper.unwrap(args.var) @@ -1531,13 +1254,6 @@ declare let Scratch: ScratchContext nop({ value }: { value: unknown }, util: VM.BlockUtility): unknown { const { thread } = util this.util = util - if (this.shouldExit(thread)) { - try { - return thread.stopThisScript() - } catch (_) { - return - } - } if ( (thread as LppCompatibleThread).isCompiled && thread.stackClick && @@ -1554,7 +1270,7 @@ declare let Scratch: ScratchContext * Handle syntax error. * @param e Error object. */ - private handleError(e: unknown): void { + private handleError(e: unknown) { if (e instanceof LppError) { const thread = this.util?.thread if (thread) { @@ -1577,7 +1293,7 @@ declare let Scratch: ScratchContext * Handle unhandled exceptions. * @param e LppException object. */ - private handleException(e: LppException): void { + private handleException(e: LppException) { warnException(this.Blockly, this.vm, this.formatMessage.bind(this), e) this.runtime.stopAll() } @@ -1634,18 +1350,6 @@ declare let Scratch: ScratchContext this.handleException(result) } } - /** - * Detect if the block should exit directly. - * @param thread Current thread. - * @returns Whether the block is triggered by clicking on the mutator icon. - */ - private shouldExit(thread: VM.Thread): boolean { - if (this.mutatorClick) { - if (thread.stack.length === 1) this.mutatorClick = false - if (thread.stackClick) return true - } - return false - } /** * Get active block instance of specified thread. * @param args Block arguments. @@ -1656,7 +1360,7 @@ declare let Scratch: ScratchContext args: object, thread: LppCompatibleThread ): VM.Block { - const container = thread.target.blocks as LppCompatibleBlocks + const container = thread.target.blocks as Blocks const id = thread.isCompiled ? thread.peekStack() : container._cache._executeCached[thread.peekStack()]?._ops?.find( @@ -1695,7 +1399,7 @@ declare let Scratch: ScratchContext }) target.id = '' target.runtime = this.runtime - const warnFn = (): void => { + const warnFn = () => { this.handleError(new LppError('useAfterDispose')) this.runtime.stopAll() } @@ -1752,10 +1456,7 @@ declare let Scratch: ScratchContext }) const info: Serialization.SerializationInfo = { signature, - script: Serialization.serializeBlock( - blocks as LppCompatibleBlocks, - block - ), + script: Serialization.serializeBlock(blocks as Blocks, block), block: block.id } return serializeObject(info) diff --git a/tsconfig.json b/tsconfig.json index c4b5788..e88ab2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,7 @@ // "./types/*" // ] "scratch-vm": ["./node_modules/@turbowarp/types/index.d.ts"], - "scratch-blocks": ["./node_modules/@turbowarp/types/index.d.ts"] + }, "lib": ["ESNext", "DOM"], "target": "ESNext",