mirror of
https://github.com/google/blockly.git
synced 2025-12-15 13:50:08 +01:00
* chore: add configuration for api extractor * fix: remove extra param names * chore: private to internal * remove unrestricted * chore: remove double backticks * chore: remove fileoverview and export * as * chore: return to returns * chore: fix backslashes and angle brackets in tsdoc * chore: final to sealed * chore: ignore to internal * chore: fix link tags * chore: add api-extractor configuration * chore: add unrecognized tag names * chore: remove tsdoc-metadata * fix: correct index.d.ts * chore: fix connection link
540 lines
19 KiB
TypeScript
540 lines
19 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2012 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Utility functions for generating executable code from
|
|
* Blockly code.
|
|
*
|
|
* @class
|
|
*/
|
|
import * as goog from '../closure/goog/goog.js';
|
|
goog.declareModuleId('Blockly.Generator');
|
|
|
|
import type {Block} from './block.js';
|
|
import * as common from './common.js';
|
|
import {Names, NameType} from './names.js';
|
|
import * as deprecation from './utils/deprecation.js';
|
|
import type {Workspace} from './workspace.js';
|
|
|
|
|
|
/**
|
|
* Class for a code generator that translates the blocks into a language.
|
|
*
|
|
* @unrestricted
|
|
* @alias Blockly.Generator
|
|
*/
|
|
export class Generator {
|
|
name_: string;
|
|
|
|
/**
|
|
* This is used as a placeholder in functions defined using
|
|
* Generator.provideFunction_. It must not be legal code that could
|
|
* legitimately appear in a function definition (or comment), and it must
|
|
* not confuse the regular expression parser.
|
|
*/
|
|
protected FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
|
|
FUNCTION_NAME_PLACEHOLDER_REGEXP_: RegExp;
|
|
|
|
/**
|
|
* Arbitrary code to inject into locations that risk causing infinite loops.
|
|
* Any instances of '%1' will be replaced by the block ID that failed.
|
|
* E.g. ` checkTimeout(%1);\n`
|
|
*/
|
|
INFINITE_LOOP_TRAP: string|null = null;
|
|
|
|
/**
|
|
* Arbitrary code to inject before every statement.
|
|
* Any instances of '%1' will be replaced by the block ID of the statement.
|
|
* E.g. `highlight(%1);\n`
|
|
*/
|
|
STATEMENT_PREFIX: string|null = null;
|
|
|
|
/**
|
|
* Arbitrary code to inject after every statement.
|
|
* Any instances of '%1' will be replaced by the block ID of the statement.
|
|
* E.g. `highlight(%1);\n`
|
|
*/
|
|
STATEMENT_SUFFIX: string|null = null;
|
|
|
|
/**
|
|
* The method of indenting. Defaults to two spaces, but language generators
|
|
* may override this to increase indent or change to tabs.
|
|
*/
|
|
INDENT = ' ';
|
|
|
|
/**
|
|
* Maximum length for a comment before wrapping. Does not account for
|
|
* indenting level.
|
|
*/
|
|
COMMENT_WRAP = 60;
|
|
|
|
/** List of outer-inner pairings that do NOT require parentheses. */
|
|
ORDER_OVERRIDES: number[][] = [];
|
|
|
|
/**
|
|
* Whether the init method has been called.
|
|
* Generators that set this flag to false after creation and true in init
|
|
* will cause blockToCode to emit a warning if the generator has not been
|
|
* initialized. If this flag is untouched, it will have no effect.
|
|
*/
|
|
isInitialized: boolean|null = null;
|
|
|
|
/** Comma-separated list of reserved words. */
|
|
protected RESERVED_WORDS_ = '';
|
|
|
|
/** A dictionary of definitions to be printed before the code. */
|
|
protected definitions_: {[key: string]: string} = Object.create(null);
|
|
|
|
/**
|
|
* A dictionary mapping desired function names in definitions_ to actual
|
|
* function names (to avoid collisions with user functions).
|
|
*/
|
|
protected functionNames_: {[key: string]: string} = Object.create(null);
|
|
|
|
/** A database of variable and procedure names. */
|
|
protected nameDB_?: Names = undefined;
|
|
|
|
/** @param name Language name of this generator. */
|
|
constructor(name: string) {
|
|
this.name_ = name;
|
|
|
|
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ =
|
|
new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g');
|
|
}
|
|
|
|
/**
|
|
* Generate code for all blocks in the workspace to the specified language.
|
|
*
|
|
* @param workspace Workspace to generate code from.
|
|
* @returns Generated code.
|
|
*/
|
|
workspaceToCode(workspace?: Workspace): string {
|
|
if (!workspace) {
|
|
// Backwards compatibility from before there could be multiple workspaces.
|
|
console.warn(
|
|
'No workspace specified in workspaceToCode call. Guessing.');
|
|
workspace = common.getMainWorkspace();
|
|
}
|
|
const code = [];
|
|
this.init(workspace);
|
|
const blocks = workspace.getTopBlocks(true);
|
|
for (let i = 0, block; block = blocks[i]; i++) {
|
|
let line = this.blockToCode(block);
|
|
if (Array.isArray(line)) {
|
|
// Value blocks return tuples of code and operator order.
|
|
// Top-level blocks don't care about operator order.
|
|
line = line[0];
|
|
}
|
|
if (line) {
|
|
if (block.outputConnection) {
|
|
// This block is a naked value. Ask the language's code generator if
|
|
// it wants to append a semicolon, or something.
|
|
line = this.scrubNakedValue(line);
|
|
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
|
|
line = this.injectId(this.STATEMENT_PREFIX, block) + line;
|
|
}
|
|
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
|
|
line = line + this.injectId(this.STATEMENT_SUFFIX, block);
|
|
}
|
|
}
|
|
code.push(line);
|
|
}
|
|
}
|
|
// Blank line between each section.
|
|
let codeString = code.join('\n');
|
|
codeString = this.finish(codeString);
|
|
// Final scrubbing of whitespace.
|
|
codeString = codeString.replace(/^\s+\n/, '');
|
|
codeString = codeString.replace(/\n\s+$/, '\n');
|
|
codeString = codeString.replace(/[ \t]+\n/g, '\n');
|
|
return codeString;
|
|
}
|
|
|
|
// The following are some helpful functions which can be used by multiple
|
|
|
|
// languages.
|
|
|
|
/**
|
|
* Prepend a common prefix onto each line of code.
|
|
* Intended for indenting code or adding comment markers.
|
|
*
|
|
* @param text The lines of code.
|
|
* @param prefix The common prefix.
|
|
* @returns The prefixed lines of code.
|
|
*/
|
|
prefixLines(text: string, prefix: string): string {
|
|
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
|
|
}
|
|
|
|
/**
|
|
* Recursively spider a tree of blocks, returning all their comments.
|
|
*
|
|
* @param block The block from which to start spidering.
|
|
* @returns Concatenated list of comments.
|
|
*/
|
|
allNestedComments(block: Block): string {
|
|
const comments = [];
|
|
const blocks = block.getDescendants(true);
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
const comment = blocks[i].getCommentText();
|
|
if (comment) {
|
|
comments.push(comment);
|
|
}
|
|
}
|
|
// Append an empty string to create a trailing line break when joined.
|
|
if (comments.length) {
|
|
comments.push('');
|
|
}
|
|
return comments.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Generate code for the specified block (and attached blocks).
|
|
* The generator must be initialized before calling this function.
|
|
*
|
|
* @param block The block to generate code for.
|
|
* @param opt_thisOnly True to generate code for only this statement.
|
|
* @returns For statement blocks, the generated code.
|
|
* For value blocks, an array containing the generated code and an
|
|
* operator order value. Returns '' if block is null.
|
|
*/
|
|
blockToCode(block: Block|null, opt_thisOnly?: boolean): string
|
|
|[string, number] {
|
|
if (this.isInitialized === false) {
|
|
console.warn(
|
|
'Generator init was not called before blockToCode was called.');
|
|
}
|
|
if (!block) {
|
|
return '';
|
|
}
|
|
if (!block.isEnabled()) {
|
|
// Skip past this block if it is disabled.
|
|
return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock());
|
|
}
|
|
if (block.isInsertionMarker()) {
|
|
// Skip past insertion markers.
|
|
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
|
|
}
|
|
|
|
const func = (this as any)[block.type];
|
|
if (typeof func !== 'function') {
|
|
throw Error(
|
|
'Language "' + this.name_ + '" does not know how to generate ' +
|
|
'code for block type "' + block.type + '".');
|
|
}
|
|
// First argument to func.call is the value of 'this' in the generator.
|
|
// Prior to 24 September 2013 'this' was the only way to access the block.
|
|
// The current preferred method of accessing the block is through the second
|
|
// argument to func.call, which becomes the first parameter to the
|
|
// generator.
|
|
let code = func.call(block, block);
|
|
if (Array.isArray(code)) {
|
|
// Value blocks return tuples of code and operator order.
|
|
if (!block.outputConnection) {
|
|
throw TypeError('Expecting string from statement block: ' + block.type);
|
|
}
|
|
return [this.scrub_(block, code[0], opt_thisOnly), code[1]];
|
|
} else if (typeof code === 'string') {
|
|
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
|
|
code = this.injectId(this.STATEMENT_PREFIX, block) + code;
|
|
}
|
|
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
|
|
code = code + this.injectId(this.STATEMENT_SUFFIX, block);
|
|
}
|
|
return this.scrub_(block, code, opt_thisOnly);
|
|
} else if (code === null) {
|
|
// Block has handled code generation itself.
|
|
return '';
|
|
}
|
|
throw SyntaxError('Invalid code generated: ' + code);
|
|
}
|
|
|
|
/**
|
|
* Generate code representing the specified value input.
|
|
*
|
|
* @param block The block containing the input.
|
|
* @param name The name of the input.
|
|
* @param outerOrder The maximum binding strength (minimum order value) of any
|
|
* operators adjacent to "block".
|
|
* @returns Generated code or '' if no blocks are connected or the specified
|
|
* input does not exist.
|
|
*/
|
|
valueToCode(block: Block, name: string, outerOrder: number): string {
|
|
if (isNaN(outerOrder)) {
|
|
throw TypeError('Expecting valid order from block: ' + block.type);
|
|
}
|
|
const targetBlock = block.getInputTargetBlock(name);
|
|
if (!targetBlock) {
|
|
return '';
|
|
}
|
|
const tuple = this.blockToCode(targetBlock);
|
|
if (tuple === '') {
|
|
// Disabled block.
|
|
return '';
|
|
}
|
|
// Value blocks must return code and order of operations info.
|
|
// Statement blocks must only return code.
|
|
if (!Array.isArray(tuple)) {
|
|
throw TypeError('Expecting tuple from value block: ' + targetBlock.type);
|
|
}
|
|
let code = tuple[0];
|
|
const innerOrder = tuple[1];
|
|
if (isNaN(innerOrder)) {
|
|
throw TypeError(
|
|
'Expecting valid order from value block: ' + targetBlock.type);
|
|
}
|
|
if (!code) {
|
|
return '';
|
|
}
|
|
|
|
// Add parentheses if needed.
|
|
let parensNeeded = false;
|
|
const outerOrderClass = Math.floor(outerOrder);
|
|
const innerOrderClass = Math.floor(innerOrder);
|
|
if (outerOrderClass <= innerOrderClass) {
|
|
if (outerOrderClass === innerOrderClass &&
|
|
(outerOrderClass === 0 || outerOrderClass === 99)) {
|
|
// Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs.
|
|
// 0 is the atomic order, 99 is the none order. No parentheses needed.
|
|
// In all known languages multiple such code blocks are not order
|
|
// sensitive. In fact in Python ('a' 'b') 'c' would fail.
|
|
} else {
|
|
// The operators outside this code are stronger than the operators
|
|
// inside this code. To prevent the code from being pulled apart,
|
|
// wrap the code in parentheses.
|
|
parensNeeded = true;
|
|
// Check for special exceptions.
|
|
for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) {
|
|
if (this.ORDER_OVERRIDES[i][0] === outerOrder &&
|
|
this.ORDER_OVERRIDES[i][1] === innerOrder) {
|
|
parensNeeded = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (parensNeeded) {
|
|
// Technically, this should be handled on a language-by-language basis.
|
|
// However all known (sane) languages use parentheses for grouping.
|
|
code = '(' + code + ')';
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Generate a code string representing the blocks attached to the named
|
|
* statement input. Indent the code.
|
|
* This is mainly used in generators. When trying to generate code to evaluate
|
|
* look at using workspaceToCode or blockToCode.
|
|
*
|
|
* @param block The block containing the input.
|
|
* @param name The name of the input.
|
|
* @returns Generated code or '' if no blocks are connected.
|
|
*/
|
|
statementToCode(block: Block, name: string): string {
|
|
const targetBlock = block.getInputTargetBlock(name);
|
|
let code = this.blockToCode(targetBlock);
|
|
// Value blocks must return code and order of operations info.
|
|
// Statement blocks must only return code.
|
|
if (typeof code !== 'string') {
|
|
throw TypeError(
|
|
'Expecting code from statement block: ' +
|
|
(targetBlock && targetBlock.type));
|
|
}
|
|
if (code) {
|
|
code = this.prefixLines((code), this.INDENT);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Add an infinite loop trap to the contents of a loop.
|
|
* Add statement suffix at the start of the loop block (right after the loop
|
|
* statement executes), and a statement prefix to the end of the loop block
|
|
* (right before the loop statement executes).
|
|
*
|
|
* @param branch Code for loop contents.
|
|
* @param block Enclosing block.
|
|
* @returns Loop contents, with infinite loop trap added.
|
|
*/
|
|
addLoopTrap(branch: string, block: Block): string {
|
|
if (this.INFINITE_LOOP_TRAP) {
|
|
branch = this.prefixLines(
|
|
this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) +
|
|
branch;
|
|
}
|
|
if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) {
|
|
branch = this.prefixLines(
|
|
this.injectId(this.STATEMENT_SUFFIX, block), this.INDENT) +
|
|
branch;
|
|
}
|
|
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
|
|
branch = branch +
|
|
this.prefixLines(
|
|
this.injectId(this.STATEMENT_PREFIX, block), this.INDENT);
|
|
}
|
|
return branch;
|
|
}
|
|
|
|
/**
|
|
* Inject a block ID into a message to replace '%1'.
|
|
* Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP.
|
|
*
|
|
* @param msg Code snippet with '%1'.
|
|
* @param block Block which has an ID.
|
|
* @returns Code snippet with ID.
|
|
*/
|
|
injectId(msg: string, block: Block): string {
|
|
const id = block.id.replace(/\$/g, '$$$$'); // Issue 251.
|
|
return msg.replace(/%1/g, '\'' + id + '\'');
|
|
}
|
|
|
|
/**
|
|
* Add one or more words to the list of reserved words for this language.
|
|
*
|
|
* @param words Comma-separated list of words to add to the list.
|
|
* No spaces. Duplicates are ok.
|
|
*/
|
|
addReservedWords(words: string) {
|
|
this.RESERVED_WORDS_ += words + ',';
|
|
}
|
|
|
|
/**
|
|
* Define a developer-defined function (not a user-defined procedure) to be
|
|
* included in the generated code. Used for creating private helper
|
|
* functions. The first time this is called with a given desiredName, the code
|
|
* is saved and an actual name is generated. Subsequent calls with the same
|
|
* desiredName have no effect but have the same return value.
|
|
*
|
|
* It is up to the caller to make sure the same desiredName is not
|
|
* used for different helper functions (e.g. use "colourRandom" and
|
|
* "listRandom", not "random"). There is no danger of colliding with reserved
|
|
* words, or user-defined variable or procedure names.
|
|
*
|
|
* The code gets output when Generator.finish() is called.
|
|
*
|
|
* @param desiredName The desired name of the function (e.g. mathIsPrime).
|
|
* @param code A list of statements or one multi-line code string. Use ' '
|
|
* for indents (they will be replaced).
|
|
* @returns The actual name of the new function. This may differ from
|
|
* desiredName if the former has already been taken by the user.
|
|
*/
|
|
protected provideFunction_(desiredName: string, code: string[]|string):
|
|
string {
|
|
if (!this.definitions_[desiredName]) {
|
|
const functionName =
|
|
this.nameDB_!.getDistinctName(desiredName, NameType.PROCEDURE);
|
|
this.functionNames_[desiredName] = functionName;
|
|
if (Array.isArray(code)) {
|
|
code = code.join('\n');
|
|
}
|
|
let codeText = code.trim().replace(
|
|
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName);
|
|
// Change all ' ' indents into the desired indent.
|
|
// To avoid an infinite loop of replacements, change all indents to '\0'
|
|
// character first, then replace them all with the indent.
|
|
// We are assuming that no provided functions contain a literal null char.
|
|
let oldCodeText;
|
|
while (oldCodeText !== codeText) {
|
|
oldCodeText = codeText;
|
|
codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0');
|
|
}
|
|
codeText = codeText.replace(/\0/g, this.INDENT);
|
|
this.definitions_[desiredName] = codeText;
|
|
}
|
|
return this.functionNames_[desiredName];
|
|
}
|
|
|
|
/**
|
|
* Hook for code to run before code generation starts.
|
|
* Subclasses may override this, e.g. to initialise the database of variable
|
|
* names.
|
|
*
|
|
* @param _workspace Workspace to generate code from.
|
|
*/
|
|
init(_workspace: Workspace) {
|
|
// Optionally override
|
|
// Create a dictionary of definitions to be printed before the code.
|
|
this.definitions_ = Object.create(null);
|
|
// Create a dictionary mapping desired developer-defined function names in
|
|
// definitions_ to actual function names (to avoid collisions with
|
|
// user-defined procedures).
|
|
this.functionNames_ = Object.create(null);
|
|
}
|
|
|
|
/**
|
|
* Common tasks for generating code from blocks. This is called from
|
|
* blockToCode and is called on every block, not just top level blocks.
|
|
* Subclasses may override this, e.g. to generate code for statements
|
|
* following the block, or to handle comments for the specified block and any
|
|
* connected value blocks.
|
|
*
|
|
* @param _block The current block.
|
|
* @param code The code created for this block.
|
|
* @param _opt_thisOnly True to generate code for only this statement.
|
|
* @returns Code with comments and subsequent blocks added.
|
|
*/
|
|
protected scrub_(_block: Block, code: string, _opt_thisOnly?: boolean):
|
|
string {
|
|
// Optionally override
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Hook for code to run at end of code generation.
|
|
* Subclasses may override this, e.g. to prepend the generated code with
|
|
* import statements or variable definitions.
|
|
*
|
|
* @param code Generated code.
|
|
* @returns Completed code.
|
|
*/
|
|
finish(code: string): string {
|
|
// Optionally override
|
|
// Clean up temporary data.
|
|
this.definitions_ = Object.create(null);
|
|
this.functionNames_ = Object.create(null);
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Naked values are top-level blocks with outputs that aren't plugged into
|
|
* anything.
|
|
* Subclasses may override this, e.g. if their language does not allow
|
|
* naked values.
|
|
*
|
|
* @param line Line of generated code.
|
|
* @returns Legal line of code.
|
|
*/
|
|
scrubNakedValue(line: string): string {
|
|
// Optionally override
|
|
return line;
|
|
}
|
|
}
|
|
|
|
Object.defineProperties(Generator.prototype, {
|
|
/**
|
|
* A database of variable names.
|
|
*
|
|
* @name Blockly.Generator.prototype.variableDB_
|
|
* @deprecated 'variableDB_' was renamed to 'nameDB_' (May 2021).
|
|
* @suppress {checkTypes}
|
|
*/
|
|
variableDB_: ({
|
|
/** @returns Name database. */
|
|
get(this: Generator): Names |
|
|
undefined {
|
|
deprecation.warn(
|
|
'variableDB_', 'May 2021', 'September 2022', 'nameDB_');
|
|
return this.nameDB_;
|
|
},
|
|
/** @param nameDb New name database. */
|
|
set(this: Generator, nameDb: Names|undefined) {
|
|
deprecation.warn('variableDB_', 'May 2021', 'September2022', 'nameDB_');
|
|
this.nameDB_ = nameDb;
|
|
},
|
|
}),
|
|
});
|