diff --git a/gulpfile.js b/gulpfile.js index 42774aaecfc4..5c61c266df04 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -38,7 +38,6 @@ const pxtlib = () => compileTsProject("pxtlib"); const pxtcompiler = () => compileTsProject("pxtcompiler"); const pxtpy = () => compileTsProject("pxtpy"); const pxtsim = () => compileTsProject("pxtsim"); -const pxtblocks = () => compileTsProject("pxtblocks"); const pxtrunner = () => compileTsProject("pxtrunner", "built", true); const pxteditor = () => compileTsProject("pxteditor", "built", true); const pxtweb = () => compileTsProject("docfiles/pxtweb", "built/web"); @@ -46,16 +45,7 @@ const backendutils = () => compileTsProject("backendutils") const cli = () => compileTsProject("cli", "built", true); const webapp = () => compileTsProject("webapp", "built", true); const reactCommon = () => compileTsProject("react-common", "built/react-common", true); - -const pxtblockly = () => gulp.src([ - "webapp/public/blockly/blockly_compressed.js", - "webapp/public/blockly/blocks_compressed.js", - "webapp/public/blockly/plugins.js", - "webapp/public/blockly/msg/js/en.js", - "built/pxtblocks.js" -]) - .pipe(concat("pxtblockly.js")) - .pipe(gulp.dest("built")); +const pxtblocks = () => compileTsProject("pxtblocks", "built/pxtblocks", true); const pxtapp = () => gulp.src([ "node_modules/lzma/src/lzma_worker-min.js", @@ -86,7 +76,6 @@ const pxtembed = () => gulp.src([ "built/pxtlib.js", "built/pxtcompiler.js", "built/pxtpy.js", - "built/pxtblockly.js", "built/pxtsim.js", "built/web/runnerembed.js" ]) @@ -121,8 +110,8 @@ function initWatch() { const tasks = [ pxtlib, gulp.parallel(pxtcompiler, pxtsim, backendutils), - gulp.parallel(pxtpy, gulp.series(copyBlockly, pxtblocks, pxtblockly)), - pxteditor, + pxtpy, + gulp.parallel(pxtblocks, pxteditor), gulp.parallel(pxtrunner, cli, pxtcommon), gulp.parallel(updatestrings, browserifyEmbed), gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), @@ -142,7 +131,7 @@ function initWatch() { gulp.watch("./backendutils/**/*", gulp.series(backendutils, ...tasks.slice(2))); gulp.watch("./pxtpy/**/*", gulp.series(pxtpy, ...tasks.slice(3))); - gulp.watch("./pxtblocks/**/*", gulp.series(gulp.series(copyBlockly, pxtblocks, pxtblockly), ...tasks.slice(3))); + gulp.watch("./pxtblocks/**/*", gulp.series(pxtblocks, ...tasks.slice(4))); gulp.watch("./pxteditor/**/*", gulp.series(pxteditor, ...tasks.slice(4))); @@ -162,7 +151,7 @@ function initWatchCli() { const tasks = [ pxtlib, gulp.parallel(pxtcompiler), - gulp.parallel(pxtpy, gulp.series(pxtblocks, pxtblockly)), + pxtpy, cli, notifyBuildComplete ] @@ -172,7 +161,6 @@ function initWatchCli() { gulp.watch("./pxtcompiler/**/*", gulp.series(pxtcompiler, ...tasks.slice(2))); gulp.watch("./pxtpy/**/*", gulp.series(pxtpy, ...tasks.slice(3))); - gulp.watch("./pxtblockly/**/*", gulp.series(gulp.series(copyBlockly, pxtblocks, pxtblockly), ...tasks.slice(3))); gulp.watch("./cli/**/*", gulp.series(cli, ...tasks.slice(5))); } @@ -388,8 +376,6 @@ const copyWebapp = () => "built/pxtlib.js", "built/pxtcompiler.js", "built/pxtpy.js", - "built/pxtblocks.js", - "built/pxtblockly.js", "built/pxtsim.js", "built/webapp/src/worker.js", "built/webapp/src/serviceworker.js", @@ -401,17 +387,16 @@ const copyWebapp = () => const copySemanticFonts = () => gulp.src("node_modules/semantic-ui-less/themes/default/assets/fonts/*") .pipe(gulp.dest("built/web/fonts")) -const browserifyWebapp = () => process.env.PXT_ENV == 'production' ? - exec('node node_modules/browserify/bin/cmd ./built/webapp/src/app.js -g [ envify --NODE_ENV production ] -g uglifyify -o ./built/web/main.js') : - exec('node node_modules/browserify/bin/cmd built/webapp/src/app.js -o built/web/main.js --debug') +const execBrowserify = (entryPoint, outfile) => process.env.PXT_ENV == 'production' ? + exec(`node node_modules/browserify/bin/cmd ${entryPoint} -g [ envify --NODE_ENV production ] -g [ uglifyify --ignore '**/node_modules/@blockly/**' ] -o ${outfile}`) : + exec(`node node_modules/browserify/bin/cmd ${entryPoint} -o ${outfile} --debug`); -const browserifyAssetEditor = () => process.env.PXT_ENV == 'production' ? - exec('node node_modules/browserify/bin/cmd ./built/webapp/src/assetEditor.js -g [ envify --NODE_ENV production ] -g uglifyify -o ./built/web/pxtasseteditor.js') : - exec('node node_modules/browserify/bin/cmd built/webapp/src/assetEditor.js -o built/web/pxtasseteditor.js --debug') +const browserifyWebapp = () => execBrowserify("./built/webapp/src/app.js", "./built/web/main.js"); + +const browserifyAssetEditor = () => execBrowserify("./built/webapp/src/assetEditor.js", "./built/web/pxtasseteditor.js"); + +const browserifyEmbed = () => execBrowserify("./built/pxtrunner/embed.js", "./built/web/runnerembed.js"); -const browserifyEmbed = () => process.env.PXT_ENV == 'production' ? - exec('node node_modules/browserify/bin/cmd ./built/pxtrunner/embed.js -g [ envify --NODE_ENV production ] -g uglifyify -o ./built/web/runnerembed.js') : - exec('node node_modules/browserify/bin/cmd built/pxtrunner/embed.js -o built/web/runnerembed.js --debug') const buildSVGIcons = () => { let webfontsGenerator = require('@vusion/webfonts-generator') @@ -534,37 +519,6 @@ const copyMonaco = gulp.series( stripMonacoSourceMaps ); - - -/******************************************************** - Blockly -*********************************************************/ - -const copyBlocklyCompressed = () => gulp.src([ - "node_modules/pxt-blockly/blocks_compressed.js", - "node_modules/pxt-blockly/blockly_compressed.js" -]) - .pipe(gulp.dest("webapp/public/blockly/")); - -const copyBlocklyExtensions = () => gulp.src("node_modules/@blockly/**/dist/index.js") - .pipe(concat("plugins.js")) - .pipe(gulp.dest("webapp/public/blockly/")); - -const copyBlocklyEnJs = () => gulp.src("node_modules/pxt-blockly/msg/js/en.js") - .pipe(gulp.dest("webapp/public/blockly/msg/js/")); - -const copyBlocklyEnJson = () => gulp.src("node_modules/pxt-blockly/msg/json/en.json") - .pipe(gulp.dest("webapp/public/blockly/msg/json/")); - -const copyBlocklyMedia = () => gulp.src("node_modules/pxt-blockly/media/*") - .pipe(gulp.dest("webapp/public/blockly/media")) - -const copyBlocklyTypings = () => gulp.src("node_modules/pxt-blockly/typings/blockly.d.ts") - .pipe(gulp.dest("localtypings/")) - -const copyBlockly = gulp.parallel(copyBlocklyCompressed, copyBlocklyExtensions, copyBlocklyEnJs, copyBlocklyEnJson, copyBlocklyMedia, copyBlocklyTypings); - - /******************************************************** Skillmap *********************************************************/ @@ -764,7 +718,10 @@ const testtutorials = testTask("tutorial-test", "tutorialrunner.js"); const testlanguageservice = testTask("language-service", "languageservicerunner.js"); const testpxteditor = pxtEditorTestTask(); -const buildKarmaRunner = () => compileTsProject("tests/blocklycompiler-test", "built/tests/", true); +const buildKarmaRunner = () => compileTsProject("tests/blocklycompiler-test", "built/", true); +const browserifyKarma = () => + exec('node node_modules/browserify/bin/cmd built/tests/blocklycompiler-test/test.spec.js -o built/tests/karma-test-runner.js --debug'); + const runKarma = () => { let command; if (isWin32) { @@ -777,9 +734,11 @@ const runKarma = () => { } return exec(command, true); } -const karma = gulp.series(buildKarmaRunner, runKarma); +const karma = gulp.series(buildKarmaRunner, browserifyKarma, runKarma); -const buildBlocksTestRunner = () => compileTsProject("tests/blocks-test", "built/tests", true) +const buildBlocksTestRunner = () => compileTsProject("tests/blocks-test", "built/", true); +const browserifyBlocksTestRunner = () => + exec('node node_modules/browserify/bin/cmd built/tests/blocks-test/blocksrunner.js -o built/tests/blocksrunner.js --debug'); const testAll = gulp.series( testdecompiler, @@ -849,11 +808,10 @@ const buildAll = gulp.series( updatestrings, maybeUpdateWebappStrings(), copyTypescriptServices, - copyBlocklyTypings, gulp.parallel(pxtlib, pxtweb), gulp.parallel(pxtcompiler, pxtsim, backendutils), - gulp.parallel(pxtpy, gulp.series(copyBlockly, pxtblocks, pxtblockly)), - pxteditor, + pxtpy, + gulp.parallel(pxteditor, pxtblocks), gulp.parallel(pxtrunner, cli, pxtcommon), browserifyEmbed, gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), @@ -866,6 +824,7 @@ const buildAll = gulp.series( browserifyAssetEditor, gulp.parallel(semanticjs, copyJquery, copyWebapp, copySemanticFonts, copyMonaco), buildBlocksTestRunner, + browserifyBlocksTestRunner, runUglify ); @@ -876,7 +835,7 @@ exports.clean = clean; exports.build = buildAll; exports.webapp = gulp.series( - reactCommon, + gulp.parallel(reactCommon, pxtblocks, pxteditor), webapp, browserifyWebapp, browserifyAssetEditor @@ -884,7 +843,6 @@ exports.webapp = gulp.series( exports.skillmapTest = testSkillmap; exports.updatestrings = updatestrings; -exports.updateblockly = copyBlockly; exports.lint = lint exports.testdecompiler = testdecompiler; exports.testlang = testlang; @@ -915,7 +873,7 @@ exports.testpxteditor = testpxteditor; exports.cli = gulp.series( gulp.parallel(pxtlib, pxtweb), gulp.parallel(pxtcompiler, pxtsim, backendutils), - gulp.parallel(pxtpy, gulp.series(copyBlockly, pxtblocks, pxtblockly)), + pxtpy, pxteditor, gulp.parallel(pxtrunner, cli, pxtcommon), pxtjs diff --git a/karma.conf.js b/karma.conf.js index bd31a0a10dd1..812a0903b95c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -21,9 +21,8 @@ module.exports = function(config) { 'built/web/typescript.js', 'webapp/public/blockly/**/*.js', 'built/pxtlib.js', - 'built/pxtblocks.js', 'built/pxtcompiler.js', - 'built/tests/tests.spec.js', + 'built/tests/karma-test-runner.js', // test assets { pattern: 'tests/blocklycompiler-test/cases/*.blocks', watched: false, included: false, served: true, nocache: false }, diff --git a/localtypings/evaluation.d.ts b/localtypings/evaluation.d.ts new file mode 100644 index 000000000000..a0630a704e9d --- /dev/null +++ b/localtypings/evaluation.d.ts @@ -0,0 +1,5 @@ +namespace pxt.blocks { + export interface EvaluationResult { + result: boolean; + } +} \ No newline at end of file diff --git a/localtypings/navigationController.d.ts b/localtypings/navigationController.d.ts new file mode 100644 index 000000000000..549a7062b600 --- /dev/null +++ b/localtypings/navigationController.d.ts @@ -0,0 +1,19 @@ +type WorkspaceSvg = import("blockly").WorkspaceSvg; + +declare module '@blockly/keyboard-navigation' { + class NavigationController { + init(): void; + addWorkspace(workspace: WorkspaceSvg): void; + enable(workspace: WorkspaceSvg): void; + disable(workspace: WorkspaceSvg): void; + focusToolbox(workspace: WorkspaceSvg): void; + navigation: Navigation; + } + + class Navigation { + resetFlyout(workspace: WorkspaceSvg, shouldHide: boolean): void; + setState(workspace: WorkspaceSvg, state: BlocklyNavigationState): void; + } + + type BlocklyNavigationState = "workspace" | "toolbox" | "flyout"; +} \ No newline at end of file diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index ce1888bcde92..d4d471eeb2ab 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -1,6 +1,5 @@ /// /// -/// /// declare namespace pxt { @@ -356,7 +355,8 @@ declare namespace pxt { invertedMonaco?: boolean; // if true: use the vs-dark monaco theme invertedGitHub?: boolean; // inverted github view lightToc?: boolean; // if true: do NOT use inverted style in docs toc - blocklyOptions?: Blockly.BlocklyOptions; // Blockly options, see Configuration: https://developers.google.com/blockly/guides/get-started/web + // FIXME (riknoll): Can't use Blockly types here + blocklyOptions?: any; // Blockly options, see Configuration: https://developers.google.com/blockly/guides/get-started/web hideFlyoutHeadings?: boolean; // Hide the flyout headings at the top of the flyout when on a mobile device. monacoColors?: pxt.Map; // Monaco theme colors, see https://code.visualstudio.com/docs/getstarted/theme-color-reference simAnimationEnter?: string; // Simulator enter animation @@ -398,8 +398,8 @@ declare namespace pxt { useUploadMessage?: boolean; // change "Download" text to "Upload" downloadIcon?: string; // which icon io use for download blockColors?: Map; // block namespace colors, used for build in categories - blockIcons?: Map; - blocklyColors?: Blockly.Colours; // Blockly workspace, flyout and other colors + blockIcons?: Map; + blocklyColors?: pxt.Map; // Overrides for the styles in the workspace Blockly.Theme.ComponentStyle socialOptions?: SocialOptions; // show social icons in share dialog, options like twitter handle and org handle noReloadOnUpdate?: boolean; // do not notify the user or reload the page when a new app cache is downloaded appPathNames?: string[]; // Authorized URL paths in embedded apps, all other paths will display a warning banner diff --git a/localtypings/pxtblockly.d.ts b/localtypings/pxtblockly.d.ts index 836c58581514..fe5bebe8a69e 100644 --- a/localtypings/pxtblockly.d.ts +++ b/localtypings/pxtblockly.d.ts @@ -1,135 +1,16 @@ -/// -/// +import * as pxtblockly from "../built/pxtblocks/index"; +import * as Blockly from "blockly"; +type pxtblockly_ = typeof pxtblockly; +type Blockly_ = typeof Blockly; -declare namespace Blockly { +declare global { + namespace pxt.blocks { + interface PxtBlockly extends pxtblockly_ { - /** - * Block Definitions - */ - - interface BlockDefinition { - codeCard?: any; - init: () => void; - getVars?: () => any[]; - renameVar?: (oldName: string, newName: string) => void; - customContextMenu?: any; - getProcedureCall?: () => string; - renameProcedure?: (oldName: string, newName: string) => void; - defType_?: string; - onchange?: (event: any) => void; - mutationToDom?: () => Element; - domToMutation?: (xmlElement: Element) => void; - } - - const Blocks: { - [index: string]: BlockDefinition; - } - - // if type == controls_if - class IfBlock extends Block { - elseifCount_: number; - elseCount_: number; - } - - /** - * Custom Fields - */ - - interface FieldCustomOptions { - blocksInfo: any; - colour?: string | number; - label?: string; - type?: string; - } - - interface FieldCustomDropdownOptions extends FieldCustomOptions { - data?: any; - } - - interface FieldCustom extends Field { - isFieldCustom_: boolean; - saveOptions?(): pxt.Map; - restoreOptions?(map: pxt.Map): void; - } - - interface FieldCustomConstructor { - new(text: string, options: FieldCustomOptions, validator?: Function): FieldCustom; - } - - /** - * Functions - */ - - namespace PXTBlockly { - namespace FunctionUtils { - let argumentIcons: {[typeName: string]: string}; - let argumentDefaultNames: {[typeName: string]: string}; - function createCustomArgumentReporter(typeName: string, ws: Blockly.Workspace): Blockly.Block; - } - } - - namespace Functions { - interface ArgumentInfo { - type: string; - name: string; - id: string; } - type ConfirmEditCallback = (mutation: Element) => void; - } - class FunctionBlockAbstract extends BlockSvg { - getArguments: () => Functions.ArgumentInfo[]; - } + interface BlocklyModule extends Blockly_ { - class FunctionDeclarationBlock extends FunctionBlockAbstract { - updateFunctionSignature: () => void; - addBooleanExternal(): void; - addStringExternal(): void; - addNumberExternal(): void; - addArrayExternal(): void; - addCustomExternal(typeName: string): void; + } } - - class FunctionDefinitionBlock extends FunctionBlockAbstract { } - class FunctionCallBlock extends FunctionBlockAbstract { } -} - -/** - * Blockly Keyboard Navigation plugin - * Used for accessible blocks experiment - */ - -declare class NavigationController { - init(): void; - addWorkspace(workspace: Blockly.WorkspaceSvg): void; - enable(workspace: Blockly.WorkspaceSvg): void; - disable(workspace: Blockly.WorkspaceSvg): void; - focusToolbox(workspace: Blockly.WorkspaceSvg): void; - navigation: Navigation; -} - -declare class Navigation { - resetFlyout(workspace: Blockly.WorkspaceSvg, shouldHide: boolean): void; - setState(workspace: Blockly.WorkspaceSvg, state: BlocklyNavigationState): void; -} - -declare type BlocklyNavigationState = "workspace" | "toolbox" | "flyout"; - -/** - * Blockly Workspace Search plugin - * Used for accessible blocks experiment - */ - -declare class WorkspaceSearch { - constructor(workspace: Blockly.WorkspaceSvg); - protected workspace_: Blockly.WorkspaceSvg; - protected htmlDiv_: HTMLDivElement; - protected inputElement_: HTMLInputElement; - init(): void; - protected createDom_(): void; - protected addEvent_(node: Element, name: string, thisObject: Object, func: Function): void; - open(): void; - close(): void; - previous(): void; - next(): void; } \ No newline at end of file diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index a34eae36c407..ebba04fdd713 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1,5 +1,4 @@ /// -/// /// /// @@ -292,7 +291,7 @@ declare namespace pxt.editor { ts: string; // rendering options snippetMode?: boolean; - layout?: pxt.blocks.BlockLayout; + layout?: BlockLayout; } export interface EditorMessageRunEvalRequest extends EditorMessageRequest { @@ -593,6 +592,14 @@ declare namespace pxt.editor { Disabled = 2 } + export const enum BlockLayout { + None = 0, + Align = 1, + // Shuffle deprecated + Clean = 3, + Flow = 4 + } + export type EditorType = 'blocks' | 'ts'; export interface EditorEvent { @@ -920,7 +927,10 @@ declare namespace pxt.editor { blocksScreenshotAsync(pixelDensity?: number, encodeBlocks?: boolean): Promise; renderBlocksAsync(req: pxt.editor.EditorMessageRenderBlocksRequest): Promise; renderPythonAsync(req: pxt.editor.EditorMessageRenderPythonRequest): Promise; - getBlocks(): Blockly.Block[]; + + // FIXME (riknoll) need to figure out how to type this better + // getBlocks(): Blockly.Block[]; + getBlocks(): any[]; toggleHighContrast(): void; setHighContrast(on: boolean): void; @@ -1013,7 +1023,10 @@ declare namespace pxt.editor { export interface IFieldCustomOptions { selector: string; - editor: Blockly.FieldCustomConstructor; + // FIXME (riknoll) need to figure out how to type this better. Also this type is from pxtblocks, but + // it uses types dervied from Blockly + // editor: Blockly.FieldCustomConstructor; + editor: any; text?: string; validator?: any; } diff --git a/package.json b/package.json index 967f4b12cff6..ba00e7c7ad81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pxt-core", - "version": "9.3.16", + "version": "10.0.0", "description": "Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors", "keywords": [ "TypeScript", @@ -66,13 +66,14 @@ "npm": ">=8.0.0" }, "dependencies": { - "@blockly/keyboard-navigation": "^0.1.18", - "@blockly/plugin-workspace-search": "^4.0.10", + "@blockly/keyboard-navigation": "0.5.4", + "@blockly/plugin-workspace-search": "8.0.9", "@fortawesome/fontawesome-free": "^5.15.4", "@microsoft/applicationinsights-web": "^2.8.11", "@microsoft/immersive-reader-sdk": "1.1.0", "@types/diff-match-patch": "^1.0.32", "@zip.js/zip.js": "2.4.20", + "blockly": "10.4.1", "browserify": "16.2.3", "chai": "^3.5.0", "cssnano": "4.1.10", @@ -110,7 +111,7 @@ "@types/highlight.js": "9.12.2", "@types/jquery": "3.3.29", "@types/marked": "0.3.0", - "@types/mocha": "2.2.44", + "@types/mocha": "^2.2.44", "@types/node": "10.14.2", "@types/react": "17.0.50", "@types/react-dom": "17.0.17", @@ -141,7 +142,6 @@ "monaco-editor": "0.24.0", "pouchdb": "7.2.1", "pouchdb-adapter-memory": "7.2.1", - "pxt-blockly": "4.0.15", "react": "17.0.2", "react-dom": "17.0.2", "react-modal": "3.15.1", @@ -166,6 +166,7 @@ "test:lang": "gulp testlang", "update": "gulp update", "watch-streamer": "cd docs/static/streamer && tsc -t es6 --watch", - "prepare": "bash scripts/npm-prepare.sh" + "prepare": "bash scripts/npm-prepare.sh", + "postinstall": "node ./scripts/patchUglifyify.js" } } diff --git a/pxtblocks/blockDragger.ts b/pxtblocks/blockDragger.ts new file mode 100644 index 000000000000..d95a8f8e165b --- /dev/null +++ b/pxtblocks/blockDragger.ts @@ -0,0 +1,51 @@ +import * as Blockly from "blockly"; +import { DuplicateOnDragBlockDragger } from "./plugins/duplicateOnDrag"; + +/** + * The following patch to blockly is to add the Trash icon on top of the toolbox, + * the trash icon should only show when a user drags a block that is already in the workspace. + */ +export class BlockDragger extends DuplicateOnDragBlockDragger { + drag(e: PointerEvent, currentDragDeltaXY: Blockly.utils.Coordinate): void { + const blocklyToolboxDiv = document.getElementsByClassName('blocklyToolboxDiv')[0] as HTMLElement; + const blocklyTreeRoot = document.getElementsByClassName('blocklyTreeRoot')[0] as HTMLElement + || document.getElementsByClassName('blocklyFlyout')[0] as HTMLElement; + const trashIcon = document.getElementById("blocklyTrashIcon"); + if (blocklyTreeRoot && trashIcon) { + const distance = calculateDistance(blocklyTreeRoot.getBoundingClientRect(), e.clientX); + if (distance < 200) { + const opacity = distance / 200; + trashIcon.style.opacity = `${1 - opacity}`; + trashIcon.style.display = 'block'; + if (blocklyToolboxDiv) { + blocklyTreeRoot.style.opacity = `${opacity}`; + if (distance < 50) { + pxt.BrowserUtils.addClass(blocklyToolboxDiv, 'blocklyToolboxDeleting'); + } + } + } else { + trashIcon.style.display = 'none'; + blocklyTreeRoot.style.opacity = '1'; + if (blocklyToolboxDiv) pxt.BrowserUtils.removeClass(blocklyToolboxDiv, 'blocklyToolboxDeleting'); + } + } + return super.drag(e, currentDragDeltaXY); + } + + endDrag(e: PointerEvent, currentDragDeltaXY: Blockly.utils.Coordinate): void { + super.endDrag(e, currentDragDeltaXY); + const blocklyToolboxDiv = document.getElementsByClassName('blocklyToolboxDiv')[0] as HTMLElement; + const blocklyTreeRoot = document.getElementsByClassName('blocklyTreeRoot')[0] as HTMLElement + || document.getElementsByClassName('blocklyFlyout')[0] as HTMLElement; + const trashIcon = document.getElementById("blocklyTrashIcon"); + if (trashIcon && blocklyTreeRoot) { + trashIcon.style.display = 'none'; + blocklyTreeRoot.style.opacity = '1'; + if (blocklyToolboxDiv) pxt.BrowserUtils.removeClass(blocklyToolboxDiv, 'blocklyToolboxDeleting'); + } + } +} + +function calculateDistance(elemBounds: DOMRect, mouseX: number) { + return Math.abs(mouseX - (elemBounds.left + (elemBounds.width / 2))); +} \ No newline at end of file diff --git a/pxtblocks/blocklycompiler.ts b/pxtblocks/blocklycompiler.ts deleted file mode 100644 index a6211ca397ee..000000000000 --- a/pxtblocks/blocklycompiler.ts +++ /dev/null @@ -1,2769 +0,0 @@ -/// -/// - -let iface: pxt.worker.Iface - -namespace pxt.blocks { - export function workerOpAsync(op: string, arg: pxtc.service.OpArg) { - return pxt.worker.getWorker(pxt.webConfig.workerjs).opAsync(op, arg) - } - - let placeholders: Map> = {}; - const MAX_COMMENT_LINE_LENGTH = 50; - - - interface CommentMap { - orphans: Blockly.WorkspaceComment[]; - idToComments: Map; - } - - interface PlaceholderLikeBlock extends Blockly.Block { - p?: Point; - } - - /////////////////////////////////////////////////////////////////////////////// - // Miscellaneous utility functions - /////////////////////////////////////////////////////////////////////////////// - - // Mutate [a1] in place and append to it the elements from [a2]. - function append(a1: T[], a2: T[]) { - a1.push.apply(a1, a2); - } - - // A few wrappers for basic Block operations that throw errors when compilation - // is not possible. (The outer code catches these and highlights the relevant - // block.) - - // Internal error (in our code). Compilation shouldn't proceed. - function assert(x: boolean) { - if (!x) - throw new Error("Assertion failure"); - } - - function throwBlockError(msg: string, block: Blockly.Block) { - let e = new Error(msg); - (e).block = block; - throw e; - } - - /////////////////////////////////////////////////////////////////////////////// - // Types - // - // We slap a very simple type system on top of Blockly. This is needed to ensure - // we generate valid TouchDevelop code (otherwise compilation from TD to C++ - // would not work). - /////////////////////////////////////////////////////////////////////////////// - - // There are several layers of abstraction for the type system. - // - Block are annotated with a string return type, and a string type for their - // input blocks (see blocks-custom.js). We use that as the reference semantics - // for the blocks. - // - In this "type system", we use the enum Type. Using an enum rules out more - // mistakes. - // - When emitting code, we target the "TouchDevelop types". - // - // Type inference / checking is done as follows. First, we try to assign a type - // to all variables. We do this by examining all variable assignments and - // figuring out the type from the right-hand side. There's a fixpoint computation - // (see [mkEnv]). Then, we propagate down the expected type when doing code - // generation; when generating code for a variable dereference, if the expected - // type doesn't match the inferred type, it's an error. If the type was - // undetermined as of yet, the type of the variable becomes the expected type. - - export class Point { - constructor( - public link: Point, - public type: string, - public parentType?: Point, - public childType?: Point, - public isArrayType?: boolean - ) { } - } - - export interface Scope { - parent?: Scope; - firstStatement: Blockly.Block; - declaredVars: Map; - referencedVars: number[]; - assignedVars: number[]; - children: Scope[]; - } - - export enum BlockDeclarationType { - None = 0, - Argument, - Assigned, - Implicit - } - - export interface BlockDiagnostic { - blockId: string; - message: string; - } - - export interface VarInfo { - name: string; - id: number; - - escapedName?: string; - type?: Point; - alreadyDeclared?: BlockDeclarationType; - firstReference?: Blockly.Block; - isAssigned?: boolean; - isFunctionParameter?: boolean; - } - - function find(p: Point): Point { - if (p.link) - return find(p.link); - return p; - } - - function union(p1: Point, p2: Point) { - let _p1 = find(p1); - let _p2 = find(p2); - assert(_p1.link == null && _p2.link == null); - - if (_p1 == _p2) { - return; - } - else if (isPrimitiveType(_p1)) { - unify(p1.type, p2.type); - return; - } - else if (isPrimitiveType(_p2)) { - unify(p1.type, p2.type); - - p1.type = null; - p1.link = _p2; - _p1.link = _p2; - _p1.isArrayType = _p2.isArrayType; - return; - } - else if (_p1.childType && _p2.childType) { - const ct = _p1.childType; - _p1.childType = null; - union(ct, _p2.childType); - } - else if (_p1.childType && !_p2.childType) { - _p2.childType = _p1.childType; - } - - if (_p1.parentType && _p2.parentType) { - const pt = _p1.parentType; - _p1.parentType = null; - union(pt, _p2.parentType); - } - else if (_p1.parentType && !_p2.parentType && !_p2.type) { - _p2.parentType = _p1.parentType; - } - - - let t = unify(_p1.type, _p2.type); - - p1.link = _p2; - _p1.link = _p2; - _p1.isArrayType = _p2.isArrayType; - p1.type = null; - p2.type = t; - } - - // Ground types. - function mkPoint(t: string, isArrayType = false): Point { - return new Point(null, t, null, null, isArrayType); - } - const pNumber = mkPoint("number"); - const pBoolean = mkPoint("boolean"); - const pString = mkPoint("string"); - const pUnit = mkPoint("void"); - - function ground(t?: string): Point { - if (!t) return mkPoint(t); - switch (t.toLowerCase()) { - case "number": return pNumber; - case "boolean": return pBoolean; - case "string": return pString; - case "void": return pUnit; - default: - // Unification variable. - return mkPoint(t); - } - } - - function isPrimitiveType(point: Point) { - return point === pNumber || point === pBoolean || point === pString || point === pUnit; - } - - /////////////////////////////////////////////////////////////////////////////// - // Type inference - // - // Expressions are now directly compiled as a tree. This requires knowing, for - // each property ref, the right value for its [parent] property. - /////////////////////////////////////////////////////////////////////////////// - - // Infers the expected type of an expression by looking at the untranslated - // block and figuring out, from the look of it, what type of expression it - // holds. - function returnType(e: Environment, b: Blockly.Block): Point { - assert(b != null); - - if (isPlaceholderBlock(b)) { - if (!b.p) b.p = mkPoint(null); - return find(b.p); - } - - if (b.type == "variables_get") - return find(lookup(e, b, b.getField("VAR").getText()).type); - - if (b.type == "function_call_output") { - return getReturnTypeOfFunctionCall(e, b); - } - - if (!b.outputConnection) { - return ground(pUnit.type); - } - - const check = b.outputConnection.check_ && b.outputConnection.check_.length ? b.outputConnection.check_[0] : "T"; - - if (check === "Array") { - if (b.outputConnection.check_.length > 1) { - // HACK: The real type is stored as the second check - return ground(b.outputConnection.check_[1]) - } - // lists_create_with and argument_reporter_array both hit this. - // For lists_create_with, we can safely infer the type from the - // first input that has a return type. - // For argument_reporter_array just return any[] for now - let tp: Point; - if (b.type == "lists_create_with") { - if (b.inputList && b.inputList.length) { - for (const input of b.inputList) { - if (input.connection && input.connection.targetBlock()) { - let t = find(returnType(e, input.connection.targetBlock())) - if (t) { - if (t.parentType) { - return t.parentType; - } - tp = t.type ? ground(t.type + "[]") : mkPoint(null); - genericLink(tp, t); - break; - } - } - } - } - } else if (b.type == "argument_reporter_array") { - if (!tp) { - tp = lookup(e, b, b.getFieldValue("VALUE")).type - } - } - - if (tp) tp.isArrayType = true; - return tp || mkPoint(null, true); - } - else if (check === "T") { - const func = e.stdCallTable[b.type]; - const isArrayGet = b.type === "lists_index_get"; - if (isArrayGet || func && func.comp.thisParameter) { - let parentInput: Blockly.Input; - - if (isArrayGet) { - parentInput = b.inputList.find(i => i.name === "LIST"); - } - else { - parentInput = b.inputList.find(i => i.name === func.comp.thisParameter.definitionName); - } - - if (parentInput.connection && parentInput.connection.targetBlock()) { - const parentType = returnType(e, parentInput.connection.targetBlock()); - if (parentType.childType) { - return parentType.childType; - } - const p = isArrayType(parentType.type) && parentType.type !== "Array" ? mkPoint(parentType.type.substr(0, parentType.type.length - 2)) : mkPoint(null); - genericLink(parentType, p); - return p; - } - } - return mkPoint(null); - } - - return ground(check); - } - - function returnTypeWithInheritance(e: Environment, b: Blockly.Block) { - if (!b.outputConnection?.check_?.length || b.outputConnection.check_[0] === "Array" || b.outputConnection.check_[0] === "T") { - return [returnType(e, b)]; - } - - return b.outputConnection.check_.map(t => ground(t)) - } - - function getReturnTypeOfFunction(e: Environment, name: string) { - if (!e.userFunctionReturnValues[name]) { - const definition = Blockly.Functions.getDefinition(name, e.workspace); - - let res = mkPoint("void"); - - if (isFunctionRecursive(definition, true)) { - res = mkPoint("any"); - } - else { - const returnTypes: Point[] = []; - for (const child of definition.getDescendants(false)) { - if (child.type === "function_return") { - attachPlaceholderIf(e, child, "RETURN_VALUE"); - returnTypes.push(returnType(e, getInputTargetBlock(child, "RETURN_VALUE"))); - } - } - - if (returnTypes.length) { - try { - const unified = mkPoint(null); - for (const point of returnTypes) { - union(unified, point); - } - res = unified - } - catch (err) { - e.diagnostics.push({ - blockId: definition.id, - message: Util.lf("Function '{0}' has an invalid return type", name) - }); - - res = mkPoint("any") - } - } - } - - e.userFunctionReturnValues[name] = res; - } - - return e.userFunctionReturnValues[name]; - } - - function getReturnTypeOfFunctionCall(e: Environment, call: Blockly.Block) { - const name = call.getField("function_name").getText(); - return getReturnTypeOfFunction(e, name); - } - - // Basic type unification routine; easy, because there's no structural types. - // FIXME: Generics are not supported - function unify(t1: string, t2: string) { - if (t1 == null || t1 === "Array" && isArrayType(t2)) - return t2; - else if (t2 == null || t2 === "Array" && isArrayType(t1)) - return t1; - else if (t1 == t2) - return t1; - else - throw new Error("cannot mix " + t1 + " with " + t2); - } - - function isArrayType(type: string) { - return type && (type.indexOf("[]") !== -1 || type == "Array"); - } - - function mkPlaceholderBlock(e: Environment, parent: Blockly.Block, type?: string): Blockly.Block { - // XXX define a proper placeholder block type - return { - type: "placeholder", - p: mkPoint(type || null), - workspace: e.workspace, - parentBlock_: parent - }; - } - - function attachPlaceholderIf(e: Environment, b: Blockly.Block, n: string, type?: string) { - // Ugly hack to keep track of the type we want there. - const target = b.getInputTargetBlock(n); - if (!target) { - if (!placeholders[b.id]) { - placeholders[b.id] = {}; - } - - if (!placeholders[b.id][n]) { - placeholders[b.id][n] = mkPlaceholderBlock(e, b, type); - } - } - else if (target.type === pxtc.TS_OUTPUT_TYPE && !((target as any).p)) { - (target as any).p = mkPoint(null); - } - } - - function getLoopVariableField(b: Blockly.Block) { - return (b.type == "pxt_controls_for" || b.type == "pxt_controls_for_of") ? - getInputTargetBlock(b, "VAR") : b; - } - - function getInputTargetBlock(b: Blockly.Block, n: string): Blockly.Block { - const res = b.getInputTargetBlock(n); - - if (!res) { - return placeholders[b.id] && placeholders[b.id][n]; - } - else { - return res - } - } - - function removeAllPlaceholders() { - placeholders = {}; - } - - // Unify the *return* type of the parameter [n] of block [b] with point [p]. - function unionParam(e: Environment, b: Blockly.Block, n: string, p: Point) { - attachPlaceholderIf(e, b, n); - try { - union(returnType(e, getInputTargetBlock(b, n)), p); - } catch (e) { - // TypeScript should catch this error and bubble it up - } - } - - function infer(allBlocks: Blockly.Block[], e: Environment, w: Blockly.Workspace) { - if (allBlocks) allBlocks.filter(b => b.isEnabled()).forEach((b: Blockly.Block) => { - try { - switch (b.type) { - case "math_op2": - unionParam(e, b, "x", ground(pNumber.type)); - unionParam(e, b, "y", ground(pNumber.type)); - break; - - case "math_op3": - unionParam(e, b, "x", ground(pNumber.type)); - break; - - case "math_arithmetic": - case "logic_compare": - switch (b.getFieldValue("OP")) { - case "ADD": case "MINUS": case "MULTIPLY": case "DIVIDE": - case "LT": case "LTE": case "GT": case "GTE": case "POWER": - unionParam(e, b, "A", ground(pNumber.type)); - unionParam(e, b, "B", ground(pNumber.type)); - break; - case "AND": case "OR": - attachPlaceholderIf(e, b, "A", pBoolean.type); - attachPlaceholderIf(e, b, "B", pBoolean.type); - break; - case "EQ": case "NEQ": - attachPlaceholderIf(e, b, "A"); - attachPlaceholderIf(e, b, "B"); - let p1 = returnType(e, getInputTargetBlock(b, "A")); - let p2 = returnType(e, getInputTargetBlock(b, "B")); - try { - union(p1, p2); - } catch (e) { - // TypeScript should catch this error and bubble it up - } - break; - } - break; - - case "logic_operation": - attachPlaceholderIf(e, b, "A", pBoolean.type); - attachPlaceholderIf(e, b, "B", pBoolean.type); - break; - - case "logic_negate": - attachPlaceholderIf(e, b, "BOOL", pBoolean.type); - break; - - case "controls_if": - for (let i = 0; i <= (b).elseifCount_; ++i) - attachPlaceholderIf(e, b, "IF" + i, pBoolean.type); - break; - - case "pxt_controls_for": - case "controls_simple_for": - unionParam(e, b, "TO", ground(pNumber.type)); - break; - case "pxt_controls_for_of": - case "controls_for_of": - const listTp = returnType(e, getInputTargetBlock(b, "LIST")); - const elementTp = lookup(e, b, getLoopVariableField(b).getField("VAR").getText()).type; - genericLink(listTp, elementTp); - break; - case "variables_set": - case "variables_change": - let p1 = lookup(e, b, b.getField("VAR").getText()).type; - attachPlaceholderIf(e, b, "VALUE"); - let rhs = getInputTargetBlock(b, "VALUE"); - if (rhs) { - // Get the inheritance chain for this type and check to see if the existing - // type shows up in it somewhere - let tr = returnTypeWithInheritance(e, rhs); - const t1 = find(p1); - if (t1.type && tr.slice(1).some(p => p.type === t1.type)) { - // If it does, we want to take the most narrow type (which will always be in 0) - p1.link = find(tr[0]); - } - else { - try { - union(p1, tr[0]); - } catch (e) { - // TypeScript should catch this error and bubble it up - } - } - } - break; - case "controls_repeat_ext": - unionParam(e, b, "TIMES", ground(pNumber.type)); - break; - - case "device_while": - attachPlaceholderIf(e, b, "COND", pBoolean.type); - break; - case "lists_index_get": - unionParam(e, b, "LIST", ground("Array")); - unionParam(e, b, "INDEX", ground(pNumber.type)); - const listType = returnType(e, getInputTargetBlock(b, "LIST")); - const ret = returnType(e, b); - genericLink(listType, ret); - break; - case "lists_index_set": - unionParam(e, b, "LIST", ground("Array")); - attachPlaceholderIf(e, b, "VALUE"); - handleGenericType(b, "LIST"); - unionParam(e, b, "INDEX", ground(pNumber.type)); - break; - case 'function_definition': - getReturnTypeOfFunction(e, b.getField("function_name",).getText()); - break; - case 'function_call': - case 'function_call_output': - (b as Blockly.FunctionCallBlock).getArguments().forEach(arg => { - unionParam(e, b, arg.id, ground(arg.type)); - }); - break; - case pxtc.TS_RETURN_STATEMENT_TYPE: - attachPlaceholderIf(e, b, "RETURN_VALUE"); - break; - case pxtc.PAUSE_UNTIL_TYPE: - unionParam(e, b, "PREDICATE", pBoolean); - break; - default: - if (b.type in e.stdCallTable) { - const call = e.stdCallTable[b.type]; - if (call.attrs.shim === "ENUM_GET" || call.attrs.shim === "KIND_GET") return; - visibleParams(call, countOptionals(b, call)).forEach((p, i) => { - const isInstance = call.isExtensionMethod && i === 0; - if (p.definitionName && !b.getFieldValue(p.definitionName)) { - let i = b.inputList.find((i: Blockly.Input) => i.name == p.definitionName); - if (i && i.connection && i.connection.check_) { - if (isInstance && connectionCheck(i) === "Array") { - let gen = handleGenericType(b, p.definitionName); - if (gen) { - return; - } - } - - // All of our injected blocks have single output checks, but the builtin - // blockly ones like string.length and array.length might have multiple - for (let j = 0; j < i.connection.check_.length; j++) { - try { - let t = i.connection.check_[j]; - unionParam(e, b, p.definitionName, ground(t)); - break; - } - catch (e) { - // Ignore type checking errors in the blocks... - } - } - } - } - }); - } - } - } catch (err) { - const be = ((err).block as Blockly.Block) || b; - be.setWarningText(err + ""); - e.errors.push(be); - } - }); - - // Last pass: if some variable has no type (because it was never used or - // assigned to), just unify it with int... - e.allVariables.forEach((v: VarInfo) => { - if (getConcreteType(v.type).type == null) { - if (!v.isFunctionParameter) { - union(v.type, ground(v.type.isArrayType ? "number[]" : pNumber.type)); - } - else if (v.type.isArrayType) { - v.type.type = "any[]" - } - } - }); - - function connectionCheck(i: Blockly.Input) { - return i.name ? i.connection && i.connection.check_ && i.connection.check_.length ? i.connection.check_[0] : "T" : undefined; - } - - function handleGenericType(b: Blockly.Block, name: string) { - let genericArgs = b.inputList.filter((input: Blockly.Input) => connectionCheck(input) === "T"); - if (genericArgs.length) { - const gen = getInputTargetBlock(b, genericArgs[0].name); - if (gen) { - const arg = returnType(e, gen); - const arrayType = arg.type ? ground(returnType(e, gen).type + "[]") : ground(null); - genericLink(arrayType, arg); - unionParam(e, b, name, arrayType); - return true; - } - } - return false; - } - } - - function genericLink(parent: Point, child: Point) { - const p = find(parent); - const c = find(child); - if (p.childType) { - union(p.childType, c); - } - else if (!p.type) { - p.childType = c; - } - - if (c.parentType) { - union(c.parentType, p); - } - else if (!c.type) { - c.parentType = p; - } - - if (isArrayType(p.type)) - p.isArrayType = true; - } - - function getConcreteType(point: Point, found: Point[] = []) { - const t = find(point) - if (found.indexOf(t) === -1) { - found.push(t); - if (!t.type || t.type === "Array") { - if (t.parentType) { - const parent = getConcreteType(t.parentType, found); - if (parent.type && parent.type !== "Array") { - if (isArrayType(parent.type)) { - t.type = parent.type.substr(0, parent.type.length - 2); - } else { - t.type = parent.type; - } - return t; - } - } - - if (t.childType) { - const child = getConcreteType(t.childType, found); - if (child.type) { - t.type = child.type + "[]"; - return t; - } - - } - } - } - return t; - } - - /////////////////////////////////////////////////////////////////////////////// - // Expressions - // - // Expressions are now directly compiled as a tree. This requires knowing, for - // each property ref, the right value for its [parent] property. - /////////////////////////////////////////////////////////////////////////////// - - function extractNumber(b: Blockly.Block): number { - let v = b.getFieldValue(b.type === "math_number_minmax" ? "SLIDER" : "NUM"); - const parsed = parseFloat(v); - checkNumber(parsed, b); - return parsed; - } - - function checkNumber(n: number, b: Blockly.Block) { - if (!isFinite(n) || isNaN(n)) { - throwBlockError(lf("Number entered is either too large or too small"), b); - } - } - - function extractTsExpression(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - return mkText(b.getFieldValue("EXPRESSION").trim()); - } - - function compileNumber(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - return H.mkNumberLiteral(extractNumber(b)); - } - - function isNumericLiteral(e: Environment, b: Blockly.Block): boolean { - if (!b) return false; - - if (b.type === "math_number" || b.type === "math_integer" || b.type === "math_number_minmax" || b.type === "math_whole_number") { - return true; - } - - const blockInfo = e.stdCallTable[b.type]; - if (!blockInfo) return false; - - const { comp } = blockInfo; - - if (blockInfo.attrs.shim === "TD_ID" && comp.parameters.length === 1) { - const fieldValue = b.getFieldValue(comp.parameters[0].definitionName); - - if (fieldValue) { - return !isNaN(parseInt(fieldValue)) - } - else { - return isNumericLiteral(e, getInputTargetBlock(b, comp.parameters[0].definitionName)); - } - } - - return false; - } - - function isLiteral(e: Environment, b: Blockly.Block) { - return isNumericLiteral(e, b) || b.type === "logic_boolean" || b.type === "text"; - } - - let opToTok: { [index: string]: string } = { - "ADD": "+", - "MINUS": "-", - "MULTIPLY": "*", - "DIVIDE": "/", - "LT": "<", - "LTE": "<=", - "GT": ">", - "GTE": ">=", - "AND": "&&", - "OR": "||", - "EQ": "==", - "NEQ": "!=", - "POWER": "**" - }; - - function isComparisonOp(op: string) { - return ["LT", "LTE", "GT", "GTE", "EQ", "NEQ"].indexOf(op) !== -1; - } - - function compileArithmetic(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let bOp = b.getFieldValue("OP"); - let left = getInputTargetBlock(b, "A"); - let right = getInputTargetBlock(b, "B"); - let args = [compileExpression(e, left, comments), compileExpression(e, right, comments)]; - - // Special handling for the case of comparing two literals (e.g. 0 === 5). TypeScript - // throws an error if we don't first cast to any - if (isComparisonOp(bOp) && isLiteral(e, left) && isLiteral(e, right)) { - if (flattenNode([args[0]]).output !== flattenNode([args[1]]).output) { - args = args.map(arg => - H.mkParenthesizedExpression( - mkGroup([arg, mkText(" as any")]) - ) - ); - } - } - - let t = returnType(e, left).type; - - if (t == pString.type) { - if (bOp == "EQ") return H.mkSimpleCall("==", args); - else if (bOp == "NEQ") return H.mkSimpleCall("!=", args); - } else if (t == pBoolean.type) - return H.mkSimpleCall(opToTok[bOp], args); - - // Compilation of math operators. - assert(bOp in opToTok); - return H.mkSimpleCall(opToTok[bOp], args); - } - - function compileModulo(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let left = getInputTargetBlock(b, "DIVIDEND"); - let right = getInputTargetBlock(b, "DIVISOR"); - let args = [compileExpression(e, left, comments), compileExpression(e, right, comments)]; - return H.mkSimpleCall("%", args); - } - - function compileMathOp2(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let op = b.getFieldValue("op"); - let x = compileExpression(e, getInputTargetBlock(b, "x"), comments); - let y = compileExpression(e, getInputTargetBlock(b, "y"), comments); - return H.mathCall(op, [x, y]) - } - - function compileMathOp3(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let x = compileExpression(e, getInputTargetBlock(b, "x"), comments); - return H.mathCall("abs", [x]); - } - - function compileText(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - return H.mkStringLiteral(b.getFieldValue("TEXT")); - } - - function compileTextJoin(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let last: JsNode; - let i = 0; - while (true) { - const val = getInputTargetBlock(b, "ADD" + i); - i++; - - if (!val) { - if (i < b.inputList.length) { - continue; - } - else { - break; - } - } - - const compiled = compileExpression(e, val, comments); - if (!last) { - if (val.type.indexOf("text") === 0) { - last = compiled; - } - else { - // If we don't start with a string, then the TS won't match - // the implied semantics of the blocks - last = H.mkSimpleCall("+", [H.mkStringLiteral(""), compiled]); - } - } - else { - last = H.mkSimpleCall("+", [last, compiled]); - } - } - - if (!last) { - return H.mkStringLiteral(""); - } - - return last; - } - - function compileBoolean(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - return H.mkBooleanLiteral(b.getFieldValue("BOOL") == "TRUE"); - } - - function compileNot(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let expr = compileExpression(e, getInputTargetBlock(b, "BOOL"), comments); - return mkPrefix("!", [H.mkParenthesizedExpression(expr)]); - } - - function compileCreateList(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - // collect argument - let args = b.inputList.map(input => input.connection && input.connection.targetBlock() ? compileExpression(e, input.connection.targetBlock(), comments) : undefined) - .filter(e => !!e); - - return H.mkArrayLiteral(args, !b.getInputsInline()); - } - - function compileListGet(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const listBlock = getInputTargetBlock(b, "LIST"); - const listExpr = compileExpression(e, listBlock, comments); - const index = compileExpression(e, getInputTargetBlock(b, "INDEX"), comments); - const res = mkGroup([listExpr, mkText("["), index, mkText("]")]); - - return res; - } - - function compileListSet(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const listBlock = getInputTargetBlock(b, "LIST"); - const listExpr = compileExpression(e, listBlock, comments); - const index = compileExpression(e, getInputTargetBlock(b, "INDEX"), comments); - const value = compileExpression(e, getInputTargetBlock(b, "VALUE"), comments); - const res = mkGroup([listExpr, mkText("["), index, mkText("] = "), value]); - - return listBlock.type === "lists_create_with" ? prefixWithSemicolon(res) : res; - - } - - function compileMathJsOp(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const op = b.getFieldValue("OP"); - const args = [compileExpression(e, getInputTargetBlock(b, "ARG0"), comments)]; - - if ((b as any).getInput("ARG1")) { - args.push(compileExpression(e, getInputTargetBlock(b, "ARG1"), comments)); - } - - return H.mathCall(op, args); - } - - function compileFunctionDefinition(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - const name = escapeVarName(b.getField("function_name").getText(), e, true); - const stmts = getInputTargetBlock(b, "STACK"); - const argsDeclaration = (b as Blockly.FunctionDefinitionBlock).getArguments().map(a => { - if (a.type == "Array") { - const binding = lookup(e, b, a.name); - const declaredType = getConcreteType(binding.type); - const paramType = (declaredType?.type && declaredType.type !== "Array") ? declaredType.type : "any[]"; - return `${escapeVarName(a.name, e)}: ${paramType}`; - } - return `${escapeVarName(a.name, e)}: ${a.type}`; - }); - - const isRecursive = isFunctionRecursive(b, false); - return [ - mkText(`function ${name} (${argsDeclaration.join(", ")})${isRecursive ? ": any" : ""}`), - compileStatements(e, stmts) - ]; - } - - function compileProcedure(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - const name = escapeVarName(b.getFieldValue("NAME"), e, true); - const stmts = getInputTargetBlock(b, "STACK"); - return [ - mkText("function " + name + "() "), - compileStatements(e, stmts) - ]; - } - - function compileProcedureCall(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const name = escapeVarName(b.getFieldValue("NAME"), e, true); - return mkStmt(mkText(name + "()")); - } - - function compileFunctionCall(e: Environment, b: Blockly.Block, comments: string[], statement: boolean): JsNode { - const name = escapeVarName(b.getField("function_name").getText(), e, true); - const externalInputs = !b.getInputsInline(); - const args: BlockParameter[] = (b as Blockly.FunctionCallBlock).getArguments().map(a => { - return { - actualName: a.name, - definitionName: a.id - }; - }); - - const compiledArgs = args.map(a => compileArgument(e, b, a, comments)); - const res = H.stdCall(name, compiledArgs, externalInputs) - - if (statement) { - return mkStmt(res); - } - return res; - } - - function compileReturnStatement(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const expression = getInputTargetBlock(b, "RETURN_VALUE"); - - if (expression && expression.type != "placeholder") { - return mkStmt(mkText("return "), compileExpression(e, expression, comments)); - } - else { - return mkStmt(mkText("return")); - } - } - - function compileArgumentReporter(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const name = escapeVarName(b.getFieldValue("VALUE"), e); - return mkText(name); - } - - function compileWorkspaceComment(c: Blockly.WorkspaceComment): JsNode { - const content = c.getContent(); - return Helpers.mkMultiComment(content.trim()); - } - - function defaultValueForType(t: Point): JsNode { - if (t.type == null) { - union(t, ground(pNumber.type)); - t = find(t); - } - - if (isArrayType(t.type) || t.isArrayType) { - return mkText("[]"); - } - - switch (t.type) { - case "boolean": - return H.mkBooleanLiteral(false); - case "number": - return H.mkNumberLiteral(0); - case "string": - return H.mkStringLiteral(""); - default: - return mkText("null"); - } - } - - // [t] is the expected type; we assume that we never null block children - // (because placeholder blocks have been inserted by the type-checking phase - // whenever a block was actually missing). - export function compileExpression(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - assert(b != null); - e.stats[b.type] = (e.stats[b.type] || 0) + 1; - maybeAddComment(b, comments); - let expr: JsNode; - if (b.type == "placeholder" || !(b.isEnabled && b.isEnabled())) { - const ret = find(returnType(e, b)); - if (ret.type === "Array") { - // FIXME: Can't use default type here because TS complains about - // the array having an implicit any type. However, forcing this - // to be a number array may cause type issues. Also, potential semicolon - // issues if we ever have a block where the array is not the first argument... - let isExpression = b.parentBlock_.type === "lists_index_get"; - if (!isExpression) { - const call = e.stdCallTable[b.parentBlock_.type]; - isExpression = call && call.isExpression; - } - const arrayNode = mkText("[0]"); - expr = isExpression ? arrayNode : prefixWithSemicolon(arrayNode); - } - else { - expr = defaultValueForType(returnType(e, b)); - } - } - else switch (b.type) { - case "math_number": - case "math_integer": - case "math_whole_number": - expr = compileNumber(e, b, comments); break; - case "math_number_minmax": - expr = compileNumber(e, b, comments); break; - case "math_op2": - expr = compileMathOp2(e, b, comments); break; - case "math_op3": - expr = compileMathOp3(e, b, comments); break; - case "math_arithmetic": - case "logic_compare": - case "logic_operation": - expr = compileArithmetic(e, b, comments); break; - case "math_modulo": - expr = compileModulo(e, b, comments); break; - case "logic_boolean": - expr = compileBoolean(e, b, comments); break; - case "logic_negate": - expr = compileNot(e, b, comments); break; - case "variables_get": - expr = compileVariableGet(e, b); break; - case "text": - expr = compileText(e, b, comments); break; - case "text_join": - expr = compileTextJoin(e, b, comments); break; - case "lists_create_with": - expr = compileCreateList(e, b, comments); break; - case "lists_index_get": - expr = compileListGet(e, b, comments); break; - case "lists_index_set": - expr = compileListSet(e, b, comments); break; - case "math_js_op": - case "math_js_round": - expr = compileMathJsOp(e, b, comments); break; - case pxtc.TS_OUTPUT_TYPE: - expr = extractTsExpression(e, b, comments); break; - case "argument_reporter_boolean": - case "argument_reporter_number": - case "argument_reporter_string": - case "argument_reporter_array": - case "argument_reporter_custom": - expr = compileArgumentReporter(e, b, comments); - break; - case "function_call_output": - expr = compileFunctionCall(e, b, comments, false); break; - default: - let call = e.stdCallTable[b.type]; - if (call) { - if (call.imageLiteral) - expr = compileImage(e, b, call.imageLiteral, call.imageLiteralColumns, call.imageLiteralRows, call.namespace, call.f, - visibleParams(call, countOptionals(b, call)).map(ar => compileArgument(e, b, ar, comments))) - else - expr = compileStdCall(e, b, call, comments); - } - else { - pxt.reportError("blocks", "unable to compile expression", { "details": b.type }); - expr = defaultValueForType(returnType(e, b)); - } - break; - } - - expr.id = b.id; - return expr; - } - - /////////////////////////////////////////////////////////////////////////////// - // Environments - /////////////////////////////////////////////////////////////////////////////// - - // Environments are persistent. - - export interface Environment { - workspace: Blockly.Workspace; - options: BlockCompileOptions; - stdCallTable: pxt.Map; - userFunctionReturnValues: pxt.Map; - diagnostics: BlockDiagnostic[]; - errors: Blockly.Block[]; - renames: RenameMap; - stats: pxt.Map; - enums: pxtc.EnumInfo[]; - kinds: pxtc.KindInfo[]; - idToScope: pxt.Map; - blockDeclarations: pxt.Map; - blocksInfo: pxtc.BlocksInfo; - allVariables: VarInfo[]; - } - - export interface RenameMap { - oldToNew: Map; - takenNames: Map; - oldToNewFunctions: Map; - } - - function lookup(e: Environment, b: Blockly.Block, name: string): VarInfo { - return getVarInfo(name, e.idToScope[b.id]); - } - - function emptyEnv(w: Blockly.Workspace, options: BlockCompileOptions): Environment { - return { - workspace: w, - options, - stdCallTable: {}, - userFunctionReturnValues: {}, - diagnostics: [], - errors: [], - renames: { - oldToNew: {}, - takenNames: {}, - oldToNewFunctions: {} - }, - stats: {}, - enums: [], - kinds: [], - idToScope: {}, - blockDeclarations: {}, - allVariables: [], - blocksInfo: null - } - }; - - /////////////////////////////////////////////////////////////////////////////// - // Statements - /////////////////////////////////////////////////////////////////////////////// - - function compileControlsIf(e: Environment, b: Blockly.IfBlock, comments: string[]): JsNode[] { - let stmts: JsNode[] = []; - // Notice the <= (if there's no else-if, we still compile the primary if). - for (let i = 0; i <= b.elseifCount_; ++i) { - let cond = compileExpression(e, getInputTargetBlock(b, "IF" + i), comments); - let thenBranch = compileStatements(e, getInputTargetBlock(b, "DO" + i)); - let startNode = mkText("if (") - if (i > 0) { - startNode = mkText("else if (") - startNode.glueToBlock = GlueMode.WithSpace; - } - append(stmts, [ - startNode, - cond, - mkText(")"), - thenBranch - ]) - } - if (b.elseCount_) { - let elseNode = mkText("else") - elseNode.glueToBlock = GlueMode.WithSpace; - append(stmts, [ - elseNode, - compileStatements(e, getInputTargetBlock(b, "ELSE")) - ]) - } - return stmts; - } - - function compileControlsFor(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - let bTo = getInputTargetBlock(b, "TO"); - let bDo = getInputTargetBlock(b, "DO"); - let bBy = getInputTargetBlock(b, "BY"); - let bFrom = getInputTargetBlock(b, "FROM"); - let incOne = !bBy || (bBy.type.match(/^math_number/) && extractNumber(bBy) == 1) - - let binding = lookup(e, b, getLoopVariableField(b).getField("VAR").getText()); - - return [ - mkText("for (let " + binding.escapedName + " = "), - bFrom ? compileExpression(e, bFrom, comments) : mkText("0"), - mkText("; "), - mkInfix(mkText(binding.escapedName), "<=", compileExpression(e, bTo, comments)), - mkText("; "), - incOne ? mkText(binding.escapedName + "++") : mkInfix(mkText(binding.escapedName), "+=", compileExpression(e, bBy, comments)), - mkText(")"), - compileStatements(e, bDo) - ] - } - - function compileControlsRepeat(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - let bound = compileExpression(e, getInputTargetBlock(b, "TIMES"), comments); - let body = compileStatements(e, getInputTargetBlock(b, "DO")); - let valid = (x: string) => !lookup(e, b, x); - - let name = "index"; - // Start at 2 because index0 and index1 are bad names - for (let i = 2; !valid(name); i++) - name = "index" + i; - return [ - mkText("for (let " + name + " = 0; "), - mkInfix(mkText(name), "<", bound), - mkText("; " + name + "++)"), - body - ] - } - - function compileWhile(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - let cond = compileExpression(e, getInputTargetBlock(b, "COND"), comments); - let body = compileStatements(e, getInputTargetBlock(b, "DO")); - return [ - mkText("while ("), - cond, - mkText(")"), - body - ] - } - - function compileControlsForOf(e: Environment, b: Blockly.Block, comments: string[]) { - let bOf = getInputTargetBlock(b, "LIST"); - let bDo = getInputTargetBlock(b, "DO"); - - let binding = lookup(e, b, getLoopVariableField(b).getField("VAR").getText()); - - return [ - mkText("for (let " + binding.escapedName + " of "), - compileExpression(e, bOf, comments), - mkText(")"), - compileStatements(e, bDo) - ] - } - - function compileForever(e: Environment, b: Blockly.Block): JsNode { - let bBody = getInputTargetBlock(b, "HANDLER"); - let body = compileStatements(e, bBody); - return mkCallWithCallback(e, "basic", "forever", [], body); - } - - // convert to javascript friendly name - export function escapeVarName(name: string, e: Environment, isFunction = false): string { - if (!name) return '_'; - - if (isFunction) { - if (e.renames.oldToNewFunctions[name]) { - return e.renames.oldToNewFunctions[name]; - } - } - else if (e.renames.oldToNew[name]) { - return e.renames.oldToNew[name]; - } - - let n = ts.pxtc.escapeIdentifier(name); - - if (e.renames.takenNames[n]) { - let i = 2; - - while (e.renames.takenNames[n + i]) { - i++; - } - - n += i; - } - - if (isFunction) { - e.renames.oldToNewFunctions[name] = n; - e.renames.takenNames[n] = true; - } - else { - e.renames.oldToNew[name] = n; - } - return n; - } - - function compileVariableGet(e: Environment, b: Blockly.Block): JsNode { - const name = b.getField("VAR").getText(); - let binding = lookup(e, b, name); - if (!binding) // trying to compile a disabled block with a bogus variable - return mkText(name); - - if (!binding.firstReference) binding.firstReference = b; - - assert(binding != null && binding.type != null); - return mkText(binding.escapedName); - } - - function compileSet(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let bExpr = getInputTargetBlock(b, "VALUE"); - let binding = lookup(e, b, b.getField("VAR").getText()); - - const currentScope = e.idToScope[b.id]; - let isDef = currentScope.declaredVars[binding.name] === binding && !binding.firstReference && !binding.alreadyDeclared; - - if (isDef) { - // Check the expression of the set block to determine if it references itself and needs - // to be hoisted - forEachChildExpression(b, child => { - if (child.type === "variables_get") { - let childBinding = lookup(e, child, child.getField("VAR").getText()); - if (childBinding === binding) isDef = false; - } - }, true); - } - - let expr = compileExpression(e, bExpr, comments); - - let bindString = binding.escapedName + " = "; - - binding.isAssigned = true; - - if (isDef) { - binding.alreadyDeclared = BlockDeclarationType.Assigned; - const declaredType = getConcreteType(binding.type); - - bindString = `let ${binding.escapedName} = `; - - if (declaredType) { - const expressionType = getConcreteType(returnType(e, bExpr)); - if (declaredType.type !== expressionType.type) { - bindString = `let ${binding.escapedName}: ${declaredType.type} = `; - } - } - } - else if (!binding.firstReference) { - binding.firstReference = b; - } - - return mkStmt( - mkText(bindString), - expr) - } - - function compileChange(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - let bExpr = getInputTargetBlock(b, "VALUE"); - let binding = lookup(e, b, b.getField("VAR").getText()); - let expr = compileExpression(e, bExpr, comments); - let ref = mkText(binding.escapedName); - return mkStmt(mkInfix(ref, "+=", expr)) - } - - function eventArgs(call: StdFunc, b: Blockly.Block): BlockParameter[] { - return visibleParams(call, countOptionals(b, call)).filter(ar => !!ar.definitionName); - } - - function compileCall(e: Environment, b: Blockly.Block, comments: string[]): JsNode { - const call = e.stdCallTable[b.type]; - if (call.imageLiteral) - return mkStmt(compileImage(e, b, call.imageLiteral, call.imageLiteralColumns, call.imageLiteralRows, call.namespace, call.f, visibleParams(call, countOptionals(b, call)).map(ar => compileArgument(e, b, ar, comments)))) - else if (call.hasHandler) - return compileEvent(e, b, call, eventArgs(call, b), call.namespace, comments) - else - return mkStmt(compileStdCall(e, b, call, comments)) - } - - function compileArgument(e: Environment, b: Blockly.Block, p: BlockParameter, comments: string[], beginningOfStatement = false): JsNode { - let f = b.getFieldValue(p.definitionName); - if (f != null) { - const field = b.getField(p.definitionName); - - if (field instanceof pxtblockly.FieldTextInput) { - return H.mkStringLiteral(f); - } - else if (field instanceof pxtblockly.FieldTilemap && !field.isGreyBlock) { - const project = pxt.react.getTilemapProject(); - const tmString = field.getValue(); - - if (tmString.startsWith("tilemap`")) { - return mkText(tmString); - } - - if (e.options.emitTilemapLiterals) { - try { - const data = pxt.sprite.decodeTilemap(tmString, "typescript", project); - if (data) { - const [ name ] = project.createNewTilemapFromData(data); - return mkText(`tilemap\`${name}\``); - } - } - catch (e) { - // This is a legacy tilemap or a grey block, ignore the exception - // and compile as a normal field - } - } - } - - // For some enums in pxt-minecraft, we emit the members as constants that are defined in - // libs/core. For example, Blocks.GoldBlock is emitted as GOLD_BLOCK - const type = e.blocksInfo.apis.byQName[p.type]; - if (type && type.attributes.emitAsConstant) { - for (const symbolName of Object.keys(e.blocksInfo.apis.byQName)) { - const symbol = e.blocksInfo.apis.byQName[symbolName]; - if (symbol && symbol.attributes && symbol.attributes.enumIdentity === f) { - return mkText(symbolName); - } - } - } - - let text = mkText(f) - text.canIndentInside = typeof f == "string" && f.indexOf('\n') >= 0; - return text; - } - else { - attachPlaceholderIf(e, b, p.definitionName); - const target = getInputTargetBlock(b, p.definitionName); - if (beginningOfStatement && target.type === "lists_create_with") { - // We have to be careful of array literals at the beginning of a statement - // because they can cause errors (i.e. they get parsed as an index). Add a - // semicolon to the previous statement just in case. - // FIXME: No need to do this if the previous statement was a code block - return prefixWithSemicolon(compileExpression(e, target, comments)); - } - - if (p.shadowOptions && p.shadowOptions.toString && returnType(e, target) !== pString) { - return H.mkSimpleCall("+", [H.mkStringLiteral(""), H.mkParenthesizedExpression(compileExpression(e, target, comments))]); - } - - return compileExpression(e, target, comments) - } - } - - function compileStdCall(e: Environment, b: Blockly.Block, func: StdFunc, comments: string[]): JsNode { - let args: JsNode[] - if (isMutatingBlock(b) && b.mutation.getMutationType() === MutatorTypes.RestParameterMutator) { - args = b.mutation.compileMutation(e, comments).children; - } - else if (func.attrs.shim === "ENUM_GET") { - const enumName = func.attrs.enumName; - const enumMember = b.getFieldValue("MEMBER").replace(/^\d+/, ""); - return H.mkPropertyAccess(enumMember, mkText(enumName)); - } - else if (func.attrs.shim === "KIND_GET") { - const info = e.kinds.filter(k => k.blockId === func.attrs.blockId)[0]; - return H.mkPropertyAccess(b.getFieldValue("MEMBER"), mkText(info.name)); - } - else { - args = visibleParams(func, countOptionals(b, func)).map((p, i) => compileArgument(e, b, p, comments, func.isExtensionMethod && i === 0 && !func.isExpression)); - } - - let callNamespace = func.namespace; - let callName = func.f - if (func.attrs.blockAliasFor) { - const aliased = e.blocksInfo.apis.byQName[func.attrs.blockAliasFor]; - - if (aliased) { - callName = aliased.name; - callNamespace = aliased.namespace; - } - } - - const externalInputs = !b.getInputsInline(); - if (func.isIdentity) - return args[0]; - else if (func.property) { - return H.mkPropertyAccess(callName, args[0]); - } else if (callName == "@get@") { - return H.mkPropertyAccess(args[1].op.replace(/.*\./, ""), args[0]); - } else if (callName == "@set@") { - return H.mkAssign(H.mkPropertyAccess(args[1].op.replace(/.*\./, "").replace(/@set/, ""), args[0]), args[2]); - } else if (callName == "@change@") { - return H.mkSimpleCall("+=", [H.mkPropertyAccess(args[1].op.replace(/.*\./, "").replace(/@set/, ""), args[0]), args[2]]) - } else if (func.isExtensionMethod) { - if (func.attrs.defaultInstance) { - let instance: JsNode; - if (isMutatingBlock(b) && b.mutation.getMutationType() === MutatorTypes.DefaultInstanceMutator) { - instance = b.mutation.compileMutation(e, comments); - } - - if (instance) { - args.unshift(instance); - } - else { - args.unshift(mkText(func.attrs.defaultInstance)); - } - } - return H.extensionCall(callName, args, externalInputs); - } else if (callNamespace) { - return H.namespaceCall(callNamespace, callName, args, externalInputs); - } else { - return H.stdCall(callName, args, externalInputs); - } - } - - function compileStdBlock(e: Environment, b: Blockly.Block, f: StdFunc, comments: string[]) { - return mkStmt(compileStdCall(e, b, f, comments)) - } - - function mkCallWithCallback(e: Environment, n: string, f: string, args: JsNode[], body: JsNode, argumentDeclaration?: JsNode, isExtension = false): JsNode { - body.noFinalNewline = true - let callback: JsNode; - if (argumentDeclaration) { - callback = mkGroup([argumentDeclaration, body]); - } - else { - callback = mkGroup([mkText("function ()"), body]); - } - - if (isExtension) - return mkStmt(H.extensionCall(f, args.concat([callback]), false)); - else if (n) - return mkStmt(H.namespaceCall(n, f, args.concat([callback]), false)); - else - return mkStmt(H.mkCall(f, args.concat([callback]), false)); - } - - function compileStartEvent(e: Environment, b: Blockly.Block): JsNode { - const bBody = getInputTargetBlock(b, "HANDLER"); - const body = compileStatements(e, bBody); - - if (pxt.appTarget.compile && pxt.appTarget.compile.onStartText && body && body.children) { - body.children.unshift(mkStmt(mkText(`// ${pxtc.ON_START_COMMENT}\n`))) - } - - return body; - } - - function compileEvent(e: Environment, b: Blockly.Block, stdfun: StdFunc, args: BlockParameter[], ns: string, comments: string[]): JsNode { - const compiledArgs: JsNode[] = args.map(arg => compileArgument(e, b, arg, comments)); - const bBody = getInputTargetBlock(b, "HANDLER"); - const body = compileStatements(e, bBody); - - if (pxt.appTarget.compile && pxt.appTarget.compile.emptyEventHandlerComments && body.children.length === 0) { - body.children.unshift(mkStmt(mkText(`// ${pxtc.HANDLER_COMMENT}`))) - } - - let argumentDeclaration: JsNode; - - if (isMutatingBlock(b) && b.mutation.getMutationType() === MutatorTypes.ObjectDestructuringMutator) { - argumentDeclaration = b.mutation.compileMutation(e, comments); - } - else if (stdfun.comp.handlerArgs.length) { - let handlerArgs = getEscapedCBParameters(b, stdfun, e); - argumentDeclaration = mkText(`function (${handlerArgs.join(", ")})`) - } - - return mkCallWithCallback(e, ns, stdfun.f, compiledArgs, body, argumentDeclaration, stdfun.isExtensionMethod); - } - - function isMutatingBlock(b: Blockly.Block): b is MutatingBlock { - return !!(b as MutatingBlock).mutation; - } - - function compileImage(e: Environment, b: Blockly.Block, frames: number, columns: number, rows: number, n: string, f: string, args?: JsNode[]): JsNode { - args = args === undefined ? [] : args; - let state = "\n"; - rows = rows || 5; - columns = (columns || 5) * frames; - let leds = b.getFieldValue("LEDS"); - leds = leds.replace(/[ `\n]+/g, ''); - for (let i = 0; i < rows; ++i) { - for (let j = 0; j < columns; ++j) { - if (j > 0) - state += ' '; - state += (leds[(i * columns) + j] === '#') ? "#" : "."; - } - state += '\n'; - } - let lit = H.mkStringLiteral(state) - lit.canIndentInside = true - return H.namespaceCall(n, f, [lit].concat(args), false); - } - - // A description of each function from the "device library". Types are fetched - // from the Blockly blocks definition. - // - the key is the name of the Blockly.Block that we compile into a device call; - // - [f] is the TouchDevelop function name we compile to - // - [args] is a list of names; the name is taken to be either the name of a - // Blockly field value or, if not found, the name of a Blockly input block; if a - // field value is found, then this generates a string expression. If argument is a literal, simply emits the literal. - // - [isExtensionMethod] is a flag so that instead of generating a TouchDevelop - // call like [f(x, y...)], we generate the more "natural" [x → f (y...)] - // - [namespace] is also an optional flag to generate a "namespace" call, that - // is, "basic -> show image" instead of "micro:bit -> show image". - export interface StdFunc { - f: string; - comp: BlockCompileInfo; - attrs: ts.pxtc.CommentAttrs; - isExtensionMethod?: boolean; - isExpression?: boolean; - imageLiteral?: number; - imageLiteralColumns?: number; - imageLiteralRows?: number; - hasHandler?: boolean; - property?: boolean; - namespace?: string; - isIdentity?: boolean; // TD_ID shim - } - - function compileStatementBlock(e: Environment, b: Blockly.Block): JsNode[] { - let r: JsNode[]; - const comments: string[] = []; - e.stats[b.type] = (e.stats[b.type] || 0) + 1; - maybeAddComment(b, comments); - switch (b.type) { - case 'controls_if': - r = compileControlsIf(e, b, comments); - break; - case 'pxt_controls_for': - case 'controls_for': - case 'controls_simple_for': - r = compileControlsFor(e, b, comments); - break; - case 'pxt_controls_for_of': - case 'controls_for_of': - r = compileControlsForOf(e, b, comments); - break; - case 'variables_set': - r = [compileSet(e, b, comments)]; - break; - - case 'variables_change': - r = [compileChange(e, b, comments)]; - break; - - case 'controls_repeat_ext': - r = compileControlsRepeat(e, b, comments); - break; - - case 'device_while': - r = compileWhile(e, b, comments); - break; - case 'procedures_defnoreturn': - r = compileProcedure(e, b, comments); - break; - case 'function_definition': - r = compileFunctionDefinition(e, b, comments); - break - case 'procedures_callnoreturn': - r = [compileProcedureCall(e, b, comments)]; - break; - case 'function_call': - r = [compileFunctionCall(e, b, comments, true)]; - break; - case pxtc.TS_RETURN_STATEMENT_TYPE: - r = [compileReturnStatement(e, b, comments)]; - break; - case ts.pxtc.ON_START_TYPE: - r = compileStartEvent(e, b).children; - break; - case pxtc.TS_STATEMENT_TYPE: - r = compileTypescriptBlock(e, b); - break; - case pxtc.PAUSE_UNTIL_TYPE: - r = compilePauseUntilBlock(e, b, comments); - break; - case pxtc.TS_DEBUGGER_TYPE: - r = compileDebuggeStatementBlock(e, b); - break; - case pxtc.TS_BREAK_TYPE: - r = compileBreakStatementBlock(e, b); - break; - case pxtc.TS_CONTINUE_TYPE: - r = compileContinueStatementBlock(e, b); - break; - default: - let call = e.stdCallTable[b.type]; - if (call) r = [compileCall(e, b, comments)]; - else r = [mkStmt(compileExpression(e, b, comments))]; - break; - } - let l = r[r.length - 1]; if (l && !l.id) l.id = b.id; - - if (comments.length) { - addCommentNodes(comments, r) - } - - r.forEach(l => { - if ((l.type === NT.Block || l.type === NT.Prefix && Util.startsWith(l.op, "//")) && (b.type != pxtc.ON_START_TYPE || !l.id)) { - l.id = b.id - } - }); - - return r; - } - - function compileStatements(e: Environment, b: Blockly.Block): JsNode { - let stmts: JsNode[] = []; - let firstBlock = b; - - while (b) { - if (b.isEnabled()) append(stmts, compileStatementBlock(e, b)); - b = b.getNextBlock(); - } - - if (firstBlock && e.blockDeclarations[firstBlock.id]) { - e.blockDeclarations[firstBlock.id].filter(v => !v.alreadyDeclared).forEach(varInfo => { - stmts.unshift(mkVariableDeclaration(varInfo, e.blocksInfo)); - varInfo.alreadyDeclared = BlockDeclarationType.Implicit; - }); - } - return mkBlock(stmts); - } - - function compileTypescriptBlock(e: Environment, b: Blockly.Block) { - return (b as GrayBlockStatement).getLines().map(line => mkText(line + "\n")); - } - - function compileDebuggeStatementBlock(e: Environment, b: Blockly.Block) { - if (b.getFieldValue("ON_OFF") == "1") { - return [ - mkText("debugger;\n") - ] - } - return []; - } - - function compileBreakStatementBlock(e: Environment, b: Blockly.Block) { - return [mkText("break;\n")] - } - - function compileContinueStatementBlock(e: Environment, b: Blockly.Block) { - return [mkText("continue;\n")] - } - - function prefixWithSemicolon(n: JsNode) { - const emptyStatement = mkStmt(mkText(";")); - emptyStatement.glueToBlock = GlueMode.NoSpace; - return mkGroup([emptyStatement, n]); - } - - function compilePauseUntilBlock(e: Environment, b: Blockly.Block, comments: string[]): JsNode[] { - const options = pxt.appTarget.runtime && pxt.appTarget.runtime.pauseUntilBlock; - Util.assert(!!options, "target has block enabled"); - - const ns = options.namespace; - const name = options.callName || "pauseUntil"; - const arg = compileArgument(e, b, { definitionName: "PREDICATE", actualName: "PREDICATE" }, comments); - const lambda = [mkGroup([mkText("() => "), arg])]; - - if (ns) { - return [mkStmt(H.namespaceCall(ns, name, lambda, false))]; - } - else { - return [mkStmt(H.mkCall(name, lambda, false, false))]; - } - } - - // This function creates an empty environment where type inference has NOT yet - // been performed. - // - All variables have been assigned an initial [Point] in the union-find. - // - Variables have been marked to indicate if they are compatible with the - // TouchDevelop for-loop model. - export function mkEnv(w: Blockly.Workspace, blockInfo?: pxtc.BlocksInfo, options: BlockCompileOptions = {}): Environment { - // The to-be-returned environment. - let e = emptyEnv(w, options); - e.blocksInfo = blockInfo; - - // append functions in stdcalltable - if (blockInfo) { - // Enums, tagged templates, and namespaces are not enclosed in namespaces, - // so add them to the taken names to avoid collision - Object.keys(blockInfo.apis.byQName).forEach(name => { - const info = blockInfo.apis.byQName[name]; - // Note: the check for info.pkg filters out functions defined in the user's project. - // Otherwise, after the first compile the function will be renamed because it conflicts - // with itself. You can still get collisions if you attempt to define a function with - // the same name as a function defined in another file in the user's project (e.g. custom.ts) - if (info.pkg && (info.kind === pxtc.SymbolKind.Enum || info.kind === pxtc.SymbolKind.Function || info.kind === pxtc.SymbolKind.Module || info.kind === pxtc.SymbolKind.Variable)) { - e.renames.takenNames[info.qName] = true; - } - }); - - if (blockInfo.enumsByName) { - Object.keys(blockInfo.enumsByName).forEach(k => e.enums.push(blockInfo.enumsByName[k])); - } - - if (blockInfo.kindsByName) { - Object.keys(blockInfo.kindsByName).forEach(k => e.kinds.push(blockInfo.kindsByName[k])); - } - - blockInfo.blocks - .forEach(fn => { - if (e.stdCallTable[fn.attributes.blockId]) { - pxt.reportError("blocks", "function already defined", { - "details": fn.attributes.blockId, - "qualifiedName": fn.qName, - "packageName": fn.pkg, - }); - return; - } - e.renames.takenNames[fn.namespace] = true; - const comp = pxt.blocks.compileInfo(fn); - const instance = !!comp.thisParameter; - - e.stdCallTable[fn.attributes.blockId] = { - namespace: fn.namespace, - f: fn.name, - comp, - attrs: fn.attributes, - isExtensionMethod: instance, - isExpression: fn.retType && fn.retType !== "void", - imageLiteral: fn.attributes.imageLiteral || fn.attributes.gridLiteral, - imageLiteralColumns: fn.attributes.imageLiteralColumns, - imageLiteralRows: fn.attributes.imageLiteralRows, - hasHandler: pxt.blocks.hasHandler(fn), - property: !fn.parameters, - isIdentity: fn.attributes.shim == "TD_ID" - } - }); - - w.getTopBlocks(false).filter(isFunctionDefinition).forEach(b => { - // Add functions to the rename map to prevent name collisions with variables - const name = b.type === "procedures_defnoreturn" ? b.getFieldValue("NAME") : b.getField("function_name").getText(); - escapeVarName(name, e, true); - }); - } - - return e; - } - - export function compileBlockAsync(b: Blockly.Block, blockInfo: pxtc.BlocksInfo): Promise { - const w = b.workspace; - const e = mkEnv(w, blockInfo); - infer(w && w.getAllBlocks(false), e, w); - const compiled = compileStatementBlock(e, b) - removeAllPlaceholders(); - return tdASTtoTS(e, compiled); - } - - function eventWeight(b: Blockly.Block, e: Environment) { - if (b.type === ts.pxtc.ON_START_TYPE) { - return 0; - } - const api = e.stdCallTable[b.type]; - const key = callKey(e, b); - const hash = 1 + ts.pxtc.Util.codalHash16(key); - if (api && api.attrs.afterOnStart) - return hash; - else - return -hash; - } - - function compileWorkspace(e: Environment, w: Blockly.Workspace, blockInfo: pxtc.BlocksInfo): [JsNode[], BlockDiagnostic[]] { - try { - // all compiled top level blocks are events - let allBlocks = w.getAllBlocks(false); - - if (pxt.react.getTilemapProject) { - pxt.react.getTilemapProject().removeInactiveBlockAssets(allBlocks.map(b => b.id)); - } - - // the top blocks are storted by blockly - let topblocks = w.getTopBlocks(true); - // reorder remaining events by names (top blocks still contains disabled blocks) - topblocks = topblocks.sort((a, b) => { - return eventWeight(a, e) - eventWeight(b, e) - }); - // update disable blocks - updateDisabledBlocks(e, allBlocks, topblocks); - // drop disabled blocks - allBlocks = allBlocks.filter(b => b.isEnabled()); - topblocks = topblocks.filter(b => b.isEnabled()); - trackAllVariables(topblocks, e); - infer(allBlocks, e, w); - - const stmtsMain: JsNode[] = []; - - // compile workspace comments, add them to the top - const topComments = w.getTopComments(true); - const commentMap = groupWorkspaceComments(topblocks as Blockly.BlockSvg[], - topComments as Blockly.WorkspaceCommentSvg[]); - - commentMap.orphans.forEach(comment => append(stmtsMain, compileWorkspaceComment(comment).children)); - - topblocks.forEach(b => { - if (commentMap.idToComments[b.id]) { - commentMap.idToComments[b.id].forEach(comment => { - append(stmtsMain, compileWorkspaceComment(comment).children); - }); - } - if (b.type == ts.pxtc.ON_START_TYPE) - append(stmtsMain, compileStatementBlock(e, b)); - else { - const compiled = mkBlock(compileStatementBlock(e, b)); - if (compiled.type == NT.Block) - append(stmtsMain, compiled.children); - else stmtsMain.push(compiled) - } - }); - - const stmtsEnums: JsNode[] = []; - e.enums.forEach(info => { - const models = w.getVariablesOfType(info.name); - if (models && models.length) { - const members: [string, number][] = models.map(m => { - const match = /^(\d+)([^0-9].*)$/.exec(m.name); - if (match) { - return [match[2], parseInt(match[1])] as [string, number]; - } - else { - // Someone has been messing with the XML... - return [m.name, -1] as [string, number]; - } - }); - - members.sort((a, b) => a[1] - b[1]); - - const nodes: JsNode[] = []; - let lastValue = -1; - members.forEach(([name, value], index) => { - let newNode: JsNode; - if (info.isBitMask) { - const shift = Math.log2(value); - if (shift >= 0 && Math.floor(shift) === shift) { - newNode = H.mkAssign(mkText(name), H.mkSimpleCall("<<", [H.mkNumberLiteral(1), H.mkNumberLiteral(shift)])); - } - } else if (info.isHash) { - const hash = ts.pxtc.Util.codalHash16(name.toLowerCase()); - newNode = H.mkAssign(mkText(name), H.mkNumberLiteral(hash)) - } - if (!newNode) { - if (value === lastValue + 1) { - newNode = mkText(name); - } - else { - newNode = H.mkAssign(mkText(name), H.mkNumberLiteral(value)); - } - } - nodes.push(newNode); - lastValue = value; - }); - const declarations = mkCommaSep(nodes, true); - declarations.glueToBlock = GlueMode.NoSpace; - stmtsEnums.push(mkGroup([ - mkText(`enum ${info.name}`), - mkBlock([declarations]) - ])); - } - }); - - e.kinds.forEach(info => { - const models = w.getVariablesOfType("KIND_" + info.name); - if (models && models.length) { - const userDefined = models.map(m => m.name).filter(n => info.initialMembers.indexOf(n) === -1); - - if (userDefined.length) { - stmtsEnums.push(mkGroup([ - mkText(`namespace ${info.name}`), - mkBlock(userDefined.map(varName => mkStmt(mkText(`export const ${varName} = ${info.name}.${info.createFunctionName}()`)))) - ])); - } - } - }); - - const leftoverVars = e.allVariables.filter(v => !v.alreadyDeclared).map(v => mkVariableDeclaration(v, blockInfo)); - - e.allVariables.filter(v => v.alreadyDeclared === BlockDeclarationType.Implicit && !v.isAssigned).forEach(v => { - const t = getConcreteType(v.type); - - // The primitive types all get initializers set to default values, other types are set to null - if (t.type === "string" || t.type === "number" || t.type === "boolean" || isArrayType(t.type)) return; - - e.diagnostics.push({ - blockId: v.firstReference && v.firstReference.id, - message: lf("Variable '{0}' is never assigned", v.name) - }); - }); - - return [stmtsEnums.concat(leftoverVars.concat(stmtsMain)), e.diagnostics]; - } catch (err) { - let be: Blockly.Block = (err as any).block; - if (be) { - be.setWarningText(err + ""); - e.errors.push(be); - } - else { - throw err; - } - } finally { - removeAllPlaceholders(); - } - - return [null, null] // unreachable - } - - export function callKey(e: Environment, b: Blockly.Block): string { - if (b.type == ts.pxtc.ON_START_TYPE) - return JSON.stringify({ name: ts.pxtc.ON_START_TYPE }); - else if (b.type == ts.pxtc.FUNCTION_DEFINITION_TYPE) - return JSON.stringify({ type: "function", name: b.getFieldValue("function_name") }); - - const key = JSON.stringify(blockKey(b)) - .replace(/"id"\s*:\s*"[^"]+"/g, ''); // remove blockly ids - - return key; - } - - function blockKey(b: Blockly.Block) { - const fields: string[] = [] - const inputs: any[] = [] - for (const input of b.inputList) { - for (const field of input.fieldRow) { - if (field.name) { - fields.push(field.getText()) - } - } - - if (input.type === Blockly.INPUT_VALUE) { - if (input.connection.targetBlock()) { - inputs.push(blockKey(input.connection.targetBlock())); - } - else { - inputs.push(null); - } - } - } - - return { - type: b.type, - fields, - inputs - }; - } - - function setChildrenEnabled(block: Blockly.Block, enabled: boolean) { - block.setEnabled(enabled); - // propagate changes - const children = block.getDescendants(false); - for (const child of children) { - child.setEnabled(enabled); - } - } - - function updateDisabledBlocks(e: Environment, allBlocks: Blockly.Block[], topBlocks: Blockly.Block[]) { - // unset disabled - allBlocks.forEach(b => b.setEnabled(true)); - - // update top blocks - const events: Map = {}; - - function flagDuplicate(key: string, block: Blockly.Block) { - const otherEvent = events[key]; - if (otherEvent) { - // another block is already registered - setChildrenEnabled(block, false); - } else { - setChildrenEnabled(block, true); - events[key] = block; - } - } - - topBlocks.forEach(b => { - const call = e.stdCallTable[b.type]; - // multiple calls allowed - if (b.type == ts.pxtc.ON_START_TYPE) - flagDuplicate(ts.pxtc.ON_START_TYPE, b); - else if (isFunctionDefinition(b) || call && call.attrs.blockAllowMultiple && !call.attrs.handlerStatement) return; - // is this an event? - else if (call && call.hasHandler && !call.attrs.handlerStatement) { - // compute key that identifies event call - // detect if same event is registered already - const key = call.attrs.blockHandlerKey || callKey(e, b); - flagDuplicate(key, b); - } else { - // all non-events are disabled - let t = b; - while (t) { - setChildrenEnabled(b, false); - t = t.getNextBlock(); - } - } - }); - } - - export interface BlockCompilationResult { - source: string; - sourceMap: BlockSourceInterval[]; - stats: pxt.Map; - diagnostics: BlockDiagnostic[]; - } - - export interface BlockCompileOptions { - emitTilemapLiterals?: boolean; - } - - export function findBlockIdByPosition(sourceMap: BlockSourceInterval[], loc: { start: number; length: number; }): string { - if (!loc) return undefined; - let bestChunk: BlockSourceInterval; - let bestChunkLength: number; - // look for smallest chunk containing the block - for (let i = 0; i < sourceMap.length; ++i) { - let chunk = sourceMap[i]; - if (chunk.startPos <= loc.start - && chunk.endPos >= loc.start + loc.length - && (!bestChunk || bestChunkLength > chunk.endPos - chunk.startPos)) { - bestChunk = chunk; - bestChunkLength = chunk.endPos - chunk.startPos; - } - } - if (bestChunk) { - return bestChunk.id; - } - return undefined; - } - - export function findBlockIdByLine(sourceMap: BlockSourceInterval[], loc: { start: number; length: number; }): string { - if (!loc) return undefined; - let bestChunk: BlockSourceInterval; - let bestChunkLength: number; - // look for smallest chunk containing the block - for (let i = 0; i < sourceMap.length; ++i) { - let chunk = sourceMap[i]; - if (chunk.startLine <= loc.start - && chunk.endLine > loc.start + loc.length - && (!bestChunk || bestChunkLength > chunk.endLine - chunk.startLine)) { - bestChunk = chunk; - bestChunkLength = chunk.endLine - chunk.startLine; - } - } - if (bestChunk) { - return bestChunk.id; - } - return undefined; - } - - export function compileAsync(b: Blockly.Workspace, blockInfo: pxtc.BlocksInfo, opts: BlockCompileOptions = {}): Promise { - const e = mkEnv(b, blockInfo, opts); - const [nodes, diags] = compileWorkspace(e, b, blockInfo); - const result = tdASTtoTS(e, nodes, diags); - return result; - } - - function tdASTtoTS(env: Environment, app: JsNode[], diags?: BlockDiagnostic[]): Promise { - let res = flattenNode(app) - - // Note: the result of format is not used! - - return workerOpAsync("format", { format: { input: res.output, pos: 1 } }).then(() => { - return { - source: res.output, - sourceMap: res.sourceMap, - stats: env.stats, - diagnostics: diags || [] - }; - }) - - } - - function maybeAddComment(b: Blockly.Block, comments: string[]) { - // Check if getCommentText exists, block may be placeholder - const text = b.getCommentText?.(); - if (text) { - comments.push(text) - } - } - - function addCommentNodes(comments: string[], r: JsNode[]) { - const commentNodes: JsNode[] = [] - - for (const comment of comments) { - for (const line of comment.split("\n")) { - commentNodes.push(mkText(`// ${line}`)) - commentNodes.push(mkNewLine()) - } - } - - for (const commentNode of commentNodes.reverse()) { - r.unshift(commentNode) - } - } - - function mkVariableDeclaration(v: VarInfo, blockInfo: pxtc.BlocksInfo) { - const t = getConcreteType(v.type); - let defl: JsNode; - - if (t.type === "Array") { - defl = mkText("[]"); - } - else { - defl = defaultValueForType(t); - } - - let tp = "" - if (defl.op == "null" || defl.op == "[]") { - let tpname = t.type - // If the type is "Array" or null[] it means that we failed to narrow the type of array. - // Best we can do is just default to number[] - if (tpname === "Array" || tpname === "null[]") { - tpname = "number[]"; - } - let tpinfo = blockInfo.apis.byQName[tpname] - if (tpinfo && tpinfo.attributes.autoCreate) - defl = mkText(tpinfo.attributes.autoCreate + "()") - else - tp = ": " + tpname - } - return mkStmt(mkText("let " + v.escapedName + tp + " = "), defl) - } - - function countOptionals(b: Blockly.Block, func: StdFunc) { - if (func.attrs.compileHiddenArguments) { - return func.comp.parameters.reduce((prev, block) => { - if (block.isOptional) prev++; - return prev - }, 0); - } - if ((b as MutatingBlock).mutationToDom) { - const el = (b as MutatingBlock).mutationToDom(); - if (el.hasAttribute("_expanded")) { - const val = parseInt(el.getAttribute("_expanded")); - return isNaN(val) ? 0 : Math.max(val, 0); - } - } - return 0; - } - - function visibleParams({ comp }: StdFunc, optionalCount: number) { - const res: pxt.blocks.BlockParameter[] = []; - if (comp.thisParameter) { - res.push(comp.thisParameter); - } - - comp.parameters.forEach(p => { - if (p.isOptional && optionalCount > 0) { - res.push(p); - --optionalCount; - } - else if (!p.isOptional) { - res.push(p); - } - }); - - return res; - } - - function getEscapedCBParameters(b: Blockly.Block, stdfun: StdFunc, e: Environment): string[] { - return getCBParameters(b, stdfun).map(binding => lookup(e, b, binding.name).escapedName); - } - - function getCBParameters(b: Blockly.Block, stdfun: StdFunc): DeclaredVariable[] { - let handlerArgs: DeclaredVariable[] = []; - if (stdfun.attrs.draggableParameters) { - for (let i = 0; i < stdfun.comp.handlerArgs.length; i++) { - const arg = stdfun.comp.handlerArgs[i]; - let varName: string; - const varBlock = getInputTargetBlock(b, "HANDLER_DRAG_PARAM_" + arg.name) as Blockly.Block; - - if (stdfun.attrs.draggableParameters === "reporter") { - varName = varBlock && varBlock.getFieldValue("VALUE"); - } else { - varName = varBlock && varBlock.getField("VAR").getText(); - } - - if (varName !== null) { - handlerArgs.push({ - name: varName, - type: mkPoint(arg.type) - }); - } - else { - break; - } - } - } - else { - for (let i = 0; i < stdfun.comp.handlerArgs.length; i++) { - const arg = stdfun.comp.handlerArgs[i]; - const varField = b.getField("HANDLER_" + arg.name); - const varName = varField && varField.getText(); - if (varName !== null) { - handlerArgs.push({ - name: varName, - type: mkPoint(arg.type) - }); - } - else { - break; - } - } - } - return handlerArgs; - } - - interface Rect { - id: string; - x: number; - y: number; - width: number; - height: number; - } - - function groupWorkspaceComments(blocks: Blockly.BlockSvg[], comments: Blockly.WorkspaceCommentSvg[]) { - if (!blocks.length || blocks.some(b => !b.rendered)) { - return { - orphans: comments, - idToComments: {} - }; - } - const blockBounds: Rect[] = blocks.map(block => { - const bounds = block.getBoundingRectangle(); - const size = block.getHeightWidth(); - return { - id: block.id, - x: bounds.left, - y: bounds.top, - width: size.width, - height: size.height - } - }); - - const map: CommentMap = { - orphans: [], - idToComments: {} - }; - - const radius = 20; - for (const comment of comments) { - const bounds = comment.getBoundingRectangle(); - const size = comment.getHeightWidth(); - - const x = bounds.left; - const y = bounds.top; - - let parent: Rect; - - for (const rect of blockBounds) { - if (doesIntersect(x, y, size.width, size.height, rect)) { - parent = rect; - } - else if (!parent && doesIntersect(x - radius, y - radius, size.width + radius * 2, size.height + radius * 2, rect)) { - parent = rect; - } - } - - if (parent) { - if (!map.idToComments[parent.id]) { - map.idToComments[parent.id] = []; - } - map.idToComments[parent.id].push(comment); - } - else { - map.orphans.push(comment); - } - } - - return map; - } - - function referencedWithinScope(scope: Scope, varID: number) { - if (scope.referencedVars.indexOf(varID) !== -1) { - return true; - } - else { - for (const child of scope.children) { - if (referencedWithinScope(child, varID)) return true; - } - } - return false; - } - - function assignedWithinScope(scope: Scope, varID: number) { - if (scope.assignedVars.indexOf(varID) !== -1) { - return true; - } - else { - for (const child of scope.children) { - if (assignedWithinScope(child, varID)) return true; - } - } - return false; - } - - function escapeVariables(current: Scope, e: Environment) { - for (const varName of Object.keys(current.declaredVars)) { - const info = current.declaredVars[varName]; - if (!info.escapedName) info.escapedName = escapeVarName(varName); - } - - current.children.forEach(c => escapeVariables(c, e)); - - - function escapeVarName(originalName: string): string { - if (!originalName) return '_'; - - let n = ts.pxtc.escapeIdentifier(originalName); - - if (e.renames.takenNames[n] || nameIsTaken(n, current, originalName)) { - let i = 2; - - while (e.renames.takenNames[n + i] || nameIsTaken(n + i, current, originalName)) { - i++; - } - - n += i; - } - - return n; - } - - function nameIsTaken(name: string, scope: Scope, originalName: string): boolean { - if (scope) { - for (const varName of Object.keys(scope.declaredVars)) { - const info = scope.declaredVars[varName]; - if ((originalName !== info.name || info.name !== info.escapedName) && info.escapedName === name) - return true; - } - return nameIsTaken(name, scope.parent, originalName); - } - - return false; - } - } - - - function findCommonScope(current: Scope, varID: number): Scope { - let ref: Scope; - - if (current.referencedVars.indexOf(varID) !== -1) { - return current; - } - - for (const child of current.children) { - if (referencedWithinScope(child, varID)) { - if (assignedWithinScope(child, varID)) { - return current; - } - if (!ref) { - ref = child; - } - else { - return current; - } - } - } - - return ref ? findCommonScope(ref, varID) : undefined; - } - - function trackAllVariables(topBlocks: Blockly.Block[], e: Environment) { - let id = 1; - let topScope: Scope; - - // First, look for on-start - topBlocks.forEach(block => { - if (block.type === ts.pxtc.ON_START_TYPE) { - const firstStatement = block.getInputTargetBlock("HANDLER"); - if (firstStatement) { - topScope = { - firstStatement: firstStatement, - declaredVars: {}, - referencedVars: [], - children: [], - assignedVars: [] - } - trackVariables(firstStatement, topScope, e); - } - } - }); - - // If we didn't find on-start, then create an empty top scope - if (!topScope) { - topScope = { - firstStatement: null, - declaredVars: {}, - referencedVars: [], - children: [], - assignedVars: [] - } - } - - topBlocks.forEach(block => { - if (block.type === ts.pxtc.ON_START_TYPE) { - return; - } - trackVariables(block, topScope, e); - }); - - Object.keys(topScope.declaredVars).forEach(varName => { - const varID = topScope.declaredVars[varName]; - delete topScope.declaredVars[varName]; - const declaringScope = findCommonScope(topScope, varID.id) || topScope; - declaringScope.declaredVars[varName] = varID; - }) - - markDeclarationLocations(topScope, e); - escapeVariables(topScope, e); - - return topScope; - - function trackVariables(block: Blockly.Block, currentScope: Scope, e: Environment) { - e.idToScope[block.id] = currentScope; - - if (block.type === "variables_get") { - const name = block.getField("VAR").getText(); - const info = findOrDeclareVariable(name, currentScope); - currentScope.referencedVars.push(info.id); - } - else if (block.type === "variables_set" || block.type === "variables_change") { - const name = block.getField("VAR").getText(); - const info = findOrDeclareVariable(name, currentScope); - currentScope.assignedVars.push(info.id); - currentScope.referencedVars.push(info.id); - } - else if (block.type === pxtc.TS_STATEMENT_TYPE) { - const declaredVars: string = (block as GrayBlockStatement).declaredVariables - if (declaredVars) { - const varNames = declaredVars.split(","); - varNames.forEach(vName => { - const info = findOrDeclareVariable(vName, currentScope); - info.alreadyDeclared = BlockDeclarationType.Argument; - }); - } - } - - if (hasStatementInput(block)) { - const vars: VarInfo[] = getDeclaredVariables(block, e).map(binding => { - return { - ...binding, - id: id++ - } - }); - - - let parentScope = currentScope; - if (vars.length) { - // We need to create a scope for this block, and then a scope - // for each statement input (in case there are multiple) - - parentScope = { - parent: currentScope, - firstStatement: block, - declaredVars: {}, - referencedVars: [], - assignedVars: [], - children: [] - }; - - vars.forEach(v => { - v.alreadyDeclared = BlockDeclarationType.Assigned; - parentScope.declaredVars[v.name] = v; - }); - - e.idToScope[block.id] = parentScope; - } - - - if (currentScope !== parentScope) { - currentScope.children.push(parentScope); - } - - forEachChildExpression(block, child => { - trackVariables(child, parentScope, e); - }); - - forEachStatementInput(block, connectedBlock => { - const newScope: Scope = { - parent: parentScope, - firstStatement: connectedBlock, - declaredVars: {}, - referencedVars: [], - assignedVars: [], - children: [] - }; - parentScope.children.push(newScope); - trackVariables(connectedBlock, newScope, e); - }); - } - else { - forEachChildExpression(block, child => { - trackVariables(child, currentScope, e); - }); - } - - if (block.nextConnection && block.nextConnection.targetBlock()) { - trackVariables(block.nextConnection.targetBlock(), currentScope, e); - } - } - - function findOrDeclareVariable(name: string, scope: Scope): VarInfo { - if (scope.declaredVars[name]) { - return scope.declaredVars[name]; - } - else if (scope.parent) { - return findOrDeclareVariable(name, scope.parent); - } - else { - // Declare it in the top scope - scope.declaredVars[name] = { - name, - type: mkPoint(null), - id: id++ - }; - return scope.declaredVars[name]; - } - } - } - - function getVarInfo(name: string, scope: Scope): VarInfo { - if (scope && scope.declaredVars[name]) { - return scope.declaredVars[name]; - } - else if (scope && scope.parent) { - return getVarInfo(name, scope.parent); - } - else { - return null; - } - } - - - function hasStatementInput(block: Blockly.Block) { - return block.inputList.some(i => i.type === Blockly.NEXT_STATEMENT); - } - - interface DeclaredVariable { - name: string; - type: Point; - isFunctionParameter?: boolean; - } - - function getDeclaredVariables(block: Blockly.Block, e: Environment): DeclaredVariable[] { - switch (block.type) { - case 'pxt_controls_for': - case 'controls_simple_for': - return [{ - name: getLoopVariableField(block).getField("VAR").getText(), - type: pNumber - }]; - case 'pxt_controls_for_of': - case 'controls_for_of': - return [{ - name: getLoopVariableField(block).getField("VAR").getText(), - type: mkPoint(null) - }]; - case 'function_definition': - return (block as Blockly.FunctionDefinitionBlock).getArguments().filter(arg => arg.type === "Array") - .map(arg => { - const point = mkPoint(null); - point.isArrayType = true; - return { - name: arg.name, - type: point, - isFunctionParameter: true - } - }); - default: - break; - } - - if (isMutatingBlock(block)) { - const declarations = block.mutation.getDeclaredVariables(); - if (declarations) { - return Object.keys(declarations).map(varName => ({ - name: varName, - type: mkPoint(declarations[varName]) - })); - } - } - - let stdFunc = e.stdCallTable[block.type]; - if (stdFunc && stdFunc.comp.handlerArgs.length) { - return getCBParameters(block, stdFunc); - } - - return []; - } - - function forEachChildExpression(block: Blockly.Block, cb: (block: Blockly.Block) => void, recursive = false) { - block.inputList.filter(i => i.type === Blockly.INPUT_VALUE).forEach(i => { - if (i.connection && i.connection.targetBlock()) { - cb(i.connection.targetBlock()); - if (recursive) { - forEachChildExpression(i.connection.targetBlock(), cb, recursive); - } - } - }); - } - - function forEachStatementInput(block: Blockly.Block, cb: (block: Blockly.Block) => void) { - block.inputList.filter(i => i.type === Blockly.NEXT_STATEMENT).forEach(i => { - if (i.connection && i.connection.targetBlock()) { - cb(i.connection.targetBlock()); - } - }) - } - - function printScope(scope: Scope, depth = 0) { - const declared = Object.keys(scope.declaredVars).map(k => `${k}(${scope.declaredVars[k].id})`).join(","); - const referenced = scope.referencedVars.join(", "); - console.log(`${mkIndent(depth)}SCOPE: ${scope.firstStatement ? scope.firstStatement.type : "TOP-LEVEL"}`) - if (declared.length) { - console.log(`${mkIndent(depth)}DECS: ${declared}`) - } - // console.log(`${mkIndent(depth)}REFS: ${referenced}`) - scope.children.forEach(s => printScope(s, depth + 1)); - } - - function mkIndent(depth: number) { - let res = ""; - for (let i = 0; i < depth; i++) { - res += " "; - } - return res; - } - - function markDeclarationLocations(scope: Scope, e: Environment) { - const declared = Object.keys(scope.declaredVars); - if (declared.length) { - const decls = declared.map(name => scope.declaredVars[name]); - - if (scope.firstStatement) { - // If we can't find a better place to declare the variable, we'll declare - // it before the first statement in the code block so we need to keep - // track of the blocks ids - e.blockDeclarations[scope.firstStatement.id] = decls.concat(e.blockDeclarations[scope.firstStatement.id] || []); - } - - decls.forEach(d => e.allVariables.push(d)); - } - - scope.children.forEach(child => markDeclarationLocations(child, e)); - } - - - function doesIntersect(x: number, y: number, width: number, height: number, other: Rect) { - const xOverlap = between(x, other.x, other.x + other.width) || between(other.x, x, x + width); - const yOverlap = between(y, other.y, other.y + other.height) || between(other.y, y, y + height); - return xOverlap && yOverlap; - - function between(val: number, lower: number, upper: number) { - return val >= lower && val <= upper; - } - } - - function isFunctionDefinition(b: Blockly.Block) { - return b.type === "procedures_defnoreturn" || b.type === "function_definition"; - } - - function getFunctionName(functionBlock: Blockly.Block) { - return functionBlock.getField("function_name").getText(); - } - - // @param strict - if true, only return true if there is a return statement - // somewhere in the call graph that returns a call to this function. If false, - // return true if the function is called as an expression anywhere in the call - // graph - function isFunctionRecursive(b: Blockly.Block, strict: boolean) { - const functionName = getFunctionName(b) - const visited: pxt.Map = {}; - - return checkForCallRecursive(b); - - function checkForCallRecursive(functionDefinition: Blockly.Block) { - let calls: Blockly.Block[]; - - if (strict) { - calls = functionDefinition.getDescendants(false) - .filter(child => child.type == "function_return") - .map(returnStatement => getInputTargetBlock(returnStatement, "RETURN_VALUE")) - .filter(returnValue => returnValue && returnValue.type === "function_call_output") - } - else { - calls = functionDefinition.getDescendants(false).filter(child => child.type == "function_call_output"); - } - - for (const call of calls) { - const callName = getFunctionName(call); - - if (callName === functionName) return true; - - if (visited[callName]) continue; - visited[callName] = true; - - if (checkForCallRecursive(Blockly.Functions.getDefinition(callName, call.workspace))) { - return true; - } - } - - return false; - } - } - - function isPlaceholderBlock(b: Blockly.Block): b is PlaceholderLikeBlock { - return b.type == "placeholder" || b.type === pxtc.TS_OUTPUT_TYPE; - } -} diff --git a/pxtblocks/blocklycustomeditor.ts b/pxtblocks/blocklycustomeditor.ts deleted file mode 100644 index 5b43a04dbbca..000000000000 --- a/pxtblocks/blocklycustomeditor.ts +++ /dev/null @@ -1,71 +0,0 @@ - -namespace pxt.blocks { - - interface FieldEditorOptions { - field: Blockly.FieldCustomConstructor; - validator?: any; - } - - let registeredFieldEditors: Map = {}; - - export function initFieldEditors() { - registerFieldEditor('text', pxtblockly.FieldTextInput); - registerFieldEditor('note', pxtblockly.FieldNote); - registerFieldEditor('gridpicker', pxtblockly.FieldGridPicker); - registerFieldEditor('textdropdown', pxtblockly.FieldTextDropdown); - registerFieldEditor('numberdropdown', pxtblockly.FieldNumberDropdown); - registerFieldEditor('imagedropdown', pxtblockly.FieldImageDropdown); - registerFieldEditor('colorwheel', pxtblockly.FieldColorWheel); - registerFieldEditor('toggle', pxtblockly.FieldToggle); - registerFieldEditor('toggleonoff', pxtblockly.FieldToggleOnOff); - registerFieldEditor('toggleyesno', pxtblockly.FieldToggleYesNo); - registerFieldEditor('toggleupdown', pxtblockly.FieldToggleUpDown); - registerFieldEditor('toggledownup', pxtblockly.FieldToggleDownUp); - registerFieldEditor('togglehighlow', pxtblockly.FieldToggleHighLow); - registerFieldEditor('togglewinlose', pxtblockly.FieldToggleWinLose); - registerFieldEditor('colornumber', pxtblockly.FieldColorNumber); - registerFieldEditor('images', pxtblockly.FieldImages); - registerFieldEditor('sprite', pxtblockly.FieldSpriteEditor); - registerFieldEditor('animation', pxtblockly.FieldAnimationEditor); - registerFieldEditor('tilemap', pxtblockly.FieldTilemap); - registerFieldEditor('tileset', pxtblockly.FieldTileset); - registerFieldEditor('speed', pxtblockly.FieldSpeed); - registerFieldEditor('turnratio', pxtblockly.FieldTurnRatio); - registerFieldEditor('protractor', pxtblockly.FieldProtractor); - registerFieldEditor('position', pxtblockly.FieldPosition); - registerFieldEditor('melody', pxtblockly.FieldCustomMelody); - registerFieldEditor('soundeffect', pxtblockly.FieldSoundEffect); - registerFieldEditor('autocomplete', pxtblockly.FieldAutoComplete); - if (pxt.appTarget.appTheme?.songEditor) { - registerFieldEditor('musiceditor', pxtblockly.FieldMusicEditor); - } - } - - export function registerFieldEditor(selector: string, field: Blockly.FieldCustomConstructor, validator?: any) { - if (registeredFieldEditors[selector] == undefined) { - registeredFieldEditors[selector] = { - field: field, - validator: validator - } - } - } - - export function createFieldEditor(selector: string, text: string, params: any): Blockly.FieldCustom { - if (registeredFieldEditors[selector] == undefined) { - console.error(`Field editor ${selector} not registered`); - return null; - } - - if (!params) { - params = {}; - } - - Util.assert(params.lightMode == undefined, "lightMode is a reserved parameter for custom fields"); - - params.lightMode = pxt.options.light; - - let customField = registeredFieldEditors[selector]; - let instance = new customField.field(text, params, customField.validator); - return instance; - } -} \ No newline at end of file diff --git a/pxtblocks/blocklydiff.ts b/pxtblocks/blocklydiff.ts deleted file mode 100644 index a46cd7341194..000000000000 --- a/pxtblocks/blocklydiff.ts +++ /dev/null @@ -1,522 +0,0 @@ -namespace pxt.blocks { - export interface DiffOptions { - hideDeletedTopBlocks?: boolean; - hideDeletedBlocks?: boolean; - renderOptions?: BlocksRenderOptions; - statementsOnly?: boolean; // consider statement as a whole - } - - export interface DiffResult { - ws?: Blockly.WorkspaceSvg; - message?: string; - error?: any; - svg?: Element; - deleted: number; - added: number; - modified: number; - } - - // sniff ids to see if the xml was completly reconstructed - export function needsDecompiledDiff(oldXml: string, newXml: string): boolean { - if (!oldXml || !newXml) - return false; - // collect all ids - const oldids: pxt.Map = {}; - oldXml.replace(/id="([^"]+)"/g, (m, id) => { oldids[id] = true; return ""; }); - if (!Object.keys(oldids).length) - return false; - // test if any newid exists in old - let total = 0; - let found = 0; - newXml.replace(/id="([^"]+)"/g, (m, id) => { - total++; - if (oldids[id]) - found++; - return ""; - }); - return total > 0 && found == 0; - } - - export function diffXml(oldXml: string, newXml: string, options?: DiffOptions): DiffResult { - const oldWs = pxt.blocks.loadWorkspaceXml(oldXml, true); - const newWs = pxt.blocks.loadWorkspaceXml(newXml, true); - return diffWorkspace(oldWs, newWs, options); - } - - const UNMODIFIED_COLOR = "#d0d0d0"; - // Workspaces are modified in place! - function diffWorkspace(oldWs: Blockly.Workspace, newWs: Blockly.Workspace, options?: DiffOptions): DiffResult { - try { - Blockly.Events.disable(); - return diffWorkspaceNoEvents(oldWs, newWs, options); - } - catch (e) { - pxt.reportException(e); - return { - ws: undefined, - message: lf("Oops, we could not diff those blocks."), - error: e, - deleted: 0, - added: 0, - modified: 0 - } - } finally { - Blockly.Events.enable(); - } - } - - function logger() { - const log = pxt.options.debug || (window && /diffdbg=1/.test(window.location.href)) - ? console.log : (message?: any, ...args: any[]) => { }; - return log; - - } - - function diffWorkspaceNoEvents(oldWs: Blockly.Workspace, newWs: Blockly.Workspace, options?: DiffOptions): DiffResult { - pxt.tickEvent("blocks.diff", { started: 1 }) - options = options || {}; - const log = logger(); - if (!oldWs) { - return { - ws: undefined, - message: lf("All blocks are new."), - added: 0, - deleted: 0, - modified: 1 - }; // corrupted blocks - } - if (!newWs) { - return { - ws: undefined, - message: lf("The current blocks seem corrupted."), - added: 0, - deleted: 0, - modified: 1 - }; // corrupted blocks - } - - // remove all unmodified topblocks - // when doing a Blocks->TS roundtrip, all ids are trashed. - const oldXml: pxt.Map = pxt.Util.toDictionary(oldWs.getTopBlocks(false), b => normalizedDom(b, true)); - newWs.getTopBlocks(false) - .forEach(newb => { - const newn = normalizedDom(newb, true); - // try to find by id or by matching normalized xml - const oldb = oldWs.getBlockById(newb.id) || oldXml[newn]; - if (oldb) { - const oldn = normalizedDom(oldb, true); - if (newn == oldn) { - log(`fast unmodified top `, newb.id); - newb.dispose(false); - oldb.dispose(false); - } - } - }) - - // we'll ignore disabled blocks in the final output - - const oldBlocks = oldWs.getAllBlocks(false).filter(b => b.isEnabled()); - const oldTopBlocks = oldWs.getTopBlocks(false).filter(b => b.isEnabled()); - const newBlocks = newWs.getAllBlocks(false).filter(b => b.isEnabled()); - log(`blocks`, newBlocks.map(b => b.toDevString())); - log(newBlocks); - - if (oldBlocks.length == 0 && newBlocks.length == 0) { - pxt.tickEvent("blocks.diff", { moves: 1 }) - return { - ws: undefined, - message: lf("Some blocks were moved or changed."), - added: 0, - deleted: 0, - modified: 1 - }; // just moves - } - - // locate deleted and added blocks - const deletedTopBlocks = oldTopBlocks.filter(b => !newWs.getBlockById(b.id)); - const deletedBlocks = oldBlocks.filter(b => !newWs.getBlockById(b.id)); - const addedBlocks = newBlocks.filter(b => !oldWs.getBlockById(b.id)); - - // clone new workspace into rendering workspace - const ws = pxt.blocks.initRenderingWorkspace(); - const newXml = pxt.blocks.saveWorkspaceXml(newWs, true); - pxt.blocks.domToWorkspaceNoEvents(Blockly.Xml.textToDom(newXml), ws); - - // delete disabled blocks from final workspace - ws.getAllBlocks(false).filter(b => !b.isEnabled()).forEach(b => { - log('disabled ', b.toDevString()) - b.dispose(false) - }) - const todoBlocks = Util.toDictionary(ws.getAllBlocks(false), b => b.id); - log(`todo blocks`, todoBlocks) - logTodo('start') - - // 1. deleted top blocks - if (!options.hideDeletedTopBlocks) { - deletedTopBlocks.forEach(b => { - log(`deleted top ${b.toDevString()}`) - done(b); - const b2 = cloneIntoDiff(b); - done(b2); - b2.setEnabled(false); - }); - logTodo('deleted top') - } - - // 2. added blocks - addedBlocks.map(b => ws.getBlockById(b.id)) - .filter(b => !!b) // ignore disabled - .forEach(b => { - log(`added ${b.toDevString()}`) - //b.inputList[0].insertFieldAt(0, new Blockly.FieldImage(ADD_IMAGE_DATAURI, 24, 24, false)); - done(b); - }); - logTodo('added') - - // 3. delete statement blocks - // inject deleted blocks in new workspace - const dids: Map = {}; - if (!options.hideDeletedBlocks) { - const deletedStatementBlocks = deletedBlocks - .filter(b => !todoBlocks[b.id] - && !isUsed(b) - && (!b.outputConnection || !b.outputConnection.isConnected()) // ignore reporters - ); - deletedStatementBlocks - .forEach(b => { - const b2 = cloneIntoDiff(b); - dids[b.id] = b2.id; - log(`deleted block ${b.toDevString()}->${b2.toDevString()}`) - }) - // connect deleted blocks together - deletedStatementBlocks - .forEach(b => stitch(b)); - } - - // 4. moved blocks - let modified = 0; - Util.values(todoBlocks).filter(b => moved(b)).forEach(b => { - log(`moved ${b.toDevString()}`) - delete todoBlocks[b.id] - markUsed(b); - modified++; - }) - logTodo('moved') - - // 5. blocks with field properties that changed - Util.values(todoBlocks).filter(b => changed(b)).forEach(b => { - log(`changed ${b.toDevString()}`) - delete todoBlocks[b.id]; - markUsed(b); - modified++; - }) - logTodo('changed') - - // delete unmodified top blocks - ws.getTopBlocks(false) - .forEach(b => { - if (!findUsed(b)) { - log(`unmodified top ${b.toDevString()}`) - delete todoBlocks[b.id]; - b.dispose(false) - } - }); - logTodo('cleaned') - - // all unmodifed blocks are greyed out - Util.values(todoBlocks).filter(b => !!ws.getBlockById(b.id)).forEach(b => { - unmodified(b); - }); - logTodo('unmodified') - - // if nothing is left in the workspace, we "missed" change - if (!ws.getAllBlocks(false).length) { - pxt.tickEvent("blocks.diff", { missed: 1 }) - return { - ws, - message: lf("Some blocks were changed."), - deleted: deletedBlocks.length, - added: addedBlocks.length, - modified: modified - } - } - - // make sure everything is rendered - ws.resize(); - Blockly.svgResize(ws); - - // final render - const svg = pxt.blocks.renderWorkspace(options.renderOptions || { - emPixels: 20, - layout: BlockLayout.Flow, - aspectRatio: 0.5, - useViewWidth: true - }); - - // and we're done - const r: DiffResult = { - ws, - svg: svg, - deleted: deletedBlocks.length, - added: addedBlocks.length, - modified: modified - } - pxt.tickEvent("blocks.diff", { deleted: r.deleted, added: r.added, modified: r.modified }) - return r; - - function stitch(b: Blockly.Block) { - log(`stitching ${b.toDevString()}->${dids[b.id]}`) - const wb = ws.getBlockById(dids[b.id]); - wb.setEnabled(false); - markUsed(wb); - done(wb); - // connect previous connection to delted or existing block - const previous = b.getPreviousBlock(); - if (previous) { - const previousw = ws.getBlockById(dids[previous.id]) || ws.getBlockById(previous.id); - log(`previous ${b.id}->${wb.toDevString()}: ${previousw.toDevString()}`) - if (previousw) { - // either connected under or in the block - if (previousw.nextConnection) - wb.previousConnection.connect(previousw.nextConnection); - else { - const ic = previousw.inputList.slice() - .reverse() - .find(input => input.connection && input.connection.type == Blockly.NEXT_STATEMENT); - if (ic) - wb.previousConnection.connect(ic.connection); - } - } - } - // connect next connection to delete or existing block - const next = b.getNextBlock(); - if (next) { - const nextw = ws.getBlockById(dids[next.id]) || ws.getBlockById(next.id); - if (nextw) { - log(`next ${b.id}->${wb.toDevString()}: ${nextw.toDevString()}`) - wb.nextConnection.connect(nextw.previousConnection); - } - } - } - - function markUsed(b: Blockly.Block) { - (b).__pxt_used = true; - } - - function isUsed(b: Blockly.Block) { - return !!(b).__pxt_used; - } - - function cloneIntoDiff(b: Blockly.Block): Blockly.Block { - const bdom = Blockly.Xml.blockToDom(b, false) as Element; - const b2 = Blockly.Xml.domToBlock(bdom, ws); - // disconnect - if (b2.nextConnection && b2.nextConnection.targetConnection) - b2.nextConnection.disconnect(); - if (b2.previousConnection && b2.previousConnection.targetConnection) - b2.previousConnection.disconnect(); - return b2; - } - - function forceRender(b: Blockly.Block) { - const a = b; - a.rendered = false; - b.inputList.forEach(i => i.fieldRow.forEach(f => { - f.init(); - if (f.borderRect_) { - f.borderRect_.setAttribute('fill', b.getColour()) - f.borderRect_.setAttribute('stroke', (b as Blockly.BlockSvg).getColourTertiary()) - } - })); - } - - function done(b: Blockly.Block) { - b.getDescendants(false).forEach(t => { delete todoBlocks[t.id]; markUsed(t); }); - } - - function findUsed(b: Blockly.Block): boolean { - return !!b.getDescendants(false).find(c => isUsed(c)); - } - - function logTodo(msg: string) { - log(`${msg}:`, Util.values(todoBlocks).map(b => b.toDevString())) - } - - function moved(b: Blockly.Block) { - const oldb = oldWs.getBlockById(b.id); // extra block created in added step - if (!oldb) - return false; - - const newPrevious = b.getPreviousBlock(); - // connection already already processed - if (newPrevious && !todoBlocks[newPrevious.id]) - return false; - const newNext = b.getNextBlock(); - // already processed - if (newNext && !todoBlocks[newNext.id]) - return false; - - const oldPrevious = oldb.getPreviousBlock(); - if (!oldPrevious && !newPrevious) return false; // no connection - if (!!oldPrevious != !!newPrevious // new connection - || oldPrevious.id != newPrevious.id) // new connected blocks - return true; - const oldNext = oldb.getNextBlock(); - if (!oldNext && !newNext) return false; // no connection - if (!!oldNext != !!newNext // new connection - || oldNext.id != newNext.id) // new connected blocks - return true; - return false; - } - - function changed(b: Blockly.Block) { - let oldb = oldWs.getBlockById(b.id); // extra block created in added step - if (!oldb) - return false; - - // normalize - //oldb = copyToTrashWs(oldb); - const oldText = normalizedDom(oldb); - - //b = copyToTrashWs(b); - const newText = normalizedDom(b); - - if (oldText != newText) { - log(`old ${oldb.toDevString()}`, oldText) - log(`new ${b.toDevString()}`, newText) - return true; - } - - // not changed! - return false; - } - - function unmodified(b: Blockly.Block) { - b.setColour(UNMODIFIED_COLOR); - forceRender(b); - - if (options.statementsOnly) { - // mark all nested reporters as unmodified - (b.inputList || []) - .map(input => input.type == Blockly.INPUT_VALUE && input.connection && input.connection.targetBlock()) - .filter(argBlock => !!argBlock) - .forEach(argBlock => unmodified(argBlock)) - } - } - } - - export function mergeXml(xmlA: string, xmlO: string, xmlB: string): string { - if (xmlA == xmlO) return xmlB; - if (xmlB == xmlO) return xmlA; - - // TODO merge - return undefined; - } - - function normalizedDom(b: Blockly.Block, keepChildren?: boolean): string { - const dom = Blockly.Xml.blockToDom(b, true) as Element; - normalizeAttributes(dom); - visDom(dom, (e) => { - normalizeAttributes(e); - if (!keepChildren) { - if (e.localName == "next") - e.remove(); // disconnect or unplug not working propertly - else if (e.localName == "statement") - e.remove(); - else if (e.localName == "shadow") // ignore internal nodes - e.remove(); - } - }) - return Blockly.Xml.domToText(dom); - } - - function normalizeAttributes(e: Element) { - e.removeAttribute("id"); - e.removeAttribute("x"); - e.removeAttribute("y"); - e.removeAttribute("deletable"); - e.removeAttribute("editable"); - e.removeAttribute("movable") - } - - function visDom(el: Element, f: (e: Element) => void) { - if (!el) return; - f(el); - for (const child of Util.toArray(el.children)) - visDom(child, f); - } - - export function decompiledDiffAsync(oldTs: string, oldResp: pxtc.CompileResult, newTs: string, newResp: pxtc.CompileResult, options: DiffOptions = {}): DiffResult { - const log = logger(); - - const oldXml = oldResp.outfiles[pxt.MAIN_BLOCKS]; - let newXml = newResp.outfiles[pxt.MAIN_BLOCKS]; - log(oldXml); - log(newXml); - - // compute diff of typescript sources - const diffLines = pxt.diff.compute(oldTs, newTs, { - ignoreWhitespace: true, - full: true - }); - log(diffLines); - - // build old -> new lines mapping - const newids: pxt.Map = {}; - let oldLineStart = 0; - let newLineStart = 0; - diffLines.forEach((ln, index) => { - // moving cursors - const marker = ln[0]; - const line = ln.substr(2); - let lineLength = line.length; - switch (marker) { - case "-": // removed - oldLineStart += lineLength + 1; - break; - case "+": // added - newLineStart += lineLength + 1; - break; - default: // unchanged - // skip leading white space - const lw = /^\s+/.exec(line); - if (lw) { - const lwl = lw[0].length; - oldLineStart += lwl; - newLineStart += lwl; - lineLength -= lwl; - } - // find block ids mapped to the ranges - const newid = pxt.blocks.findBlockIdByPosition(newResp.blockSourceMap, { - start: newLineStart, - length: lineLength - }); - if (newid && !newids[newid]) { - const oldid = pxt.blocks.findBlockIdByPosition(oldResp.blockSourceMap, { - start: oldLineStart, - length: lineLength - }); - - // patch workspace - if (oldid) { - log(ln); - log(`id ${oldLineStart}:${line.length}>${oldid} ==> ${newLineStart}:${line.length}>${newid}`) - newids[newid] = oldid; - newXml = newXml.replace(newid, oldid); - } - - } - oldLineStart += lineLength + 1; - newLineStart += lineLength + 1; - break; - } - }) - - // parse workspacews - const oldWs = pxt.blocks.loadWorkspaceXml(oldXml, true); - const newWs = pxt.blocks.loadWorkspaceXml(newXml, true); - - options.statementsOnly = true; // no info on expression diffs - return diffWorkspace(oldWs, newWs, options); - } -} \ No newline at end of file diff --git a/pxtblocks/blocklyimporter.ts b/pxtblocks/blocklyimporter.ts deleted file mode 100644 index c4cc30e97c95..000000000000 --- a/pxtblocks/blocklyimporter.ts +++ /dev/null @@ -1,356 +0,0 @@ -/// -/// - -namespace pxt.blocks { - export interface BlockSnippet { - target: string; // pxt.appTarget.id - versions: pxt.TargetVersions; - xml: string[]; // xml for each top level block - extensions?: string[]; // currently unpopulated. list of extensions used in screenshotted projects - } - - export interface DomToWorkspaceOptions { - applyHideMetaComment?: boolean; - keepMetaComments?: boolean; - } - - /** - * Converts a DOM into workspace without triggering any Blockly event. Returns the new block ids - * @param dom - * @param workspace - */ - export function domToWorkspaceNoEvents(dom: Element, workspace: Blockly.Workspace, opts?: DomToWorkspaceOptions): string[] { - pxt.tickEvent(`blocks.domtow`) - let newBlockIds: string[] = []; - try { - Blockly.Events.disable(); - newBlockIds = Blockly.Xml.domToWorkspace(dom, workspace); - applyMetaComments(workspace, opts); - } catch (e) { - pxt.reportException(e); - } finally { - Blockly.Events.enable(); - } - return newBlockIds.filter(id => !!workspace.getBlockById(id)); - } - - function applyMetaComments(workspace: Blockly.Workspace, opts?: DomToWorkspaceOptions) { - // process meta comments - // @highlight -> highlight block - workspace.getAllBlocks(false) - .filter(b => !!b.getCommentText()) - .forEach(b => { - const initialCommentText = b.getCommentText(); - if (/@hide/.test(initialCommentText) && opts?.applyHideMetaComment) { - b.dispose(true); - return; - } - - let newCommentText = initialCommentText; - if (/@highlight/.test(newCommentText)) { - newCommentText = newCommentText.replace(/@highlight/g, '').trim(); - (workspace as Blockly.WorkspaceSvg).highlightBlock?.(b.id, true) - } - if (/@collapsed/.test(newCommentText) && !b.getParent()) { - newCommentText = newCommentText.replace(/@collapsed/g, '').trim(); - b.setCollapsed(true); - } - newCommentText = newCommentText.replace(/@validate-\S+/g, '').trim(); - - if (initialCommentText !== newCommentText && !opts?.keepMetaComments) { - b.setCommentText(newCommentText || null); - } - }); - } - - export function clearWithoutEvents(workspace: Blockly.Workspace) { - pxt.tickEvent(`blocks.clear`) - if (!workspace) return; - try { - Blockly.Events.disable(); - workspace.clear(); - workspace.clearUndo(); - } finally { - Blockly.Events.enable(); - } - } - - // Saves entire workspace, including variables, into an xml string - export function saveWorkspaceXml(ws: Blockly.Workspace, keepIds?: boolean): string { - const xml = Blockly.Xml.workspaceToDom(ws, !keepIds); - const text = Blockly.Xml.domToText(xml); - return text; - } - - // Saves only the blocks xml by iterating over the top blocks - export function saveBlocksXml(ws: Blockly.Workspace, keepIds?: boolean): string[] { - let topBlocks = ws.getTopBlocks(false); - return topBlocks.map(block => { - return Blockly.Xml.domToText(Blockly.Xml.blockToDom(block, !keepIds)); - }); - } - - export function getDirectChildren(parent: Element, tag: string) { - const res: Element[] = []; - for (let i = 0; i < parent.childNodes.length; i++) { - const n = parent.childNodes.item(i) as Element; - if (n.tagName === tag) { - res.push(n); - } - } - return res; - } - - export function getBlocksWithType(parent: Document | Element, type: string) { - return getChildrenWithAttr(parent, "block", "type", type).concat(getChildrenWithAttr(parent, "shadow", "type", type)); - } - - export function getChildrenWithAttr(parent: Document | Element, tag: string, attr: string, value: string) { - return Util.toArray(parent.getElementsByTagName(tag)).filter(b => b.getAttribute(attr) === value); - } - - export function getFirstChildWithAttr(parent: Document | Element, tag: string, attr: string, value: string) { - const res = getChildrenWithAttr(parent, tag, attr, value); - return res.length ? res[0] : undefined; - } - - export function loadBlocksXml(ws: Blockly.WorkspaceSvg, text: string) { - let xmlBlock = Blockly.Xml.textToDom(text); - let block = Blockly.Xml.domToBlock(xmlBlock, ws) as Blockly.BlockSvg; - if (ws.getMetrics) { - let metrics = ws.getMetrics(); - let blockDimensions = block.getHeightWidth(); - block.moveBy( - metrics.viewLeft + (metrics.viewWidth / 2) - (blockDimensions.width / 2), - metrics.viewTop + (metrics.viewHeight / 2) - (blockDimensions.height / 2) - ); - } - } - - /** - * Loads the xml into a off-screen workspace (not suitable for size computations) - */ - export function loadWorkspaceXml(xml: string, skipReport = false, opts?: DomToWorkspaceOptions): Blockly.Workspace { - const workspace = new Blockly.Workspace() as Blockly.WorkspaceSvg; - try { - const dom = Blockly.Xml.textToDom(xml); - pxt.blocks.domToWorkspaceNoEvents(dom, workspace, opts); - return workspace; - } catch (e) { - if (!skipReport) - pxt.reportException(e); - return null; - } - } - - function patchFloatingBlocks(dom: Element, info: pxtc.BlocksInfo) { - const onstarts = getBlocksWithType(dom, ts.pxtc.ON_START_TYPE); - let onstart = onstarts.length ? onstarts[0] : undefined; - if (onstart) { // nothing to do - onstart.removeAttribute("deletable"); - return; - } - - let newnodes: Element[] = []; - - const blocks: Map = info.blocksById; - - // walk top level blocks - let node = dom.firstElementChild; - let insertNode: Element = undefined; - while (node) { - const nextNode = node.nextElementSibling; - // does this block is disable or have s nested statement block? - const nodeType = node.getAttribute("type"); - if (!node.getAttribute("disabled") && !node.getElementsByTagName("statement").length - && (pxt.blocks.buildinBlockStatements[nodeType] || - (blocks[nodeType] && blocks[nodeType].retType == "void" && !hasArrowFunction(blocks[nodeType]))) - ) { - // old block, needs to be wrapped in onstart - if (!insertNode) { - insertNode = dom.ownerDocument.createElement("statement"); - insertNode.setAttribute("name", "HANDLER"); - if (!onstart) { - onstart = dom.ownerDocument.createElement("block"); - onstart.setAttribute("type", ts.pxtc.ON_START_TYPE); - newnodes.push(onstart); - } - onstart.appendChild(insertNode); - insertNode.appendChild(node); - - node.removeAttribute("x"); - node.removeAttribute("y"); - insertNode = node; - } else { - // event, add nested statement - const next = dom.ownerDocument.createElement("next"); - next.appendChild(node); - insertNode.appendChild(next); - node.removeAttribute("x"); - node.removeAttribute("y"); - insertNode = node; - } - } - node = nextNode; - } - - newnodes.forEach(n => dom.appendChild(n)); - } - - /** - * Patch to transform old function blocks to new ones, and rename child nodes - */ - function patchFunctionBlocks(dom: Element, info: pxtc.BlocksInfo) { - let functionNodes = pxt.U.toArray(dom.querySelectorAll("block[type=procedures_defnoreturn]")); - functionNodes.forEach(node => { - node.setAttribute("type", "function_definition"); - node.querySelector("field[name=NAME]").setAttribute("name", "function_name"); - }) - - let functionCallNodes = pxt.U.toArray(dom.querySelectorAll("block[type=procedures_callnoreturn]")); - functionCallNodes.forEach(node => { - node.setAttribute("type", "function_call"); - node.querySelector("field[name=NAME]").setAttribute("name", "function_name"); - }) - } - - /** - * This callback is populated from the editor extension result. - * Allows a target to provide version specific blockly updates - */ - export let extensionBlocklyPatch: (pkgTargetVersion: string, dom: Element) => void; - - export function importXml(pkgTargetVersion: string, xml: string, info: pxtc.BlocksInfo, skipReport = false): string { - try { - // If it's the first project we're importing in the session, Blockly is not initialized - // and blocks haven't been injected yet - pxt.blocks.initializeAndInject(info); - - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, "application/xml"); - - const upgrades = pxt.patching.computePatches(pkgTargetVersion); - if (upgrades) { - // patch block types - upgrades.filter(up => up.type == "blockId") - .forEach(up => Object.keys(up.map).forEach(type => { - getBlocksWithType(doc, type) - .forEach(blockNode => { - blockNode.setAttribute("type", up.map[type]); - pxt.debug(`patched block ${type} -> ${up.map[type]}`); - }); - })) - - // patch block value - upgrades.filter(up => up.type == "blockValue") - .forEach(up => Object.keys(up.map).forEach(k => { - const m = k.split('.'); - const type = m[0]; - const name = m[1]; - getBlocksWithType(doc, type) - .reduce((prev, current) => prev.concat(getDirectChildren(current, "value")), []) - .forEach(blockNode => { - blockNode.setAttribute("name", up.map[k]); - pxt.debug(`patched block value ${k} -> ${up.map[k]}`); - }); - })) - - // patch enum variables - upgrades.filter(up => up.type == "userenum") - .forEach(up => Object.keys(up.map).forEach(k => { - getChildrenWithAttr(doc, "variable", "type", k).forEach(el => { - el.setAttribute("type", up.map[k]); - pxt.debug(`patched enum variable type ${k} -> ${up.map[k]}`); - }) - })); - } - - // Blockly doesn't allow top-level shadow blocks. We've had bugs in the past where shadow blocks - // have ended up as top-level blocks, so promote them to regular blocks just in case - const shadows = getDirectChildren(doc.children.item(0), "shadow"); - for (const shadow of shadows) { - const block = doc.createElement("block"); - shadow.getAttributeNames().forEach(attr => block.setAttribute(attr, shadow.getAttribute(attr))); - for (let j = 0; j < shadow.childNodes.length; j++) { - block.appendChild(shadow.childNodes.item(j)); - } - shadow.replaceWith(block); - } - - // build upgrade map - const enums: Map = {}; - Object.keys(info.apis.byQName).forEach(k => { - let api = info.apis.byQName[k]; - if (api.kind == pxtc.SymbolKind.EnumMember) - enums[api.namespace + '.' + (api.attributes.blockImportId || api.attributes.block || api.attributes.blockId || api.name)] - = api.namespace + '.' + api.name; - }) - - // walk through blocks and patch enums - const blocks = doc.getElementsByTagName("block"); - for (let i = 0; i < blocks.length; ++i) - patchBlock(info, enums, blocks[i]); - - // patch floating blocks - patchFloatingBlocks(doc.documentElement, info); - - // patch function blocks - patchFunctionBlocks(doc.documentElement, info) - - // apply extension patches - if (pxt.blocks.extensionBlocklyPatch) - pxt.blocks.extensionBlocklyPatch(pkgTargetVersion, doc.documentElement); - - // serialize and return - return new XMLSerializer().serializeToString(doc); - } - catch (e) { - if (!skipReport) - reportException(e); - return xml; - } - } - - function patchBlock(info: pxtc.BlocksInfo, enums: Map, block: Element): void { - let type = block.getAttribute("type"); - let b = Blockly.Blocks[type]; - let symbol = blockSymbol(type); - if (!symbol || !b) return; - - let comp = compileInfo(symbol); - symbol.parameters?.forEach((p, i) => { - let ptype = info.apis.byQName[p.type]; - if (ptype && ptype.kind == pxtc.SymbolKind.Enum) { - let field = getFirstChildWithAttr(block, "field", "name", comp.actualNameToParam[p.name].definitionName); - if (field) { - let en = enums[ptype.name + '.' + field.textContent]; - if (en) field.textContent = en; - } - /* - - Button.AB - - */ - } - }) - } - - export function validateAllReferencedBlocksExist(xml: string) { - pxt.U.assert(!!Blockly?.Blocks, "Called validateAllReferencedBlocksExist before initializing Blockly"); - const dom = Blockly.Xml.textToDom(xml); - - const blocks = dom.querySelectorAll("block"); - - for (let i = 0; i < blocks.length; i++) { - if (!Blockly.Blocks[blocks.item(i).getAttribute("type")]) return false; - } - - const shadows = dom.querySelectorAll("shadow"); - - for (let i = 0; i < shadows.length; i++) { - if (!Blockly.Blocks[shadows.item(i).getAttribute("type")]) return false; - } - - return true; - } -} diff --git a/pxtblocks/blocklylayout.ts b/pxtblocks/blocklylayout.ts deleted file mode 100644 index 0261919abb55..000000000000 --- a/pxtblocks/blocklylayout.ts +++ /dev/null @@ -1,557 +0,0 @@ -namespace pxt.blocks.layout { - export interface FlowOptions { - ratio?: number; - useViewWidth?: boolean; - } - - export function patchBlocksFromOldWorkspace(blockInfo: ts.pxtc.BlocksInfo, oldWs: Blockly.Workspace, newXml: string): string { - const newWs = pxt.blocks.loadWorkspaceXml(newXml, true); - // position blocks - alignBlocks(blockInfo, oldWs, newWs); - // inject disabled blocks - return injectDisabledBlocks(oldWs, newWs); - } - - function injectDisabledBlocks(oldWs: Blockly.Workspace, newWs: Blockly.Workspace): string { - const oldDom = Blockly.Xml.workspaceToDom(oldWs, true); - const newDom = Blockly.Xml.workspaceToDom(newWs, true); - Util.toArray(oldDom.childNodes) - .filter((n: ChildNode) => n.nodeType == Node.ELEMENT_NODE && (n as Element).localName == "block" && (n).getAttribute("disabled") == "true") - .filter((n: Element) => !!Blockly.Blocks[n.getAttribute("type")]) - .forEach(n => newDom.appendChild(newDom.ownerDocument.importNode(n, true))); - const updatedXml = Blockly.Xml.domToText(newDom); - return updatedXml; - } - - function alignBlocks(blockInfo: ts.pxtc.BlocksInfo, oldWs: Blockly.Workspace, newWs: Blockly.Workspace) { - let env: pxt.blocks.Environment; - let newBlocks: pxt.Map; // support for multiple events with similar name - oldWs.getTopBlocks(false).filter(ob => ob.isEnabled()) - .forEach(ob => { - const otp = ob.xy_; - if (otp && otp.x != 0 && otp.y != 0) { - if (!env) { - env = pxt.blocks.mkEnv(oldWs, blockInfo); - newBlocks = {}; - newWs.getTopBlocks(false).forEach(b => { - const nkey = pxt.blocks.callKey(env, b); - const nbs = newBlocks[nkey] || []; - nbs.push(b); - newBlocks[nkey] = nbs; - }); - } - const oldKey = pxt.blocks.callKey(env, ob); - const newBlock = (newBlocks[oldKey] || []).shift(); - if (newBlock) - newBlock.xy_ = otp.clone(); - } - }) - } - - declare function unescape(escapeUri: string): string; - - /** - * Splits a blockly SVG AFTER a vertical layout. This function relies on the ordering - * of blocks / comments to get as getTopBlock(true)/getTopComment(true) - */ - export function splitSvg(svg: SVGSVGElement, ws: Blockly.WorkspaceSvg, emPixels: number = 18): Element { - const comments = ws.getTopComments(true) as Blockly.WorkspaceCommentSvg[]; - const blocks = ws.getTopBlocks(true) as Blockly.BlockSvg[]; - // don't split for a single block - if (comments.length + blocks.length < 2) - return svg; - - const div = document.createElement("div") as HTMLDivElement; - div.className = `blocks-svg-list ${ws.getInjectionDiv().className}` - - function extract( - parentClass: string, - otherClass: string, - blocki: number, - size: { height: number, width: number }, - translate: { x: number, y: number }, - itemClass?: string - ) { - const svgclone = svg.cloneNode(true) as SVGSVGElement; - // collect all blocks - const parentSvg = svgclone.querySelector(`g.blocklyWorkspace > g.${parentClass}`) as SVGGElement; - const otherSvg = svgclone.querySelector(`g.blocklyWorkspace > g.${otherClass}`) as SVGGElement; - const blocksSvg = Util.toArray(parentSvg.querySelectorAll(`g.blocklyWorkspace > g.${parentClass} > ${itemClass ? ("." + itemClass) : "g[transform]"}`)); - const blockSvg = blocksSvg.splice(blocki, 1)[0]; - if (!blockSvg) { - // seems like no blocks were generated - pxt.log(`missing block, did block failed to load?`) - return; - } - // remove all but the block we care about - blocksSvg.filter(g => g != blockSvg) - .forEach(g => { - g.parentNode.removeChild(g); - }); - // clear transform, remove other group - parentSvg.removeAttribute("transform"); - otherSvg.parentNode.removeChild(otherSvg); - // patch size - blockSvg.setAttribute("transform", `translate(${translate.x}, ${translate.y})`) - const width = (size.width / emPixels) + "em"; - const height = (size.height / emPixels) + "em"; - svgclone.setAttribute("viewBox", `0 0 ${size.width} ${size.height}`) - svgclone.style.width = width; - svgclone.style.height = height; - svgclone.setAttribute("width", width); - svgclone.setAttribute("height", height); - div.appendChild(svgclone); - } - - comments.forEach((comment, commenti) => extract('blocklyBubbleCanvas', 'blocklyBlockCanvas', - commenti, comment.getHeightWidth(), { x: 0, y: 0 }, "blocklyComment")); - blocks.forEach((block, blocki) => { - const size = block.getHeightWidth(); - const translate = { x: 0, y: 0 }; - if (block.getStartHat()) { - size.height += emPixels; - translate.y += emPixels; - } - extract('blocklyBlockCanvas', 'blocklyBubbleCanvas', - blocki, size, translate) - }); - return div; - } - - export function verticalAlign(ws: Blockly.WorkspaceSvg, emPixels: number) { - let y = 0 - let comments = ws.getTopComments(true) as Blockly.WorkspaceCommentSvg[]; - comments.forEach(comment => { - comment.moveBy(0, y) - y += comment.getHeightWidth().height - y += emPixels; //buffer - }) - let blocks = ws.getTopBlocks(true) as Blockly.BlockSvg[]; - blocks.forEach((block, bi) => { - // TODO: REMOVE THIS WHEN FIXED IN PXT-BLOCKLY - if (block.getStartHat()) - y += emPixels; // hat height - block.moveBy(0, y) - y += block.getHeightWidth().height - y += emPixels; //buffer - }) - } - - export function setCollapsedAll(ws: Blockly.WorkspaceSvg, collapsed: boolean) { - ws.getTopBlocks(false) - .filter(b => b.isEnabled()) - .forEach(b => b.setCollapsed(collapsed)); - } - - // Workspace margins - const marginx = 20; - const marginy = 20; - export function flow(ws: Blockly.WorkspaceSvg, opts?: FlowOptions) { - if (opts) { - if (opts.useViewWidth) { - const metrics = ws.getMetrics(); - - // Only use the width if in portrait, otherwise the blocks are too spread out - if (metrics.viewHeight > metrics.viewWidth) { - flowBlocks(ws.getTopComments(true) as Blockly.WorkspaceCommentSvg[], ws.getTopBlocks(true) as Blockly.BlockSvg[], undefined, metrics.viewWidth) - ws.scroll(marginx, marginy); - return; - } - } - flowBlocks(ws.getTopComments(true) as Blockly.WorkspaceCommentSvg[], ws.getTopBlocks(true) as Blockly.BlockSvg[], opts.ratio); - } - else { - flowBlocks(ws.getTopComments(true) as Blockly.WorkspaceCommentSvg[], ws.getTopBlocks(true) as Blockly.BlockSvg[]); - } - ws.scroll(marginx, marginy); - } - - export function screenshotEnabled(): boolean { - return !BrowserUtils.isIE() - } - - export function screenshotAsync(ws: Blockly.WorkspaceSvg, pixelDensity?: number, encodeBlocks?: boolean): Promise { - return toPngAsync(ws, pixelDensity, encodeBlocks); - } - - export function toPngAsync(ws: Blockly.WorkspaceSvg, pixelDensity?: number, encodeBlocks?: boolean): Promise { - let blockSnippet: BlockSnippet; - if (encodeBlocks) { - blockSnippet = { - target: pxt.appTarget.id, - versions: pxt.appTarget.versions, - xml: pxt.blocks.saveBlocksXml(ws).map(text => pxt.Util.htmlEscape(text)) - }; - } - - const density = (pixelDensity | 0) || 4 - return toSvgAsync(ws, density) - .then(sg => { - if (!sg) return Promise.resolve(undefined); - return pxt.BrowserUtils.encodeToPngAsync(sg.xml, - { - width: sg.width, - height: sg.height, - pixelDensity: density, - text: encodeBlocks ? JSON.stringify(blockSnippet, null, 2) : null - }); - }).catch(e => { - pxt.reportException(e); - return undefined; - }) - } - - const XLINK_NAMESPACE = "http://www.w3.org/1999/xlink"; - const MAX_AREA = 120000000; // https://github.com/jhildenbiddle/canvas-size - - export function toSvgAsync(ws: Blockly.WorkspaceSvg, pixelDensity: number): Promise<{ - width: number; height: number; xml: string; - }> { - if (!ws) - return Promise.resolve<{ width: number; height: number; xml: string; }>(undefined); - - const metrics = ws.getBlocksBoundingBox(); - const sg = ws.getParentSvg().cloneNode(true) as SVGElement; - cleanUpBlocklySvg(sg); - - let width = metrics.right - metrics.left; - let height = metrics.bottom - metrics.top; - let scale = 1; - - const area = width * height * Math.pow(pixelDensity, 2); - if (area > MAX_AREA) { - scale = Math.sqrt(MAX_AREA / area); - } - - return blocklyToSvgAsync(sg, metrics.left, metrics.top, width, height, scale); - } - - export function serializeNode(sg: Node): string { - return serializeSvgString(new XMLSerializer().serializeToString(sg)); - } - - export function serializeSvgString(xmlString: string): string { - return xmlString - .replace(new RegExp(' ', 'g'), ' '); // Replace   with   as a workaround for having nbsp missing from SVG xml - } - - export interface BlockSvg { - width: number; height: number; svg: string; xml: string; css: string; - } - - export function cleanUpBlocklySvg(svg: SVGElement): SVGElement { - pxt.BrowserUtils.removeClass(svg, "blocklySvg"); - pxt.BrowserUtils.addClass(svg, "blocklyPreview pxt-renderer classic-theme"); - - // Remove background elements - pxt.U.toArray(svg.querySelectorAll('.blocklyMainBackground,.blocklyScrollbarBackground')) - .forEach(el => { if (el) el.parentNode.removeChild(el) }); - - // Remove connection indicator elements - pxt.U.toArray(svg.querySelectorAll('.blocklyConnectionIndicator,.blocklyInputConnectionIndicator')) - .forEach(el => { if (el) el.parentNode.removeChild(el) }); - - svg.removeAttribute('width'); - svg.removeAttribute('height'); - - pxt.U.toArray(svg.querySelectorAll('.blocklyBlockCanvas,.blocklyBubbleCanvas')) - .forEach(el => el.removeAttribute('transform')); - - // In order to get the Blockly comment's text area to serialize properly they have to have names - const parser = new DOMParser(); - pxt.U.toArray(svg.querySelectorAll('.blocklyCommentTextarea')) - .forEach(el => { - const dom = parser.parseFromString( - '' + pxt.docs.html2Quote((el as any).value), - 'text/html'); - (el as any).textContent = dom.body.textContent; - }); - - return svg; - } - - export function blocklyToSvgAsync(sg: SVGElement, x: number, y: number, width: number, height: number, scale?: number): Promise { - if (!sg.childNodes[0]) - return Promise.resolve(undefined); - - sg.removeAttribute("width"); - sg.removeAttribute("height"); - sg.removeAttribute("transform"); - - let renderWidth = Math.round(width * (scale || 1)); - let renderHeight = Math.round(height * (scale || 1)); - - const xmlString = serializeNode(sg) - .replace(/^\s*]+>/i, '') - .replace(/<\/svg>\s*$/i, '') // strip out svg tag - const svgXml = `${xmlString}`; - const xsg = new DOMParser().parseFromString(svgXml, "image/svg+xml"); - - const cssLink = xsg.createElementNS("http://www.w3.org/1999/xhtml", "style"); - const isRtl = Util.isUserLanguageRtl(); - const customCssHref = (document.getElementById(`style-${isRtl ? 'rtl' : ''}blockly.css`) as HTMLLinkElement).href; - const semanticCssHref = Util.toArray(document.head.getElementsByTagName("link")) - .filter(l => Util.endsWith(l.getAttribute("href"), "semantic.css"))[0].href; - return Promise.all([pxt.BrowserUtils.loadAjaxAsync(customCssHref), pxt.BrowserUtils.loadAjaxAsync(semanticCssHref)]) - .then((customCss) => { - const blocklySvg = Util.toArray(document.head.querySelectorAll("style")) - .filter((el: HTMLStyleElement) => /\.blocklySvg/.test(el.innerText))[0] as HTMLStyleElement; - // Custom CSS injected directly into the DOM by Blockly - customCss.unshift((document.getElementById(`blockly-common-style`) as HTMLLinkElement)?.innerText || ""); - customCss.unshift((document.getElementById(`blockly-renderer-style-pxt-classic`) as HTMLLinkElement)?.innerText || ""); - // CSS may contain <, > which need to be stored in CDATA section - const cssString = (blocklySvg ? blocklySvg.innerText : "") + '\n\n' + customCss.map(el => el + '\n\n'); - cssLink.appendChild(xsg.createCDATASection(cssString)); - xsg.documentElement.insertBefore(cssLink, xsg.documentElement.firstElementChild); - - return expandImagesAsync(xsg) - .then(() => convertIconsToPngAsync(xsg)) - .then(() => { - return { - width: renderWidth, - height: renderHeight, - svg: serializeNode(xsg).replace('