diff --git a/extensions/src/common/extension/decorators/blocks.ts b/extensions/src/common/extension/decorators/blocks.ts index 0077118bc..b3b45c4e7 100644 --- a/extensions/src/common/extension/decorators/blocks.ts +++ b/extensions/src/common/extension/decorators/blocks.ts @@ -6,6 +6,7 @@ import { getImplementationName } from "../mixins/base/scratchInfo/index"; import { ExtensionInstance } from ".."; import { isFunction, isString, tryCreateBundleTimeEvent } from "$common/utils"; import { extractArgs } from "../mixins/base/scratchInfo/args"; +import { makeDecorator } from "$common/extension/decorators/newBlocks" type BlockFunctionMetadata = { methodName: string, @@ -16,9 +17,12 @@ type BlockFunctionMetadata = { export const blockBundleEvent = tryCreateBundleTimeEvent("blocks"); + export const getAccessorPrefix = "__getter__"; export const setAccessorPrefix = "__setter__"; +export const reporter = makeDecorator("reporter"); + /** * This a decorator function that should be associated with methods of your Extension class, all in order to turn your class methods * into Blocks that can be executed in the Block Programming Environment. @@ -52,6 +56,7 @@ export const setAccessorPrefix = "__setter__"; * ``` * @returns A manipulated version of the original method that is */ + export function block< const This extends ExtensionInstance, const Args extends any[], diff --git a/extensions/src/common/extension/decorators/newBlocks.ts b/extensions/src/common/extension/decorators/newBlocks.ts new file mode 100644 index 000000000..3eebdd40e --- /dev/null +++ b/extensions/src/common/extension/decorators/newBlocks.ts @@ -0,0 +1,188 @@ +import { getImplementationName } from "../mixins/base/scratchInfo/index"; +import { BlockMetadata } from "$common/types"; +import { blockBundleEvent } from "$common/extension/decorators/blocks"; +import { BlockType } from "$common/types/enums"; +import { ExtensionInstance } from ".."; +import type BlockUtility from "$scratch-vm/engine/block-utility"; +import { isFunction, isString, tryCreateBundleTimeEvent } from "$common/utils"; +import { extractArgs } from "../mixins/base/scratchInfo/args"; + + +type BlockFunctionMetadata = { + methodName: string, + scratchType: string, + args: string[], + returns: string, + } + + +type TRemoveUtil = any[] extends [...infer R extends any[], BlockUtility] ? R : any[] +//export const blockBundleEvent = tryCreateBundleTimeEvent("blocks"); + +export function makeDecorator(type: T): TemplateEngine["execute"] { + + // function takes T and returns a function of TemplateEngine type + // TemplateEngine returns based on the ScratchType of the block + return function(builderOrStrings, ...args) { + return function(target, context) { + //this is of type TypedMethodDecorator + //context is ClassMethodDecoratorContext + const opcode = target.name; + const internalFuncName = getImplementationName(opcode); + + const argList: any[] = args; + + const blockType = (type === "reporter") ? BlockType.Reporter : BlockType.Command; + const textFunction = (...args) => { + // Concatenate template strings and arguments dynamically + const strings = Array.isArray(builderOrStrings) ? builderOrStrings : [builderOrStrings]; + let result = ''; + strings.forEach((str, index) => { + result += str; + if (index < args.length) { + result += args[index]; + } + }); + return result; + }; + type Fn = (this: ExtensionInstance, value: any, util: BlockUtility) => void; + const blockInfo = { type: blockType, text: textFunction, args: argList }; + context.addInitializer(function () { this.pushBlock(opcode, blockInfo as BlockMetadata, target) }); + + const isProbableAtBundleTime = !isFunction(blockInfo); + if (isProbableAtBundleTime) { + const { type } = blockInfo; + blockBundleEvent?.fire({ + methodName: opcode, + args: extractArgs(blockInfo as BlockMetadata).map(a => isString(a) ? a : a.type), + // is 'any' an issue? Likely! + returns: type === "command" ? "void" : "any", + scratchType: blockInfo.type + }); + } + + + //target is the function being decorated, return it + return (function () { return this[internalFuncName].call(this, ...arguments) }); + //this is the actual function + } + } +} + + +// const reporter = makeDecorator("reporter"); +// //returns +// const command = makeDecorator("command"); + +// class Example { +// @reporter`Add ${{type: "number", default: 3}} to ${"number"}` +// simpleReporter(x: number, y: number) { +// return x + y; +// } +// } + +// Reporter is created, it says that the function will a type based on command or reporter +// the first part of the argument is the strings, and then the second part is the arguments in the string +// the function returned is the decorator function + + +namespace Utility { + export type TypedMethodDecorator< + This, + Args extends any[], //args that the decorator accepts + Return, //what the decorator returns + Fn extends (...args: Args) => Return // function signature that matches the method being decorated. + > = (target: Fn, context: ClassMethodDecoratorContext) => Fn; + //ClassMethodDecoratorContext is actually defining the decorator function + + export type Method = (this: This, ...args: Args) => Return; + //this is target's type + + export type TaggedTemplate = (strings: TemplateStringsArray, ...args: TArgs) => TReturn; +} + +namespace Framework { + export class ExtensionInstance { } + export type BlockUtility = { dummy: true } + // Q: what is the purpose of this? And why do they need to be filtered out? +} + +namespace Argument { + export type ScratchType = "number" | "string" | "angle"; + + export type ToType = + T extends "number" | "angle" ? number : + T extends "string" ? string : + never; + + export type ScratchConfig = T | { type: T, default?: ToType }; + + export type RemoveUtil = T extends [...infer R extends any[], Framework.BlockUtility] ? R : T; + + /** + * Transform plain typescript type to it's Scratch representation (`ScratchConfig`) + */ + export type FromType = + T extends number ? ScratchConfig<"number"> : + T extends string ? ScratchConfig<"string"> : + never; + + /** + * Transform arguments of method into a corresponding tuple of `ScratchConfig` types. + * + * NOTE: The second type parameter should not be specified. + */ + export type MapToScratch = RemoveUtil> = { + [k in keyof Internal]: FromType + } +} + +namespace Block { + export type ScratchType = "reporter" | "command"; + + export type Reportable = number | string; + + export type ToReturnType = + T extends "reporter" ? Reportable | Promise : + T extends "command" ? void | Promise : + never; + + export type Config = { + type: ScratchType, + text: string, + args: Argument.ScratchConfig[] + } + //Q: what is this used for? +} + +interface TemplateEngine { + /** + * + */ + execute< + const This extends Framework.ExtensionInstance, + const Args extends any[], + const Return extends Block.ToReturnType, + > + ( + strings: TemplateStringsArray, ...args: Argument.MapToScratch + ): Utility.TypedMethodDecorator>; + // parameters match builderOrStrings, ...args + // args are mapped + // return of block function is based on report or command + + /** + * + */ + execute< + const This extends Framework.ExtensionInstance, + const Args extends any[], + const Return extends Block.ToReturnType, + > + ( + builder: (instance: This, tag: Utility.TaggedTemplate, Block.Config>) => Block.Config + ): Utility.TypedMethodDecorator>; + // function takes the string of Tagged Template + // parameters match builderOrStrings, ...args + // return of block function is based on report or command +} \ No newline at end of file diff --git a/extensions/src/common/index.ts b/extensions/src/common/index.ts index bcce9b4bb..6c9636f02 100644 --- a/extensions/src/common/index.ts +++ b/extensions/src/common/index.ts @@ -17,6 +17,7 @@ export * from "./extension/GenericExtension"; export * from "./extension/ExtensionBase"; export * from "./extension/decorators/blocks"; +export * from "./extension/decorators/newBlocks"; export * from "./extension/index"; export * from "./extension/decorators/legacySupport/index"; export * from "./extension/decorators/validators"; \ No newline at end of file diff --git a/extensions/src/simple_example/index.ts b/extensions/src/simple_example/index.ts index 17c8c0b4c..f39f62db1 100644 --- a/extensions/src/simple_example/index.ts +++ b/extensions/src/simple_example/index.ts @@ -1,4 +1,4 @@ -import { ArgumentType, BlockType, BlockUtilityWithID, Environment, ExtensionMenuDisplayDetails, Language, Menu, SaveDataHandler, block, buttonBlock, extension, tryCastToArgumentType, untilTimePassed } from "$common"; +import { ArgumentType, BlockType, BlockUtilityWithID, Environment, ExtensionMenuDisplayDetails, Language, Menu, SaveDataHandler, block, buttonBlock, extension, tryCastToArgumentType, untilTimePassed, reporter } from "$common"; import jibo from "./jibo.png"; import five from "./five.png"; @@ -54,6 +54,11 @@ export default class SimpleTypescript extends extension(details, "ui", "customSa console.log(value); } + @reporter`TEST ${{type: "number", default: 3}} to ${"number"}` + simpleReporter(x: number, y: number) { + return x + y; + } + @block({ type: "command", args: [