From 130989763c0c3af21c34de7352bda3b612863d15 Mon Sep 17 00:00:00 2001 From: Christopher Allen Date: Tue, 20 Jun 2023 23:22:44 +0100 Subject: [PATCH] refactor(generators): Restructure generator modules to contain side effects (#7173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(generators): Move lang.js -> lang/lang_gernator.js Move the LangGenerator definitions into their respective subdirectories and add a _generator suffix to their filenames, i.e. generators/javascript.js becomes generators/javascript/javascript_generator.js. This is to keep related code together and allow the `lang/all.js` entrypoints to be moved to the top level generators/ directory. No goog module IDs were changed, so playground and test code that accesses this modules by filename does not need to be modified. * refactor(generators) Move lang/all.js -> lang.js - Move the entrypoints in generators/*/all.js to correspondingly-named files in generators/ instead—i.e., generators/javascript/all.js becomes generators/javascript.js. - Update build_tasks.js accordingly. * fix(generators): Add missing exports for LuaGenerator, PhpGenerator These were inadvertently omitted from #7161 and #7162, respectively. * refactor(generators): Make block generator modules side-effect free - Move declaration of Generator instance from generators//_generator.js to generators/.js. - Move .addReservedWords() calls from generators//*.js to generators/.js - Modify generators//*.js to export block generator functions individually, rather than installing on Generator instance. - Modify generators/.js to import and install block generator functions on Generator instance. * fix(tests): Fix tests broken by restructuring of generators Where these tests needed block generator functions preinstalled they should have been importing the Blockly..all module. Where they do not need the provided block generator functions they can now create their own empty Generator instances. * chore: Update renamings file - Fix a malformation in previous entries that was not detected by the renaming file validator test. - Add entries describing the work done in this and related recent PRs. * fix: Correct minor errors in PR #7173 - Fix a search-and-replace error in renamings.json5 - Fix an incorrect-but-usable import in generator_test.js --- generators/dart.js | 329 ++-------------- generators/dart/all.js | 26 -- generators/dart/colour.js | 12 +- generators/dart/dart_generator.js | 311 +++++++++++++++ generators/dart/lists.js | 28 +- generators/dart/logic.js | 19 +- generators/dart/loops.js | 15 +- generators/dart/math.js | 32 +- generators/dart/procedures.js | 12 +- generators/dart/text.js | 39 +- generators/dart/variables.js | 6 +- generators/dart/variables_dynamic.js | 11 +- generators/javascript.js | 347 ++--------------- generators/javascript/all.js | 26 -- generators/javascript/colour.js | 10 +- generators/javascript/javascript_generator.js | 335 ++++++++++++++++ generators/javascript/lists.js | 26 +- generators/javascript/logic.js | 19 +- generators/javascript/loops.js | 15 +- generators/javascript/math.js | 32 +- generators/javascript/procedures.js | 13 +- generators/javascript/text.js | 37 +- generators/javascript/variables.js | 6 +- generators/javascript/variables_dynamic.js | 10 +- generators/lua.js | 229 ++--------- generators/lua/all.js | 26 -- generators/lua/colour.js | 10 +- generators/lua/lists.js | 26 +- generators/lua/logic.js | 18 +- generators/lua/loops.js | 15 +- generators/lua/lua_generator.js | 213 +++++++++++ generators/lua/math.js | 30 +- generators/lua/procedures.js | 13 +- generators/lua/text.js | 36 +- generators/lua/variables.js | 6 +- generators/lua/variables_dynamic.js | 11 +- generators/php.js | 324 ++-------------- generators/php/all.js | 26 -- generators/php/colour.js | 10 +- generators/php/lists.js | 26 +- generators/php/logic.js | 18 +- generators/php/loops.js | 15 +- generators/php/math.js | 30 +- generators/php/php_generator.js | 310 +++++++++++++++ generators/php/procedures.js | 13 +- generators/php/text.js | 36 +- generators/php/variables.js | 6 +- generators/php/variables_dynamic.js | 11 +- generators/python.js | 359 ++---------------- generators/python/all.js | 26 -- generators/python/colour.js | 10 +- generators/python/lists.js | 26 +- generators/python/logic.js | 19 +- generators/python/loops.js | 15 +- generators/python/math.js | 34 +- generators/python/procedures.js | 13 +- generators/python/python_generator.js | 340 +++++++++++++++++ generators/python/text.js | 37 +- generators/python/variables.js | 6 +- generators/python/variables_dynamic.js | 9 +- scripts/gulpfiles/build_tasks.js | 10 +- scripts/migration/renamings.json5 | 58 +++ tests/mocha/field_multilineinput_test.js | 10 +- tests/mocha/generator_test.js | 20 +- 64 files changed, 2120 insertions(+), 2046 deletions(-) delete mode 100644 generators/dart/all.js create mode 100644 generators/dart/dart_generator.js delete mode 100644 generators/javascript/all.js create mode 100644 generators/javascript/javascript_generator.js delete mode 100644 generators/lua/all.js create mode 100644 generators/lua/lua_generator.js delete mode 100644 generators/php/all.js create mode 100644 generators/php/php_generator.js delete mode 100644 generators/python/all.js create mode 100644 generators/python/python_generator.js diff --git a/generators/dart.js b/generators/dart.js index f79687940..4d2943ce7 100644 --- a/generators/dart.js +++ b/generators/dart.js @@ -1,317 +1,44 @@ /** * @license - * Copyright 2014 Google LLC + * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Helper functions for generating Dart for blocks. - * @suppress {checkTypes|globalThis} + * @fileoverview Complete helper functions for generating Dart for + * blocks. This is the entrypoint for dart_compressed.js. + * @suppress {extraRequire} */ import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Dart'); +goog.declareModuleId('Blockly.Dart.all'); -import * as Variables from '../core/variables.js'; -import * as stringUtils from '../core/utils/string.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 {inputTypes} from '../core/inputs/input_types.js'; +import {DartGenerator} from './dart/dart_generator.js'; +import * as colour from './dart/colour.js'; +import * as lists from './dart/lists.js'; +import * as logic from './dart/logic.js'; +import * as loops from './dart/loops.js'; +import * as math from './dart/math.js'; +import * as procedures from './dart/procedures.js'; +import * as text from './dart/text.js'; +import * as variables from './dart/variables.js'; +import * as variablesDynamic from './dart/variables_dynamic.js'; +export * from './dart/dart_generator.js'; /** - * Order of operation ENUMs. - * https://dart.dev/guides/language/language-tour#operators - * @enum {number} - */ -export const Order = { - ATOMIC: 0, // 0 "" ... - UNARY_POSTFIX: 1, // expr++ expr-- () [] . ?. - UNARY_PREFIX: 2, // -expr !expr ~expr ++expr --expr - MULTIPLICATIVE: 3, // * / % ~/ - ADDITIVE: 4, // + - - SHIFT: 5, // << >> - BITWISE_AND: 6, // & - BITWISE_XOR: 7, // ^ - BITWISE_OR: 8, // | - RELATIONAL: 9, // >= > <= < as is is! - EQUALITY: 10, // == != - LOGICAL_AND: 11, // && - LOGICAL_OR: 12, // || - IF_NULL: 13, // ?? - CONDITIONAL: 14, // expr ? expr : expr - CASCADE: 15, // .. - ASSIGNMENT: 16, // = *= /= ~/= %= += -= <<= >>= &= ^= |= - NONE: 99, // (...) -}; - -/** - * Dart code generator class. - */ -export class DartGenerator extends CodeGenerator { - constructor(name) { - super(name ?? 'Dart'); - this.isInitialized = false; - - // Copy Order values onto instance for backwards compatibility - // while ensuring they are not part of the publically-advertised - // API. - // - // TODO(#7085): deprecate these in due course. (Could initially - // replace data properties with get accessors that call - // deprecate.warn().) - for (const key in Order) { - this['ORDER_' + key] = Order[key]; - } - - // 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. - this.addReservedWords( - // https://www.dartlang.org/docs/spec/latest/dart-language-specification.pdf - // Section 16.1.1 - 'assert,break,case,catch,class,const,continue,default,do,else,enum,' + - 'extends,false,final,finally,for,if,in,is,new,null,rethrow,return,' + - 'super,switch,this,throw,true,try,var,void,while,with,' + - // https://api.dartlang.org/dart_core.html - 'print,identityHashCode,identical,BidirectionalIterator,Comparable,' + - 'double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,' + - 'Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,' + - 'Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,' + - 'StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,' + - 'ArgumentError,AssertionError,CastError,ConcurrentModificationError,' + - 'CyclicInitializationError,Error,Exception,FallThroughError,' + - 'FormatException,IntegerDivisionByZeroException,NoSuchMethodError,' + - 'NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,' + - 'StateError,TypeError,UnimplementedError,UnsupportedError' - ); - } - - /** - * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. - */ - init(workspace) { - super.init(); - - if (!this.nameDB_) { - this.nameDB_ = new Names(this.RESERVED_WORDS_); - } else { - this.nameDB_.reset(); - } - - this.nameDB_.setVariableMap(workspace.getVariableMap()); - this.nameDB_.populateVariables(workspace); - this.nameDB_.populateProcedures(workspace); - - const defvars = []; - // Add developer variables (not created or named by the user). - const devVarList = Variables.allDeveloperVariables(workspace); - for (let i = 0; i < devVarList.length; i++) { - defvars.push(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)); - } - - // Declare all of the variables. - if (defvars.length) { - this.definitions_['variables'] = - 'var ' + defvars.join(', ') + ';'; - } - this.isInitialized = true; - } - - /** - * Prepend the generated code with import statements and variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ - finish(code) { - // Indent every line. - if (code) { - code = this.prefixLines(code, this.INDENT); - } - code = 'main() {\n' + code + '}'; - - // Convert the definitions dictionary into a list. - const imports = []; - const definitions = []; - for (let name in this.definitions_) { - const def = this.definitions_[name]; - if (def.match(/^import\s/)) { - imports.push(def); - } else { - definitions.push(def); - } - } - // Call Blockly.CodeGenerator's finish. - code = super.finish(code); - this.isInitialized = false; - - this.nameDB_.reset(); - const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); - return allDefs.replace(/\n\n+/g, '\n\n').replace(/\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. - */ - scrubNakedValue(line) { - return line + ';\n'; - } - - /** - * Encode a string as a properly escaped Dart string, complete with quotes. - * @param {string} string Text to encode. - * @return {string} Dart string. - * @protected - */ - quote_(string) { - // Can't use goog.string.quote since $ must also be escaped. - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/\$/g, '\\$') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; - } - - /** - * Encode a string as a properly escaped multiline Dart string, complete with - * quotes. - * @param {string} string Text to encode. - * @return {string} Dart string. - * @protected - */ - multiline_quote_(string) { - const lines = string.split(/\n/g).map(this.quote_); - // Join with the following, plus a newline: - // + '\n' + - return lines.join(' + \'\\n\' + \n'); - } - - /** - * Common tasks for generating Dart 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 Dart code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this - * statement. - * @return {string} Dart code with comments and subsequent blocks added. - * @protected - */ - scrub_(block, code, opt_thisOnly) { - let commentCode = ''; - // Only collect comments for blocks that aren't inline. - if (!block.outputConnection || !block.outputConnection.targetConnection) { - // Collect comment for this block. - let comment = block.getCommentText(); - if (comment) { - comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - if (block.getProcedureDef) { - // Use documentation comment for function comments. - commentCode += this.prefixLines(comment + '\n', '/// '); - } else { - commentCode += this.prefixLines(comment + '\n', '// '); - } - } - // Collect comments for all value arguments. - // 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(); - if (childBlock) { - comment = this.allNestedComments(childBlock); - if (comment) { - commentCode += this.prefixLines(comment, '// '); - } - } - } - } - } - const nextBlock = - block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_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} - */ - getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { - let delta = opt_delta || 0; - let order = opt_order || this.ORDER_NONE; - if (block.workspace.options.oneBasedIndex) { - delta--; - } - const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - - /** @type {number} */ - let outerOrder; - let innerOrder; - if (delta) { - outerOrder = this.ORDER_ADDITIVE; - innerOrder = this.ORDER_ADDITIVE; - } else if (opt_negate) { - outerOrder = this.ORDER_UNARY_PREFIX; - innerOrder = this.ORDER_UNARY_PREFIX; - } else { - outerOrder = order; - } - - /** @type {string|number} */ - let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; - - if (stringUtils.isNumber(at)) { - // If the index is a naked number, adjust it right now. - at = parseInt(at, 10) + 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 + ')'; - } - } - return at; - } -} - -/** - * Dart code generator. + * Dart code generator instance. * @type {!DartGenerator} */ export const dartGenerator = new DartGenerator(); + +// Add reserved words. This list should include all words mentioned +// in RESERVED WORDS: comments in the imports above. +dartGenerator.addReservedWords('Html,Math'); + +// Install per-block-type generator functions: +Object.assign( + dartGenerator.forBlock, + colour, lists, logic, loops, math, procedures, + text, variables, variablesDynamic +); diff --git a/generators/dart/all.js b/generators/dart/all.js deleted file mode 100644 index 6902d882b..000000000 --- a/generators/dart/all.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Complete helper functions for generating Dart for - * blocks. This is the entrypoint for dart_compressed.js. - * @suppress {extraRequire} - */ - -import * as goog from '../../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Dart.all'); - -import './colour.js'; -import './lists.js'; -import './logic.js'; -import './loops.js'; -import './math.js'; -import './procedures.js'; -import './text.js'; -import './variables.js'; -import './variables_dynamic.js'; - -export * from '../dart.js'; diff --git a/generators/dart/colour.js b/generators/dart/colour.js index f82e66977..e415d589a 100644 --- a/generators/dart/colour.js +++ b/generators/dart/colour.js @@ -11,18 +11,18 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.colour'); -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.addReservedWords('Math'); +// RESERVED WORDS: 'Math' -dartGenerator.forBlock['colour_picker'] = function(block, generator) { +export function colour_picker(block, generator) { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; }; -dartGenerator.forBlock['colour_random'] = function(block, generator) { +export function colour_random(block, generator) { // Generate a random colour. generator.definitions_['import_dart_math'] = "import 'dart:math' as Math;"; @@ -39,7 +39,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}() { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['colour_rgb'] = function(block, generator) { +export function colour_rgb(block, generator) { // 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; @@ -68,7 +68,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(num r, num g, num b) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['colour_blend'] = function(block, generator) { +export function colour_blend(block, generator) { // Blend two colours together. const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; diff --git a/generators/dart/dart_generator.js b/generators/dart/dart_generator.js new file mode 100644 index 000000000..5f64e919f --- /dev/null +++ b/generators/dart/dart_generator.js @@ -0,0 +1,311 @@ +/** + * @license + * Copyright 2014 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helper functions for generating Dart for blocks. + * @suppress {checkTypes|globalThis} + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('Blockly.Dart'); + +import * as Variables from '../../core/variables.js'; +import * as stringUtils from '../../core/utils/string.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 {inputTypes} from '../../core/inputs/input_types.js'; + + +/** + * Order of operation ENUMs. + * https://dart.dev/guides/language/language-tour#operators + * @enum {number} + */ +export const Order = { + ATOMIC: 0, // 0 "" ... + UNARY_POSTFIX: 1, // expr++ expr-- () [] . ?. + UNARY_PREFIX: 2, // -expr !expr ~expr ++expr --expr + MULTIPLICATIVE: 3, // * / % ~/ + ADDITIVE: 4, // + - + SHIFT: 5, // << >> + BITWISE_AND: 6, // & + BITWISE_XOR: 7, // ^ + BITWISE_OR: 8, // | + RELATIONAL: 9, // >= > <= < as is is! + EQUALITY: 10, // == != + LOGICAL_AND: 11, // && + LOGICAL_OR: 12, // || + IF_NULL: 13, // ?? + CONDITIONAL: 14, // expr ? expr : expr + CASCADE: 15, // .. + ASSIGNMENT: 16, // = *= /= ~/= %= += -= <<= >>= &= ^= |= + NONE: 99, // (...) +}; + +/** + * Dart code generator class. + */ +export class DartGenerator extends CodeGenerator { + constructor(name) { + super(name ?? 'Dart'); + this.isInitialized = false; + + // Copy Order values onto instance for backwards compatibility + // while ensuring they are not part of the publically-advertised + // API. + // + // TODO(#7085): deprecate these in due course. (Could initially + // replace data properties with get accessors that call + // deprecate.warn().) + for (const key in Order) { + this['ORDER_' + key] = Order[key]; + } + + // 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. + this.addReservedWords( + // https://www.dartlang.org/docs/spec/latest/dart-language-specification.pdf + // Section 16.1.1 + 'assert,break,case,catch,class,const,continue,default,do,else,enum,' + + 'extends,false,final,finally,for,if,in,is,new,null,rethrow,return,' + + 'super,switch,this,throw,true,try,var,void,while,with,' + + // https://api.dartlang.org/dart_core.html + 'print,identityHashCode,identical,BidirectionalIterator,Comparable,' + + 'double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,' + + 'Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,' + + 'Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,' + + 'StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,' + + 'ArgumentError,AssertionError,CastError,ConcurrentModificationError,' + + 'CyclicInitializationError,Error,Exception,FallThroughError,' + + 'FormatException,IntegerDivisionByZeroException,NoSuchMethodError,' + + 'NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,' + + 'StateError,TypeError,UnimplementedError,UnsupportedError' + ); + } + + /** + * Initialise the database of variable names. + * @param {!Workspace} workspace Workspace to generate code from. + */ + init(workspace) { + super.init(); + + if (!this.nameDB_) { + this.nameDB_ = new Names(this.RESERVED_WORDS_); + } else { + this.nameDB_.reset(); + } + + this.nameDB_.setVariableMap(workspace.getVariableMap()); + this.nameDB_.populateVariables(workspace); + this.nameDB_.populateProcedures(workspace); + + const defvars = []; + // Add developer variables (not created or named by the user). + const devVarList = Variables.allDeveloperVariables(workspace); + for (let i = 0; i < devVarList.length; i++) { + defvars.push(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)); + } + + // Declare all of the variables. + if (defvars.length) { + this.definitions_['variables'] = + 'var ' + defvars.join(', ') + ';'; + } + this.isInitialized = true; + } + + /** + * Prepend the generated code with import statements and variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // Indent every line. + if (code) { + code = this.prefixLines(code, this.INDENT); + } + code = 'main() {\n' + code + '}'; + + // Convert the definitions dictionary into a list. + const imports = []; + const definitions = []; + for (let name in this.definitions_) { + const def = this.definitions_[name]; + if (def.match(/^import\s/)) { + imports.push(def); + } else { + definitions.push(def); + } + } + // Call Blockly.CodeGenerator's finish. + code = super.finish(code); + this.isInitialized = false; + + this.nameDB_.reset(); + const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); + return allDefs.replace(/\n\n+/g, '\n\n').replace(/\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. + */ + scrubNakedValue(line) { + return line + ';\n'; + } + + /** + * Encode a string as a properly escaped Dart string, complete with quotes. + * @param {string} string Text to encode. + * @return {string} Dart string. + * @protected + */ + quote_(string) { + // Can't use goog.string.quote since $ must also be escaped. + string = string.replace(/\\/g, '\\\\') + .replace(/\n/g, '\\\n') + .replace(/\$/g, '\\$') + .replace(/'/g, '\\\''); + return '\'' + string + '\''; + } + + /** + * Encode a string as a properly escaped multiline Dart string, complete with + * quotes. + * @param {string} string Text to encode. + * @return {string} Dart string. + * @protected + */ + multiline_quote_(string) { + const lines = string.split(/\n/g).map(this.quote_); + // Join with the following, plus a newline: + // + '\n' + + return lines.join(' + \'\\n\' + \n'); + } + + /** + * Common tasks for generating Dart 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 Dart code created for this block. + * @param {boolean=} opt_thisOnly True to generate code for only this + * statement. + * @return {string} Dart code with comments and subsequent blocks added. + * @protected + */ + scrub_(block, code, opt_thisOnly) { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + if (block.getProcedureDef) { + // Use documentation comment for function comments. + commentCode += this.prefixLines(comment + '\n', '/// '); + } else { + commentCode += this.prefixLines(comment + '\n', '// '); + } + } + // Collect comments for all value arguments. + // 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(); + if (childBlock) { + comment = this.allNestedComments(childBlock); + if (comment) { + commentCode += this.prefixLines(comment, '// '); + } + } + } + } + } + const nextBlock = + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = opt_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} + */ + getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { + let delta = opt_delta || 0; + let order = opt_order || this.ORDER_NONE; + if (block.workspace.options.oneBasedIndex) { + delta--; + } + const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + + /** @type {number} */ + let outerOrder; + let innerOrder; + if (delta) { + outerOrder = this.ORDER_ADDITIVE; + innerOrder = this.ORDER_ADDITIVE; + } else if (opt_negate) { + outerOrder = this.ORDER_UNARY_PREFIX; + innerOrder = this.ORDER_UNARY_PREFIX; + } else { + outerOrder = order; + } + + /** @type {string|number} */ + let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; + + if (stringUtils.isNumber(at)) { + // If the index is a naked number, adjust it right now. + at = parseInt(at, 10) + 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 + ')'; + } + } + return at; + } +} diff --git a/generators/dart/lists.js b/generators/dart/lists.js index 2c9ef7456..0f7a24d64 100644 --- a/generators/dart/lists.js +++ b/generators/dart/lists.js @@ -12,17 +12,17 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.lists'); import {NameType} from '../../core/names.js'; -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.addReservedWords('Math'); +// RESERVED WORDS: 'Math' -dartGenerator.forBlock['lists_create_empty'] = function(block, generator) { +export function lists_create_empty(block, generator) { // Create an empty list. return ['[]', Order.ATOMIC]; }; -dartGenerator.forBlock['lists_create_with'] = function(block, generator) { +export function lists_create_with(block, generator) { // 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++) { @@ -33,7 +33,7 @@ dartGenerator.forBlock['lists_create_with'] = function(block, generator) { return [code, Order.ATOMIC]; }; -dartGenerator.forBlock['lists_repeat'] = function(block, generator) { +export function lists_repeat(block, generator) { // Create a list with one element repeated. const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; @@ -43,21 +43,21 @@ dartGenerator.forBlock['lists_repeat'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_length'] = function(block, generator) { +export function lists_length(block, generator) { // String or array length. const list = generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; return [list + '.length', Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_isEmpty'] = function(block, generator) { +export function lists_isEmpty(block, generator) { // Is the string null or array empty? const list = generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; return [list + '.isEmpty', Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_indexOf'] = function(block, generator) { +export function lists_indexOf(block, generator) { // Find an item in the list. const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; @@ -71,7 +71,7 @@ dartGenerator.forBlock['lists_indexOf'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_getIndex'] = function(block, generator) { +export function lists_getIndex(block, generator) { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -234,7 +234,7 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list) { throw Error('Unhandled combination (lists_getIndex).'); }; -dartGenerator.forBlock['lists_setIndex'] = function(block, generator) { +export function lists_setIndex(block, generator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -316,7 +316,7 @@ dartGenerator.forBlock['lists_setIndex'] = function(block, generator) { throw Error('Unhandled combination (lists_setIndex).'); }; -dartGenerator.forBlock['lists_getSublist'] = function(block, generator) { +export function lists_getSublist(block, generator) { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.UNARY_POSTFIX) || '[]'; @@ -390,7 +390,7 @@ List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String where1, num at1, return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_sort'] = function(block, generator) { +export function lists_sort(block, generator) { // Block for sorting a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; @@ -417,7 +417,7 @@ List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String type, int directi ]; }; -dartGenerator.forBlock['lists_split'] = function(block, generator) { +export function lists_split(block, generator) { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.UNARY_POSTFIX); const delimiter = @@ -441,7 +441,7 @@ dartGenerator.forBlock['lists_split'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['lists_reverse'] = function(block, generator) { +export function lists_reverse(block, generator) { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; // XXX What should the operator precedence be for a `new`? diff --git a/generators/dart/logic.js b/generators/dart/logic.js index 01e4a4756..ab589a133 100644 --- a/generators/dart/logic.js +++ b/generators/dart/logic.js @@ -11,10 +11,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.logic'); -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.forBlock['controls_if'] = function(block, generator) { +export function controls_if(block, generator) { // If/elseif/else condition. let n = 0; let code = '', branchCode, conditionCode; @@ -52,10 +52,9 @@ dartGenerator.forBlock['controls_if'] = function(block, generator) { return code + '\n'; }; -dartGenerator.forBlock['controls_ifelse'] = - dartGenerator.forBlock['controls_if']; +export const controls_ifelse = controls_if; -dartGenerator.forBlock['logic_compare'] = function(block, generator) { +export function logic_compare(block, generator) { // Comparison operator. const OPERATORS = {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; @@ -69,7 +68,7 @@ dartGenerator.forBlock['logic_compare'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['logic_operation'] = function(block, generator) { +export function logic_operation(block, generator) { // Operations 'and', 'or'. const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; const order = @@ -94,7 +93,7 @@ dartGenerator.forBlock['logic_operation'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['logic_negate'] = function(block, generator) { +export function logic_negate(block, generator) { // Negation. const order = Order.UNARY_PREFIX; const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; @@ -102,18 +101,18 @@ dartGenerator.forBlock['logic_negate'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['logic_boolean'] = function(block, generator) { +export function logic_boolean(block, generator) { // Boolean values true and false. const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; return [code, Order.ATOMIC]; }; -dartGenerator.forBlock['logic_null'] = function(block, generator) { +export function logic_null(block, generator) { // Null data type. return ['null', Order.ATOMIC]; }; -dartGenerator.forBlock['logic_ternary'] = function(block, generator) { +export function logic_ternary(block, generator) { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; diff --git a/generators/dart/loops.js b/generators/dart/loops.js index 8951d02df..b7735abf9 100644 --- a/generators/dart/loops.js +++ b/generators/dart/loops.js @@ -11,12 +11,12 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.loops'); -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -dartGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { +export function controls_repeat_ext(block, generator) { let repeats; // Repeat n times. if (block.getField('TIMES')) { @@ -43,10 +43,9 @@ dartGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { return code; }; -dartGenerator.forBlock['controls_repeat'] = - dartGenerator.forBlock['controls_repeat_ext']; +export const controls_repeat = controls_repeat_ext; -dartGenerator.forBlock['controls_whileUntil'] = function(block, generator) { +export function controls_whileUntil(block, generator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = @@ -61,7 +60,7 @@ dartGenerator.forBlock['controls_whileUntil'] = function(block, generator) { return 'while (' + argument0 + ') {\n' + branch + '}\n'; }; -dartGenerator.forBlock['controls_for'] = function(block, generator) { +export function controls_for(block, generator) { // For loop. const variable0 = generator.nameDB_.getName( @@ -127,7 +126,7 @@ dartGenerator.forBlock['controls_for'] = function(block, generator) { return code; }; -dartGenerator.forBlock['controls_forEach'] = function(block, generator) { +export function controls_forEach(block, generator) { // For each loop. const variable0 = generator.nameDB_.getName( @@ -141,7 +140,7 @@ dartGenerator.forBlock['controls_forEach'] = function(block, generator) { return code; }; -dartGenerator.forBlock['controls_flow_statements'] = function(block, generator) { +export function controls_flow_statements(block, generator) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { diff --git a/generators/dart/math.js b/generators/dart/math.js index 2233e51be..3d15d5a2f 100644 --- a/generators/dart/math.js +++ b/generators/dart/math.js @@ -12,12 +12,12 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.math'); import {NameType} from '../../core/names.js'; -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.addReservedWords('Math'); +// RESERVED WORDS: 'Math' -dartGenerator.forBlock['math_number'] = function(block, generator) { +export function math_number(block, generator) { // Numeric value. let code = Number(block.getFieldValue('NUM')); let order; @@ -35,7 +35,7 @@ dartGenerator.forBlock['math_number'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['math_arithmetic'] = function(block, generator) { +export function math_arithmetic(block, generator) { // Basic arithmetic operators, and power. const OPERATORS = { 'ADD': [' + ', Order.ADDITIVE], @@ -61,7 +61,7 @@ dartGenerator.forBlock['math_arithmetic'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['math_single'] = function(block, generator) { +export function math_single(block, generator) { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -146,7 +146,7 @@ dartGenerator.forBlock['math_single'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -dartGenerator.forBlock['math_constant'] = function(block, generator) { +export function math_constant(block, generator) { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. const CONSTANTS = { 'PI': ['Math.pi', Order.UNARY_POSTFIX], @@ -164,7 +164,7 @@ dartGenerator.forBlock['math_constant'] = function(block, generator) { return CONSTANTS[constant]; }; -dartGenerator.forBlock['math_number_property'] = function(block, generator) { +export function math_number_property(block, generator) { // 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 = { @@ -219,7 +219,7 @@ bool ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { return [code, outputOrder]; }; -dartGenerator.forBlock['math_change'] = function(block, generator) { +export function math_change(block, generator) { // Add to a variable in place. const argument0 = generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; @@ -231,11 +231,11 @@ dartGenerator.forBlock['math_change'] = function(block, generator) { }; // Rounding functions have a single operand. -dartGenerator.forBlock['math_round'] = dartGenerator.forBlock['math_single']; +export const math_round = math_single; // Trigonometry functions have a single operand. -dartGenerator.forBlock['math_trig'] = dartGenerator.forBlock['math_single']; +export const math_trig = math_single; -dartGenerator.forBlock['math_on_list'] = function(block, generator) { +export function math_on_list(block, generator) { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; @@ -396,7 +396,7 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['math_modulo'] = function(block, generator) { +export function math_modulo(block, generator) { // Remainder computation. const argument0 = generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; @@ -406,7 +406,7 @@ dartGenerator.forBlock['math_modulo'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -dartGenerator.forBlock['math_constrain'] = function(block, generator) { +export function math_constrain(block, generator) { // Constrain a number between two limits. generator.definitions_['import_dart_math'] = 'import \'dart:math\' as Math;'; @@ -420,7 +420,7 @@ dartGenerator.forBlock['math_constrain'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['math_random_int'] = function(block, generator) { +export function math_random_int(block, generator) { // Random integer between [X] and [Y]. generator.definitions_['import_dart_math'] = 'import \'dart:math\' as Math;'; @@ -441,14 +441,14 @@ int ${generator.FUNCTION_NAME_PLACEHOLDER_}(num a, num b) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['math_random_float'] = function(block, generator) { +export function math_random_float(block, generator) { // Random fraction between 0 and 1. generator.definitions_['import_dart_math'] = 'import \'dart:math\' as Math;'; return ['new Math.Random().nextDouble()', Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['math_atan2'] = function(block, generator) { +export function math_atan2(block, generator) { // Arctangent of point (X, Y) in degrees from -180 to 180. generator.definitions_['import_dart_math'] = 'import \'dart:math\' as Math;'; diff --git a/generators/dart/procedures.js b/generators/dart/procedures.js index 18712f142..c8d61cc81 100644 --- a/generators/dart/procedures.js +++ b/generators/dart/procedures.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.procedures'); import {NameType} from '../../core/names.js'; -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.forBlock['procedures_defreturn'] = function(block, generator) { +export function procedures_defreturn(block, generator) { // Define a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -63,9 +63,9 @@ dartGenerator.forBlock['procedures_defreturn'] = function(block, generator) { // Defining a procedure without a return value uses the same generator as // a procedure with a return value. -dartGenerator.forBlock['procedures_defnoreturn'] = dartGenerator.forBlock['procedures_defreturn']; +export const procedures_defnoreturn = procedures_defreturn; -dartGenerator.forBlock['procedures_callreturn'] = function(block, generator) { +export function procedures_callreturn(block, generator) { // Call a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -79,7 +79,7 @@ dartGenerator.forBlock['procedures_callreturn'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { +export function procedures_callnoreturn(block, generator) { // 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. @@ -87,7 +87,7 @@ dartGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { return tuple[0] + ';\n'; }; -dartGenerator.forBlock['procedures_ifreturn'] = function(block, generator) { +export function procedures_ifreturn(block, generator) { // Conditionally return value from a procedure. const condition = generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; diff --git a/generators/dart/text.js b/generators/dart/text.js index 472c161df..a1dcade42 100644 --- a/generators/dart/text.js +++ b/generators/dart/text.js @@ -12,18 +12,18 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.texts'); import {NameType} from '../../core/names.js'; -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.addReservedWords('Html,Math'); +// RESERVED WORDS: 'Html,Math' -dartGenerator.forBlock['text'] = function(block, generator) { +export function text(block, generator) { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; }; -dartGenerator.forBlock['text_multiline'] = function(block, generator) { +export function text_multiline(block, generator) { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); const order = @@ -31,7 +31,7 @@ dartGenerator.forBlock['text_multiline'] = function(block, generator) { return [code, order]; }; -dartGenerator.forBlock['text_join'] = function(block, generator) { +export function text_join(block, generator) { // Create a string made up of any number of elements of any type. switch (block.itemCount_) { case 0: @@ -54,7 +54,7 @@ dartGenerator.forBlock['text_join'] = function(block, generator) { } }; -dartGenerator.forBlock['text_append'] = function(block, generator) { +export function text_append(block, generator) { // Append to a variable in place. const varName = generator.nameDB_.getName( @@ -63,21 +63,21 @@ dartGenerator.forBlock['text_append'] = function(block, generator) { return varName + ' = [' + varName + ', ' + value + '].join();\n'; }; -dartGenerator.forBlock['text_length'] = function(block, generator) { +export function text_length(block, generator) { // String or array length. const text = generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; return [text + '.length', Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_isEmpty'] = function(block, generator) { +export function text_isEmpty(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; return [text + '.isEmpty', Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_indexOf'] = function(block, generator) { +export function text_indexOf(block, generator) { // Search the text for a substring. const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; @@ -92,7 +92,7 @@ dartGenerator.forBlock['text_indexOf'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_charAt'] = function(block, generator) { +export function text_charAt(block, generator) { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -141,7 +141,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text) { throw Error('Unhandled option (text_charAt).'); }; -dartGenerator.forBlock['text_getSubstring'] = function(block, generator) { +export function text_getSubstring(block, generator) { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); @@ -220,7 +220,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text, String where1, num a return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_changeCase'] = function(block, generator) { +export function text_changeCase(block, generator) { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.toUpperCase()', @@ -257,7 +257,7 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String str) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_trim'] = function(block, generator) { +export function text_trim(block, generator) { // Trim spaces. const OPERATORS = { 'LEFT': '.replaceFirst(new RegExp(r\'^\\s+\'), \'\')', @@ -270,13 +270,13 @@ dartGenerator.forBlock['text_trim'] = function(block, generator) { return [text + operator, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_print'] = function(block, generator) { +export function text_print(block, generator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ');\n'; }; -dartGenerator.forBlock['text_prompt_ext'] = function(block, generator) { +export function text_prompt_ext(block, generator) { // Prompt function. generator.definitions_['import_dart_html'] = 'import \'dart:html\' as Html;'; @@ -298,10 +298,9 @@ dartGenerator.forBlock['text_prompt_ext'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_prompt'] = - dartGenerator.forBlock['text_prompt_ext']; +export const text_prompt = text_prompt_ext; -dartGenerator.forBlock['text_count'] = function(block, generator) { +export function text_count(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; // Substring count is not a native generator function. Define one. @@ -326,7 +325,7 @@ int ${generator.FUNCTION_NAME_PLACEHOLDER_}(String haystack, String needle) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_replace'] = function(block, generator) { +export function text_replace(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; @@ -335,7 +334,7 @@ dartGenerator.forBlock['text_replace'] = function(block, generator) { return [code, Order.UNARY_POSTFIX]; }; -dartGenerator.forBlock['text_reverse'] = function(block, generator) { +export function text_reverse(block, generator) { // There isn't a sensible way to do this in generator. See: // http://stackoverflow.com/a/21613700/3529104 // Implementing something is possibly better than not implementing anything? diff --git a/generators/dart/variables.js b/generators/dart/variables.js index 61a9b97c4..2f628e39c 100644 --- a/generators/dart/variables.js +++ b/generators/dart/variables.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.variables'); import {NameType} from '../../core/names.js'; -import {dartGenerator, Order} from '../dart.js'; +import {Order} from './dart_generator.js'; -dartGenerator.forBlock['variables_get'] = function(block, generator) { +export function variables_get(block, generator) { // Variable getter. const code = generator.nameDB_.getName( @@ -23,7 +23,7 @@ dartGenerator.forBlock['variables_get'] = function(block, generator) { return [code, Order.ATOMIC]; }; -dartGenerator.forBlock['variables_set'] = function(block, generator) { +export function variables_set(block, generator) { // Variable setter. const argument0 = generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; diff --git a/generators/dart/variables_dynamic.js b/generators/dart/variables_dynamic.js index f19320ded..00c2c3cd7 100644 --- a/generators/dart/variables_dynamic.js +++ b/generators/dart/variables_dynamic.js @@ -11,12 +11,9 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Dart.variablesDynamic'); -import {dartGenerator} from '../dart.js'; -import './variables.js'; - // generator is dynamically typed. -dartGenerator.forBlock['variables_get_dynamic'] = - dartGenerator.forBlock['variables_get']; -dartGenerator.forBlock['variables_set_dynamic'] = - dartGenerator.forBlock['variables_set']; +export { + variables_get as variables_get_dynamic, + variables_set as variables_set_dynamic, +} from './variables.js'; diff --git a/generators/javascript.js b/generators/javascript.js index f1afb7d30..ec877c364 100644 --- a/generators/javascript.js +++ b/generators/javascript.js @@ -1,341 +1,40 @@ /** * @license - * Copyright 2012 Google LLC + * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Helper functions for generating JavaScript for blocks. - * @suppress {checkTypes|globalThis} + * @fileoverview Complete helper functions for generating JavaScript for + * blocks. This is the entrypoint for javascript_compressed.js. + * @suppress {extraRequire} */ import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.JavaScript'); +goog.declareModuleId('Blockly.JavaScript.all'); -import * as Variables from '../core/variables.js'; -import * as stringUtils from '../core/utils/string.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 {inputTypes} from '../core/inputs/input_types.js'; +import {JavascriptGenerator} from './javascript/javascript_generator.js'; +import * as colour from './javascript/colour.js'; +import * as lists from './javascript/lists.js'; +import * as logic from './javascript/logic.js'; +import * as loops from './javascript/loops.js'; +import * as math from './javascript/math.js'; +import * as procedures from './javascript/procedures.js'; +import * as text from './javascript/text.js'; +import * as variables from './javascript/variables.js'; +import * as variablesDynamic from './javascript/variables_dynamic.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, // (...) -}; - -/** - * JavaScript code generator class. - */ -export class JavascriptGenerator extends CodeGenerator { - /** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} - */ - ORDER_OVERRIDES = [ - // (foo()).bar -> foo().bar - // (foo())[0] -> foo()[0] - [Order.FUNCTION_CALL, Order.MEMBER], - // (foo())() -> foo()() - [Order.FUNCTION_CALL, Order.FUNCTION_CALL], - // (foo.bar).baz -> foo.bar.baz - // (foo.bar)[0] -> foo.bar[0] - // (foo[0]).bar -> foo[0].bar - // (foo[0])[1] -> foo[0][1] - [Order.MEMBER, Order.MEMBER], - // (foo.bar)() -> foo.bar() - // (foo[0])() -> foo[0]() - [Order.MEMBER, Order.FUNCTION_CALL], - - // !(!foo) -> !!foo - [Order.LOGICAL_NOT, Order.LOGICAL_NOT], - // a * (b * c) -> a * b * c - [Order.MULTIPLICATION, Order.MULTIPLICATION], - // a + (b + c) -> a + b + c - [Order.ADDITION, Order.ADDITION], - // a && (b && c) -> a && b && c - [Order.LOGICAL_AND, Order.LOGICAL_AND], - // a || (b || c) -> a || b || c - [Order.LOGICAL_OR, Order.LOGICAL_OR] - ]; - - constructor(name) { - super(name ?? 'JavaScript'); - this.isInitialized = false; - - // Copy Order values onto instance for backwards compatibility - // while ensuring they are not part of the publically-advertised - // API. - // - // TODO(#7085): deprecate these in due course. (Could initially - // replace data properties with get accessors that call - // deprecate.warn().) - for (const key in Order) { - this['ORDER_' + key] = Order[key]; - } - - // 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. - 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,' + - 'else,export,extends,finally,for,function,if,import,in,instanceof,' + - 'new,return,super,switch,this,throw,try,typeof,var,void,' + - 'while,with,yield,' + - 'enum,' + - 'implements,interface,let,package,private,protected,public,static,' + - 'await,' + - 'null,true,false,' + - // Magic variable. - 'arguments,' + - // Everything in the current environment (835 items in Chrome, - // 104 in Node). - Object.getOwnPropertyNames(globalThis).join(',') - ); - } - - /** - * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. - */ - init(workspace) { - super.init(workspace); - - if (!this.nameDB_) { - this.nameDB_ = new Names(this.RESERVED_WORDS_); - } else { - this.nameDB_.reset(); - } - - this.nameDB_.setVariableMap(workspace.getVariableMap()); - this.nameDB_.populateVariables(workspace); - this.nameDB_.populateProcedures(workspace); - - const defvars = []; - // Add developer variables (not created or named by the user). - const devVarList = Variables.allDeveloperVariables(workspace); - for (let i = 0; i < devVarList.length; i++) { - defvars.push( - 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)); - } - - // Declare all of the variables. - if (defvars.length) { - this.definitions_['variables'] = 'var ' + defvars.join(', ') + ';'; - } - this.isInitialized = true; - } - - /** - * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ - finish(code) { - // 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(); - 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. - */ - scrubNakedValue(line) { - 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. - * @protected - */ - quote_(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 + '\''; - } - - /** - * Encode a string as a properly escaped multiline JavaScript string, complete - * with quotes. - * @param {string} string Text to encode. - * @return {string} JavaScript string. - * @protected - */ - multiline_quote_(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'); - } - - /** - * 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. - * @protected - */ - scrub_(block, code, opt_thisOnly) { - let commentCode = ''; - // Only collect comments for blocks that aren't inline. - if (!block.outputConnection || !block.outputConnection.targetConnection) { - // Collect comment for this block. - let comment = block.getCommentText(); - if (comment) { - comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - commentCode += this.prefixLines(comment + '\n', '// '); - } - // Collect comments for all value arguments. - // 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(); - if (childBlock) { - comment = this.allNestedComments(childBlock); - if (comment) { - commentCode += this.prefixLines(comment, '// '); - } - } - } - } - } - const nextBlock = - block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_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} - */ - getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { - let delta = opt_delta || 0; - let order = opt_order || this.ORDER_NONE; - if (block.workspace.options.oneBasedIndex) { - delta--; - } - const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - - let innerOrder; - let outerOrder = order; - if (delta > 0) { - outerOrder = this.ORDER_ADDITION; - innerOrder = this.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; - } - - let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; - - 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 + ')'; - } - } - return at; - } -} +export * from './javascript/javascript_generator.js'; /** * JavaScript code generator instance. * @type {!JavascriptGenerator} */ export const javascriptGenerator = new JavascriptGenerator(); + +// Install per-block-type generator functions: +Object.assign( + javascriptGenerator.forBlock, + colour, lists, logic, loops, math, procedures, + text, variables, variablesDynamic +); diff --git a/generators/javascript/all.js b/generators/javascript/all.js deleted file mode 100644 index 430a8d86f..000000000 --- a/generators/javascript/all.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Complete helper functions for generating JavaScript for - * blocks. This is the entrypoint for javascript_compressed.js. - * @suppress {extraRequire} - */ - -import * as goog from '../../closure/goog/goog.js'; -goog.declareModuleId('Blockly.JavaScript.all'); - -import './colour.js'; -import './lists.js'; -import './logic.js'; -import './loops.js'; -import './math.js'; -import './procedures.js'; -import './text.js'; -import './variables.js'; -import './variables_dynamic.js'; - -export * from '../javascript.js'; diff --git a/generators/javascript/colour.js b/generators/javascript/colour.js index 3e0ab2882..d8976d3e9 100644 --- a/generators/javascript/colour.js +++ b/generators/javascript/colour.js @@ -11,16 +11,16 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.colour'); -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['colour_picker'] = function(block, generator) { +export function colour_picker(block, generator) { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; }; -javascriptGenerator.forBlock['colour_random'] = function(block, generator) { +export function colour_random(block, generator) { // Generate a random colour. const functionName = generator.provideFunction_('colourRandom', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { @@ -32,7 +32,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['colour_rgb'] = function(block, generator) { +export function colour_rgb(block, generator) { // Compose a colour from RGB components expressed as percentages. const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; const green = @@ -54,7 +54,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['colour_blend'] = function(block, generator) { +export function colour_blend(block, generator) { // Blend two colours together. const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; diff --git a/generators/javascript/javascript_generator.js b/generators/javascript/javascript_generator.js new file mode 100644 index 000000000..18811011c --- /dev/null +++ b/generators/javascript/javascript_generator.js @@ -0,0 +1,335 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helper functions for generating JavaScript for blocks. + * @suppress {checkTypes|globalThis} + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('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 {CodeGenerator} from '../../core/generator.js'; +import {Names, NameType} from '../../core/names.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, // (...) +}; + +/** + * JavaScript code generator class. + */ +export class JavascriptGenerator extends CodeGenerator { + /** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array>} + */ + ORDER_OVERRIDES = [ + // (foo()).bar -> foo().bar + // (foo())[0] -> foo()[0] + [Order.FUNCTION_CALL, Order.MEMBER], + // (foo())() -> foo()() + [Order.FUNCTION_CALL, Order.FUNCTION_CALL], + // (foo.bar).baz -> foo.bar.baz + // (foo.bar)[0] -> foo.bar[0] + // (foo[0]).bar -> foo[0].bar + // (foo[0])[1] -> foo[0][1] + [Order.MEMBER, Order.MEMBER], + // (foo.bar)() -> foo.bar() + // (foo[0])() -> foo[0]() + [Order.MEMBER, Order.FUNCTION_CALL], + + // !(!foo) -> !!foo + [Order.LOGICAL_NOT, Order.LOGICAL_NOT], + // a * (b * c) -> a * b * c + [Order.MULTIPLICATION, Order.MULTIPLICATION], + // a + (b + c) -> a + b + c + [Order.ADDITION, Order.ADDITION], + // a && (b && c) -> a && b && c + [Order.LOGICAL_AND, Order.LOGICAL_AND], + // a || (b || c) -> a || b || c + [Order.LOGICAL_OR, Order.LOGICAL_OR] + ]; + + constructor(name) { + super(name ?? 'JavaScript'); + this.isInitialized = false; + + // Copy Order values onto instance for backwards compatibility + // while ensuring they are not part of the publically-advertised + // API. + // + // TODO(#7085): deprecate these in due course. (Could initially + // replace data properties with get accessors that call + // deprecate.warn().) + for (const key in Order) { + this['ORDER_' + key] = Order[key]; + } + + // 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. + 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,' + + 'else,export,extends,finally,for,function,if,import,in,instanceof,' + + 'new,return,super,switch,this,throw,try,typeof,var,void,' + + 'while,with,yield,' + + 'enum,' + + 'implements,interface,let,package,private,protected,public,static,' + + 'await,' + + 'null,true,false,' + + // Magic variable. + 'arguments,' + + // Everything in the current environment (835 items in Chrome, + // 104 in Node). + Object.getOwnPropertyNames(globalThis).join(',') + ); + } + + /** + * Initialise the database of variable names. + * @param {!Workspace} workspace Workspace to generate code from. + */ + init(workspace) { + super.init(workspace); + + if (!this.nameDB_) { + this.nameDB_ = new Names(this.RESERVED_WORDS_); + } else { + this.nameDB_.reset(); + } + + this.nameDB_.setVariableMap(workspace.getVariableMap()); + this.nameDB_.populateVariables(workspace); + this.nameDB_.populateProcedures(workspace); + + const defvars = []; + // Add developer variables (not created or named by the user). + const devVarList = Variables.allDeveloperVariables(workspace); + for (let i = 0; i < devVarList.length; i++) { + defvars.push( + 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)); + } + + // Declare all of the variables. + if (defvars.length) { + this.definitions_['variables'] = 'var ' + defvars.join(', ') + ';'; + } + this.isInitialized = true; + } + + /** + * Prepend the generated code with the variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // 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(); + 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. + */ + scrubNakedValue(line) { + 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. + * @protected + */ + quote_(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 + '\''; + } + + /** + * Encode a string as a properly escaped multiline JavaScript string, complete + * with quotes. + * @param {string} string Text to encode. + * @return {string} JavaScript string. + * @protected + */ + multiline_quote_(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'); + } + + /** + * 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. + * @protected + */ + scrub_(block, code, opt_thisOnly) { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + commentCode += this.prefixLines(comment + '\n', '// '); + } + // Collect comments for all value arguments. + // 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(); + if (childBlock) { + comment = this.allNestedComments(childBlock); + if (comment) { + commentCode += this.prefixLines(comment, '// '); + } + } + } + } + } + const nextBlock = + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = opt_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} + */ + getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { + let delta = opt_delta || 0; + let order = opt_order || this.ORDER_NONE; + if (block.workspace.options.oneBasedIndex) { + delta--; + } + const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + + let innerOrder; + let outerOrder = order; + if (delta > 0) { + outerOrder = this.ORDER_ADDITION; + innerOrder = this.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; + } + + let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; + + 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 + ')'; + } + } + return at; + } +} diff --git a/generators/javascript/lists.js b/generators/javascript/lists.js index ff9041ea2..3106906b0 100644 --- a/generators/javascript/lists.js +++ b/generators/javascript/lists.js @@ -13,15 +13,15 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.lists'); import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['lists_create_empty'] = function(block, generator) { +export function lists_create_empty(block, generator) { // Create an empty list. return ['[]', Order.ATOMIC]; }; -javascriptGenerator.forBlock['lists_create_with'] = function(block, generator) { +export function lists_create_with(block, generator) { // 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++) { @@ -33,7 +33,7 @@ javascriptGenerator.forBlock['lists_create_with'] = function(block, generator) { return [code, Order.ATOMIC]; }; -javascriptGenerator.forBlock['lists_repeat'] = function(block, generator) { +export function lists_repeat(block, generator) { // Create a list with one element repeated. const functionName = generator.provideFunction_('listsRepeat', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { @@ -52,21 +52,21 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['lists_length'] = function(block, generator) { +export function lists_length(block, generator) { // String or array length. const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return [list + '.length', Order.MEMBER]; }; -javascriptGenerator.forBlock['lists_isEmpty'] = function(block, generator) { +export function lists_isEmpty(block, generator) { // Is the string null or array empty? const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return ['!' + list + '.length', Order.LOGICAL_NOT]; }; -javascriptGenerator.forBlock['lists_indexOf'] = function(block, generator) { +export function lists_indexOf(block, generator) { // Find an item in the list. const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; @@ -81,7 +81,7 @@ javascriptGenerator.forBlock['lists_indexOf'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['lists_getIndex'] = function(block, generator) { +export function lists_getIndex(block, generator) { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -164,7 +164,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { throw Error('Unhandled combination (lists_getIndex).'); }; -javascriptGenerator.forBlock['lists_setIndex'] = function(block, generator) { +export function lists_setIndex(block, generator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. let list = @@ -266,7 +266,7 @@ const getSubstringIndex = function(listName, where, opt_at) { } }; -javascriptGenerator.forBlock['lists_getSublist'] = function(block, generator) { +export function lists_getSublist(block, generator) { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; @@ -346,7 +346,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['lists_sort'] = function(block, generator) { +export function lists_sort(block, generator) { // Block for sorting a list. const list = generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || @@ -375,7 +375,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { ]; }; -javascriptGenerator.forBlock['lists_split'] = function(block, generator) { +export function lists_split(block, generator) { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.MEMBER); const delimiter = @@ -399,7 +399,7 @@ javascriptGenerator.forBlock['lists_split'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['lists_reverse'] = function(block, generator) { +export function lists_reverse(block, generator) { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || diff --git a/generators/javascript/logic.js b/generators/javascript/logic.js index 2070e1e04..8cbdc1b5e 100644 --- a/generators/javascript/logic.js +++ b/generators/javascript/logic.js @@ -11,10 +11,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.logic'); -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['controls_if'] = function(block, generator) { +export function controls_if(block, generator) { // If/elseif/else condition. let n = 0; let code = ''; @@ -54,10 +54,9 @@ javascriptGenerator.forBlock['controls_if'] = function(block, generator) { return code + '\n'; }; -javascriptGenerator.forBlock['controls_ifelse'] = - javascriptGenerator.forBlock['controls_if']; +export const controls_ifelse = controls_if; -javascriptGenerator.forBlock['logic_compare'] = function(block, generator) { +export function logic_compare(block, generator) { // Comparison operator. const OPERATORS = {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; @@ -71,7 +70,7 @@ javascriptGenerator.forBlock['logic_compare'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['logic_operation'] = function(block, generator) { +export function logic_operation(block, generator) { // Operations 'and', 'or'. const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; const order = (operator === '&&') ? Order.LOGICAL_AND : @@ -96,7 +95,7 @@ javascriptGenerator.forBlock['logic_operation'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['logic_negate'] = function(block, generator) { +export function logic_negate(block, generator) { // Negation. const order = Order.LOGICAL_NOT; const argument0 = @@ -105,18 +104,18 @@ javascriptGenerator.forBlock['logic_negate'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['logic_boolean'] = function(block, generator) { +export function logic_boolean(block, generator) { // Boolean values true and false. const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; return [code, Order.ATOMIC]; }; -javascriptGenerator.forBlock['logic_null'] = function(block, generator) { +export function logic_null(block, generator) { // Null data type. return ['null', Order.ATOMIC]; }; -javascriptGenerator.forBlock['logic_ternary'] = function(block, generator) { +export function logic_ternary(block, generator) { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.CONDITIONAL) || diff --git a/generators/javascript/loops.js b/generators/javascript/loops.js index cafbd3a50..c93438b56 100644 --- a/generators/javascript/loops.js +++ b/generators/javascript/loops.js @@ -13,10 +13,10 @@ goog.declareModuleId('Blockly.JavaScript.loops'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { +export function controls_repeat_ext(block, generator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -45,10 +45,9 @@ javascriptGenerator.forBlock['controls_repeat_ext'] = function(block, generator) return code; }; -javascriptGenerator.forBlock['controls_repeat'] = - javascriptGenerator.forBlock['controls_repeat_ext']; +export const controls_repeat = controls_repeat_ext; -javascriptGenerator.forBlock['controls_whileUntil'] = function(block, generator) { +export function controls_whileUntil(block, generator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = @@ -64,7 +63,7 @@ javascriptGenerator.forBlock['controls_whileUntil'] = function(block, generator) return 'while (' + argument0 + ') {\n' + branch + '}\n'; }; -javascriptGenerator.forBlock['controls_for'] = function(block, generator) { +export function controls_for(block, generator) { // For loop. const variable0 = generator.nameDB_.getName( @@ -127,7 +126,7 @@ javascriptGenerator.forBlock['controls_for'] = function(block, generator) { return code; }; -javascriptGenerator.forBlock['controls_forEach'] = function(block, generator) { +export function controls_forEach(block, generator) { // For each loop. const variable0 = generator.nameDB_.getName( @@ -153,7 +152,7 @@ javascriptGenerator.forBlock['controls_forEach'] = function(block, generator) { return code; }; -javascriptGenerator.forBlock['controls_flow_statements'] = function(block, generator) { +export function controls_flow_statements(block, generator) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { diff --git a/generators/javascript/math.js b/generators/javascript/math.js index 7089f8ac3..ecba63523 100644 --- a/generators/javascript/math.js +++ b/generators/javascript/math.js @@ -13,10 +13,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.math'); import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['math_number'] = function(block, generator) { +export function math_number(block, generator) { // Numeric value. const code = Number(block.getFieldValue('NUM')); const order = code >= 0 ? Order.ATOMIC : @@ -24,7 +24,7 @@ javascriptGenerator.forBlock['math_number'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['math_arithmetic'] = function(block, generator) { +export function math_arithmetic(block, generator) { // Basic arithmetic operators, and power. const OPERATORS = { 'ADD': [' + ', Order.ADDITION], @@ -48,7 +48,7 @@ javascriptGenerator.forBlock['math_arithmetic'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['math_single'] = function(block, generator) { +export function math_single(block, generator) { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -132,7 +132,7 @@ javascriptGenerator.forBlock['math_single'] = function(block, generator) { return [code, Order.DIVISION]; }; -javascriptGenerator.forBlock['math_constant'] = function(block, generator) { +export function math_constant(block, generator) { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. const CONSTANTS = { 'PI': ['Math.PI', Order.MEMBER], @@ -145,7 +145,7 @@ javascriptGenerator.forBlock['math_constant'] = function(block, generator) { return CONSTANTS[block.getFieldValue('CONSTANT')]; }; -javascriptGenerator.forBlock['math_number_property'] = function(block, generator) { +export function math_number_property(block, generator) { // 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 = { @@ -199,7 +199,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { return [code, outputOrder]; }; -javascriptGenerator.forBlock['math_change'] = function(block, generator) { +export function math_change(block, generator) { // Add to a variable in place. const argument0 = generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; @@ -210,13 +210,11 @@ javascriptGenerator.forBlock['math_change'] = function(block, generator) { }; // Rounding functions have a single operand. -javascriptGenerator.forBlock['math_round'] = - javascriptGenerator.forBlock['math_single']; +export const math_round = math_single; // Trigonometry functions have a single operand. -javascriptGenerator.forBlock['math_trig'] = - javascriptGenerator.forBlock['math_single']; +export const math_trig = math_single; -javascriptGenerator.forBlock['math_on_list'] = function(block, generator) { +export function math_on_list(block, generator) { // Math functions for lists. const func = block.getFieldValue('OP'); let list; @@ -346,7 +344,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['math_modulo'] = function(block, generator) { +export function math_modulo(block, generator) { // Remainder computation. const argument0 = generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; @@ -356,7 +354,7 @@ javascriptGenerator.forBlock['math_modulo'] = function(block, generator) { return [code, Order.MODULUS]; }; -javascriptGenerator.forBlock['math_constrain'] = function(block, generator) { +export function math_constrain(block, generator) { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; @@ -369,7 +367,7 @@ javascriptGenerator.forBlock['math_constrain'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['math_random_int'] = function(block, generator) { +export function math_random_int(block, generator) { // Random integer between [X] and [Y]. const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; @@ -390,12 +388,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(a, b) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['math_random_float'] = function(block, generator) { +export function math_random_float(block, generator) { // Random fraction between 0 and 1. return ['Math.random()', Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['math_atan2'] = function(block, generator) { +export function math_atan2(block, generator) { // Arctangent of point (X, Y) in degrees from -180 to 180. const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; diff --git a/generators/javascript/procedures.js b/generators/javascript/procedures.js index 1dae9c327..baa0ebe48 100644 --- a/generators/javascript/procedures.js +++ b/generators/javascript/procedures.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.procedures'); import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['procedures_defreturn'] = function(block, generator) { +export function procedures_defreturn(block, generator) { // Define a procedure with a return value. const funcName = generator.nameDB_.getName( block.getFieldValue('NAME'), NameType.PROCEDURE); @@ -65,10 +65,9 @@ javascriptGenerator.forBlock['procedures_defreturn'] = function(block, generator // Defining a procedure without a return value uses the same generator as // a procedure with a return value. -javascriptGenerator.forBlock['procedures_defnoreturn'] = - javascriptGenerator.forBlock['procedures_defreturn']; +export const procedures_defnoreturn = procedures_defreturn; -javascriptGenerator.forBlock['procedures_callreturn'] = function(block, generator) { +export function procedures_callreturn(block, generator) { // Call a procedure with a return value. const funcName = generator.nameDB_.getName( block.getFieldValue('NAME'), NameType.PROCEDURE); @@ -82,7 +81,7 @@ javascriptGenerator.forBlock['procedures_callreturn'] = function(block, generato return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { +export function procedures_callnoreturn(block, generator) { // 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. @@ -90,7 +89,7 @@ javascriptGenerator.forBlock['procedures_callnoreturn'] = function(block, genera return tuple[0] + ';\n'; }; -javascriptGenerator.forBlock['procedures_ifreturn'] = function(block, generator) { +export function procedures_ifreturn(block, generator) { // Conditionally return value from a procedure. const condition = generator.valueToCode(block, 'CONDITION', Order.NONE) || diff --git a/generators/javascript/text.js b/generators/javascript/text.js index e4da2b9df..e3761293b 100644 --- a/generators/javascript/text.js +++ b/generators/javascript/text.js @@ -12,7 +12,7 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.texts'); import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; /** @@ -53,13 +53,13 @@ const getSubstringIndex = function(stringName, where, opt_at) { } }; -javascriptGenerator.forBlock['text'] = function(block, generator) { +export function text(block, generator) { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; }; -javascriptGenerator.forBlock['text_multiline'] = function(block, generator) { +export function text_multiline(block, generator) { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); @@ -68,7 +68,7 @@ javascriptGenerator.forBlock['text_multiline'] = function(block, generator) { return [code, order]; }; -javascriptGenerator.forBlock['text_join'] = function(block, generator) { +export function text_join(block, generator) { // Create a string made up of any number of elements of any type. switch (block.itemCount_) { case 0: @@ -100,7 +100,7 @@ javascriptGenerator.forBlock['text_join'] = function(block, generator) { } }; -javascriptGenerator.forBlock['text_append'] = function(block, generator) { +export function text_append(block, generator) { // Append to a variable in place. const varName = generator.nameDB_.getName( block.getFieldValue('VAR'), NameType.VARIABLE); @@ -111,21 +111,21 @@ javascriptGenerator.forBlock['text_append'] = function(block, generator) { return code; }; -javascriptGenerator.forBlock['text_length'] = function(block, generator) { +export function text_length(block, generator) { // String or array length. const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return [text + '.length', Order.MEMBER]; }; -javascriptGenerator.forBlock['text_isEmpty'] = function(block, generator) { +export function text_isEmpty(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return ['!' + text + '.length', Order.LOGICAL_NOT]; }; -javascriptGenerator.forBlock['text_indexOf'] = function(block, generator) { +export function text_indexOf(block, generator) { // Search the text for a substring. const operator = block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; @@ -141,7 +141,7 @@ javascriptGenerator.forBlock['text_indexOf'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_charAt'] = function(block, generator) { +export function text_charAt(block, generator) { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -184,7 +184,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text) { throw Error('Unhandled option (text_charAt).'); }; -javascriptGenerator.forBlock['text_getSubstring'] = function(block, generator) { +export function text_getSubstring(block, generator) { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); @@ -263,7 +263,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_changeCase'] = function(block, generator) { +export function text_changeCase(block, generator) { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.toUpperCase()', @@ -292,7 +292,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_trim'] = function(block, generator) { +export function text_trim(block, generator) { // Trim spaces. const OPERATORS = { 'LEFT': ".replace(/^[\\s\\xa0]+/, '')", @@ -305,14 +305,14 @@ javascriptGenerator.forBlock['text_trim'] = function(block, generator) { return [text + operator, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_print'] = function(block, generator) { +export function text_print(block, generator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'window.alert(' + msg + ');\n'; }; -javascriptGenerator.forBlock['text_prompt_ext'] = function(block, generator) { +export function text_prompt_ext(block, generator) { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -330,10 +330,9 @@ javascriptGenerator.forBlock['text_prompt_ext'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_prompt'] = - javascriptGenerator.forBlock['text_prompt_ext']; +export const text_prompt = text_prompt_ext; -javascriptGenerator.forBlock['text_count'] = function(block, generator) { +export function text_count(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const sub = generator.valueToCode(block, 'SUB', @@ -351,7 +350,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) { return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_replace'] = function(block, generator) { +export function text_replace(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const from = generator.valueToCode(block, 'FROM', @@ -370,7 +369,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) return [code, Order.FUNCTION_CALL]; }; -javascriptGenerator.forBlock['text_reverse'] = function(block, generator) { +export function text_reverse(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const code = text + ".split('').reverse().join('')"; diff --git a/generators/javascript/variables.js b/generators/javascript/variables.js index e02197ed1..e67ce645b 100644 --- a/generators/javascript/variables.js +++ b/generators/javascript/variables.js @@ -12,17 +12,17 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.variables'); import {NameType} from '../../core/names.js'; -import {Order, javascriptGenerator} from '../javascript.js'; +import {Order} from './javascript_generator.js'; -javascriptGenerator.forBlock['variables_get'] = function(block, generator) { +export function variables_get(block, generator) { // Variable getter. const code = generator.nameDB_.getName(block.getFieldValue('VAR'), NameType.VARIABLE); return [code, Order.ATOMIC]; }; -javascriptGenerator.forBlock['variables_set'] = function(block, generator) { +export function variables_set(block, generator) { // Variable setter. const argument0 = generator.valueToCode( block, 'VALUE', Order.ASSIGNMENT) || '0'; diff --git a/generators/javascript/variables_dynamic.js b/generators/javascript/variables_dynamic.js index f37bff49d..aaae27311 100644 --- a/generators/javascript/variables_dynamic.js +++ b/generators/javascript/variables_dynamic.js @@ -11,11 +11,9 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.JavaScript.variablesDynamic'); -import {javascriptGenerator} from '../javascript.js'; -import './variables.js'; // JavaScript is dynamically typed. -javascriptGenerator.forBlock['variables_get_dynamic'] = - javascriptGenerator.forBlock['variables_get']; -javascriptGenerator.forBlock['variables_set_dynamic'] = - javascriptGenerator.forBlock['variables_set']; +export { + variables_get as variables_get_dynamic, + variables_set as variables_set_dynamic, +} from './variables.js'; diff --git a/generators/lua.js b/generators/lua.js index 391a0ca87..349ea2431 100644 --- a/generators/lua.js +++ b/generators/lua.js @@ -1,219 +1,40 @@ /** * @license - * Copyright 2016 Google LLC + * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Helper functions for generating Lua for blocks. - * Based on Ellen Spertus's blocky-lua project. - * @suppress {checkTypes|globalThis} + * @fileoverview Complete helper functions for generating Lua for + * blocks. This is the entrypoint for lua_compressed.js. + * @suppress {extraRequire} */ import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Lua'); +goog.declareModuleId('Blockly.Lua.all'); -import * as stringUtils from '../core/utils/string.js'; -// import type {Block} from '../core/block.js'; -import {CodeGenerator} from '../core/generator.js'; -import {Names} from '../core/names.js'; -// import type {Workspace} from '../core/workspace.js'; -import {inputTypes} from '../core/inputs/input_types.js'; +import {LuaGenerator} from './lua/lua_generator.js'; +import * as colour from './lua/colour.js'; +import * as lists from './lua/lists.js'; +import * as logic from './lua/logic.js'; +import * as loops from './lua/loops.js'; +import * as math from './lua/math.js'; +import * as procedures from './lua/procedures.js'; +import * as text from './lua/text.js'; +import * as variables from './lua/variables.js'; +import * as variablesDynamic from './lua/variables_dynamic.js'; +export * from './lua/lua_generator.js'; /** - * Order of operation ENUMs. - * http://www.lua.org/manual/5.3/manual.html#3.4.8 - * @enum {number} - */ -export const Order = { - ATOMIC: 0, // literals - // The next level was not explicit in documentation and inferred by Ellen. - HIGH: 1, // Function calls, tables[] - EXPONENTIATION: 2, // ^ - UNARY: 3, // not # - ~ - MULTIPLICATIVE: 4, // * / % - ADDITIVE: 5, // + - - CONCATENATION: 6, // .. - RELATIONAL: 7, // < > <= >= ~= == - AND: 8, // and - OR: 9, // or - NONE: 99, -}; - -/** - * Lua code generator class. - * - * Note: Lua is not supporting zero-indexing since the language itself is - * one-indexed, so the generator does not repoct the oneBasedIndex configuration - * option used for lists and text. - */ -class LuaGenerator extends CodeGenerator { - constructor(name) { - super(name ?? 'Lua'); - this.isInitialized = false; - - // Copy Order values onto instance for backwards compatibility - // while ensuring they are not part of the publically-advertised - // API. - // - // TODO(#7085): deprecate these in due course. (Could initially - // replace data properties with get accessors that call - // deprecate.warn().) - for (const key in Order) { - this['ORDER_' + key] = Order[key]; - } - - // 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. - this.addReservedWords( - // Special character - '_,' + - // From theoriginalbit's script: - // https://github.com/espertus/blockly-lua/issues/6 - '__inext,assert,bit,colors,colours,coroutine,disk,dofile,error,fs,' + - 'fetfenv,getmetatable,gps,help,io,ipairs,keys,loadfile,loadstring,math,' + - 'native,next,os,paintutils,pairs,parallel,pcall,peripheral,print,' + - 'printError,rawequal,rawget,rawset,read,rednet,redstone,rs,select,' + - 'setfenv,setmetatable,sleep,string,table,term,textutils,tonumber,' + - 'tostring,turtle,type,unpack,vector,write,xpcall,_VERSION,__indext,' + - // Not included in the script, probably because it wasn't enabled: - 'HTTP,' + - // Keywords (http://www.lua.org/pil/1.3.html). - 'and,break,do,else,elseif,end,false,for,function,if,in,local,nil,not,' + - 'or,repeat,return,then,true,until,while,' + - // Metamethods (http://www.lua.org/manual/5.2/manual.html). - 'add,sub,mul,div,mod,pow,unm,concat,len,eq,lt,le,index,newindex,call,' + - // Basic functions (http://www.lua.org/manual/5.2/manual.html, - // section 6.1). - 'assert,collectgarbage,dofile,error,_G,getmetatable,inpairs,load,' + - 'loadfile,next,pairs,pcall,print,rawequal,rawget,rawlen,rawset,select,' + - 'setmetatable,tonumber,tostring,type,_VERSION,xpcall,' + - // Modules (http://www.lua.org/manual/5.2/manual.html, section 6.3). - 'require,package,string,table,math,bit32,io,file,os,debug' - ); - } - - /** - * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. - */ - init(workspace) { - // Call Blockly.CodeGenerator's init. - super.init(); - - if (!this.nameDB_) { - this.nameDB_ = new Names(this.RESERVED_WORDS_); - } else { - this.nameDB_.reset(); - } - this.nameDB_.setVariableMap(workspace.getVariableMap()); - this.nameDB_.populateVariables(workspace); - this.nameDB_.populateProcedures(workspace); - - this.isInitialized = true; - }; - - /** - * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ - finish(code) { - // Convert the definitions dictionary into a list. - const definitions = Object.values(this.definitions_); - // Call Blockly.CodeGenerator's finish. - code = super.finish(code); - this.isInitialized = false; - - 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. In Lua, an expression is not a legal statement, so we must assign - * the value to the (conventionally ignored) _. - * http://lua-users.org/wiki/ExpressionsAsStatements - * @param {string} line Line of generated code. - * @return {string} Legal line of code. - */ - scrubNakedValue(line) { - return 'local _ = ' + line + '\n'; - }; - - /** - * Encode a string as a properly escaped Lua string, complete with - * quotes. - * @param {string} string Text to encode. - * @return {string} Lua string. - * @protected - */ - quote_(string) { - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; - }; - - /** - * Encode a string as a properly escaped multiline Lua string, complete with - * quotes. - * @param {string} string Text to encode. - * @return {string} Lua string. - * @protected - */ - multiline_quote_(string) { - const lines = string.split(/\n/g).map(this.quote_); - // Join with the following, plus a newline: - // .. '\n' .. - return lines.join(' .. \'\\n\' ..\n'); - }; - - /** - * Common tasks for generating Lua 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 Lua code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this statement. - * @return {string} Lua code with comments and subsequent blocks added. - * @protected - */ - scrub_(block, code, opt_thisOnly) { - let commentCode = ''; - // Only collect comments for blocks that aren't inline. - if (!block.outputConnection || !block.outputConnection.targetConnection) { - // Collect comment for this block. - let comment = block.getCommentText(); - if (comment) { - comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - commentCode += this.prefixLines(comment, '-- ') + '\n'; - } - // Collect comments for all value arguments. - // 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(); - if (childBlock) { - comment = this.allNestedComments(childBlock); - if (comment) { - commentCode += this.prefixLines(comment, '-- '); - } - } - } - } - } - const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); - return commentCode + code + nextCode; - }; -} - -/** - * Lua code generator. + * Lua code generator instance. * @type {!LuaGenerator} */ -export const luaGenerator = new LuaGenerator('Lua'); +export const luaGenerator = new LuaGenerator(); + +// Install per-block-type generator functions: +Object.assign( + luaGenerator.forBlock, + colour, lists, logic, loops, math, procedures, + text, variables, variablesDynamic +); diff --git a/generators/lua/all.js b/generators/lua/all.js deleted file mode 100644 index 0607d5c3e..000000000 --- a/generators/lua/all.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Complete helper functions for generating Lua for - * blocks. This is the entrypoint for lua_compressed.js. - * @suppress {extraRequire} - */ - -import * as goog from '../../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Lua.all'); - -import './colour.js'; -import './lists.js'; -import './logic.js'; -import './loops.js'; -import './math.js'; -import './procedures.js'; -import './text.js'; -import './variables.js'; -import './variables_dynamic.js'; - -export * from '../lua.js'; diff --git a/generators/lua/colour.js b/generators/lua/colour.js index 85b081817..a69dd41c6 100644 --- a/generators/lua/colour.js +++ b/generators/lua/colour.js @@ -11,22 +11,22 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.colour'); -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['colour_picker'] = function(block, generator) { +export function colour_picker(block, generator) { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; }; -luaGenerator.forBlock['colour_random'] = function(block, generator) { +export function colour_random(block, generator) { // Generate a random colour. const code = 'string.format("#%06x", math.random(0, 2^24 - 1))'; return [code, Order.HIGH]; }; -luaGenerator.forBlock['colour_rgb'] = function(block, generator) { +export function colour_rgb(block, generator) { // Compose a colour from RGB components expressed as percentages. const functionName = generator.provideFunction_('colour_rgb', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) @@ -43,7 +43,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['colour_blend'] = function(block, generator) { +export function colour_blend(block, generator) { // Blend two colours together. const functionName = generator.provideFunction_('colour_blend', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio) diff --git a/generators/lua/lists.js b/generators/lua/lists.js index dac1806b9..8643911f3 100644 --- a/generators/lua/lists.js +++ b/generators/lua/lists.js @@ -12,15 +12,15 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.lists'); import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['lists_create_empty'] = function(block, generator) { +export function lists_create_empty(block, generator) { // Create an empty list. return ['{}', Order.HIGH]; }; -luaGenerator.forBlock['lists_create_with'] = function(block, generator) { +export function lists_create_with(block, generator) { // 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++) { @@ -31,7 +31,7 @@ luaGenerator.forBlock['lists_create_with'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['lists_repeat'] = function(block, generator) { +export function lists_repeat(block, generator) { // Create a list with one element repeated. const functionName = generator.provideFunction_('create_list_repeated', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(item, count) @@ -48,20 +48,20 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['lists_length'] = function(block, generator) { +export function lists_length(block, generator) { // String or array length. const list = generator.valueToCode(block, 'VALUE', Order.UNARY) || '{}'; return ['#' + list, Order.UNARY]; }; -luaGenerator.forBlock['lists_isEmpty'] = function(block, generator) { +export function lists_isEmpty(block, generator) { // Is the string null or array empty? const list = generator.valueToCode(block, 'VALUE', Order.UNARY) || '{}'; const code = '#' + list + ' == 0'; return [code, Order.RELATIONAL]; }; -luaGenerator.forBlock['lists_indexOf'] = function(block, generator) { +export function lists_indexOf(block, generator) { // Find an item in the list. const item = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const list = generator.valueToCode(block, 'VALUE', Order.NONE) || '{}'; @@ -114,7 +114,7 @@ const getListIndex = function(listName, where, opt_at) { } }; -luaGenerator.forBlock['lists_getIndex'] = function(block, generator) { +export function lists_getIndex(block, generator) { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -194,7 +194,7 @@ luaGenerator.forBlock['lists_getIndex'] = function(block, generator) { } }; -luaGenerator.forBlock['lists_setIndex'] = function(block, generator) { +export function lists_setIndex(block, generator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. let list = generator.valueToCode(block, 'LIST', Order.HIGH) || '{}'; @@ -227,7 +227,7 @@ luaGenerator.forBlock['lists_setIndex'] = function(block, generator) { return code + '\n'; }; -luaGenerator.forBlock['lists_getSublist'] = function(block, generator) { +export function lists_getSublist(block, generator) { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; const where1 = block.getFieldValue('WHERE1'); @@ -262,7 +262,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['lists_sort'] = function(block, generator) { +export function lists_sort(block, generator) { // Block for sorting a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; @@ -296,7 +296,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['lists_split'] = function(block, generator) { +export function lists_split(block, generator) { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.NONE); const delimiter = @@ -336,7 +336,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['lists_reverse'] = function(block, generator) { +export function lists_reverse(block, generator) { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; const functionName = generator.provideFunction_('list_reverse', ` diff --git a/generators/lua/logic.js b/generators/lua/logic.js index 4bed86a5a..01c6b6c18 100644 --- a/generators/lua/logic.js +++ b/generators/lua/logic.js @@ -11,10 +11,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.logic'); -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['controls_if'] = function(block, generator) { +export function controls_if(block, generator) { // If/elseif/else condition. let n = 0; let code = ''; @@ -51,9 +51,9 @@ luaGenerator.forBlock['controls_if'] = function(block, generator) { return code + 'end\n'; }; -luaGenerator.forBlock['controls_ifelse'] = luaGenerator.forBlock['controls_if']; +export const controls_ifelse = controls_if; -luaGenerator.forBlock['logic_compare'] = function(block, generator) { +export function logic_compare(block, generator) { // Comparison operator. const OPERATORS = {'EQ': '==', 'NEQ': '~=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; @@ -66,7 +66,7 @@ luaGenerator.forBlock['logic_compare'] = function(block, generator) { return [code, Order.RELATIONAL]; }; -luaGenerator.forBlock['logic_operation'] = function(block, generator) { +export function logic_operation(block, generator) { // Operations 'and', 'or'. const operator = (block.getFieldValue('OP') === 'AND') ? 'and' : 'or'; const order = (operator === 'and') ? Order.AND : Order.OR; @@ -90,7 +90,7 @@ luaGenerator.forBlock['logic_operation'] = function(block, generator) { return [code, order]; }; -luaGenerator.forBlock['logic_negate'] = function(block, generator) { +export function logic_negate(block, generator) { // Negation. const argument0 = generator.valueToCode(block, 'BOOL', Order.UNARY) || 'true'; @@ -98,18 +98,18 @@ luaGenerator.forBlock['logic_negate'] = function(block, generator) { return [code, Order.UNARY]; }; -luaGenerator.forBlock['logic_boolean'] = function(block, generator) { +export function logic_boolean(block, generator) { // Boolean values true and false. const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; return [code, Order.ATOMIC]; }; -luaGenerator.forBlock['logic_null'] = function(block, generator) { +export function logic_null(block, generator) { // Null data type. return ['nil', Order.ATOMIC]; }; -luaGenerator.forBlock['logic_ternary'] = function(block, generator) { +export function logic_ternary(block, generator) { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.AND) || 'false'; const value_then = diff --git a/generators/lua/loops.js b/generators/lua/loops.js index 02f40b410..3c613cb98 100644 --- a/generators/lua/loops.js +++ b/generators/lua/loops.js @@ -13,7 +13,7 @@ goog.declareModuleId('Blockly.Lua.loops'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; /** @@ -43,7 +43,7 @@ function addContinueLabel(branch, indent) { } }; -luaGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { +export function controls_repeat_ext(block, generator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -67,10 +67,9 @@ luaGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { return code; }; -luaGenerator.forBlock['controls_repeat'] = - luaGenerator.forBlock['controls_repeat_ext']; +export const controls_repeat = controls_repeat_ext; -luaGenerator.forBlock['controls_whileUntil'] = function(block, generator) { +export function controls_whileUntil(block, generator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = @@ -86,7 +85,7 @@ luaGenerator.forBlock['controls_whileUntil'] = function(block, generator) { return 'while ' + argument0 + ' do\n' + branch + 'end\n'; }; -luaGenerator.forBlock['controls_for'] = function(block, generator) { +export function controls_for(block, generator) { // For loop. const variable0 = generator.nameDB_.getName( @@ -128,7 +127,7 @@ luaGenerator.forBlock['controls_for'] = function(block, generator) { return code; }; -luaGenerator.forBlock['controls_forEach'] = function(block, generator) { +export function controls_forEach(block, generator) { // For each loop. const variable0 = generator.nameDB_.getName( @@ -142,7 +141,7 @@ luaGenerator.forBlock['controls_forEach'] = function(block, generator) { return code; }; -luaGenerator.forBlock['controls_flow_statements'] = function(block, generator) { +export function controls_flow_statements(block, generator) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { diff --git a/generators/lua/lua_generator.js b/generators/lua/lua_generator.js new file mode 100644 index 000000000..587fea90b --- /dev/null +++ b/generators/lua/lua_generator.js @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helper functions for generating Lua for blocks. + * Based on Ellen Spertus's blocky-lua project. + * @suppress {checkTypes|globalThis} + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('Blockly.Lua'); + +import * as stringUtils from '../../core/utils/string.js'; +// import type {Block} from '../../core/block.js'; +import {CodeGenerator} from '../../core/generator.js'; +import {Names} from '../../core/names.js'; +// import type {Workspace} from '../../core/workspace.js'; +import {inputTypes} from '../../core/inputs/input_types.js'; + + +/** + * Order of operation ENUMs. + * http://www.lua.org/manual/5.3/manual.html#3.4.8 + * @enum {number} + */ +export const Order = { + ATOMIC: 0, // literals + // The next level was not explicit in documentation and inferred by Ellen. + HIGH: 1, // Function calls, tables[] + EXPONENTIATION: 2, // ^ + UNARY: 3, // not # - ~ + MULTIPLICATIVE: 4, // * / % + ADDITIVE: 5, // + - + CONCATENATION: 6, // .. + RELATIONAL: 7, // < > <= >= ~= == + AND: 8, // and + OR: 9, // or + NONE: 99, +}; + +/** + * Lua code generator class. + * + * Note: Lua is not supporting zero-indexing since the language itself is + * one-indexed, so the generator does not repoct the oneBasedIndex configuration + * option used for lists and text. + */ +export class LuaGenerator extends CodeGenerator { + constructor(name) { + super(name ?? 'Lua'); + this.isInitialized = false; + + // Copy Order values onto instance for backwards compatibility + // while ensuring they are not part of the publically-advertised + // API. + // + // TODO(#7085): deprecate these in due course. (Could initially + // replace data properties with get accessors that call + // deprecate.warn().) + for (const key in Order) { + this['ORDER_' + key] = Order[key]; + } + + // 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. + this.addReservedWords( + // Special character + '_,' + + // From theoriginalbit's script: + // https://github.com/espertus/blockly-lua/issues/6 + '__inext,assert,bit,colors,colours,coroutine,disk,dofile,error,fs,' + + 'fetfenv,getmetatable,gps,help,io,ipairs,keys,loadfile,loadstring,math,' + + 'native,next,os,paintutils,pairs,parallel,pcall,peripheral,print,' + + 'printError,rawequal,rawget,rawset,read,rednet,redstone,rs,select,' + + 'setfenv,setmetatable,sleep,string,table,term,textutils,tonumber,' + + 'tostring,turtle,type,unpack,vector,write,xpcall,_VERSION,__indext,' + + // Not included in the script, probably because it wasn't enabled: + 'HTTP,' + + // Keywords (http://www.lua.org/pil/1.3.html). + 'and,break,do,else,elseif,end,false,for,function,if,in,local,nil,not,' + + 'or,repeat,return,then,true,until,while,' + + // Metamethods (http://www.lua.org/manual/5.2/manual.html). + 'add,sub,mul,div,mod,pow,unm,concat,len,eq,lt,le,index,newindex,call,' + + // Basic functions (http://www.lua.org/manual/5.2/manual.html, + // section 6.1). + 'assert,collectgarbage,dofile,error,_G,getmetatable,inpairs,load,' + + 'loadfile,next,pairs,pcall,print,rawequal,rawget,rawlen,rawset,select,' + + 'setmetatable,tonumber,tostring,type,_VERSION,xpcall,' + + // Modules (http://www.lua.org/manual/5.2/manual.html, section 6.3). + 'require,package,string,table,math,bit32,io,file,os,debug' + ); + } + + /** + * Initialise the database of variable names. + * @param {!Workspace} workspace Workspace to generate code from. + */ + init(workspace) { + // Call Blockly.CodeGenerator's init. + super.init(); + + if (!this.nameDB_) { + this.nameDB_ = new Names(this.RESERVED_WORDS_); + } else { + this.nameDB_.reset(); + } + this.nameDB_.setVariableMap(workspace.getVariableMap()); + this.nameDB_.populateVariables(workspace); + this.nameDB_.populateProcedures(workspace); + + this.isInitialized = true; + }; + + /** + * Prepend the generated code with the variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // Convert the definitions dictionary into a list. + const definitions = Object.values(this.definitions_); + // Call Blockly.CodeGenerator's finish. + code = super.finish(code); + this.isInitialized = false; + + 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. In Lua, an expression is not a legal statement, so we must assign + * the value to the (conventionally ignored) _. + * http://lua-users.org/wiki/ExpressionsAsStatements + * @param {string} line Line of generated code. + * @return {string} Legal line of code. + */ + scrubNakedValue(line) { + return 'local _ = ' + line + '\n'; + }; + + /** + * Encode a string as a properly escaped Lua string, complete with + * quotes. + * @param {string} string Text to encode. + * @return {string} Lua string. + * @protected + */ + quote_(string) { + string = string.replace(/\\/g, '\\\\') + .replace(/\n/g, '\\\n') + .replace(/'/g, '\\\''); + return '\'' + string + '\''; + }; + + /** + * Encode a string as a properly escaped multiline Lua string, complete with + * quotes. + * @param {string} string Text to encode. + * @return {string} Lua string. + * @protected + */ + multiline_quote_(string) { + const lines = string.split(/\n/g).map(this.quote_); + // Join with the following, plus a newline: + // .. '\n' .. + return lines.join(' .. \'\\n\' ..\n'); + }; + + /** + * Common tasks for generating Lua 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 Lua code created for this block. + * @param {boolean=} opt_thisOnly True to generate code for only this statement. + * @return {string} Lua code with comments and subsequent blocks added. + * @protected + */ + scrub_(block, code, opt_thisOnly) { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + commentCode += this.prefixLines(comment, '-- ') + '\n'; + } + // Collect comments for all value arguments. + // 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(); + if (childBlock) { + comment = this.allNestedComments(childBlock); + if (comment) { + commentCode += this.prefixLines(comment, '-- '); + } + } + } + } + } + const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + return commentCode + code + nextCode; + }; +} diff --git a/generators/lua/math.js b/generators/lua/math.js index b6d69fa1a..eb9f55a39 100644 --- a/generators/lua/math.js +++ b/generators/lua/math.js @@ -12,17 +12,17 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.math'); import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['math_number'] = function(block, generator) { +export function math_number(block, generator) { // Numeric value. const code = Number(block.getFieldValue('NUM')); const order = code < 0 ? Order.UNARY : Order.ATOMIC; return [code, order]; }; -luaGenerator.forBlock['math_arithmetic'] = function(block, generator) { +export function math_arithmetic(block, generator) { // Basic arithmetic operators, and power. const OPERATORS = { 'ADD': [' + ', Order.ADDITIVE], @@ -40,7 +40,7 @@ luaGenerator.forBlock['math_arithmetic'] = function(block, generator) { return [code, order]; }; -luaGenerator.forBlock['math_single'] = function(block, generator) { +export function math_single(block, generator) { // Math operators with single operand. const operator = block.getFieldValue('OP'); let arg; @@ -110,7 +110,7 @@ luaGenerator.forBlock['math_single'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['math_constant'] = function(block, generator) { +export function math_constant(block, generator) { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. const CONSTANTS = { 'PI': ['math.pi', Order.HIGH], @@ -123,7 +123,7 @@ luaGenerator.forBlock['math_constant'] = function(block, generator) { return CONSTANTS[block.getFieldValue('CONSTANT')]; }; -luaGenerator.forBlock['math_number_property'] = function(block, generator) { +export function math_number_property(block, generator) { // 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 = { @@ -181,7 +181,7 @@ end return [code, outputOrder]; }; -luaGenerator.forBlock['math_change'] = function(block, generator) { +export function math_change(block, generator) { // Add to a variable in place. const argument0 = generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; @@ -192,11 +192,11 @@ luaGenerator.forBlock['math_change'] = function(block, generator) { }; // Rounding functions have a single operand. -luaGenerator.forBlock['math_round'] = luaGenerator.forBlock['math_single']; +export const math_round = math_single; // Trigonometry functions have a single operand. -luaGenerator.forBlock['math_trig'] = luaGenerator.forBlock['math_single']; +export const math_trig = math_single; -luaGenerator.forBlock['math_on_list'] = function(block, generator) { +export function math_on_list(block, generator) { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; @@ -363,7 +363,7 @@ end return [functionName + '(' + list + ')', Order.HIGH]; }; -luaGenerator.forBlock['math_modulo'] = function(block, generator) { +export function math_modulo(block, generator) { // Remainder computation. const argument0 = generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; @@ -373,7 +373,7 @@ luaGenerator.forBlock['math_modulo'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -luaGenerator.forBlock['math_constrain'] = function(block, generator) { +export function math_constrain(block, generator) { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const argument1 = @@ -385,7 +385,7 @@ luaGenerator.forBlock['math_constrain'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['math_random_int'] = function(block, generator) { +export function math_random_int(block, generator) { // Random integer between [X] and [Y]. const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; @@ -393,12 +393,12 @@ luaGenerator.forBlock['math_random_int'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['math_random_float'] = function(block, generator) { +export function math_random_float(block, generator) { // Random fraction between 0 and 1. return ['math.random()', Order.HIGH]; }; -luaGenerator.forBlock['math_atan2'] = function(block, generator) { +export function math_atan2(block, generator) { // 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'; diff --git a/generators/lua/procedures.js b/generators/lua/procedures.js index e73f14413..cff655aae 100644 --- a/generators/lua/procedures.js +++ b/generators/lua/procedures.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.procedures'); import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['procedures_defreturn'] = function(block, generator) { +export function procedures_defreturn(block, generator) { // Define a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -63,10 +63,9 @@ luaGenerator.forBlock['procedures_defreturn'] = function(block, generator) { // Defining a procedure without a return value uses the same generator as // a procedure with a return value. -luaGenerator.forBlock['procedures_defnoreturn'] = - luaGenerator.forBlock['procedures_defreturn']; +export const procedures_defnoreturn = procedures_defreturn; -luaGenerator.forBlock['procedures_callreturn'] = function(block, generator) { +export function procedures_callreturn(block, generator) { // Call a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -80,7 +79,7 @@ luaGenerator.forBlock['procedures_callreturn'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { +export function procedures_callnoreturn(block, generator) { // 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. @@ -88,7 +87,7 @@ luaGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { return tuple[0] + '\n'; }; -luaGenerator.forBlock['procedures_ifreturn'] = function(block, generator) { +export function procedures_ifreturn(block, generator) { // Conditionally return value from a procedure. const condition = generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; diff --git a/generators/lua/text.js b/generators/lua/text.js index dc29b800e..2cf13f7c6 100644 --- a/generators/lua/text.js +++ b/generators/lua/text.js @@ -12,16 +12,16 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.texts'); import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['text'] = function(block, generator) { +export function text(block, generator) { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; }; -luaGenerator.forBlock['text_multiline'] = function(block, generator) { +export function text_multiline(block, generator) { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); const order = @@ -29,7 +29,7 @@ luaGenerator.forBlock['text_multiline'] = function(block, generator) { return [code, order]; }; -luaGenerator.forBlock['text_join'] = function(block, generator) { +export function text_join(block, generator) { // Create a string made up of any number of elements of any type. if (block.itemCount_ === 0) { return ["''", Order.ATOMIC]; @@ -55,7 +55,7 @@ luaGenerator.forBlock['text_join'] = function(block, generator) { } }; -luaGenerator.forBlock['text_append'] = function(block, generator) { +export function text_append(block, generator) { // Append to a variable in place. const varName = generator.nameDB_.getName( @@ -65,19 +65,19 @@ luaGenerator.forBlock['text_append'] = function(block, generator) { return varName + ' = ' + varName + ' .. ' + value + '\n'; }; -luaGenerator.forBlock['text_length'] = function(block, generator) { +export function text_length(block, generator) { // String or array length. const text = generator.valueToCode(block, 'VALUE', Order.UNARY) || "''"; return ['#' + text, Order.UNARY]; }; -luaGenerator.forBlock['text_isEmpty'] = function(block, generator) { +export function text_isEmpty(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.UNARY) || "''"; return ['#' + text + ' == 0', Order.RELATIONAL]; }; -luaGenerator.forBlock['text_indexOf'] = function(block, generator) { +export function text_indexOf(block, generator) { // Search the text for a substring. const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; @@ -107,7 +107,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_charAt'] = function(block, generator) { +export function text_charAt(block, generator) { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -153,7 +153,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_getSubstring'] = function(block, generator) { +export function text_getSubstring(block, generator) { // Get substring. const text = generator.valueToCode(block, 'STRING', Order.NONE) || "''"; @@ -190,7 +190,7 @@ luaGenerator.forBlock['text_getSubstring'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_changeCase'] = function(block, generator) { +export function text_changeCase(block, generator) { // Change capitalization. const operator = block.getFieldValue('CASE'); const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; @@ -227,7 +227,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_trim'] = function(block, generator) { +export function text_trim(block, generator) { // Trim spaces. const OPERATORS = {LEFT: '^%s*(,-)', RIGHT: '(.-)%s*$', BOTH: '^%s*(.-)%s*$'}; const operator = OPERATORS[block.getFieldValue('MODE')]; @@ -236,13 +236,13 @@ luaGenerator.forBlock['text_trim'] = function(block, generator) { return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_print'] = function(block, generator) { +export function text_print(block, generator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ')\n'; }; -luaGenerator.forBlock['text_prompt_ext'] = function(block, generator) { +export function text_prompt_ext(block, generator) { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -269,9 +269,9 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_prompt'] = luaGenerator.forBlock['text_prompt_ext']; +export const text_prompt = text_prompt_ext; -luaGenerator.forBlock['text_count'] = function(block, generator) { +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_('text_count', ` @@ -296,7 +296,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_replace'] = function(block, generator) { +export function text_replace(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; @@ -322,7 +322,7 @@ end return [code, Order.HIGH]; }; -luaGenerator.forBlock['text_reverse'] = function(block, generator) { +export function text_reverse(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const code = 'string.reverse(' + text + ')'; return [code, Order.HIGH]; diff --git a/generators/lua/variables.js b/generators/lua/variables.js index e90179101..f149f8841 100644 --- a/generators/lua/variables.js +++ b/generators/lua/variables.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.variables'); import {NameType} from '../../core/names.js'; -import {luaGenerator, Order} from '../lua.js'; +import {Order} from './lua_generator.js'; -luaGenerator.forBlock['variables_get'] = function(block, generator) { +export function variables_get(block, generator) { // Variable getter. const code = generator.nameDB_.getName( @@ -23,7 +23,7 @@ luaGenerator.forBlock['variables_get'] = function(block, generator) { return [code, Order.ATOMIC]; }; -luaGenerator.forBlock['variables_set'] = function(block, generator) { +export function variables_set(block, generator) { // Variable setter. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const varName = diff --git a/generators/lua/variables_dynamic.js b/generators/lua/variables_dynamic.js index 0a627fbdf..b78368a21 100644 --- a/generators/lua/variables_dynamic.js +++ b/generators/lua/variables_dynamic.js @@ -11,12 +11,9 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Lua.variablesDynamic'); -import {luaGenerator} from '../lua.js'; -import './variables.js'; - // Lua is dynamically typed. -luaGenerator.forBlock['variables_get_dynamic'] = - luaGenerator.forBlock['variables_get']; -luaGenerator.forBlock['variables_set_dynamic'] = - luaGenerator.forBlock['variables_set']; +export { + variables_get as variables_get_dynamic, + variables_set as variables_set_dynamic, +} from './variables.js'; diff --git a/generators/php.js b/generators/php.js index df90cdc20..666f081f7 100644 --- a/generators/php.js +++ b/generators/php.js @@ -1,316 +1,40 @@ /** * @license - * Copyright 2015 Google LLC + * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Helper functions for generating PHP for blocks. - * @suppress {checkTypes|globalThis} + * @fileoverview Complete helper functions for generating PHP for + * blocks. This is the entrypoint for php_compressed.js. + * @suppress {extraRequire} */ import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.PHP'); +goog.declareModuleId('Blockly.PHP.all'); -import * as stringUtils from '../core/utils/string.js'; -// import type {Block} from '../core/block.js'; -import {CodeGenerator} from '../core/generator.js'; -import {Names} from '../core/names.js'; -// import type {Workspace} from '../core/workspace.js'; -import {inputTypes} from '../core/inputs/input_types.js'; +import {PhpGenerator} from './php/php_generator.js'; +import * as colour from './php/colour.js'; +import * as lists from './php/lists.js'; +import * as logic from './php/logic.js'; +import * as loops from './php/loops.js'; +import * as math from './php/math.js'; +import * as procedures from './php/procedures.js'; +import * as text from './php/text.js'; +import * as variables from './php/variables.js'; +import * as variablesDynamic from './php/variables_dynamic.js'; +export * from './php/php_generator.js'; /** - * Order of operation ENUMs. - * http://php.net/manual/en/language.operators.precedence.php - * @enum {number} - */ -export const Order = { - ATOMIC: 0, // 0 "" ... - CLONE: 1, // clone - NEW: 1, // new - MEMBER: 2.1, // [] - FUNCTION_CALL: 2.2, // () - POWER: 3, // ** - INCREMENT: 4, // ++ - DECREMENT: 4, // -- - BITWISE_NOT: 4, // ~ - CAST: 4, // (int) (float) (string) (array) ... - SUPPRESS_ERROR: 4, // @ - INSTANCEOF: 5, // instanceof - LOGICAL_NOT: 6, // ! - UNARY_PLUS: 7.1, // + - UNARY_NEGATION: 7.2, // - - MULTIPLICATION: 8.1, // * - DIVISION: 8.2, // / - MODULUS: 8.3, // % - ADDITION: 9.1, // + - SUBTRACTION: 9.2, // - - STRING_CONCAT: 9.3, // . - BITWISE_SHIFT: 10, // << >> - RELATIONAL: 11, // < <= > >= - EQUALITY: 12, // == != === !== <> <=> - REFERENCE: 13, // & - BITWISE_AND: 13, // & - BITWISE_XOR: 14, // ^ - BITWISE_OR: 15, // | - LOGICAL_AND: 16, // && - LOGICAL_OR: 17, // || - IF_NULL: 18, // ?? - CONDITIONAL: 19, // ?: - ASSIGNMENT: 20, // = += -= *= /= %= <<= >>= ... - LOGICAL_AND_WEAK: 21, // and - LOGICAL_XOR: 22, // xor - LOGICAL_OR_WEAK: 23, // or - NONE: 99, // (...) -}; - -class PhpGenerator extends CodeGenerator { - /** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} - */ - ORDER_OVERRIDES = [ - // (foo()).bar() -> foo().bar() - // (foo())[0] -> foo()[0] - [Order.MEMBER, Order.FUNCTION_CALL], - // (foo[0])[1] -> foo[0][1] - // (foo.bar).baz -> foo.bar.baz - [Order.MEMBER, Order.MEMBER], - // !(!foo) -> !!foo - [Order.LOGICAL_NOT, Order.LOGICAL_NOT], - // a * (b * c) -> a * b * c - [Order.MULTIPLICATION, Order.MULTIPLICATION], - // a + (b + c) -> a + b + c - [Order.ADDITION, Order.ADDITION], - // a && (b && c) -> a && b && c - [Order.LOGICAL_AND, Order.LOGICAL_AND], - // a || (b || c) -> a || b || c - [Order.LOGICAL_OR, Order.LOGICAL_OR] - ]; - - constructor(name) { - super(name ?? 'PHP'); - this.isInitialized = false; - - // Copy Order values onto instance for backwards compatibility - // while ensuring they are not part of the publically-advertised - // API. - // - // TODO(#7085): deprecate these in due course. (Could initially - // replace data properties with get accessors that call - // deprecate.warn().) - for (const key in Order) { - this['ORDER_' + key] = Order[key]; - } - - // 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. - this.addReservedWords( - // http://php.net/manual/en/reserved.keywords.php - '__halt_compiler,abstract,and,array,as,break,callable,case,catch,class,' + - 'clone,const,continue,declare,default,die,do,echo,else,elseif,empty,' + - 'enddeclare,endfor,endforeach,endif,endswitch,endwhile,eval,exit,' + - 'extends,final,for,foreach,function,global,goto,if,implements,include,' + - 'include_once,instanceof,insteadof,interface,isset,list,namespace,new,' + - 'or,print,private,protected,public,require,require_once,return,static,' + - 'switch,throw,trait,try,unset,use,var,while,xor,' + - // http://php.net/manual/en/reserved.constants.php - 'PHP_VERSION,PHP_MAJOR_VERSION,PHP_MINOR_VERSION,PHP_RELEASE_VERSION,' + - 'PHP_VERSION_ID,PHP_EXTRA_VERSION,PHP_ZTS,PHP_DEBUG,PHP_MAXPATHLEN,' + - 'PHP_OS,PHP_SAPI,PHP_EOL,PHP_INT_MAX,PHP_INT_SIZE,DEFAULT_INCLUDE_PATH,' + - 'PEAR_INSTALL_DIR,PEAR_EXTENSION_DIR,PHP_EXTENSION_DIR,PHP_PREFIX,' + - 'PHP_BINDIR,PHP_BINARY,PHP_MANDIR,PHP_LIBDIR,PHP_DATADIR,' + - 'PHP_SYSCONFDIR,PHP_LOCALSTATEDIR,PHP_CONFIG_FILE_PATH,' + - 'PHP_CONFIG_FILE_SCAN_DIR,PHP_SHLIB_SUFFIX,E_ERROR,E_WARNING,E_PARSE,' + - 'E_NOTICE,E_CORE_ERROR,E_CORE_WARNING,E_COMPILE_ERROR,' + - 'E_COMPILE_WARNING,E_USER_ERROR,E_USER_WARNING,E_USER_NOTICE,' + - 'E_DEPRECATED,E_USER_DEPRECATED,E_ALL,E_STRICT,' + - '__COMPILER_HALT_OFFSET__,TRUE,FALSE,NULL,__CLASS__,__DIR__,__FILE__,' + - '__FUNCTION__,__LINE__,__METHOD__,__NAMESPACE__,__TRAIT__' - ); - } - - /** - * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. - */ - init(workspace) { - super.init(workspace); - - if (!this.nameDB_) { - this.nameDB_ = new Names(this.RESERVED_WORDS_, '$'); - } else { - this.nameDB_.reset(); - } - - this.nameDB_.setVariableMap(workspace.getVariableMap()); - this.nameDB_.populateVariables(workspace); - this.nameDB_.populateProcedures(workspace); - - this.isInitialized = true; - }; - - /** - * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ - finish(code) { - // Convert the definitions dictionary into a list. - const definitions = Object.values(this.definitions_); - // Call Blockly.CodeGenerator's finish. - code = super.finish(code); - this.isInitialized = false; - - 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. - */ - scrubNakedValue(line) { - return line + ';\n'; - }; - - /** - * Encode a string as a properly escaped PHP string, complete with - * quotes. - * @param {string} string Text to encode. - * @return {string} PHP string. - * @protected - */ - quote_(string) { - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; - }; - - /** - * Encode a string as a properly escaped multiline PHP string, complete with - * quotes. - * @param {string} string Text to encode. - * @return {string} PHP string. - * @protected - */ - multiline_quote_(string) { - const lines = string.split(/\n/g).map(this.quote_); - // Join with the following, plus a newline: - // . "\n" . - // Newline escaping only works in double-quoted strings. - return lines.join(' . \"\\n\" .\n'); - }; - - /** - * Common tasks for generating PHP 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 PHP code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this - * statement. - * @return {string} PHP code with comments and subsequent blocks added. - * @protected - */ - scrub_(block, code, opt_thisOnly) { - let commentCode = ''; - // Only collect comments for blocks that aren't inline. - if (!block.outputConnection || !block.outputConnection.targetConnection) { - // Collect comment for this block. - let comment = block.getCommentText(); - if (comment) { - comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - commentCode += this.prefixLines(comment, '// ') + '\n'; - } - // Collect comments for all value arguments. - // 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(); - if (childBlock) { - comment = this.allNestedComments(childBlock); - if (comment) { - commentCode += this.prefixLines(comment, '// '); - } - } - } - } - } - const nextBlock = - block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_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} - */ - getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { - let delta = opt_delta || 0; - let order = opt_order || this.ORDER_NONE; - if (block.workspace.options.oneBasedIndex) { - delta--; - } - let defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - let outerOrder = order; - let innerOrder; - if (delta > 0) { - outerOrder = this.ORDER_ADDITION; - innerOrder = this.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; - } - let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; - - 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 + ')'; - } - } - return at; - }; -} - -/** - * PHP code generator. + * Php code generator instance. * @type {!PhpGenerator} */ export const phpGenerator = new PhpGenerator(); + +// Install per-block-type generator functions: +Object.assign( + phpGenerator.forBlock, + colour, lists, logic, loops, math, procedures, + text, variables, variablesDynamic +); diff --git a/generators/php/all.js b/generators/php/all.js deleted file mode 100644 index 2fd3b505a..000000000 --- a/generators/php/all.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Complete helper functions for generating PHP for - * blocks. This is the entrypoint for php_compressed.js. - * @suppress {extraRequire} - */ - -import * as goog from '../../closure/goog/goog.js'; -goog.declareModuleId('Blockly.PHP.all'); - -import './colour.js'; -import './lists.js'; -import './logic.js'; -import './loops.js'; -import './math.js'; -import './procedures.js'; -import './text.js'; -import './variables.js'; -import './variables_dynamic.js'; - -export * from '../php.js'; diff --git a/generators/php/colour.js b/generators/php/colour.js index 58f5dd645..a9874fcdf 100644 --- a/generators/php/colour.js +++ b/generators/php/colour.js @@ -11,16 +11,16 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.colour'); -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['colour_picker'] = function(block, generator) { +export function colour_picker(block, generator) { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; }; -phpGenerator.forBlock['colour_random'] = function(block, generator) { +export function colour_random(block, generator) { // Generate a random colour. const functionName = generator.provideFunction_('colour_random', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { @@ -31,7 +31,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['colour_rgb'] = function(block, generator) { +export function colour_rgb(block, generator) { // 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; @@ -52,7 +52,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($r, $g, $b) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['colour_blend'] = function(block, generator) { +export function colour_blend(block, generator) { // Blend two colours together. const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; diff --git a/generators/php/lists.js b/generators/php/lists.js index de7172ad0..5e1aa7f79 100644 --- a/generators/php/lists.js +++ b/generators/php/lists.js @@ -24,14 +24,14 @@ goog.declareModuleId('Blockly.generator.lists'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['lists_create_empty'] = function(block, generator) { +export function lists_create_empty(block, generator) { // Create an empty list. return ['array()', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_create_with'] = function(block, generator) { +export function lists_create_with(block, generator) { // Create a list with any number of elements of any type. let code = new Array(block.itemCount_); for (let i = 0; i < block.itemCount_; i++) { @@ -41,7 +41,7 @@ phpGenerator.forBlock['lists_create_with'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_repeat'] = function(block, generator) { +export function lists_repeat(block, generator) { // Create a list with one element repeated. const functionName = generator.provideFunction_('lists_repeat', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value, $count) { @@ -58,7 +58,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value, $count) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_length'] = function(block, generator) { +export function lists_length(block, generator) { // String or array length. const functionName = generator.provideFunction_('length', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { @@ -73,7 +73,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { return [functionName + '(' + list + ')', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_isEmpty'] = function(block, generator) { +export function lists_isEmpty(block, generator) { // Is the string null or array empty? const argument0 = generator.valueToCode(block, 'VALUE', Order.FUNCTION_CALL) @@ -81,7 +81,7 @@ phpGenerator.forBlock['lists_isEmpty'] = function(block, generator) { return ['empty(' + argument0 + ')', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_indexOf'] = function(block, generator) { +export function lists_indexOf(block, generator) { // Find an item in the list. const argument0 = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const argument1 = @@ -120,7 +120,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($haystack, $needle) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_getIndex'] = function(block, generator) { +export function lists_getIndex(block, generator) { // Get element at index. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -237,7 +237,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list) { throw Error('Unhandled combination (lists_getIndex).'); }; -phpGenerator.forBlock['lists_setIndex'] = function(block, generator) { +export function lists_setIndex(block, generator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -341,7 +341,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $at, $value) { throw Error('Unhandled combination (lists_setIndex).'); }; -phpGenerator.forBlock['lists_getSublist'] = function(block, generator) { +export function lists_getSublist(block, generator) { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; const where1 = block.getFieldValue('WHERE1'); @@ -441,7 +441,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $where1, $at1, $where2, return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_sort'] = function(block, generator) { +export function lists_sort(block, generator) { // Block for sorting a list. const listCode = generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; @@ -468,7 +468,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $type, $direction) { return [sortCode, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_split'] = function(block, generator) { +export function lists_split(block, generator) { // Block for splitting text into a list, or joining a list into text. let value_input = generator.valueToCode(block, 'INPUT', Order.NONE); const value_delim = @@ -492,7 +492,7 @@ phpGenerator.forBlock['lists_split'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['lists_reverse'] = function(block, generator) { +export function lists_reverse(block, generator) { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const code = 'array_reverse(' + list + ')'; diff --git a/generators/php/logic.js b/generators/php/logic.js index b2c98b01c..5c7ea0e93 100644 --- a/generators/php/logic.js +++ b/generators/php/logic.js @@ -11,10 +11,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.logic'); -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['controls_if'] = function(block, generator) { +export function controls_if(block, generator) { // If/elseif/else condition. let n = 0; let code = '', branchCode, conditionCode; @@ -52,9 +52,9 @@ phpGenerator.forBlock['controls_if'] = function(block, generator) { return code + '\n'; }; -phpGenerator.forBlock['controls_ifelse'] = phpGenerator.forBlock['controls_if']; +export const controls_ifelse = controls_if; -phpGenerator.forBlock['logic_compare'] = function(block, generator) { +export function logic_compare(block, generator) { // Comparison operator. const OPERATORS = {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; @@ -67,7 +67,7 @@ phpGenerator.forBlock['logic_compare'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['logic_operation'] = function(block, generator) { +export function logic_operation(block, generator) { // Operations 'and', 'or'. const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; const order = @@ -92,7 +92,7 @@ phpGenerator.forBlock['logic_operation'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['logic_negate'] = function(block, generator) { +export function logic_negate(block, generator) { // Negation. const order = Order.LOGICAL_NOT; const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; @@ -100,18 +100,18 @@ phpGenerator.forBlock['logic_negate'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['logic_boolean'] = function(block, generator) { +export function logic_boolean(block, generator) { // Boolean values true and false. const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; return [code, Order.ATOMIC]; }; -phpGenerator.forBlock['logic_null'] = function(block, generator) { +export function logic_null(block, generator) { // Null data type. return ['null', Order.ATOMIC]; }; -phpGenerator.forBlock['logic_ternary'] = function(block, generator) { +export function logic_ternary(block, generator) { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; diff --git a/generators/php/loops.js b/generators/php/loops.js index ad4577ec3..3e2cfab74 100644 --- a/generators/php/loops.js +++ b/generators/php/loops.js @@ -13,10 +13,10 @@ goog.declareModuleId('Blockly.PHP.loops'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { +export function controls_repeat_ext(block, generator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -42,10 +42,9 @@ phpGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { return code; }; -phpGenerator.forBlock['controls_repeat'] = - phpGenerator.forBlock['controls_repeat_ext']; +export const controls_repeat = controls_repeat_ext; -phpGenerator.forBlock['controls_whileUntil'] = function(block, generator) { +export function controls_whileUntil(block, generator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = @@ -60,7 +59,7 @@ phpGenerator.forBlock['controls_whileUntil'] = function(block, generator) { return 'while (' + argument0 + ') {\n' + branch + '}\n'; }; -phpGenerator.forBlock['controls_for'] = function(block, generator) { +export function controls_for(block, generator) { // For loop. const variable0 = generator.nameDB_.getName(block.getFieldValue('VAR'), NameType.VARIABLE); @@ -125,7 +124,7 @@ phpGenerator.forBlock['controls_for'] = function(block, generator) { return code; }; -phpGenerator.forBlock['controls_forEach'] = function(block, generator) { +export function controls_forEach(block, generator) { // For each loop. const variable0 = generator.nameDB_.getName( @@ -140,7 +139,7 @@ phpGenerator.forBlock['controls_forEach'] = function(block, generator) { return code; }; -phpGenerator.forBlock['controls_flow_statements'] = function(block, generator) { +export function controls_flow_statements(block, generator) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { diff --git a/generators/php/math.js b/generators/php/math.js index 8fba46dcf..b5cd53404 100644 --- a/generators/php/math.js +++ b/generators/php/math.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.math'); import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['math_number'] = function(block, generator) { +export function math_number(block, generator) { // Numeric value. let code = Number(block.getFieldValue('NUM')); const order = code >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION; @@ -27,7 +27,7 @@ phpGenerator.forBlock['math_number'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['math_arithmetic'] = function(block, generator) { +export function math_arithmetic(block, generator) { // Basic arithmetic operators, and power. const OPERATORS = { 'ADD': [' + ', Order.ADDITION], @@ -45,7 +45,7 @@ phpGenerator.forBlock['math_arithmetic'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['math_single'] = function(block, generator) { +export function math_single(block, generator) { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -126,7 +126,7 @@ phpGenerator.forBlock['math_single'] = function(block, generator) { return [code, Order.DIVISION]; }; -phpGenerator.forBlock['math_constant'] = function(block, generator) { +export function math_constant(block, generator) { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. const CONSTANTS = { 'PI': ['M_PI', Order.ATOMIC], @@ -139,7 +139,7 @@ phpGenerator.forBlock['math_constant'] = function(block, generator) { return CONSTANTS[block.getFieldValue('CONSTANT')]; }; -phpGenerator.forBlock['math_number_property'] = function(block, generator) { +export function math_number_property(block, generator) { // 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 = { @@ -194,7 +194,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($n) { return [code, outputOrder]; }; -phpGenerator.forBlock['math_change'] = function(block, generator) { +export function math_change(block, generator) { // Add to a variable in place. const argument0 = generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; @@ -205,11 +205,11 @@ phpGenerator.forBlock['math_change'] = function(block, generator) { }; // Rounding functions have a single operand. -phpGenerator.forBlock['math_round'] = phpGenerator.forBlock['math_single']; +export const math_round = math_single; // Trigonometry functions have a single operand. -phpGenerator.forBlock['math_trig'] = phpGenerator.forBlock['math_single']; +export const math_trig = math_single; -phpGenerator.forBlock['math_on_list'] = function(block, generator) { +export function math_on_list(block, generator) { // Math functions for lists. const func = block.getFieldValue('OP'); let list; @@ -304,7 +304,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['math_modulo'] = function(block, generator) { +export function math_modulo(block, generator) { // Remainder computation. const argument0 = generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; @@ -314,7 +314,7 @@ phpGenerator.forBlock['math_modulo'] = function(block, generator) { return [code, Order.MODULUS]; }; -phpGenerator.forBlock['math_constrain'] = function(block, generator) { +export function math_constrain(block, generator) { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; @@ -325,7 +325,7 @@ phpGenerator.forBlock['math_constrain'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['math_random_int'] = function(block, generator) { +export function math_random_int(block, generator) { // Random integer between [X] and [Y]. const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; @@ -341,12 +341,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($a, $b) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['math_random_float'] = function(block, generator) { +export function math_random_float(block, generator) { // Random fraction between 0 and 1. return ['(float)rand()/(float)getrandmax()', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['math_atan2'] = function(block, generator) { +export function math_atan2(block, generator) { // 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'; diff --git a/generators/php/php_generator.js b/generators/php/php_generator.js new file mode 100644 index 000000000..59d595b83 --- /dev/null +++ b/generators/php/php_generator.js @@ -0,0 +1,310 @@ +/** + * @license + * Copyright 2015 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helper functions for generating PHP for blocks. + * @suppress {checkTypes|globalThis} + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('Blockly.PHP'); + +import * as stringUtils from '../../core/utils/string.js'; +// import type {Block} from '../../core/block.js'; +import {CodeGenerator} from '../../core/generator.js'; +import {Names} from '../../core/names.js'; +// import type {Workspace} from '../../core/workspace.js'; +import {inputTypes} from '../../core/inputs/input_types.js'; + + +/** + * Order of operation ENUMs. + * http://php.net/manual/en/language.operators.precedence.php + * @enum {number} + */ +export const Order = { + ATOMIC: 0, // 0 "" ... + CLONE: 1, // clone + NEW: 1, // new + MEMBER: 2.1, // [] + FUNCTION_CALL: 2.2, // () + POWER: 3, // ** + INCREMENT: 4, // ++ + DECREMENT: 4, // -- + BITWISE_NOT: 4, // ~ + CAST: 4, // (int) (float) (string) (array) ... + SUPPRESS_ERROR: 4, // @ + INSTANCEOF: 5, // instanceof + LOGICAL_NOT: 6, // ! + UNARY_PLUS: 7.1, // + + UNARY_NEGATION: 7.2, // - + MULTIPLICATION: 8.1, // * + DIVISION: 8.2, // / + MODULUS: 8.3, // % + ADDITION: 9.1, // + + SUBTRACTION: 9.2, // - + STRING_CONCAT: 9.3, // . + BITWISE_SHIFT: 10, // << >> + RELATIONAL: 11, // < <= > >= + EQUALITY: 12, // == != === !== <> <=> + REFERENCE: 13, // & + BITWISE_AND: 13, // & + BITWISE_XOR: 14, // ^ + BITWISE_OR: 15, // | + LOGICAL_AND: 16, // && + LOGICAL_OR: 17, // || + IF_NULL: 18, // ?? + CONDITIONAL: 19, // ?: + ASSIGNMENT: 20, // = += -= *= /= %= <<= >>= ... + LOGICAL_AND_WEAK: 21, // and + LOGICAL_XOR: 22, // xor + LOGICAL_OR_WEAK: 23, // or + NONE: 99, // (...) +}; + +export class PhpGenerator extends CodeGenerator { + /** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array>} + */ + ORDER_OVERRIDES = [ + // (foo()).bar() -> foo().bar() + // (foo())[0] -> foo()[0] + [Order.MEMBER, Order.FUNCTION_CALL], + // (foo[0])[1] -> foo[0][1] + // (foo.bar).baz -> foo.bar.baz + [Order.MEMBER, Order.MEMBER], + // !(!foo) -> !!foo + [Order.LOGICAL_NOT, Order.LOGICAL_NOT], + // a * (b * c) -> a * b * c + [Order.MULTIPLICATION, Order.MULTIPLICATION], + // a + (b + c) -> a + b + c + [Order.ADDITION, Order.ADDITION], + // a && (b && c) -> a && b && c + [Order.LOGICAL_AND, Order.LOGICAL_AND], + // a || (b || c) -> a || b || c + [Order.LOGICAL_OR, Order.LOGICAL_OR] + ]; + + constructor(name) { + super(name ?? 'PHP'); + this.isInitialized = false; + + // Copy Order values onto instance for backwards compatibility + // while ensuring they are not part of the publically-advertised + // API. + // + // TODO(#7085): deprecate these in due course. (Could initially + // replace data properties with get accessors that call + // deprecate.warn().) + for (const key in Order) { + this['ORDER_' + key] = Order[key]; + } + + // 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. + this.addReservedWords( + // http://php.net/manual/en/reserved.keywords.php + '__halt_compiler,abstract,and,array,as,break,callable,case,catch,class,' + + 'clone,const,continue,declare,default,die,do,echo,else,elseif,empty,' + + 'enddeclare,endfor,endforeach,endif,endswitch,endwhile,eval,exit,' + + 'extends,final,for,foreach,function,global,goto,if,implements,include,' + + 'include_once,instanceof,insteadof,interface,isset,list,namespace,new,' + + 'or,print,private,protected,public,require,require_once,return,static,' + + 'switch,throw,trait,try,unset,use,var,while,xor,' + + // http://php.net/manual/en/reserved.constants.php + 'PHP_VERSION,PHP_MAJOR_VERSION,PHP_MINOR_VERSION,PHP_RELEASE_VERSION,' + + 'PHP_VERSION_ID,PHP_EXTRA_VERSION,PHP_ZTS,PHP_DEBUG,PHP_MAXPATHLEN,' + + 'PHP_OS,PHP_SAPI,PHP_EOL,PHP_INT_MAX,PHP_INT_SIZE,DEFAULT_INCLUDE_PATH,' + + 'PEAR_INSTALL_DIR,PEAR_EXTENSION_DIR,PHP_EXTENSION_DIR,PHP_PREFIX,' + + 'PHP_BINDIR,PHP_BINARY,PHP_MANDIR,PHP_LIBDIR,PHP_DATADIR,' + + 'PHP_SYSCONFDIR,PHP_LOCALSTATEDIR,PHP_CONFIG_FILE_PATH,' + + 'PHP_CONFIG_FILE_SCAN_DIR,PHP_SHLIB_SUFFIX,E_ERROR,E_WARNING,E_PARSE,' + + 'E_NOTICE,E_CORE_ERROR,E_CORE_WARNING,E_COMPILE_ERROR,' + + 'E_COMPILE_WARNING,E_USER_ERROR,E_USER_WARNING,E_USER_NOTICE,' + + 'E_DEPRECATED,E_USER_DEPRECATED,E_ALL,E_STRICT,' + + '__COMPILER_HALT_OFFSET__,TRUE,FALSE,NULL,__CLASS__,__DIR__,__FILE__,' + + '__FUNCTION__,__LINE__,__METHOD__,__NAMESPACE__,__TRAIT__' + ); + } + + /** + * Initialise the database of variable names. + * @param {!Workspace} workspace Workspace to generate code from. + */ + init(workspace) { + super.init(workspace); + + if (!this.nameDB_) { + this.nameDB_ = new Names(this.RESERVED_WORDS_, '$'); + } else { + this.nameDB_.reset(); + } + + this.nameDB_.setVariableMap(workspace.getVariableMap()); + this.nameDB_.populateVariables(workspace); + this.nameDB_.populateProcedures(workspace); + + this.isInitialized = true; + }; + + /** + * Prepend the generated code with the variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // Convert the definitions dictionary into a list. + const definitions = Object.values(this.definitions_); + // Call Blockly.CodeGenerator's finish. + code = super.finish(code); + this.isInitialized = false; + + 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. + */ + scrubNakedValue(line) { + return line + ';\n'; + }; + + /** + * Encode a string as a properly escaped PHP string, complete with + * quotes. + * @param {string} string Text to encode. + * @return {string} PHP string. + * @protected + */ + quote_(string) { + string = string.replace(/\\/g, '\\\\') + .replace(/\n/g, '\\\n') + .replace(/'/g, '\\\''); + return '\'' + string + '\''; + }; + + /** + * Encode a string as a properly escaped multiline PHP string, complete with + * quotes. + * @param {string} string Text to encode. + * @return {string} PHP string. + * @protected + */ + multiline_quote_(string) { + const lines = string.split(/\n/g).map(this.quote_); + // Join with the following, plus a newline: + // . "\n" . + // Newline escaping only works in double-quoted strings. + return lines.join(' . \"\\n\" .\n'); + }; + + /** + * Common tasks for generating PHP 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 PHP code created for this block. + * @param {boolean=} opt_thisOnly True to generate code for only this + * statement. + * @return {string} PHP code with comments and subsequent blocks added. + * @protected + */ + scrub_(block, code, opt_thisOnly) { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + commentCode += this.prefixLines(comment, '// ') + '\n'; + } + // Collect comments for all value arguments. + // 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(); + if (childBlock) { + comment = this.allNestedComments(childBlock); + if (comment) { + commentCode += this.prefixLines(comment, '// '); + } + } + } + } + } + const nextBlock = + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = opt_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} + */ + getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { + let delta = opt_delta || 0; + let order = opt_order || this.ORDER_NONE; + if (block.workspace.options.oneBasedIndex) { + delta--; + } + let defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + let outerOrder = order; + let innerOrder; + if (delta > 0) { + outerOrder = this.ORDER_ADDITION; + innerOrder = this.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; + } + let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; + + 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 + ')'; + } + } + return at; + }; +} diff --git a/generators/php/procedures.js b/generators/php/procedures.js index 1c2ed3dbf..9ff0de082 100644 --- a/generators/php/procedures.js +++ b/generators/php/procedures.js @@ -13,10 +13,10 @@ goog.declareModuleId('Blockly.PHP.procedures'); import * as Variables from '../../core/variables.js'; import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['procedures_defreturn'] = function(block, generator) { +export function procedures_defreturn(block, generator) { // Define a procedure with a return value. // First, add a 'global' statement for every variable that is not shadowed by // a local parameter. @@ -84,10 +84,9 @@ phpGenerator.forBlock['procedures_defreturn'] = function(block, generator) { // Defining a procedure without a return value uses the same generator as // a procedure with a return value. -phpGenerator.forBlock['procedures_defnoreturn'] = - phpGenerator.forBlock['procedures_defreturn']; +export const procedures_defnoreturn = procedures_defreturn; -phpGenerator.forBlock['procedures_callreturn'] = function(block, generator) { +export function procedures_callreturn(block, generator) { // Call a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -101,7 +100,7 @@ phpGenerator.forBlock['procedures_callreturn'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { +export function procedures_callnoreturn(block, generator) { // 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. @@ -109,7 +108,7 @@ phpGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { return tuple[0] + ';\n'; }; -phpGenerator.forBlock['procedures_ifreturn'] = function(block, generator) { +export function procedures_ifreturn(block, generator) { // Conditionally return value from a procedure. const condition = generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; diff --git a/generators/php/text.js b/generators/php/text.js index 6992dad12..91da583a8 100644 --- a/generators/php/text.js +++ b/generators/php/text.js @@ -12,16 +12,16 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.texts'); import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['text'] = function(block, generator) { +export function text(block, generator) { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; }; -phpGenerator.forBlock['text_multiline'] = function(block, generator) { +export function text_multiline(block, generator) { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); const order = @@ -29,7 +29,7 @@ phpGenerator.forBlock['text_multiline'] = function(block, generator) { return [code, order]; }; -phpGenerator.forBlock['text_join'] = function(block, generator) { +export function text_join(block, generator) { // Create a string made up of any number of elements of any type. if (block.itemCount_ === 0) { return ["''", Order.ATOMIC]; @@ -55,7 +55,7 @@ phpGenerator.forBlock['text_join'] = function(block, generator) { } }; -phpGenerator.forBlock['text_append'] = function(block, generator) { +export function text_append(block, generator) { // Append to a variable in place. const varName = generator.nameDB_.getName( @@ -65,7 +65,7 @@ phpGenerator.forBlock['text_append'] = function(block, generator) { return varName + ' .= ' + value + ';\n'; }; -phpGenerator.forBlock['text_length'] = function(block, generator) { +export function text_length(block, generator) { // String or array length. const functionName = generator.provideFunction_('length', ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { @@ -79,13 +79,13 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { return [functionName + '(' + text + ')', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_isEmpty'] = function(block, generator) { +export function text_isEmpty(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return ['empty(' + text + ')', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_indexOf'] = function(block, generator) { +export function text_indexOf(block, generator) { // Search the text for a substring. const operator = block.getFieldValue('END') === 'FIRST' ? 'strpos' : 'strrpos'; @@ -110,7 +110,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text, $search) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_charAt'] = function(block, generator) { +export function text_charAt(block, generator) { // Get letter at index. const where = block.getFieldValue('WHERE') || 'FROM_START'; const textOrder = (where === 'RANDOM') ? Order.NONE : Order.NONE; @@ -147,7 +147,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text) { throw Error('Unhandled option (text_charAt).'); }; -phpGenerator.forBlock['text_getSubstring'] = function(block, generator) { +export function text_getSubstring(block, generator) { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); @@ -186,7 +186,7 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text, $where1, $at1, $where2, } }; -phpGenerator.forBlock['text_changeCase'] = function(block, generator) { +export function text_changeCase(block, generator) { // Change capitalization. const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; let code; @@ -200,7 +200,7 @@ phpGenerator.forBlock['text_changeCase'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_trim'] = function(block, generator) { +export function text_trim(block, generator) { // Trim spaces. const OPERATORS = {'LEFT': 'ltrim', 'RIGHT': 'rtrim', 'BOTH': 'trim'}; const operator = OPERATORS[block.getFieldValue('MODE')]; @@ -208,13 +208,13 @@ phpGenerator.forBlock['text_trim'] = function(block, generator) { return [operator + '(' + text + ')', Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_print'] = function(block, generator) { +export function text_print(block, generator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ');\n'; }; -phpGenerator.forBlock['text_prompt_ext'] = function(block, generator) { +export function text_prompt_ext(block, generator) { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -232,9 +232,9 @@ phpGenerator.forBlock['text_prompt_ext'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_prompt'] = phpGenerator.forBlock['text_prompt_ext']; +export const text_prompt = text_prompt_ext; -phpGenerator.forBlock['text_count'] = function(block, generator) { +export function text_count(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; const code = 'strlen(' + sub + ') === 0' + @@ -243,7 +243,7 @@ phpGenerator.forBlock['text_count'] = function(block, generator) { return [code, Order.CONDITIONAL]; }; -phpGenerator.forBlock['text_replace'] = function(block, generator) { +export function text_replace(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; @@ -251,7 +251,7 @@ phpGenerator.forBlock['text_replace'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -phpGenerator.forBlock['text_reverse'] = function(block, generator) { +export function text_reverse(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const code = 'strrev(' + text + ')'; return [code, Order.FUNCTION_CALL]; diff --git a/generators/php/variables.js b/generators/php/variables.js index f1c47d1cd..f29274da7 100644 --- a/generators/php/variables.js +++ b/generators/php/variables.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.variables'); import {NameType} from '../../core/names.js'; -import {phpGenerator, Order} from '../php.js'; +import {Order} from './php_generator.js'; -phpGenerator.forBlock['variables_get'] = function(block, generator) { +export function variables_get(block, generator) { // Variable getter. const code = generator.nameDB_.getName( @@ -23,7 +23,7 @@ phpGenerator.forBlock['variables_get'] = function(block, generator) { return [code, Order.ATOMIC]; }; -phpGenerator.forBlock['variables_set'] = function(block, generator) { +export function variables_set(block, generator) { // Variable setter. const argument0 = generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; diff --git a/generators/php/variables_dynamic.js b/generators/php/variables_dynamic.js index 3cfd652ad..030edc92c 100644 --- a/generators/php/variables_dynamic.js +++ b/generators/php/variables_dynamic.js @@ -11,12 +11,9 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.PHP.variablesDynamic'); -import {phpGenerator} from '../php.js'; -import './variables.js'; - // generator is dynamically typed. -phpGenerator.forBlock['variables_get_dynamic'] = - phpGenerator.forBlock['variables_get']; -phpGenerator.forBlock['variables_set_dynamic'] = - phpGenerator.forBlock['variables_set']; +export { + variables_get as variables_get_dynamic, + variables_set as variables_set_dynamic, +} from './variables.js'; diff --git a/generators/python.js b/generators/python.js index 6c1e7d7b1..19b64ffd2 100644 --- a/generators/python.js +++ b/generators/python.js @@ -1,347 +1,44 @@ /** * @license - * Copyright 2012 Google LLC + * Copyright 2021 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** - * @fileoverview Helper functions for generating Python for blocks. - * @suppress {checkTypes|globalThis} + * @fileoverview Complete helper functions for generating Python for + * blocks. This is the entrypoint for python_compressed.js. + * @suppress {extraRequire} */ import * as goog from '../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Python'); +goog.declareModuleId('Blockly.Python.all'); -import * as stringUtils from '../core/utils/string.js'; -import * as Variables from '../core/variables.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 {inputTypes} from '../core/inputs/input_types.js'; +import {PythonGenerator} from './python/python_generator.js'; +import * as colour from './python/colour.js'; +import * as lists from './python/lists.js'; +import * as logic from './python/logic.js'; +import * as loops from './python/loops.js'; +import * as math from './python/math.js'; +import * as procedures from './python/procedures.js'; +import * as text from './python/text.js'; +import * as variables from './python/variables.js'; +import * as variablesDynamic from './python/variables_dynamic.js'; +export * from './python/python_generator.js'; /** - * Order of operation ENUMs. - * http://docs.python.org/reference/expressions.html#summary - * @enum {number} - */ -export const Order = { - ATOMIC: 0, // 0 "" ... - COLLECTION: 1, // tuples, lists, dictionaries - STRING_CONVERSION: 1, // `expression...` - MEMBER: 2.1, // . [] - FUNCTION_CALL: 2.2, // () - EXPONENTIATION: 3, // ** - UNARY_SIGN: 4, // + - - BITWISE_NOT: 4, // ~ - MULTIPLICATIVE: 5, // * / // % - ADDITIVE: 6, // + - - BITWISE_SHIFT: 7, // << >> - BITWISE_AND: 8, // & - BITWISE_XOR: 9, // ^ - BITWISE_OR: 10, // | - RELATIONAL: 11, // in, not in, is, is not, >, >=, <>, !=, == - LOGICAL_NOT: 12, // not - LOGICAL_AND: 13, // and - LOGICAL_OR: 14, // or - CONDITIONAL: 15, // if else - LAMBDA: 16, // lambda - NONE: 99, // (...) -}; - -/** - * PythonScript code generator class. - */ -export class PythonGenerator extends CodeGenerator { - /** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} - */ - ORDER_OVERRIDES = [ - // (foo()).bar -> foo().bar - // (foo())[0] -> foo()[0] - [Order.FUNCTION_CALL, Order.MEMBER], - // (foo())() -> foo()() - [Order.FUNCTION_CALL, Order.FUNCTION_CALL], - // (foo.bar).baz -> foo.bar.baz - // (foo.bar)[0] -> foo.bar[0] - // (foo[0]).bar -> foo[0].bar - // (foo[0])[1] -> foo[0][1] - [Order.MEMBER, Order.MEMBER], - // (foo.bar)() -> foo.bar() - // (foo[0])() -> foo[0]() - [Order.MEMBER, Order.FUNCTION_CALL], - - // not (not foo) -> not not foo - [Order.LOGICAL_NOT, Order.LOGICAL_NOT], - // a and (b and c) -> a and b and c - [Order.LOGICAL_AND, Order.LOGICAL_AND], - // a or (b or c) -> a or b or c - [Order.LOGICAL_OR, Order.LOGICAL_OR] - ]; - - constructor(name) { - super(name ?? 'Python'); - this.isInitialized = false; - - // Copy Order values onto instance for backwards compatibility - // while ensuring they are not part of the publically-advertised - // API. - // - // TODO(#7085): deprecate these in due course. (Could initially - // replace data properties with get accessors that call - // deprecate.warn().) - for (const key in Order) { - this['ORDER_' + key] = Order[key]; - } - - // 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. - this.addReservedWords( - // import keyword - // print(','.join(sorted(keyword.kwlist))) - // https://docs.python.org/3/reference/lexical_analysis.html#keywords - // https://docs.python.org/2/reference/lexical_analysis.html#keywords - 'False,None,True,and,as,assert,break,class,continue,def,del,elif,else,' + - 'except,exec,finally,for,from,global,if,import,in,is,lambda,nonlocal,' + - 'not,or,pass,print,raise,return,try,while,with,yield,' + - // https://docs.python.org/3/library/constants.html - // https://docs.python.org/2/library/constants.html - 'NotImplemented,Ellipsis,__debug__,quit,exit,copyright,license,credits,' + - // >>> print(','.join(sorted(dir(__builtins__)))) - // https://docs.python.org/3/library/functions.html - // https://docs.python.org/2/library/functions.html - 'ArithmeticError,AssertionError,AttributeError,BaseException,' + - 'BlockingIOError,BrokenPipeError,BufferError,BytesWarning,' + - 'ChildProcessError,ConnectionAbortedError,ConnectionError,' + - 'ConnectionRefusedError,ConnectionResetError,DeprecationWarning,' + - 'EOFError,Ellipsis,EnvironmentError,Exception,FileExistsError,' + - 'FileNotFoundError,FloatingPointError,FutureWarning,GeneratorExit,' + - 'IOError,ImportError,ImportWarning,IndentationError,IndexError,' + - 'InterruptedError,IsADirectoryError,KeyError,KeyboardInterrupt,' + - 'LookupError,MemoryError,ModuleNotFoundError,NameError,' + - 'NotADirectoryError,NotImplemented,NotImplementedError,OSError,' + - 'OverflowError,PendingDeprecationWarning,PermissionError,' + - 'ProcessLookupError,RecursionError,ReferenceError,ResourceWarning,' + - 'RuntimeError,RuntimeWarning,StandardError,StopAsyncIteration,' + - 'StopIteration,SyntaxError,SyntaxWarning,SystemError,SystemExit,' + - 'TabError,TimeoutError,TypeError,UnboundLocalError,UnicodeDecodeError,' + - 'UnicodeEncodeError,UnicodeError,UnicodeTranslateError,UnicodeWarning,' + - 'UserWarning,ValueError,Warning,ZeroDivisionError,_,__build_class__,' + - '__debug__,__doc__,__import__,__loader__,__name__,__package__,__spec__,' + - 'abs,all,any,apply,ascii,basestring,bin,bool,buffer,bytearray,bytes,' + - 'callable,chr,classmethod,cmp,coerce,compile,complex,copyright,credits,' + - 'delattr,dict,dir,divmod,enumerate,eval,exec,execfile,exit,file,filter,' + - 'float,format,frozenset,getattr,globals,hasattr,hash,help,hex,id,input,' + - 'int,intern,isinstance,issubclass,iter,len,license,list,locals,long,' + - 'map,max,memoryview,min,next,object,oct,open,ord,pow,print,property,' + - 'quit,range,raw_input,reduce,reload,repr,reversed,round,set,setattr,' + - 'slice,sorted,staticmethod,str,sum,super,tuple,type,unichr,unicode,' + - 'vars,xrange,zip' - ); - } - - /** - * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. - * @this {CodeGenerator} - */ - init(workspace) { - super.init(workspace); - - /** - * Empty loops or conditionals are not allowed in Python. - */ - this.PASS = this.INDENT + 'pass\n'; - - if (!this.nameDB_) { - this.nameDB_ = new Names(this.RESERVED_WORDS_); - } else { - this.nameDB_.reset(); - } - - this.nameDB_.setVariableMap(workspace.getVariableMap()); - this.nameDB_.populateVariables(workspace); - this.nameDB_.populateProcedures(workspace); - - const defvars = []; - // Add developer variables (not created or named by the user). - const devVarList = Variables.allDeveloperVariables(workspace); - for (let i = 0; i < devVarList.length; i++) { - defvars.push( - this.nameDB_.getName(devVarList[i], Names.DEVELOPER_VARIABLE_TYPE) + - ' = None'); - } - - // 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) + - ' = None'); - } - - this.definitions_['variables'] = defvars.join('\n'); - this.isInitialized = true; - } - - /** - * Prepend the generated code with import statements and variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ - finish(code) { - // Convert the definitions dictionary into a list. - const imports = []; - const definitions = []; - for (let name in this.definitions_) { - const def = this.definitions_[name]; - if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) { - imports.push(def); - } else { - definitions.push(def); - } - } - // Call Blockly.CodeGenerator's finish. - code = super.finish(code); - this.isInitialized = false; - - this.nameDB_.reset(); - const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); - return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code; - } - - /** - * Naked values are top-level blocks with outputs that aren't plugged into - * anything. - * @param {string} line Line of generated code. - * @return {string} Legal line of code. - */ - scrubNakedValue(line) { - return line + '\n'; - } - - /** - * Encode a string as a properly escaped Python string, complete with quotes. - * @param {string} string Text to encode. - * @return {string} Python string. - * @protected - */ - quote_(string) { - string = string.replace(/\\/g, '\\\\').replace(/\n/g, '\\\n'); - - // Follow the CPython behaviour of repr() for a non-byte string. - let quote = '\''; - if (string.indexOf('\'') !== -1) { - if (string.indexOf('"') === -1) { - quote = '"'; - } else { - string = string.replace(/'/g, '\\\''); - } - } - return quote + string + quote; - } - - /** - * Encode a string as a properly escaped multiline Python string, complete - * with quotes. - * @param {string} string Text to encode. - * @return {string} Python string. - * @protected - */ - multiline_quote_(string) { - const lines = string.split(/\n/g).map(this.quote_); - // Join with the following, plus a newline: - // + '\n' + - return lines.join(' + \'\\n\' + \n'); - } - - /** - * Common tasks for generating Python 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 Python code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this statement. - * @return {string} Python code with comments and subsequent blocks added. - * @protected - */ - scrub_(block, code, opt_thisOnly) { - let commentCode = ''; - // Only collect comments for blocks that aren't inline. - if (!block.outputConnection || !block.outputConnection.targetConnection) { - // Collect comment for this block. - let comment = block.getCommentText(); - if (comment) { - comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); - commentCode += this.prefixLines(comment + '\n', '# '); - } - // Collect comments for all value arguments. - // 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(); - if (childBlock) { - comment = this.allNestedComments(childBlock); - if (comment) { - commentCode += this.prefixLines(comment, '# '); - } - } - } - } - } - const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); - return commentCode + code + nextCode; - } - - /** - * Gets a property and adjusts the value, taking into account indexing. - * If a static int, casts to an integer, otherwise returns a code string. - * @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. - * @return {string|number} - */ - getAdjustedInt(block, atId, opt_delta, opt_negate) { - let delta = opt_delta || 0; - if (block.workspace.options.oneBasedIndex) { - delta--; - } - const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - const atOrder = delta ? this.ORDER_ADDITIVE : this.ORDER_NONE; - let at = this.valueToCode(block, atId, atOrder) || defaultAtIndex; - - if (stringUtils.isNumber(at)) { - // If the index is a naked number, adjust it right now. - at = parseInt(at, 10) + delta; - if (opt_negate) { - at = -at; - } - } else { - // If the index is dynamic, adjust it in code. - if (delta > 0) { - at = 'int(' + at + ' + ' + delta + ')'; - } else if (delta < 0) { - at = 'int(' + at + ' - ' + -delta + ')'; - } else { - at = 'int(' + at + ')'; - } - if (opt_negate) { - at = '-' + at; - } - } - return at; - } -} - -/** - * Python code generator. + * Python code generator instance. * @type {!PythonGenerator} */ export const pythonGenerator = new PythonGenerator(); - + +// Add reserved words. This list should include all words mentioned +// in RESERVED WORDS: comments in the imports above. +pythonGenerator.addReservedWords('math,random,Number'); + +// Install per-block-type generator functions: +Object.assign( + pythonGenerator.forBlock, + colour, lists, logic, loops, math, procedures, + text, variables, variablesDynamic +); diff --git a/generators/python/all.js b/generators/python/all.js deleted file mode 100644 index 10c97e029..000000000 --- a/generators/python/all.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Complete helper functions for generating Python for - * blocks. This is the entrypoint for python_compressed.js. - * @suppress {extraRequire} - */ - -import * as goog from '../../closure/goog/goog.js'; -goog.declareModuleId('Blockly.Python.all'); - -import './colour.js'; -import './lists.js'; -import './logic.js'; -import './loops.js'; -import './math.js'; -import './procedures.js'; -import './text.js'; -import './variables.js'; -import './variables_dynamic.js'; - -export * from '../python.js'; diff --git a/generators/python/colour.js b/generators/python/colour.js index a1d06a380..3236fc408 100644 --- a/generators/python/colour.js +++ b/generators/python/colour.js @@ -11,23 +11,23 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Python.colour'); -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['colour_picker'] = function(block, generator) { +export function colour_picker(block, generator) { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; }; -pythonGenerator.forBlock['colour_random'] = function(block, generator) { +export function colour_random(block, generator) { // Generate a random colour. generator.definitions_['import_random'] = 'import random'; const code = '\'#%06x\' % random.randint(0, 2**24 - 1)'; return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['colour_rgb'] = function(block, generator) { +export function colour_rgb(block, generator) { // Compose a colour from RGB components expressed as percentages. const functionName = generator.provideFunction_('colour_rgb', ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b): @@ -43,7 +43,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b): return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['colour_blend'] = function(block, generator) { +export function colour_blend(block, generator) { // Blend two colours together. const functionName = generator.provideFunction_('colour_blend', ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio): diff --git a/generators/python/lists.js b/generators/python/lists.js index 08fb410c5..f4dbd1166 100644 --- a/generators/python/lists.js +++ b/generators/python/lists.js @@ -13,15 +13,15 @@ goog.declareModuleId('Blockly.Python.lists'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['lists_create_empty'] = function(block, generator) { +export function lists_create_empty(block, generator) { // Create an empty list. return ['[]', Order.ATOMIC]; }; -pythonGenerator.forBlock['lists_create_with'] = function(block, generator) { +export function lists_create_with(block, generator) { // 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++) { @@ -32,7 +32,7 @@ pythonGenerator.forBlock['lists_create_with'] = function(block, generator) { return [code, Order.ATOMIC]; }; -pythonGenerator.forBlock['lists_repeat'] = function(block, generator) { +export function lists_repeat(block, generator) { // Create a list with one element repeated. const item = generator.valueToCode(block, 'ITEM', Order.NONE) || 'None'; const times = @@ -41,20 +41,20 @@ pythonGenerator.forBlock['lists_repeat'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -pythonGenerator.forBlock['lists_length'] = function(block, generator) { +export function lists_length(block, generator) { // String or array length. const list = generator.valueToCode(block, 'VALUE', Order.NONE) || '[]'; return ['len(' + list + ')', Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['lists_isEmpty'] = function(block, generator) { +export function lists_isEmpty(block, generator) { // Is the string null or array empty? const list = generator.valueToCode(block, 'VALUE', Order.NONE) || '[]'; const code = 'not len(' + list + ')'; return [code, Order.LOGICAL_NOT]; }; -pythonGenerator.forBlock['lists_indexOf'] = function(block, generator) { +export function lists_indexOf(block, generator) { // Find an item in the list. const item = generator.valueToCode(block, 'FIND', Order.NONE) || '[]'; const list = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; @@ -88,7 +88,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, elem): return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['lists_getIndex'] = function(block, generator) { +export function lists_getIndex(block, generator) { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; @@ -170,7 +170,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): throw Error('Unhandled combination (lists_getIndex).'); }; -pythonGenerator.forBlock['lists_setIndex'] = function(block, generator) { +export function lists_setIndex(block, generator) { // 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) || '[]'; @@ -242,7 +242,7 @@ pythonGenerator.forBlock['lists_setIndex'] = function(block, generator) { throw Error('Unhandled combination (lists_setIndex).'); }; -pythonGenerator.forBlock['lists_getSublist'] = function(block, generator) { +export function lists_getSublist(block, generator) { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; const where1 = block.getFieldValue('WHERE1'); @@ -291,7 +291,7 @@ pythonGenerator.forBlock['lists_getSublist'] = function(block, generator) { return [code, Order.MEMBER]; }; -pythonGenerator.forBlock['lists_sort'] = function(block, generator) { +export function lists_sort(block, generator) { // Block for sorting a list. const list = (generator.valueToCode(block, 'LIST', Order.NONE) || '[]'); const type = block.getFieldValue('TYPE'); @@ -318,7 +318,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, type, reverse): return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['lists_split'] = function(block, generator) { +export function lists_split(block, generator) { // Block for splitting text into a list, or joining a list into text. const mode = block.getFieldValue('MODE'); let code; @@ -339,7 +339,7 @@ pythonGenerator.forBlock['lists_split'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['lists_reverse'] = function(block, generator) { +export function lists_reverse(block, generator) { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const code = 'list(reversed(' + list + '))'; diff --git a/generators/python/logic.js b/generators/python/logic.js index 4308d6cb8..a71a0ef5f 100644 --- a/generators/python/logic.js +++ b/generators/python/logic.js @@ -11,10 +11,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Python.logic'); -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['controls_if'] = function(block, generator) { +export function controls_if(block, generator) { // If/elseif/else condition. let n = 0; let code = '', branchCode, conditionCode; @@ -55,10 +55,9 @@ pythonGenerator.forBlock['controls_if'] = function(block, generator) { return code; }; -pythonGenerator.forBlock['controls_ifelse'] = - pythonGenerator.forBlock['controls_if']; +export const controls_ifelse = controls_if; -pythonGenerator.forBlock['logic_compare'] = function(block, generator) { +export function logic_compare(block, generator) { // Comparison operator. const OPERATORS = {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; @@ -70,7 +69,7 @@ pythonGenerator.forBlock['logic_compare'] = function(block, generator) { return [code, order]; }; -pythonGenerator.forBlock['logic_operation'] = function(block, generator) { +export function logic_operation(block, generator) { // Operations 'and', 'or'. const operator = (block.getFieldValue('OP') === 'AND') ? 'and' : 'or'; const order = @@ -95,7 +94,7 @@ pythonGenerator.forBlock['logic_operation'] = function(block, generator) { return [code, order]; }; -pythonGenerator.forBlock['logic_negate'] = function(block, generator) { +export function logic_negate(block, generator) { // Negation. const argument0 = generator.valueToCode(block, 'BOOL', Order.LOGICAL_NOT) || 'True'; @@ -103,18 +102,18 @@ pythonGenerator.forBlock['logic_negate'] = function(block, generator) { return [code, Order.LOGICAL_NOT]; }; -pythonGenerator.forBlock['logic_boolean'] = function(block, generator) { +export function logic_boolean(block, generator) { // Boolean values true and false. const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'True' : 'False'; return [code, Order.ATOMIC]; }; -pythonGenerator.forBlock['logic_null'] = function(block, generator) { +export function logic_null(block, generator) { // Null data type. return ['None', Order.ATOMIC]; }; -pythonGenerator.forBlock['logic_ternary'] = function(block, generator) { +export function logic_ternary(block, generator) { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'False'; diff --git a/generators/python/loops.js b/generators/python/loops.js index aef33c2f9..f66d90802 100644 --- a/generators/python/loops.js +++ b/generators/python/loops.js @@ -13,10 +13,10 @@ goog.declareModuleId('Blockly.Python.loops'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { +export function controls_repeat_ext(block, generator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -39,10 +39,9 @@ pythonGenerator.forBlock['controls_repeat_ext'] = function(block, generator) { return code; }; -pythonGenerator.forBlock['controls_repeat'] = - pythonGenerator.forBlock['controls_repeat_ext']; +export const controls_repeat = controls_repeat_ext; -pythonGenerator.forBlock['controls_whileUntil'] = function(block, generator) { +export function controls_whileUntil(block, generator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = generator.valueToCode( @@ -57,7 +56,7 @@ pythonGenerator.forBlock['controls_whileUntil'] = function(block, generator) { return 'while ' + argument0 + ':\n' + branch; }; -pythonGenerator.forBlock['controls_for'] = function(block, generator) { +export function controls_for(block, generator) { // For loop. const variable0 = generator.nameDB_.getName( @@ -166,7 +165,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(start, stop, step): return code; }; -pythonGenerator.forBlock['controls_forEach'] = function(block, generator) { +export function controls_forEach(block, generator) { // For each loop. const variable0 = generator.nameDB_.getName( @@ -179,7 +178,7 @@ pythonGenerator.forBlock['controls_forEach'] = function(block, generator) { return code; }; -pythonGenerator.forBlock['controls_flow_statements'] = function(block, generator) { +export function controls_flow_statements(block, generator) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { diff --git a/generators/python/math.js b/generators/python/math.js index cfd61a118..da792346c 100644 --- a/generators/python/math.js +++ b/generators/python/math.js @@ -12,13 +12,13 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Python.math'); import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; // If any new block imports any library, add that library name here. -pythonGenerator.addReservedWords('math,random,Number'); +// RESERVED WORDS: 'math,random,Number' -pythonGenerator.forBlock['math_number'] = function(block, generator) { +export function math_number(block, generator) { // Numeric value. let code = Number(block.getFieldValue('NUM')); let order; @@ -34,7 +34,7 @@ pythonGenerator.forBlock['math_number'] = function(block, generator) { return [code, order]; }; -pythonGenerator.forBlock['math_arithmetic'] = function(block, generator) { +export function math_arithmetic(block, generator) { // Basic arithmetic operators, and power. const OPERATORS = { 'ADD': [' + ', Order.ADDITIVE], @@ -57,7 +57,7 @@ pythonGenerator.forBlock['math_arithmetic'] = function(block, generator) { // legibility of the generated code. }; -pythonGenerator.forBlock['math_single'] = function(block, generator) { +export function math_single(block, generator) { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -135,7 +135,7 @@ pythonGenerator.forBlock['math_single'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -pythonGenerator.forBlock['math_constant'] = function(block, generator) { +export function math_constant(block, generator) { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. const CONSTANTS = { 'PI': ['math.pi', Order.MEMBER], @@ -152,7 +152,7 @@ pythonGenerator.forBlock['math_constant'] = function(block, generator) { return CONSTANTS[constant]; }; -pythonGenerator.forBlock['math_number_property'] = function(block, generator) { +export function math_number_property(block, generator) { // 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 = { @@ -211,7 +211,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(n): return [code, outputOrder]; }; -pythonGenerator.forBlock['math_change'] = function(block, generator) { +export function math_change(block, generator) { // Add to a variable in place. generator.definitions_['from_numbers_import_Number'] = 'from numbers import Number'; @@ -225,13 +225,11 @@ pythonGenerator.forBlock['math_change'] = function(block, generator) { }; // Rounding functions have a single operand. -pythonGenerator.forBlock['math_round'] = - pythonGenerator.forBlock['math_single']; +export const math_round = math_single; // Trigonometry functions have a single operand. -pythonGenerator.forBlock['math_trig'] = - pythonGenerator.forBlock['math_single']; +export const math_trig = math_single; -pythonGenerator.forBlock['math_on_list'] = function(block, generator) { +export function math_on_list(block, generator) { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; @@ -329,7 +327,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers): return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['math_modulo'] = function(block, generator) { +export function math_modulo(block, generator) { // Remainder computation. const argument0 = generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || @@ -341,7 +339,7 @@ pythonGenerator.forBlock['math_modulo'] = function(block, generator) { return [code, Order.MULTIPLICATIVE]; }; -pythonGenerator.forBlock['math_constrain'] = function(block, generator) { +export function math_constrain(block, generator) { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; @@ -355,7 +353,7 @@ pythonGenerator.forBlock['math_constrain'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['math_random_int'] = function(block, generator) { +export function math_random_int(block, generator) { // Random integer between [X] and [Y]. generator.definitions_['import_random'] = 'import random'; const argument0 = @@ -366,13 +364,13 @@ pythonGenerator.forBlock['math_random_int'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['math_random_float'] = function(block, generator) { +export function math_random_float(block, generator) { // Random fraction between 0 and 1. generator.definitions_['import_random'] = 'import random'; return ['random.random()', Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['math_atan2'] = function(block, generator) { +export function math_atan2(block, generator) { // Arctangent of point (X, Y) in degrees from -180 to 180. generator.definitions_['import_math'] = 'import math'; const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; diff --git a/generators/python/procedures.js b/generators/python/procedures.js index e5c2cca4b..65e17ddc3 100644 --- a/generators/python/procedures.js +++ b/generators/python/procedures.js @@ -13,10 +13,10 @@ goog.declareModuleId('Blockly.Python.procedures'); import * as Variables from '../../core/variables.js'; import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['procedures_defreturn'] = function(block, generator) { +export function procedures_defreturn(block, generator) { // Define a procedure with a return value. // First, add a 'global' statement for every variable that is not shadowed by // a local parameter. @@ -87,10 +87,9 @@ pythonGenerator.forBlock['procedures_defreturn'] = function(block, generator) { // Defining a procedure without a return value uses the same generator as // a procedure with a return value. -pythonGenerator.forBlock['procedures_defnoreturn'] = - pythonGenerator.forBlock['procedures_defreturn']; +export const procedures_defnoreturn = procedures_defreturn; -pythonGenerator.forBlock['procedures_callreturn'] = function(block, generator) { +export function procedures_callreturn(block, generator) { // Call a procedure with a return value. const funcName = generator.nameDB_.getName( @@ -105,7 +104,7 @@ pythonGenerator.forBlock['procedures_callreturn'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) { +export function procedures_callnoreturn(block, generator) { // 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. @@ -113,7 +112,7 @@ pythonGenerator.forBlock['procedures_callnoreturn'] = function(block, generator) return tuple[0] + '\n'; }; -pythonGenerator.forBlock['procedures_ifreturn'] = function(block, generator) { +export function procedures_ifreturn(block, generator) { // Conditionally return value from a procedure. const condition = generator.valueToCode(block, 'CONDITION', Order.NONE) || 'False'; diff --git a/generators/python/python_generator.js b/generators/python/python_generator.js new file mode 100644 index 000000000..5897b4366 --- /dev/null +++ b/generators/python/python_generator.js @@ -0,0 +1,340 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Helper functions for generating Python for blocks. + * @suppress {checkTypes|globalThis} + */ + +import * as goog from '../../closure/goog/goog.js'; +goog.declareModuleId('Blockly.Python'); + +import * as stringUtils from '../../core/utils/string.js'; +import * as Variables from '../../core/variables.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 {inputTypes} from '../../core/inputs/input_types.js'; + + +/** + * Order of operation ENUMs. + * http://docs.python.org/reference/expressions.html#summary + * @enum {number} + */ +export const Order = { + ATOMIC: 0, // 0 "" ... + COLLECTION: 1, // tuples, lists, dictionaries + STRING_CONVERSION: 1, // `expression...` + MEMBER: 2.1, // . [] + FUNCTION_CALL: 2.2, // () + EXPONENTIATION: 3, // ** + UNARY_SIGN: 4, // + - + BITWISE_NOT: 4, // ~ + MULTIPLICATIVE: 5, // * / // % + ADDITIVE: 6, // + - + BITWISE_SHIFT: 7, // << >> + BITWISE_AND: 8, // & + BITWISE_XOR: 9, // ^ + BITWISE_OR: 10, // | + RELATIONAL: 11, // in, not in, is, is not, >, >=, <>, !=, == + LOGICAL_NOT: 12, // not + LOGICAL_AND: 13, // and + LOGICAL_OR: 14, // or + CONDITIONAL: 15, // if else + LAMBDA: 16, // lambda + NONE: 99, // (...) +}; + +/** + * PythonScript code generator class. + */ +export class PythonGenerator extends CodeGenerator { + /** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array>} + */ + ORDER_OVERRIDES = [ + // (foo()).bar -> foo().bar + // (foo())[0] -> foo()[0] + [Order.FUNCTION_CALL, Order.MEMBER], + // (foo())() -> foo()() + [Order.FUNCTION_CALL, Order.FUNCTION_CALL], + // (foo.bar).baz -> foo.bar.baz + // (foo.bar)[0] -> foo.bar[0] + // (foo[0]).bar -> foo[0].bar + // (foo[0])[1] -> foo[0][1] + [Order.MEMBER, Order.MEMBER], + // (foo.bar)() -> foo.bar() + // (foo[0])() -> foo[0]() + [Order.MEMBER, Order.FUNCTION_CALL], + + // not (not foo) -> not not foo + [Order.LOGICAL_NOT, Order.LOGICAL_NOT], + // a and (b and c) -> a and b and c + [Order.LOGICAL_AND, Order.LOGICAL_AND], + // a or (b or c) -> a or b or c + [Order.LOGICAL_OR, Order.LOGICAL_OR] + ]; + + constructor(name) { + super(name ?? 'Python'); + this.isInitialized = false; + + // Copy Order values onto instance for backwards compatibility + // while ensuring they are not part of the publically-advertised + // API. + // + // TODO(#7085): deprecate these in due course. (Could initially + // replace data properties with get accessors that call + // deprecate.warn().) + for (const key in Order) { + this['ORDER_' + key] = Order[key]; + } + + // 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. + this.addReservedWords( + // import keyword + // print(','.join(sorted(keyword.kwlist))) + // https://docs.python.org/3/reference/lexical_analysis.html#keywords + // https://docs.python.org/2/reference/lexical_analysis.html#keywords + 'False,None,True,and,as,assert,break,class,continue,def,del,elif,else,' + + 'except,exec,finally,for,from,global,if,import,in,is,lambda,nonlocal,' + + 'not,or,pass,print,raise,return,try,while,with,yield,' + + // https://docs.python.org/3/library/constants.html + // https://docs.python.org/2/library/constants.html + 'NotImplemented,Ellipsis,__debug__,quit,exit,copyright,license,credits,' + + // >>> print(','.join(sorted(dir(__builtins__)))) + // https://docs.python.org/3/library/functions.html + // https://docs.python.org/2/library/functions.html + 'ArithmeticError,AssertionError,AttributeError,BaseException,' + + 'BlockingIOError,BrokenPipeError,BufferError,BytesWarning,' + + 'ChildProcessError,ConnectionAbortedError,ConnectionError,' + + 'ConnectionRefusedError,ConnectionResetError,DeprecationWarning,' + + 'EOFError,Ellipsis,EnvironmentError,Exception,FileExistsError,' + + 'FileNotFoundError,FloatingPointError,FutureWarning,GeneratorExit,' + + 'IOError,ImportError,ImportWarning,IndentationError,IndexError,' + + 'InterruptedError,IsADirectoryError,KeyError,KeyboardInterrupt,' + + 'LookupError,MemoryError,ModuleNotFoundError,NameError,' + + 'NotADirectoryError,NotImplemented,NotImplementedError,OSError,' + + 'OverflowError,PendingDeprecationWarning,PermissionError,' + + 'ProcessLookupError,RecursionError,ReferenceError,ResourceWarning,' + + 'RuntimeError,RuntimeWarning,StandardError,StopAsyncIteration,' + + 'StopIteration,SyntaxError,SyntaxWarning,SystemError,SystemExit,' + + 'TabError,TimeoutError,TypeError,UnboundLocalError,UnicodeDecodeError,' + + 'UnicodeEncodeError,UnicodeError,UnicodeTranslateError,UnicodeWarning,' + + 'UserWarning,ValueError,Warning,ZeroDivisionError,_,__build_class__,' + + '__debug__,__doc__,__import__,__loader__,__name__,__package__,__spec__,' + + 'abs,all,any,apply,ascii,basestring,bin,bool,buffer,bytearray,bytes,' + + 'callable,chr,classmethod,cmp,coerce,compile,complex,copyright,credits,' + + 'delattr,dict,dir,divmod,enumerate,eval,exec,execfile,exit,file,filter,' + + 'float,format,frozenset,getattr,globals,hasattr,hash,help,hex,id,input,' + + 'int,intern,isinstance,issubclass,iter,len,license,list,locals,long,' + + 'map,max,memoryview,min,next,object,oct,open,ord,pow,print,property,' + + 'quit,range,raw_input,reduce,reload,repr,reversed,round,set,setattr,' + + 'slice,sorted,staticmethod,str,sum,super,tuple,type,unichr,unicode,' + + 'vars,xrange,zip' + ); + } + + /** + * Initialise the database of variable names. + * @param {!Workspace} workspace Workspace to generate code from. + * @this {CodeGenerator} + */ + init(workspace) { + super.init(workspace); + + /** + * Empty loops or conditionals are not allowed in Python. + */ + this.PASS = this.INDENT + 'pass\n'; + + if (!this.nameDB_) { + this.nameDB_ = new Names(this.RESERVED_WORDS_); + } else { + this.nameDB_.reset(); + } + + this.nameDB_.setVariableMap(workspace.getVariableMap()); + this.nameDB_.populateVariables(workspace); + this.nameDB_.populateProcedures(workspace); + + const defvars = []; + // Add developer variables (not created or named by the user). + const devVarList = Variables.allDeveloperVariables(workspace); + for (let i = 0; i < devVarList.length; i++) { + defvars.push( + this.nameDB_.getName(devVarList[i], Names.DEVELOPER_VARIABLE_TYPE) + + ' = None'); + } + + // 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) + + ' = None'); + } + + this.definitions_['variables'] = defvars.join('\n'); + this.isInitialized = true; + } + + /** + * Prepend the generated code with import statements and variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // Convert the definitions dictionary into a list. + const imports = []; + const definitions = []; + for (let name in this.definitions_) { + const def = this.definitions_[name]; + if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) { + imports.push(def); + } else { + definitions.push(def); + } + } + // Call Blockly.CodeGenerator's finish. + code = super.finish(code); + this.isInitialized = false; + + this.nameDB_.reset(); + const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); + return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code; + } + + /** + * Naked values are top-level blocks with outputs that aren't plugged into + * anything. + * @param {string} line Line of generated code. + * @return {string} Legal line of code. + */ + scrubNakedValue(line) { + return line + '\n'; + } + + /** + * Encode a string as a properly escaped Python string, complete with quotes. + * @param {string} string Text to encode. + * @return {string} Python string. + * @protected + */ + quote_(string) { + string = string.replace(/\\/g, '\\\\').replace(/\n/g, '\\\n'); + + // Follow the CPython behaviour of repr() for a non-byte string. + let quote = '\''; + if (string.indexOf('\'') !== -1) { + if (string.indexOf('"') === -1) { + quote = '"'; + } else { + string = string.replace(/'/g, '\\\''); + } + } + return quote + string + quote; + } + + /** + * Encode a string as a properly escaped multiline Python string, complete + * with quotes. + * @param {string} string Text to encode. + * @return {string} Python string. + * @protected + */ + multiline_quote_(string) { + const lines = string.split(/\n/g).map(this.quote_); + // Join with the following, plus a newline: + // + '\n' + + return lines.join(' + \'\\n\' + \n'); + } + + /** + * Common tasks for generating Python 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 Python code created for this block. + * @param {boolean=} opt_thisOnly True to generate code for only this statement. + * @return {string} Python code with comments and subsequent blocks added. + * @protected + */ + scrub_(block, code, opt_thisOnly) { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + commentCode += this.prefixLines(comment + '\n', '# '); + } + // Collect comments for all value arguments. + // 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(); + if (childBlock) { + comment = this.allNestedComments(childBlock); + if (comment) { + commentCode += this.prefixLines(comment, '# '); + } + } + } + } + } + const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + return commentCode + code + nextCode; + } + + /** + * Gets a property and adjusts the value, taking into account indexing. + * If a static int, casts to an integer, otherwise returns a code string. + * @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. + * @return {string|number} + */ + getAdjustedInt(block, atId, opt_delta, opt_negate) { + let delta = opt_delta || 0; + if (block.workspace.options.oneBasedIndex) { + delta--; + } + const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + const atOrder = delta ? this.ORDER_ADDITIVE : this.ORDER_NONE; + let at = this.valueToCode(block, atId, atOrder) || defaultAtIndex; + + if (stringUtils.isNumber(at)) { + // If the index is a naked number, adjust it right now. + at = parseInt(at, 10) + delta; + if (opt_negate) { + at = -at; + } + } else { + // If the index is dynamic, adjust it in code. + if (delta > 0) { + at = 'int(' + at + ' + ' + delta + ')'; + } else if (delta < 0) { + at = 'int(' + at + ' - ' + -delta + ')'; + } else { + at = 'int(' + at + ')'; + } + if (opt_negate) { + at = '-' + at; + } + } + return at; + } +} diff --git a/generators/python/text.js b/generators/python/text.js index 7f3621e95..84fef5515 100644 --- a/generators/python/text.js +++ b/generators/python/text.js @@ -13,16 +13,16 @@ goog.declareModuleId('Blockly.Python.texts'); import * as stringUtils from '../../core/utils/string.js'; import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['text'] = function(block, generator) { +export function text(block, generator) { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; }; -pythonGenerator.forBlock['text_multiline'] = function(block, generator) { +export function text_multiline(block, generator) { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); const order = @@ -50,7 +50,7 @@ const forceString = function(value) { return ['str(' + value + ')', Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_join'] = function(block, generator) { +export function text_join(block, generator) { // Create a string made up of any number of elements of any type. // Should we allow joining by '-' or ',' or any other characters? switch (block.itemCount_) { @@ -85,7 +85,7 @@ pythonGenerator.forBlock['text_join'] = function(block, generator) { } }; -pythonGenerator.forBlock['text_append'] = function(block, generator) { +export function text_append(block, generator) { // Append to a variable in place. const varName = generator.nameDB_.getName( @@ -94,20 +94,20 @@ pythonGenerator.forBlock['text_append'] = function(block, generator) { return varName + ' = str(' + varName + ') + ' + forceString(value)[0] + '\n'; }; -pythonGenerator.forBlock['text_length'] = function(block, generator) { +export function text_length(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return ['len(' + text + ')', Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_isEmpty'] = function(block, generator) { +export function text_isEmpty(block, generator) { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; const code = 'not len(' + text + ')'; return [code, Order.LOGICAL_NOT]; }; -pythonGenerator.forBlock['text_indexOf'] = function(block, generator) { +export function text_indexOf(block, generator) { // Search the text for a substring. // Should we allow for non-case sensitive??? const operator = block.getFieldValue('END') === 'FIRST' ? 'find' : 'rfind'; @@ -122,7 +122,7 @@ pythonGenerator.forBlock['text_indexOf'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_charAt'] = function(block, generator) { +export function text_charAt(block, generator) { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -163,7 +163,7 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(text): throw Error('Unhandled option (text_charAt).'); }; -pythonGenerator.forBlock['text_getSubstring'] = function(block, generator) { +export function text_getSubstring(block, generator) { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); @@ -213,7 +213,7 @@ pythonGenerator.forBlock['text_getSubstring'] = function(block, generator) { return [code, Order.MEMBER]; }; -pythonGenerator.forBlock['text_changeCase'] = function(block, generator) { +export function text_changeCase(block, generator) { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.upper()', @@ -226,7 +226,7 @@ pythonGenerator.forBlock['text_changeCase'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_trim'] = function(block, generator) { +export function text_trim(block, generator) { // Trim spaces. const OPERATORS = { 'LEFT': '.lstrip()', @@ -239,13 +239,13 @@ pythonGenerator.forBlock['text_trim'] = function(block, generator) { return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_print'] = function(block, generator) { +export function text_print(block, generator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ')\n'; }; -pythonGenerator.forBlock['text_prompt_ext'] = function(block, generator) { +export function text_prompt_ext(block, generator) { // Prompt function. const functionName = generator.provideFunction_('text_prompt', ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(msg): @@ -270,17 +270,16 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(msg): return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_prompt'] = - pythonGenerator.forBlock['text_prompt_ext']; +export const text_prompt = text_prompt_ext; -pythonGenerator.forBlock['text_count'] = function(block, generator) { +export function text_count(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; const code = text + '.count(' + sub + ')'; return [code, Order.FUNCTION_CALL]; }; -pythonGenerator.forBlock['text_replace'] = function(block, generator) { +export function text_replace(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; @@ -288,7 +287,7 @@ pythonGenerator.forBlock['text_replace'] = function(block, generator) { return [code, Order.MEMBER]; }; -pythonGenerator.forBlock['text_reverse'] = function(block, generator) { +export function text_reverse(block, generator) { const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const code = text + '[::-1]'; return [code, Order.MEMBER]; diff --git a/generators/python/variables.js b/generators/python/variables.js index 872bf3dbb..5228bb892 100644 --- a/generators/python/variables.js +++ b/generators/python/variables.js @@ -12,10 +12,10 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Python.variables'); import {NameType} from '../../core/names.js'; -import {pythonGenerator, Order} from '../python.js'; +import {Order} from './python_generator.js'; -pythonGenerator.forBlock['variables_get'] = function(block, generator) { +export function variables_get(block, generator) { // Variable getter. const code = generator.nameDB_.getName( @@ -23,7 +23,7 @@ pythonGenerator.forBlock['variables_get'] = function(block, generator) { return [code, Order.ATOMIC]; }; -pythonGenerator.forBlock['variables_set'] = function(block, generator) { +export function variables_set(block, generator) { // Variable setter. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; diff --git a/generators/python/variables_dynamic.js b/generators/python/variables_dynamic.js index 1925fc5a9..5a19faf47 100644 --- a/generators/python/variables_dynamic.js +++ b/generators/python/variables_dynamic.js @@ -11,10 +11,9 @@ import * as goog from '../../closure/goog/goog.js'; goog.declareModuleId('Blockly.Python.variablesDynamic'); -import {pythonGenerator} from '../python.js'; -import './variables.js'; - // generator is dynamically typed. -pythonGenerator.forBlock['variables_get_dynamic'] = pythonGenerator.forBlock['variables_get']; -pythonGenerator.forBlock['variables_set_dynamic'] = pythonGenerator.forBlock['variables_set']; +export { + variables_get as variables_get_dynamic, + variables_set as variables_set_dynamic, +} from './variables.js'; diff --git a/scripts/gulpfiles/build_tasks.js b/scripts/gulpfiles/build_tasks.js index 82404f299..678925105 100644 --- a/scripts/gulpfiles/build_tasks.js +++ b/scripts/gulpfiles/build_tasks.js @@ -110,35 +110,35 @@ const chunks = [ }, { name: 'javascript', - entry: path.join(TSC_OUTPUT_DIR, 'generators', 'javascript', 'all.js'), + entry: path.join(TSC_OUTPUT_DIR, 'generators', 'javascript.js'), exports: 'module$build$src$generators$javascript', scriptExport: 'javascript', scriptNamedExports: {'Blockly.Javascript': 'javascriptGenerator'}, }, { name: 'python', - entry: path.join(TSC_OUTPUT_DIR, 'generators', 'python', 'all.js'), + entry: path.join(TSC_OUTPUT_DIR, 'generators', 'python.js'), exports: 'module$build$src$generators$python', scriptExport: 'python', scriptNamedExports: {'Blockly.Python': 'pythonGenerator'}, }, { name: 'php', - entry: path.join(TSC_OUTPUT_DIR, 'generators', 'php', 'all.js'), + entry: path.join(TSC_OUTPUT_DIR, 'generators', 'php.js'), exports: 'module$build$src$generators$php', scriptExport: 'php', scriptNamedExports: {'Blockly.PHP': 'phpGenerator'}, }, { name: 'lua', - entry: path.join(TSC_OUTPUT_DIR, 'generators', 'lua', 'all.js'), + entry: path.join(TSC_OUTPUT_DIR, 'generators', 'lua.js'), exports: 'module$build$src$generators$lua', scriptExport: 'lua', scriptNameExports: {'Blockly.Lua': 'luaGenerator'}, }, { name: 'dart', - entry: path.join(TSC_OUTPUT_DIR, 'generators', 'dart', 'all.js'), + entry: path.join(TSC_OUTPUT_DIR, 'generators', 'dart.js'), exports: 'module$build$src$generators$dart', scriptExport: 'dart', scriptNameExports: {'Blockly.Dart': 'dartGenerator'}, diff --git a/scripts/migration/renamings.json5 b/scripts/migration/renamings.json5 index d01907939..532ab6a1f 100644 --- a/scripts/migration/renamings.json5 +++ b/scripts/migration/renamings.json5 @@ -1376,6 +1376,8 @@ oldName: 'Blockly.Python', newExport: 'pythonGenerator', newPath: 'Blockly.Python', + }, + { oldName: 'Blockly.ContextMenuRegistry', exports: { 'ContextMenuRegistry.ScopeType': { @@ -1510,5 +1512,61 @@ }, }, }, + // The following renamings serve two purposes: + // - Record that the langGenerator instances have moved to a + // different module (though this is not actually actioned by the + // renaming script). + // - Record a change of path, applicable only to code that imports + // generators via