From 4ab8d000995a93eafbc29444abb76e535aedf134 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Mon, 30 Oct 2023 09:16:13 +0100 Subject: [PATCH] refactor(generators): Migrate JavaScript generators to TypeScript (#7602) * refactor(generators): Migrate javascript_generator.js to TypeScript * refactor(generators): Simplify getAdjusted Slightly simplify the implementation of getAdjusted, in part to make it more readable. Also improve its JSDoc comment. * refactor(generators): Migrate generators/javascript/* to TypeScript First pass doing very mechanistic migration, not attempting to fix all the resulting type errors. * fix(generators): Fix type errors in generator functions This consists almost entirely of adding casts, so the code output by tsc should be as similar as possible to the pre-migration .js source files. * refactor(generators): Migrate generators/javascript.js to TypeScript The way the generator functions are added to javascriptGenerator.forBlock has been modified so that incorrect generator function signatures will cause tsc to generate a type error. * chore(generator): Format One block protected with // prettier-ignore to preserve careful comment formatting. Where there are repeated concatenations prettier has made a pretty mess of things, but the correct fix is probably to use template literals instead (rather than just locally disabling prettier). This has been added to the to-do list in #7600. * fix(generators): Fixes for PR #7602 * fix(generators): Fix syntax error --- .prettierignore | 5 +- blocks/lists.ts | 8 +- blocks/loops.ts | 8 +- blocks/procedures.ts | 8 +- blocks/text.ts | 8 +- generators/{javascript.js => javascript.ts} | 19 +- .../javascript/{colour.js => colour.ts} | 67 ++-- ...t_generator.js => javascript_generator.ts} | 269 ++++++++------- generators/javascript/{lists.js => lists.ts} | 302 ++++++++++------- generators/javascript/logic.js | 130 -------- generators/javascript/logic.ts | 153 +++++++++ generators/javascript/{loops.js => loops.ts} | 191 +++++++---- generators/javascript/{math.js => math.ts} | 270 +++++++++------ .../{procedures.js => procedures.ts} | 90 +++-- generators/javascript/{text.js => text.ts} | 314 ++++++++++-------- .../javascript/{variables.js => variables.ts} | 18 +- ...iables_dynamic.js => variables_dynamic.ts} | 1 - 17 files changed, 1095 insertions(+), 766 deletions(-) rename generators/{javascript.js => javascript.ts} (79%) rename generators/javascript/{colour.js => colour.ts} (69%) rename generators/javascript/{javascript_generator.js => javascript_generator.ts} (54%) rename generators/javascript/{lists.js => lists.ts} (60%) delete mode 100644 generators/javascript/logic.js create mode 100644 generators/javascript/logic.ts rename generators/javascript/{loops.js => loops.ts} (53%) rename generators/javascript/{math.js => math.ts} (62%) rename generators/javascript/{procedures.js => procedures.ts} (58%) rename generators/javascript/{text.js => text.ts} (52%) rename generators/javascript/{variables.js => variables.ts} (57%) rename generators/javascript/{variables_dynamic.js => variables_dynamic.ts} (99%) diff --git a/.prettierignore b/.prettierignore index 64dd749f1..9d52f19fe 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,7 +18,6 @@ # Demos, scripts, misc /node_modules/* -/generators/* /demos/* /appengine/* /externs/* @@ -27,5 +26,5 @@ CHANGELOG.md PULL_REQUEST_TEMPLATE.md -# Don't bother formatting js blocks since we're getting rid of them -/blocks/*.js +# Don't bother formatting JavaScript files we're about to migrate: +/generators/**/*.js diff --git a/blocks/lists.ts b/blocks/lists.ts index a85e39352..d115a321c 100644 --- a/blocks/lists.ts +++ b/blocks/lists.ts @@ -111,8 +111,12 @@ export const blocks = createBlockDefinitionsFromJsonArray([ }, ]); -/** Type of a 'lists_create_with' block. */ -type CreateWithBlock = Block & ListCreateWithMixin; +/** + * Type of a 'lists_create_with' block. + * + * @internal + */ +export type CreateWithBlock = Block & ListCreateWithMixin; interface ListCreateWithMixin extends ListCreateWithMixinType { itemCount_: number; } diff --git a/blocks/loops.ts b/blocks/loops.ts index e70fdae31..02d9d34be 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -325,8 +325,12 @@ export const loopTypes: Set = new Set([ 'controls_whileUntil', ]); -/** Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN */ -type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin; +/** + * Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN + * + * @internal + */ +export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin; interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {} type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN; diff --git a/blocks/procedures.ts b/blocks/procedures.ts index 46109fa27..7150bda8a 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -1209,8 +1209,12 @@ blocks['procedures_callreturn'] = { defType_: 'procedures_defreturn', }; -/** Type of a procedures_ifreturn block. */ -type IfReturnBlock = Block & IfReturnMixin; +/** + * Type of a procedures_ifreturn block. + * + * @internal + */ +export type IfReturnBlock = Block & IfReturnMixin; interface IfReturnMixin extends IfReturnMixinType { hasReturnValue_: boolean; } diff --git a/blocks/text.ts b/blocks/text.ts index 10a757d21..824da782b 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -725,8 +725,12 @@ const QUOTES_EXTENSION = function (this: QuoteImageBlock) { this.quoteField_('TEXT'); }; -/** Type of a block that has TEXT_JOIN_MUTATOR_MIXIN */ -type JoinMutatorBlock = BlockSvg & JoinMutatorMixin & QuoteImageMixin; +/** + * Type of a block that has TEXT_JOIN_MUTATOR_MIXIN + * + * @internal + */ +export type JoinMutatorBlock = BlockSvg & JoinMutatorMixin & QuoteImageMixin; interface JoinMutatorMixin extends JoinMutatorMixinType {} type JoinMutatorMixinType = typeof JOIN_MUTATOR_MIXIN; diff --git a/generators/javascript.js b/generators/javascript.ts similarity index 79% rename from generators/javascript.js rename to generators/javascript.ts index 7ff772c5b..079c2f793 100644 --- a/generators/javascript.js +++ b/generators/javascript.ts @@ -32,8 +32,17 @@ export * from './javascript/javascript_generator.js'; export const javascriptGenerator = new JavascriptGenerator(); // Install per-block-type generator functions: -Object.assign( - javascriptGenerator.forBlock, - colour, lists, logic, loops, math, procedures, - text, variables, variablesDynamic -); +const generators: typeof javascriptGenerator.forBlock = { + ...colour, + ...lists, + ...logic, + ...loops, + ...math, + ...procedures, + ...text, + ...variables, + ...variablesDynamic, +}; +for (const name in generators) { + javascriptGenerator.forBlock[name] = generators[name]; +} diff --git a/generators/javascript/colour.js b/generators/javascript/colour.ts similarity index 69% rename from generators/javascript/colour.js rename to generators/javascript/colour.ts index 41d588c75..3e7089cd7 100644 --- a/generators/javascript/colour.js +++ b/generators/javascript/colour.ts @@ -10,35 +10,48 @@ // Former goog.module ID: Blockly.JavaScript.colour +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function colour_picker(block, generator) { +export function colour_picker( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; -}; +} -export function colour_random(block, generator) { +export function colour_random( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Generate a random colour. - const functionName = generator.provideFunction_('colourRandom', ` + const functionName = generator.provideFunction_( + 'colourRandom', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { var num = Math.floor(Math.random() * Math.pow(2, 24)); return '#' + ('00000' + num.toString(16)).substr(-6); } -`); +`, + ); const code = functionName + '()'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_rgb(block, generator) { +export function colour_rgb( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Compose a colour from RGB components expressed as percentages. const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const green = - generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const blue = - generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const functionName = generator.provideFunction_('colourRgb', ` + const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; + const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; + const functionName = generator.provideFunction_( + 'colourRgb', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { r = Math.max(Math.min(Number(r), 100), 0) * 2.55; g = Math.max(Math.min(Number(g), 100), 0) * 2.55; @@ -48,20 +61,23 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { b = ('0' + (Math.round(b) || 0).toString(16)).slice(-2); return '#' + r + g + b; } -`); +`, + ); const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_blend(block, generator) { +export function colour_blend( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Blend two colours together. - const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || - "'#000000'"; - const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || - "'#000000'"; - const ratio = - generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - const functionName = generator.provideFunction_('colourBlend', ` + const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; + const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; + const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; + const functionName = generator.provideFunction_( + 'colourBlend', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(c1, c2, ratio) { ratio = Math.max(Math.min(Number(ratio), 1), 0); var r1 = parseInt(c1.substring(1, 3), 16); @@ -78,7 +94,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(c1, c2, ratio) { b = ('0' + (b || 0).toString(16)).slice(-2); return '#' + r + g + b; } -`); +`, + ); const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/javascript/javascript_generator.js b/generators/javascript/javascript_generator.ts similarity index 54% rename from generators/javascript/javascript_generator.js rename to generators/javascript/javascript_generator.ts index f85fce18f..547b7daf7 100644 --- a/generators/javascript/javascript_generator.js +++ b/generators/javascript/javascript_generator.ts @@ -5,63 +5,62 @@ */ /** - * @fileoverview Helper functions for generating JavaScript for blocks. - * @suppress {checkTypes|globalThis} + * @file JavaScript code generator class, including helper methods for + * generating JavaScript for blocks. */ // Former goog.module ID: Blockly.JavaScript import * as Variables from '../../core/variables.js'; import * as stringUtils from '../../core/utils/string.js'; -// import type {Block} from '../../core/block.js'; +import type {Block} from '../../core/block.js'; import {CodeGenerator} from '../../core/generator.js'; import {Names, NameType} from '../../core/names.js'; -// import type {Workspace} from '../../core/workspace.js'; +import type {Workspace} from '../../core/workspace.js'; import {inputTypes} from '../../core/inputs/input_types.js'; - /** * Order of operation ENUMs. * https://developer.mozilla.org/en/JavaScript/Reference/Operators/Operator_Precedence - * @enum {number} */ -export const Order = { - ATOMIC: 0, // 0 "" ... - NEW: 1.1, // new - MEMBER: 1.2, // . [] - FUNCTION_CALL: 2, // () - INCREMENT: 3, // ++ - DECREMENT: 3, // -- - BITWISE_NOT: 4.1, // ~ - UNARY_PLUS: 4.2, // + - UNARY_NEGATION: 4.3, // - - LOGICAL_NOT: 4.4, // ! - TYPEOF: 4.5, // typeof - VOID: 4.6, // void - DELETE: 4.7, // delete - AWAIT: 4.8, // await - EXPONENTIATION: 5.0, // ** - MULTIPLICATION: 5.1, // * - DIVISION: 5.2, // / - MODULUS: 5.3, // % - SUBTRACTION: 6.1, // - - ADDITION: 6.2, // + - BITWISE_SHIFT: 7, // << >> >>> - RELATIONAL: 8, // < <= > >= - IN: 8, // in - INSTANCEOF: 8, // instanceof - EQUALITY: 9, // == != === !== - BITWISE_AND: 10, // & - BITWISE_XOR: 11, // ^ - BITWISE_OR: 12, // | - LOGICAL_AND: 13, // && - LOGICAL_OR: 14, // || - CONDITIONAL: 15, // ?: - ASSIGNMENT: 16, //: += -= **= *= /= %= <<= >>= ... - YIELD: 17, // yield - COMMA: 18, // , - NONE: 99, // (...) -}; +// prettier-ignore +export enum Order { + ATOMIC = 0, // 0 "" ... + NEW = 1.1, // new + MEMBER = 1.2, // . [] + FUNCTION_CALL = 2, // () + INCREMENT = 3, // ++ + DECREMENT = 3, // -- + BITWISE_NOT = 4.1, // ~ + UNARY_PLUS = 4.2, // + + UNARY_NEGATION = 4.3, // - + LOGICAL_NOT = 4.4, // ! + TYPEOF = 4.5, // typeof + VOID = 4.6, // void + DELETE = 4.7, // delete + AWAIT = 4.8, // await + EXPONENTIATION = 5.0, // ** + MULTIPLICATION = 5.1, // * + DIVISION = 5.2, // / + MODULUS = 5.3, // % + SUBTRACTION = 6.1, // - + ADDITION = 6.2, // + + BITWISE_SHIFT = 7, // << >> >>> + RELATIONAL = 8, // < <= > >= + IN = 8, // in + INSTANCEOF = 8, // instanceof + EQUALITY = 9, // == != === !== + BITWISE_AND = 10, // & + BITWISE_XOR = 11, // ^ + BITWISE_OR = 12, // | + LOGICAL_AND = 13, // && + LOGICAL_OR = 14, // || + CONDITIONAL = 15, // ?: + ASSIGNMENT = 16, // = += -= **= *= /= %= <<= >>= ... + YIELD = 17, // yield + COMMA = 18, // , + NONE = 99, // (...) +} /** * JavaScript code generator class. @@ -69,9 +68,8 @@ export const Order = { export class JavascriptGenerator extends CodeGenerator { /** * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} */ - ORDER_OVERRIDES = [ + ORDER_OVERRIDES: number[][] = [ // (foo()).bar -> foo().bar // (foo())[0] -> foo()[0] [Order.FUNCTION_CALL, Order.MEMBER], @@ -95,11 +93,12 @@ export class JavascriptGenerator extends CodeGenerator { // a && (b && c) -> a && b && c [Order.LOGICAL_AND, Order.LOGICAL_AND], // a || (b || c) -> a || b || c - [Order.LOGICAL_OR, Order.LOGICAL_OR] + [Order.LOGICAL_OR, Order.LOGICAL_OR], ]; - constructor(name) { - super(name ?? 'JavaScript'); + /** @param name Name of the language the generator is for. */ + constructor(name = 'JavaScript') { + super(name); this.isInitialized = false; // Copy Order values onto instance for backwards compatibility @@ -110,16 +109,26 @@ export class JavascriptGenerator extends CodeGenerator { // replace data properties with get accessors that call // deprecate.warn().) for (const key in Order) { - this['ORDER_' + key] = Order[key]; + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; } // List of illegal variable names. This is not intended to be a // security feature. Blockly is 100% client-side, so bypassing // this list is trivial. This is intended to prevent users from // accidentally clobbering a built-in object or function. + // + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords this.addReservedWords( - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords - 'break,case,catch,class,const,continue,debugger,default,delete,do,' + + 'break,case,catch,class,const,continue,debugger,default,delete,do,' + 'else,export,extends,finally,for,function,if,import,in,instanceof,' + 'new,return,super,switch,this,throw,try,typeof,var,void,' + 'while,with,yield,' + @@ -131,15 +140,16 @@ export class JavascriptGenerator extends CodeGenerator { 'arguments,' + // Everything in the current environment (835 items in Chrome, // 104 in Node). - Object.getOwnPropertyNames(globalThis).join(',') + Object.getOwnPropertyNames(globalThis).join(','), ); } /** * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. + * + * @param workspace Workspace to generate code from. */ - init(workspace) { + init(workspace: Workspace) { super.init(workspace); if (!this.nameDB_) { @@ -157,14 +167,16 @@ export class JavascriptGenerator extends CodeGenerator { const devVarList = Variables.allDeveloperVariables(workspace); for (let i = 0; i < devVarList.length; i++) { defvars.push( - this.nameDB_.getName(devVarList[i], NameType.DEVELOPER_VARIABLE)); + this.nameDB_.getName(devVarList[i], NameType.DEVELOPER_VARIABLE), + ); } // Add user variables, but only ones that are being used. const variables = Variables.allUsedVarModels(workspace); for (let i = 0; i < variables.length; i++) { defvars.push( - this.nameDB_.getName(variables[i].getId(), NameType.VARIABLE)); + this.nameDB_.getName(variables[i].getId(), NameType.VARIABLE), + ); } // Declare all of the variables. @@ -176,70 +188,74 @@ export class JavascriptGenerator extends CodeGenerator { /** * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. + * + * @param code Generated code. + * @returns Completed code. */ - finish(code) { + finish(code: string): string { // Convert the definitions dictionary into a list. const definitions = Object.values(this.definitions_); // Call Blockly.CodeGenerator's finish. super.finish(code); this.isInitialized = false; - this.nameDB_.reset(); + this.nameDB_!.reset(); return definitions.join('\n\n') + '\n\n\n' + code; } /** * Naked values are top-level blocks with outputs that aren't plugged into * anything. A trailing semicolon is needed to make this legal. - * @param {string} line Line of generated code. - * @return {string} Legal line of code. + * + * @param line Line of generated code. + * @returns Legal line of code. */ - scrubNakedValue(line) { + scrubNakedValue(line: string): string { return line + ';\n'; } /** * Encode a string as a properly escaped JavaScript string, complete with * quotes. - * @param {string} string Text to encode. - * @return {string} JavaScript string. + * + * @param string Text to encode. + * @returns JavaScript string. */ - quote_(string) { + quote_(string: string): string { // Can't use goog.string.quote since Google's style guide recommends // JS string literals use single quotes. - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; + string = string + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\\n') + .replace(/'/g, "\\'"); + return "'" + string + "'"; } /** * Encode a string as a properly escaped multiline JavaScript string, complete * with quotes. - * @param {string} string Text to encode. - * @return {string} JavaScript string. + * @param string Text to encode. + * @returns JavaScript string. */ - multiline_quote_(string) { + multiline_quote_(string: string): string { // Can't use goog.string.quote since Google's style guide recommends // JS string literals use single quotes. const lines = string.split(/\n/g).map(this.quote_); - return lines.join(' + \'\\n\' +\n'); + return lines.join(" + '\\n' +\n"); } /** * Common tasks for generating JavaScript from blocks. * Handles comments for the specified block and any connected value blocks. * Calls any statements following this block. - * @param {!Block} block The current block. - * @param {string} code The JavaScript code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this - * statement. - * @return {string} JavaScript code with comments and subsequent blocks added. + * + * @param block The current block. + * @param code The JavaScript code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns JavaScript code with comments and subsequent blocks added. * @protected */ - scrub_(block, code, opt_thisOnly) { + scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; // Only collect comments for blocks that aren't inline. if (!block.outputConnection || !block.outputConnection.targetConnection) { @@ -253,7 +269,7 @@ export class JavascriptGenerator extends CodeGenerator { // Don't collect comments for nested statements. for (let i = 0; i < block.inputList.length; i++) { if (block.inputList[i].type === inputTypes.VALUE) { - const childBlock = block.inputList[i].connection.targetBlock(); + const childBlock = block.inputList[i].connection!.targetBlock(); if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { @@ -264,68 +280,69 @@ export class JavascriptGenerator extends CodeGenerator { } } const nextBlock = - block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = thisOnly ? '' : this.blockToCode(nextBlock); return commentCode + code + nextCode; } /** - * Gets a property and adjusts the value while taking into account indexing. - * @param {!Block} block The block. - * @param {string} atId The property ID of the element to get. - * @param {number=} opt_delta Value to add. - * @param {boolean=} opt_negate Whether to negate the value. - * @param {number=} opt_order The highest order acting on this value. - * @return {string|number} + * Generate code representing the specified value input, adjusted to take into + * account indexing (zero- or one-based) and optionally by a specified delta + * and/or by negation. + * + * @param block The block. + * @param atId The ID of the input block to get (and adjust) the value of. + * @param delta Value to add. + * @param negate Whether to negate the value. + * @param order The highest order acting on this value. + * @returns The adjusted value. */ - getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { - let delta = opt_delta || 0; - let order = opt_order || this.ORDER_NONE; + getAdjusted( + block: Block, + atId: string, + delta = 0, + negate = false, + order = Order.NONE, + ): string | number { if (block.workspace.options.oneBasedIndex) { delta--; } const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - let innerOrder; - let outerOrder = order; + let orderForInput = order; if (delta > 0) { - outerOrder = this.ORDER_ADDITION; - innerOrder = this.ORDER_ADDITION; + orderForInput = Order.ADDITION; } else if (delta < 0) { - outerOrder = this.ORDER_SUBTRACTION; - innerOrder = this.ORDER_SUBTRACTION; - } else if (opt_negate) { - outerOrder = this.ORDER_UNARY_NEGATION; - innerOrder = this.ORDER_UNARY_NEGATION; + orderForInput = Order.SUBTRACTION; + } else if (negate) { + orderForInput = Order.UNARY_NEGATION; } - let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; + let at = this.valueToCode(block, atId, orderForInput) || defaultAtIndex; + // Easy case: no adjustments. + if (delta === 0 && !negate) { + return at; + } + // If the index is a naked number, adjust it right now. if (stringUtils.isNumber(at)) { - // If the index is a naked number, adjust it right now. - at = Number(at) + delta; - if (opt_negate) { - at = -at; - } - } else { - // If the index is dynamic, adjust it in code. - if (delta > 0) { - at = at + ' + ' + delta; - } else if (delta < 0) { - at = at + ' - ' + -delta; - } - if (opt_negate) { - if (delta) { - at = '-(' + at + ')'; - } else { - at = '-' + at; - } - } - innerOrder = Math.floor(innerOrder); - order = Math.floor(order); - if (innerOrder && order >= innerOrder) { - at = '(' + at + ')'; + at = String(Number(at) + delta); + if (negate) { + at = String(-Number(at)); } + return at; + } + // If the index is dynamic, adjust it in code. + if (delta > 0) { + at = `${at} + ${delta}`; + } else if (delta < 0) { + at = `${at} - ${-delta}`; + } + if (negate) { + at = delta ? `-(${at})` : `-${at}`; + } + if (Math.floor(order) >= Math.floor(orderForInput)) { + at = `(${at})`; } return at; } diff --git a/generators/javascript/lists.js b/generators/javascript/lists.ts similarity index 60% rename from generators/javascript/lists.js rename to generators/javascript/lists.ts index 9e4265716..6283f1d27 100644 --- a/generators/javascript/lists.js +++ b/generators/javascript/lists.ts @@ -11,30 +11,42 @@ // Former goog.module ID: Blockly.JavaScript.lists +import type {Block} from '../../core/block.js'; +import type {CreateWithBlock} from '../../blocks/lists.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './javascript_generator.js'; - -export function lists_create_empty(block, generator) { +export function lists_create_empty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create an empty list. return ['[]', Order.ATOMIC]; -}; +} -export function lists_create_with(block, generator) { +export function lists_create_with( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const createWithBlock = block as CreateWithBlock; // Create a list with any number of elements of any type. - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || - 'null'; + const elements = new Array(createWithBlock.itemCount_); + for (let i = 0; i < createWithBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; } const code = '[' + elements.join(', ') + ']'; return [code, Order.ATOMIC]; -}; +} -export function lists_repeat(block, generator) { +export function lists_repeat( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create a list with one element repeated. - const functionName = generator.provideFunction_('listsRepeat', ` + const functionName = generator.provideFunction_( + 'listsRepeat', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { var array = []; for (var i = 0; i < n; i++) { @@ -42,56 +54,61 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { } return array; } -`); - const element = - generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; - const repeatCount = - generator.valueToCode(block, 'NUM', Order.NONE) || '0'; +`, + ); + const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; + const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; const code = functionName + '(' + element + ', ' + repeatCount + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_length(block, generator) { +export function lists_length( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // String or array length. - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return [list + '.length', Order.MEMBER]; -}; +} -export function lists_isEmpty(block, generator) { +export function lists_isEmpty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Is the string null or array empty? - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return ['!' + list + '.length', Order.LOGICAL_NOT]; -}; +} -export function lists_indexOf(block, generator) { +export function lists_indexOf( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Find an item in the list. const operator = - block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; - const item = - generator.valueToCode(block, 'FIND', Order.NONE) || "''"; - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const item = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; const code = list + '.' + operator + '(' + item + ')'; if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITION]; } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_getIndex(block, generator) { +export function lists_getIndex( + block: Block, + generator: JavascriptGenerator, +): [string, Order] | string { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const listOrder = - (where === 'RANDOM') ? Order.NONE : Order.MEMBER; - const list = - generator.valueToCode(block, 'VALUE', listOrder) || '[]'; + const listOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; + const list = generator.valueToCode(block, 'VALUE', listOrder) || '[]'; switch (where) { - case ('FIRST'): + case 'FIRST': if (mode === 'GET') { const code = list + '[0]'; return [code, Order.MEMBER]; @@ -102,7 +119,7 @@ export function lists_getIndex(block, generator) { return list + '.shift();\n'; } break; - case ('LAST'): + case 'LAST': if (mode === 'GET') { const code = list + '.slice(-1)[0]'; return [code, Order.MEMBER]; @@ -113,7 +130,7 @@ export function lists_getIndex(block, generator) { return list + '.pop();\n'; } break; - case ('FROM_START'): { + case 'FROM_START': { const at = generator.getAdjusted(block, 'AT'); if (mode === 'GET') { const code = list + '[' + at + ']'; @@ -126,7 +143,7 @@ export function lists_getIndex(block, generator) { } break; } - case ('FROM_END'): { + case 'FROM_END': { const at = generator.getAdjusted(block, 'AT', 1, true); if (mode === 'GET') { const code = list + '.slice(' + at + ')[0]'; @@ -139,9 +156,10 @@ export function lists_getIndex(block, generator) { } break; } - case ('RANDOM'): { - const functionName = - generator.provideFunction_('listsGetRandomItem', ` + case 'RANDOM': { + const functionName = generator.provideFunction_( + 'listsGetRandomItem', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { var x = Math.floor(Math.random() * list.length); if (remove) { @@ -150,7 +168,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { return list[x]; } } -`); +`, + ); const code = functionName + '(' + list + ', ' + (mode !== 'GET') + ')'; if (mode === 'GET' || mode === 'GET_REMOVE') { return [code, Order.FUNCTION_CALL]; @@ -161,40 +180,38 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { } } throw Error('Unhandled combination (lists_getIndex).'); -}; +} -export function lists_setIndex(block, generator) { +export function lists_setIndex(block: Block, generator: JavascriptGenerator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. - let list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; + let list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const value = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || - 'null'; + const value = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; // Cache non-trivial values to variables to prevent repeated look-ups. // Closure, which accesses and modifies 'list'. function cacheList() { if (list.match(/^\w+$/)) { return ''; } - const listVar = - generator.nameDB_.getDistinctName( - 'tmpList', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmpList', + NameType.VARIABLE, + )!; const code = 'var ' + listVar + ' = ' + list + ';\n'; list = listVar; return code; } switch (where) { - case ('FIRST'): + case 'FIRST': if (mode === 'SET') { return list + '[0] = ' + value + ';\n'; } else if (mode === 'INSERT') { return list + '.unshift(' + value + ');\n'; } break; - case ('LAST'): + case 'LAST': if (mode === 'SET') { let code = cacheList(); code += list + '[' + list + '.length - 1] = ' + value + ';\n'; @@ -203,7 +220,7 @@ export function lists_setIndex(block, generator) { return list + '.push(' + value + ');\n'; } break; - case ('FROM_START'): { + case 'FROM_START': { const at = generator.getAdjusted(block, 'AT'); if (mode === 'SET') { return list + '[' + at + '] = ' + value + ';\n'; @@ -212,27 +229,40 @@ export function lists_setIndex(block, generator) { } break; } - case ('FROM_END'): { + case 'FROM_END': { const at = generator.getAdjusted( - block, 'AT', 1, false, Order.SUBTRACTION); + block, + 'AT', + 1, + false, + Order.SUBTRACTION, + ); let code = cacheList(); if (mode === 'SET') { code += list + '[' + list + '.length - ' + at + '] = ' + value + ';\n'; return code; } else if (mode === 'INSERT') { - code += list + '.splice(' + list + '.length - ' + at + ', 0, ' + value + - ');\n'; + code += + list + + '.splice(' + + list + + '.length - ' + + at + + ', 0, ' + + value + + ');\n'; return code; } break; } - case ('RANDOM'): { + case 'RANDOM': { let code = cacheList(); - const xVar = - generator.nameDB_.getDistinctName( - 'tmpX', NameType.VARIABLE); - code += 'var ' + xVar + ' = Math.floor(Math.random() * ' + list + - '.length);\n'; + const xVar = generator.nameDB_!.getDistinctName( + 'tmpX', + NameType.VARIABLE, + ); + code += + 'var ' + xVar + ' = Math.floor(Math.random() * ' + list + '.length);\n'; if (mode === 'SET') { code += list + '[' + xVar + '] = ' + value + ';\n'; return code; @@ -244,16 +274,20 @@ export function lists_setIndex(block, generator) { } } throw Error('Unhandled combination (lists_setIndex).'); -}; +} /** * Returns an expression calculating the index into a list. - * @param {string} listName Name of the list, used to calculate length. - * @param {string} where The method of indexing, selected by dropdown in Blockly - * @param {string=} opt_at The optional offset when indexing from start/end. - * @return {string|undefined} Index expression. + * @param listName Name of the list, used to calculate length. + * @param where The method of indexing, selected by dropdown in Blockly + * @param opt_at The optional offset when indexing from start/end. + * @returns Index expression. */ -const getSubstringIndex = function(listName, where, opt_at) { +const getSubstringIndex = function ( + listName: string, + where: string, + opt_at?: string, +): string | undefined { if (where === 'FIRST') { return '0'; } else if (where === 'FROM_END') { @@ -265,18 +299,29 @@ const getSubstringIndex = function(listName, where, opt_at) { } }; -export function lists_getSublist(block, generator) { +export function lists_getSublist( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Dictionary of WHEREn field choices and their CamelCase equivalents. + const wherePascalCase = { + 'FIRST': 'First', + 'LAST': 'Last', + 'FROM_START': 'FromStart', + 'FROM_END': 'FromEnd', + }; + type WhereOption = keyof typeof wherePascalCase; // Get sublist. - const list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; - const where1 = block.getFieldValue('WHERE1'); - const where2 = block.getFieldValue('WHERE2'); + const list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; + const where1 = block.getFieldValue('WHERE1') as WhereOption; + const where2 = block.getFieldValue('WHERE2') as WhereOption; let code; if (where1 === 'FIRST' && where2 === 'LAST') { code = list + '.slice(0)'; } else if ( - list.match(/^\w+$/) || - (where1 !== 'FROM_END' && where2 === 'FROM_START')) { + list.match(/^\w+$/) || + (where1 !== 'FROM_END' && where2 === 'FROM_START') + ) { // If the list is a variable or doesn't require a call for length, don't // generate a helper function. let at1; @@ -285,8 +330,7 @@ export function lists_getSublist(block, generator) { at1 = generator.getAdjusted(block, 'AT1'); break; case 'FROM_END': - at1 = generator.getAdjusted( - block, 'AT1', 1, false, Order.SUBTRACTION); + at1 = generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); at1 = list + '.length - ' + at1; break; case 'FIRST': @@ -301,8 +345,7 @@ export function lists_getSublist(block, generator) { at2 = generator.getAdjusted(block, 'AT2', 1); break; case 'FROM_END': - at2 = generator.getAdjusted( - block, 'AT2', 0, false, Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); at2 = list + '.length - ' + at2; break; case 'LAST': @@ -315,45 +358,49 @@ export function lists_getSublist(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const wherePascalCase = { - 'FIRST': 'First', - 'LAST': 'Last', - 'FROM_START': 'FromStart', - 'FROM_END': 'FromEnd', - }; // The value for 'FROM_END' and'FROM_START' depends on `at` so // we add it as a parameter. const at1Param = - (where1 === 'FROM_END' || where1 === 'FROM_START') ? ', at1' : ''; + where1 === 'FROM_END' || where1 === 'FROM_START' ? ', at1' : ''; const at2Param = - (where2 === 'FROM_END' || where2 === 'FROM_START') ? ', at2' : ''; + where2 === 'FROM_END' || where2 === 'FROM_START' ? ', at2' : ''; const functionName = generator.provideFunction_( - 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) { + 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], + ` +function ${ + generator.FUNCTION_NAME_PLACEHOLDER_ + }(sequence${at1Param}${at2Param}) { var start = ${getSubstringIndex('sequence', where1, 'at1')}; var end = ${getSubstringIndex('sequence', where2, 'at2')} + 1; return sequence.slice(start, end); } -`); - code = functionName + '(' + list + - // The value for 'FROM_END' and 'FROM_START' depends on `at` so we - // pass it. - ((where1 === 'FROM_END' || where1 === 'FROM_START') ? ', ' + at1 : '') + - ((where2 === 'FROM_END' || where2 === 'FROM_START') ? ', ' + at2 : '') + - ')'; +`, + ); + code = + functionName + + '(' + + list + + // The value for 'FROM_END' and 'FROM_START' depends on `at` so we + // pass it. + (where1 === 'FROM_END' || where1 === 'FROM_START' ? ', ' + at1 : '') + + (where2 === 'FROM_END' || where2 === 'FROM_START' ? ', ' + at2 : '') + + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_sort(block, generator) { +export function lists_sort( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for sorting a list. const list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || - '[]'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || '[]'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; const type = block.getFieldValue('TYPE'); - const getCompareFunctionName = - generator.provideFunction_('listsGetSortCompare', ` + const getCompareFunctionName = generator.provideFunction_( + 'listsGetSortCompare', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { var compareFuncs = { 'NUMERIC': function(a, b) { @@ -366,19 +413,28 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { var compare = compareFuncs[type]; return function(a, b) { return compare(a, b) * direction; }; } - `); + `, + ); return [ - list + '.slice().sort(' + getCompareFunctionName + '("' + type + '", ' + - direction + '))', - Order.FUNCTION_CALL + list + + '.slice().sort(' + + getCompareFunctionName + + '("' + + type + + '", ' + + direction + + '))', + Order.FUNCTION_CALL, ]; -}; +} -export function lists_split(block, generator) { +export function lists_split( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.MEMBER); - const delimiter = - generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; + const delimiter = generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; const mode = block.getFieldValue('MODE'); let functionName; if (mode === 'SPLIT') { @@ -396,13 +452,15 @@ export function lists_split(block, generator) { } const code = input + '.' + functionName + '(' + delimiter + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_reverse(block, generator) { +export function lists_reverse( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for reversing a list. const list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || - '[]'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || '[]'; const code = list + '.slice().reverse()'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/javascript/logic.js b/generators/javascript/logic.js deleted file mode 100644 index 896c8954b..000000000 --- a/generators/javascript/logic.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating JavaScript for logic blocks. - */ - -// Former goog.module ID: Blockly.JavaScript.logic - -import {Order} from './javascript_generator.js'; - - -export function controls_if(block, generator) { - // If/elseif/else condition. - let n = 0; - let code = ''; - if (generator.STATEMENT_PREFIX) { - // Automatic prefix insertion is switched off for this block. Add manually. - code += generator.injectId( - generator.STATEMENT_PREFIX, block); - } - do { - const conditionCode = - generator.valueToCode(block, 'IF' + n, Order.NONE) || - 'false'; - let branchCode = generator.statementToCode(block, 'DO' + n); - if (generator.STATEMENT_SUFFIX) { - branchCode = generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += (n > 0 ? ' else ' : '') + 'if (' + conditionCode + ') {\n' + - branchCode + '}'; - n++; - } while (block.getInput('IF' + n)); - - if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { - let branchCode = generator.statementToCode(block, 'ELSE'); - if (generator.STATEMENT_SUFFIX) { - branchCode = generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += ' else {\n' + branchCode + '}'; - } - return code + '\n'; -}; - -export const controls_ifelse = controls_if; - -export function logic_compare(block, generator) { - // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const order = (operator === '==' || operator === '!=') ? - Order.EQUALITY : - Order.RELATIONAL; - const argument0 = generator.valueToCode(block, 'A', order) || '0'; - const argument1 = generator.valueToCode(block, 'B', order) || '0'; - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_operation(block, generator) { - // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; - const order = (operator === '&&') ? Order.LOGICAL_AND : - Order.LOGICAL_OR; - let argument0 = generator.valueToCode(block, 'A', order); - let argument1 = generator.valueToCode(block, 'B', order); - if (!argument0 && !argument1) { - // If there are no arguments, then the return value is false. - argument0 = 'false'; - argument1 = 'false'; - } else { - // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === '&&') ? 'true' : 'false'; - if (!argument0) { - argument0 = defaultArgument; - } - if (!argument1) { - argument1 = defaultArgument; - } - } - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_negate(block, generator) { - // Negation. - const order = Order.LOGICAL_NOT; - const argument0 = - generator.valueToCode(block, 'BOOL', order) || 'true'; - const code = '!' + argument0; - return [code, order]; -}; - -export function logic_boolean(block, generator) { - // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; - return [code, Order.ATOMIC]; -}; - -export function logic_null(block, generator) { - // Null data type. - return ['null', Order.ATOMIC]; -}; - -export function logic_ternary(block, generator) { - // Ternary operator. - const value_if = - generator.valueToCode(block, 'IF', Order.CONDITIONAL) || - 'false'; - const value_then = - generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || - 'null'; - const value_else = - generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || - 'null'; - const code = value_if + ' ? ' + value_then + ' : ' + value_else; - return [code, Order.CONDITIONAL]; -}; diff --git a/generators/javascript/logic.ts b/generators/javascript/logic.ts new file mode 100644 index 000000000..36604ea7d --- /dev/null +++ b/generators/javascript/logic.ts @@ -0,0 +1,153 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Generating JavaScript for logic blocks. + */ + +// Former goog.module ID: Blockly.JavaScript.logic + +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; +import {Order} from './javascript_generator.js'; + +export function controls_if(block: Block, generator: JavascriptGenerator) { + // If/elseif/else condition. + let n = 0; + let code = ''; + if (generator.STATEMENT_PREFIX) { + // Automatic prefix insertion is switched off for this block. Add manually. + code += generator.injectId(generator.STATEMENT_PREFIX, block); + } + do { + const conditionCode = + generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; + let branchCode = generator.statementToCode(block, 'DO' + n); + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += + (n > 0 ? ' else ' : '') + + 'if (' + + conditionCode + + ') {\n' + + branchCode + + '}'; + n++; + } while (block.getInput('IF' + n)); + + if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { + let branchCode = generator.statementToCode(block, 'ELSE'); + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += ' else {\n' + branchCode + '}'; + } + return code + '\n'; +} + +export const controls_ifelse = controls_if; + +export function logic_compare( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Dictionary of OP comparison operators and their implementations. + const OPERATORS = { + 'EQ': '==', + 'NEQ': '!=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const order = + operator === '==' || operator === '!=' ? Order.EQUALITY : Order.RELATIONAL; + const argument0 = generator.valueToCode(block, 'A', order) || '0'; + const argument1 = generator.valueToCode(block, 'B', order) || '0'; + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_operation( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Operations 'and', 'or'. + const operator = block.getFieldValue('OP') === 'AND' ? '&&' : '||'; + const order = operator === '&&' ? Order.LOGICAL_AND : Order.LOGICAL_OR; + let argument0 = generator.valueToCode(block, 'A', order); + let argument1 = generator.valueToCode(block, 'B', order); + if (!argument0 && !argument1) { + // If there are no arguments, then the return value is false. + argument0 = 'false'; + argument1 = 'false'; + } else { + // Single missing arguments have no effect on the return value. + const defaultArgument = operator === '&&' ? 'true' : 'false'; + if (!argument0) { + argument0 = defaultArgument; + } + if (!argument1) { + argument1 = defaultArgument; + } + } + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_negate( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Negation. + const order = Order.LOGICAL_NOT; + const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; + const code = '!' + argument0; + return [code, order]; +} + +export function logic_boolean( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Boolean values true and false. + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; + return [code, Order.ATOMIC]; +} + +export function logic_null( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Null data type. + return ['null', Order.ATOMIC]; +} + +export function logic_ternary( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Ternary operator. + const value_if = + generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; + const value_then = + generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; + const value_else = + generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; + const code = value_if + ' ? ' + value_then + ' : ' + value_else; + return [code, Order.CONDITIONAL]; +} diff --git a/generators/javascript/loops.js b/generators/javascript/loops.ts similarity index 53% rename from generators/javascript/loops.js rename to generators/javascript/loops.ts index fbe532ce7..a5dd86357 100644 --- a/generators/javascript/loops.js +++ b/generators/javascript/loops.ts @@ -11,11 +11,16 @@ // Former goog.module ID: Blockly.JavaScript.loops import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './javascript_generator.js'; - -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext( + block: Block, + generator: JavascriptGenerator, +) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -23,65 +28,88 @@ export function controls_repeat_ext(block, generator) { repeats = String(Number(block.getFieldValue('TIMES'))); } else { // External number. - repeats = - generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || - '0'; + repeats = generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0'; } let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; - const loopVar = - generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); let endVar = repeats; if (!repeats.match(/^\w+$/) && !stringUtils.isNumber(repeats)) { - endVar = - generator.nameDB_.getDistinctName( - 'repeat_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + 'repeat_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + repeats + ';\n'; } - code += 'for (var ' + loopVar + ' = 0; ' + loopVar + ' < ' + endVar + '; ' + - loopVar + '++) {\n' + branch + '}\n'; + code += + 'for (var ' + + loopVar + + ' = 0; ' + + loopVar + + ' < ' + + endVar + + '; ' + + loopVar + + '++) {\n' + + branch + + '}\n'; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil( + block: Block, + generator: JavascriptGenerator, +) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = - generator.valueToCode( - block, 'BOOL', - until ? Order.LOGICAL_NOT : Order.NONE) || - 'false'; + generator.valueToCode( + block, + 'BOOL', + until ? Order.LOGICAL_NOT : Order.NONE, + ) || 'false'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); if (until) { argument0 = '!' + argument0; } return 'while (' + argument0 + ') {\n' + branch + '}\n'; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: JavascriptGenerator) { // For loop. - const variable0 = - generator.getVariableName( - block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; - const argument1 = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; - const increment = - generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; + generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code; - if (stringUtils.isNumber(argument0) && stringUtils.isNumber(argument1) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(argument0) && + stringUtils.isNumber(argument1) && + stringUtils.isNumber(increment) + ) { // All arguments are simple numbers. const up = Number(argument0) <= Number(argument1); - code = 'for (' + variable0 + ' = ' + argument0 + '; ' + variable0 + - (up ? ' <= ' : ' >= ') + argument1 + '; ' + variable0; + code = + 'for (' + + variable0 + + ' = ' + + argument0 + + '; ' + + variable0 + + (up ? ' <= ' : ' >= ') + + argument1 + + '; ' + + variable0; const step = Math.abs(Number(increment)); if (step === 1) { code += up ? '++' : '--'; @@ -94,84 +122,117 @@ export function controls_for(block, generator) { // Cache non-trivial values to variables to prevent repeated look-ups. let startVar = argument0; if (!argument0.match(/^\w+$/) && !stringUtils.isNumber(argument0)) { - startVar = generator.nameDB_.getDistinctName( - variable0 + '_start', NameType.VARIABLE); + startVar = generator.nameDB_!.getDistinctName( + variable0 + '_start', + NameType.VARIABLE, + ); code += 'var ' + startVar + ' = ' + argument0 + ';\n'; } let endVar = argument1; if (!argument1.match(/^\w+$/) && !stringUtils.isNumber(argument1)) { - endVar = generator.nameDB_.getDistinctName( - variable0 + '_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + variable0 + '_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + argument1 + ';\n'; } // Determine loop direction at start, in case one of the bounds // changes during loop execution. - const incVar = generator.nameDB_.getDistinctName( - variable0 + '_inc', NameType.VARIABLE); + const incVar = generator.nameDB_!.getDistinctName( + variable0 + '_inc', + NameType.VARIABLE, + ); code += 'var ' + incVar + ' = '; if (stringUtils.isNumber(increment)) { - code += Math.abs(increment) + ';\n'; + code += Math.abs(Number(increment)) + ';\n'; } else { code += 'Math.abs(' + increment + ');\n'; } code += 'if (' + startVar + ' > ' + endVar + ') {\n'; code += generator.INDENT + incVar + ' = -' + incVar + ';\n'; code += '}\n'; - code += 'for (' + variable0 + ' = ' + startVar + '; ' + incVar + - ' >= 0 ? ' + variable0 + ' <= ' + endVar + ' : ' + variable0 + - ' >= ' + endVar + '; ' + variable0 + ' += ' + incVar + ') {\n' + - branch + '}\n'; + code += + 'for (' + + variable0 + + ' = ' + + startVar + + '; ' + + incVar + + ' >= 0 ? ' + + variable0 + + ' <= ' + + endVar + + ' : ' + + variable0 + + ' >= ' + + endVar + + '; ' + + variable0 + + ' += ' + + incVar + + ') {\n' + + branch + + '}\n'; } return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach(block: Block, generator: JavascriptGenerator) { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || - '[]'; + generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; // Cache non-trivial values to variables to prevent repeated look-ups. let listVar = argument0; if (!argument0.match(/^\w+$/)) { - listVar = generator.nameDB_.getDistinctName( - variable0 + '_list', NameType.VARIABLE); + listVar = generator.nameDB_!.getDistinctName( + variable0 + '_list', + NameType.VARIABLE, + ); code += 'var ' + listVar + ' = ' + argument0 + ';\n'; } - const indexVar = generator.nameDB_.getDistinctName( - variable0 + '_index', NameType.VARIABLE); - branch = generator.INDENT + variable0 + ' = ' + listVar + - '[' + indexVar + '];\n' + branch; + const indexVar = generator.nameDB_!.getDistinctName( + variable0 + '_index', + NameType.VARIABLE, + ); + branch = + generator.INDENT + + variable0 + + ' = ' + + listVar + + '[' + + indexVar + + '];\n' + + branch; code += 'for (var ' + indexVar + ' in ' + listVar + ') {\n' + branch + '}\n'; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: JavascriptGenerator, +) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { // Automatic prefix insertion is switched off for this block. Add manually. - xfix += generator.injectId( - generator.STATEMENT_PREFIX, block); + xfix += generator.injectId(generator.STATEMENT_PREFIX, block); } if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the break/continue is triggered. - xfix += generator.injectId( - generator.STATEMENT_SUFFIX, block); + xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. // In the case of 'break', a prefix is needed due to the loop's suffix. - xfix += generator.injectId( - generator.STATEMENT_PREFIX, loop); + xfix += generator.injectId(generator.STATEMENT_PREFIX, loop); } } switch (block.getFieldValue('FLOW')) { @@ -181,4 +242,4 @@ export function controls_flow_statements(block, generator) { return xfix + 'continue;\n'; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/javascript/math.js b/generators/javascript/math.ts similarity index 62% rename from generators/javascript/math.js rename to generators/javascript/math.ts index 95fa32f5a..c05e817cf 100644 --- a/generators/javascript/math.js +++ b/generators/javascript/math.ts @@ -11,27 +11,34 @@ // Former goog.module ID: Blockly.JavaScript.math +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Numeric value. - const code = Number(block.getFieldValue('NUM')); - const order = code >= 0 ? Order.ATOMIC : - Order.UNARY_NEGATION; - return [code, order]; -}; + const number = Number(block.getFieldValue('NUM')); + const order = number >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION; + return [String(number), order]; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITION], 'MINUS': [' - ', Order.SUBTRACTION], 'MULTIPLY': [' * ', Order.MULTIPLICATION], 'DIVIDE': [' / ', Order.DIVISION], - 'POWER': [null, Order.NONE], // Handle power separately. + 'POWER': [null, Order.NONE], // Handle power separately. }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; @@ -44,17 +51,19 @@ export function math_arithmetic(block, generator) { } code = argument0 + operator + argument1; return [code, order]; -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; let arg; if (operator === 'NEG') { // Negation is a special case given its different operator precedence. - arg = generator.valueToCode(block, 'NUM', - Order.UNARY_NEGATION) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.UNARY_NEGATION) || '0'; if (arg[0] === '-') { // --3 is not legal in JS. arg = ' ' + arg; @@ -63,11 +72,9 @@ export function math_single(block, generator) { return [code, Order.UNARY_NEGATION]; } if (operator === 'SIN' || operator === 'COS' || operator === 'TAN') { - arg = generator.valueToCode(block, 'NUM', - Order.DIVISION) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.DIVISION) || '0'; } else { - arg = generator.valueToCode(block, 'NUM', - Order.NONE) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; } // First, handle cases which generate values that don't need parentheses // wrapping the code. @@ -128,11 +135,14 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.DIVISION]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['Math.PI', Order.MEMBER], 'E': ['Math.E', Order.MEMBER], 'GOLDEN_RATIO': ['(1 + Math.sqrt(5)) / 2', Order.DIVISION], @@ -140,33 +150,36 @@ export function math_constant(block, generator) { 'SQRT1_2': ['Math.SQRT1_2', Order.MEMBER], 'INFINITY': ['Infinity', Order.ATOMIC], }; - return CONSTANTS[block.getFieldValue('CONSTANT')]; -}; + type ConstantOption = keyof typeof CONSTANTS; + return CONSTANTS[block.getFieldValue('CONSTANT') as ConstantOption]; +} -export function math_number_property(block, generator) { +export function math_number_property( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Check if a number is even, odd, prime, whole, positive, or negative // or if it is divisible by certain number. Returns true or false. - const PROPERTIES = { + const PROPERTIES: Record = { 'EVEN': [' % 2 === 0', Order.MODULUS, Order.EQUALITY], 'ODD': [' % 2 === 1', Order.MODULUS, Order.EQUALITY], - 'WHOLE': [' % 1 === 0', Order.MODULUS, - Order.EQUALITY], - 'POSITIVE': [' > 0', Order.RELATIONAL, - Order.RELATIONAL], - 'NEGATIVE': [' < 0', Order.RELATIONAL, - Order.RELATIONAL], + 'WHOLE': [' % 1 === 0', Order.MODULUS, Order.EQUALITY], + 'POSITIVE': [' > 0', Order.RELATIONAL, Order.RELATIONAL], + 'NEGATIVE': [' < 0', Order.RELATIONAL, Order.RELATIONAL], 'DIVISIBLE_BY': [null, Order.MODULUS, Order.EQUALITY], 'PRIME': [null, Order.NONE, Order.FUNCTION_CALL], }; - const dropdownProperty = block.getFieldValue('PROPERTY'); + type PropertyOption = keyof typeof PROPERTIES; + const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption; const [suffix, inputOrder, outputOrder] = PROPERTIES[dropdownProperty]; const numberToCheck = - generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || - '0'; + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - const functionName = generator.provideFunction_('mathIsPrime', ` + const functionName = generator.provideFunction_( + 'mathIsPrime', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { // https://en.wikipedia.org/wiki/Primality_test#Naive_methods if (n == 2 || n == 3) { @@ -185,68 +198,81 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { } return true; } -`); +`, + ); code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MODULUS) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; code = numberToCheck + ' % ' + divisor + ' === 0'; } else { code = numberToCheck + suffix; } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: JavascriptGenerator) { // Add to a variable in place. - const argument0 = generator.valueToCode(block, 'DELTA', - Order.ADDITION) || '0'; + const argument0 = + generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; const varName = generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = (typeof ' + varName + ' === \'number\' ? ' + varName + - ' : 0) + ' + argument0 + ';\n'; -}; + return ( + varName + + ' = (typeof ' + + varName + + " === 'number' ? " + + varName + + ' : 0) + ' + + argument0 + + ';\n' + ); +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); let list; let code; switch (func) { case 'SUM': - list = generator.valueToCode(block, 'LIST', - Order.MEMBER) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; code = list + '.reduce(function(x, y) {return x + y;}, 0)'; break; case 'MIN': - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = 'Math.min.apply(null, ' + list + ')'; break; case 'MAX': - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = 'Math.max.apply(null, ' + list + ')'; break; case 'AVERAGE': { // mathMean([null,null,1,3]) === 2.0. - const functionName = generator.provideFunction_('mathMean', ` + const functionName = generator.provideFunction_( + 'mathMean', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { return myList.reduce(function(x, y) {return x + y;}, 0) / myList.length; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'MEDIAN': { // mathMedian([null,null,1,3]) === 2.0. - const functionName = generator.provideFunction_('mathMedian', ` + const functionName = generator.provideFunction_( + 'mathMedian', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { var localList = myList.filter(function (x) {return typeof x === 'number';}); if (!localList.length) return null; @@ -257,9 +283,9 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { return localList[(localList.length - 1) / 2]; } } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } @@ -267,7 +293,9 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1]. - const functionName = generator.provideFunction_('mathModes', ` + const functionName = generator.provideFunction_( + 'mathModes', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(values) { var modes = []; var counts = []; @@ -296,15 +324,16 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(values) { } return modes; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'STD_DEV': { - const functionName = - generator.provideFunction_('mathStandardDeviation', ` + const functionName = generator.provideFunction_( + 'mathStandardDeviation', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers) { var n = numbers.length; if (!n) return null; @@ -316,22 +345,23 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers) { variance = variance / n; return Math.sqrt(variance); } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'RANDOM': { - const functionName = - generator.provideFunction_('mathRandomList', ` + const functionName = generator.provideFunction_( + 'mathRandomList', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list) { var x = Math.floor(Math.random() * list.length); return list[x]; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } @@ -339,38 +369,51 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list) { throw Error('Unknown operator: ' + func); } return [code, Order.FUNCTION_CALL]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Remainder computation. - const argument0 = generator.valueToCode(block, 'DIVIDEND', - Order.MODULUS) || '0'; - const argument1 = generator.valueToCode(block, 'DIVISOR', - Order.MODULUS) || '0'; + const argument0 = + generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; + const argument1 = + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MODULUS]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Constrain a number between two limits. - const argument0 = generator.valueToCode(block, 'VALUE', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'LOW', - Order.NONE) || '0'; - const argument2 = generator.valueToCode(block, 'HIGH', - Order.NONE) || 'Infinity'; - const code = 'Math.min(Math.max(' + argument0 + ', ' + argument1 + '), ' + - argument2 + ')'; + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; + const argument2 = + generator.valueToCode(block, 'HIGH', Order.NONE) || 'Infinity'; + const code = + 'Math.min(Math.max(' + + argument0 + + ', ' + + argument1 + + '), ' + + argument2 + + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Random integer between [X] and [Y]. - const argument0 = generator.valueToCode(block, 'FROM', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'TO', - Order.NONE) || '0'; - const functionName = generator.provideFunction_('mathRandomInt', ` + const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; + const functionName = generator.provideFunction_( + 'mathRandomInt', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(a, b) { if (a > b) { // Swap a and b to ensure a is smaller. @@ -380,22 +423,29 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(a, b) { } return Math.floor(Math.random() * (b - a + 1) + a); } -`); +`, + ); const code = functionName + '(' + argument0 + ', ' + argument1 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Random fraction between 0 and 1. return ['Math.random()', Order.FUNCTION_CALL]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. - const argument0 = generator.valueToCode(block, 'X', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'Y', - Order.NONE) || '0'; - return ['Math.atan2(' + argument1 + ', ' + argument0 + ') / Math.PI * 180', - Order.DIVISION]; -}; + const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; + return [ + 'Math.atan2(' + argument1 + ', ' + argument0 + ') / Math.PI * 180', + Order.DIVISION, + ]; +} diff --git a/generators/javascript/procedures.js b/generators/javascript/procedures.ts similarity index 58% rename from generators/javascript/procedures.js rename to generators/javascript/procedures.ts index 7f975289c..f0b5c94f0 100644 --- a/generators/javascript/procedures.js +++ b/generators/javascript/procedures.ts @@ -10,20 +10,23 @@ // Former goog.module ID: Blockly.JavaScript.procedures +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn( + block: Block, + generator: JavascriptGenerator, +) { // Define a procedure with a return value. const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { - xfix1 += generator.injectId( - generator.STATEMENT_PREFIX, block); + xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); } if (generator.STATEMENT_SUFFIX) { - xfix1 += generator.injectId( - generator.STATEMENT_SUFFIX, block); + xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (xfix1) { xfix1 = generator.prefixLines(xfix1, generator.INDENT); @@ -31,13 +34,12 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId( - generator.INFINITE_LOOP_TRAP, block), - generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } const branch = generator.statementToCode(block, 'STACK'); - let returnValue = - generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. @@ -49,63 +51,83 @@ export function procedures_defreturn(block, generator) { const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { - args[i] = - generator.getVariableName(variables[i]); + args[i] = generator.getVariableName(variables[i]); } - let code = 'function ' + funcName + '(' + args.join(', ') + ') {\n' + xfix1 + - loopTrap + branch + xfix2 + returnValue + '}'; + let code = + 'function ' + + funcName + + '(' + + args.join(', ') + + ') {\n' + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue + + '}'; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Call a procedure with a return value. const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { - args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || - 'null'; + args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'null'; } const code = funcName + '(' + args.join(', ') + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn( + block: Block, + generator: JavascriptGenerator, +) { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn']( + block, + generator, + ) as [string, Order]; return tuple[0] + ';\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn( + block: Block, + generator: JavascriptGenerator, +) { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || - 'false'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; let code = 'if (' + condition + ') {\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. code += generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT); + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { - const value = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; + if ((block as IfReturnBlock).hasReturnValue_) { + const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; code += generator.INDENT + 'return ' + value + ';\n'; } else { code += generator.INDENT + 'return;\n'; } code += '}\n'; return code; -}; +} diff --git a/generators/javascript/text.js b/generators/javascript/text.ts similarity index 52% rename from generators/javascript/text.js rename to generators/javascript/text.ts index 32125051f..79008cba9 100644 --- a/generators/javascript/text.js +++ b/generators/javascript/text.ts @@ -10,9 +10,11 @@ // Former goog.module ID: Blockly.JavaScript.texts +import type {Block} from '../../core/block.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - /** * Regular expression to detect a single-quoted string literal. */ @@ -21,11 +23,11 @@ const strRegExp = /^\s*'([^']|\\')*'\s*$/; /** * Enclose the provided value in 'String(...)' function. * Leave string literals alone. - * @param {string} value Code evaluating to a value. - * @return {Array} Array containing code evaluating to a string + * @param value Code evaluating to a value. + * @returns Array containing code evaluating to a string * and the order of the returned code.[string, number] */ -const forceString = function(value) { +const forceString = function (value: string): [string, Order] { if (strRegExp.test(value)) { return [value, Order.ATOMIC]; } @@ -34,12 +36,16 @@ const forceString = function(value) { /** * Returns an expression calculating the index into a string. - * @param {string} stringName Name of the string, used to calculate length. - * @param {string} where The method of indexing, selected by dropdown in Blockly - * @param {string=} opt_at The optional offset when indexing from start/end. - * @return {string|undefined} Index expression. + * @param stringName Name of the string, used to calculate length. + * @param where The method of indexing, selected by dropdown in Blockly + * @param opt_at The optional offset when indexing from start/end. + * @returns Index expression. */ -const getSubstringIndex = function(stringName, where, opt_at) { +const getSubstringIndex = function ( + stringName: string, + where: string, + opt_at?: string, +): string | undefined { if (where === 'FIRST') { return '0'; } else if (where === 'FROM_END') { @@ -51,101 +57,112 @@ const getSubstringIndex = function(stringName, where, opt_at) { } }; -export function text(block, generator) { +export function text( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Text value. - const code = - generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.indexOf('+') !== -1 ? Order.ADDITION : - Order.ATOMIC; + const code = generator.multiline_quote_(block.getFieldValue('TEXT')); + const order = code.indexOf('+') !== -1 ? Order.ADDITION : Order.ATOMIC; return [code, order]; -}; +} -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const joinBlock = block as JoinMutatorBlock; // Create a string made up of any number of elements of any type. - switch (block.itemCount_) { + switch (joinBlock.itemCount_) { case 0: return ["''", Order.ATOMIC]; case 1: { - const element = generator.valueToCode(block, 'ADD0', - Order.NONE) || "''"; + const element = + generator.valueToCode(joinBlock, 'ADD0', Order.NONE) || "''"; const codeAndOrder = forceString(element); return codeAndOrder; } case 2: { - const element0 = generator.valueToCode(block, 'ADD0', - Order.NONE) || "''"; - const element1 = generator.valueToCode(block, 'ADD1', - Order.NONE) || "''"; - const code = forceString(element0)[0] + - ' + ' + forceString(element1)[0]; + const element0 = + generator.valueToCode(joinBlock, 'ADD0', Order.NONE) || "''"; + const element1 = + generator.valueToCode(joinBlock, 'ADD1', Order.NONE) || "''"; + const code = forceString(element0)[0] + ' + ' + forceString(element1)[0]; return [code, Order.ADDITION]; } default: { - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = generator.valueToCode(block, 'ADD' + i, - Order.NONE) || "''"; + const elements = new Array(joinBlock.itemCount_); + for (let i = 0; i < joinBlock.itemCount_; i++) { + elements[i] = + generator.valueToCode(joinBlock, 'ADD' + i, Order.NONE) || "''"; } - const code = '[' + elements.join(',') + '].join(\'\')'; + const code = '[' + elements.join(',') + "].join('')"; return [code, Order.FUNCTION_CALL]; } } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: JavascriptGenerator) { // Append to a variable in place. const varName = generator.getVariableName(block.getFieldValue('VAR')); - const value = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const code = varName + ' += ' + - forceString(value)[0] + ';\n'; + const value = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const code = varName + ' += ' + forceString(value)[0] + ';\n'; return code; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // String or array length. - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return [text + '.length', Order.MEMBER]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Is the string null or array empty? - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return ['!' + text + '.length', Order.LOGICAL_NOT]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Search the text for a substring. - const operator = block.getFieldValue('END') === 'FIRST' ? - 'indexOf' : 'lastIndexOf'; - const substring = generator.valueToCode(block, 'FIND', - Order.NONE) || "''"; - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const operator = + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; const code = text + '.' + operator + '(' + substring + ')'; // Adjust index if using one-based indices. if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITION]; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const textOrder = (where === 'RANDOM') ? Order.NONE : - Order.MEMBER; - const text = - generator.valueToCode(block, 'VALUE', textOrder) || "''"; + const textOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; + const text = generator.valueToCode(block, 'VALUE', textOrder) || "''"; switch (where) { case 'FIRST': { const code = text + '.charAt(0)'; @@ -167,30 +184,44 @@ export function text_charAt(block, generator) { return [code, Order.FUNCTION_CALL]; } case 'RANDOM': { - const functionName = - generator.provideFunction_('textRandomLetter', ` + const functionName = generator.provideFunction_( + 'textRandomLetter', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text) { var x = Math.floor(Math.random() * text.length); return text[x]; } -`); +`, + ); const code = functionName + '(' + text + ')'; return [code, Order.FUNCTION_CALL]; } } throw Error('Unhandled option (text_charAt).'); -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Dictionary of WHEREn field choices and their CamelCase equivalents. */ + const wherePascalCase = { + 'FIRST': 'First', + 'LAST': 'Last', + 'FROM_START': 'FromStart', + 'FROM_END': 'FromEnd', + }; + type WhereOption = keyof typeof wherePascalCase; // Get substring. - const where1 = block.getFieldValue('WHERE1'); - const where2 = block.getFieldValue('WHERE2'); - const requiresLengthCall = (where1 !== 'FROM_END' && where1 !== 'LAST' && - where2 !== 'FROM_END' && where2 !== 'LAST'); - const textOrder = requiresLengthCall ? Order.MEMBER : - Order.NONE; - const text = - generator.valueToCode(block, 'STRING', textOrder) || "''"; + const where1 = block.getFieldValue('WHERE1') as WhereOption; + const where2 = block.getFieldValue('WHERE2') as WhereOption; + const requiresLengthCall = + where1 !== 'FROM_END' && + where1 !== 'LAST' && + where2 !== 'FROM_END' && + where2 !== 'LAST'; + const textOrder = requiresLengthCall ? Order.MEMBER : Order.NONE; + const text = generator.valueToCode(block, 'STRING', textOrder) || "''"; let code; if (where1 === 'FIRST' && where2 === 'LAST') { code = text; @@ -204,8 +235,7 @@ export function text_getSubstring(block, generator) { at1 = generator.getAdjusted(block, 'AT1'); break; case 'FROM_END': - at1 = generator.getAdjusted(block, 'AT1', 1, false, - Order.SUBTRACTION); + at1 = generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); at1 = text + '.length - ' + at1; break; case 'FIRST': @@ -220,8 +250,7 @@ export function text_getSubstring(block, generator) { at2 = generator.getAdjusted(block, 'AT2', 1); break; case 'FROM_END': - at2 = generator.getAdjusted(block, 'AT2', 0, false, - Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); at2 = text + '.length - ' + at2; break; case 'LAST': @@ -234,82 +263,97 @@ export function text_getSubstring(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const wherePascalCase = {'FIRST': 'First', 'LAST': 'Last', - 'FROM_START': 'FromStart', 'FROM_END': 'FromEnd'}; // The value for 'FROM_END' and'FROM_START' depends on `at` so // we add it as a parameter. const at1Param = - (where1 === 'FROM_END' || where1 === 'FROM_START') ? ', at1' : ''; + where1 === 'FROM_END' || where1 === 'FROM_START' ? ', at1' : ''; const at2Param = - (where2 === 'FROM_END' || where2 === 'FROM_START') ? ', at2' : ''; + where2 === 'FROM_END' || where2 === 'FROM_START' ? ', at2' : ''; const functionName = generator.provideFunction_( - 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) { + 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], + ` +function ${ + generator.FUNCTION_NAME_PLACEHOLDER_ + }(sequence${at1Param}${at2Param}) { var start = ${getSubstringIndex('sequence', where1, 'at1')}; var end = ${getSubstringIndex('sequence', where2, 'at2')} + 1; return sequence.slice(start, end); } -`); - code = functionName + '(' + text + - // The value for 'FROM_END' and 'FROM_START' depends on `at` so we - // pass it. - ((where1 === 'FROM_END' || where1 === 'FROM_START') ? ', ' + at1 : '') + - ((where2 === 'FROM_END' || where2 === 'FROM_START') ? ', ' + at2 : '') + - ')'; +`, + ); + code = + functionName + + '(' + + text + + // The value for 'FROM_END' and 'FROM_START' depends on `at` so we + // pass it. + (where1 === 'FROM_END' || where1 === 'FROM_START' ? ', ' + at1 : '') + + (where2 === 'FROM_END' || where2 === 'FROM_START' ? ', ' + at2 : '') + + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.toUpperCase()', 'LOWERCASE': '.toLowerCase()', 'TITLECASE': null, }; - const operator = OPERATORS[block.getFieldValue('CASE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('CASE') as OperatorOption]; const textOrder = operator ? Order.MEMBER : Order.NONE; - const text = - generator.valueToCode(block, 'TEXT', textOrder) || "''"; + const text = generator.valueToCode(block, 'TEXT', textOrder) || "''"; let code; if (operator) { // Upper and lower case are functions built into generator. code = text + operator; } else { // Title case is not a native JavaScript function. Define one. - const functionName = - generator.provideFunction_('textToTitleCase', ` + const functionName = generator.provideFunction_( + 'textToTitleCase', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) { return str.replace(/\\S+/g, function(txt) {return txt[0].toUpperCase() + txt.substring(1).toLowerCase();}); } -`); +`, + ); code = functionName + '(' + text + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = { 'LEFT': ".replace(/^[\\s\\xa0]+/, '')", 'RIGHT': ".replace(/[\\s\\xa0]+$/, '')", 'BOTH': '.trim()', }; - const operator = OPERATORS[block.getFieldValue('MODE')]; - const text = generator.valueToCode(block, 'TEXT', - Order.MEMBER) || "''"; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; + const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; return [text + operator, Order.FUNCTION_CALL]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: JavascriptGenerator) { // Print statement. - const msg = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; + const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'window.alert(' + msg + ');\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -325,16 +369,19 @@ export function text_prompt_ext(block, generator) { code = 'Number(' + code + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { - const text = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const sub = generator.valueToCode(block, 'SUB', - Order.NONE) || "''"; - const functionName = generator.provideFunction_('textCount', ` +export function text_count( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; + const functionName = generator.provideFunction_( + 'textCount', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) { if (needle.length === 0) { return haystack.length + 1; @@ -342,33 +389,40 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) { return haystack.split(needle).length - 1; } } -`); +`, + ); const code = functionName + '(' + text + ', ' + sub + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_replace(block, generator) { - const text = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const from = generator.valueToCode(block, 'FROM', - Order.NONE) || "''"; +export function text_replace( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; // The regex escaping code below is taken from the implementation of // goog.string.regExpEscape. - const functionName = generator.provideFunction_('textReplace', ` + const functionName = generator.provideFunction_( + 'textReplace', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) { needle = needle.replace(/([-()\\[\\]{}+?*.$\\^|,:#