diff --git a/.github/workflows/assign_reviewers.yml b/.github/workflows/assign_reviewers.yml index 765f0e655..33bd9e778 100644 --- a/.github/workflows/assign_reviewers.yml +++ b/.github/workflows/assign_reviewers.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Assign requested reviewer - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | try { diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 9b832e246..3675af7b0 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -19,7 +19,7 @@ jobs: # TODO (#2114): re-enable osx build. # os: [ubuntu-latest, macos-latest] os: [macos-latest] - node-version: [16.x, 18.x, 20.x] + node-version: [18.x, 20.x] # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ @@ -34,7 +34,7 @@ jobs: ssh://git@github.com/ - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85866cb2e..ad62722e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: # TODO (#2114): re-enable osx build. # os: [ubuntu-latest, macos-latest] os: [ubuntu-latest] - node-version: [16.x, 18.x, 20.x] + node-version: [18.x, 20.x] # See supported Node.js release schedule at # https://nodejs.org/en/about/releases/ @@ -33,7 +33,7 @@ jobs: ssh://git@github.com/ - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20.x diff --git a/.github/workflows/tag_module_cleanup.yml b/.github/workflows/tag_module_cleanup.yml index e0186af67..d83d0e937 100644 --- a/.github/workflows/tag_module_cleanup.yml +++ b/.github/workflows/tag_module_cleanup.yml @@ -15,7 +15,7 @@ jobs: # Add the type: cleanup label runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: script: | // Note that pull requests are considered issues and "shared" diff --git a/.github/workflows/welcome_new_contributors.yml b/.github/workflows/welcome_new_contributors.yml index 6e598ee4a..37ca9ef89 100644 --- a/.github/workflows/welcome_new_contributors.yml +++ b/.github/workflows/welcome_new_contributors.yml @@ -16,22 +16,21 @@ jobs: Welcome! It looks like this is your first pull request in Blockly, so here are a couple of tips: - - You can find tips about contributing to Blockly and how to - validate your changes on our - [developer site](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change). + - You can find tips about contributing to Blockly and how to + validate your changes on our + [developer site](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change). - - All contributors must sign the Google Contributor License - Agreement (CLA). If the google-cla bot leaves a comment on this - PR, make sure you follow the instructions. + - All contributors must sign the Google Contributor License + Agreement (CLA). If the google-cla bot leaves a comment on this + PR, make sure you follow the instructions. - - We use [conventional commits](https://www.conventionalcommits.org/) - to make versioning the package easier. Make sure your commit - message is in the [proper format](https://developers.google.com/blockly/guides/contribute/get-started/commits) - or [learn how to fix it](https://developers.google.com/blockly/guides/contribute/get-started/commits#fixing_non-conventional_commits). - - - If any of the other checks on this PR fail, you can click on - them to learn why. It might be that your change caused a test - failure, or that you need to double-check the - [style guide](https://developers.google.com/blockly/guides/contribute/core/style_guide). + - We use conventional commits to make versioning the package easier. Make sure your commit + message is in the [proper format](https://developers.google.com/blockly/guides/contribute/get-started/commits) + or [learn how to fix it](https://developers.google.com/blockly/guides/contribute/get-started/commits#fixing_non-conventional_commits). + + - If any of the other checks on this PR fail, you can click on + them to learn why. It might be that your change caused a test + failure, or that you need to double-check the + [style guide](https://developers.google.com/blockly/guides/contribute/core/style_guide). Thank you for opening this PR! A member of the Blockly team will review it soon. diff --git a/.prettierignore b/.prettierignore index 64dd749f1..9d52f19fe 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,7 +18,6 @@ # Demos, scripts, misc /node_modules/* -/generators/* /demos/* /appengine/* /externs/* @@ -27,5 +26,5 @@ CHANGELOG.md PULL_REQUEST_TEMPLATE.md -# Don't bother formatting js blocks since we're getting rid of them -/blocks/*.js +# Don't bother formatting JavaScript files we're about to migrate: +/generators/**/*.js diff --git a/blocks/lists.ts b/blocks/lists.ts index a85e39352..d115a321c 100644 --- a/blocks/lists.ts +++ b/blocks/lists.ts @@ -111,8 +111,12 @@ export const blocks = createBlockDefinitionsFromJsonArray([ }, ]); -/** Type of a 'lists_create_with' block. */ -type CreateWithBlock = Block & ListCreateWithMixin; +/** + * Type of a 'lists_create_with' block. + * + * @internal + */ +export type CreateWithBlock = Block & ListCreateWithMixin; interface ListCreateWithMixin extends ListCreateWithMixinType { itemCount_: number; } diff --git a/blocks/loops.ts b/blocks/loops.ts index e70fdae31..02d9d34be 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -325,8 +325,12 @@ export const loopTypes: Set = new Set([ 'controls_whileUntil', ]); -/** Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN */ -type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin; +/** + * Type of a block that has CONTROL_FLOW_IN_LOOP_CHECK_MIXIN + * + * @internal + */ +export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin; interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {} type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN; diff --git a/blocks/procedures.ts b/blocks/procedures.ts index 46109fa27..7150bda8a 100644 --- a/blocks/procedures.ts +++ b/blocks/procedures.ts @@ -1209,8 +1209,12 @@ blocks['procedures_callreturn'] = { defType_: 'procedures_defreturn', }; -/** Type of a procedures_ifreturn block. */ -type IfReturnBlock = Block & IfReturnMixin; +/** + * Type of a procedures_ifreturn block. + * + * @internal + */ +export type IfReturnBlock = Block & IfReturnMixin; interface IfReturnMixin extends IfReturnMixinType { hasReturnValue_: boolean; } diff --git a/blocks/text.ts b/blocks/text.ts index eba64d9ab..824da782b 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -725,8 +725,12 @@ const QUOTES_EXTENSION = function (this: QuoteImageBlock) { this.quoteField_('TEXT'); }; -/** Type of a block that has TEXT_JOIN_MUTATOR_MIXIN */ -type JoinMutatorBlock = BlockSvg & JoinMutatorMixin & QuoteImageMixin; +/** + * Type of a block that has TEXT_JOIN_MUTATOR_MIXIN + * + * @internal + */ +export type JoinMutatorBlock = BlockSvg & JoinMutatorMixin & QuoteImageMixin; interface JoinMutatorMixin extends JoinMutatorMixinType {} type JoinMutatorMixinType = typeof JOIN_MUTATOR_MIXIN; @@ -1018,6 +1022,8 @@ Extensions.register('text_indexOf_tooltip', INDEXOF_TOOLTIP_EXTENSION); Extensions.register('text_quotes', QUOTES_EXTENSION); +Extensions.registerMixin('quote_image_mixin', QUOTE_IMAGE_MIXIN); + Extensions.registerMutator( 'text_join_mutator', JOIN_MUTATOR_MIXIN, diff --git a/core/block.ts b/core/block.ts index 927d74b0c..8f64b20a2 100644 --- a/core/block.ts +++ b/core/block.ts @@ -865,6 +865,8 @@ export class Block implements IASTNodeLocation, IDeletable { /** * Set whether this block is a shadow block or not. + * This method is internal and should not be called by users of Blockly. To + * create shadow blocks programmatically call connection.setShadowState * * @param shadow True if a shadow. * @internal @@ -1641,6 +1643,18 @@ export class Block implements IASTNodeLocation, IDeletable { ); } + // Validate that each arg has a corresponding message + let n = 0; + while (json['args' + n]) { + if (json['message' + n] === undefined) { + throw Error( + warningPrefix + + `args${n} must have a corresponding message (message${n}).`, + ); + } + n++; + } + // Set basic properties of block. // Makes styles backward compatible with old way of defining hat style. if (json['style'] && json['style'].hat) { @@ -2208,6 +2222,18 @@ export class Block implements IASTNodeLocation, IDeletable { const comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | null; const oldText = comment?.getText() ?? null; if (oldText === text) return; + if (text !== null) { + let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined; + if (!comment) { + comment = this.addIcon(new CommentIcon(this)); + } + eventUtils.disable(); + comment.setText(text); + eventUtils.enable(); + } else { + this.removeIcon(CommentIcon.TYPE); + } + eventUtils.fire( new (eventUtils.get(eventUtils.BLOCK_CHANGE))( this, @@ -2217,16 +2243,6 @@ export class Block implements IASTNodeLocation, IDeletable { text, ), ); - - if (text !== null) { - let comment = this.getIcon(CommentIcon.TYPE) as CommentIcon | undefined; - if (!comment) { - comment = this.addIcon(new CommentIcon(this)); - } - comment.setText(text); - } else { - this.removeIcon(CommentIcon.TYPE); - } } /** diff --git a/core/block_dragger.ts b/core/block_dragger.ts index 2d72d55de..78c1381ae 100644 --- a/core/block_dragger.ts +++ b/core/block_dragger.ts @@ -29,6 +29,8 @@ import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import type {WorkspaceSvg} from './workspace_svg.js'; import {hasBubble} from './interfaces/i_has_bubble.js'; +import * as deprecation from './utils/deprecation.js'; +import * as layers from './layers.js'; /** * Class for a block dragger. It moves blocks around the workspace when they @@ -48,7 +50,12 @@ export class BlockDragger implements IBlockDragger { /** Whether the block would be deleted if dropped immediately. */ protected wouldDeleteBlock_ = false; protected startXY_: Coordinate; - protected dragIconData_: IconPositionData[]; + + /** + * @deprecated To be removed in v11. Updating icons is now handled by the + * block's `moveDuringDrag` method. + */ + protected dragIconData_: IconPositionData[] = []; /** * @param block The block to drag. @@ -70,11 +77,6 @@ export class BlockDragger implements IBlockDragger { */ this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY(); - /** - * A list of all of the icons (comment, warning, and mutator) that are - * on this block and its descendants. Moving an icon moves the bubble that - * extends from it if that bubble is open. - */ this.dragIconData_ = initIconData(block, this.startXY_); } @@ -85,7 +87,6 @@ export class BlockDragger implements IBlockDragger { */ dispose() { this.dragIconData_.length = 0; - if (this.draggedConnectionManager_) { this.draggedConnectionManager_.dispose(); } @@ -119,6 +120,7 @@ export class BlockDragger implements IBlockDragger { this.disconnectBlock_(healStack, currentDragDeltaXY); } this.draggingBlock_.setDragging(true); + this.workspace_.getLayerManager()?.moveToDragLayer(this.draggingBlock_); } /** @@ -178,7 +180,6 @@ export class BlockDragger implements IBlockDragger { const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const newLoc = Coordinate.sum(this.startXY_, delta); this.draggingBlock_.moveDuringDrag(newLoc); - this.dragIcons_(delta); const oldDragTarget = this.dragTarget_; this.dragTarget_ = this.workspace_.getDragTarget(e); @@ -210,7 +211,6 @@ export class BlockDragger implements IBlockDragger { endDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) { // Make sure internal state is fresh. this.drag(e, currentDragDeltaXY); - this.dragIconData_ = []; this.fireDragEndEvent_(); dom.stopTextWidthCache(); @@ -233,6 +233,9 @@ export class BlockDragger implements IBlockDragger { const deleted = this.maybeDeleteBlock_(); if (!deleted) { // These are expensive and don't need to be done if we're deleting. + this.workspace_ + .getLayerManager() + ?.moveOffDragLayer(this.draggingBlock_, layers.BLOCK); this.draggingBlock_.setDragging(false); if (delta) { // !preventMove @@ -398,14 +401,11 @@ export class BlockDragger implements IBlockDragger { /** * Move all of the icons connected to this drag. * - * @param dxy How far to move the icons from their original positions, in - * workspace units. + * @deprecated To be removed in v11. This is now handled by the block's + * `moveDuringDrag` method. */ - protected dragIcons_(dxy: Coordinate) { - // Moving icons moves their associated bubbles. - for (const data of this.dragIconData_) { - data.icon.onLocationChange(Coordinate.sum(data.location, dxy)); - } + protected dragIcons_() { + deprecation.warn('Blockly.BlockDragger.prototype.dragIcons_', 'v10', 'v11'); } /** diff --git a/core/block_svg.ts b/core/block_svg.ts index b419122ba..2655642d7 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -154,6 +154,9 @@ export class BlockSvg */ private bumpNeighboursPid = 0; + /** Whether this block is currently being dragged. */ + private dragging = false; + /** * The location of the top left of this block (in workspace coordinates) * relative to either its parent block, or the workspace origin if it has no @@ -351,6 +354,12 @@ export class BlockSvg * @returns Object with .x and .y properties in workspace coordinates. */ override getRelativeToSurfaceXY(): Coordinate { + const layerManger = this.workspace.getLayerManager(); + if (!layerManger) { + throw new Error( + 'Cannot calculate position because the workspace has not been appended', + ); + } let x = 0; let y = 0; @@ -362,7 +371,7 @@ export class BlockSvg x += xy.x; y += xy.y; element = element.parentNode as SVGElement; - } while (element && element !== this.workspace.getCanvas()); + } while (element && !layerManger.hasLayer(element)); } return new Coordinate(x, y); } @@ -384,9 +393,13 @@ export class BlockSvg event = new (eventUtils.get(eventUtils.BLOCK_MOVE)!)(this) as BlockMove; reason && event.setReason(reason); } - const xy = this.getRelativeToSurfaceXY(); - this.translate(xy.x + dx, xy.y + dy); - this.moveConnections(dx, dy); + + const delta = new Coordinate(dx, dy); + const currLoc = this.getRelativeToSurfaceXY(); + const newLoc = Coordinate.sum(currLoc, delta); + this.translate(newLoc.x, newLoc.y); + this.updateComponentLocations(newLoc); + if (eventsEnabled && event) { event!.recordNew(); eventUtils.fire(event); @@ -437,6 +450,7 @@ export class BlockSvg moveDuringDrag(newLoc: Coordinate) { this.translate(newLoc.x, newLoc.y); this.getSvgRoot().setAttribute('transform', this.getTranslation()); + this.updateComponentLocations(newLoc); } /** Snap this block to the nearest grid point. */ @@ -649,32 +663,47 @@ export class BlockSvg } /** - * Move the connections for this block and all blocks attached under it. - * Also update any attached bubbles. + * Updates the locations of any parts of the block that need to know where + * they are (e.g. connections, icons). * - * @param dx Horizontal offset from current location, in workspace units. - * @param dy Vertical offset from current location, in workspace units. + * @param blockOrigin The top-left of this block in workspace coordinates. * @internal */ - moveConnections(dx: number, dy: number) { + updateComponentLocations(blockOrigin: Coordinate) { if (!this.rendered) { // Rendering is required to lay out the blocks. // This is probably an invisible block attached to a collapsed block. return; } - const myConnections = this.getConnections_(false); - for (let i = 0; i < myConnections.length; i++) { - myConnections[i].moveBy(dx, dy); - } - const icons = this.getIcons(); - const pos = this.getRelativeToSurfaceXY(); - for (const icon of icons) { - icon.onLocationChange(pos); - } - // Recurse through all blocks attached under this one. - for (let i = 0; i < this.childBlocks_.length; i++) { - (this.childBlocks_[i] as BlockSvg).moveConnections(dx, dy); + if (!this.dragging) this.updateConnectionLocations(blockOrigin); + this.updateIconLocations(blockOrigin); + this.updateFieldLocations(blockOrigin); + + for (const child of this.getChildren(false)) { + child.updateComponentLocations( + Coordinate.sum(blockOrigin, child.relativeCoords), + ); + } + } + + private updateConnectionLocations(blockOrigin: Coordinate) { + for (const conn of this.getConnections_(false)) { + conn.moveToOffset(blockOrigin); + } + } + + private updateIconLocations(blockOrigin: Coordinate) { + for (const icon of this.getIcons()) { + icon.onLocationChange(blockOrigin); + } + } + + private updateFieldLocations(blockOrigin: Coordinate) { + for (const input of this.inputList) { + for (const field of input.fieldRow) { + field.onLocationChange(blockOrigin); + } } } @@ -686,6 +715,7 @@ export class BlockSvg * @internal */ setDragging(adding: boolean) { + this.dragging = adding; if (adding) { this.translation = ''; common.draggingConnections.push(...this.getConnections_(true)); @@ -725,6 +755,8 @@ export class BlockSvg /** * Sets whether this block is a shadow block or not. + * This method is internal and should not be called by users of Blockly. To + * create shadow blocks programmatically call connection.setShadowState * * @param shadow True if a shadow. * @internal @@ -1159,6 +1191,9 @@ export class BlockSvg bringToFront(blockOnly = false) { /* eslint-disable-next-line @typescript-eslint/no-this-alias */ let block: this | null = this; + if (block.isDeadOrDying()) { + return; + } do { const root = block.getSvgRoot(); const parent = root.parentNode; @@ -1588,6 +1623,11 @@ export class BlockSvg if (this.isCollapsed()) { this.updateCollapsed_(); } + + if (!this.isEnabled()) { + this.updateDisabled(); + } + this.workspace.getRenderer().render(this); this.tightenChildrenEfficiently(); @@ -1628,46 +1668,6 @@ export class BlockSvg } } - /** - * Update all of the connections on this block with the new locations - * calculated during rendering. Also move all of the connected blocks based - * on the new connection locations. - * - * @internal - */ - private updateConnectionAndIconLocations() { - const blockTL = this.getRelativeToSurfaceXY(); - // Don't tighten previous or output connections because they are inferior - // connections. - if (this.previousConnection) { - this.previousConnection.moveToOffset(blockTL); - } - if (this.outputConnection) { - this.outputConnection.moveToOffset(blockTL); - } - - for (let i = 0; i < this.inputList.length; i++) { - const conn = this.inputList[i].connection as RenderedConnection; - if (conn) { - conn.moveToOffset(blockTL); - if (conn.isConnected()) { - conn.tighten(); - } - } - } - - if (this.nextConnection) { - this.nextConnection.moveToOffset(blockTL); - if (this.nextConnection.isConnected()) { - this.nextConnection.tighten(); - } - } - - for (const icon of this.getIcons()) { - icon.onLocationChange(blockTL); - } - } - /** * Add the cursor SVG to this block's SVG group. * diff --git a/core/bubble_dragger.ts b/core/bubble_dragger.ts index 47d03f6cc..e494c7ad2 100644 --- a/core/bubble_dragger.ts +++ b/core/bubble_dragger.ts @@ -20,6 +20,7 @@ import type {IDragTarget} from './interfaces/i_drag_target.js'; import {Coordinate} from './utils/coordinate.js'; import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +import * as layers from './layers.js'; /** * Class for a bubble dragger. It moves things on the bubble canvas around the @@ -64,6 +65,8 @@ export class BubbleDragger { (this.bubble as AnyDuringMigration).setAutoLayout(false); } + this.workspace.getLayerManager()?.moveToDragLayer(this.bubble); + this.bubble.setDragging && this.bubble.setDragging(true); } @@ -163,6 +166,9 @@ export class BubbleDragger { // Put everything back onto the bubble canvas. if (this.bubble.setDragging) { this.bubble.setDragging(false); + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.bubble, layers.BUBBLE); } this.fireMoveEvent_(); } diff --git a/core/css.ts b/core/css.ts index 5d52ea1a5..07e9c98a4 100644 --- a/core/css.ts +++ b/core/css.ts @@ -496,4 +496,15 @@ input[type=number] { float: right; margin-right: -24px; } + +.blocklyBlockDragSurface { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: visible !important; + z-index: 80; + pointer-events: none; +} `; diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index f5d8076fa..c90661c4e 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -113,7 +113,7 @@ export interface PositionMetrics { * @internal */ export function createDom() { - if (div) { + if (document.querySelector('.blocklyDropDownDiv')) { return; // Already created. } div = document.createElement('div'); diff --git a/core/events/events_block_field_intermediate_change.ts b/core/events/events_block_field_intermediate_change.ts index 1e19a04a6..264f910a8 100644 --- a/core/events/events_block_field_intermediate_change.ts +++ b/core/events/events_block_field_intermediate_change.ts @@ -122,6 +122,36 @@ export class BlockFieldIntermediateChange extends BlockBase { override isNull(): boolean { return this.oldValue === this.newValue; } + + /** + * Run a change event. + * + * @param forward True if run forward, false if run backward (undo). + */ + override run(forward: boolean) { + const workspace = this.getEventWorkspace_(); + if (!this.blockId) { + throw new Error( + 'The block ID is undefined. Either pass a block to ' + + 'the constructor, or call fromJson', + ); + } + const block = workspace.getBlockById(this.blockId); + if (!block) { + throw new Error( + 'The associated block is undefined. Either pass a ' + + 'block to the constructor, or call fromJson', + ); + } + + const value = forward ? this.newValue : this.oldValue; + const field = block.getField(this.name!); + if (field) { + field.setValue(value); + } else { + console.warn("Can't set non-existent field: " + this.name); + } + } } export interface BlockFieldIntermediateChangeJson extends BlockBaseJson { diff --git a/core/field.ts b/core/field.ts index 529c5aaef..7522cde93 100644 --- a/core/field.ts +++ b/core/field.ts @@ -581,6 +581,20 @@ export abstract class Field ); } + /** + * Check whether the field should be clickable while the block is in a flyout. + * The default is that fields are clickable in always-open flyouts such as the + * simple toolbox, but not in autoclosing flyouts such as the category toolbox. + * Subclasses may override this function to change this behavior. Note that + * `isClickable` must also return true for this to have any effect. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the field should be clickable while the block is in a flyout. + */ + isClickableInFlyout(autoClosingFlyout: boolean): boolean { + return !autoClosingFlyout; + } + /** * Check whether this field is currently editable. Some fields are never * EDITABLE (e.g. text labels). Other fields may be EDITABLE but may exist on @@ -810,8 +824,8 @@ export abstract class Field margin !== undefined ? margin : !this.isFullBlockField() - ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING - : 0; + ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING + : 0; let totalWidth = xOffset * 2; let totalHeight = constants!.FIELD_TEXT_HEIGHT; @@ -960,6 +974,14 @@ export abstract class Field return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth); } + /** + * Notifies the field that it has changed locations. + * + * @param _ The location of this field's block's top-start corner + * in workspace coordinates. + */ + onLocationChange(_: Coordinate) {} + /** * Get the text from this field to display on the block. May differ from * `getText` due to ellipsis, and other formatting. diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 4aaed32b8..ad728f226 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -278,12 +278,8 @@ export class FieldDropdown extends Field { dom.addClass(menuElement, 'blocklyDropdownMenu'); if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { - const primaryColour = block.isShadow() - ? block.getParent()!.getColour() - : block.getColour(); - const borderColour = block.isShadow() - ? (block.getParent() as BlockSvg).style.colourTertiary - : (this.sourceBlock_ as BlockSvg).style.colourTertiary; + const primaryColour = block.getColour(); + const borderColour = (this.sourceBlock_ as BlockSvg).style.colourTertiary; dropDownDiv.setColour(primaryColour, borderColour); } diff --git a/core/field_input.ts b/core/field_input.ts index 22c6765b4..2cd4015b0 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -33,7 +33,6 @@ import {Coordinate} from './utils/coordinate.js'; import * as userAgent from './utils/useragent.js'; import * as WidgetDiv from './widgetdiv.js'; import type {WorkspaceSvg} from './workspace_svg.js'; -import * as renderManagement from './render_management.js'; import {Size} from './utils/size.js'; /** @@ -251,6 +250,14 @@ export abstract class FieldInput extends Field< return super.getSize(); } + /** + * Notifies the field that it has changed locations. Moves the widget div to + * be in the correct place if it is open. + */ + onLocationChange(): void { + if (this.isBeingEdited_) this.resizeEditor_(); + } + /** * Updates the colour of the htmlInput given the current validity of the * field's value. @@ -263,7 +270,6 @@ export abstract class FieldInput extends Field< // This logic is done in render_ rather than doValueInvalid_ or // doValueUpdate_ so that the code is more centralized. if (this.isBeingEdited_) { - this.resizeEditor_(); const htmlInput = this.htmlInput_ as HTMLElement; if (!this.isTextValid_) { dom.addClass(htmlInput, 'blocklyInvalidInput'); @@ -576,11 +582,6 @@ export abstract class FieldInput extends Field< ), ); } - - // Resize the widget div after the block has finished rendering. - renderManagement.finishQueuedRenders().then(() => { - this.resizeEditor_(); - }); } /** @@ -631,7 +632,7 @@ export abstract class FieldInput extends Field< /** * Handles repositioning the WidgetDiv used for input fields when the * workspace is resized. Will bump the block into the viewport and update the - * position of the field if necessary. + * position of the text input if necessary. * * @returns True for rendered workspaces, as we never want to hide the widget * div. @@ -642,13 +643,13 @@ export abstract class FieldInput extends Field< // rendered blocks. if (!(block instanceof BlockSvg)) return false; - bumpObjects.bumpIntoBounds( + const bumped = bumpObjects.bumpIntoBounds( this.workspace_!, this.workspace_!.getMetricsManager().getViewMetrics(true), block, ); - this.resizeEditor_(); + if (!bumped) this.resizeEditor_(); return true; } diff --git a/core/flyout_base.ts b/core/flyout_base.ts index bb30a45cb..76d2e8f8c 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -36,6 +36,7 @@ import {WorkspaceSvg} from './workspace_svg.js'; import * as utilsXml from './utils/xml.js'; import * as Xml from './xml.js'; import * as renderManagement from './render_management.js'; +import {IAutoHideable} from './interfaces/i_autohideable.js'; enum FlyoutItemType { BLOCK = 'block', @@ -45,7 +46,10 @@ enum FlyoutItemType { /** * Class for a flyout. */ -export abstract class Flyout extends DeleteArea implements IFlyout { +export abstract class Flyout + extends DeleteArea + implements IAutoHideable, IFlyout +{ /** * Position the flyout. */ @@ -385,10 +389,8 @@ export abstract class Flyout extends DeleteArea implements IFlyout { this.wheel_, ), ); - if (!this.autoClose) { - this.filterWrapper = this.filterForCapacity.bind(this); - this.targetWorkspace.addChangeListener(this.filterWrapper); - } + this.filterWrapper = this.filterForCapacity.bind(this); + this.targetWorkspace.addChangeListener(this.filterWrapper); // Dragging the flyout up and down. this.boundEvents.push( @@ -414,6 +416,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { component: this, weight: 1, capabilities: [ + ComponentManager.Capability.AUTOHIDEABLE, ComponentManager.Capability.DELETE_AREA, ComponentManager.Capability.DRAG_TARGET, ], @@ -426,7 +429,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { */ dispose() { this.hide(); - this.workspace_.getComponentManager().removeComponent(this.id); + this.targetWorkspace.getComponentManager().removeComponent(this.id); for (const event of this.boundEvents) { browserEvents.unbind(event); } @@ -480,6 +483,26 @@ export abstract class Flyout extends DeleteArea implements IFlyout { return this.workspace_; } + /** + * Sets whether this flyout automatically closes when blocks are dragged out, + * the workspace is clicked, etc, or not. + */ + setAutoClose(autoClose: boolean) { + this.autoClose = autoClose; + this.targetWorkspace.recordDragTargets(); + this.targetWorkspace.resizeContents(); + } + + /** Automatically hides the flyout if it is an autoclosing flyout. */ + autoHide(onlyClosePopups: boolean): void { + if ( + !onlyClosePopups && + this.targetWorkspace.getFlyout(true) === this && + this.autoClose + ) + this.hide(); + } + /** * Is the flyout visible? * @@ -504,7 +527,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { if (!this.autoClose) { // Auto-close flyouts are ignored as drag targets, so only non // auto-close flyouts need to have their drag target updated. - this.workspace_.recordDragTargets(); + this.targetWorkspace.recordDragTargets(); } this.updateDisplay(); } @@ -1213,7 +1236,7 @@ export abstract class Flyout extends DeleteArea implements IFlyout { // Clone the block. // TODO(#7432): Add a saveIds parameter to `save`. - const json = blocks.save(oldBlock) as blocks.State; + const json = blocks.save(oldBlock, {saveIds: false}) as blocks.State; // Normallly this resizes leading to weird jumps. Save it for terminateDrag. targetWorkspace.setResizesEnabled(false); const block = blocks.append(json, targetWorkspace) as BlockSvg; diff --git a/core/flyout_horizontal.ts b/core/flyout_horizontal.ts index 9d827f4ea..9c9490b22 100644 --- a/core/flyout_horizontal.ts +++ b/core/flyout_horizontal.ts @@ -382,22 +382,25 @@ export class HorizontalFlyout extends Flyout { } } + // TODO(#7689): Remove this. + // Workspace with no scrollbars where this is permanently open on the top. + // If scrollbars exist they properly update the metrics. if ( - this.targetWorkspace!.toolboxPosition === this.toolboxPosition_ && - this.toolboxPosition_ === toolbox.Position.TOP && - !this.targetWorkspace!.getToolbox() + !this.targetWorkspace.scrollbar && + !this.autoClose && + this.targetWorkspace.getFlyout() === this && + this.toolboxPosition_ === toolbox.Position.TOP ) { - // This flyout is a simple toolbox. Reposition the workspace so that - // (0,0) is in the correct position relative to the new absolute edge - // (ie toolbox edge). - this.targetWorkspace!.translate( - this.targetWorkspace!.scrollX, - this.targetWorkspace!.scrollY + flyoutHeight, + this.targetWorkspace.translate( + this.targetWorkspace.scrollX, + this.targetWorkspace.scrollY + flyoutHeight, ); } + this.height_ = flyoutHeight; this.position(); - this.targetWorkspace!.recordDragTargets(); + this.targetWorkspace.resizeContents(); + this.targetWorkspace.recordDragTargets(); } } } diff --git a/core/flyout_vertical.ts b/core/flyout_vertical.ts index b3e656d0c..c9ce4f659 100644 --- a/core/flyout_vertical.ts +++ b/core/flyout_vertical.ts @@ -375,22 +375,26 @@ export class VerticalFlyout extends Flyout { } } + // TODO(#7689): Remove this. + // Workspace with no scrollbars where this is permanently + // open on the left. + // If scrollbars exist they properly update the metrics. if ( - this.targetWorkspace!.toolboxPosition === this.toolboxPosition_ && - this.toolboxPosition_ === toolbox.Position.LEFT && - !this.targetWorkspace!.getToolbox() + !this.targetWorkspace.scrollbar && + !this.autoClose && + this.targetWorkspace.getFlyout() === this && + this.toolboxPosition_ === toolbox.Position.LEFT ) { - // This flyout is a simple toolbox. Reposition the workspace so that - // (0,0) is in the correct position relative to the new absolute edge - // (ie toolbox edge). - this.targetWorkspace!.translate( - this.targetWorkspace!.scrollX + flyoutWidth, - this.targetWorkspace!.scrollY, + this.targetWorkspace.translate( + this.targetWorkspace.scrollX + flyoutWidth, + this.targetWorkspace.scrollY, ); } + this.width_ = flyoutWidth; this.position(); - this.targetWorkspace!.recordDragTargets(); + this.targetWorkspace.resizeContents(); + this.targetWorkspace.recordDragTargets(); } } } diff --git a/core/generator.ts b/core/generator.ts index c50a24853..d94597fc1 100644 --- a/core/generator.ts +++ b/core/generator.ts @@ -19,8 +19,10 @@ import type {Workspace} from './workspace.js'; import {warn} from './utils/deprecation.js'; /** - * Type declaration for per-block-type generator functions. + * Deprecated, no-longer used type declaration for per-block-type generator + * functions. * + * @deprecated * @see {@link https://developers.google.com/blockly/guides/create-custom-blocks/generating-code} * @param block The Block instance to generate code for. * @param genearator The CodeGenerator calling the function. @@ -39,8 +41,25 @@ export type BlockGenerator = ( export class CodeGenerator { name_: string; - /** A dictionary of block generator functions keyed by block type. */ - forBlock: {[type: string]: BlockGenerator} = Object.create(null); + /** + * A dictionary of block generator functions, keyed by block type. + * Each block generator function takes two parameters: + * + * - the Block to generate code for, and + * - the calling CodeGenerator (or subclass) instance, so the + * function can call methods defined below (e.g. blockToCode) or + * on the relevant subclass (e.g. JavascripGenerator), + * + * and returns: + * + * - a [code, precedence] tuple (for value/expression blocks), or + * - a string containing the generated code (for statement blocks), or + * - null if no code should be emitted for block. + */ + forBlock: Record< + string, + (block: Block, generator: this) => [string, number] | string | null + > = Object.create(null); /** * This is used as a placeholder in functions defined using @@ -248,7 +267,7 @@ export class CodeGenerator { } if (typeof func !== 'function') { throw Error( - `${this.name_} generator does not know how to generate code` + + `${this.name_} generator does not know how to generate code ` + `for block type "${block.type}".`, ); } diff --git a/core/gesture.ts b/core/gesture.ts index 03c81c3cc..4b85c4f6f 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -945,7 +945,7 @@ export class Gesture { 'Cannot do an icon click because the start icon is undefined', ); } - + this.bringBlockToFront(); this.startIcon.onClick(); } @@ -1157,25 +1157,29 @@ export class Gesture { } /** - * Whether this gesture is a click on a field. This should only be called + * Whether this gesture is a click on a field that should be handled. This should only be called * when ending a gesture (pointerup). * * @returns Whether this gesture was a click on a field. */ private isFieldClick(): boolean { - const fieldClickable = this.startField - ? this.startField.isClickable() - : false; + if (!this.startField) return false; return ( - fieldClickable && + this.startField.isClickable() && !this.hasExceededDragRadius && - (!this.flyout || !this.flyout.autoClose) + (!this.flyout || + this.startField.isClickableInFlyout(this.flyout.autoClose)) ); } - /** @returns Whether this gesture is a click on an icon. */ + /** @returns Whether this gesture is a click on an icon that should be handled. */ private isIconClick(): boolean { - return !!this.startIcon && !this.hasExceededDragRadius; + if (!this.startIcon) return false; + const handleInFlyout = + !this.flyout || + !this.startIcon.isClickableInFlyout || + this.startIcon.isClickableInFlyout(this.flyout.autoClose); + return !this.hasExceededDragRadius && handleInFlyout; } /** diff --git a/core/icons/comment_icon.ts b/core/icons/comment_icon.ts index d52782437..7756088b6 100644 --- a/core/icons/comment_icon.ts +++ b/core/icons/comment_icon.ts @@ -110,6 +110,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { }, this.svgRoot, ); + dom.addClass(this.svgRoot!, 'blockly-icon-comment'); } override dispose() { @@ -155,6 +156,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { /** Sets the text of this comment. Updates any bubbles if they are visible. */ setText(text: string) { + const oldText = this.text; + eventUtils.fire( + new (eventUtils.get(eventUtils.BLOCK_CHANGE))( + this.sourceBlock, + 'comment', + null, + oldText, + text, + ), + ); this.text = text; this.textInputBubble?.setText(this.text); this.textBubble?.setText(this.text); @@ -212,14 +223,30 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable { this.setBubbleVisible(!this.bubbleIsVisible()); } + override isClickableInFlyout(): boolean { + return false; + } + /** * Updates the text of this comment in response to changes in the text of * the input bubble. */ onTextChange(): void { - if (this.textInputBubble) { - this.text = this.textInputBubble.getText(); - } + if (!this.textInputBubble) return; + + const newText = this.textInputBubble.getText(); + if (this.text === newText) return; + + eventUtils.fire( + new (eventUtils.get(eventUtils.BLOCK_CHANGE))( + this.sourceBlock, + 'comment', + null, + this.text, + newText, + ), + ); + this.text = newText; } /** diff --git a/core/icons/icon.ts b/core/icons/icon.ts index 095f52509..b1104b157 100644 --- a/core/icons/icon.ts +++ b/core/icons/icon.ts @@ -15,6 +15,7 @@ import {Size} from '../utils/size.js'; import {Svg} from '../utils/svg.js'; import type {IconType} from './icon_types.js'; import * as deprecation from '../utils/deprecation.js'; +import * as tooltip from '../tooltip.js'; /** * The abstract icon class. Icons are visual elements that live in the top-start @@ -35,7 +36,12 @@ export abstract class Icon implements IIcon { /** The root svg element visually representing this icon. */ protected svgRoot: SVGGElement | null = null; - constructor(protected sourceBlock: Block) {} + /** The tooltip for this icon. */ + protected tooltip: tooltip.TipInfo; + + constructor(protected sourceBlock: Block) { + this.tooltip = sourceBlock; + } getType(): IconType { throw new Error('Icons must implement getType'); @@ -54,9 +60,12 @@ export abstract class Icon implements IIcon { this, pointerdownListener, ); + (this.svgRoot as any).tooltip = this; + tooltip.bindMouseEvents(this.svgRoot); } dispose(): void { + tooltip.unbindMouseEvents(this.svgRoot); dom.removeNode(this.svgRoot); } @@ -68,6 +77,19 @@ export abstract class Icon implements IIcon { return new Size(0, 0); } + /** + * Sets the tooltip for this icon to the given value. Null to show the + * tooltip of the block. + */ + setTooltip(tip: tooltip.TipInfo | null) { + this.tooltip = tip ?? this.sourceBlock; + } + + /** Returns the tooltip for this icon. */ + getTooltip(): tooltip.TipInfo { + return this.tooltip; + } + applyColour(): void {} updateEditable(): void {} @@ -111,6 +133,19 @@ export abstract class Icon implements IIcon { onClick(): void {} + /** + * Check whether the icon should be clickable while the block is in a flyout. + * The default is that icons are clickable in all flyouts (auto-closing or not). + * Subclasses may override this function to change this behavior. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the icon should be clickable while the block is in a flyout. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isClickableInFlyout(autoClosingFlyout: boolean): boolean { + return true; + } + /** * Sets the visibility of the icon's bubble if one exists. * diff --git a/core/icons/mutator_icon.ts b/core/icons/mutator_icon.ts index a2987bff2..56365ed1f 100644 --- a/core/icons/mutator_icon.ts +++ b/core/icons/mutator_icon.ts @@ -118,6 +118,7 @@ export class MutatorIcon extends Icon implements IHasBubble { {'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'}, this.svgRoot, ); + dom.addClass(this.svgRoot!, 'blockly-icon-mutator'); } override dispose(): void { @@ -151,7 +152,13 @@ export class MutatorIcon extends Icon implements IHasBubble { override onClick(): void { super.onClick(); - this.setBubbleVisible(!this.bubbleIsVisible()); + if (this.sourceBlock.isEditable()) { + this.setBubbleVisible(!this.bubbleIsVisible()); + } + } + + override isClickableInFlyout(): boolean { + return false; } bubbleIsVisible(): boolean { diff --git a/core/icons/warning_icon.ts b/core/icons/warning_icon.ts index edb763f82..f0862c3e9 100644 --- a/core/icons/warning_icon.ts +++ b/core/icons/warning_icon.ts @@ -88,6 +88,7 @@ export class WarningIcon extends Icon implements IHasBubble { }, this.svgRoot, ); + dom.addClass(this.svgRoot!, 'blockly-icon-warning'); } override dispose() { @@ -159,6 +160,10 @@ export class WarningIcon extends Icon implements IHasBubble { this.setBubbleVisible(!this.bubbleIsVisible()); } + override isClickableInFlyout(): boolean { + return false; + } + bubbleIsVisible(): boolean { return !!this.textBubble; } diff --git a/core/inject.ts b/core/inject.ts index 78c80e1a7..b938abaa4 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -60,7 +60,7 @@ export function inject( containerElement!.appendChild(subContainer); const svg = createDom(subContainer, options); - const workspace = createMainWorkspace(svg, options); + const workspace = createMainWorkspace(subContainer, svg, options); init(workspace); @@ -138,15 +138,20 @@ function createDom(container: Element, options: Options): SVGElement { * @param options Dictionary of options. * @returns Newly created main workspace. */ -function createMainWorkspace(svg: SVGElement, options: Options): WorkspaceSvg { +function createMainWorkspace( + injectionDiv: Element, + svg: SVGElement, + options: Options, +): WorkspaceSvg { options.parentWorkspace = null; const mainWorkspace = new WorkspaceSvg(options); const wsOptions = mainWorkspace.options; mainWorkspace.scale = wsOptions.zoomOptions.startScale; - svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); + svg.appendChild( + mainWorkspace.createDom('blocklyMainBackground', injectionDiv), + ); // Set the theme name and renderer name onto the injection div. - const injectionDiv = mainWorkspace.getInjectionDiv(); const rendererClassName = mainWorkspace.getRenderer().getClassName(); if (rendererClassName) { dom.addClass(injectionDiv, rendererClassName); diff --git a/core/insertion_marker_manager.ts b/core/insertion_marker_manager.ts index e9d04f728..69d3d2cb9 100644 --- a/core/insertion_marker_manager.ts +++ b/core/insertion_marker_manager.ts @@ -271,6 +271,10 @@ export class InsertionMarkerManager { } } + for (const block of result.getDescendants(false)) { + block.setInsertionMarker(true); + } + result.setCollapsed(sourceBlock.isCollapsed()); result.setInputsInline(sourceBlock.getInputsInline()); diff --git a/core/interfaces/i_icon.ts b/core/interfaces/i_icon.ts index 8c1477997..a6159985f 100644 --- a/core/interfaces/i_icon.ts +++ b/core/interfaces/i_icon.ts @@ -83,6 +83,15 @@ export interface IIcon { * Notifies the icon that it has been clicked. */ onClick(): void; + + /** + * Check whether the icon should be clickable while the block is in a flyout. + * If this function is not defined, the icon will be clickable in all flyouts. + * + * @param autoClosingFlyout true if the containing flyout is an auto-closing one. + * @returns Whether the icon should be clickable while the block is in a flyout. + */ + isClickableInFlyout?(autoClosingFlyout: boolean): boolean; } /** Type guard that checks whether the given object is an IIcon. */ diff --git a/core/interfaces/i_rendered_element.ts b/core/interfaces/i_rendered_element.ts new file mode 100644 index 000000000..7e6981ca6 --- /dev/null +++ b/core/interfaces/i_rendered_element.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** @internal */ +export interface IRenderedElement { + /** + * @returns The root SVG element of htis rendered element. + */ + getSvgRoot(): SVGElement; +} + +/** + * @returns True if the given object is an IRenderedElement. + * + * @internal + */ +export function isRenderedElement(obj: any): obj is IRenderedElement { + return obj['getSvgRoot'] !== undefined; +} diff --git a/core/layer_manager.ts b/core/layer_manager.ts new file mode 100644 index 000000000..c27339d9b --- /dev/null +++ b/core/layer_manager.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {WorkspaceSvg} from './workspace_svg.js'; +import * as dom from './utils/dom.js'; +import {Svg} from './utils/svg.js'; +import {IRenderedElement} from './interfaces/i_rendered_element.js'; +import * as layerNums from './layers.js'; +import {Coordinate} from './utils/coordinate.js'; + +/** @internal */ +export class LayerManager { + /** The layer elements being dragged are appended to. */ + private dragLayer: SVGGElement | undefined; + /** The layers elements not being dragged are appended to. */ + private layers = new Map(); + + /** @internal */ + constructor(private workspace: WorkspaceSvg) { + const injectionDiv = workspace.getInjectionDiv(); + // `getInjectionDiv` is actually nullable. We hit this if the workspace + // is part of a flyout and the workspace the flyout is attached to hasn't + // been appended yet. + if (injectionDiv) { + this.dragLayer = this.createDragLayer(injectionDiv); + } + + // We construct these manually so we can add the css class for backwards + // compatibility. + const blockLayer = this.createLayer(layerNums.BLOCK); + dom.addClass(blockLayer, 'blocklyBlockCanvas'); + const bubbleLayer = this.createLayer(layerNums.BUBBLE); + dom.addClass(bubbleLayer, 'blocklyBubbleCanvas'); + } + + private createDragLayer(injectionDiv: Element) { + const svg = dom.createSvgElement(Svg.SVG, { + 'class': 'blocklyBlockDragSurface', + 'xmlns': dom.SVG_NS, + 'xmlns:html': dom.HTML_NS, + 'xmlns:xlink': dom.XLINK_NS, + 'version': '1.1', + }); + injectionDiv.append(svg); + return dom.createSvgElement(Svg.G, {}, svg); + } + + /** + * Translates layers when the workspace is dragged or zoomed. + * + * @internal + */ + translateLayers(newCoord: Coordinate, newScale: number) { + const translation = `translate(${newCoord.x}, ${newCoord.y}) scale(${newScale})`; + this.dragLayer?.setAttribute('transform', translation); + for (const [_, layer] of this.layers) { + layer.setAttribute('transform', translation); + } + } + + /** + * Moves the given element to the drag layer, which exists on top of all other + * layers, and the drag surface. + * + * @internal + */ + moveToDragLayer(elem: IRenderedElement) { + this.dragLayer?.appendChild(elem.getSvgRoot()); + } + + /** + * Moves the given element off of the drag layer. + * + * @internal + */ + moveOffDragLayer(elem: IRenderedElement, layerNum: number) { + this.append(elem, layerNum); + } + + /** + * Appends the given element to a layer. If the layer does not exist, it is + * created. + * + * @internal + */ + append(elem: IRenderedElement, layerNum: number) { + if (!this.layers.has(layerNum)) { + this.createLayer(layerNum); + } + this.layers.get(layerNum)?.appendChild(elem.getSvgRoot()); + } + + /** + * Creates a layer and inserts it at the proper place given the layer number. + * + * More positive layers exist later in the dom and are rendered ontop of + * less positive layers. Layers are added to the layer map as a side effect. + */ + private createLayer(layerNum: number): SVGGElement { + const parent = this.workspace.getSvgGroup(); + const layer = dom.createSvgElement(Svg.G, {}); + + let inserted = false; + const sortedLayers = [...this.layers].sort((a, b) => a[0] - b[0]); + for (const [num, sib] of sortedLayers) { + if (layerNum < num) { + parent.insertBefore(layer, sib); + inserted = true; + break; + } + } + if (!inserted) { + parent.appendChild(layer); + } + this.layers.set(layerNum, layer); + return layer; + } + + /** + * Returns true if the given element is a layer managed by the layer manager. + * False otherwise. + * + * @internal + */ + hasLayer(elem: SVGElement) { + return ( + elem === this.dragLayer || + new Set(this.layers.values()).has(elem as SVGGElement) + ); + } + + /** + * We need to be able to access this layer explicitly for backwards + * compatibility. + * + * @internal + */ + getBlockLayer(): SVGGElement { + return this.layers.get(layerNums.BLOCK)!; + } + + /** + * We need to be able to access this layer explicitly for backwards + * compatibility. + * + * @internal + */ + getBubbleLayer(): SVGGElement { + return this.layers.get(layerNums.BUBBLE)!; + } +} diff --git a/core/layers.ts b/core/layers.ts new file mode 100644 index 000000000..60a30c8f6 --- /dev/null +++ b/core/layers.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * The layer to place blocks on. + * + * @internal + */ +export const BLOCK = 50; + +/** + * The layer to place bubbles on. + * + * @internal + */ +export const BUBBLE = 100; diff --git a/core/metrics_manager.ts b/core/metrics_manager.ts index cbadcefac..62a2614b6 100644 --- a/core/metrics_manager.ts +++ b/core/metrics_manager.ts @@ -111,26 +111,25 @@ export class MetricsManager implements IMetricsManager { */ getAbsoluteMetrics(): AbsoluteMetrics { let absoluteLeft = 0; + let absoluteTop = 0; + const toolboxMetrics = this.getToolboxMetrics(); - const flyoutMetrics = this.getFlyoutMetrics(true); - const doesToolboxExist = !!this.workspace_.getToolbox(); - const doesFlyoutExist = !!this.workspace_.getFlyout(true); - const toolboxPosition = doesToolboxExist + const flyoutMetrics = this.getFlyoutMetrics(); + const respectToolbox = !!this.workspace_.getToolbox(); + const respectFlyout = !this.workspace_.getFlyout()?.autoClose; + const toolboxPosition = respectToolbox ? toolboxMetrics.position : flyoutMetrics.position; const atLeft = toolboxPosition === toolboxUtils.Position.LEFT; const atTop = toolboxPosition === toolboxUtils.Position.TOP; - if (doesToolboxExist && atLeft) { - absoluteLeft = toolboxMetrics.width; - } else if (doesFlyoutExist && atLeft) { - absoluteLeft = flyoutMetrics.width; + if (atLeft) { + if (respectToolbox) absoluteLeft += toolboxMetrics.width; + if (respectFlyout) absoluteLeft += flyoutMetrics.width; } - let absoluteTop = 0; - if (doesToolboxExist && atTop) { - absoluteTop = toolboxMetrics.height; - } else if (doesFlyoutExist && atTop) { - absoluteTop = flyoutMetrics.height; + if (atTop) { + if (respectToolbox) absoluteTop += toolboxMetrics.height; + if (respectFlyout) absoluteTop += flyoutMetrics.height; } return { @@ -152,36 +151,26 @@ export class MetricsManager implements IMetricsManager { const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1; const svgMetrics = this.getSvgMetrics(); const toolboxMetrics = this.getToolboxMetrics(); - const flyoutMetrics = this.getFlyoutMetrics(true); - const doesToolboxExist = !!this.workspace_.getToolbox(); - const toolboxPosition = doesToolboxExist + const flyoutMetrics = this.getFlyoutMetrics(); + const respectToolbox = !!this.workspace_.getToolbox(); + const respectFlyout = !this.workspace_.getFlyout()?.autoClose; + const toolboxPosition = respectToolbox ? toolboxMetrics.position : flyoutMetrics.position; - if (this.workspace_.getToolbox()) { - if ( - toolboxPosition === toolboxUtils.Position.TOP || - toolboxPosition === toolboxUtils.Position.BOTTOM - ) { - svgMetrics.height -= toolboxMetrics.height; - } else if ( - toolboxPosition === toolboxUtils.Position.LEFT || - toolboxPosition === toolboxUtils.Position.RIGHT - ) { - svgMetrics.width -= toolboxMetrics.width; - } - } else if (this.workspace_.getFlyout(true)) { - if ( - toolboxPosition === toolboxUtils.Position.TOP || - toolboxPosition === toolboxUtils.Position.BOTTOM - ) { - svgMetrics.height -= flyoutMetrics.height; - } else if ( - toolboxPosition === toolboxUtils.Position.LEFT || - toolboxPosition === toolboxUtils.Position.RIGHT - ) { - svgMetrics.width -= flyoutMetrics.width; - } + const horizToolbox = + toolboxPosition === toolboxUtils.Position.TOP || + toolboxPosition === toolboxUtils.Position.BOTTOM; + const vertToolbox = + toolboxPosition === toolboxUtils.Position.LEFT || + toolboxPosition === toolboxUtils.Position.RIGHT; + if (horizToolbox) { + if (respectToolbox) svgMetrics.height -= toolboxMetrics.height; + if (respectFlyout) svgMetrics.height -= flyoutMetrics.height; + } + if (vertToolbox) { + if (respectToolbox) svgMetrics.width -= toolboxMetrics.width; + if (respectFlyout) svgMetrics.width -= flyoutMetrics.width; } return { height: svgMetrics.height / scale, diff --git a/core/render_management.ts b/core/render_management.ts index 8c088eacf..541459860 100644 --- a/core/render_management.ts +++ b/core/render_management.ts @@ -5,7 +5,6 @@ */ import {BlockSvg} from './block_svg.js'; -import {Coordinate} from './utils/coordinate.js'; import * as userAgent from './utils/useragent.js'; /** The set of all blocks in need of rendering which don't have parents. */ @@ -114,28 +113,36 @@ function queueBlock(block: BlockSvg) { */ function doRenders() { const workspaces = new Set([...rootBlocks].map((block) => block.workspace)); - for (const block of rootBlocks) { - // No need to render a dead block. - if (block.isDisposed()) continue; - // A render for this block may have been queued, and then the block was - // connected to a parent, so it is no longer a root block. - // Rendering will be triggered through the real root block. - if (block.getParent()) continue; - + const blocks = [...rootBlocks].filter(shouldRenderRootBlock); + for (const block of blocks) { renderBlock(block); - const blockOrigin = block.getRelativeToSurfaceXY(); - updateConnectionLocations(block, blockOrigin); - updateIconLocations(block, blockOrigin); } for (const workspace of workspaces) { workspace.resizeContents(); } + for (const block of blocks) { + const blockOrigin = block.getRelativeToSurfaceXY(); + block.updateComponentLocations(blockOrigin); + } rootBlocks.clear(); dirtyBlocks = new Set(); afterRendersPromise = null; } +/** + * Returns true if the block should be rendered. + * + * No need to render dead blocks. + * + * No need to render blocks with parents. A render for the block may have been + * queued, and the the block was connected to a parent, so it is no longer a + * root block. Rendering will be triggered through the real root block. + */ +function shouldRenderRootBlock(block: BlockSvg): boolean { + return !block.isDisposed() && !block.getParent(); +} + /** * Recursively renders all of the dirty children of the given block, and * then renders the block. @@ -149,44 +156,3 @@ function renderBlock(block: BlockSvg) { } block.renderEfficiently(); } - -/** - * Updates the connection database with the new locations of all of the - * connections that are children of the given block. - * - * @param block The block to update the connection locations of. - * @param blockOrigin The top left of the given block in workspace coordinates. - */ -function updateConnectionLocations(block: BlockSvg, blockOrigin: Coordinate) { - for (const conn of block.getConnections_(false)) { - const moved = conn.moveToOffset(blockOrigin); - const target = conn.targetBlock(); - if (!conn.isSuperior()) continue; - if (!target) continue; - if (moved || dirtyBlocks.has(target)) { - updateConnectionLocations( - target, - Coordinate.sum(blockOrigin, target.relativeCoords), - ); - } - } -} - -/** - * Updates all icons that are children of the given block with their new - * locations. - * - * @param block The block to update the icon locations of. - */ -function updateIconLocations(block: BlockSvg, blockOrigin: Coordinate) { - if (!block.getIcons) return; - for (const icon of block.getIcons()) { - icon.onLocationChange(blockOrigin); - } - for (const child of block.getChildren(false)) { - updateIconLocations( - child, - Coordinate.sum(blockOrigin, child.relativeCoords), - ); - } -} diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 3a1c32385..08c0471a2 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -24,7 +24,6 @@ import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; import {Svg} from './utils/svg.js'; -import * as svgMath from './utils/svg_math.js'; import * as svgPaths from './utils/svg_paths.js'; /** A shape that has a pathDown property. */ @@ -270,27 +269,6 @@ export class RenderedConnection extends Connection { return this.offsetInBlock; } - /** - * Move the blocks on either side of this connection right next to each other. - * - * @internal - */ - tighten() { - const dx = this.targetConnection!.x - this.x; - const dy = this.targetConnection!.y - this.y; - if (dx !== 0 || dy !== 0) { - const block = this.targetBlock(); - const svgRoot = block!.getSvgRoot(); - if (!svgRoot) { - throw Error('block is not rendered.'); - } - // Workspace coordinates. - const xy = svgMath.getRelativeXY(svgRoot); - block!.translate(xy.x - dx, xy.y - dy); - block!.moveConnections(-dx, -dy); - } - } - /** * Moves the blocks on either side of this connection right next to * each other, based on their local offsets, not global positions. diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index 9830e890a..fa44b7743 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -345,34 +345,34 @@ export class RenderInfo { /** * Decide whether to start a new row between the two Blockly.Inputs. * - * @param input The first input to consider - * @param lastInput The input that follows. - * @returns True if the next input should be rendered on a new row. + * @param currInput The current input. + * @param prevInput The previous input. + * @returns True if the current input should be rendered on a new row. */ - protected shouldStartNewRow_(input: Input, lastInput?: Input): boolean { + protected shouldStartNewRow_(currInput: Input, prevInput?: Input): boolean { // If this is the first input, just add to the existing row. // That row is either empty or has some icons in it. - if (!lastInput) { + if (!prevInput) { return false; } // If the previous input was an end-row input, then any following input // should always be rendered on the next row. - if (lastInput instanceof EndRowInput) { + if (prevInput instanceof EndRowInput) { return true; } // A statement input or an input following one always gets a new row. if ( - input instanceof StatementInput || - lastInput instanceof StatementInput + currInput instanceof StatementInput || + prevInput instanceof StatementInput ) { return true; } // Value inputs, dummy inputs, and any input following an external value // input get a new row if inputs are not inlined. if ( - input instanceof ValueInput || - input instanceof DummyInput || - lastInput instanceof ValueInput + currInput instanceof ValueInput || + currInput instanceof DummyInput || + prevInput instanceof ValueInput ) { return !this.isInline; } diff --git a/core/renderers/zelos/info.ts b/core/renderers/zelos/info.ts index 8748bcf5f..623f0a3b0 100644 --- a/core/renderers/zelos/info.ts +++ b/core/renderers/zelos/info.ts @@ -118,29 +118,29 @@ export class RenderInfo extends BaseRenderInfo { this.finalize_(); } - override shouldStartNewRow_(input: Input, lastInput: Input): boolean { + override shouldStartNewRow_(currInput: Input, prevInput: Input): boolean { // If this is the first input, just add to the existing row. // That row is either empty or has some icons in it. - if (!lastInput) { + if (!prevInput) { return false; } // If the previous input was an end-row input, then any following input // should always be rendered on the next row. - if (lastInput instanceof EndRowInput) { + if (prevInput instanceof EndRowInput) { return true; } // A statement input or an input following one always gets a new row. if ( - input instanceof StatementInput || - lastInput instanceof StatementInput + currInput instanceof StatementInput || + prevInput instanceof StatementInput ) { return true; } // Value, dummy, and end-row inputs get new row if inputs are not inlined. if ( - input instanceof ValueInput || - input instanceof DummyInput || - input instanceof EndRowInput + currInput instanceof ValueInput || + currInput instanceof DummyInput || + currInput instanceof EndRowInput ) { return !this.isInline || this.isMultiRow; } diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts index e53f30944..229686a0d 100644 --- a/core/serialization/blocks.ts +++ b/core/serialization/blocks.ts @@ -86,20 +86,21 @@ export function save( addInputBlocks = true, addNextBlocks = true, doFullSerialization = true, + saveIds = true, }: { addCoordinates?: boolean; addInputBlocks?: boolean; addNextBlocks?: boolean; doFullSerialization?: boolean; + saveIds?: boolean; } = {}, ): State | null { if (block.isInsertionMarker()) { return null; } - const state = { 'type': block.type, - 'id': block.id, + 'id': saveIds ? block.id : undefined, }; if (addCoordinates) { @@ -122,12 +123,22 @@ export function save( if (addInputBlocks) { // AnyDuringMigration because: Argument of type '{ type: string; id: // string; }' is not assignable to parameter of type 'State'. - saveInputBlocks(block, state as AnyDuringMigration, doFullSerialization); + saveInputBlocks( + block, + state as AnyDuringMigration, + doFullSerialization, + saveIds, + ); } if (addNextBlocks) { // AnyDuringMigration because: Argument of type '{ type: string; id: // string; }' is not assignable to parameter of type 'State'. - saveNextBlocks(block, state as AnyDuringMigration, doFullSerialization); + saveNextBlocks( + block, + state as AnyDuringMigration, + doFullSerialization, + saveIds, + ); } // AnyDuringMigration because: Type '{ type: string; id: string; }' is not @@ -270,6 +281,7 @@ function saveInputBlocks( block: Block, state: State, doFullSerialization: boolean, + saveIds: boolean, ) { const inputs = Object.create(null); for (let i = 0; i < block.inputList.length; i++) { @@ -278,6 +290,7 @@ function saveInputBlocks( const connectionState = saveConnection( input.connection as Connection, doFullSerialization, + saveIds, ); if (connectionState) { inputs[input.name] = connectionState; @@ -301,6 +314,7 @@ function saveNextBlocks( block: Block, state: State, doFullSerialization: boolean, + saveIds: boolean, ) { if (!block.nextConnection) { return; @@ -308,6 +322,7 @@ function saveNextBlocks( const connectionState = saveConnection( block.nextConnection, doFullSerialization, + saveIds, ); if (connectionState) { state['next'] = connectionState; @@ -326,6 +341,7 @@ function saveNextBlocks( function saveConnection( connection: Connection, doFullSerialization: boolean, + saveIds: boolean, ): ConnectionState | null { const shadow = connection.getShadowState(true); const child = connection.targetBlock(); @@ -337,7 +353,7 @@ function saveConnection( state['shadow'] = shadow; } if (child && !child.isShadow()) { - state['block'] = save(child, {doFullSerialization}); + state['block'] = save(child, {doFullSerialization, saveIds}); } return state; } @@ -717,7 +733,6 @@ function initBlock(block: Block, rendered: boolean) { blockSvg.initSvg(); blockSvg.queueRender(); - blockSvg.updateDisabled(); // fixes #6076 JSO deserialization doesn't // set .iconXY_ property so here it will be set diff --git a/core/tooltip.ts b/core/tooltip.ts index 51c38e653..0478b91fd 100644 --- a/core/tooltip.ts +++ b/core/tooltip.ts @@ -184,7 +184,7 @@ function getTargetObject( * Create the tooltip div and inject it onto the page. */ export function createDom() { - if (containerDiv) { + if (document.querySelector('.blocklyTooltipDiv')) { return; // Already created. } // Create an HTML container for popup overlays (e.g. editor widgets). diff --git a/core/trashcan.ts b/core/trashcan.ts index ee7c98eeb..42c32ea1f 100644 --- a/core/trashcan.ts +++ b/core/trashcan.ts @@ -545,7 +545,7 @@ export class Trashcan /** Inspect the contents of the trash. */ click() { - if (!this.hasContents()) { + if (!this.hasContents() || this.workspace.isDragging()) { return; } this.openFlyout(); diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index b53f7cd28..52816f363 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -19,6 +19,9 @@ let owner: unknown = null; /** Optional cleanup function set by whichever object uses the widget. */ let dispose: (() => void) | null = null; +/** A class name representing the current owner's workspace container. */ +const containerClassName = 'blocklyWidgetDiv'; + /** A class name representing the current owner's workspace renderer. */ let rendererClassName = ''; @@ -45,18 +48,21 @@ export function getDiv(): HTMLDivElement | null { */ export function testOnly_setDiv(newDiv: HTMLDivElement | null) { containerDiv = newDiv; + if (newDiv === null) { + document.querySelector('.' + containerClassName)?.remove(); + } } /** * Create the widget div and inject it onto the page. */ export function createDom() { - if (containerDiv) { + if (document.querySelector('.' + containerClassName)) { return; // Already created. } containerDiv = document.createElement('div') as HTMLDivElement; - containerDiv.className = 'blocklyWidgetDiv'; + containerDiv.className = containerClassName; const container = common.getParentContainer() || document.body; container.appendChild(containerDiv); } diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts index 7d8e18e68..0aa4292c6 100644 --- a/core/workspace_comment_svg.ts +++ b/core/workspace_comment_svg.ts @@ -315,22 +315,25 @@ export class WorkspaceCommentSvg * @internal */ override getRelativeToSurfaceXY(): Coordinate { + const layerManger = this.workspace.getLayerManager(); + if (!layerManger) { + throw new Error( + 'Cannot calculate position because the workspace has not been appended', + ); + } + let x = 0; let y = 0; - let element: Node | null = this.getSvgRoot(); + let element: SVGElement | null = this.getSvgRoot(); if (element) { do { // Loop through this comment and every parent. - const xy = svgMath.getRelativeXY(element as Element); + const xy = svgMath.getRelativeXY(element); x += xy.x; y += xy.y; - element = element.parentNode; - } while ( - element && - element !== this.workspace.getBubbleCanvas() && - element !== null - ); + element = element.parentNode as SVGElement; + } while (element && !layerManger.hasLayer(element) && element !== null); } this.xy_ = new Coordinate(x, y); return this.xy_; diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 00ebcbdc1..fe1386587 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -78,6 +78,7 @@ import {ZoomControls} from './zoom_controls.js'; import {ContextMenuOption} from './contextmenu_registry.js'; import * as renderManagement from './render_management.js'; import * as deprecation from './utils/deprecation.js'; +import {LayerManager} from './layer_manager.js'; /** Margin around the top/bottom/left/right after a zoomToFit call. */ const ZOOM_TO_FIT_MARGIN = 20; @@ -246,6 +247,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { | ((menuOptions: ContextMenuOption[], e: Event) => void) | null = null; + /** + * A dummy wheel event listener used as a workaround for a Safari scrolling issue. + * Set in createDom and used for removal in dispose to ensure proper cleanup. + */ + private dummyWheelListener: (() => void) | null = null; + /** * In a flyout, the target workspace where blocks should be placed after a * drag. Otherwise null. @@ -305,6 +312,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { private dragTargetAreas: Array<{component: IDragTarget; clientRect: Rect}> = []; private readonly cachedParentSvgSize: Size; + private layerManager: LayerManager | null = null; // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. svgGroup_!: SVGElement; // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. @@ -639,7 +647,11 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { x += xy.x * scale; y += xy.y * scale; element = element.parentNode as SVGElement; - } while (element && element !== this.getParentSvg()); + } while ( + element && + element !== this.getParentSvg() && + element !== this.getInjectionDiv() + ); return new Coordinate(x, y); } @@ -709,7 +721,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ getBlockCanvas(): SVGElement | null { - return this.svgBlockCanvas_; + return this.getCanvas(); } /** @@ -728,7 +740,11 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * 'blocklyMutatorBackground'. * @returns The workspace's SVG group. */ - createDom(opt_backgroundClass?: string): Element { + createDom(opt_backgroundClass?: string, injectionDiv?: Element): Element { + if (!this.injectionDiv) { + this.injectionDiv = injectionDiv ?? null; + } + /** * * @@ -760,16 +776,11 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { ); } } - this.svgBlockCanvas_ = dom.createSvgElement( - Svg.G, - {'class': 'blocklyBlockCanvas'}, - this.svgGroup_, - ); - this.svgBubbleCanvas_ = dom.createSvgElement( - Svg.G, - {'class': 'blocklyBubbleCanvas'}, - this.svgGroup_, - ); + + this.layerManager = new LayerManager(this); + // Assign the canvases for backwards compatibility. + this.svgBlockCanvas_ = this.layerManager.getBlockLayer(); + this.svgBubbleCanvas_ = this.layerManager.getBubbleLayer(); if (!this.isFlyout) { browserEvents.conditionalBind( @@ -782,7 +793,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { // This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683, // which otherwise prevents zoom/scroll events from being observed in // Safari. Once that bug is fixed it should be removed. - document.body.addEventListener('wheel', function () {}); + this.dummyWheelListener = () => {}; + document.body.addEventListener('wheel', this.dummyWheelListener); browserEvents.conditionalBind( this.svgGroup_, 'wheel', @@ -891,6 +903,12 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { browserEvents.unbind(this.resizeHandlerWrapper); this.resizeHandlerWrapper = null; } + + // Remove the dummy wheel listener + if (this.dummyWheelListener) { + document.body.removeEventListener('wheel', this.dummyWheelListener); + this.dummyWheelListener = null; + } } /** @@ -901,7 +919,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { addTrashcan() { this.trashcan = WorkspaceSvg.newTrashcan(this); const svgTrashcan = this.trashcan.createDom(); - this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_); + this.svgGroup_.insertBefore(svgTrashcan, this.getCanvas()); } /** @@ -1074,13 +1092,22 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { } /* eslint-enable indent */ + /** + * @returns The layer manager for this workspace. + * + * @internal + */ + getLayerManager(): LayerManager | null { + return this.layerManager; + } + /** * Get the SVG element that forms the drawing surface. * * @returns SVG group element. */ getCanvas(): SVGGElement { - return this.svgBlockCanvas_ as SVGGElement; + return this.layerManager!.getBlockLayer(); } /** @@ -1113,7 +1140,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @returns SVG group element. */ getBubbleCanvas(): SVGGElement { - return this.svgBubbleCanvas_ as SVGGElement; + return this.layerManager!.getBubbleLayer(); } /** @@ -1181,15 +1208,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * the Blockly div. */ translate(x: number, y: number) { - const translation = - 'translate(' + x + ',' + y + ') ' + 'scale(' + this.scale + ')'; - this.svgBlockCanvas_.setAttribute('transform', translation); - this.svgBubbleCanvas_.setAttribute('transform', translation); - // And update the grid if we're using one. - if (this.grid) { - this.grid.moveTo(x, y); - } - + this.layerManager?.translateLayers(new Coordinate(x, y), this.scale); + this.grid?.moveTo(x, y); this.maybeFireViewportChangeEvent(); } @@ -2023,8 +2043,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ beginCanvasTransition() { - dom.addClass(this.svgBlockCanvas_, 'blocklyCanvasTransitioning'); - dom.addClass(this.svgBubbleCanvas_, 'blocklyCanvasTransitioning'); + dom.addClass(this.getCanvas(), 'blocklyCanvasTransitioning'); + dom.addClass(this.getBubbleCanvas(), 'blocklyCanvasTransitioning'); } /** @@ -2033,8 +2053,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ endCanvasTransition() { - dom.removeClass(this.svgBlockCanvas_, 'blocklyCanvasTransitioning'); - dom.removeClass(this.svgBubbleCanvas_, 'blocklyCanvasTransitioning'); + dom.removeClass(this.getCanvas(), 'blocklyCanvasTransitioning'); + dom.removeClass(this.getBubbleCanvas(), 'blocklyCanvasTransitioning'); } /** Center the workspace. */ diff --git a/core/xml.ts b/core/xml.ts index ca8f6c8f4..c3b2d3ebc 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -592,7 +592,6 @@ export function domToBlockInternal( topBlockSvg.setConnectionTracking(true); } }, 1); - topBlockSvg.updateDisabled(); // Allow the scrollbars to resize and move based on the new contents. // TODO(@picklesrus): #387. Remove when domToBlock avoids resizing. (workspace as WorkspaceSvg).resizeContents(); diff --git a/demos/blockfactory/workspacefactory/wfactory_generator.js b/demos/blockfactory/workspacefactory/wfactory_generator.js index 9f115c40b..ca1a47b09 100644 --- a/demos/blockfactory/workspacefactory/wfactory_generator.js +++ b/demos/blockfactory/workspacefactory/wfactory_generator.js @@ -45,7 +45,7 @@ WorkspaceFactoryGenerator.prototype.generateToolboxXml = function() { // Create DOM for XML. var xmlDom = Blockly.utils.xml.createElement('xml'); xmlDom.id = 'toolbox'; - xmlDom.style.display = 'none'; + xmlDom.setAttribute('style', 'display: none'); if (!this.model.hasElements()) { // Toolbox has no categories. Use XML directly from workspace. @@ -111,7 +111,7 @@ WorkspaceFactoryGenerator.prototype.generateWorkspaceXml = function() { // Generate XML and set attributes. var xmlDom = Blockly.Xml.workspaceToDom(this.hiddenWorkspace); xmlDom.id = 'workspaceBlocks'; - xmlDom.style.display = 'none'; + xmlDom.setAttribute('style', 'display: none'); return xmlDom; }; diff --git a/generators/dart.js b/generators/dart.ts similarity index 69% rename from generators/dart.js rename to generators/dart.ts index 26a3c62c7..ee21b1fbd 100644 --- a/generators/dart.js +++ b/generators/dart.ts @@ -5,9 +5,9 @@ */ /** - * @fileoverview Complete helper functions for generating Dart for - * blocks. This is the entrypoint for dart_compressed.js. - * @suppress {extraRequire} + * @file Instantiate a DartGenerator and populate it with the complete + * set of block generator functions for Dart. This is the entrypoint + * for dart_compressed.js. */ // Former goog.module ID: Blockly.Dart.all @@ -36,8 +36,17 @@ export const dartGenerator = new DartGenerator(); dartGenerator.addReservedWords('Html,Math'); // Install per-block-type generator functions: -Object.assign( - dartGenerator.forBlock, - colour, lists, logic, loops, math, procedures, - text, variables, variablesDynamic -); +const generators: typeof dartGenerator.forBlock = { + ...colour, + ...lists, + ...logic, + ...loops, + ...math, + ...procedures, + ...text, + ...variables, + ...variablesDynamic, +}; +for (const name in generators) { + dartGenerator.forBlock[name] = generators[name]; +} diff --git a/generators/dart/colour.js b/generators/dart/colour.ts similarity index 61% rename from generators/dart/colour.js rename to generators/dart/colour.ts index b5405a34f..ac72fc04c 100644 --- a/generators/dart/colour.js +++ b/generators/dart/colour.ts @@ -5,27 +5,38 @@ */ /** - * @fileoverview Generating Dart for colour blocks. + * @file Generating Dart for colour blocks. */ // Former goog.module ID: Blockly.Dart.colour +import type {Block} from '../../core/block.js'; +import type {DartGenerator} from './dart_generator.js'; import {Order} from './dart_generator.js'; - // RESERVED WORDS: 'Math' -export function colour_picker(block, generator) { +export function colour_picker( + block: Block, + generator: DartGenerator, +): [string, Order] { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; -}; +} -export function colour_random(block, generator) { +export function colour_random( + block: Block, + generator: DartGenerator, +): [string, Order] { // Generate a random colour. - generator.definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_('colour_random', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'colour_random', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}() { String hex = '0123456789abcdef'; var rnd = new Math.Random(); @@ -33,20 +44,28 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}() { '\${hex[rnd.nextInt(16)]}\${hex[rnd.nextInt(16)]}' '\${hex[rnd.nextInt(16)]}\${hex[rnd.nextInt(16)]}'; } -`); +`, + ); const code = functionName + '()'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function colour_rgb(block, generator) { +export function colour_rgb( + block: Block, + generator: DartGenerator, +): [string, Order] { // Compose a colour from RGB components expressed as percentages. const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - generator.definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_('colour_rgb', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'colour_rgb', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(num r, num g, num b) { num rn = (Math.max(Math.min(r, 100), 0) * 2.55).round(); String rs = rn.toInt().toRadixString(16); @@ -62,23 +81,28 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(num r, num g, num b) { bs = bs.substring(bs.length - 2); return '#$rs$gs$bs'; } -`); +`, + ); const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function colour_blend(block, generator) { +export function colour_blend( + block: Block, + generator: DartGenerator, +): [string, Order] { // Blend two colours together. - const c1 = - generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const c2 = - generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; - const ratio = - generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; + const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; + const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; + const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - generator.definitions_['import_dart_math'] = - "import 'dart:math' as Math;"; - const functionName = generator.provideFunction_('colour_blend', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'colour_blend', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String c1, String c2, num ratio) { ratio = Math.max(Math.min(ratio, 1), 0); int r1 = int.parse('0x\${c1.substring(1, 3)}'); @@ -101,7 +125,8 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String c1, String c2, num ratio) bs = bs.substring(bs.length - 2); return '#$rs$gs$bs'; } -`); +`, + ); const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} diff --git a/generators/dart/dart_generator.js b/generators/dart/dart_generator.js deleted file mode 100644 index 412af49ee..000000000 --- a/generators/dart/dart_generator.js +++ /dev/null @@ -1,308 +0,0 @@ -/** - * @license - * Copyright 2014 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Helper functions for generating Dart for blocks. - * @suppress {checkTypes|globalThis} - */ - -// Former goog.module ID: 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. - */ - 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. - */ - 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/dart_generator.ts b/generators/dart/dart_generator.ts new file mode 100644 index 000000000..222bbd41d --- /dev/null +++ b/generators/dart/dart_generator.ts @@ -0,0 +1,321 @@ +/** + * @license + * Copyright 2014 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Dart code generator class, including helper methods for + * generating Dart for blocks. + */ + +// Former goog.module ID: 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 + */ +// prettier-ignore +export enum 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 { + /** @param name Name of the language the generator is for. */ + constructor(name = 'Dart') { + super(name); + 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) { + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; + } + + // List of illegal variable names. This is not intended to be a + // security feature. Blockly is 100% client-side, so bypassing + // this list is trivial. This is intended to prevent users from + // accidentally clobbering a built-in object or function. + 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 to generate code from. + */ + init(workspace: 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 import statements and variable definitions. + * + * @param code Generated code. + * @returns Completed code. + */ + finish(code: string): string { + // 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. + * + * @param line Line of generated code. + * @returns Legal line of code. + */ + scrubNakedValue(line: string): string { + return line + ';\n'; + } + + /** + * Encode a string as a properly escaped Dart string, complete with quotes. + * + * @param string Text to encode. + * @returns Dart string. + */ + quote_(string: string): 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 Text to encode. + * @returns Dart string. + */ + multiline_quote_(string: string): 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 The current block. + * @param code The Dart code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns Dart code with comments and subsequent blocks added. + */ + scrub_(block: Block, code: string, thisOnly = false): string { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // Collect comment for this block. + let comment = block.getCommentText(); + if (comment) { + comment = stringUtils.wrap(comment, this.COMMENT_WRAP - 3); + if ((block as AnyDuringMigration).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 = thisOnly ? '' : this.blockToCode(nextBlock); + return commentCode + code + nextCode; + } + + /** + * Generate code representing the specified value input, adjusted to take into + * account indexing (zero- or one-based) and optionally by a specified delta + * and/or by negation. + * + * @param block The block. + * @param atId The ID of the input block to get (and adjust) the value of. + * @param delta Value to add. + * @param negate Whether to negate the value. + * @param order The highest order acting on this value. + * @returns The adjusted value or code that evaluates to it. + */ + getAdjusted( + block: Block, + atId: string, + delta = 0, + negate = false, + order = Order.NONE, + ): string { + if (block.workspace.options.oneBasedIndex) { + delta--; + } + const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + + let orderForInput = order; + if (delta) { + orderForInput = Order.ADDITIVE; + } else if (negate) { + orderForInput = Order.UNARY_PREFIX; + } + + let at = this.valueToCode(block, atId, orderForInput) || defaultAtIndex; + + // Easy case: no adjustments. + if (delta === 0 && !negate) { + return at; + } + // If the index is a naked number, adjust it right now. + if (stringUtils.isNumber(at)) { + at = String(Number(at) + delta); + if (negate) { + at = String(-Number(at)); + } + return at; + } + // If the index is dynamic, adjust it in code. + if (delta > 0) { + at = `${at} + ${delta}`; + } else if (delta < 0) { + at = `${at} - ${-delta}`; + } + if (negate) { + at = delta ? `-(${at})` : `-${at}`; + } + if (Math.floor(order) >= Math.floor(orderForInput)) { + at = `(${at})`; + } + return at; + } +} diff --git a/generators/dart/lists.js b/generators/dart/lists.ts similarity index 66% rename from generators/dart/lists.js rename to generators/dart/lists.ts index c7a540b69..4aaf39905 100644 --- a/generators/dart/lists.js +++ b/generators/dart/lists.ts @@ -5,85 +5,109 @@ */ /** - * @fileoverview Generating Dart for list blocks. + * @file Generating Dart for list blocks. */ // Former goog.module ID: Blockly.Dart.lists +import type {Block} from '../../core/block.js'; +import type {CreateWithBlock} from '../../blocks/lists.js'; +import type {DartGenerator} from './dart_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './dart_generator.js'; - // RESERVED WORDS: 'Math' -export function lists_create_empty(block, generator) { +export function lists_create_empty( + block: Block, + generator: DartGenerator, +): [string, Order] { // Create an empty list. return ['[]', Order.ATOMIC]; -}; +} -export function lists_create_with(block, generator) { +export function lists_create_with( + block: Block, + generator: DartGenerator, +): [string, Order] { // Create a list with any number of elements of any type. - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; + const createWithBlock = block as CreateWithBlock; + const elements = new Array(createWithBlock.itemCount_); + for (let i = 0; i < createWithBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; } const code = '[' + elements.join(', ') + ']'; return [code, Order.ATOMIC]; -}; +} -export function lists_repeat(block, generator) { +export function lists_repeat( + block: Block, + generator: DartGenerator, +): [string, Order] { // Create a list with one element repeated. - const element = - generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; - const repeatCount = - generator.valueToCode(block, 'NUM', Order.NONE) || '0'; + const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; + const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; const code = 'new List.filled(' + repeatCount + ', ' + element + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function lists_length(block, generator) { +export function lists_length( + block: Block, + generator: DartGenerator, +): [string, Order] { // String or array length. const list = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; return [list + '.length', Order.UNARY_POSTFIX]; -}; +} -export function lists_isEmpty(block, generator) { +export function lists_isEmpty( + block: Block, + generator: DartGenerator, +): [string, Order] { // Is the string null or array empty? const list = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; return [list + '.isEmpty', Order.UNARY_POSTFIX]; -}; +} -export function lists_indexOf(block, generator) { +export function lists_indexOf( + block: Block, + generator: DartGenerator, +): [string, Order] { // Find an item in the list. const operator = - block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; const item = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const list = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || '[]'; const code = list + '.' + operator + '(' + item + ')'; if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITIVE]; } return [code, Order.UNARY_POSTFIX]; -}; +} -export function lists_getIndex(block, generator) { +export function lists_getIndex( + block: Block, + generator: DartGenerator, +): [string, Order] | string { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const listOrder = (where === 'RANDOM' || where === 'FROM_END') ? - Order.NONE : - Order.UNARY_POSTFIX; + const listOrder = + where === 'RANDOM' || where === 'FROM_END' + ? Order.NONE + : Order.UNARY_POSTFIX; let list = generator.valueToCode(block, 'VALUE', listOrder) || '[]'; // Cache non-trivial values to variables to prevent repeated look-ups. // Closure, which accesses and modifies 'list'. function cacheList() { - const listVar = - generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmp_list', + NameType.VARIABLE, + ); const code = 'List ' + listVar + ' = ' + list + ';\n'; list = listVar; return code; @@ -91,51 +115,60 @@ export function lists_getIndex(block, generator) { // If `list` would be evaluated more than once (which is the case for // RANDOM REMOVE and FROM_END) and is non-trivial, make sure to access it // only once. - if (((where === 'RANDOM' && mode === 'REMOVE') || where === 'FROM_END') && - !list.match(/^\w+$/)) { + if ( + ((where === 'RANDOM' && mode === 'REMOVE') || where === 'FROM_END') && + !list.match(/^\w+$/) + ) { // `list` is an expression, so we may not evaluate it more than once. if (where === 'RANDOM') { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; // We can use multiple statements. let code = cacheList(); - const xVar = - generator.nameDB_.getDistinctName('tmp_x', NameType.VARIABLE); - code += 'int ' + xVar + ' = new Math.Random().nextInt(' + list + - '.length);\n'; + const xVar = generator.nameDB_!.getDistinctName( + 'tmp_x', + NameType.VARIABLE, + ); + code += + 'int ' + xVar + ' = new Math.Random().nextInt(' + list + '.length);\n'; code += list + '.removeAt(' + xVar + ');\n'; return code; - } else { // where === 'FROM_END' + } else { + // where === 'FROM_END' if (mode === 'REMOVE') { // We can use multiple statements. - const at = - generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); + const at = generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); let code = cacheList(); - code += list + '.removeAt(' + list + '.length' + - ' - ' + at + ');\n'; + code += list + '.removeAt(' + list + '.length' + ' - ' + at + ');\n'; return code; - } else if (mode === 'GET') { const at = generator.getAdjusted(block, 'AT', 1); // We need to create a procedure to avoid reevaluating values. - const functionName = generator.provideFunction_('lists_get_from_end', ` + const functionName = generator.provideFunction_( + 'lists_get_from_end', + ` dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list, num x) { x = my_list.length - x; return my_list[x]; } -`); +`, + ); const code = functionName + '(' + list + ', ' + at + ')'; return [code, Order.UNARY_POSTFIX]; } else if (mode === 'GET_REMOVE') { const at = generator.getAdjusted(block, 'AT', 1); // We need to create a procedure to avoid reevaluating values. - const functionName = - generator.provideFunction_('lists_remove_from_end', ` + const functionName = generator.provideFunction_( + 'lists_remove_from_end', + ` dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list, num x) { x = my_list.length - x; return my_list.removeAt(x); } -`); +`, + ); const code = functionName + '(' + list + ', ' + at + ')'; return [code, Order.UNARY_POSTFIX]; } @@ -180,8 +213,7 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list, num x) { break; } case 'FROM_END': { - const at = - generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); + const at = generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); if (mode === 'GET') { const code = list + '[' + list + '.length - ' + at + ']'; return [code, Order.UNARY_POSTFIX]; @@ -196,34 +228,46 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list, num x) { break; } case 'RANDOM': - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; if (mode === 'REMOVE') { // We can use multiple statements. - const xVar = - generator.nameDB_.getDistinctName('tmp_x', NameType.VARIABLE); - let code = 'int ' + xVar + ' = new Math.Random().nextInt(' + list + - '.length);\n'; + const xVar = generator.nameDB_!.getDistinctName( + 'tmp_x', + NameType.VARIABLE, + ); + let code = + 'int ' + + xVar + + ' = new Math.Random().nextInt(' + + list + + '.length);\n'; code += list + '.removeAt(' + xVar + ');\n'; return code; } else if (mode === 'GET') { - const functionName = - generator.provideFunction_('lists_get_random_item', ` + const functionName = generator.provideFunction_( + 'lists_get_random_item', + ` dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list) { int x = new Math.Random().nextInt(my_list.length); return my_list[x]; } -`); +`, + ); const code = functionName + '(' + list + ')'; return [code, Order.UNARY_POSTFIX]; } else if (mode === 'GET_REMOVE') { - const functionName = - generator.provideFunction_('lists_remove_random_item', ` + const functionName = generator.provideFunction_( + 'lists_remove_random_item', + ` dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list) { int x = new Math.Random().nextInt(my_list.length); return my_list.removeAt(x); } -`); +`, + ); const code = functionName + '(' + list + ')'; return [code, Order.UNARY_POSTFIX]; } @@ -231,25 +275,25 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List my_list) { } } throw Error('Unhandled combination (lists_getIndex).'); -}; +} -export function lists_setIndex(block, generator) { +export function lists_setIndex(block: Block, generator: DartGenerator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - let list = - generator.valueToCode(block, 'LIST', Order.UNARY_POSTFIX) || '[]'; - const value = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; + let list = generator.valueToCode(block, 'LIST', Order.UNARY_POSTFIX) || '[]'; + const value = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; // Cache non-trivial values to variables to prevent repeated look-ups. // Closure, which accesses and modifies 'list'. function cacheList() { if (list.match(/^\w+$/)) { return ''; } - const listVar = - generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmp_list', + NameType.VARIABLE, + ); const code = 'List ' + listVar + ' = ' + list + ';\n'; list = listVar; return code; @@ -281,27 +325,30 @@ export function lists_setIndex(block, generator) { break; } case 'FROM_END': { - const at = - generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); + const at = generator.getAdjusted(block, 'AT', 1, false, Order.ADDITIVE); let code = cacheList(); if (mode === 'SET') { code += list + '[' + list + '.length - ' + at + '] = ' + value + ';\n'; return code; } else if (mode === 'INSERT') { - code += list + '.insert(' + list + '.length - ' + at + ', ' + value + - ');\n'; + code += + list + '.insert(' + list + '.length - ' + at + ', ' + value + ');\n'; return code; } break; } case 'RANDOM': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; let code = cacheList(); - const xVar = - generator.nameDB_.getDistinctName('tmp_x', NameType.VARIABLE); - code += 'int ' + xVar + ' = new Math.Random().nextInt(' + list + - '.length);\n'; + const xVar = generator.nameDB_!.getDistinctName( + 'tmp_x', + NameType.VARIABLE, + ); + code += + 'int ' + xVar + ' = new Math.Random().nextInt(' + list + '.length);\n'; if (mode === 'SET') { code += list + '[' + xVar + '] = ' + value + ';\n'; return code; @@ -313,17 +360,22 @@ export function lists_setIndex(block, generator) { } } throw Error('Unhandled combination (lists_setIndex).'); -}; +} -export function lists_getSublist(block, generator) { +export function lists_getSublist( + block: Block, + generator: DartGenerator, +): [string, Order] { // Get sublist. const list = - generator.valueToCode(block, 'LIST', Order.UNARY_POSTFIX) || '[]'; + generator.valueToCode(block, 'LIST', Order.UNARY_POSTFIX) || '[]'; const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); let code; - if (list.match(/^\w+$/) || - (where1 !== 'FROM_END' && where2 === 'FROM_START')) { + if ( + list.match(/^\w+$/) || + (where1 !== 'FROM_END' && where2 === 'FROM_START') + ) { // If the list is a is a variable or doesn't require a call for length, // don't generate a helper function. let at1; @@ -364,7 +416,9 @@ export function lists_getSublist(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const functionName = generator.provideFunction_('lists_get_sublist', ` + const functionName = generator.provideFunction_( + 'lists_get_sublist', + ` List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String where1, num at1, String where2, num at2) { int getAt(String where, num at) { if (where == 'FROM_END') { @@ -382,19 +436,36 @@ List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String where1, num at1, at2 = getAt(where2, at2) + 1; return list.sublist(at1, at2); } -`); - code = functionName + '(' + list + ', \'' + where1 + '\', ' + at1 + ', \'' + - where2 + '\', ' + at2 + ')'; +`, + ); + code = + functionName + + '(' + + list + + ", '" + + where1 + + "', " + + at1 + + ", '" + + where2 + + "', " + + at2 + + ')'; } return [code, Order.UNARY_POSTFIX]; -}; +} -export function lists_sort(block, generator) { +export function lists_sort( + block: Block, + generator: DartGenerator, +): [string, Order] { // Block for sorting a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; const type = block.getFieldValue('TYPE'); - const sortFunctionName = generator.provideFunction_('lists_sort', ` + const sortFunctionName = generator.provideFunction_( + 'lists_sort', + ` List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String type, int direction) { var compareFuncs = { 'NUMERIC': (a, b) => (direction * a.compareTo(b)).toInt(), @@ -408,19 +479,21 @@ List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List list, String type, int directi list.sort(compare); return list; } -`); +`, + ); return [ - sortFunctionName + '(' + list + ', ' + - '"' + type + '", ' + direction + ')', - Order.UNARY_POSTFIX + sortFunctionName + '(' + list + ', ' + '"' + type + '", ' + direction + ')', + Order.UNARY_POSTFIX, ]; -}; +} -export function lists_split(block, generator) { +export function lists_split( + block: Block, + generator: DartGenerator, +): [string, Order] { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.UNARY_POSTFIX); - const delimiter = - generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; + const delimiter = generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; const mode = block.getFieldValue('MODE'); let functionName; if (mode === 'SPLIT') { @@ -438,12 +511,15 @@ export function lists_split(block, generator) { } const code = input + '.' + functionName + '(' + delimiter + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function lists_reverse(block, generator) { +export function lists_reverse( + block: Block, + generator: DartGenerator, +): [string, Order] { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; // XXX What should the operator precedence be for a `new`? const code = 'new List.from(' + list + '.reversed)'; return [code, Order.UNARY_POSTFIX]; -}; +} diff --git a/generators/dart/logic.js b/generators/dart/logic.ts similarity index 51% rename from generators/dart/logic.js rename to generators/dart/logic.ts index f3cbcb8ae..45c385b86 100644 --- a/generators/dart/logic.js +++ b/generators/dart/logic.ts @@ -5,35 +5,43 @@ */ /** - * @fileoverview Generating Dart for logic blocks. + * @file Generating Dart for logic blocks. */ // Former goog.module ID: Blockly.Dart.logic +import type {Block} from '../../core/block.js'; +import type {DartGenerator} from './dart_generator.js'; import {Order} from './dart_generator.js'; - -export function controls_if(block, generator) { +export function controls_if(block: Block, generator: DartGenerator) { // If/elseif/else condition. let n = 0; - let code = '', branchCode, conditionCode; + let code = '', + branchCode, + conditionCode; if (generator.STATEMENT_PREFIX) { // Automatic prefix insertion is switched off for this block. Add manually. code += generator.injectId(generator.STATEMENT_PREFIX, block); } do { conditionCode = - generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; + generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; branchCode = generator.statementToCode(block, 'DO' + n); if (generator.STATEMENT_SUFFIX) { branchCode = - generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), generator.INDENT) + - branchCode; + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; } - code += (n > 0 ? 'else ' : '') + 'if (' + conditionCode + ') {\n' + - branchCode + '}'; + code += + (n > 0 ? 'else ' : '') + + 'if (' + + conditionCode + + ') {\n' + + branchCode + + '}'; n++; } while (block.getInput('IF' + n)); @@ -41,37 +49,48 @@ export function controls_if(block, generator) { branchCode = generator.statementToCode(block, 'ELSE'); if (generator.STATEMENT_SUFFIX) { branchCode = - generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), generator.INDENT) + - branchCode; + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; } code += ' else {\n' + branchCode + '}'; } return code + '\n'; -}; +} export const controls_ifelse = controls_if; -export function logic_compare(block, generator) { +export function logic_compare( + block: Block, + generator: DartGenerator, +): [string, Order] { // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const order = (operator === '==' || operator === '!=') ? - Order.EQUALITY : - Order.RELATIONAL; + const OPERATORS = { + 'EQ': '==', + 'NEQ': '!=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const order = + operator === '==' || operator === '!=' ? Order.EQUALITY : Order.RELATIONAL; const argument0 = generator.valueToCode(block, 'A', order) || '0'; const argument1 = generator.valueToCode(block, 'B', order) || '0'; const code = argument0 + ' ' + operator + ' ' + argument1; return [code, order]; -}; +} -export function logic_operation(block, generator) { +export function logic_operation( + block: Block, + generator: DartGenerator, +): [string, Order] { // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; - const order = - (operator === '&&') ? Order.LOGICAL_AND : Order.LOGICAL_OR; + const operator = block.getFieldValue('OP') === 'AND' ? '&&' : '||'; + const order = operator === '&&' ? Order.LOGICAL_AND : Order.LOGICAL_OR; let argument0 = generator.valueToCode(block, 'A', order); let argument1 = generator.valueToCode(block, 'B', order); if (!argument0 && !argument1) { @@ -80,7 +99,7 @@ export function logic_operation(block, generator) { argument1 = 'false'; } else { // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === '&&') ? 'true' : 'false'; + const defaultArgument = operator === '&&' ? 'true' : 'false'; if (!argument0) { argument0 = defaultArgument; } @@ -90,35 +109,47 @@ export function logic_operation(block, generator) { } const code = argument0 + ' ' + operator + ' ' + argument1; return [code, order]; -}; +} -export function logic_negate(block, generator) { +export function logic_negate( + block: Block, + generator: DartGenerator, +): [string, Order] { // Negation. const order = Order.UNARY_PREFIX; const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; const code = '!' + argument0; return [code, order]; -}; +} -export function logic_boolean(block, generator) { +export function logic_boolean( + block: Block, + generator: DartGenerator, +): [string, Order] { // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; return [code, Order.ATOMIC]; -}; +} -export function logic_null(block, generator) { +export function logic_null( + block: Block, + generator: DartGenerator, +): [string, Order] { // Null data type. return ['null', Order.ATOMIC]; -}; +} -export function logic_ternary(block, generator) { +export function logic_ternary( + block: Block, + generator: DartGenerator, +): [string, Order] { // Ternary operator. const value_if = - generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; + generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; const value_then = - generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; + generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; const value_else = - generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; + generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; const code = value_if + ' ? ' + value_then + ' : ' + value_else; return [code, Order.CONDITIONAL]; -}; +} diff --git a/generators/dart/loops.js b/generators/dart/loops.ts similarity index 56% rename from generators/dart/loops.js rename to generators/dart/loops.ts index 1ead2d7e8..a6b6bb180 100644 --- a/generators/dart/loops.js +++ b/generators/dart/loops.ts @@ -5,17 +5,19 @@ */ /** - * @fileoverview Generating Dart for loop blocks. + * @file Generating Dart for loop blocks. */ // Former goog.module ID: Blockly.Dart.loops -import {Order} from './dart_generator.js'; import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; +import type {DartGenerator} from './dart_generator.js'; import {NameType} from '../../core/names.js'; +import {Order} from './dart_generator.js'; - -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext(block: Block, generator: DartGenerator) { let repeats; // Repeat n times. if (block.getField('TIMES')) { @@ -23,61 +25,85 @@ export function controls_repeat_ext(block, generator) { repeats = String(Number(block.getFieldValue('TIMES'))); } else { // External number. - repeats = - generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0'; + repeats = generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0'; } let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; - const loopVar = - generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); let endVar = repeats; if (!repeats.match(/^\w+$/) && !stringUtils.isNumber(repeats)) { - endVar = - generator.nameDB_.getDistinctName('repeat_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + 'repeat_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + repeats + ';\n'; } - code += 'for (int ' + loopVar + ' = 0; ' + loopVar + ' < ' + endVar + '; ' + - loopVar + '++) {\n' + branch + '}\n'; + code += + 'for (int ' + + loopVar + + ' = 0; ' + + loopVar + + ' < ' + + endVar + + '; ' + + loopVar + + '++) {\n' + + branch + + '}\n'; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil(block: Block, generator: DartGenerator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = - generator.valueToCode( - block, 'BOOL', until ? Order.UNARY_PREFIX : Order.NONE) || - 'false'; + generator.valueToCode( + block, + 'BOOL', + until ? Order.UNARY_PREFIX : Order.NONE, + ) || 'false'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); if (until) { argument0 = '!' + argument0; } return 'while (' + argument0 + ') {\n' + branch + '}\n'; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: DartGenerator) { // For loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; - const argument1 = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; - const increment = - generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; + generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code; - if (stringUtils.isNumber(argument0) && stringUtils.isNumber(argument1) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(argument0) && + stringUtils.isNumber(argument1) && + stringUtils.isNumber(increment) + ) { // All arguments are simple numbers. const up = Number(argument0) <= Number(argument1); - code = 'for (' + variable0 + ' = ' + argument0 + '; ' + variable0 + - (up ? ' <= ' : ' >= ') + argument1 + '; ' + variable0; + code = + 'for (' + + variable0 + + ' = ' + + argument0 + + '; ' + + variable0 + + (up ? ' <= ' : ' >= ') + + argument1 + + '; ' + + variable0; const step = Math.abs(Number(increment)); if (step === 1) { code += up ? '++' : '--'; @@ -90,54 +116,77 @@ export function controls_for(block, generator) { // Cache non-trivial values to variables to prevent repeated look-ups. let startVar = argument0; if (!argument0.match(/^\w+$/) && !stringUtils.isNumber(argument0)) { - startVar = - generator.nameDB_.getDistinctName( - variable0 + '_start', NameType.VARIABLE); + startVar = generator.nameDB_!.getDistinctName( + variable0 + '_start', + NameType.VARIABLE, + ); code += 'var ' + startVar + ' = ' + argument0 + ';\n'; } let endVar = argument1; if (!argument1.match(/^\w+$/) && !stringUtils.isNumber(argument1)) { - endVar = - generator.nameDB_.getDistinctName( - variable0 + '_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + variable0 + '_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + argument1 + ';\n'; } // Determine loop direction at start, in case one of the bounds // changes during loop execution. - const incVar = - generator.nameDB_.getDistinctName( - variable0 + '_inc', NameType.VARIABLE); + const incVar = generator.nameDB_!.getDistinctName( + variable0 + '_inc', + NameType.VARIABLE, + ); code += 'num ' + incVar + ' = '; if (stringUtils.isNumber(increment)) { - code += Math.abs(increment) + ';\n'; + code += Math.abs(Number(increment)) + ';\n'; } else { code += '(' + increment + ').abs();\n'; } code += 'if (' + startVar + ' > ' + endVar + ') {\n'; code += generator.INDENT + incVar + ' = -' + incVar + ';\n'; code += '}\n'; - code += 'for (' + variable0 + ' = ' + startVar + '; ' + incVar + - ' >= 0 ? ' + variable0 + ' <= ' + endVar + ' : ' + variable0 + - ' >= ' + endVar + '; ' + variable0 + ' += ' + incVar + ') {\n' + - branch + '}\n'; + code += + 'for (' + + variable0 + + ' = ' + + startVar + + '; ' + + incVar + + ' >= 0 ? ' + + variable0 + + ' <= ' + + endVar + + ' : ' + + variable0 + + ' >= ' + + endVar + + '; ' + + variable0 + + ' += ' + + incVar + + ') {\n' + + branch + + '}\n'; } return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach(block: Block, generator: DartGenerator) { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; + generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); const code = - 'for (var ' + variable0 + ' in ' + argument0 + ') {\n' + branch + '}\n'; + 'for (var ' + variable0 + ' in ' + argument0 + ') {\n' + branch + '}\n'; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: DartGenerator, +) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { @@ -150,7 +199,7 @@ export function controls_flow_statements(block, generator) { xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. @@ -165,4 +214,4 @@ export function controls_flow_statements(block, generator) { return xfix + 'continue;\n'; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/dart/math.js b/generators/dart/math.ts similarity index 59% rename from generators/dart/math.js rename to generators/dart/math.ts index 8541b95a8..550821e9f 100644 --- a/generators/dart/math.js +++ b/generators/dart/math.ts @@ -5,44 +5,49 @@ */ /** - * @fileoverview Generating Dart for math blocks. + * @file Generating Dart for math blocks. */ // Former goog.module ID: Blockly.Dart.math +import type {Block} from '../../core/block.js'; +import type {DartGenerator} from './dart_generator.js'; import {Order} from './dart_generator.js'; - // RESERVED WORDS: 'Math' -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: DartGenerator, +): [string, Order] { // Numeric value. - let code = Number(block.getFieldValue('NUM')); - let order; - if (code === Infinity) { - code = 'double.infinity'; - order = Order.UNARY_POSTFIX; - } else if (code === -Infinity) { - code = '-double.infinity'; - order = Order.UNARY_PREFIX; + const number = Number(block.getFieldValue('NUM')); + if (number === Infinity) { + return ['double.infinity', Order.UNARY_POSTFIX]; + } else if (number === -Infinity) { + return ['-double.infinity', Order.UNARY_PREFIX]; } else { - // -4.abs() returns -4 in generator due to strange order of operation choices. - // -4 is actually an operator and a number. Reflect this in the order. - order = code < 0 ? Order.UNARY_PREFIX : Order.ATOMIC; + // -4.abs() returns -4 in generator due to strange order of + // operation choices. 4 is actually an operator and a number. + // Reflect this in the order. + return [String(number), number < 0 ? Order.UNARY_PREFIX : Order.ATOMIC]; } - return [code, order]; -}; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: DartGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITIVE], 'MINUS': [' - ', Order.ADDITIVE], 'MULTIPLY': [' * ', Order.MULTIPLICATIVE], 'DIVIDE': [' / ', Order.MULTIPLICATIVE], - 'POWER': [null, Order.NONE], // Handle power separately. + 'POWER': [null, Order.NONE], // Handle power separately. }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; @@ -50,16 +55,21 @@ export function math_arithmetic(block, generator) { let code; // Power in generator requires a special case since it has no operator. if (!operator) { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; code = 'Math.pow(' + argument0 + ', ' + argument1 + ')'; return [code, Order.UNARY_POSTFIX]; } code = argument0 + operator + argument1; return [code, order]; -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: DartGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -74,8 +84,10 @@ export function math_single(block, generator) { code = '-' + arg; return [code, Order.UNARY_PREFIX]; } - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; if (operator === 'ABS' || operator.substring(0, 5) === 'ROUND') { arg = generator.valueToCode(block, 'NUM', Order.UNARY_POSTFIX) || '0'; } else if (operator === 'SIN' || operator === 'COS' || operator === 'TAN') { @@ -142,11 +154,14 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.MULTIPLICATIVE]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: DartGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['Math.pi', Order.UNARY_POSTFIX], 'E': ['Math.e', Order.UNARY_POSTFIX], 'GOLDEN_RATIO': ['(1 + Math.sqrt(5)) / 2', Order.MULTIPLICATIVE], @@ -154,18 +169,24 @@ export function math_constant(block, generator) { 'SQRT1_2': ['Math.sqrt1_2', Order.UNARY_POSTFIX], 'INFINITY': ['double.infinity', Order.ATOMIC], }; - const constant = block.getFieldValue('CONSTANT'); + type ConstantOption = keyof typeof CONSTANTS; + const constant = block.getFieldValue('CONSTANT') as ConstantOption; if (constant !== 'INFINITY') { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; } return CONSTANTS[constant]; -}; +} -export function math_number_property(block, generator) { +export function math_number_property( + block: Block, + generator: DartGenerator, +): [string, Order] { // Check if a number is even, odd, prime, whole, positive, or negative // or if it is divisible by certain number. Returns true or false. - const PROPERTIES = { + const PROPERTIES: Record = { 'EVEN': [' % 2 == 0', Order.MULTIPLICATIVE, Order.EQUALITY], 'ODD': [' % 2 == 1', Order.MULTIPLICATIVE, Order.EQUALITY], 'WHOLE': [' % 1 == 0', Order.MULTIPLICATIVE, Order.EQUALITY], @@ -174,16 +195,21 @@ export function math_number_property(block, generator) { 'DIVISIBLE_BY': [null, Order.MULTIPLICATIVE, Order.EQUALITY], 'PRIME': [null, Order.NONE, Order.UNARY_POSTFIX], }; - const dropdownProperty = block.getFieldValue('PROPERTY'); + type PropertyOption = keyof typeof PROPERTIES; + const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption; const [suffix, inputOrder, outputOrder] = PROPERTIES[dropdownProperty]; - const numberToCheck = generator.valueToCode(block, 'NUMBER_TO_CHECK', - inputOrder) || '0'; + const numberToCheck = + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = generator.provideFunction_('math_isPrime', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'math_isPrime', + ` bool ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { // https://en.wikipedia.org/wiki/Primality_test#Naive_methods if (n == 2 || n == 3) { @@ -202,11 +228,12 @@ bool ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { } return true; } -`); +`, + ); code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MULTIPLICATIVE) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; if (divisor === '0') { return ['false', Order.ATOMIC]; } @@ -215,72 +242,97 @@ bool ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { code = numberToCheck + suffix; } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: DartGenerator) { // Add to a variable in place. const argument0 = - generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = (' + varName + ' is num ? ' + varName + ' : 0) + ' + - argument0 + ';\n'; -}; + generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + return ( + varName + + ' = (' + + varName + + ' is num ? ' + + varName + + ' : 0) + ' + + argument0 + + ';\n' + ); +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: DartGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; let code; switch (func) { case 'SUM': { - const functionName = generator.provideFunction_('math_sum', ` + const functionName = generator.provideFunction_( + 'math_sum', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { num sumVal = 0; myList.forEach((num entry) {sumVal += entry;}); return sumVal; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'MIN': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = generator.provideFunction_('math_min', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'math_min', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { if (myList.isEmpty) return null; num minVal = myList[0]; myList.forEach((num entry) {minVal = Math.min(minVal, entry);}); return minVal; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'MAX': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = generator.provideFunction_('math_max', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'math_max', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { if (myList.isEmpty) return null; num maxVal = myList[0]; myList.forEach((num entry) {maxVal = Math.max(maxVal, entry);}); return maxVal; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'AVERAGE': { // This operation exclude null and values that are not int or float: // math_mean([null,null,"aString",1,9]) -> 5.0 - const functionName = generator.provideFunction_('math_mean', ` + const functionName = generator.provideFunction_( + 'math_mean', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { // First filter list for numbers only. List localList = new List.from(myList); @@ -290,12 +342,15 @@ num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { localList.forEach((var entry) {sumVal += entry;}); return sumVal / localList.length; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'MEDIAN': { - const functionName = generator.provideFunction_('math_median', ` + const functionName = generator.provideFunction_( + 'math_median', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { // First filter list for numbers only, then sort, then return middle value // or the average of two middle values if list has an even number of elements. @@ -310,17 +365,22 @@ num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { return (localList[index - 1] + localList[index]) / 2; } } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'MODE': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1] - const functionName = generator.provideFunction_('math_modes', ` + const functionName = generator.provideFunction_( + 'math_modes', + ` List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List values) { List modes = []; List counts = []; @@ -349,15 +409,19 @@ List ${generator.FUNCTION_NAME_PLACEHOLDER_}(List values) { } return modes; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'STD_DEV': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = - generator.provideFunction_('math_standard_deviation', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'math_standard_deviation', + ` num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { // First filter list for numbers only. List numbers = new List.from(myList); @@ -371,19 +435,25 @@ num ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { numbers.forEach((x) => sumSquare += Math.pow(x - mean, 2)); return Math.sqrt(sumSquare / n); } -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'RANDOM': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = generator.provideFunction_('math_random_item', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'math_random_item', + ` dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { int x = new Math.Random().nextInt(myList.length); return myList[x]; } -`); +`, + ); code = functionName + '(' + list + ')'; break; } @@ -391,39 +461,59 @@ dynamic ${generator.FUNCTION_NAME_PLACEHOLDER_}(List myList) { throw Error('Unknown operator: ' + func); } return [code, Order.UNARY_POSTFIX]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: DartGenerator, +): [string, Order] { // Remainder computation. const argument0 = - generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; + generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; const argument1 = - generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MULTIPLICATIVE]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: DartGenerator, +): [string, Order] { // Constrain a number between two limits. - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const argument0 = - generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; const argument2 = - generator.valueToCode(block, 'HIGH', Order.NONE) || 'double.infinity'; - const code = 'Math.min(Math.max(' + argument0 + ', ' + argument1 + '), ' + - argument2 + ')'; + generator.valueToCode(block, 'HIGH', Order.NONE) || 'double.infinity'; + const code = + 'Math.min(Math.max(' + + argument0 + + ', ' + + argument1 + + '), ' + + argument2 + + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: DartGenerator, +): [string, Order] { // Random integer between [X] and [Y]. - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; - const functionName = generator.provideFunction_('math_random_int', ` + const functionName = generator.provideFunction_( + 'math_random_int', + ` int ${generator.FUNCTION_NAME_PLACEHOLDER_}(num a, num b) { if (a > b) { // Swap a and b to ensure a is smaller. @@ -433,26 +523,37 @@ int ${generator.FUNCTION_NAME_PLACEHOLDER_}(num a, num b) { } return new Math.Random().nextInt(b - a + 1) + a; } -`); +`, + ); const code = functionName + '(' + argument0 + ', ' + argument1 + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: DartGenerator, +): [string, Order] { // Random fraction between 0 and 1. - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; return ['new Math.Random().nextDouble()', Order.UNARY_POSTFIX]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: DartGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; return [ 'Math.atan2(' + argument1 + ', ' + argument0 + ') / Math.pi * 180', - Order.MULTIPLICATIVE + Order.MULTIPLICATIVE, ]; -}; +} diff --git a/generators/dart/procedures.js b/generators/dart/procedures.ts similarity index 59% rename from generators/dart/procedures.js rename to generators/dart/procedures.ts index e782abb68..5102432ad 100644 --- a/generators/dart/procedures.js +++ b/generators/dart/procedures.ts @@ -5,18 +5,19 @@ */ /** - * @fileoverview Generating Dart for procedure blocks. + * @file Generating Dart for procedure blocks. */ // Former goog.module ID: Blockly.Dart.procedures +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; +import type {DartGenerator} from './dart_generator.js'; import {Order} from './dart_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn(block: Block, generator: DartGenerator) { // Define a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -30,12 +31,12 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId(generator.INFINITE_LOOP_TRAP, block), - generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } const branch = generator.statementToCode(block, 'STACK'); - let returnValue = - generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. @@ -50,22 +51,37 @@ export function procedures_defreturn(block, generator) { for (let i = 0; i < variables.length; i++) { args[i] = generator.getVariableName(variables[i]); } - let code = returnType + ' ' + funcName + '(' + args.join(', ') + ') {\n' + - xfix1 + loopTrap + branch + xfix2 + returnValue + '}'; + let code = + returnType + + ' ' + + funcName + + '(' + + args.join(', ') + + ') {\n' + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue + + '}'; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: DartGenerator, +): [string, Order] { // Call a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { @@ -73,35 +89,41 @@ export function procedures_callreturn(block, generator) { } let code = funcName + '(' + args.join(', ') + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn( + block: Block, + generator: DartGenerator, +) { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn']( + block, + generator, + ) as [string, Order]; return tuple[0] + ';\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn(block: Block, generator: DartGenerator) { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; let code = 'if (' + condition + ') {\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. code += generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), generator.INDENT); + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { - const value = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; + if ((block as IfReturnBlock).hasReturnValue_) { + const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; code += generator.INDENT + 'return ' + value + ';\n'; } else { code += generator.INDENT + 'return;\n'; } code += '}\n'; return code; -}; +} diff --git a/generators/dart/text.js b/generators/dart/text.ts similarity index 61% rename from generators/dart/text.js rename to generators/dart/text.ts index 69ffbf7df..108516019 100644 --- a/generators/dart/text.js +++ b/generators/dart/text.ts @@ -5,97 +5,116 @@ */ /** - * @fileoverview Generating Dart for text blocks. + * @file Generating Dart for text blocks. */ // Former goog.module ID: Blockly.Dart.texts +import type {Block} from '../../core/block.js'; +import type {DartGenerator} from './dart_generator.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; import {Order} from './dart_generator.js'; - // RESERVED WORDS: 'Html,Math' -export function text(block, generator) { +export function text(block: Block, generator: DartGenerator): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: DartGenerator, +): [string, Order] { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = - code.indexOf('+') !== -1 ? Order.ADDITIVE : Order.ATOMIC; + const order = code.indexOf('+') !== -1 ? Order.ADDITIVE : Order.ATOMIC; return [code, order]; -}; +} -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: DartGenerator, +): [string, Order] { // Create a string made up of any number of elements of any type. - switch (block.itemCount_) { + const joinBlock = block as JoinMutatorBlock; + switch (joinBlock.itemCount_) { case 0: return ["''", Order.ATOMIC]; case 1: { const element = - generator.valueToCode(block, 'ADD0', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'ADD0', Order.UNARY_POSTFIX) || "''"; const code = element + '.toString()'; return [code, Order.UNARY_POSTFIX]; } default: { - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { + const elements = new Array(joinBlock.itemCount_); + for (let i = 0; i < joinBlock.itemCount_; i++) { elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; + generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; } const code = '[' + elements.join(',') + '].join()'; return [code, Order.UNARY_POSTFIX]; } } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: DartGenerator) { // Append to a variable in place. - const varName = - generator.getVariableName(block.getFieldValue('VAR')); + const varName = generator.getVariableName(block.getFieldValue('VAR')); const value = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return varName + ' = [' + varName + ', ' + value + '].join();\n'; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: DartGenerator, +): [string, Order] { // String or array length. const text = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; return [text + '.length', Order.UNARY_POSTFIX]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: DartGenerator, +): [string, Order] { // Is the string null or array empty? const text = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; return [text + '.isEmpty', Order.UNARY_POSTFIX]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: DartGenerator, +): [string, Order] { // Search the text for a substring. const operator = - block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; - const substring = - generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const text = - generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'VALUE', Order.UNARY_POSTFIX) || "''"; const code = text + '.' + operator + '(' + substring + ')'; if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITIVE]; } return [code, Order.UNARY_POSTFIX]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: DartGenerator, +): [string, Order] { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const textOrder = (where === 'FIRST' || where === 'FROM_START') ? - Order.UNARY_POSTFIX : - Order.NONE; + const textOrder = + where === 'FIRST' || where === 'FROM_START' + ? Order.UNARY_POSTFIX + : Order.NONE; const text = generator.valueToCode(block, 'VALUE', textOrder) || "''"; let at; switch (where) { @@ -110,41 +129,50 @@ export function text_charAt(block, generator) { } case 'LAST': at = 1; - // Fall through. + // Fall through. case 'FROM_END': { at = generator.getAdjusted(block, 'AT', 1); - const functionName = generator.provideFunction_('text_get_from_end', ` + const functionName = generator.provideFunction_( + 'text_get_from_end', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text, num x) { return text[text.length - x]; } -`); +`, + ); const code = functionName + '(' + text + ', ' + at + ')'; return [code, Order.UNARY_POSTFIX]; } case 'RANDOM': { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; - const functionName = - generator.provideFunction_('text_random_letter', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; + const functionName = generator.provideFunction_( + 'text_random_letter', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text) { int x = new Math.Random().nextInt(text.length); return text[x]; } -`); +`, + ); const code = functionName + '(' + text + ')'; return [code, Order.UNARY_POSTFIX]; } } throw Error('Unhandled option (text_charAt).'); -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: DartGenerator, +): [string, Order] { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); - const requiresLengthCall = (where1 !== 'FROM_END' && where2 === 'FROM_START'); - const textOrder = - requiresLengthCall ? Order.UNARY_POSTFIX : Order.NONE; + const requiresLengthCall = where1 !== 'FROM_END' && where2 === 'FROM_START'; + const textOrder = requiresLengthCall ? Order.UNARY_POSTFIX : Order.NONE; const text = generator.valueToCode(block, 'STRING', textOrder) || "''"; let code; if (where1 === 'FIRST' && where2 === 'LAST') { @@ -191,8 +219,9 @@ export function text_getSubstring(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const functionName = - generator.provideFunction_('text_get_substring', ` + const functionName = generator.provideFunction_( + 'text_get_substring', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text, String where1, num at1, String where2, num at2) { int getAt(String where, num at) { if (where == 'FROM_END') { @@ -210,21 +239,37 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String text, String where1, num a at2 = getAt(where2, at2) + 1; return text.substring(at1, at2); } -`); - code = functionName + '(' + text + ', \'' + where1 + '\', ' + at1 + ', \'' + - where2 + '\', ' + at2 + ')'; +`, + ); + code = + functionName + + '(' + + text + + ", '" + + where1 + + "', " + + at1 + + ", '" + + where2 + + "', " + + at2 + + ')'; } return [code, Order.UNARY_POSTFIX]; -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: DartGenerator, +): [string, Order] { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.toUpperCase()', 'LOWERCASE': '.toLowerCase()', - 'TITLECASE': null + 'TITLECASE': null, }; - const operator = OPERATORS[block.getFieldValue('CASE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('CASE') as OperatorOption]; const textOrder = operator ? Order.UNARY_POSTFIX : Order.NONE; const text = generator.valueToCode(block, 'TEXT', textOrder) || "''"; let code; @@ -233,7 +278,9 @@ export function text_changeCase(block, generator) { code = text + operator; } else { // Title case is not a native generator function. Define one. - const functionName = generator.provideFunction_('text_toTitleCase', ` + const functionName = generator.provideFunction_( + 'text_toTitleCase', + ` String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String str) { RegExp exp = new RegExp(r'\\b'); List list = str.split(exp); @@ -248,35 +295,45 @@ String ${generator.FUNCTION_NAME_PLACEHOLDER_}(String str) { } return title.toString(); } -`); +`, + ); code = functionName + '(' + text + ')'; } return [code, Order.UNARY_POSTFIX]; -}; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: DartGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = { - 'LEFT': '.replaceFirst(new RegExp(r\'^\\s+\'), \'\')', - 'RIGHT': '.replaceFirst(new RegExp(r\'\\s+$\'), \'\')', - 'BOTH': '.trim()' + 'LEFT': ".replaceFirst(new RegExp(r'^\\s+'), '')", + 'RIGHT': ".replaceFirst(new RegExp(r'\\s+$'), '')", + 'BOTH': '.trim()', }; - const operator = OPERATORS[block.getFieldValue('MODE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; const text = - generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; return [text + operator, Order.UNARY_POSTFIX]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: DartGenerator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ');\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: DartGenerator, +): [string, Order] { // Prompt function. - generator.definitions_['import_dart_html'] = - 'import \'dart:html\' as Html;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_html'] = + "import 'dart:html' as Html;"; let msg; if (block.getField('TEXT')) { // Internal message. @@ -285,23 +342,30 @@ export function text_prompt_ext(block, generator) { // External message. msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; } - let code = 'Html.window.prompt(' + msg + ', \'\')'; + let code = 'Html.window.prompt(' + msg + ", '')"; const toNumber = block.getFieldValue('TYPE') === 'NUMBER'; if (toNumber) { - generator.definitions_['import_dart_math'] = - 'import \'dart:math\' as Math;'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_dart_math'] = + "import 'dart:math' as Math;"; code = 'Math.parseDouble(' + code + ')'; } return [code, Order.UNARY_POSTFIX]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { +export function text_count( + block: Block, + generator: DartGenerator, +): [string, Order] { 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. - const functionName = generator.provideFunction_('text_count', ` + const functionName = generator.provideFunction_( + 'text_count', + ` int ${generator.FUNCTION_NAME_PLACEHOLDER_}(String haystack, String needle) { if (needle.length == 0) { return haystack.length + 1; @@ -317,26 +381,33 @@ int ${generator.FUNCTION_NAME_PLACEHOLDER_}(String haystack, String needle) { } return count; } -`); +`, + ); const code = functionName + '(' + text + ', ' + sub + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function text_replace(block, generator) { +export function text_replace( + block: Block, + generator: DartGenerator, +): [string, Order] { const text = - generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; const code = text + '.replaceAll(' + from + ', ' + to + ')'; return [code, Order.UNARY_POSTFIX]; -}; +} -export function text_reverse(block, generator) { +export function text_reverse( + block: Block, + generator: DartGenerator, +): [string, Order] { // 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? const text = - generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; + generator.valueToCode(block, 'TEXT', Order.UNARY_POSTFIX) || "''"; const code = 'new String.fromCharCodes(' + text + '.runes.toList().reversed)'; return [code, Order.UNARY_PREFIX]; -}; +} diff --git a/generators/dart/variables.js b/generators/dart/variables.js deleted file mode 100644 index c08843760..000000000 --- a/generators/dart/variables.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2014 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating Dart for variable blocks. - */ - -// Former goog.module ID: Blockly.Dart.variables - -import {Order} from './dart_generator.js'; - - -export function variables_get(block, generator) { - // Variable getter. - const code = - generator.getVariableName(block.getFieldValue('VAR')); - return [code, Order.ATOMIC]; -}; - -export function variables_set(block, generator) { - // Variable setter. - const argument0 = - generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = ' + argument0 + ';\n'; -}; diff --git a/generators/dart/variables.ts b/generators/dart/variables.ts new file mode 100644 index 000000000..4863d0417 --- /dev/null +++ b/generators/dart/variables.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2014 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating Dart for variable blocks. + */ + +// Former goog.module ID: Blockly.Dart.variables + +import type {Block} from '../../core/block.js'; +import type {DartGenerator} from './dart_generator.js'; +import {Order} from './dart_generator.js'; + +export function variables_get( + block: Block, + generator: DartGenerator, +): [string, Order] { + // Variable getter. + const code = generator.getVariableName(block.getFieldValue('VAR')); + return [code, Order.ATOMIC]; +} + +export function variables_set(block: Block, generator: DartGenerator) { + // Variable setter. + const argument0 = + generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + return varName + ' = ' + argument0 + ';\n'; +} diff --git a/generators/dart/variables_dynamic.js b/generators/dart/variables_dynamic.ts similarity index 83% rename from generators/dart/variables_dynamic.js rename to generators/dart/variables_dynamic.ts index 22be966dc..68cdf22d6 100644 --- a/generators/dart/variables_dynamic.js +++ b/generators/dart/variables_dynamic.ts @@ -5,12 +5,11 @@ */ /** - * @fileoverview Generating Dart for dynamic variable blocks. + * @file Generating Dart for dynamic variable blocks. */ // Former goog.module ID: Blockly.Dart.variablesDynamic - // generator is dynamically typed. export { variables_get as variables_get_dynamic, diff --git a/generators/javascript.js b/generators/javascript.ts similarity index 66% rename from generators/javascript.js rename to generators/javascript.ts index 7ff772c5b..2358d187d 100644 --- a/generators/javascript.js +++ b/generators/javascript.ts @@ -5,9 +5,9 @@ */ /** - * @fileoverview Complete helper functions for generating JavaScript for - * blocks. This is the entrypoint for javascript_compressed.js. - * @suppress {extraRequire} + * @file Instantiate a JavascriptGenerator and populate it with the + * complete set of block generator functions for JavaScript. This is + * the entrypoint for javascript_compressed.js. */ // Former goog.module ID: Blockly.JavaScript.all @@ -32,8 +32,17 @@ export * from './javascript/javascript_generator.js'; export const javascriptGenerator = new JavascriptGenerator(); // Install per-block-type generator functions: -Object.assign( - javascriptGenerator.forBlock, - colour, lists, logic, loops, math, procedures, - text, variables, variablesDynamic -); +const generators: typeof javascriptGenerator.forBlock = { + ...colour, + ...lists, + ...logic, + ...loops, + ...math, + ...procedures, + ...text, + ...variables, + ...variablesDynamic, +}; +for (const name in generators) { + javascriptGenerator.forBlock[name] = generators[name]; +} diff --git a/generators/javascript/colour.js b/generators/javascript/colour.ts similarity index 67% rename from generators/javascript/colour.js rename to generators/javascript/colour.ts index 41d588c75..b599e76d9 100644 --- a/generators/javascript/colour.js +++ b/generators/javascript/colour.ts @@ -5,40 +5,53 @@ */ /** - * @fileoverview Generating JavaScript for colour blocks. + * @file Generating JavaScript for colour blocks. */ // Former goog.module ID: Blockly.JavaScript.colour +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function colour_picker(block, generator) { +export function colour_picker( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; -}; +} -export function colour_random(block, generator) { +export function colour_random( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Generate a random colour. - const functionName = generator.provideFunction_('colourRandom', ` + const functionName = generator.provideFunction_( + 'colourRandom', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { var num = Math.floor(Math.random() * Math.pow(2, 24)); return '#' + ('00000' + num.toString(16)).substr(-6); } -`); +`, + ); const code = functionName + '()'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_rgb(block, generator) { +export function colour_rgb( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Compose a colour from RGB components expressed as percentages. const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; - const green = - generator.valueToCode(block, 'GREEN', Order.NONE) || 0; - const blue = - generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const functionName = generator.provideFunction_('colourRgb', ` + const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; + const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; + const functionName = generator.provideFunction_( + 'colourRgb', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { r = Math.max(Math.min(Number(r), 100), 0) * 2.55; g = Math.max(Math.min(Number(g), 100), 0) * 2.55; @@ -48,20 +61,23 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b) { b = ('0' + (Math.round(b) || 0).toString(16)).slice(-2); return '#' + r + g + b; } -`); +`, + ); const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_blend(block, generator) { +export function colour_blend( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Blend two colours together. - const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || - "'#000000'"; - const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || - "'#000000'"; - const ratio = - generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - const functionName = generator.provideFunction_('colourBlend', ` + const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; + const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; + const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; + const functionName = generator.provideFunction_( + 'colourBlend', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(c1, c2, ratio) { ratio = Math.max(Math.min(Number(ratio), 1), 0); var r1 = parseInt(c1.substring(1, 3), 16); @@ -78,7 +94,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(c1, c2, ratio) { b = ('0' + (b || 0).toString(16)).slice(-2); return '#' + r + g + b; } -`); +`, + ); const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/javascript/javascript_generator.js b/generators/javascript/javascript_generator.ts similarity index 53% rename from generators/javascript/javascript_generator.js rename to generators/javascript/javascript_generator.ts index f85fce18f..02602e71f 100644 --- a/generators/javascript/javascript_generator.js +++ b/generators/javascript/javascript_generator.ts @@ -5,73 +5,69 @@ */ /** - * @fileoverview Helper functions for generating JavaScript for blocks. - * @suppress {checkTypes|globalThis} + * @file JavaScript code generator class, including helper methods for + * generating JavaScript for blocks. */ // Former goog.module ID: Blockly.JavaScript import * as Variables from '../../core/variables.js'; import * as stringUtils from '../../core/utils/string.js'; -// import type {Block} from '../../core/block.js'; +import type {Block} from '../../core/block.js'; import {CodeGenerator} from '../../core/generator.js'; import {Names, NameType} from '../../core/names.js'; -// import type {Workspace} from '../../core/workspace.js'; +import type {Workspace} from '../../core/workspace.js'; import {inputTypes} from '../../core/inputs/input_types.js'; - /** * Order of operation ENUMs. * https://developer.mozilla.org/en/JavaScript/Reference/Operators/Operator_Precedence - * @enum {number} */ -export const Order = { - ATOMIC: 0, // 0 "" ... - NEW: 1.1, // new - MEMBER: 1.2, // . [] - FUNCTION_CALL: 2, // () - INCREMENT: 3, // ++ - DECREMENT: 3, // -- - BITWISE_NOT: 4.1, // ~ - UNARY_PLUS: 4.2, // + - UNARY_NEGATION: 4.3, // - - LOGICAL_NOT: 4.4, // ! - TYPEOF: 4.5, // typeof - VOID: 4.6, // void - DELETE: 4.7, // delete - AWAIT: 4.8, // await - EXPONENTIATION: 5.0, // ** - MULTIPLICATION: 5.1, // * - DIVISION: 5.2, // / - MODULUS: 5.3, // % - SUBTRACTION: 6.1, // - - ADDITION: 6.2, // + - BITWISE_SHIFT: 7, // << >> >>> - RELATIONAL: 8, // < <= > >= - IN: 8, // in - INSTANCEOF: 8, // instanceof - EQUALITY: 9, // == != === !== - BITWISE_AND: 10, // & - BITWISE_XOR: 11, // ^ - BITWISE_OR: 12, // | - LOGICAL_AND: 13, // && - LOGICAL_OR: 14, // || - CONDITIONAL: 15, // ?: - ASSIGNMENT: 16, //: += -= **= *= /= %= <<= >>= ... - YIELD: 17, // yield - COMMA: 18, // , - NONE: 99, // (...) -}; +// prettier-ignore +export enum Order { + ATOMIC = 0, // 0 "" ... + NEW = 1.1, // new + MEMBER = 1.2, // . [] + FUNCTION_CALL = 2, // () + INCREMENT = 3, // ++ + DECREMENT = 3, // -- + BITWISE_NOT = 4.1, // ~ + UNARY_PLUS = 4.2, // + + UNARY_NEGATION = 4.3, // - + LOGICAL_NOT = 4.4, // ! + TYPEOF = 4.5, // typeof + VOID = 4.6, // void + DELETE = 4.7, // delete + AWAIT = 4.8, // await + EXPONENTIATION = 5.0, // ** + MULTIPLICATION = 5.1, // * + DIVISION = 5.2, // / + MODULUS = 5.3, // % + SUBTRACTION = 6.1, // - + ADDITION = 6.2, // + + BITWISE_SHIFT = 7, // << >> >>> + RELATIONAL = 8, // < <= > >= + IN = 8, // in + INSTANCEOF = 8, // instanceof + EQUALITY = 9, // == != === !== + BITWISE_AND = 10, // & + BITWISE_XOR = 11, // ^ + BITWISE_OR = 12, // | + LOGICAL_AND = 13, // && + LOGICAL_OR = 14, // || + CONDITIONAL = 15, // ?: + ASSIGNMENT = 16, // = += -= **= *= /= %= <<= >>= ... + YIELD = 17, // yield + COMMA = 18, // , + NONE = 99, // (...) +} /** * JavaScript code generator class. */ export class JavascriptGenerator extends CodeGenerator { - /** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} - */ - ORDER_OVERRIDES = [ + /** List of outer-inner pairings that do NOT require parentheses. */ + ORDER_OVERRIDES: [Order, Order][] = [ // (foo()).bar -> foo().bar // (foo())[0] -> foo()[0] [Order.FUNCTION_CALL, Order.MEMBER], @@ -95,11 +91,12 @@ export class JavascriptGenerator extends CodeGenerator { // a && (b && c) -> a && b && c [Order.LOGICAL_AND, Order.LOGICAL_AND], // a || (b || c) -> a || b || c - [Order.LOGICAL_OR, Order.LOGICAL_OR] + [Order.LOGICAL_OR, Order.LOGICAL_OR], ]; - constructor(name) { - super(name ?? 'JavaScript'); + /** @param name Name of the language the generator is for. */ + constructor(name = 'JavaScript') { + super(name); this.isInitialized = false; // Copy Order values onto instance for backwards compatibility @@ -110,16 +107,26 @@ export class JavascriptGenerator extends CodeGenerator { // replace data properties with get accessors that call // deprecate.warn().) for (const key in Order) { - this['ORDER_' + key] = Order[key]; + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; } // List of illegal variable names. This is not intended to be a // security feature. Blockly is 100% client-side, so bypassing // this list is trivial. This is intended to prevent users from // accidentally clobbering a built-in object or function. + // + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords this.addReservedWords( - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords - 'break,case,catch,class,const,continue,debugger,default,delete,do,' + + 'break,case,catch,class,const,continue,debugger,default,delete,do,' + 'else,export,extends,finally,for,function,if,import,in,instanceof,' + 'new,return,super,switch,this,throw,try,typeof,var,void,' + 'while,with,yield,' + @@ -131,15 +138,16 @@ export class JavascriptGenerator extends CodeGenerator { 'arguments,' + // Everything in the current environment (835 items in Chrome, // 104 in Node). - Object.getOwnPropertyNames(globalThis).join(',') + Object.getOwnPropertyNames(globalThis).join(','), ); } /** * Initialise the database of variable names. - * @param {!Workspace} workspace Workspace to generate code from. + * + * @param workspace Workspace to generate code from. */ - init(workspace) { + init(workspace: Workspace) { super.init(workspace); if (!this.nameDB_) { @@ -157,14 +165,16 @@ export class JavascriptGenerator extends CodeGenerator { const devVarList = Variables.allDeveloperVariables(workspace); for (let i = 0; i < devVarList.length; i++) { defvars.push( - this.nameDB_.getName(devVarList[i], NameType.DEVELOPER_VARIABLE)); + this.nameDB_.getName(devVarList[i], NameType.DEVELOPER_VARIABLE), + ); } // Add user variables, but only ones that are being used. const variables = Variables.allUsedVarModels(workspace); for (let i = 0; i < variables.length; i++) { defvars.push( - this.nameDB_.getName(variables[i].getId(), NameType.VARIABLE)); + this.nameDB_.getName(variables[i].getId(), NameType.VARIABLE), + ); } // Declare all of the variables. @@ -176,70 +186,74 @@ export class JavascriptGenerator extends CodeGenerator { /** * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. + * + * @param code Generated code. + * @returns Completed code. */ - finish(code) { + finish(code: string): string { // Convert the definitions dictionary into a list. const definitions = Object.values(this.definitions_); // Call Blockly.CodeGenerator's finish. super.finish(code); this.isInitialized = false; - this.nameDB_.reset(); + this.nameDB_!.reset(); return definitions.join('\n\n') + '\n\n\n' + code; } /** * Naked values are top-level blocks with outputs that aren't plugged into * anything. A trailing semicolon is needed to make this legal. - * @param {string} line Line of generated code. - * @return {string} Legal line of code. + * + * @param line Line of generated code. + * @returns Legal line of code. */ - scrubNakedValue(line) { + scrubNakedValue(line: string): string { return line + ';\n'; } /** * Encode a string as a properly escaped JavaScript string, complete with * quotes. - * @param {string} string Text to encode. - * @return {string} JavaScript string. + * + * @param string Text to encode. + * @returns JavaScript string. */ - quote_(string) { + quote_(string: string): string { // Can't use goog.string.quote since Google's style guide recommends // JS string literals use single quotes. - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; + string = string + .replace(/\\/g, '\\\\') + .replace(/\n/g, '\\\n') + .replace(/'/g, "\\'"); + return "'" + string + "'"; } /** * Encode a string as a properly escaped multiline JavaScript string, complete * with quotes. - * @param {string} string Text to encode. - * @return {string} JavaScript string. + * @param string Text to encode. + * @returns JavaScript string. */ - multiline_quote_(string) { + multiline_quote_(string: string): string { // Can't use goog.string.quote since Google's style guide recommends // JS string literals use single quotes. const lines = string.split(/\n/g).map(this.quote_); - return lines.join(' + \'\\n\' +\n'); + return lines.join(" + '\\n' +\n"); } /** * Common tasks for generating JavaScript from blocks. * Handles comments for the specified block and any connected value blocks. * Calls any statements following this block. - * @param {!Block} block The current block. - * @param {string} code The JavaScript code created for this block. - * @param {boolean=} opt_thisOnly True to generate code for only this - * statement. - * @return {string} JavaScript code with comments and subsequent blocks added. + * + * @param block The current block. + * @param code The JavaScript code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns JavaScript code with comments and subsequent blocks added. * @protected */ - scrub_(block, code, opt_thisOnly) { + scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; // Only collect comments for blocks that aren't inline. if (!block.outputConnection || !block.outputConnection.targetConnection) { @@ -253,7 +267,7 @@ export class JavascriptGenerator extends CodeGenerator { // Don't collect comments for nested statements. for (let i = 0; i < block.inputList.length; i++) { if (block.inputList[i].type === inputTypes.VALUE) { - const childBlock = block.inputList[i].connection.targetBlock(); + const childBlock = block.inputList[i].connection!.targetBlock(); if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { @@ -264,68 +278,69 @@ export class JavascriptGenerator extends CodeGenerator { } } const nextBlock = - block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = thisOnly ? '' : this.blockToCode(nextBlock); return commentCode + code + nextCode; } /** - * Gets a property and adjusts the value while taking into account indexing. - * @param {!Block} block The block. - * @param {string} atId The property ID of the element to get. - * @param {number=} opt_delta Value to add. - * @param {boolean=} opt_negate Whether to negate the value. - * @param {number=} opt_order The highest order acting on this value. - * @return {string|number} + * Generate code representing the specified value input, adjusted to take into + * account indexing (zero- or one-based) and optionally by a specified delta + * and/or by negation. + * + * @param block The block. + * @param atId The ID of the input block to get (and adjust) the value of. + * @param delta Value to add. + * @param negate Whether to negate the value. + * @param order The highest order acting on this value. + * @returns The adjusted value or code that evaluates to it. */ - getAdjusted(block, atId, opt_delta, opt_negate, opt_order) { - let delta = opt_delta || 0; - let order = opt_order || this.ORDER_NONE; + getAdjusted( + block: Block, + atId: string, + delta = 0, + negate = false, + order = Order.NONE, + ): string { if (block.workspace.options.oneBasedIndex) { delta--; } const defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; - let innerOrder; - let outerOrder = order; + let orderForInput = order; if (delta > 0) { - outerOrder = this.ORDER_ADDITION; - innerOrder = this.ORDER_ADDITION; + orderForInput = Order.ADDITION; } else if (delta < 0) { - outerOrder = this.ORDER_SUBTRACTION; - innerOrder = this.ORDER_SUBTRACTION; - } else if (opt_negate) { - outerOrder = this.ORDER_UNARY_NEGATION; - innerOrder = this.ORDER_UNARY_NEGATION; + orderForInput = Order.SUBTRACTION; + } else if (negate) { + orderForInput = Order.UNARY_NEGATION; } - let at = this.valueToCode(block, atId, outerOrder) || defaultAtIndex; + let at = this.valueToCode(block, atId, orderForInput) || defaultAtIndex; + // Easy case: no adjustments. + if (delta === 0 && !negate) { + return at; + } + // If the index is a naked number, adjust it right now. if (stringUtils.isNumber(at)) { - // If the index is a naked number, adjust it right now. - at = Number(at) + delta; - if (opt_negate) { - at = -at; - } - } else { - // If the index is dynamic, adjust it in code. - if (delta > 0) { - at = at + ' + ' + delta; - } else if (delta < 0) { - at = at + ' - ' + -delta; - } - if (opt_negate) { - if (delta) { - at = '-(' + at + ')'; - } else { - at = '-' + at; - } - } - innerOrder = Math.floor(innerOrder); - order = Math.floor(order); - if (innerOrder && order >= innerOrder) { - at = '(' + at + ')'; + at = String(Number(at) + delta); + if (negate) { + at = String(-Number(at)); } + return at; + } + // If the index is dynamic, adjust it in code. + if (delta > 0) { + at = `${at} + ${delta}`; + } else if (delta < 0) { + at = `${at} - ${-delta}`; + } + if (negate) { + at = delta ? `-(${at})` : `-${at}`; + } + if (Math.floor(order) >= Math.floor(orderForInput)) { + at = `(${at})`; } return at; } diff --git a/generators/javascript/lists.js b/generators/javascript/lists.ts similarity index 60% rename from generators/javascript/lists.js rename to generators/javascript/lists.ts index 9e4265716..bbed5231b 100644 --- a/generators/javascript/lists.js +++ b/generators/javascript/lists.ts @@ -5,36 +5,47 @@ */ /** - * @fileoverview Generating JavaScript for list blocks. - * @suppress {missingRequire} + * @file Generating JavaScript for list blocks. */ // Former goog.module ID: Blockly.JavaScript.lists +import type {Block} from '../../core/block.js'; +import type {CreateWithBlock} from '../../blocks/lists.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './javascript_generator.js'; - -export function lists_create_empty(block, generator) { +export function lists_create_empty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create an empty list. return ['[]', Order.ATOMIC]; -}; +} -export function lists_create_with(block, generator) { +export function lists_create_with( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create a list with any number of elements of any type. - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || - 'null'; + const createWithBlock = block as CreateWithBlock; + const elements = new Array(createWithBlock.itemCount_); + for (let i = 0; i < createWithBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; } const code = '[' + elements.join(', ') + ']'; return [code, Order.ATOMIC]; -}; +} -export function lists_repeat(block, generator) { +export function lists_repeat( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create a list with one element repeated. - const functionName = generator.provideFunction_('listsRepeat', ` + const functionName = generator.provideFunction_( + 'listsRepeat', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { var array = []; for (var i = 0; i < n; i++) { @@ -42,56 +53,61 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(value, n) { } return array; } -`); - const element = - generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; - const repeatCount = - generator.valueToCode(block, 'NUM', Order.NONE) || '0'; +`, + ); + const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; + const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; const code = functionName + '(' + element + ', ' + repeatCount + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_length(block, generator) { +export function lists_length( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // String or array length. - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return [list + '.length', Order.MEMBER]; -}; +} -export function lists_isEmpty(block, generator) { +export function lists_isEmpty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Is the string null or array empty? - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; return ['!' + list + '.length', Order.LOGICAL_NOT]; -}; +} -export function lists_indexOf(block, generator) { +export function lists_indexOf( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Find an item in the list. const operator = - block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; - const item = - generator.valueToCode(block, 'FIND', Order.NONE) || "''"; - const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const item = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + const list = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; const code = list + '.' + operator + '(' + item + ')'; if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITION]; } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_getIndex(block, generator) { +export function lists_getIndex( + block: Block, + generator: JavascriptGenerator, +): [string, Order] | string { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const listOrder = - (where === 'RANDOM') ? Order.NONE : Order.MEMBER; - const list = - generator.valueToCode(block, 'VALUE', listOrder) || '[]'; + const listOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; + const list = generator.valueToCode(block, 'VALUE', listOrder) || '[]'; switch (where) { - case ('FIRST'): + case 'FIRST': if (mode === 'GET') { const code = list + '[0]'; return [code, Order.MEMBER]; @@ -102,7 +118,7 @@ export function lists_getIndex(block, generator) { return list + '.shift();\n'; } break; - case ('LAST'): + case 'LAST': if (mode === 'GET') { const code = list + '.slice(-1)[0]'; return [code, Order.MEMBER]; @@ -113,7 +129,7 @@ export function lists_getIndex(block, generator) { return list + '.pop();\n'; } break; - case ('FROM_START'): { + case 'FROM_START': { const at = generator.getAdjusted(block, 'AT'); if (mode === 'GET') { const code = list + '[' + at + ']'; @@ -126,7 +142,7 @@ export function lists_getIndex(block, generator) { } break; } - case ('FROM_END'): { + case 'FROM_END': { const at = generator.getAdjusted(block, 'AT', 1, true); if (mode === 'GET') { const code = list + '.slice(' + at + ')[0]'; @@ -139,9 +155,10 @@ export function lists_getIndex(block, generator) { } break; } - case ('RANDOM'): { - const functionName = - generator.provideFunction_('listsGetRandomItem', ` + case 'RANDOM': { + const functionName = generator.provideFunction_( + 'listsGetRandomItem', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { var x = Math.floor(Math.random() * list.length); if (remove) { @@ -150,7 +167,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { return list[x]; } } -`); +`, + ); const code = functionName + '(' + list + ', ' + (mode !== 'GET') + ')'; if (mode === 'GET' || mode === 'GET_REMOVE') { return [code, Order.FUNCTION_CALL]; @@ -161,40 +179,38 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, remove) { } } throw Error('Unhandled combination (lists_getIndex).'); -}; +} -export function lists_setIndex(block, generator) { +export function lists_setIndex(block: Block, generator: JavascriptGenerator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. - let list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; + let list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const value = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || - 'null'; + const value = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; // Cache non-trivial values to variables to prevent repeated look-ups. // Closure, which accesses and modifies 'list'. function cacheList() { if (list.match(/^\w+$/)) { return ''; } - const listVar = - generator.nameDB_.getDistinctName( - 'tmpList', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmpList', + NameType.VARIABLE, + )!; const code = 'var ' + listVar + ' = ' + list + ';\n'; list = listVar; return code; } switch (where) { - case ('FIRST'): + case 'FIRST': if (mode === 'SET') { return list + '[0] = ' + value + ';\n'; } else if (mode === 'INSERT') { return list + '.unshift(' + value + ');\n'; } break; - case ('LAST'): + case 'LAST': if (mode === 'SET') { let code = cacheList(); code += list + '[' + list + '.length - 1] = ' + value + ';\n'; @@ -203,7 +219,7 @@ export function lists_setIndex(block, generator) { return list + '.push(' + value + ');\n'; } break; - case ('FROM_START'): { + case 'FROM_START': { const at = generator.getAdjusted(block, 'AT'); if (mode === 'SET') { return list + '[' + at + '] = ' + value + ';\n'; @@ -212,27 +228,40 @@ export function lists_setIndex(block, generator) { } break; } - case ('FROM_END'): { + case 'FROM_END': { const at = generator.getAdjusted( - block, 'AT', 1, false, Order.SUBTRACTION); + block, + 'AT', + 1, + false, + Order.SUBTRACTION, + ); let code = cacheList(); if (mode === 'SET') { code += list + '[' + list + '.length - ' + at + '] = ' + value + ';\n'; return code; } else if (mode === 'INSERT') { - code += list + '.splice(' + list + '.length - ' + at + ', 0, ' + value + - ');\n'; + code += + list + + '.splice(' + + list + + '.length - ' + + at + + ', 0, ' + + value + + ');\n'; return code; } break; } - case ('RANDOM'): { + case 'RANDOM': { let code = cacheList(); - const xVar = - generator.nameDB_.getDistinctName( - 'tmpX', NameType.VARIABLE); - code += 'var ' + xVar + ' = Math.floor(Math.random() * ' + list + - '.length);\n'; + const xVar = generator.nameDB_!.getDistinctName( + 'tmpX', + NameType.VARIABLE, + ); + code += + 'var ' + xVar + ' = Math.floor(Math.random() * ' + list + '.length);\n'; if (mode === 'SET') { code += list + '[' + xVar + '] = ' + value + ';\n'; return code; @@ -244,16 +273,20 @@ export function lists_setIndex(block, generator) { } } throw Error('Unhandled combination (lists_setIndex).'); -}; +} /** * Returns an expression calculating the index into a list. - * @param {string} listName Name of the list, used to calculate length. - * @param {string} where The method of indexing, selected by dropdown in Blockly - * @param {string=} opt_at The optional offset when indexing from start/end. - * @return {string|undefined} Index expression. + * @param listName Name of the list, used to calculate length. + * @param where The method of indexing, selected by dropdown in Blockly + * @param opt_at The optional offset when indexing from start/end. + * @returns Index expression. */ -const getSubstringIndex = function(listName, where, opt_at) { +const getSubstringIndex = function ( + listName: string, + where: string, + opt_at?: string, +): string | undefined { if (where === 'FIRST') { return '0'; } else if (where === 'FROM_END') { @@ -265,18 +298,29 @@ const getSubstringIndex = function(listName, where, opt_at) { } }; -export function lists_getSublist(block, generator) { +export function lists_getSublist( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Get sublist. - const list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; - const where1 = block.getFieldValue('WHERE1'); - const where2 = block.getFieldValue('WHERE2'); + // Dictionary of WHEREn field choices and their CamelCase equivalents. + const wherePascalCase = { + 'FIRST': 'First', + 'LAST': 'Last', + 'FROM_START': 'FromStart', + 'FROM_END': 'FromEnd', + }; + type WhereOption = keyof typeof wherePascalCase; + const list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; + const where1 = block.getFieldValue('WHERE1') as WhereOption; + const where2 = block.getFieldValue('WHERE2') as WhereOption; let code; if (where1 === 'FIRST' && where2 === 'LAST') { code = list + '.slice(0)'; } else if ( - list.match(/^\w+$/) || - (where1 !== 'FROM_END' && where2 === 'FROM_START')) { + list.match(/^\w+$/) || + (where1 !== 'FROM_END' && where2 === 'FROM_START') + ) { // If the list is a variable or doesn't require a call for length, don't // generate a helper function. let at1; @@ -285,8 +329,7 @@ export function lists_getSublist(block, generator) { at1 = generator.getAdjusted(block, 'AT1'); break; case 'FROM_END': - at1 = generator.getAdjusted( - block, 'AT1', 1, false, Order.SUBTRACTION); + at1 = generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); at1 = list + '.length - ' + at1; break; case 'FIRST': @@ -301,8 +344,7 @@ export function lists_getSublist(block, generator) { at2 = generator.getAdjusted(block, 'AT2', 1); break; case 'FROM_END': - at2 = generator.getAdjusted( - block, 'AT2', 0, false, Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); at2 = list + '.length - ' + at2; break; case 'LAST': @@ -315,45 +357,49 @@ export function lists_getSublist(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const wherePascalCase = { - 'FIRST': 'First', - 'LAST': 'Last', - 'FROM_START': 'FromStart', - 'FROM_END': 'FromEnd', - }; // The value for 'FROM_END' and'FROM_START' depends on `at` so // we add it as a parameter. const at1Param = - (where1 === 'FROM_END' || where1 === 'FROM_START') ? ', at1' : ''; + where1 === 'FROM_END' || where1 === 'FROM_START' ? ', at1' : ''; const at2Param = - (where2 === 'FROM_END' || where2 === 'FROM_START') ? ', at2' : ''; + where2 === 'FROM_END' || where2 === 'FROM_START' ? ', at2' : ''; const functionName = generator.provideFunction_( - 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) { + 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], + ` +function ${ + generator.FUNCTION_NAME_PLACEHOLDER_ + }(sequence${at1Param}${at2Param}) { var start = ${getSubstringIndex('sequence', where1, 'at1')}; var end = ${getSubstringIndex('sequence', where2, 'at2')} + 1; return sequence.slice(start, end); } -`); - code = functionName + '(' + list + - // The value for 'FROM_END' and 'FROM_START' depends on `at` so we - // pass it. - ((where1 === 'FROM_END' || where1 === 'FROM_START') ? ', ' + at1 : '') + - ((where2 === 'FROM_END' || where2 === 'FROM_START') ? ', ' + at2 : '') + - ')'; +`, + ); + code = + functionName + + '(' + + list + + // The value for 'FROM_END' and 'FROM_START' depends on `at` so we + // pass it. + (where1 === 'FROM_END' || where1 === 'FROM_START' ? ', ' + at1 : '') + + (where2 === 'FROM_END' || where2 === 'FROM_START' ? ', ' + at2 : '') + + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_sort(block, generator) { +export function lists_sort( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for sorting a list. const list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || - '[]'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || '[]'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; const type = block.getFieldValue('TYPE'); - const getCompareFunctionName = - generator.provideFunction_('listsGetSortCompare', ` + const getCompareFunctionName = generator.provideFunction_( + 'listsGetSortCompare', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { var compareFuncs = { 'NUMERIC': function(a, b) { @@ -366,19 +412,28 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(type, direction) { var compare = compareFuncs[type]; return function(a, b) { return compare(a, b) * direction; }; } - `); + `, + ); return [ - list + '.slice().sort(' + getCompareFunctionName + '("' + type + '", ' + - direction + '))', - Order.FUNCTION_CALL + list + + '.slice().sort(' + + getCompareFunctionName + + '("' + + type + + '", ' + + direction + + '))', + Order.FUNCTION_CALL, ]; -}; +} -export function lists_split(block, generator) { +export function lists_split( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for splitting text into a list, or joining a list into text. let input = generator.valueToCode(block, 'INPUT', Order.MEMBER); - const delimiter = - generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; + const delimiter = generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; const mode = block.getFieldValue('MODE'); let functionName; if (mode === 'SPLIT') { @@ -396,13 +451,15 @@ export function lists_split(block, generator) { } const code = input + '.' + functionName + '(' + delimiter + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_reverse(block, generator) { +export function lists_reverse( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Block for reversing a list. const list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || - '[]'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || '[]'; const code = list + '.slice().reverse()'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/javascript/logic.js b/generators/javascript/logic.js deleted file mode 100644 index 896c8954b..000000000 --- a/generators/javascript/logic.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating JavaScript for logic blocks. - */ - -// Former goog.module ID: Blockly.JavaScript.logic - -import {Order} from './javascript_generator.js'; - - -export function controls_if(block, generator) { - // If/elseif/else condition. - let n = 0; - let code = ''; - if (generator.STATEMENT_PREFIX) { - // Automatic prefix insertion is switched off for this block. Add manually. - code += generator.injectId( - generator.STATEMENT_PREFIX, block); - } - do { - const conditionCode = - generator.valueToCode(block, 'IF' + n, Order.NONE) || - 'false'; - let branchCode = generator.statementToCode(block, 'DO' + n); - if (generator.STATEMENT_SUFFIX) { - branchCode = generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += (n > 0 ? ' else ' : '') + 'if (' + conditionCode + ') {\n' + - branchCode + '}'; - n++; - } while (block.getInput('IF' + n)); - - if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { - let branchCode = generator.statementToCode(block, 'ELSE'); - if (generator.STATEMENT_SUFFIX) { - branchCode = generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += ' else {\n' + branchCode + '}'; - } - return code + '\n'; -}; - -export const controls_ifelse = controls_if; - -export function logic_compare(block, generator) { - // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const order = (operator === '==' || operator === '!=') ? - Order.EQUALITY : - Order.RELATIONAL; - const argument0 = generator.valueToCode(block, 'A', order) || '0'; - const argument1 = generator.valueToCode(block, 'B', order) || '0'; - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_operation(block, generator) { - // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; - const order = (operator === '&&') ? Order.LOGICAL_AND : - Order.LOGICAL_OR; - let argument0 = generator.valueToCode(block, 'A', order); - let argument1 = generator.valueToCode(block, 'B', order); - if (!argument0 && !argument1) { - // If there are no arguments, then the return value is false. - argument0 = 'false'; - argument1 = 'false'; - } else { - // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === '&&') ? 'true' : 'false'; - if (!argument0) { - argument0 = defaultArgument; - } - if (!argument1) { - argument1 = defaultArgument; - } - } - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_negate(block, generator) { - // Negation. - const order = Order.LOGICAL_NOT; - const argument0 = - generator.valueToCode(block, 'BOOL', order) || 'true'; - const code = '!' + argument0; - return [code, order]; -}; - -export function logic_boolean(block, generator) { - // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; - return [code, Order.ATOMIC]; -}; - -export function logic_null(block, generator) { - // Null data type. - return ['null', Order.ATOMIC]; -}; - -export function logic_ternary(block, generator) { - // Ternary operator. - const value_if = - generator.valueToCode(block, 'IF', Order.CONDITIONAL) || - 'false'; - const value_then = - generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || - 'null'; - const value_else = - generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || - 'null'; - const code = value_if + ' ? ' + value_then + ' : ' + value_else; - return [code, Order.CONDITIONAL]; -}; diff --git a/generators/javascript/logic.ts b/generators/javascript/logic.ts new file mode 100644 index 000000000..60174c85a --- /dev/null +++ b/generators/javascript/logic.ts @@ -0,0 +1,153 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating JavaScript for logic blocks. + */ + +// Former goog.module ID: Blockly.JavaScript.logic + +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; +import {Order} from './javascript_generator.js'; + +export function controls_if(block: Block, generator: JavascriptGenerator) { + // If/elseif/else condition. + let n = 0; + let code = ''; + if (generator.STATEMENT_PREFIX) { + // Automatic prefix insertion is switched off for this block. Add manually. + code += generator.injectId(generator.STATEMENT_PREFIX, block); + } + do { + const conditionCode = + generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; + let branchCode = generator.statementToCode(block, 'DO' + n); + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += + (n > 0 ? ' else ' : '') + + 'if (' + + conditionCode + + ') {\n' + + branchCode + + '}'; + n++; + } while (block.getInput('IF' + n)); + + if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { + let branchCode = generator.statementToCode(block, 'ELSE'); + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += ' else {\n' + branchCode + '}'; + } + return code + '\n'; +} + +export const controls_ifelse = controls_if; + +export function logic_compare( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Comparison operator. + const OPERATORS = { + 'EQ': '==', + 'NEQ': '!=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const order = + operator === '==' || operator === '!=' ? Order.EQUALITY : Order.RELATIONAL; + const argument0 = generator.valueToCode(block, 'A', order) || '0'; + const argument1 = generator.valueToCode(block, 'B', order) || '0'; + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_operation( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Operations 'and', 'or'. + const operator = block.getFieldValue('OP') === 'AND' ? '&&' : '||'; + const order = operator === '&&' ? Order.LOGICAL_AND : Order.LOGICAL_OR; + let argument0 = generator.valueToCode(block, 'A', order); + let argument1 = generator.valueToCode(block, 'B', order); + if (!argument0 && !argument1) { + // If there are no arguments, then the return value is false. + argument0 = 'false'; + argument1 = 'false'; + } else { + // Single missing arguments have no effect on the return value. + const defaultArgument = operator === '&&' ? 'true' : 'false'; + if (!argument0) { + argument0 = defaultArgument; + } + if (!argument1) { + argument1 = defaultArgument; + } + } + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_negate( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Negation. + const order = Order.LOGICAL_NOT; + const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; + const code = '!' + argument0; + return [code, order]; +} + +export function logic_boolean( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Boolean values true and false. + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; + return [code, Order.ATOMIC]; +} + +export function logic_null( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Null data type. + return ['null', Order.ATOMIC]; +} + +export function logic_ternary( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Ternary operator. + const value_if = + generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; + const value_then = + generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; + const value_else = + generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; + const code = value_if + ' ? ' + value_then + ' : ' + value_else; + return [code, Order.CONDITIONAL]; +} diff --git a/generators/javascript/loops.js b/generators/javascript/loops.ts similarity index 53% rename from generators/javascript/loops.js rename to generators/javascript/loops.ts index fbe532ce7..555e43ff1 100644 --- a/generators/javascript/loops.js +++ b/generators/javascript/loops.ts @@ -5,17 +5,22 @@ */ /** - * @fileoverview Generating JavaScript for loop blocks. + * @file Generating JavaScript for loop blocks. */ // Former goog.module ID: Blockly.JavaScript.loops import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './javascript_generator.js'; - -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext( + block: Block, + generator: JavascriptGenerator, +) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -23,65 +28,88 @@ export function controls_repeat_ext(block, generator) { repeats = String(Number(block.getFieldValue('TIMES'))); } else { // External number. - repeats = - generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || - '0'; + repeats = generator.valueToCode(block, 'TIMES', Order.ASSIGNMENT) || '0'; } let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; - const loopVar = - generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); let endVar = repeats; if (!repeats.match(/^\w+$/) && !stringUtils.isNumber(repeats)) { - endVar = - generator.nameDB_.getDistinctName( - 'repeat_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + 'repeat_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + repeats + ';\n'; } - code += 'for (var ' + loopVar + ' = 0; ' + loopVar + ' < ' + endVar + '; ' + - loopVar + '++) {\n' + branch + '}\n'; + code += + 'for (var ' + + loopVar + + ' = 0; ' + + loopVar + + ' < ' + + endVar + + '; ' + + loopVar + + '++) {\n' + + branch + + '}\n'; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil( + block: Block, + generator: JavascriptGenerator, +) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = - generator.valueToCode( - block, 'BOOL', - until ? Order.LOGICAL_NOT : Order.NONE) || - 'false'; + generator.valueToCode( + block, + 'BOOL', + until ? Order.LOGICAL_NOT : Order.NONE, + ) || 'false'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); if (until) { argument0 = '!' + argument0; } return 'while (' + argument0 + ') {\n' + branch + '}\n'; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: JavascriptGenerator) { // For loop. - const variable0 = - generator.getVariableName( - block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; - const argument1 = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; - const increment = - generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; + generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code; - if (stringUtils.isNumber(argument0) && stringUtils.isNumber(argument1) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(argument0) && + stringUtils.isNumber(argument1) && + stringUtils.isNumber(increment) + ) { // All arguments are simple numbers. const up = Number(argument0) <= Number(argument1); - code = 'for (' + variable0 + ' = ' + argument0 + '; ' + variable0 + - (up ? ' <= ' : ' >= ') + argument1 + '; ' + variable0; + code = + 'for (' + + variable0 + + ' = ' + + argument0 + + '; ' + + variable0 + + (up ? ' <= ' : ' >= ') + + argument1 + + '; ' + + variable0; const step = Math.abs(Number(increment)); if (step === 1) { code += up ? '++' : '--'; @@ -94,84 +122,117 @@ export function controls_for(block, generator) { // Cache non-trivial values to variables to prevent repeated look-ups. let startVar = argument0; if (!argument0.match(/^\w+$/) && !stringUtils.isNumber(argument0)) { - startVar = generator.nameDB_.getDistinctName( - variable0 + '_start', NameType.VARIABLE); + startVar = generator.nameDB_!.getDistinctName( + variable0 + '_start', + NameType.VARIABLE, + ); code += 'var ' + startVar + ' = ' + argument0 + ';\n'; } let endVar = argument1; if (!argument1.match(/^\w+$/) && !stringUtils.isNumber(argument1)) { - endVar = generator.nameDB_.getDistinctName( - variable0 + '_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + variable0 + '_end', + NameType.VARIABLE, + ); code += 'var ' + endVar + ' = ' + argument1 + ';\n'; } // Determine loop direction at start, in case one of the bounds // changes during loop execution. - const incVar = generator.nameDB_.getDistinctName( - variable0 + '_inc', NameType.VARIABLE); + const incVar = generator.nameDB_!.getDistinctName( + variable0 + '_inc', + NameType.VARIABLE, + ); code += 'var ' + incVar + ' = '; if (stringUtils.isNumber(increment)) { - code += Math.abs(increment) + ';\n'; + code += Math.abs(Number(increment)) + ';\n'; } else { code += 'Math.abs(' + increment + ');\n'; } code += 'if (' + startVar + ' > ' + endVar + ') {\n'; code += generator.INDENT + incVar + ' = -' + incVar + ';\n'; code += '}\n'; - code += 'for (' + variable0 + ' = ' + startVar + '; ' + incVar + - ' >= 0 ? ' + variable0 + ' <= ' + endVar + ' : ' + variable0 + - ' >= ' + endVar + '; ' + variable0 + ' += ' + incVar + ') {\n' + - branch + '}\n'; + code += + 'for (' + + variable0 + + ' = ' + + startVar + + '; ' + + incVar + + ' >= 0 ? ' + + variable0 + + ' <= ' + + endVar + + ' : ' + + variable0 + + ' >= ' + + endVar + + '; ' + + variable0 + + ' += ' + + incVar + + ') {\n' + + branch + + '}\n'; } return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach(block: Block, generator: JavascriptGenerator) { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || - '[]'; + generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; // Cache non-trivial values to variables to prevent repeated look-ups. let listVar = argument0; if (!argument0.match(/^\w+$/)) { - listVar = generator.nameDB_.getDistinctName( - variable0 + '_list', NameType.VARIABLE); + listVar = generator.nameDB_!.getDistinctName( + variable0 + '_list', + NameType.VARIABLE, + ); code += 'var ' + listVar + ' = ' + argument0 + ';\n'; } - const indexVar = generator.nameDB_.getDistinctName( - variable0 + '_index', NameType.VARIABLE); - branch = generator.INDENT + variable0 + ' = ' + listVar + - '[' + indexVar + '];\n' + branch; + const indexVar = generator.nameDB_!.getDistinctName( + variable0 + '_index', + NameType.VARIABLE, + ); + branch = + generator.INDENT + + variable0 + + ' = ' + + listVar + + '[' + + indexVar + + '];\n' + + branch; code += 'for (var ' + indexVar + ' in ' + listVar + ') {\n' + branch + '}\n'; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: JavascriptGenerator, +) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { // Automatic prefix insertion is switched off for this block. Add manually. - xfix += generator.injectId( - generator.STATEMENT_PREFIX, block); + xfix += generator.injectId(generator.STATEMENT_PREFIX, block); } if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the break/continue is triggered. - xfix += generator.injectId( - generator.STATEMENT_SUFFIX, block); + xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. // In the case of 'break', a prefix is needed due to the loop's suffix. - xfix += generator.injectId( - generator.STATEMENT_PREFIX, loop); + xfix += generator.injectId(generator.STATEMENT_PREFIX, loop); } } switch (block.getFieldValue('FLOW')) { @@ -181,4 +242,4 @@ export function controls_flow_statements(block, generator) { return xfix + 'continue;\n'; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/javascript/math.js b/generators/javascript/math.ts similarity index 62% rename from generators/javascript/math.js rename to generators/javascript/math.ts index 95fa32f5a..e8ab2852f 100644 --- a/generators/javascript/math.js +++ b/generators/javascript/math.ts @@ -5,33 +5,39 @@ */ /** - * @fileoverview Generating JavaScript for math blocks. - * @suppress {missingRequire} + * @file Generating JavaScript for math blocks. */ // Former goog.module ID: Blockly.JavaScript.math +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Numeric value. - const code = Number(block.getFieldValue('NUM')); - const order = code >= 0 ? Order.ATOMIC : - Order.UNARY_NEGATION; - return [code, order]; -}; + const number = Number(block.getFieldValue('NUM')); + const order = number >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION; + return [String(number), order]; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITION], 'MINUS': [' - ', Order.SUBTRACTION], 'MULTIPLY': [' * ', Order.MULTIPLICATION], 'DIVIDE': [' / ', Order.DIVISION], - 'POWER': [null, Order.NONE], // Handle power separately. + 'POWER': [null, Order.NONE], // Handle power separately. }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; @@ -44,17 +50,19 @@ export function math_arithmetic(block, generator) { } code = argument0 + operator + argument1; return [code, order]; -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; let arg; if (operator === 'NEG') { // Negation is a special case given its different operator precedence. - arg = generator.valueToCode(block, 'NUM', - Order.UNARY_NEGATION) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.UNARY_NEGATION) || '0'; if (arg[0] === '-') { // --3 is not legal in JS. arg = ' ' + arg; @@ -63,11 +71,9 @@ export function math_single(block, generator) { return [code, Order.UNARY_NEGATION]; } if (operator === 'SIN' || operator === 'COS' || operator === 'TAN') { - arg = generator.valueToCode(block, 'NUM', - Order.DIVISION) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.DIVISION) || '0'; } else { - arg = generator.valueToCode(block, 'NUM', - Order.NONE) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; } // First, handle cases which generate values that don't need parentheses // wrapping the code. @@ -128,11 +134,14 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.DIVISION]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['Math.PI', Order.MEMBER], 'E': ['Math.E', Order.MEMBER], 'GOLDEN_RATIO': ['(1 + Math.sqrt(5)) / 2', Order.DIVISION], @@ -140,33 +149,36 @@ export function math_constant(block, generator) { 'SQRT1_2': ['Math.SQRT1_2', Order.MEMBER], 'INFINITY': ['Infinity', Order.ATOMIC], }; - return CONSTANTS[block.getFieldValue('CONSTANT')]; -}; + type ConstantOption = keyof typeof CONSTANTS; + return CONSTANTS[block.getFieldValue('CONSTANT') as ConstantOption]; +} -export function math_number_property(block, generator) { +export function math_number_property( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Check if a number is even, odd, prime, whole, positive, or negative // or if it is divisible by certain number. Returns true or false. - const PROPERTIES = { + const PROPERTIES: Record = { 'EVEN': [' % 2 === 0', Order.MODULUS, Order.EQUALITY], 'ODD': [' % 2 === 1', Order.MODULUS, Order.EQUALITY], - 'WHOLE': [' % 1 === 0', Order.MODULUS, - Order.EQUALITY], - 'POSITIVE': [' > 0', Order.RELATIONAL, - Order.RELATIONAL], - 'NEGATIVE': [' < 0', Order.RELATIONAL, - Order.RELATIONAL], + 'WHOLE': [' % 1 === 0', Order.MODULUS, Order.EQUALITY], + 'POSITIVE': [' > 0', Order.RELATIONAL, Order.RELATIONAL], + 'NEGATIVE': [' < 0', Order.RELATIONAL, Order.RELATIONAL], 'DIVISIBLE_BY': [null, Order.MODULUS, Order.EQUALITY], 'PRIME': [null, Order.NONE, Order.FUNCTION_CALL], }; - const dropdownProperty = block.getFieldValue('PROPERTY'); + type PropertyOption = keyof typeof PROPERTIES; + const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption; const [suffix, inputOrder, outputOrder] = PROPERTIES[dropdownProperty]; const numberToCheck = - generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || - '0'; + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - const functionName = generator.provideFunction_('mathIsPrime', ` + const functionName = generator.provideFunction_( + 'mathIsPrime', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { // https://en.wikipedia.org/wiki/Primality_test#Naive_methods if (n == 2 || n == 3) { @@ -185,68 +197,81 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) { } return true; } -`); +`, + ); code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MODULUS) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; code = numberToCheck + ' % ' + divisor + ' === 0'; } else { code = numberToCheck + suffix; } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: JavascriptGenerator) { // Add to a variable in place. - const argument0 = generator.valueToCode(block, 'DELTA', - Order.ADDITION) || '0'; + const argument0 = + generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; const varName = generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = (typeof ' + varName + ' === \'number\' ? ' + varName + - ' : 0) + ' + argument0 + ';\n'; -}; + return ( + varName + + ' = (typeof ' + + varName + + " === 'number' ? " + + varName + + ' : 0) + ' + + argument0 + + ';\n' + ); +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); let list; let code; switch (func) { case 'SUM': - list = generator.valueToCode(block, 'LIST', - Order.MEMBER) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; code = list + '.reduce(function(x, y) {return x + y;}, 0)'; break; case 'MIN': - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = 'Math.min.apply(null, ' + list + ')'; break; case 'MAX': - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = 'Math.max.apply(null, ' + list + ')'; break; case 'AVERAGE': { // mathMean([null,null,1,3]) === 2.0. - const functionName = generator.provideFunction_('mathMean', ` + const functionName = generator.provideFunction_( + 'mathMean', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { return myList.reduce(function(x, y) {return x + y;}, 0) / myList.length; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'MEDIAN': { // mathMedian([null,null,1,3]) === 2.0. - const functionName = generator.provideFunction_('mathMedian', ` + const functionName = generator.provideFunction_( + 'mathMedian', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { var localList = myList.filter(function (x) {return typeof x === 'number';}); if (!localList.length) return null; @@ -257,9 +282,9 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { return localList[(localList.length - 1) / 2]; } } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } @@ -267,7 +292,9 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList) { // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1]. - const functionName = generator.provideFunction_('mathModes', ` + const functionName = generator.provideFunction_( + 'mathModes', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(values) { var modes = []; var counts = []; @@ -296,15 +323,16 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(values) { } return modes; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'STD_DEV': { - const functionName = - generator.provideFunction_('mathStandardDeviation', ` + const functionName = generator.provideFunction_( + 'mathStandardDeviation', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers) { var n = numbers.length; if (!n) return null; @@ -316,22 +344,23 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers) { variance = variance / n; return Math.sqrt(variance); } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'RANDOM': { - const functionName = - generator.provideFunction_('mathRandomList', ` + const functionName = generator.provideFunction_( + 'mathRandomList', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list) { var x = Math.floor(Math.random() * list.length); return list[x]; } -`); - list = generator.valueToCode(block, 'LIST', - Order.NONE) || '[]'; +`, + ); + list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } @@ -339,38 +368,51 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list) { throw Error('Unknown operator: ' + func); } return [code, Order.FUNCTION_CALL]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Remainder computation. - const argument0 = generator.valueToCode(block, 'DIVIDEND', - Order.MODULUS) || '0'; - const argument1 = generator.valueToCode(block, 'DIVISOR', - Order.MODULUS) || '0'; + const argument0 = + generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; + const argument1 = + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MODULUS]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Constrain a number between two limits. - const argument0 = generator.valueToCode(block, 'VALUE', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'LOW', - Order.NONE) || '0'; - const argument2 = generator.valueToCode(block, 'HIGH', - Order.NONE) || 'Infinity'; - const code = 'Math.min(Math.max(' + argument0 + ', ' + argument1 + '), ' + - argument2 + ')'; + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; + const argument2 = + generator.valueToCode(block, 'HIGH', Order.NONE) || 'Infinity'; + const code = + 'Math.min(Math.max(' + + argument0 + + ', ' + + argument1 + + '), ' + + argument2 + + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Random integer between [X] and [Y]. - const argument0 = generator.valueToCode(block, 'FROM', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'TO', - Order.NONE) || '0'; - const functionName = generator.provideFunction_('mathRandomInt', ` + const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; + const functionName = generator.provideFunction_( + 'mathRandomInt', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(a, b) { if (a > b) { // Swap a and b to ensure a is smaller. @@ -380,22 +422,29 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(a, b) { } return Math.floor(Math.random() * (b - a + 1) + a); } -`); +`, + ); const code = functionName + '(' + argument0 + ', ' + argument1 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Random fraction between 0 and 1. return ['Math.random()', Order.FUNCTION_CALL]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. - const argument0 = generator.valueToCode(block, 'X', - Order.NONE) || '0'; - const argument1 = generator.valueToCode(block, 'Y', - Order.NONE) || '0'; - return ['Math.atan2(' + argument1 + ', ' + argument0 + ') / Math.PI * 180', - Order.DIVISION]; -}; + const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; + return [ + 'Math.atan2(' + argument1 + ', ' + argument0 + ') / Math.PI * 180', + Order.DIVISION, + ]; +} diff --git a/generators/javascript/procedures.js b/generators/javascript/procedures.ts similarity index 57% rename from generators/javascript/procedures.js rename to generators/javascript/procedures.ts index 7f975289c..a835271e7 100644 --- a/generators/javascript/procedures.js +++ b/generators/javascript/procedures.ts @@ -5,25 +5,28 @@ */ /** - * @fileoverview Generating JavaScript for procedure blocks. + * @file Generating JavaScript for procedure blocks. */ // Former goog.module ID: Blockly.JavaScript.procedures +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; import {Order} from './javascript_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn( + block: Block, + generator: JavascriptGenerator, +) { // Define a procedure with a return value. const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { - xfix1 += generator.injectId( - generator.STATEMENT_PREFIX, block); + xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); } if (generator.STATEMENT_SUFFIX) { - xfix1 += generator.injectId( - generator.STATEMENT_SUFFIX, block); + xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (xfix1) { xfix1 = generator.prefixLines(xfix1, generator.INDENT); @@ -31,13 +34,12 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId( - generator.INFINITE_LOOP_TRAP, block), - generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } const branch = generator.statementToCode(block, 'STACK'); - let returnValue = - generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. @@ -49,63 +51,83 @@ export function procedures_defreturn(block, generator) { const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { - args[i] = - generator.getVariableName(variables[i]); + args[i] = generator.getVariableName(variables[i]); } - let code = 'function ' + funcName + '(' + args.join(', ') + ') {\n' + xfix1 + - loopTrap + branch + xfix2 + returnValue + '}'; + let code = + 'function ' + + funcName + + '(' + + args.join(', ') + + ') {\n' + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue + + '}'; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Call a procedure with a return value. const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { - args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || - 'null'; + args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'null'; } const code = funcName + '(' + args.join(', ') + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn( + block: Block, + generator: JavascriptGenerator, +) { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn']( + block, + generator, + ) as [string, Order]; return tuple[0] + ';\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn( + block: Block, + generator: JavascriptGenerator, +) { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || - 'false'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; let code = 'if (' + condition + ') {\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. code += generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT); + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { - const value = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; + if ((block as IfReturnBlock).hasReturnValue_) { + const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; code += generator.INDENT + 'return ' + value + ';\n'; } else { code += generator.INDENT + 'return;\n'; } code += '}\n'; return code; -}; +} diff --git a/generators/javascript/text.js b/generators/javascript/text.ts similarity index 51% rename from generators/javascript/text.js rename to generators/javascript/text.ts index 32125051f..1681979ec 100644 --- a/generators/javascript/text.js +++ b/generators/javascript/text.ts @@ -5,14 +5,16 @@ */ /** - * @fileoverview Generating JavaScript for text blocks. + * @file Generating JavaScript for text blocks. */ // Former goog.module ID: Blockly.JavaScript.texts +import type {Block} from '../../core/block.js'; +import type {JavascriptGenerator} from './javascript_generator.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; import {Order} from './javascript_generator.js'; - /** * Regular expression to detect a single-quoted string literal. */ @@ -21,11 +23,11 @@ const strRegExp = /^\s*'([^']|\\')*'\s*$/; /** * Enclose the provided value in 'String(...)' function. * Leave string literals alone. - * @param {string} value Code evaluating to a value. - * @return {Array} Array containing code evaluating to a string + * @param value Code evaluating to a value. + * @returns Array containing code evaluating to a string * and the order of the returned code.[string, number] */ -const forceString = function(value) { +const forceString = function (value: string): [string, Order] { if (strRegExp.test(value)) { return [value, Order.ATOMIC]; } @@ -34,12 +36,16 @@ const forceString = function(value) { /** * Returns an expression calculating the index into a string. - * @param {string} stringName Name of the string, used to calculate length. - * @param {string} where The method of indexing, selected by dropdown in Blockly - * @param {string=} opt_at The optional offset when indexing from start/end. - * @return {string|undefined} Index expression. + * @param stringName Name of the string, used to calculate length. + * @param where The method of indexing, selected by dropdown in Blockly + * @param opt_at The optional offset when indexing from start/end. + * @returns Index expression. */ -const getSubstringIndex = function(stringName, where, opt_at) { +const getSubstringIndex = function ( + stringName: string, + where: string, + opt_at?: string, +): string | undefined { if (where === 'FIRST') { return '0'; } else if (where === 'FROM_END') { @@ -51,101 +57,112 @@ const getSubstringIndex = function(stringName, where, opt_at) { } }; -export function text(block, generator) { +export function text( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Text value. - const code = - generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = code.indexOf('+') !== -1 ? Order.ADDITION : - Order.ATOMIC; + const code = generator.multiline_quote_(block.getFieldValue('TEXT')); + const order = code.indexOf('+') !== -1 ? Order.ADDITION : Order.ATOMIC; return [code, order]; -}; +} -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Create a string made up of any number of elements of any type. - switch (block.itemCount_) { + const joinBlock = block as JoinMutatorBlock; + switch (joinBlock.itemCount_) { case 0: return ["''", Order.ATOMIC]; case 1: { - const element = generator.valueToCode(block, 'ADD0', - Order.NONE) || "''"; + const element = + generator.valueToCode(joinBlock, 'ADD0', Order.NONE) || "''"; const codeAndOrder = forceString(element); return codeAndOrder; } case 2: { - const element0 = generator.valueToCode(block, 'ADD0', - Order.NONE) || "''"; - const element1 = generator.valueToCode(block, 'ADD1', - Order.NONE) || "''"; - const code = forceString(element0)[0] + - ' + ' + forceString(element1)[0]; + const element0 = + generator.valueToCode(joinBlock, 'ADD0', Order.NONE) || "''"; + const element1 = + generator.valueToCode(joinBlock, 'ADD1', Order.NONE) || "''"; + const code = forceString(element0)[0] + ' + ' + forceString(element1)[0]; return [code, Order.ADDITION]; } default: { - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = generator.valueToCode(block, 'ADD' + i, - Order.NONE) || "''"; + const elements = new Array(joinBlock.itemCount_); + for (let i = 0; i < joinBlock.itemCount_; i++) { + elements[i] = + generator.valueToCode(joinBlock, 'ADD' + i, Order.NONE) || "''"; } - const code = '[' + elements.join(',') + '].join(\'\')'; + const code = '[' + elements.join(',') + "].join('')"; return [code, Order.FUNCTION_CALL]; } } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: JavascriptGenerator) { // Append to a variable in place. const varName = generator.getVariableName(block.getFieldValue('VAR')); - const value = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const code = varName + ' += ' + - forceString(value)[0] + ';\n'; + const value = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const code = varName + ' += ' + forceString(value)[0] + ';\n'; return code; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // String or array length. - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return [text + '.length', Order.MEMBER]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Is the string null or array empty? - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; return ['!' + text + '.length', Order.LOGICAL_NOT]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Search the text for a substring. - const operator = block.getFieldValue('END') === 'FIRST' ? - 'indexOf' : 'lastIndexOf'; - const substring = generator.valueToCode(block, 'FIND', - Order.NONE) || "''"; - const text = generator.valueToCode(block, 'VALUE', - Order.MEMBER) || "''"; + const operator = + block.getFieldValue('END') === 'FIRST' ? 'indexOf' : 'lastIndexOf'; + const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; const code = text + '.' + operator + '(' + substring + ')'; // Adjust index if using one-based indices. if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITION]; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const textOrder = (where === 'RANDOM') ? Order.NONE : - Order.MEMBER; - const text = - generator.valueToCode(block, 'VALUE', textOrder) || "''"; + const textOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; + const text = generator.valueToCode(block, 'VALUE', textOrder) || "''"; switch (where) { case 'FIRST': { const code = text + '.charAt(0)'; @@ -167,30 +184,44 @@ export function text_charAt(block, generator) { return [code, Order.FUNCTION_CALL]; } case 'RANDOM': { - const functionName = - generator.provideFunction_('textRandomLetter', ` + const functionName = generator.provideFunction_( + 'textRandomLetter', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text) { var x = Math.floor(Math.random() * text.length); return text[x]; } -`); +`, + ); const code = functionName + '(' + text + ')'; return [code, Order.FUNCTION_CALL]; } } throw Error('Unhandled option (text_charAt).'); -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + // Dictionary of WHEREn field choices and their CamelCase equivalents. */ + const wherePascalCase = { + 'FIRST': 'First', + 'LAST': 'Last', + 'FROM_START': 'FromStart', + 'FROM_END': 'FromEnd', + }; + type WhereOption = keyof typeof wherePascalCase; // Get substring. - const where1 = block.getFieldValue('WHERE1'); - const where2 = block.getFieldValue('WHERE2'); - const requiresLengthCall = (where1 !== 'FROM_END' && where1 !== 'LAST' && - where2 !== 'FROM_END' && where2 !== 'LAST'); - const textOrder = requiresLengthCall ? Order.MEMBER : - Order.NONE; - const text = - generator.valueToCode(block, 'STRING', textOrder) || "''"; + const where1 = block.getFieldValue('WHERE1') as WhereOption; + const where2 = block.getFieldValue('WHERE2') as WhereOption; + const requiresLengthCall = + where1 !== 'FROM_END' && + where1 !== 'LAST' && + where2 !== 'FROM_END' && + where2 !== 'LAST'; + const textOrder = requiresLengthCall ? Order.MEMBER : Order.NONE; + const text = generator.valueToCode(block, 'STRING', textOrder) || "''"; let code; if (where1 === 'FIRST' && where2 === 'LAST') { code = text; @@ -204,8 +235,7 @@ export function text_getSubstring(block, generator) { at1 = generator.getAdjusted(block, 'AT1'); break; case 'FROM_END': - at1 = generator.getAdjusted(block, 'AT1', 1, false, - Order.SUBTRACTION); + at1 = generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); at1 = text + '.length - ' + at1; break; case 'FIRST': @@ -220,8 +250,7 @@ export function text_getSubstring(block, generator) { at2 = generator.getAdjusted(block, 'AT2', 1); break; case 'FROM_END': - at2 = generator.getAdjusted(block, 'AT2', 0, false, - Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); at2 = text + '.length - ' + at2; break; case 'LAST': @@ -234,82 +263,97 @@ export function text_getSubstring(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const wherePascalCase = {'FIRST': 'First', 'LAST': 'Last', - 'FROM_START': 'FromStart', 'FROM_END': 'FromEnd'}; // The value for 'FROM_END' and'FROM_START' depends on `at` so // we add it as a parameter. const at1Param = - (where1 === 'FROM_END' || where1 === 'FROM_START') ? ', at1' : ''; + where1 === 'FROM_END' || where1 === 'FROM_START' ? ', at1' : ''; const at2Param = - (where2 === 'FROM_END' || where2 === 'FROM_START') ? ', at2' : ''; + where2 === 'FROM_END' || where2 === 'FROM_START' ? ', at2' : ''; const functionName = generator.provideFunction_( - 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], ` -function ${generator.FUNCTION_NAME_PLACEHOLDER_}(sequence${at1Param}${at2Param}) { + 'subsequence' + wherePascalCase[where1] + wherePascalCase[where2], + ` +function ${ + generator.FUNCTION_NAME_PLACEHOLDER_ + }(sequence${at1Param}${at2Param}) { var start = ${getSubstringIndex('sequence', where1, 'at1')}; var end = ${getSubstringIndex('sequence', where2, 'at2')} + 1; return sequence.slice(start, end); } -`); - code = functionName + '(' + text + - // The value for 'FROM_END' and 'FROM_START' depends on `at` so we - // pass it. - ((where1 === 'FROM_END' || where1 === 'FROM_START') ? ', ' + at1 : '') + - ((where2 === 'FROM_END' || where2 === 'FROM_START') ? ', ' + at2 : '') + - ')'; +`, + ); + code = + functionName + + '(' + + text + + // The value for 'FROM_END' and 'FROM_START' depends on `at` so we + // pass it. + (where1 === 'FROM_END' || where1 === 'FROM_START' ? ', ' + at1 : '') + + (where2 === 'FROM_END' || where2 === 'FROM_START' ? ', ' + at2 : '') + + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.toUpperCase()', 'LOWERCASE': '.toLowerCase()', 'TITLECASE': null, }; - const operator = OPERATORS[block.getFieldValue('CASE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('CASE') as OperatorOption]; const textOrder = operator ? Order.MEMBER : Order.NONE; - const text = - generator.valueToCode(block, 'TEXT', textOrder) || "''"; + const text = generator.valueToCode(block, 'TEXT', textOrder) || "''"; let code; if (operator) { // Upper and lower case are functions built into generator. code = text + operator; } else { // Title case is not a native JavaScript function. Define one. - const functionName = - generator.provideFunction_('textToTitleCase', ` + const functionName = generator.provideFunction_( + 'textToTitleCase', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) { return str.replace(/\\S+/g, function(txt) {return txt[0].toUpperCase() + txt.substring(1).toLowerCase();}); } -`); +`, + ); code = functionName + '(' + text + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = { 'LEFT': ".replace(/^[\\s\\xa0]+/, '')", 'RIGHT': ".replace(/[\\s\\xa0]+$/, '')", 'BOTH': '.trim()', }; - const operator = OPERATORS[block.getFieldValue('MODE')]; - const text = generator.valueToCode(block, 'TEXT', - Order.MEMBER) || "''"; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; + const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; return [text + operator, Order.FUNCTION_CALL]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: JavascriptGenerator) { // Print statement. - const msg = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; + const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'window.alert(' + msg + ');\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -325,16 +369,19 @@ export function text_prompt_ext(block, generator) { code = 'Number(' + code + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { - const text = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const sub = generator.valueToCode(block, 'SUB', - Order.NONE) || "''"; - const functionName = generator.provideFunction_('textCount', ` +export function text_count( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; + const functionName = generator.provideFunction_( + 'textCount', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) { if (needle.length === 0) { return haystack.length + 1; @@ -342,33 +389,40 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) { return haystack.split(needle).length - 1; } } -`); +`, + ); const code = functionName + '(' + text + ', ' + sub + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_replace(block, generator) { - const text = generator.valueToCode(block, 'TEXT', - Order.NONE) || "''"; - const from = generator.valueToCode(block, 'FROM', - Order.NONE) || "''"; +export function text_replace( + block: Block, + generator: JavascriptGenerator, +): [string, Order] { + const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; + const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; // The regex escaping code below is taken from the implementation of // goog.string.regExpEscape. - const functionName = generator.provideFunction_('textReplace', ` + const functionName = generator.provideFunction_( + 'textReplace', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) { needle = needle.replace(/([-()\\[\\]{}+?*.$\\^|,:# 0 ? 'else' : '') + 'if ' + conditionCode + ' then\n' + branchCode; + (n > 0 ? 'else' : '') + 'if ' + conditionCode + ' then\n' + branchCode; n++; } while (block.getInput('IF' + n)); @@ -39,36 +42,46 @@ export function controls_if(block, generator) { let branchCode = generator.statementToCode(block, 'ELSE'); if (generator.STATEMENT_SUFFIX) { branchCode = - generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; } code += 'else\n' + branchCode; } return code + 'end\n'; -}; +} export const controls_ifelse = controls_if; -export function logic_compare(block, generator) { +export function logic_compare( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '~=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const argument0 = - generator.valueToCode(block, 'A', Order.RELATIONAL) || '0'; - const argument1 = - generator.valueToCode(block, 'B', Order.RELATIONAL) || '0'; + const OPERATORS = { + 'EQ': '==', + 'NEQ': '~=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const argument0 = generator.valueToCode(block, 'A', Order.RELATIONAL) || '0'; + const argument1 = generator.valueToCode(block, 'B', Order.RELATIONAL) || '0'; const code = argument0 + ' ' + operator + ' ' + argument1; return [code, Order.RELATIONAL]; -}; +} -export function logic_operation(block, generator) { +export function logic_operation( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? 'and' : 'or'; - const order = (operator === 'and') ? Order.AND : Order.OR; + const operator = block.getFieldValue('OP') === 'AND' ? 'and' : 'or'; + const order = operator === 'and' ? Order.AND : Order.OR; let argument0 = generator.valueToCode(block, 'A', order); let argument1 = generator.valueToCode(block, 'B', order); if (!argument0 && !argument1) { @@ -77,7 +90,7 @@ export function logic_operation(block, generator) { argument1 = 'false'; } else { // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === 'and') ? 'true' : 'false'; + const defaultArgument = operator === 'and' ? 'true' : 'false'; if (!argument0) { argument0 = defaultArgument; } @@ -87,33 +100,43 @@ export function logic_operation(block, generator) { } const code = argument0 + ' ' + operator + ' ' + argument1; return [code, order]; -}; +} -export function logic_negate(block, generator) { +export function logic_negate( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Negation. - const argument0 = - generator.valueToCode(block, 'BOOL', Order.UNARY) || 'true'; + const argument0 = generator.valueToCode(block, 'BOOL', Order.UNARY) || 'true'; const code = 'not ' + argument0; return [code, Order.UNARY]; -}; +} -export function logic_boolean(block, generator) { +export function logic_boolean( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; return [code, Order.ATOMIC]; -}; +} -export function logic_null(block, generator) { +export function logic_null( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Null data type. return ['nil', Order.ATOMIC]; -}; +} -export function logic_ternary(block, generator) { +export function logic_ternary( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Ternary operator. const value_if = generator.valueToCode(block, 'IF', Order.AND) || 'false'; - const value_then = - generator.valueToCode(block, 'THEN', Order.AND) || 'nil'; + const value_then = generator.valueToCode(block, 'THEN', Order.AND) || 'nil'; const value_else = generator.valueToCode(block, 'ELSE', Order.OR) || 'nil'; const code = value_if + ' and ' + value_then + ' or ' + value_else; return [code, Order.OR]; -}; +} diff --git a/generators/lua/loops.js b/generators/lua/loops.ts similarity index 70% rename from generators/lua/loops.js rename to generators/lua/loops.ts index b7f9311c7..fec175b48 100644 --- a/generators/lua/loops.js +++ b/generators/lua/loops.ts @@ -5,21 +5,22 @@ */ /** - * @fileoverview Generating Lua for loop blocks. + * @file Generating Lua for loop blocks. */ // Former goog.module ID: Blockly.Lua.loops import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; +import type {LuaGenerator} from './lua_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './lua_generator.js'; - /** * This is the text used to implement a
continue
. * It is also used to recognise
continue
s in generated code so that * the appropriate label can be put at the end of the loop body. - * @const {string} */ const CONTINUE_STATEMENT = 'goto continue\n'; @@ -29,20 +30,23 @@ const CONTINUE_STATEMENT = 'goto continue\n'; * in all outer loops, but this is safer than duplicating the logic of * blockToCode. * - * @param {string} branch Generated code of the loop body - * @param {string} indent Whitespace by which to indent a continue statement. - * @return {string} Generated label or '' if unnecessary + * @param branch Generated code of the loop body + * @param indent Whitespace by which to indent a continue statement. + * @returns Generated label or '' if unnecessary */ -function addContinueLabel(branch, indent) { +function addContinueLabel(branch: string, indent: string): string { if (branch.indexOf(CONTINUE_STATEMENT) !== -1) { // False positives are possible (e.g. a string literal), but are harmless. return branch + indent + '::continue::\n'; } else { return branch; } -}; +} -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext( + block: Block, + generator: LuaGenerator, +): string { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -60,21 +64,26 @@ export function controls_repeat_ext(block, generator) { let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); branch = addContinueLabel(branch, generator.INDENT); - const loopVar = generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); const code = - 'for ' + loopVar + ' = 1, ' + repeats + ' do\n' + branch + 'end\n'; + 'for ' + loopVar + ' = 1, ' + repeats + ' do\n' + branch + 'end\n'; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil( + block: Block, + generator: LuaGenerator, +): string { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = - generator.valueToCode( - block, 'BOOL', until ? Order.UNARY : Order.NONE) || - 'false'; + generator.valueToCode(block, 'BOOL', until ? Order.UNARY : Order.NONE) || + 'false'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); branch = addContinueLabel(branch, generator.INDENT); @@ -82,12 +91,11 @@ export function controls_whileUntil(block, generator) { argument0 = 'not ' + argument0; } return 'while ' + argument0 + ' do\n' + branch + 'end\n'; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: LuaGenerator): string { // For loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const startVar = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const endVar = generator.valueToCode(block, 'TO', Order.NONE) || '0'; const increment = generator.valueToCode(block, 'BY', Order.NONE) || '1'; @@ -96,8 +104,11 @@ export function controls_for(block, generator) { branch = addContinueLabel(branch, generator.INDENT); let code = ''; let incValue; - if (stringUtils.isNumber(startVar) && stringUtils.isNumber(endVar) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(startVar) && + stringUtils.isNumber(endVar) && + stringUtils.isNumber(increment) + ) { // All arguments are simple numbers. const up = Number(startVar) <= Number(endVar); const step = Math.abs(Number(increment)); @@ -106,12 +117,13 @@ export function controls_for(block, generator) { code = ''; // Determine loop direction at start, in case one of the bounds // changes during loop execution. - incValue = - generator.nameDB_.getDistinctName( - variable0 + '_inc', NameType.VARIABLE); + incValue = generator.nameDB_!.getDistinctName( + variable0 + '_inc', + NameType.VARIABLE, + ); code += incValue + ' = '; if (stringUtils.isNumber(increment)) { - code += Math.abs(increment) + '\n'; + code += Math.abs(increment as unknown as number) + '\n'; } else { code += 'math.abs(' + increment + ')\n'; } @@ -120,25 +132,36 @@ export function controls_for(block, generator) { code += 'end\n'; } code += - 'for ' + variable0 + ' = ' + startVar + ', ' + endVar + ', ' + incValue; + 'for ' + variable0 + ' = ' + startVar + ', ' + endVar + ', ' + incValue; code += ' do\n' + branch + 'end\n'; return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach( + block: Block, + generator: LuaGenerator, +): string { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); branch = addContinueLabel(branch, generator.INDENT); - const code = 'for _, ' + variable0 + ' in ipairs(' + argument0 + ') do \n' + - branch + 'end\n'; + const code = + 'for _, ' + + variable0 + + ' in ipairs(' + + argument0 + + ') do \n' + + branch + + 'end\n'; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: LuaGenerator, +): string { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { @@ -151,7 +174,7 @@ export function controls_flow_statements(block, generator) { xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. @@ -166,4 +189,4 @@ export function controls_flow_statements(block, generator) { return xfix + CONTINUE_STATEMENT; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/lua/lua_generator.js b/generators/lua/lua_generator.ts similarity index 50% rename from generators/lua/lua_generator.js rename to generators/lua/lua_generator.ts index ea8caab87..76517c77d 100644 --- a/generators/lua/lua_generator.js +++ b/generators/lua/lua_generator.ts @@ -5,40 +5,40 @@ */ /** - * @fileoverview Helper functions for generating Lua for blocks. + * @file Lua code generator class, including helper methods for + * generating Lua for blocks. + * * Based on Ellen Spertus's blocky-lua project. - * @suppress {checkTypes|globalThis} */ // Former goog.module ID: Blockly.Lua import * as stringUtils from '../../core/utils/string.js'; -// import type {Block} from '../../core/block.js'; +import type {Block} from '../../core/block.js'; import {CodeGenerator} from '../../core/generator.js'; import {Names} from '../../core/names.js'; -// import type {Workspace} from '../../core/workspace.js'; +import type {Workspace} from '../../core/workspace.js'; import {inputTypes} from '../../core/inputs/input_types.js'; - /** * Order of operation ENUMs. * http://www.lua.org/manual/5.3/manual.html#3.4.8 - * @enum {number} */ -export const Order = { - ATOMIC: 0, // literals +// prettier-ignore +export enum 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, -}; + 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. @@ -48,8 +48,8 @@ export const Order = { * option used for lists and text. */ export class LuaGenerator extends CodeGenerator { - constructor(name) { - super(name ?? 'Lua'); + constructor(name = 'Lua') { + super(name); this.isInitialized = false; // Copy Order values onto instance for backwards compatibility @@ -60,7 +60,16 @@ export class LuaGenerator extends CodeGenerator { // replace data properties with get accessors that call // deprecate.warn().) for (const key in Order) { - this['ORDER_' + key] = Order[key]; + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; } // List of illegal variable names. This is not intended to be a @@ -70,38 +79,39 @@ export class LuaGenerator extends CodeGenerator { 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' + // 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. + * + * @param workspace Workspace to generate code from. */ - init(workspace) { + init(workspace: Workspace) { // Call Blockly.CodeGenerator's init. - super.init(); + super.init(workspace); if (!this.nameDB_) { this.nameDB_ = new Names(this.RESERVED_WORDS_); @@ -113,73 +123,77 @@ export class LuaGenerator extends CodeGenerator { this.nameDB_.populateProcedures(workspace); this.isInitialized = true; - }; + } /** * Prepend the generated code with the variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. + * + * @param code Generated code. + * @returns Completed code. */ - finish(code) { + finish(code: string): string { // Convert the definitions dictionary into a list. const definitions = Object.values(this.definitions_); // Call Blockly.CodeGenerator's finish. code = super.finish(code); this.isInitialized = false; - this.nameDB_.reset(); + this.nameDB_!.reset(); return definitions.join('\n\n') + '\n\n\n' + code; - }; + } /** * Naked values are top-level blocks with outputs that aren't plugged into * anything. 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. + * + * @param line Line of generated code. + * @return Legal line of code. */ - scrubNakedValue(line) { + scrubNakedValue(line: string): string { 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. + * + * @param string Text to encode. + * @returns Lua string. */ - quote_(string) { - string = string.replace(/\\/g, '\\\\') - .replace(/\n/g, '\\\n') - .replace(/'/g, '\\\''); - return '\'' + string + '\''; - }; + quote_(string: string): 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. + * + * @param string Text to encode. + * @returns Lua string. */ - multiline_quote_(string) { + multiline_quote_(string: string): string { const lines = string.split(/\n/g).map(this.quote_); // Join with the following, plus a newline: // .. '\n' .. - return lines.join(' .. \'\\n\' ..\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 + * @param block The current block. + * @param code The Lua code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns Lua code with comments and subsequent blocks added. */ - scrub_(block, code, opt_thisOnly) { + scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; // Only collect comments for blocks that aren't inline. if (!block.outputConnection || !block.outputConnection.targetConnection) { @@ -193,7 +207,7 @@ export class LuaGenerator extends CodeGenerator { // Don't collect comments for nested statements. for (let i = 0; i < block.inputList.length; i++) { if (block.inputList[i].type === inputTypes.VALUE) { - const childBlock = block.inputList[i].connection.targetBlock(); + const childBlock = block.inputList[i].connection!.targetBlock(); if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { @@ -203,8 +217,9 @@ export class LuaGenerator extends CodeGenerator { } } } - const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + const nextBlock = + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = thisOnly ? '' : this.blockToCode(nextBlock); return commentCode + code + nextCode; - }; + } } diff --git a/generators/lua/math.js b/generators/lua/math.ts similarity index 75% rename from generators/lua/math.js rename to generators/lua/math.ts index af1ff738f..8cf87cc6b 100644 --- a/generators/lua/math.js +++ b/generators/lua/math.ts @@ -5,40 +5,51 @@ */ /** - * @fileoverview Generating Lua for math blocks. + * @file Generating Lua for math blocks. */ // Former goog.module ID: Blockly.Lua.math +import type {Block} from '../../core/block.js'; +import type {LuaGenerator} from './lua_generator.js'; import {Order} from './lua_generator.js'; - -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Numeric value. const code = Number(block.getFieldValue('NUM')); const order = code < 0 ? Order.UNARY : Order.ATOMIC; - return [code, order]; -}; + return [String(code), order]; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITIVE], 'MINUS': [' - ', Order.ADDITIVE], 'MULTIPLY': [' * ', Order.MULTIPLICATIVE], 'DIVIDE': [' / ', Order.MULTIPLICATIVE], 'POWER': [' ^ ', Order.EXPONENTIATION], }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; const argument1 = generator.valueToCode(block, 'B', order) || '0'; const code = argument0 + operator + argument1; return [code, order]; -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let arg; @@ -106,11 +117,14 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.HIGH]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['math.pi', Order.HIGH], 'E': ['math.exp(1)', Order.HIGH], 'GOLDEN_RATIO': ['(1 + math.sqrt(5)) / 2', Order.MULTIPLICATIVE], @@ -119,12 +133,15 @@ export function math_constant(block, generator) { 'INFINITY': ['math.huge', Order.HIGH], }; return CONSTANTS[block.getFieldValue('CONSTANT')]; -}; +} -export function math_number_property(block, generator) { +export function math_number_property( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Check if a number is even, odd, prime, whole, positive, or negative // or if it is divisible by certain number. Returns true or false. - const PROPERTIES = { + const PROPERTIES: Record = { 'EVEN': [' % 2 == 0', Order.MULTIPLICATIVE, Order.RELATIONAL], 'ODD': [' % 2 == 1', Order.MULTIPLICATIVE, Order.RELATIONAL], 'WHOLE': [' % 1 == 0', Order.MULTIPLICATIVE, Order.RELATIONAL], @@ -135,12 +152,14 @@ export function math_number_property(block, generator) { }; const dropdownProperty = block.getFieldValue('PROPERTY'); const [suffix, inputOrder, outputOrder] = PROPERTIES[dropdownProperty]; - const numberToCheck = generator.valueToCode(block, 'NUMBER_TO_CHECK', - inputOrder) || '0'; + const numberToCheck = + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - const functionName = generator.provideFunction_('math_isPrime', ` + const functionName = generator.provideFunction_( + 'math_isPrime', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) -- https://en.wikipedia.org/wiki/Primality_test#Naive_methods if n == 2 or n == 3 then @@ -159,11 +178,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(n) end return true end -`); +`, + ); code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MULTIPLICATIVE) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; // If 'divisor' is some code that evals to 0, generator will produce a nan. // Let's produce nil if we can determine this at compile-time. if (divisor === '0') { @@ -177,23 +197,25 @@ end code = numberToCheck + suffix; } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: LuaGenerator): string { // Add to a variable in place. const argument0 = - generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); + generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); return varName + ' = ' + varName + ' + ' + argument0 + '\n'; -}; +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}'; @@ -201,7 +223,9 @@ export function math_on_list(block, generator) { // Functions needed in more than one case. function provideSum() { - return generator.provideFunction_('math_sum', ` + return generator.provideFunction_( + 'math_sum', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) local result = 0 for _, v in ipairs(t) do @@ -209,7 +233,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) end return result end -`); +`, + ); } switch (func) { @@ -219,7 +244,9 @@ end case 'MIN': // Returns 0 for the empty list. - functionName = generator.provideFunction_('math_min', ` + functionName = generator.provideFunction_( + 'math_min', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) if #t == 0 then return 0 @@ -232,24 +259,30 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) end return result end -`); +`, + ); break; case 'AVERAGE': // Returns 0 for the empty list. - functionName = generator.provideFunction_('math_average', ` + functionName = generator.provideFunction_( + 'math_average', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) if #t == 0 then return 0 end return ${provideSum()}(t) / #t end -`); +`, + ); break; case 'MAX': // Returns 0 for the empty list. - functionName = generator.provideFunction_('math_max', ` + functionName = generator.provideFunction_( + 'math_max', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) if #t == 0 then return 0 @@ -262,12 +295,15 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) end return result end -`); +`, + ); break; case 'MEDIAN': // This operation excludes non-numbers. - functionName = generator.provideFunction_('math_median', ` + functionName = generator.provideFunction_( + 'math_median', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) -- Source: http://lua-users.org/wiki/SimpleStats if #t == 0 then @@ -286,14 +322,17 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) return temp[math.ceil(#temp / 2)] end end -`); +`, + ); break; case 'MODE': // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // The generator version includes non-numbers. - functionName = generator.provideFunction_('math_modes', ` + functionName = generator.provideFunction_( + 'math_modes', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) -- Source: http://lua-users.org/wiki/SimpleStats local counts = {} @@ -318,11 +357,14 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) end return temp end -`); +`, + ); break; case 'STD_DEV': - functionName = generator.provideFunction_('math_standard_deviation', ` + functionName = generator.provideFunction_( + 'math_standard_deviation', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) local m local vm @@ -340,66 +382,92 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) result = math.sqrt(total / (count-1)) return result end -`); +`, + ); break; case 'RANDOM': - functionName = generator.provideFunction_('math_random_list', ` + functionName = generator.provideFunction_( + 'math_random_list', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t) if #t == 0 then return nil end return t[math.random(#t)] end -`); +`, + ); break; default: throw Error('Unknown operator: ' + func); } return [functionName + '(' + list + ')', Order.HIGH]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Remainder computation. const argument0 = - generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; + generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; const argument1 = - generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MULTIPLICATIVE]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const argument1 = - generator.valueToCode(block, 'LOW', Order.NONE) || '-math.huge'; + generator.valueToCode(block, 'LOW', Order.NONE) || '-math.huge'; const argument2 = - generator.valueToCode(block, 'HIGH', Order.NONE) || 'math.huge'; - const code = 'math.min(math.max(' + argument0 + ', ' + argument1 + '), ' + - argument2 + ')'; + generator.valueToCode(block, 'HIGH', Order.NONE) || 'math.huge'; + const code = + 'math.min(math.max(' + + argument0 + + ', ' + + argument1 + + '), ' + + argument2 + + ')'; return [code, Order.HIGH]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Random integer between [X] and [Y]. const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; const code = 'math.random(' + argument0 + ', ' + argument1 + ')'; return [code, Order.HIGH]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Random fraction between 0 and 1. return ['math.random()', Order.HIGH]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; return [ - 'math.deg(math.atan2(' + argument1 + ', ' + argument0 + '))', Order.HIGH + 'math.deg(math.atan2(' + argument1 + ', ' + argument0 + '))', + Order.HIGH, ]; -}; +} diff --git a/generators/lua/procedures.js b/generators/lua/procedures.ts similarity index 61% rename from generators/lua/procedures.js rename to generators/lua/procedures.ts index 935ca1136..07003aee9 100644 --- a/generators/lua/procedures.js +++ b/generators/lua/procedures.ts @@ -5,18 +5,22 @@ */ /** - * @fileoverview Generating Lua for procedure blocks. + * @file Generating Lua for procedure blocks. */ // Former goog.module ID: Blockly.Lua.procedures +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; +import type {LuaGenerator} from './lua_generator.js'; import {Order} from './lua_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn( + block: Block, + generator: LuaGenerator, +): null { // Define a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -30,8 +34,9 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId( - generator.INFINITE_LOOP_TRAP, block), generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } let branch = generator.statementToCode(block, 'STACK'); let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; @@ -50,22 +55,36 @@ export function procedures_defreturn(block, generator) { for (let i = 0; i < variables.length; i++) { args[i] = generator.getVariableName(variables[i]); } - let code = 'function ' + funcName + '(' + args.join(', ') + ')\n' + xfix1 + - loopTrap + branch + xfix2 + returnValue + 'end\n'; + let code = + 'function ' + + funcName + + '(' + + args.join(', ') + + ')\n' + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue + + 'end\n'; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Call a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { @@ -73,30 +92,39 @@ export function procedures_callreturn(block, generator) { } const code = funcName + '(' + args.join(', ') + ')'; return [code, Order.HIGH]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn( + block: Block, + generator: LuaGenerator, +): string { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn']( + block, + generator, + ) as [string, number]; return tuple[0] + '\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn( + block: Block, + generator: LuaGenerator, +): string { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; let code = 'if ' + condition + ' then\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. - code += - generator.prefixLines( - generator.injectId(generator.STATEMENT_SUFFIX, block), - generator.INDENT); + code += generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { + if ((block as IfReturnBlock).hasReturnValue_) { const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'nil'; code += generator.INDENT + 'return ' + value + '\n'; } else { @@ -104,4 +132,4 @@ export function procedures_ifreturn(block, generator) { } code += 'end\n'; return code; -}; +} diff --git a/generators/lua/text.js b/generators/lua/text.ts similarity index 69% rename from generators/lua/text.js rename to generators/lua/text.ts index 606fcce32..ff65327cf 100644 --- a/generators/lua/text.js +++ b/generators/lua/text.ts @@ -5,82 +5,99 @@ */ /** - * @fileoverview Generating Lua for text blocks. + * @file Generating Lua for text blocks. */ // Former goog.module ID: Blockly.Lua.texts +import type {Block} from '../../core/block.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; +import type {LuaGenerator} from './lua_generator.js'; import {Order} from './lua_generator.js'; - -export function text(block, generator) { +export function text(block: Block, generator: LuaGenerator): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = - code.indexOf('..') !== -1 ? Order.CONCATENATION : Order.ATOMIC; + const order = code.indexOf('..') !== -1 ? Order.CONCATENATION : Order.ATOMIC; return [code, order]; -}; +} -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: LuaGenerator, +): [string, Order] { + const joinBlock = block as JoinMutatorBlock; // Create a string made up of any number of elements of any type. - if (block.itemCount_ === 0) { + if (joinBlock.itemCount_ === 0) { return ["''", Order.ATOMIC]; - } else if (block.itemCount_ === 1) { + } else if (joinBlock.itemCount_ === 1) { const element = generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; const code = 'tostring(' + element + ')'; return [code, Order.HIGH]; - } else if (block.itemCount_ === 2) { + } else if (joinBlock.itemCount_ === 2) { const element0 = - generator.valueToCode(block, 'ADD0', Order.CONCATENATION) || "''"; + generator.valueToCode(block, 'ADD0', Order.CONCATENATION) || "''"; const element1 = - generator.valueToCode(block, 'ADD1', Order.CONCATENATION) || "''"; + generator.valueToCode(block, 'ADD1', Order.CONCATENATION) || "''"; const code = element0 + ' .. ' + element1; return [code, Order.CONCATENATION]; } else { const elements = []; - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; + for (let i = 0; i < joinBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; } const code = 'table.concat({' + elements.join(', ') + '})'; return [code, Order.HIGH]; } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: LuaGenerator): string { // Append to a variable in place. - const varName = - generator.getVariableName(block.getFieldValue('VAR')); + const varName = generator.getVariableName(block.getFieldValue('VAR')); const value = - generator.valueToCode(block, 'TEXT', Order.CONCATENATION) || "''"; + generator.valueToCode(block, 'TEXT', Order.CONCATENATION) || "''"; return varName + ' = ' + varName + ' .. ' + value + '\n'; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: LuaGenerator, +): [string, Order] { // String or array length. const text = generator.valueToCode(block, 'VALUE', Order.UNARY) || "''"; return ['#' + text, Order.UNARY]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.UNARY) || "''"; return ['#' + text + ' == 0', Order.RELATIONAL]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Search the text for a substring. const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; let functionName; if (block.getFieldValue('END') === 'FIRST') { - functionName = generator.provideFunction_('firstIndexOf', ` + functionName = generator.provideFunction_( + 'firstIndexOf', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str, substr) local i = string.find(str, substr, 1, true) if i == nil then @@ -88,9 +105,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str, substr) end return i end -`); +`, + ); } else { - functionName = generator.provideFunction_('lastIndexOf', ` + functionName = generator.provideFunction_( + 'lastIndexOf', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str, substr) local i = string.find(string.reverse(str), string.reverse(substr), 1, true) if i then @@ -98,27 +118,34 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str, substr) end return 0 end -`); +`, + ); } const code = functionName + '(' + text + ', ' + substring + ')'; return [code, Order.HIGH]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const atOrder = (where === 'FROM_END') ? Order.UNARY : Order.NONE; + const atOrder = where === 'FROM_END' ? Order.UNARY : Order.NONE; const at = generator.valueToCode(block, 'AT', atOrder) || '1'; const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; let code; if (where === 'RANDOM') { - const functionName = generator.provideFunction_('text_random_letter', ` + const functionName = generator.provideFunction_( + 'text_random_letter', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) local index = math.random(string.len(str)) return string.sub(str, index, index) end -`); +`, + ); code = functionName + '(' + text + ')'; } else { let start; @@ -139,24 +166,30 @@ end code = 'string.sub(' + text + ', ' + start + ', ' + start + ')'; } else { // use function to avoid reevaluation - const functionName = generator.provideFunction_('text_char_at', ` + const functionName = generator.provideFunction_( + 'text_char_at', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str, index) return string.sub(str, index, index) end -`); +`, + ); code = functionName + '(' + text + ', ' + start + ')'; } } return [code, Order.HIGH]; -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Get substring. const text = generator.valueToCode(block, 'STRING', Order.NONE) || "''"; // Get start index. const where1 = block.getFieldValue('WHERE1'); - const at1Order = (where1 === 'FROM_END') ? Order.UNARY : Order.NONE; + const at1Order = where1 === 'FROM_END' ? Order.UNARY : Order.NONE; const at1 = generator.valueToCode(block, 'AT1', at1Order) || '1'; let start; if (where1 === 'FIRST') { @@ -171,7 +204,7 @@ export function text_getSubstring(block, generator) { // Get end index. const where2 = block.getFieldValue('WHERE2'); - const at2Order = (where2 === 'FROM_END') ? Order.UNARY : Order.NONE; + const at2Order = where2 === 'FROM_END' ? Order.UNARY : Order.NONE; const at2 = generator.valueToCode(block, 'AT2', at2Order) || '1'; let end; if (where2 === 'LAST') { @@ -185,9 +218,12 @@ export function text_getSubstring(block, generator) { } const code = 'string.sub(' + text + ', ' + start + ', ' + end + ')'; return [code, Order.HIGH]; -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Change capitalization. const operator = block.getFieldValue('CASE'); const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; @@ -200,7 +236,9 @@ export function text_changeCase(block, generator) { // There are shorter versions at // http://lua-users.org/wiki/SciteTitleCase // that do not preserve whitespace. - functionName = generator.provideFunction_('text_titlecase', ` + functionName = generator.provideFunction_( + 'text_titlecase', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) local buf = {} local inWord = false @@ -218,28 +256,36 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(str) end return table.concat(buf) end -`); +`, + ); } const code = functionName + '(' + text + ')'; return [code, Order.HIGH]; -}; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = {LEFT: '^%s*(,-)', RIGHT: '(.-)%s*$', BOTH: '^%s*(.-)%s*$'}; - const operator = OPERATORS[block.getFieldValue('MODE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const code = 'string.gsub(' + text + ', "' + operator + '", "%1")'; return [code, Order.HIGH]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: LuaGenerator): string { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ')\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: LuaGenerator, +): [string, Order] { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -250,13 +296,16 @@ export function text_prompt_ext(block, generator) { msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; } - const functionName = generator.provideFunction_('text_prompt', ` + const functionName = generator.provideFunction_( + 'text_prompt', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(msg) io.write(msg) io.flush() return io.read() end -`); +`, + ); let code = functionName + '(' + msg + ')'; const toNumber = block.getFieldValue('TYPE') === 'NUMBER'; @@ -264,14 +313,19 @@ end code = 'tonumber(' + code + ', 10)'; } return [code, Order.HIGH]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { +export function text_count( + block: Block, + generator: LuaGenerator, +): [string, Order] { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; - const functionName = generator.provideFunction_('text_count', ` + const functionName = generator.provideFunction_( + 'text_count', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) if #needle == 0 then return #haystack + 1 @@ -288,16 +342,22 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle) end return count end -`); +`, + ); const code = functionName + '(' + text + ', ' + sub + ')'; return [code, Order.HIGH]; -}; +} -export function text_replace(block, generator) { +export function text_replace( + block: Block, + generator: LuaGenerator, +): [string, Order] { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; - const functionName = generator.provideFunction_('text_replace', ` + const functionName = generator.provideFunction_( + 'text_replace', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) local buf = {} local i = 1 @@ -314,13 +374,17 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(haystack, needle, replacement) end return table.concat(buf) end -`); +`, + ); const code = functionName + '(' + text + ', ' + from + ', ' + to + ')'; return [code, Order.HIGH]; -}; +} -export function text_reverse(block, generator) { +export function text_reverse( + block: Block, + generator: LuaGenerator, +): [string, Order] { 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 deleted file mode 100644 index 1913321c9..000000000 --- a/generators/lua/variables.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating Lua for variable blocks. - */ - -// Former goog.module ID: Blockly.Lua.variables - -import {Order} from './lua_generator.js'; - - -export function variables_get(block, generator) { - // Variable getter. - const code = - generator.getVariableName(block.getFieldValue('VAR')); - return [code, Order.ATOMIC]; -}; - -export function variables_set(block, generator) { - // Variable setter. - const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = ' + argument0 + '\n'; -}; diff --git a/generators/lua/variables.ts b/generators/lua/variables.ts new file mode 100644 index 000000000..b70f6892b --- /dev/null +++ b/generators/lua/variables.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating Lua for variable blocks. + */ + +// Former goog.module ID: Blockly.Lua.variables + +import type {Block} from '../../core/block.js'; +import type {LuaGenerator} from './lua_generator.js'; +import {Order} from './lua_generator.js'; + +export function variables_get( + block: Block, + generator: LuaGenerator, +): [string, Order] { + // Variable getter. + const code = generator.getVariableName(block.getFieldValue('VAR')); + return [code, Order.ATOMIC]; +} + +export function variables_set(block: Block, generator: LuaGenerator): string { + // Variable setter. + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + return varName + ' = ' + argument0 + '\n'; +} diff --git a/generators/lua/variables_dynamic.js b/generators/lua/variables_dynamic.ts similarity index 82% rename from generators/lua/variables_dynamic.js rename to generators/lua/variables_dynamic.ts index 4b42be990..62b981b31 100644 --- a/generators/lua/variables_dynamic.js +++ b/generators/lua/variables_dynamic.ts @@ -5,12 +5,11 @@ */ /** - * @fileoverview Generating Lua for dynamic variable blocks. + * @file Generating Lua for dynamic variable blocks. */ // Former goog.module ID: Blockly.Lua.variablesDynamic - // Lua is dynamically typed. export { variables_get as variables_get_dynamic, diff --git a/generators/php.js b/generators/php.ts similarity index 65% rename from generators/php.js rename to generators/php.ts index f61daccf7..18631ad9a 100644 --- a/generators/php.js +++ b/generators/php.ts @@ -5,9 +5,9 @@ */ /** - * @fileoverview Complete helper functions for generating PHP for - * blocks. This is the entrypoint for php_compressed.js. - * @suppress {extraRequire} + * @file Instantiate a PhpGenerator and populate it with the complete + * set of block generator functions for PHP. This is the entrypoint + * for php_compressed.js. */ // Former goog.module ID: Blockly.PHP.all @@ -32,8 +32,17 @@ export * from './php/php_generator.js'; export const phpGenerator = new PhpGenerator(); // Install per-block-type generator functions: -Object.assign( - phpGenerator.forBlock, - colour, lists, logic, loops, math, procedures, - text, variables, variablesDynamic -); +const generators: typeof phpGenerator.forBlock = { + ...colour, + ...lists, + ...logic, + ...loops, + ...math, + ...procedures, + ...text, + ...variables, + ...variablesDynamic, +}; +for (const name in generators) { + phpGenerator.forBlock[name] = generators[name]; +} diff --git a/generators/php/colour.js b/generators/php/colour.ts similarity index 69% rename from generators/php/colour.js rename to generators/php/colour.ts index e15cc385c..eefb7cba7 100644 --- a/generators/php/colour.js +++ b/generators/php/colour.ts @@ -5,37 +5,52 @@ */ /** - * @fileoverview Generating PHP for colour blocks. + * @file Generating PHP for colour blocks. */ // Former goog.module ID: Blockly.PHP.colour +import type {Block} from '../../core/block.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function colour_picker(block, generator) { +export function colour_picker( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; -}; +} -export function colour_random(block, generator) { +export function colour_random( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Generate a random colour. - const functionName = generator.provideFunction_('colour_random', ` + const functionName = generator.provideFunction_( + 'colour_random', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}() { return '#' . str_pad(dechex(mt_rand(0, 0xFFFFFF)), 6, '0', STR_PAD_LEFT); } -`); +`, + ); const code = functionName + '()'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_rgb(block, generator) { +export function colour_rgb( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Compose a colour from RGB components expressed as percentages. const red = generator.valueToCode(block, 'RED', Order.NONE) || 0; const green = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; const blue = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; - const functionName = generator.provideFunction_('colour_rgb', ` + const functionName = generator.provideFunction_( + 'colour_rgb', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($r, $g, $b) { $r = round(max(min($r, 100), 0) * 2.55); $g = round(max(min($g, 100), 0) * 2.55); @@ -46,19 +61,23 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($r, $g, $b) { $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); return $hex; } -`); +`, + ); const code = functionName + '(' + red + ', ' + green + ', ' + blue + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_blend(block, generator) { +export function colour_blend( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Blend two colours together. - const c1 = - generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; - const c2 = - generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; + const c1 = generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; + const c2 = generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0.5; - const functionName = generator.provideFunction_('colour_blend', ` + const functionName = generator.provideFunction_( + 'colour_blend', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($c1, $c2, $ratio) { $ratio = max(min($ratio, 1), 0); $r1 = hexdec(substr($c1, 1, 2)); @@ -76,7 +95,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($c1, $c2, $ratio) { $hex .= str_pad(dechex($b), 2, '0', STR_PAD_LEFT); return $hex; } -`); +`, + ); const code = functionName + '(' + c1 + ', ' + c2 + ', ' + ratio + ')'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/php/lists.js b/generators/php/lists.ts similarity index 68% rename from generators/php/lists.js rename to generators/php/lists.ts index d2d5a0842..7ca2639b7 100644 --- a/generators/php/lists.js +++ b/generators/php/lists.ts @@ -5,7 +5,7 @@ */ /** - * @fileoverview Generating PHP for list blocks. + * @file Generating PHP for list blocks. */ /** @@ -22,27 +22,42 @@ // Former goog.module ID: Blockly.generator.lists import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {CreateWithBlock} from '../../blocks/lists.js'; import {NameType} from '../../core/names.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; -export function lists_create_empty(block, generator) { +export function lists_create_empty( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Create an empty list. return ['array()', Order.FUNCTION_CALL]; -}; +} -export function lists_create_with(block, generator) { +export function lists_create_with( + block: Block, + generator: PhpGenerator, +): [string, Order] { // 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++) { - code[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; + const createWithBlock = block as CreateWithBlock; + const elements = new Array(createWithBlock.itemCount_); + for (let i = 0; i < createWithBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'null'; } - code = 'array(' + code.join(', ') + ')'; + const code = 'array(' + elements.join(', ') + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_repeat(block, generator) { +export function lists_repeat( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Create a list with one element repeated. - const functionName = generator.provideFunction_('lists_repeat', ` + const functionName = generator.provideFunction_( + 'lists_repeat', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value, $count) { $array = array(); for ($index = 0; $index < $count; $index++) { @@ -50,16 +65,22 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value, $count) { } return $array; } -`); +`, + ); const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'null'; const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; const code = functionName + '(' + element + ', ' + repeatCount + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_length(block, generator) { +export function lists_length( + block: Block, + generator: PhpGenerator, +): [string, Order] { // String or array length. - const functionName = generator.provideFunction_('length', ` + const functionName = generator.provideFunction_( + 'length', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { if (is_string($value)) { return strlen($value); @@ -67,24 +88,29 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { return count($value); } } -`); +`, + ); const list = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return [functionName + '(' + list + ')', Order.FUNCTION_CALL]; -}; +} -export function lists_isEmpty(block, generator) { +export function lists_isEmpty( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Is the string null or array empty? const argument0 = - generator.valueToCode(block, 'VALUE', Order.FUNCTION_CALL) - || 'array()'; + generator.valueToCode(block, 'VALUE', Order.FUNCTION_CALL) || 'array()'; return ['empty(' + argument0 + ')', Order.FUNCTION_CALL]; -}; +} -export function lists_indexOf(block, generator) { +export function lists_indexOf( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Find an item in the list. const argument0 = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; - const argument1 = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; + const argument1 = generator.valueToCode(block, 'VALUE', Order.MEMBER) || '[]'; let errorIndex = ' -1'; let indexAdjustment = ''; if (block.workspace.options.oneBasedIndex) { @@ -94,17 +120,22 @@ export function lists_indexOf(block, generator) { let functionName; if (block.getFieldValue('END') === 'FIRST') { // indexOf - functionName = generator.provideFunction_('indexOf', ` + functionName = generator.provideFunction_( + 'indexOf', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($haystack, $needle) { for ($index = 0; $index < count($haystack); $index++) { if ($haystack[$index] == $needle) return $index${indexAdjustment}; } return ${errorIndex}; } -`); +`, + ); } else { // lastIndexOf - functionName = generator.provideFunction_('lastIndexOf', ` + functionName = generator.provideFunction_( + 'lastIndexOf', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($haystack, $needle) { $last = ${errorIndex}; for ($index = 0; $index < count($haystack); $index++) { @@ -112,14 +143,18 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($haystack, $needle) { } return $last; } -`); +`, + ); } const code = functionName + '(' + argument1 + ', ' + argument0 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_getIndex(block, generator) { +export function lists_getIndex( + block: Block, + generator: PhpGenerator, +): [string, Order] | string { // Get element at index. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; @@ -127,34 +162,34 @@ export function lists_getIndex(block, generator) { case 'FIRST': if (mode === 'GET') { const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.MEMBER) || 'array()'; const code = list + '[0]'; return [code, Order.MEMBER]; } else if (mode === 'GET_REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; const code = 'array_shift(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; return 'array_shift(' + list + ');\n'; } break; case 'LAST': if (mode === 'GET') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; const code = 'end(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'GET_REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; const code = 'array_pop(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; return 'array_pop(' + list + ');\n'; } break; @@ -162,17 +197,17 @@ export function lists_getIndex(block, generator) { const at = generator.getAdjusted(block, 'AT'); if (mode === 'GET') { const list = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.MEMBER) || 'array()'; const code = list + '[' + at + ']'; return [code, Order.MEMBER]; } else if (mode === 'GET_REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; const code = 'array_splice(' + list + ', ' + at + ', 1)[0]'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; return 'array_splice(' + list + ', ' + at + ', 1);\n'; } break; @@ -180,17 +215,22 @@ export function lists_getIndex(block, generator) { case 'FROM_END': if (mode === 'GET') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; const at = generator.getAdjusted(block, 'AT', 1, true); const code = 'array_slice(' + list + ', ' + at + ', 1)[0]'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'GET_REMOVE' || mode === 'REMOVE') { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; - const at = - generator.getAdjusted(block, 'AT', 1, false, Order.SUBTRACTION); - const code = 'array_splice(' + list + ', count(' + list + ') - ' + at + - ', 1)[0]'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + const at = generator.getAdjusted( + block, + 'AT', + 1, + false, + Order.SUBTRACTION, + ); + const code = + 'array_splice(' + list + ', count(' + list + ') - ' + at + ', 1)[0]'; if (mode === 'GET_REMOVE') { return [code, Order.FUNCTION_CALL]; } else if (mode === 'REMOVE') { @@ -200,58 +240,65 @@ export function lists_getIndex(block, generator) { break; case 'RANDOM': { const list = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; + generator.valueToCode(block, 'VALUE', Order.NONE) || 'array()'; if (mode === 'GET') { - const functionName = - generator.provideFunction_('lists_get_random_item', ` + const functionName = generator.provideFunction_( + 'lists_get_random_item', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list) { return $list[rand(0,count($list)-1)]; } -`); +`, + ); const code = functionName + '(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'GET_REMOVE') { - const functionName = - generator.provideFunction_('lists_get_remove_random_item', ` + const functionName = generator.provideFunction_( + 'lists_get_remove_random_item', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list) { $x = rand(0,count($list)-1); unset($list[$x]); return array_values($list); } -`); +`, + ); const code = functionName + '(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else if (mode === 'REMOVE') { - const functionName = - generator.provideFunction_('lists_remove_random_item', ` + const functionName = generator.provideFunction_( + 'lists_remove_random_item', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list) { unset($list[rand(0,count($list)-1)]); } -`); +`, + ); return functionName + '(' + list + ');\n'; } break; } } throw Error('Unhandled combination (lists_getIndex).'); -}; +} -export function lists_setIndex(block, generator) { +export function lists_setIndex(block: Block, generator: PhpGenerator) { // Set element at index. // Note: Until February 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const value = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; + const value = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || 'null'; // Cache non-trivial values to variables to prevent repeated look-ups. // Closure, which accesses and modifies 'list'. - let cachedList; + let cachedList: string; function cacheList() { if (cachedList.match(/^\$\w+$/)) { return ''; } - const listVar = - generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmp_list', + NameType.VARIABLE, + ); const code = listVar + ' = &' + cachedList + ';\n'; cachedList = listVar; return code; @@ -260,24 +307,26 @@ export function lists_setIndex(block, generator) { case 'FIRST': if (mode === 'SET') { const list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || 'array()'; + generator.valueToCode(block, 'LIST', Order.MEMBER) || 'array()'; return list + '[0] = ' + value + ';\n'; } else if (mode === 'INSERT') { const list = - generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; return 'array_unshift(' + list + ', ' + value + ');\n'; } break; case 'LAST': { const list = - generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; if (mode === 'SET') { - const functionName = - generator.provideFunction_('lists_set_last_item', ` + const functionName = generator.provideFunction_( + 'lists_set_last_item', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $value) { $list[count($list) - 1] = $value; } -`); +`, + ); return functionName + '(' + list + ', ' + value + ');\n'; } else if (mode === 'INSERT') { return 'array_push(' + list + ', ' + value + ');\n'; @@ -288,45 +337,51 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $value) { const at = generator.getAdjusted(block, 'AT'); if (mode === 'SET') { const list = - generator.valueToCode(block, 'LIST', Order.MEMBER) || 'array()'; + generator.valueToCode(block, 'LIST', Order.MEMBER) || 'array()'; return list + '[' + at + '] = ' + value + ';\n'; } else if (mode === 'INSERT') { const list = - generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; return 'array_splice(' + list + ', ' + at + ', 0, ' + value + ');\n'; } break; } case 'FROM_END': { const list = - generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; const at = generator.getAdjusted(block, 'AT', 1); if (mode === 'SET') { - const functionName = - generator.provideFunction_('lists_set_from_end', ` + const functionName = generator.provideFunction_( + 'lists_set_from_end', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $at, $value) { $list[count($list) - $at] = $value; } -`); +`, + ); return functionName + '(' + list + ', ' + at + ', ' + value + ');\n'; } else if (mode === 'INSERT') { - const functionName = - generator.provideFunction_('lists_insert_from_end', ` + const functionName = generator.provideFunction_( + 'lists_insert_from_end', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $at, $value) { return array_splice($list, count($list) - $at, 0, $value); } -`); +`, + ); return functionName + '(' + list + ', ' + at + ', ' + value + ');\n'; } break; } case 'RANDOM': cachedList = - generator.valueToCode(block, 'LIST', Order.REFERENCE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.REFERENCE) || 'array()'; let code = cacheList(); const list = cachedList; - const xVar = - generator.nameDB_.getDistinctName('tmp_x', NameType.VARIABLE); + const xVar = generator.nameDB_!.getDistinctName( + 'tmp_x', + NameType.VARIABLE, + ); code += xVar + ' = rand(0, count(' + list + ')-1);\n'; if (mode === 'SET') { code += list + '[' + xVar + '] = ' + value + ';\n'; @@ -338,9 +393,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(&$list, $at, $value) { break; } throw Error('Unhandled combination (lists_setIndex).'); -}; +} -export function lists_getSublist(block, generator) { +export function lists_getSublist( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; const where1 = block.getFieldValue('WHERE1'); @@ -349,8 +407,9 @@ export function lists_getSublist(block, generator) { if (where1 === 'FIRST' && where2 === 'LAST') { code = list; } else if ( - list.match(/^\$\w+$/) || - (where1 !== 'FROM_END' && where2 === 'FROM_START')) { + list.match(/^\$\w+$/) || + (where1 !== 'FROM_END' && where2 === 'FROM_START') + ) { // If the list is a simple value or doesn't require a call for length, don't // generate a helper function. let at1; @@ -359,8 +418,7 @@ export function lists_getSublist(block, generator) { at1 = generator.getAdjusted(block, 'AT1'); break; case 'FROM_END': - at1 = - generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); + at1 = generator.getAdjusted(block, 'AT1', 1, false, Order.SUBTRACTION); at1 = 'count(' + list + ') - ' + at1; break; case 'FIRST': @@ -373,11 +431,12 @@ export function lists_getSublist(block, generator) { let length; switch (where2) { case 'FROM_START': - at2 = - generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); length = at2 + ' - '; - if (stringUtils.isNumber(String(at1)) || - String(at1).match(/^\(.+\)$/)) { + if ( + stringUtils.isNumber(String(at1)) || + String(at1).match(/^\(.+\)$/) + ) { length += at1; } else { length += '(' + at1 + ')'; @@ -385,11 +444,12 @@ export function lists_getSublist(block, generator) { length += ' + 1'; break; case 'FROM_END': - at2 = - generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); + at2 = generator.getAdjusted(block, 'AT2', 0, false, Order.SUBTRACTION); length = 'count(' + list + ') - ' + at2 + ' - '; - if (stringUtils.isNumber(String(at1)) || - String(at1).match(/^\(.+\)$/)) { + if ( + stringUtils.isNumber(String(at1)) || + String(at1).match(/^\(.+\)$/) + ) { length += at1; } else { length += '(' + at1 + ')'; @@ -397,8 +457,10 @@ export function lists_getSublist(block, generator) { break; case 'LAST': length = 'count(' + list + ') - '; - if (stringUtils.isNumber(String(at1)) || - String(at1).match(/^\(.+\)$/)) { + if ( + stringUtils.isNumber(String(at1)) || + String(at1).match(/^\(.+\)$/) + ) { length += at1; } else { length += '(' + at1 + ')'; @@ -411,8 +473,9 @@ export function lists_getSublist(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const functionName = - generator.provideFunction_('lists_get_sublist', ` + const functionName = generator.provideFunction_( + 'lists_get_sublist', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $where1, $at1, $where2, $at2) { if ($where1 == 'FROM_END') { $at1 = count($list) - 1 - $at1; @@ -433,20 +496,37 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $where1, $at1, $where2, } return array_slice($list, $at1, $length); } -`); - code = functionName + '(' + list + ', \'' + where1 + '\', ' + at1 + ', \'' + - where2 + '\', ' + at2 + ')'; +`, + ); + code = + functionName + + '(' + + list + + ", '" + + where1 + + "', " + + at1 + + ", '" + + where2 + + "', " + + at2 + + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_sort(block, generator) { +export function lists_sort( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Block for sorting a list. const listCode = - generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; + generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; const direction = block.getFieldValue('DIRECTION') === '1' ? 1 : -1; const type = block.getFieldValue('TYPE'); - const functionName = generator.provideFunction_('lists_sort', ` + const functionName = generator.provideFunction_( + 'lists_sort', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $type, $direction) { $sortCmpFuncs = array( 'NUMERIC' => 'strnatcasecmp', @@ -461,17 +541,20 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list, $type, $direction) { } return $list2; } -`); +`, + ); const sortCode = - functionName + '(' + listCode + ', "' + type + '", ' + direction + ')'; + functionName + '(' + listCode + ', "' + type + '", ' + direction + ')'; return [sortCode, Order.FUNCTION_CALL]; -}; +} -export function lists_split(block, generator) { +export function lists_split( + block: Block, + generator: PhpGenerator, +): [string, Order] { // 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 = - generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; + const value_delim = generator.valueToCode(block, 'DELIM', Order.NONE) || "''"; const mode = block.getFieldValue('MODE'); let functionName; if (mode === 'SPLIT') { @@ -489,11 +572,14 @@ export function lists_split(block, generator) { } const code = functionName + '(' + value_delim + ', ' + value_input + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_reverse(block, generator) { +export function lists_reverse( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const code = 'array_reverse(' + list + ')'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/php/logic.js b/generators/php/logic.ts similarity index 51% rename from generators/php/logic.js rename to generators/php/logic.ts index 0968c1ea4..8aaa5c892 100644 --- a/generators/php/logic.js +++ b/generators/php/logic.ts @@ -5,35 +5,43 @@ */ /** - * @fileoverview Generating PHP for logic blocks. + * @file Generating PHP for logic blocks. */ // Former goog.module ID: Blockly.PHP.logic +import type {Block} from '../../core/block.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function controls_if(block, generator) { +export function controls_if(block: Block, generator: PhpGenerator) { // If/elseif/else condition. let n = 0; - let code = '', branchCode, conditionCode; + let code = '', + branchCode, + conditionCode; if (generator.STATEMENT_PREFIX) { // Automatic prefix insertion is switched off for this block. Add manually. code += generator.injectId(generator.STATEMENT_PREFIX, block); } do { conditionCode = - generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; + generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false'; branchCode = generator.statementToCode(block, 'DO' + n); if (generator.STATEMENT_SUFFIX) { branchCode = - generator.prefixLines( - generator.injectId(generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; } - code += (n > 0 ? ' else ' : '') + 'if (' + conditionCode + ') {\n' + - branchCode + '}'; + code += + (n > 0 ? ' else ' : '') + + 'if (' + + conditionCode + + ') {\n' + + branchCode + + '}'; n++; } while (block.getInput('IF' + n)); @@ -41,36 +49,48 @@ export function controls_if(block, generator) { branchCode = generator.statementToCode(block, 'ELSE'); if (generator.STATEMENT_SUFFIX) { branchCode = - generator.prefixLines( - generator.injectId(generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; } code += ' else {\n' + branchCode + '}'; } return code + '\n'; -}; +} export const controls_ifelse = controls_if; -export function logic_compare(block, generator) { +export function logic_compare( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const order = (operator === '==' || operator === '!=') ? Order.EQUALITY : - Order.RELATIONAL; + const OPERATORS = { + 'EQ': '==', + 'NEQ': '!=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const order = + operator === '==' || operator === '!=' ? Order.EQUALITY : Order.RELATIONAL; const argument0 = generator.valueToCode(block, 'A', order) || '0'; const argument1 = generator.valueToCode(block, 'B', order) || '0'; const code = argument0 + ' ' + operator + ' ' + argument1; return [code, order]; -}; +} -export function logic_operation(block, generator) { +export function logic_operation( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? '&&' : '||'; - const order = - (operator === '&&') ? Order.LOGICAL_AND : Order.LOGICAL_OR; + const operator = block.getFieldValue('OP') === 'AND' ? '&&' : '||'; + const order = operator === '&&' ? Order.LOGICAL_AND : Order.LOGICAL_OR; let argument0 = generator.valueToCode(block, 'A', order); let argument1 = generator.valueToCode(block, 'B', order); if (!argument0 && !argument1) { @@ -79,7 +99,7 @@ export function logic_operation(block, generator) { argument1 = 'false'; } else { // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === '&&') ? 'true' : 'false'; + const defaultArgument = operator === '&&' ? 'true' : 'false'; if (!argument0) { argument0 = defaultArgument; } @@ -89,35 +109,47 @@ export function logic_operation(block, generator) { } const code = argument0 + ' ' + operator + ' ' + argument1; return [code, order]; -}; +} -export function logic_negate(block, generator) { +export function logic_negate( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Negation. const order = Order.LOGICAL_NOT; const argument0 = generator.valueToCode(block, 'BOOL', order) || 'true'; const code = '!' + argument0; return [code, order]; -}; +} -export function logic_boolean(block, generator) { +export function logic_boolean( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false'; + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'true' : 'false'; return [code, Order.ATOMIC]; -}; +} -export function logic_null(block, generator) { +export function logic_null( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Null data type. return ['null', Order.ATOMIC]; -}; +} -export function logic_ternary(block, generator) { +export function logic_ternary( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Ternary operator. const value_if = - generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; + generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'false'; const value_then = - generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; + generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'null'; const value_else = - generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; + generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'null'; const code = value_if + ' ? ' + value_then + ' : ' + value_else; return [code, Order.CONDITIONAL]; -}; +} diff --git a/generators/php/loops.js b/generators/php/loops.ts similarity index 57% rename from generators/php/loops.js rename to generators/php/loops.ts index 981a4e219..ae87dbb29 100644 --- a/generators/php/loops.js +++ b/generators/php/loops.ts @@ -5,17 +5,19 @@ */ /** - * @fileoverview Generating PHP for loop blocks. + * @file Generating PHP for loop blocks. */ // Former goog.module ID: Blockly.PHP.loops import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; import {NameType} from '../../core/names.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext(block: Block, generator: PhpGenerator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -28,55 +30,80 @@ export function controls_repeat_ext(block, generator) { let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; - const loopVar = - generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); let endVar = repeats; if (!repeats.match(/^\w+$/) && !stringUtils.isNumber(repeats)) { - endVar = - generator.nameDB_.getDistinctName('repeat_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + 'repeat_end', + NameType.VARIABLE, + ); code += endVar + ' = ' + repeats + ';\n'; } - code += 'for (' + loopVar + ' = 0; ' + loopVar + ' < ' + endVar + '; ' + - loopVar + '++) {\n' + branch + '}\n'; + code += + 'for (' + + loopVar + + ' = 0; ' + + loopVar + + ' < ' + + endVar + + '; ' + + loopVar + + '++) {\n' + + branch + + '}\n'; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil(block: Block, generator: PhpGenerator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; let argument0 = - generator.valueToCode( - block, 'BOOL', until ? Order.LOGICAL_NOT : Order.NONE) || - 'false'; + generator.valueToCode( + block, + 'BOOL', + until ? Order.LOGICAL_NOT : Order.NONE, + ) || 'false'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); if (until) { argument0 = '!' + argument0; } return 'while (' + argument0 + ') {\n' + branch + '}\n'; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: PhpGenerator) { // For loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; - const argument1 = - generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; - const increment = - generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; + generator.valueToCode(block, 'FROM', Order.ASSIGNMENT) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.ASSIGNMENT) || '0'; + const increment = generator.valueToCode(block, 'BY', Order.ASSIGNMENT) || '1'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code; - if (stringUtils.isNumber(argument0) && stringUtils.isNumber(argument1) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(argument0) && + stringUtils.isNumber(argument1) && + stringUtils.isNumber(increment) + ) { // All arguments are simple numbers. const up = Number(argument0) <= Number(argument1); - code = 'for (' + variable0 + ' = ' + argument0 + '; ' + variable0 + - (up ? ' <= ' : ' >= ') + argument1 + '; ' + variable0; + code = + 'for (' + + variable0 + + ' = ' + + argument0 + + '; ' + + variable0 + + (up ? ' <= ' : ' >= ') + + argument1 + + '; ' + + variable0; const step = Math.abs(Number(increment)); if (step === 1) { code += up ? '++' : '--'; @@ -89,55 +116,78 @@ export function controls_for(block, generator) { // Cache non-trivial values to variables to prevent repeated look-ups. let startVar = argument0; if (!argument0.match(/^\w+$/) && !stringUtils.isNumber(argument0)) { - startVar = - generator.nameDB_.getDistinctName( - variable0 + '_start', NameType.VARIABLE); + startVar = generator.nameDB_!.getDistinctName( + variable0 + '_start', + NameType.VARIABLE, + ); code += startVar + ' = ' + argument0 + ';\n'; } let endVar = argument1; if (!argument1.match(/^\w+$/) && !stringUtils.isNumber(argument1)) { - endVar = - generator.nameDB_.getDistinctName( - variable0 + '_end', NameType.VARIABLE); + endVar = generator.nameDB_!.getDistinctName( + variable0 + '_end', + NameType.VARIABLE, + ); code += endVar + ' = ' + argument1 + ';\n'; } // Determine loop direction at start, in case one of the bounds // changes during loop execution. - const incVar = - generator.nameDB_.getDistinctName( - variable0 + '_inc', NameType.VARIABLE); + const incVar = generator.nameDB_!.getDistinctName( + variable0 + '_inc', + NameType.VARIABLE, + ); code += incVar + ' = '; if (stringUtils.isNumber(increment)) { - code += Math.abs(increment) + ';\n'; + code += Math.abs(Number(increment)) + ';\n'; } else { code += 'abs(' + increment + ');\n'; } code += 'if (' + startVar + ' > ' + endVar + ') {\n'; code += generator.INDENT + incVar + ' = -' + incVar + ';\n'; code += '}\n'; - code += 'for (' + variable0 + ' = ' + startVar + '; ' + incVar + - ' >= 0 ? ' + variable0 + ' <= ' + endVar + ' : ' + variable0 + - ' >= ' + endVar + '; ' + variable0 + ' += ' + incVar + ') {\n' + - branch + '}\n'; + code += + 'for (' + + variable0 + + ' = ' + + startVar + + '; ' + + incVar + + ' >= 0 ? ' + + variable0 + + ' <= ' + + endVar + + ' : ' + + variable0 + + ' >= ' + + endVar + + '; ' + + variable0 + + ' += ' + + incVar + + ') {\n' + + branch + + '}\n'; } return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach(block: Block, generator: PhpGenerator) { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; + generator.valueToCode(block, 'LIST', Order.ASSIGNMENT) || '[]'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block); let code = ''; code += - 'foreach (' + argument0 + ' as ' + variable0 + ') {\n' + branch + '}\n'; + 'foreach (' + argument0 + ' as ' + variable0 + ') {\n' + branch + '}\n'; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: PhpGenerator, +) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { @@ -150,7 +200,7 @@ export function controls_flow_statements(block, generator) { xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. @@ -165,4 +215,4 @@ export function controls_flow_statements(block, generator) { return xfix + 'continue;\n'; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/php/math.js b/generators/php/math.ts similarity index 70% rename from generators/php/math.js rename to generators/php/math.ts index f1985949e..6218bf4d1 100644 --- a/generators/php/math.js +++ b/generators/php/math.ts @@ -5,45 +5,55 @@ */ /** - * @fileoverview Generating PHP for math blocks. + * @file Generating PHP for math blocks. */ // Former goog.module ID: Blockly.PHP.math +import type {Block} from '../../core/block.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Numeric value. - let code = Number(block.getFieldValue('NUM')); - const order = code >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION; - if (code === Infinity) { - code = 'INF'; - } else if (code === -Infinity) { - code = '-INF'; + let number = Number(block.getFieldValue('NUM')); + if (number === Infinity) { + return ['INF', Order.ATOMIC]; + } else if (number === -Infinity) { + return ['-INF', Order.UNARY_NEGATION]; } - return [code, order]; -}; + return [String(number), number >= 0 ? Order.ATOMIC : Order.UNARY_NEGATION]; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITION], 'MINUS': [' - ', Order.SUBTRACTION], 'MULTIPLY': [' * ', Order.MULTIPLICATION], 'DIVIDE': [' / ', Order.DIVISION], 'POWER': [' ** ', Order.POWER], }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; const argument1 = generator.valueToCode(block, 'B', order) || '0'; const code = argument0 + operator + argument1; return [code, order]; -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -122,11 +132,14 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.DIVISION]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['M_PI', Order.ATOMIC], 'E': ['M_E', Order.ATOMIC], 'GOLDEN_RATIO': ['(1 + sqrt(5)) / 2', Order.DIVISION], @@ -134,13 +147,20 @@ export function math_constant(block, generator) { 'SQRT1_2': ['M_SQRT1_2', Order.ATOMIC], 'INFINITY': ['INF', Order.ATOMIC], }; - return CONSTANTS[block.getFieldValue('CONSTANT')]; -}; + type ConstantOption = keyof typeof CONSTANTS; + return CONSTANTS[block.getFieldValue('CONSTANT') as ConstantOption]; +} -export function math_number_property(block, generator) { +export function math_number_property( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Check if a number is even, odd, prime, whole, positive, or negative // or if it is divisible by certain number. Returns true or false. - const PROPERTIES = { + const PROPERTIES: Record< + string, + [string, string, Order, Order] | [null, null, Order, Order] + > = { 'EVEN': ['', ' % 2 == 0', Order.MODULUS, Order.EQUALITY], 'ODD': ['', ' % 2 == 1', Order.MODULUS, Order.EQUALITY], 'WHOLE': ['is_int(', ')', Order.NONE, Order.FUNCTION_CALL], @@ -149,15 +169,18 @@ export function math_number_property(block, generator) { 'DIVISIBLE_BY': [null, null, Order.MODULUS, Order.EQUALITY], 'PRIME': [null, null, Order.NONE, Order.FUNCTION_CALL], }; - const dropdownProperty = block.getFieldValue('PROPERTY'); + type PropertyOption = keyof typeof PROPERTIES; + const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption; const [prefix, suffix, inputOrder, outputOrder] = - PROPERTIES[dropdownProperty]; - const numberToCheck = generator.valueToCode(block, 'NUMBER_TO_CHECK', - inputOrder) || '0'; + PROPERTIES[dropdownProperty]; + const numberToCheck = + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - const functionName = generator.provideFunction_('math_isPrime', ` + const functionName = generator.provideFunction_( + 'math_isPrime', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($n) { // https://en.wikipedia.org/wiki/Primality_test#Naive_methods if ($n == 2 || $n == 3) { @@ -176,37 +199,39 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($n) { } return true; } -`); +`, + ); code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MODULUS) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; if (divisor === '0') { return ['false', Order.ATOMIC]; - } code = numberToCheck + ' % ' + divisor + ' == 0'; } else { code = prefix + numberToCheck + suffix; } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: PhpGenerator) { // Add to a variable in place. const argument0 = - generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); + generator.valueToCode(block, 'DELTA', Order.ADDITION) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); return varName + ' += ' + argument0 + ';\n'; -}; +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); let list; @@ -214,40 +239,43 @@ export function math_on_list(block, generator) { switch (func) { case 'SUM': list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) - || 'array()'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()'; code = 'array_sum(' + list + ')'; break; case 'MIN': list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) - || 'array()'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()'; code = 'min(' + list + ')'; break; case 'MAX': list = - generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) - || 'array()'; + generator.valueToCode(block, 'LIST', Order.FUNCTION_CALL) || 'array()'; code = 'max(' + list + ')'; break; case 'AVERAGE': { - const functionName = generator.provideFunction_('math_mean', ` + const functionName = generator.provideFunction_( + 'math_mean', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($myList) { return array_sum($myList) / count($myList); } -`); +`, + ); list = generator.valueToCode(block, 'LIST', Order.NONE) || 'array()'; code = functionName + '(' + list + ')'; break; } case 'MEDIAN': { - const functionName = generator.provideFunction_('math_median', ` + const functionName = generator.provideFunction_( + 'math_median', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($arr) { sort($arr,SORT_NUMERIC); return (count($arr) % 2) ? $arr[floor(count($arr) / 2)] : ($arr[floor(count($arr) / 2)] + $arr[floor(count($arr) / 2) - 1]) / 2; } -`); +`, + ); list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; @@ -256,7 +284,9 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($arr) { // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1]. - const functionName = generator.provideFunction_('math_modes', ` + const functionName = generator.provideFunction_( + 'math_modes', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($values) { if (empty($values)) return array(); $counts = array_count_values($values); @@ -264,14 +294,16 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($values) { $modes = array_keys($counts, current($counts), true); return $modes; } -`); +`, + ); list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'STD_DEV': { - const functionName = - generator.provideFunction_('math_standard_deviation', ` + const functionName = generator.provideFunction_( + 'math_standard_deviation', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($numbers) { $n = count($numbers); if (!$n) return null; @@ -279,18 +311,22 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($numbers) { foreach($numbers as $key => $num) $devs[$key] = pow($num - $mean, 2); return sqrt(array_sum($devs) / (count($devs) - 1)); } -`); +`, + ); list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; } case 'RANDOM': { - const functionName = generator.provideFunction_('math_random_list', ` + const functionName = generator.provideFunction_( + 'math_random_list', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list) { $x = rand(0, count($list)-1); return $list[$x]; } -`); +`, + ); list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; code = functionName + '(' + list + ')'; break; @@ -299,56 +335,74 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($list) { throw Error('Unknown operator: ' + func); } return [code, Order.FUNCTION_CALL]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Remainder computation. const argument0 = - generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; + generator.valueToCode(block, 'DIVIDEND', Order.MODULUS) || '0'; const argument1 = - generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; + generator.valueToCode(block, 'DIVISOR', Order.MODULUS) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MODULUS]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Constrain a number between two limits. const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; const argument2 = - generator.valueToCode(block, 'HIGH', Order.NONE) || 'Infinity'; + generator.valueToCode(block, 'HIGH', Order.NONE) || 'Infinity'; const code = - 'min(max(' + argument0 + ', ' + argument1 + '), ' + argument2 + ')'; + 'min(max(' + argument0 + ', ' + argument1 + '), ' + argument2 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Random integer between [X] and [Y]. const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; - const functionName = generator.provideFunction_('math_random_int', ` + const functionName = generator.provideFunction_( + 'math_random_int', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($a, $b) { if ($a > $b) { return rand($b, $a); } return rand($a, $b); } -`); +`, + ); const code = functionName + '(' + argument0 + ', ' + argument1 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Random fraction between 0 and 1. return ['(float)rand()/(float)getrandmax()', Order.FUNCTION_CALL]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; return [ 'atan2(' + argument1 + ', ' + argument0 + ') / pi() * 180', - Order.DIVISION + Order.DIVISION, ]; -}; +} diff --git a/generators/php/php_generator.js b/generators/php/php_generator.js deleted file mode 100644 index dfe6f8543..000000000 --- a/generators/php/php_generator.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * @license - * Copyright 2015 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Helper functions for generating PHP for blocks. - * @suppress {checkTypes|globalThis} - */ - -// Former goog.module ID: 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. - */ - 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. - */ - 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/php_generator.ts b/generators/php/php_generator.ts new file mode 100644 index 000000000..ecdfb76fd --- /dev/null +++ b/generators/php/php_generator.ts @@ -0,0 +1,320 @@ +/** + * @license + * Copyright 2015 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file PHP code generator class, including helper methods for + * generating PHP for blocks. + */ + +// Former goog.module ID: 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 + */ +// prettier-ignore +export enum 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. */ + ORDER_OVERRIDES: [Order, Order][] = [ + // (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], + ]; + + /** @param name Name of the language the generator is for. */ + constructor(name = 'PHP') { + super(name); + 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) { + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; + } + + // List of illegal variable names. This is not intended to be a + // security feature. Blockly is 100% client-side, so bypassing + // this list is trivial. This is intended to prevent users from + // accidentally clobbering a built-in object or function. + 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 to generate code from. + */ + init(workspace: 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 code Generated code. + * @returns Completed code. + */ + finish(code: string): string { + // 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. + * + * @param line Line of generated code. + * @returns Legal line of code. + */ + scrubNakedValue(line: string): string { + return line + ';\n'; + } + + /** + * Encode a string as a properly escaped PHP string, complete with + * quotes. + * + * @param string Text to encode. + * @returns PHP string. + */ + quote_(string: string): 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 Text to encode. + * @returns PHP string. + */ + multiline_quote_(string: string): 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 The current block. + * @param code The PHP code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns PHP code with comments and subsequent blocks added. + */ + scrub_(block: Block, code: string, thisOnly = false): string { + let commentCode = ''; + // Only collect comments for blocks that aren't inline. + if (!block.outputConnection || !block.outputConnection.targetConnection) { + // 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 = thisOnly ? '' : this.blockToCode(nextBlock); + return commentCode + code + nextCode; + } + + /** + * Generate code representing the specified value input, adjusted to take into + * account indexing (zero- or one-based) and optionally by a specified delta + * and/or by negation. + * + * @param block The block. + * @param atId The ID of the input block to get (and adjust) the value of. + * @param delta Value to add. + * @param negate Whether to negate the value. + * @param order The highest order acting on this value. + * @returns The adjusted value or code that evaluates to it. + */ + getAdjusted( + block: Block, + atId: string, + delta = 0, + negate = false, + order = Order.NONE, + ): string { + if (block.workspace.options.oneBasedIndex) { + delta--; + } + let defaultAtIndex = block.workspace.options.oneBasedIndex ? '1' : '0'; + + let orderForInput = order; + if (delta > 0) { + orderForInput = Order.ADDITION; + } else if (delta < 0) { + orderForInput = Order.SUBTRACTION; + } else if (negate) { + orderForInput = Order.UNARY_NEGATION; + } + + let at = this.valueToCode(block, atId, orderForInput) || defaultAtIndex; + + // Easy case: no adjustments. + if (delta === 0 && !negate) { + return at; + } + // If the index is a naked number, adjust it right now. + if (stringUtils.isNumber(at)) { + at = String(Number(at) + delta); + if (negate) { + at = String(-Number(at)); + } + return at; + } + // If the index is dynamic, adjust it in code. + if (delta > 0) { + at = `${at} + ${delta}`; + } else if (delta < 0) { + at = `${at} - ${-delta}`; + } + if (negate) { + at = delta ? `-(${at})` : `-${at}`; + } + if (Math.floor(order) >= Math.floor(orderForInput)) { + at = `(${at})`; + } + return at; + } +} diff --git a/generators/php/procedures.js b/generators/php/procedures.ts similarity index 65% rename from generators/php/procedures.js rename to generators/php/procedures.ts index f261ec919..5d44eb21b 100644 --- a/generators/php/procedures.js +++ b/generators/php/procedures.ts @@ -5,17 +5,19 @@ */ /** - * @fileoverview Generating PHP for procedure blocks. + * @file Generating PHP for procedure blocks. */ // Former goog.module ID: Blockly.PHP.procedures import * as Variables from '../../core/variables.js'; +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; import {NameType} from '../../core/names.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn(block: Block, generator: PhpGenerator) { // Define a procedure with a return value. // First, add a 'global' statement for every variable that is not shadowed by // a local parameter. @@ -33,15 +35,14 @@ export function procedures_defreturn(block, generator) { const devVarList = Variables.allDeveloperVariables(workspace); for (let i = 0; i < devVarList.length; i++) { globals.push( - generator.nameDB_.getName( - devVarList[i], NameType.DEVELOPER_VARIABLE)); + generator.nameDB_!.getName(devVarList[i], NameType.DEVELOPER_VARIABLE), + ); } - const globalStr = - globals.length ? - generator.INDENT + 'global ' + globals.join(', ') + ';\n' : ''; + const globalStr = globals.length + ? generator.INDENT + 'global ' + globals.join(', ') + ';\n' + : ''; - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -55,8 +56,9 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId(generator.INFINITE_LOOP_TRAP, block), - generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } const branch = generator.statementToCode(block, 'STACK'); let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; @@ -73,22 +75,37 @@ export function procedures_defreturn(block, generator) { for (let i = 0; i < variables.length; i++) { args[i] = generator.getVariableName(variables[i]); } - let code = 'function ' + funcName + '(' + args.join(', ') + ') {\n' + - globalStr + xfix1 + loopTrap + branch + xfix2 + returnValue + '}'; + let code = + 'function ' + + funcName + + '(' + + args.join(', ') + + ') {\n' + + globalStr + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue + + '}'; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Call a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { @@ -96,30 +113,33 @@ export function procedures_callreturn(block, generator) { } const code = funcName + '(' + args.join(', ') + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn(block: Block, generator: PhpGenerator) { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn']( + block, + generator, + ) as [string, Order]; return tuple[0] + ';\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn(block: Block, generator: PhpGenerator) { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'false'; let code = 'if (' + condition + ') {\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. - code += - generator.prefixLines( - generator.injectId(generator.STATEMENT_SUFFIX, block), - generator.INDENT); + code += generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { + if ((block as IfReturnBlock).hasReturnValue_) { const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'null'; code += generator.INDENT + 'return ' + value + ';\n'; } else { @@ -127,4 +147,4 @@ export function procedures_ifreturn(block, generator) { } code += '}\n'; return code; -}; +} diff --git a/generators/php/text.js b/generators/php/text.ts similarity index 63% rename from generators/php/text.js rename to generators/php/text.ts index b5941315b..f2fa69fae 100644 --- a/generators/php/text.js +++ b/generators/php/text.ts @@ -5,87 +5,104 @@ */ /** - * @fileoverview Generating PHP for text blocks. + * @file Generating PHP for text blocks. */ // Former goog.module ID: Blockly.PHP.texts +import type {Block} from '../../core/block.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; - -export function text(block, generator) { +export function text(block: Block, generator: PhpGenerator): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = - code.indexOf('.') !== -1 ? Order.STRING_CONCAT : Order.ATOMIC; + const order = code.indexOf('.') !== -1 ? Order.STRING_CONCAT : Order.ATOMIC; return [code, order]; -}; +} -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Create a string made up of any number of elements of any type. - if (block.itemCount_ === 0) { + const joinBlock = block as JoinMutatorBlock; + if (joinBlock.itemCount_ === 0) { return ["''", Order.ATOMIC]; - } else if (block.itemCount_ === 1) { + } else if (joinBlock.itemCount_ === 1) { const element = generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; const code = element; return [code, Order.NONE]; - } else if (block.itemCount_ === 2) { + } else if (joinBlock.itemCount_ === 2) { const element0 = - generator.valueToCode(block, 'ADD0', Order.STRING_CONCAT) || "''"; + generator.valueToCode(block, 'ADD0', Order.STRING_CONCAT) || "''"; const element1 = - generator.valueToCode(block, 'ADD1', Order.STRING_CONCAT) || "''"; + generator.valueToCode(block, 'ADD1', Order.STRING_CONCAT) || "''"; const code = element0 + ' . ' + element1; return [code, Order.STRING_CONCAT]; } else { - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; + const elements = new Array(joinBlock.itemCount_); + for (let i = 0; i < joinBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; } - const code = 'implode(\'\', array(' + elements.join(',') + '))'; + const code = "implode('', array(" + elements.join(',') + '))'; return [code, Order.FUNCTION_CALL]; } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: PhpGenerator) { // Append to a variable in place. - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - const value = - generator.valueToCode(block, 'TEXT', Order.ASSIGNMENT) || "''"; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + const value = generator.valueToCode(block, 'TEXT', Order.ASSIGNMENT) || "''"; return varName + ' .= ' + value + ';\n'; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: PhpGenerator, +): [string, Order] { // String or array length. - const functionName = generator.provideFunction_('length', ` + const functionName = generator.provideFunction_( + 'length', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($value) { if (is_string($value)) { return strlen($value); } return count($value); } -`); +`, + ); const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return [functionName + '(' + text + ')', Order.FUNCTION_CALL]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return ['empty(' + text + ')', Order.FUNCTION_CALL]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Search the text for a substring. const operator = - block.getFieldValue('END') === 'FIRST' ? 'strpos' : 'strrpos'; + block.getFieldValue('END') === 'FIRST' ? 'strpos' : 'strrpos'; const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; let errorIndex = ' -1'; @@ -95,22 +112,27 @@ export function text_indexOf(block, generator) { indexAdjustment = ' + 1'; } const functionName = generator.provideFunction_( - block.getFieldValue('END') === 'FIRST' ? 'text_indexOf' : - 'text_lastIndexOf', - ` + block.getFieldValue('END') === 'FIRST' + ? 'text_indexOf' + : 'text_lastIndexOf', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text, $search) { $pos = ${operator}($text, $search); return $pos === false ? ${errorIndex} : $pos${indexAdjustment}; } -`); +`, + ); const code = functionName + '(' + text + ', ' + substring + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Get letter at index. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const textOrder = (where === 'RANDOM') ? Order.NONE : Order.NONE; + const textOrder = where === 'RANDOM' ? Order.NONE : Order.NONE; const text = generator.valueToCode(block, 'VALUE', textOrder) || "''"; switch (where) { case 'FIRST': { @@ -132,19 +154,25 @@ export function text_charAt(block, generator) { return [code, Order.FUNCTION_CALL]; } case 'RANDOM': { - const functionName = generator.provideFunction_('text_random_letter', ` + const functionName = generator.provideFunction_( + 'text_random_letter', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text) { return $text[rand(0, strlen($text) - 1)]; } -`); +`, + ); const code = functionName + '(' + text + ')'; return [code, Order.FUNCTION_CALL]; } } throw Error('Unhandled option (text_charAt).'); -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); @@ -155,7 +183,9 @@ export function text_getSubstring(block, generator) { } else { const at1 = generator.getAdjusted(block, 'AT1'); const at2 = generator.getAdjusted(block, 'AT2'); - const functionName = generator.provideFunction_('text_get_substring', ` + const functionName = generator.provideFunction_( + 'text_get_substring', + ` function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text, $where1, $at1, $where2, $at2) { if ($where1 == 'FROM_END') { $at1 = strlen($text) - 1 - $at1; @@ -176,14 +206,29 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}($text, $where1, $at1, $where2, } return substr($text, $at1, $length); } -`); - const code = functionName + '(' + text + ', \'' + where1 + '\', ' + at1 + - ', \'' + where2 + '\', ' + at2 + ')'; +`, + ); + const code = + functionName + + '(' + + text + + ", '" + + where1 + + "', " + + at1 + + ", '" + + where2 + + "', " + + at2 + + ')'; return [code, Order.FUNCTION_CALL]; } -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Change capitalization. const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; let code; @@ -194,24 +239,31 @@ export function text_changeCase(block, generator) { } else if (block.getFieldValue('CASE') === 'TITLECASE') { code = 'ucwords(strtolower(' + text + '))'; } - return [code, Order.FUNCTION_CALL]; -}; + return [code as string, Order.FUNCTION_CALL]; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = {'LEFT': 'ltrim', 'RIGHT': 'rtrim', 'BOTH': 'trim'}; - const operator = OPERATORS[block.getFieldValue('MODE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return [operator + '(' + text + ')', Order.FUNCTION_CALL]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: PhpGenerator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ');\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: PhpGenerator, +): [string, Order] { // Prompt function. let msg; if (block.getField('TEXT')) { @@ -227,29 +279,47 @@ export function text_prompt_ext(block, generator) { code = 'floatval(' + code + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { +export function text_count( + block: Block, + generator: PhpGenerator, +): [string, Order] { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const sub = generator.valueToCode(block, 'SUB', Order.NONE) || "''"; - const code = 'strlen(' + sub + ') === 0' + - ' ? strlen(' + text + ') + 1' + - ' : substr_count(' + text + ', ' + sub + ')'; + const code = + 'strlen(' + + sub + + ') === 0' + + ' ? strlen(' + + text + + ') + 1' + + ' : substr_count(' + + text + + ', ' + + sub + + ')'; return [code, Order.CONDITIONAL]; -}; +} -export function text_replace(block, generator) { +export function text_replace( + block: Block, + generator: PhpGenerator, +): [string, Order] { const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; const code = 'str_replace(' + from + ', ' + to + ', ' + text + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_reverse(block, generator) { +export function text_reverse( + block: Block, + generator: PhpGenerator, +): [string, Order] { 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 deleted file mode 100644 index 50429fa9b..000000000 --- a/generators/php/variables.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2015 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating PHP for variable blocks. - */ - -// Former goog.module ID: Blockly.PHP.variables - -import {Order} from './php_generator.js'; - - -export function variables_get(block, generator) { - // Variable getter. - const code = - generator.getVariableName(block.getFieldValue('VAR')); - return [code, Order.ATOMIC]; -}; - -export function variables_set(block, generator) { - // Variable setter. - const argument0 = - generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = ' + argument0 + ';\n'; -}; diff --git a/generators/php/variables.ts b/generators/php/variables.ts new file mode 100644 index 000000000..4aacc293b --- /dev/null +++ b/generators/php/variables.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2015 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating PHP for variable blocks. + */ + +// Former goog.module ID: Blockly.PHP.variables + +import type {Block} from '../../core/block.js'; +import {Order} from './php_generator.js'; +import type {PhpGenerator} from './php_generator.js'; + +export function variables_get( + block: Block, + generator: PhpGenerator, +): [string, Order] { + // Variable getter. + const code = generator.getVariableName(block.getFieldValue('VAR')); + return [code, Order.ATOMIC]; +} + +export function variables_set(block: Block, generator: PhpGenerator) { + // Variable setter. + const argument0 = + generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + return varName + ' = ' + argument0 + ';\n'; +} diff --git a/generators/php/variables_dynamic.js b/generators/php/variables_dynamic.ts similarity index 83% rename from generators/php/variables_dynamic.js rename to generators/php/variables_dynamic.ts index 1f4a3c100..5ab5ec6ca 100644 --- a/generators/php/variables_dynamic.js +++ b/generators/php/variables_dynamic.ts @@ -5,12 +5,11 @@ */ /** - * @fileoverview Generating PHP for dynamic variable blocks. + * @file Generating PHP for dynamic variable blocks. */ // Former goog.module ID: Blockly.PHP.variablesDynamic - // generator is dynamically typed. export { variables_get as variables_get_dynamic, diff --git a/generators/python.js b/generators/python.ts similarity index 67% rename from generators/python.js rename to generators/python.ts index 938da3b7e..08ab10e81 100644 --- a/generators/python.js +++ b/generators/python.ts @@ -5,9 +5,9 @@ */ /** - * @fileoverview Complete helper functions for generating Python for - * blocks. This is the entrypoint for python_compressed.js. - * @suppress {extraRequire} + * @file Instantiate a PythonGenerator and populate it with the + * complete set of block generator functions for Python. This is the + * entrypoint for python_compressed.js. */ // Former goog.module ID: Blockly.Python.all @@ -36,8 +36,18 @@ export const pythonGenerator = new PythonGenerator(); pythonGenerator.addReservedWords('math,random,Number'); // Install per-block-type generator functions: -Object.assign( - pythonGenerator.forBlock, - colour, lists, logic, loops, math, procedures, - text, variables, variablesDynamic -); +// Install per-block-type generator functions: +const generators: typeof pythonGenerator.forBlock = { + ...colour, + ...lists, + ...logic, + ...loops, + ...math, + ...procedures, + ...text, + ...variables, + ...variablesDynamic, +}; +for (const name in generators) { + pythonGenerator.forBlock[name] = generators[name]; +} diff --git a/generators/python/colour.js b/generators/python/colour.ts similarity index 56% rename from generators/python/colour.js rename to generators/python/colour.ts index 36cb13c97..729d87cf5 100644 --- a/generators/python/colour.js +++ b/generators/python/colour.ts @@ -5,46 +5,67 @@ */ /** - * @fileoverview Generating Python for colour blocks. + * @file Generating Python for colour blocks. */ // Former goog.module ID: Blockly.Python.colour +import type {Block} from '../../core/block.js'; +import type {PythonGenerator} from './python_generator.js'; import {Order} from './python_generator.js'; - -export function colour_picker(block, generator) { +export function colour_picker( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Colour picker. const code = generator.quote_(block.getFieldValue('COLOUR')); return [code, Order.ATOMIC]; -}; +} -export function colour_random(block, generator) { +export function colour_random( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Generate a random colour. - generator.definitions_['import_random'] = 'import random'; - const code = '\'#%06x\' % random.randint(0, 2**24 - 1)'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; + const code = "'#%06x' % random.randint(0, 2**24 - 1)"; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_rgb(block, generator) { +export function colour_rgb( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Compose a colour from RGB components expressed as percentages. - const functionName = generator.provideFunction_('colour_rgb', ` + const functionName = generator.provideFunction_( + 'colour_rgb', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b): r = round(min(100, max(0, r)) * 2.55) g = round(min(100, max(0, g)) * 2.55) b = round(min(100, max(0, b)) * 2.55) return '#%02x%02x%02x' % (r, g, b) -`); +`, + ); const r = generator.valueToCode(block, 'RED', Order.NONE) || 0; const g = generator.valueToCode(block, 'GREEN', Order.NONE) || 0; const b = generator.valueToCode(block, 'BLUE', Order.NONE) || 0; const code = functionName + '(' + r + ', ' + g + ', ' + b + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function colour_blend(block, generator) { +export function colour_blend( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Blend two colours together. - const functionName = generator.provideFunction_('colour_blend', ` + const functionName = generator.provideFunction_( + 'colour_blend', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio): r1, r2 = int(colour1[1:3], 16), int(colour2[1:3], 16) g1, g2 = int(colour1[3:5], 16), int(colour2[3:5], 16) @@ -54,15 +75,14 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio): g = round(g1 * (1 - ratio) + g2 * ratio) b = round(b1 * (1 - ratio) + b2 * ratio) return '#%02x%02x%02x' % (r, g, b) -`); +`, + ); const colour1 = - generator.valueToCode(block, 'COLOUR1', Order.NONE) - || '\'#000000\''; + generator.valueToCode(block, 'COLOUR1', Order.NONE) || "'#000000'"; const colour2 = - generator.valueToCode(block, 'COLOUR2', Order.NONE) - || '\'#000000\''; + generator.valueToCode(block, 'COLOUR2', Order.NONE) || "'#000000'"; const ratio = generator.valueToCode(block, 'RATIO', Order.NONE) || 0; const code = - functionName + '(' + colour1 + ', ' + colour2 + ', ' + ratio + ')'; + functionName + '(' + colour1 + ', ' + colour2 + ', ' + ratio + ')'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/python/lists.js b/generators/python/lists.ts similarity index 75% rename from generators/python/lists.js rename to generators/python/lists.ts index 9f270b78e..6e412c23f 100644 --- a/generators/python/lists.js +++ b/generators/python/lists.ts @@ -5,55 +5,75 @@ */ /** - * @fileoverview Generating Python for list blocks. + * @file Generating Python for list blocks. */ // Former goog.module ID: Blockly.Python.lists import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {CreateWithBlock} from '../../blocks/lists.js'; import {NameType} from '../../core/names.js'; import {Order} from './python_generator.js'; +import type {PythonGenerator} from './python_generator.js'; - -export function lists_create_empty(block, generator) { +export function lists_create_empty( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Create an empty list. return ['[]', Order.ATOMIC]; -}; +} -export function lists_create_with(block, generator) { +export function lists_create_with( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Create a list with any number of elements of any type. - const elements = new Array(block.itemCount_); - for (let i = 0; i < block.itemCount_; i++) { - elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'None'; + const createWithBlock = block as CreateWithBlock; + const elements = new Array(createWithBlock.itemCount_); + for (let i = 0; i < createWithBlock.itemCount_; i++) { + elements[i] = generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'None'; } const code = '[' + elements.join(', ') + ']'; return [code, Order.ATOMIC]; -}; +} -export function lists_repeat(block, generator) { +export function lists_repeat( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Create a list with one element repeated. const item = generator.valueToCode(block, 'ITEM', Order.NONE) || 'None'; const times = - generator.valueToCode(block, 'NUM', Order.MULTIPLICATIVE) || '0'; + generator.valueToCode(block, 'NUM', Order.MULTIPLICATIVE) || '0'; const code = '[' + item + '] * ' + times; return [code, Order.MULTIPLICATIVE]; -}; +} -export function lists_length(block, generator) { +export function lists_length( + block: Block, + generator: PythonGenerator, +): [string, Order] { // String or array length. const list = generator.valueToCode(block, 'VALUE', Order.NONE) || '[]'; return ['len(' + list + ')', Order.FUNCTION_CALL]; -}; +} -export function lists_isEmpty(block, generator) { +export function lists_isEmpty( + block: Block, + generator: PythonGenerator, +): [string, Order] { // 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]; -}; +} -export function lists_indexOf(block, generator) { +export function lists_indexOf( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Find an item in the list. const item = generator.valueToCode(block, 'FIND', Order.NONE) || '[]'; const list = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; @@ -69,31 +89,39 @@ export function lists_indexOf(block, generator) { let functionName; if (block.getFieldValue('END') === 'FIRST') { - functionName = generator.provideFunction_('first_index', ` + functionName = generator.provideFunction_( + 'first_index', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, elem): try: index = my_list.index(elem)${firstIndexAdjustment} except: index =${errorIndex} return index -`); +`, + ); } else { - functionName = generator.provideFunction_('last_index', ` + functionName = generator.provideFunction_( + 'last_index', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, elem): try: index = len(my_list) - my_list[::-1].index(elem)${lastIndexAdjustment} except: index =${errorIndex} return index -`); +`, + ); } const code = functionName + '(' + list + ', ' + item + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_getIndex(block, generator) { +export function lists_getIndex( + block: Block, + generator: PythonGenerator, +): [string, Order] | string { // Get element at index. // Note: Until January 2013 this block did not have MODE or WHERE inputs. const mode = block.getFieldValue('MODE') || 'GET'; const where = block.getFieldValue('WHERE') || 'FROM_START'; - const listOrder = - (where === 'RANDOM') ? Order.NONE : Order.MEMBER; + const listOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; const list = generator.valueToCode(block, 'VALUE', listOrder) || '[]'; switch (where) { @@ -146,17 +174,20 @@ export function lists_getIndex(block, generator) { break; } case 'RANDOM': - generator.definitions_['import_random'] = 'import random'; + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; if (mode === 'GET') { const code = 'random.choice(' + list + ')'; return [code, Order.FUNCTION_CALL]; } else { - const functionName = - generator.provideFunction_('lists_remove_random_item', ` + const functionName = generator.provideFunction_( + 'lists_remove_random_item', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): x = int(random.random() * len(myList)) return myList.pop(x) -`); +`, + ); const code = functionName + '(' + list + ')'; if (mode === 'GET_REMOVE') { return [code, Order.FUNCTION_CALL]; @@ -167,9 +198,9 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): break; } throw Error('Unhandled combination (lists_getIndex).'); -}; +} -export function lists_setIndex(block, generator) { +export function lists_setIndex(block: Block, generator: PythonGenerator) { // 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) || '[]'; @@ -182,8 +213,10 @@ export function lists_setIndex(block, generator) { if (list.match(/^\w+$/)) { return ''; } - const listVar = - generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE); + const listVar = generator.nameDB_!.getDistinctName( + 'tmp_list', + NameType.VARIABLE, + ); const code = listVar + ' = ' + list + '\n'; list = listVar; return code; @@ -223,10 +256,13 @@ export function lists_setIndex(block, generator) { break; } case 'RANDOM': { - generator.definitions_['import_random'] = 'import random'; + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; let code = cacheList(); - const xVar = - generator.nameDB_.getDistinctName('tmp_x', NameType.VARIABLE); + const xVar = generator.nameDB_!.getDistinctName( + 'tmp_x', + NameType.VARIABLE, + ); code += xVar + ' = int(random.random() * len(' + list + '))\n'; if (mode === 'SET') { code += list + '[' + xVar + '] = ' + value + '\n'; @@ -239,9 +275,12 @@ export function lists_setIndex(block, generator) { } } throw Error('Unhandled combination (lists_setIndex).'); -}; +} -export function lists_getSublist(block, generator) { +export function lists_getSublist( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Get sublist. const list = generator.valueToCode(block, 'LIST', Order.MEMBER) || '[]'; const where1 = block.getFieldValue('WHERE1'); @@ -274,7 +313,8 @@ export function lists_getSublist(block, generator) { // Ensure that if the result calculated is 0 that sub-sequence will // include all elements as expected. if (!stringUtils.isNumber(String(at2))) { - generator.definitions_['import_sys'] = 'import sys'; + (generator as AnyDuringMigration).definitions_['import_sys'] = + 'import sys'; at2 += ' or sys.maxsize'; } else if (at2 === 0) { at2 = ''; @@ -288,14 +328,19 @@ export function lists_getSublist(block, generator) { } const code = list + '[' + at1 + ' : ' + at2 + ']'; return [code, Order.MEMBER]; -}; +} -export function lists_sort(block, generator) { +export function lists_sort( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Block for sorting a list. - const list = (generator.valueToCode(block, 'LIST', Order.NONE) || '[]'); + const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const type = block.getFieldValue('TYPE'); const reverse = block.getFieldValue('DIRECTION') === '1' ? 'False' : 'True'; - const sortFunctionName = generator.provideFunction_('lists_sort', ` + const sortFunctionName = generator.provideFunction_( + 'lists_sort', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, type, reverse): def try_float(s): try: @@ -310,37 +355,44 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(my_list, type, reverse): key_func = key_funcs[type] list_cpy = list(my_list) return sorted(list_cpy, key=key_func, reverse=reverse) -`); +`, + ); const code = - sortFunctionName + '(' + list + ', "' + type + '", ' + reverse + ')'; + sortFunctionName + '(' + list + ', "' + type + '", ' + reverse + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_split(block, generator) { +export function lists_split( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Block for splitting text into a list, or joining a list into text. const mode = block.getFieldValue('MODE'); let code; if (mode === 'SPLIT') { const value_input = - generator.valueToCode(block, 'INPUT', Order.MEMBER) || "''"; + generator.valueToCode(block, 'INPUT', Order.MEMBER) || "''"; const value_delim = generator.valueToCode(block, 'DELIM', Order.NONE); code = value_input + '.split(' + value_delim + ')'; } else if (mode === 'JOIN') { const value_input = - generator.valueToCode(block, 'INPUT', Order.NONE) || '[]'; + generator.valueToCode(block, 'INPUT', Order.NONE) || '[]'; const value_delim = - generator.valueToCode(block, 'DELIM', Order.MEMBER) || "''"; + generator.valueToCode(block, 'DELIM', Order.MEMBER) || "''"; code = value_delim + '.join(' + value_input + ')'; } else { throw Error('Unknown mode: ' + mode); } return [code, Order.FUNCTION_CALL]; -}; +} -export function lists_reverse(block, generator) { +export function lists_reverse( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Block for reversing a list. const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; const code = 'list(reversed(' + list + '))'; return [code, Order.FUNCTION_CALL]; -}; +} diff --git a/generators/python/logic.js b/generators/python/logic.js deleted file mode 100644 index 33729b860..000000000 --- a/generators/python/logic.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating Python for logic blocks. - */ - -// Former goog.module ID: Blockly.Python.logic - -import {Order} from './python_generator.js'; - - -export function controls_if(block, generator) { - // If/elseif/else condition. - let n = 0; - let code = '', branchCode, conditionCode; - if (generator.STATEMENT_PREFIX) { - // Automatic prefix insertion is switched off for this block. Add manually. - code += generator.injectId(generator.STATEMENT_PREFIX, block); - } - do { - conditionCode = - generator.valueToCode(block, 'IF' + n, Order.NONE) || 'False'; - branchCode = - generator.statementToCode(block, 'DO' + n) || - generator.PASS; - if (generator.STATEMENT_SUFFIX) { - branchCode = - generator.prefixLines( - generator.injectId(generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += (n === 0 ? 'if ' : 'elif ') + conditionCode + ':\n' + branchCode; - n++; - } while (block.getInput('IF' + n)); - - if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { - branchCode = - generator.statementToCode(block, 'ELSE') || generator.PASS; - if (generator.STATEMENT_SUFFIX) { - branchCode = - generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), - generator.INDENT) + - branchCode; - } - code += 'else:\n' + branchCode; - } - return code; -}; - -export const controls_ifelse = controls_if; - -export function logic_compare(block, generator) { - // Comparison operator. - const OPERATORS = - {'EQ': '==', 'NEQ': '!=', 'LT': '<', 'LTE': '<=', 'GT': '>', 'GTE': '>='}; - const operator = OPERATORS[block.getFieldValue('OP')]; - const order = Order.RELATIONAL; - const argument0 = generator.valueToCode(block, 'A', order) || '0'; - const argument1 = generator.valueToCode(block, 'B', order) || '0'; - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_operation(block, generator) { - // Operations 'and', 'or'. - const operator = (block.getFieldValue('OP') === 'AND') ? 'and' : 'or'; - const order = - (operator === 'and') ? Order.LOGICAL_AND : Order.LOGICAL_OR; - let argument0 = generator.valueToCode(block, 'A', order); - let argument1 = generator.valueToCode(block, 'B', order); - if (!argument0 && !argument1) { - // If there are no arguments, then the return value is false. - argument0 = 'False'; - argument1 = 'False'; - } else { - // Single missing arguments have no effect on the return value. - const defaultArgument = (operator === 'and') ? 'True' : 'False'; - if (!argument0) { - argument0 = defaultArgument; - } - if (!argument1) { - argument1 = defaultArgument; - } - } - const code = argument0 + ' ' + operator + ' ' + argument1; - return [code, order]; -}; - -export function logic_negate(block, generator) { - // Negation. - const argument0 = - generator.valueToCode(block, 'BOOL', Order.LOGICAL_NOT) || 'True'; - const code = 'not ' + argument0; - return [code, Order.LOGICAL_NOT]; -}; - -export function logic_boolean(block, generator) { - // Boolean values true and false. - const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'True' : 'False'; - return [code, Order.ATOMIC]; -}; - -export function logic_null(block, generator) { - // Null data type. - return ['None', Order.ATOMIC]; -}; - -export function logic_ternary(block, generator) { - // Ternary operator. - const value_if = - generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'False'; - const value_then = - generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'None'; - const value_else = - generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'None'; - const code = value_then + ' if ' + value_if + ' else ' + value_else; - return [code, Order.CONDITIONAL]; -}; diff --git a/generators/python/logic.ts b/generators/python/logic.ts new file mode 100644 index 000000000..40133600d --- /dev/null +++ b/generators/python/logic.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating Python for logic blocks. + */ + +// Former goog.module ID: Blockly.Python.logic + +import type {Block} from '../../core/block.js'; +import type {PythonGenerator} from './python_generator.js'; +import {Order} from './python_generator.js'; + +export function controls_if(block: Block, generator: PythonGenerator) { + // If/elseif/else condition. + let n = 0; + let code = '', + branchCode, + conditionCode; + if (generator.STATEMENT_PREFIX) { + // Automatic prefix insertion is switched off for this block. Add manually. + code += generator.injectId(generator.STATEMENT_PREFIX, block); + } + do { + conditionCode = + generator.valueToCode(block, 'IF' + n, Order.NONE) || 'False'; + branchCode = generator.statementToCode(block, 'DO' + n) || generator.PASS; + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += (n === 0 ? 'if ' : 'elif ') + conditionCode + ':\n' + branchCode; + n++; + } while (block.getInput('IF' + n)); + + if (block.getInput('ELSE') || generator.STATEMENT_SUFFIX) { + branchCode = generator.statementToCode(block, 'ELSE') || generator.PASS; + if (generator.STATEMENT_SUFFIX) { + branchCode = + generator.prefixLines( + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ) + branchCode; + } + code += 'else:\n' + branchCode; + } + return code; +} + +export const controls_ifelse = controls_if; + +export function logic_compare( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Comparison operator. + const OPERATORS = { + 'EQ': '==', + 'NEQ': '!=', + 'LT': '<', + 'LTE': '<=', + 'GT': '>', + 'GTE': '>=', + }; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('OP') as OperatorOption]; + const order = Order.RELATIONAL; + const argument0 = generator.valueToCode(block, 'A', order) || '0'; + const argument1 = generator.valueToCode(block, 'B', order) || '0'; + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_operation( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Operations 'and', 'or'. + const operator = block.getFieldValue('OP') === 'AND' ? 'and' : 'or'; + const order = operator === 'and' ? Order.LOGICAL_AND : Order.LOGICAL_OR; + let argument0 = generator.valueToCode(block, 'A', order); + let argument1 = generator.valueToCode(block, 'B', order); + if (!argument0 && !argument1) { + // If there are no arguments, then the return value is false. + argument0 = 'False'; + argument1 = 'False'; + } else { + // Single missing arguments have no effect on the return value. + const defaultArgument = operator === 'and' ? 'True' : 'False'; + if (!argument0) { + argument0 = defaultArgument; + } + if (!argument1) { + argument1 = defaultArgument; + } + } + const code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +} + +export function logic_negate( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Negation. + const argument0 = + generator.valueToCode(block, 'BOOL', Order.LOGICAL_NOT) || 'True'; + const code = 'not ' + argument0; + return [code, Order.LOGICAL_NOT]; +} + +export function logic_boolean( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Boolean values true and false. + const code = block.getFieldValue('BOOL') === 'TRUE' ? 'True' : 'False'; + return [code, Order.ATOMIC]; +} + +export function logic_null( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Null data type. + return ['None', Order.ATOMIC]; +} + +export function logic_ternary( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Ternary operator. + const value_if = + generator.valueToCode(block, 'IF', Order.CONDITIONAL) || 'False'; + const value_then = + generator.valueToCode(block, 'THEN', Order.CONDITIONAL) || 'None'; + const value_else = + generator.valueToCode(block, 'ELSE', Order.CONDITIONAL) || 'None'; + const code = value_then + ' if ' + value_if + ' else ' + value_else; + return [code, Order.CONDITIONAL]; +} diff --git a/generators/python/loops.js b/generators/python/loops.ts similarity index 68% rename from generators/python/loops.js rename to generators/python/loops.ts index 824d4f2bf..7ca7a5582 100644 --- a/generators/python/loops.js +++ b/generators/python/loops.ts @@ -5,17 +5,19 @@ */ /** - * @fileoverview Generating Python for loop blocks. + * @file Generating Python for loop blocks. */ // Former goog.module ID: Blockly.Python.loops import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {ControlFlowInLoopBlock} from '../../blocks/loops.js'; +import type {PythonGenerator} from './python_generator.js'; import {NameType} from '../../core/names.js'; import {Order} from './python_generator.js'; - -export function controls_repeat_ext(block, generator) { +export function controls_repeat_ext(block: Block, generator: PythonGenerator) { // Repeat n times. let repeats; if (block.getField('TIMES')) { @@ -32,36 +34,42 @@ export function controls_repeat_ext(block, generator) { } let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block) || generator.PASS; - const loopVar = - generator.nameDB_.getDistinctName('count', NameType.VARIABLE); + const loopVar = generator.nameDB_!.getDistinctName( + 'count', + NameType.VARIABLE, + ); const code = 'for ' + loopVar + ' in range(' + repeats + '):\n' + branch; return code; -}; +} export const controls_repeat = controls_repeat_ext; -export function controls_whileUntil(block, generator) { +export function controls_whileUntil(block: Block, generator: PythonGenerator) { // Do while/until loop. const until = block.getFieldValue('MODE') === 'UNTIL'; - let argument0 = generator.valueToCode( - block, 'BOOL', - until ? Order.LOGICAL_NOT : Order.NONE) || - 'False'; + let argument0 = + generator.valueToCode( + block, + 'BOOL', + until ? Order.LOGICAL_NOT : Order.NONE, + ) || 'False'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block) || generator.PASS; if (until) { argument0 = 'not ' + argument0; } return 'while ' + argument0 + ':\n' + branch; -}; +} -export function controls_for(block, generator) { +export function controls_for(block: Block, generator: PythonGenerator) { // For loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); - let argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; - let argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; - let increment = generator.valueToCode(block, 'BY', Order.NONE) || '1'; + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); + let argument0: string | number = + generator.valueToCode(block, 'FROM', Order.NONE) || '0'; + let argument1: string | number = + generator.valueToCode(block, 'TO', Order.NONE) || '0'; + let increment: string | number = + generator.valueToCode(block, 'BY', Order.NONE) || '1'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block) || generator.PASS; @@ -69,31 +77,64 @@ export function controls_for(block, generator) { let range; // Helper functions. - const defineUpRange = function() { - return generator.provideFunction_('upRange', ` + const defineUpRange = function () { + return generator.provideFunction_( + 'upRange', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(start, stop, step): while start <= stop: yield start start += abs(step) -`); +`, + ); }; - const defineDownRange = function() { - return generator.provideFunction_('downRange', ` + const defineDownRange = function () { + return generator.provideFunction_( + 'downRange', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(start, stop, step): while start >= stop: yield start start -= abs(step) -`); +`, + ); }; // Arguments are legal generator code (numbers or strings returned by scrub()). - const generateUpDownRange = function(start, end, inc) { - return '(' + start + ' <= ' + end + ') and ' + defineUpRange() + '(' + - start + ', ' + end + ', ' + inc + ') or ' + defineDownRange() + '(' + - start + ', ' + end + ', ' + inc + ')'; + const generateUpDownRange = function ( + start: string, + end: string, + inc: string, + ) { + return ( + '(' + + start + + ' <= ' + + end + + ') and ' + + defineUpRange() + + '(' + + start + + ', ' + + end + + ', ' + + inc + + ') or ' + + defineDownRange() + + '(' + + start + + ', ' + + end + + ', ' + + inc + + ')' + ); }; - if (stringUtils.isNumber(argument0) && stringUtils.isNumber(argument1) && - stringUtils.isNumber(increment)) { + if ( + stringUtils.isNumber(argument0) && + stringUtils.isNumber(argument1) && + stringUtils.isNumber(increment) + ) { // All parameters are simple numbers. argument0 = Number(argument0); argument1 = Number(argument1); @@ -130,14 +171,16 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(start, stop, step): } } else { // Cache non-trivial values to variables to prevent repeated look-ups. - const scrub = function(arg, suffix) { + const scrub = function (arg: string, suffix: string) { if (stringUtils.isNumber(arg)) { // Simple number. - arg = Number(arg); + arg = String(Number(arg)); } else if (!arg.match(/^\w+$/)) { // Not a variable, it's complicated. - const varName = generator.nameDB_.getDistinctName( - variable0 + suffix, NameType.VARIABLE); + const varName = generator.nameDB_!.getDistinctName( + variable0 + suffix, + NameType.VARIABLE, + ); code += varName + ' = ' + arg + '\n'; arg = varName; } @@ -161,21 +204,23 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(start, stop, step): } code += 'for ' + variable0 + ' in ' + range + ':\n' + branch; return code; -}; +} -export function controls_forEach(block, generator) { +export function controls_forEach(block: Block, generator: PythonGenerator) { // For each loop. - const variable0 = - generator.getVariableName(block.getFieldValue('VAR')); + const variable0 = generator.getVariableName(block.getFieldValue('VAR')); const argument0 = - generator.valueToCode(block, 'LIST', Order.RELATIONAL) || '[]'; + generator.valueToCode(block, 'LIST', Order.RELATIONAL) || '[]'; let branch = generator.statementToCode(block, 'DO'); branch = generator.addLoopTrap(branch, block) || generator.PASS; const code = 'for ' + variable0 + ' in ' + argument0 + ':\n' + branch; return code; -}; +} -export function controls_flow_statements(block, generator) { +export function controls_flow_statements( + block: Block, + generator: PythonGenerator, +) { // Flow statements: continue, break. let xfix = ''; if (generator.STATEMENT_PREFIX) { @@ -188,7 +233,7 @@ export function controls_flow_statements(block, generator) { xfix += generator.injectId(generator.STATEMENT_SUFFIX, block); } if (generator.STATEMENT_PREFIX) { - const loop = block.getSurroundLoop(); + const loop = (block as ControlFlowInLoopBlock).getSurroundLoop(); if (loop && !loop.suppressPrefixSuffix) { // Inject loop's statement prefix here since the regular one at the end // of the loop will not get executed if 'continue' is triggered. @@ -203,4 +248,4 @@ export function controls_flow_statements(block, generator) { return xfix + 'continue\n'; } throw Error('Unknown flow statement.'); -}; +} diff --git a/generators/python/math.js b/generators/python/math.ts similarity index 62% rename from generators/python/math.js rename to generators/python/math.ts index 0ba351187..59b68c7a7 100644 --- a/generators/python/math.js +++ b/generators/python/math.ts @@ -5,43 +5,47 @@ */ /** - * @fileoverview Generating Python for math blocks. + * @file Generating Python for math blocks. */ // Former goog.module ID: Blockly.Python.math +import type {Block} from '../../core/block.js'; +import type {PythonGenerator} from './python_generator.js'; import {Order} from './python_generator.js'; - // If any new block imports any library, add that library name here. // RESERVED WORDS: 'math,random,Number' -export function math_number(block, generator) { +export function math_number( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Numeric value. - let code = Number(block.getFieldValue('NUM')); - let order; - if (code === Infinity) { - code = 'float("inf")'; - order = Order.FUNCTION_CALL; - } else if (code === -Infinity) { - code = '-float("inf")'; - order = Order.UNARY_SIGN; + let number = Number(block.getFieldValue('NUM')); + if (number === Infinity) { + return ['float("inf")', Order.FUNCTION_CALL]; + } else if (number === -Infinity) { + return ['-float("inf")', Order.UNARY_SIGN]; } else { - order = code < 0 ? Order.UNARY_SIGN : Order.ATOMIC; + return [String(number), number < 0 ? Order.UNARY_SIGN : Order.ATOMIC]; } - return [code, order]; -}; +} -export function math_arithmetic(block, generator) { +export function math_arithmetic( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Basic arithmetic operators, and power. - const OPERATORS = { + const OPERATORS: Record = { 'ADD': [' + ', Order.ADDITIVE], 'MINUS': [' - ', Order.ADDITIVE], 'MULTIPLY': [' * ', Order.MULTIPLICATIVE], 'DIVIDE': [' / ', Order.MULTIPLICATIVE], 'POWER': [' ** ', Order.EXPONENTIATION], }; - const tuple = OPERATORS[block.getFieldValue('OP')]; + type OperatorOption = keyof typeof OPERATORS; + const tuple = OPERATORS[block.getFieldValue('OP') as OperatorOption]; const operator = tuple[0]; const order = tuple[1]; const argument0 = generator.valueToCode(block, 'A', order) || '0'; @@ -53,9 +57,12 @@ export function math_arithmetic(block, generator) { // guarantee identical results in all languages. To do otherwise would // require every operator to be wrapped in a function call. This would kill // legibility of the generated code. -}; +} -export function math_single(block, generator) { +export function math_single( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Math operators with single operand. const operator = block.getFieldValue('OP'); let code; @@ -65,10 +72,11 @@ export function math_single(block, generator) { code = generator.valueToCode(block, 'NUM', Order.UNARY_SIGN) || '0'; return ['-' + code, Order.UNARY_SIGN]; } - generator.definitions_['import_math'] = 'import math'; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected (here and below). + (generator as AnyDuringMigration).definitions_['import_math'] = 'import math'; if (operator === 'SIN' || operator === 'COS' || operator === 'TAN') { - arg = - generator.valueToCode(block, 'NUM', Order.MULTIPLICATIVE) || '0'; + arg = generator.valueToCode(block, 'NUM', Order.MULTIPLICATIVE) || '0'; } else { arg = generator.valueToCode(block, 'NUM', Order.NONE) || '0'; } @@ -131,50 +139,61 @@ export function math_single(block, generator) { throw Error('Unknown math operator: ' + operator); } return [code, Order.MULTIPLICATIVE]; -}; +} -export function math_constant(block, generator) { +export function math_constant( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Constants: PI, E, the Golden Ratio, sqrt(2), 1/sqrt(2), INFINITY. - const CONSTANTS = { + const CONSTANTS: Record = { 'PI': ['math.pi', Order.MEMBER], 'E': ['math.e', Order.MEMBER], 'GOLDEN_RATIO': ['(1 + math.sqrt(5)) / 2', Order.MULTIPLICATIVE], 'SQRT2': ['math.sqrt(2)', Order.MEMBER], 'SQRT1_2': ['math.sqrt(1.0 / 2)', Order.MEMBER], - 'INFINITY': ['float(\'inf\')', Order.ATOMIC], + 'INFINITY': ["float('inf')", Order.ATOMIC], }; - const constant = block.getFieldValue('CONSTANT'); + type ConstantOption = keyof typeof CONSTANTS; + const constant = block.getFieldValue('CONSTANT') as ConstantOption; if (constant !== 'INFINITY') { - generator.definitions_['import_math'] = 'import math'; + (generator as AnyDuringMigration).definitions_['import_math'] = + 'import math'; } return CONSTANTS[constant]; -}; +} -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 = { +export function math_number_property( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Check if a number is even, odd, prime, whole, positive, or negative + // or if it is divisible by certain number. Returns true or false. + const PROPERTIES: Record = { 'EVEN': [' % 2 == 0', Order.MULTIPLICATIVE, Order.RELATIONAL], 'ODD': [' % 2 == 1', Order.MULTIPLICATIVE, Order.RELATIONAL], - 'WHOLE': [' % 1 == 0', Order.MULTIPLICATIVE, - Order.RELATIONAL], + 'WHOLE': [' % 1 == 0', Order.MULTIPLICATIVE, Order.RELATIONAL], 'POSITIVE': [' > 0', Order.RELATIONAL, Order.RELATIONAL], 'NEGATIVE': [' < 0', Order.RELATIONAL, Order.RELATIONAL], - 'DIVISIBLE_BY': [null, Order.MULTIPLICATIVE, - Order.RELATIONAL], + 'DIVISIBLE_BY': [null, Order.MULTIPLICATIVE, Order.RELATIONAL], 'PRIME': [null, Order.NONE, Order.FUNCTION_CALL], - } - const dropdownProperty = block.getFieldValue('PROPERTY'); + }; + type PropertyOption = keyof typeof PROPERTIES; + const dropdownProperty = block.getFieldValue('PROPERTY') as PropertyOption; const [suffix, inputOrder, outputOrder] = PROPERTIES[dropdownProperty]; - const numberToCheck = generator.valueToCode(block, 'NUMBER_TO_CHECK', - inputOrder) || '0'; + const numberToCheck = + generator.valueToCode(block, 'NUMBER_TO_CHECK', inputOrder) || '0'; let code; if (dropdownProperty === 'PRIME') { // Prime is a special case as it is not a one-liner test. - generator.definitions_['import_math'] = 'import math'; - generator.definitions_['from_numbers_import_Number'] = - 'from numbers import Number'; - const functionName = generator.provideFunction_('math_isPrime', ` + (generator as AnyDuringMigration).definitions_['import_math'] = + 'import math'; + (generator as AnyDuringMigration).definitions_[ + 'from_numbers_import_Number' + ] = 'from numbers import Number'; + const functionName = generator.provideFunction_( + 'math_isPrime', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(n): # https://en.wikipedia.org/wiki/Primality_test#Naive_methods # If n is not a number but a string, try parsing it. @@ -193,11 +212,12 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(n): if n % (x - 1) == 0 or n % (x + 1) == 0: return False return True -`); - code = functionName + '(' + numberToCheck + ')'; +`, + ); + code = functionName + '(' + numberToCheck + ')'; } else if (dropdownProperty === 'DIVISIBLE_BY') { - const divisor = generator.valueToCode(block, 'DIVISOR', - Order.MULTIPLICATIVE) || '0'; + const divisor = + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; // If 'divisor' is some code that evals to 0, generator will raise an error. if (divisor === '0') { return ['False', Order.ATOMIC]; @@ -205,27 +225,38 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(n): code = numberToCheck + ' % ' + divisor + ' == 0'; } else { code = numberToCheck + suffix; - }; + } return [code, outputOrder]; -}; +} -export function math_change(block, generator) { +export function math_change(block: Block, generator: PythonGenerator) { // Add to a variable in place. - generator.definitions_['from_numbers_import_Number'] = - 'from numbers import Number'; + (generator as AnyDuringMigration).definitions_['from_numbers_import_Number'] = + 'from numbers import Number'; const argument0 = - generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; + generator.valueToCode(block, 'DELTA', Order.ADDITIVE) || '0'; const varName = generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = (' + varName + ' if isinstance(' + varName + - ', Number) else 0) + ' + argument0 + '\n'; -}; + return ( + varName + + ' = (' + + varName + + ' if isinstance(' + + varName + + ', Number) else 0) + ' + + argument0 + + '\n' + ); +} // Rounding functions have a single operand. export const math_round = math_single; // Trigonometry functions have a single operand. export const math_trig = math_single; -export function math_on_list(block, generator) { +export function math_on_list( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Math functions for lists. const func = block.getFieldValue('OP'); const list = generator.valueToCode(block, 'LIST', Order.NONE) || '[]'; @@ -241,25 +272,32 @@ export function math_on_list(block, generator) { code = 'max(' + list + ')'; break; case 'AVERAGE': { - generator.definitions_['from_numbers_import_Number'] = - 'from numbers import Number'; + (generator as AnyDuringMigration).definitions_[ + 'from_numbers_import_Number' + ] = 'from numbers import Number'; // This operation excludes null and values that aren't int or float: // math_mean([null, null, "aString", 1, 9]) -> 5.0 - const functionName = generator.provideFunction_('math_mean', ` + const functionName = generator.provideFunction_( + 'math_mean', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): localList = [e for e in myList if isinstance(e, Number)] if not localList: return return float(sum(localList)) / len(localList) -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'MEDIAN': { - generator.definitions_['from_numbers_import_Number'] = - 'from numbers import Number'; + (generator as AnyDuringMigration).definitions_[ + 'from_numbers_import_Number' + ] = 'from numbers import Number'; // This operation excludes null values: // math_median([null, null, 1, 3]) -> 2.0 - const functionName = generator.provideFunction_( 'math_median', ` + const functionName = generator.provideFunction_( + 'math_median', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): localList = sorted([e for e in myList if isinstance(e, Number)]) if not localList: return @@ -267,7 +305,8 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): return (localList[len(localList) // 2 - 1] + localList[len(localList) // 2]) / 2.0 else: return localList[(len(localList) - 1) // 2] -`); +`, + ); code = functionName + '(' + list + ')'; break; } @@ -275,7 +314,9 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(myList): // As a list of numbers can contain more than one mode, // the returned result is provided as an array. // Mode of [3, 'x', 'x', 1, 1, 2, '3'] -> ['x', 1] - const functionName = generator.provideFunction_('math_modes', ` + const functionName = generator.provideFunction_( + 'math_modes', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(some_list): modes = [] # Using a lists of [item, count] to keep count rather than dict @@ -295,84 +336,99 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(some_list): if item_count == maxCount: modes.append(counted_item) return modes -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'STD_DEV': { - generator.definitions_['import_math'] = 'import math'; - const functionName = - generator.provideFunction_('math_standard_deviation', ` + (generator as AnyDuringMigration).definitions_['import_math'] = + 'import math'; + const functionName = generator.provideFunction_( + 'math_standard_deviation', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(numbers): n = len(numbers) if n == 0: return mean = float(sum(numbers)) / n variance = sum((x - mean) ** 2 for x in numbers) / n return math.sqrt(variance) -`); +`, + ); code = functionName + '(' + list + ')'; break; } case 'RANDOM': - generator.definitions_['import_random'] = 'import random'; + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; code = 'random.choice(' + list + ')'; break; default: throw Error('Unknown operator: ' + func); } return [code, Order.FUNCTION_CALL]; -}; +} -export function math_modulo(block, generator) { +export function math_modulo( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Remainder computation. const argument0 = - generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || - '0'; + generator.valueToCode(block, 'DIVIDEND', Order.MULTIPLICATIVE) || '0'; const argument1 = - generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || - '0'; + generator.valueToCode(block, 'DIVISOR', Order.MULTIPLICATIVE) || '0'; const code = argument0 + ' % ' + argument1; return [code, Order.MULTIPLICATIVE]; -}; +} -export function math_constrain(block, generator) { +export function math_constrain( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Constrain a number between two limits. - const argument0 = - generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; - const argument1 = - generator.valueToCode(block, 'LOW', Order.NONE) || '0'; + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'LOW', Order.NONE) || '0'; const argument2 = - generator.valueToCode(block, 'HIGH', Order.NONE) || - 'float(\'inf\')'; + generator.valueToCode(block, 'HIGH', Order.NONE) || "float('inf')"; const code = - 'min(max(' + argument0 + ', ' + argument1 + '), ' + argument2 + ')'; + 'min(max(' + argument0 + ', ' + argument1 + '), ' + argument2 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_int(block, generator) { +export function math_random_int( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Random integer between [X] and [Y]. - generator.definitions_['import_random'] = 'import random'; - const argument0 = - generator.valueToCode(block, 'FROM', Order.NONE) || '0'; - const argument1 = - generator.valueToCode(block, 'TO', Order.NONE) || '0'; + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; + const argument0 = generator.valueToCode(block, 'FROM', Order.NONE) || '0'; + const argument1 = generator.valueToCode(block, 'TO', Order.NONE) || '0'; const code = 'random.randint(' + argument0 + ', ' + argument1 + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function math_random_float(block, generator) { +export function math_random_float( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Random fraction between 0 and 1. - generator.definitions_['import_random'] = 'import random'; + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; return ['random.random()', Order.FUNCTION_CALL]; -}; +} -export function math_atan2(block, generator) { +export function math_atan2( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Arctangent of point (X, Y) in degrees from -180 to 180. - generator.definitions_['import_math'] = 'import math'; + (generator as AnyDuringMigration).definitions_['import_math'] = 'import math'; const argument0 = generator.valueToCode(block, 'X', Order.NONE) || '0'; const argument1 = generator.valueToCode(block, 'Y', Order.NONE) || '0'; return [ 'math.atan2(' + argument1 + ', ' + argument0 + ') / math.pi * 180', - Order.MULTIPLICATIVE + Order.MULTIPLICATIVE, ]; -}; +} diff --git a/generators/python/procedures.js b/generators/python/procedures.ts similarity index 63% rename from generators/python/procedures.js rename to generators/python/procedures.ts index 4539ca296..0d9b2e85d 100644 --- a/generators/python/procedures.js +++ b/generators/python/procedures.ts @@ -5,17 +5,19 @@ */ /** - * @fileoverview Generating Python for procedure blocks. + * @file Generating Python for procedure blocks. */ // Former goog.module ID: Blockly.Python.procedures import * as Variables from '../../core/variables.js'; +import type {Block} from '../../core/block.js'; +import type {IfReturnBlock} from '../../blocks/procedures.js'; import {NameType} from '../../core/names.js'; import {Order} from './python_generator.js'; +import type {PythonGenerator} from './python_generator.js'; - -export function procedures_defreturn(block, generator) { +export function procedures_defreturn(block: Block, generator: PythonGenerator) { // Define a procedure with a return value. // First, add a 'global' statement for every variable that is not shadowed by // a local parameter. @@ -33,15 +35,14 @@ export function procedures_defreturn(block, generator) { const devVarList = Variables.allDeveloperVariables(workspace); for (let i = 0; i < devVarList.length; i++) { globals.push( - generator.nameDB_.getName( - devVarList[i], NameType.DEVELOPER_VARIABLE)); + generator.nameDB_!.getName(devVarList[i], NameType.DEVELOPER_VARIABLE), + ); } - const globalString = globals.length ? - generator.INDENT + 'global ' + globals.join(', ') + '\n' : - ''; - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const globalString = globals.length + ? generator.INDENT + 'global ' + globals.join(', ') + '\n' + : ''; + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -55,12 +56,12 @@ export function procedures_defreturn(block, generator) { let loopTrap = ''; if (generator.INFINITE_LOOP_TRAP) { loopTrap = generator.prefixLines( - generator.injectId(generator.INFINITE_LOOP_TRAP, block), - generator.INDENT); + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); } let branch = generator.statementToCode(block, 'STACK'); - let returnValue = - generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + let returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; let xfix2 = ''; if (branch && returnValue) { // After executing the function body, revisit this block for the return. @@ -76,58 +77,74 @@ export function procedures_defreturn(block, generator) { for (let i = 0; i < variables.length; i++) { args[i] = generator.getVariableName(variables[i]); } - let code = 'def ' + funcName + '(' + args.join(', ') + '):\n' + globalString + - xfix1 + loopTrap + branch + xfix2 + returnValue; + let code = + 'def ' + + funcName + + '(' + + args.join(', ') + + '):\n' + + globalString + + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue; code = generator.scrub_(block, code); // Add % so as not to collide with helper functions in definitions list. - generator.definitions_['%' + funcName] = code; + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected. + (generator as AnyDuringMigration).definitions_['%' + funcName] = code; return null; -}; +} // Defining a procedure without a return value uses the same generator as // a procedure with a return value. export const procedures_defnoreturn = procedures_defreturn; -export function procedures_callreturn(block, generator) { +export function procedures_callreturn( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Call a procedure with a return value. - const funcName = - generator.getProcedureName(block.getFieldValue('NAME')); + const funcName = generator.getProcedureName(block.getFieldValue('NAME')); const args = []; const variables = block.getVars(); for (let i = 0; i < variables.length; i++) { - args[i] = - generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'None'; + args[i] = generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'None'; } const code = funcName + '(' + args.join(', ') + ')'; return [code, Order.FUNCTION_CALL]; -}; +} -export function procedures_callnoreturn(block, generator) { +export function procedures_callnoreturn( + block: Block, + generator: PythonGenerator, +) { // Call a procedure with no return value. // Generated code is for a function call as a statement is the same as a // function call as a value, with the addition of line ending. - const tuple = generator.forBlock['procedures_callreturn'](block, generator); + const tuple = generator.forBlock['procedures_callreturn'](block, generator)!; return tuple[0] + '\n'; -}; +} -export function procedures_ifreturn(block, generator) { +export function procedures_ifreturn(block: Block, generator: PythonGenerator) { // Conditionally return value from a procedure. const condition = - generator.valueToCode(block, 'CONDITION', Order.NONE) || 'False'; + generator.valueToCode(block, 'CONDITION', Order.NONE) || 'False'; let code = 'if ' + condition + ':\n'; if (generator.STATEMENT_SUFFIX) { // Inject any statement suffix here since the regular one at the end // will not get executed if the return is triggered. code += generator.prefixLines( - generator.injectId( - generator.STATEMENT_SUFFIX, block), generator.INDENT); + generator.injectId(generator.STATEMENT_SUFFIX, block), + generator.INDENT, + ); } - if (block.hasReturnValue_) { - const value = - generator.valueToCode(block, 'VALUE', Order.NONE) || 'None'; + if ((block as IfReturnBlock).hasReturnValue_) { + const value = generator.valueToCode(block, 'VALUE', Order.NONE) || 'None'; code += generator.INDENT + 'return ' + value + '\n'; } else { code += generator.INDENT + 'return\n'; } return code; -}; +} diff --git a/generators/python/python_generator.js b/generators/python/python_generator.ts similarity index 50% rename from generators/python/python_generator.js rename to generators/python/python_generator.ts index 275caef46..229c2022c 100644 --- a/generators/python/python_generator.js +++ b/generators/python/python_generator.ts @@ -5,59 +5,55 @@ */ /** - * @fileoverview Helper functions for generating Python for blocks. - * @suppress {checkTypes|globalThis} + * @file Python code generator class, including helper methods for + * generating Python for blocks. */ // Former goog.module ID: 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 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 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, // (...) -}; +// prettier-ignore +export enum 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 = [ + /** List of outer-inner pairings that do NOT require parentheses. */ + ORDER_OVERRIDES: [Order, Order][] = [ // (foo()).bar -> foo().bar // (foo())[0] -> foo()[0] [Order.FUNCTION_CALL, Order.MEMBER], @@ -77,11 +73,17 @@ export class PythonGenerator extends CodeGenerator { // 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] + [Order.LOGICAL_OR, Order.LOGICAL_OR], ]; - constructor(name) { - super(name ?? 'Python'); + /** + * Empty loops or conditionals are not allowed in Python. + */ + PASS: string = ''; // Initialised by init(). + + /** @param name Name of the language the generator is for. */ + constructor(name = 'Python') { + super(name); this.isInitialized = false; // Copy Order values onto instance for backwards compatibility @@ -92,7 +94,16 @@ export class PythonGenerator extends CodeGenerator { // replace data properties with get accessors that call // deprecate.warn().) for (const key in Order) { - this['ORDER_' + key] = Order[key]; + // Must assign Order[key] to a temporary to get the type guard to work; + // see https://github.com/microsoft/TypeScript/issues/10530. + const value = Order[key]; + // Skip reverse-lookup entries in the enum. Due to + // https://github.com/microsoft/TypeScript/issues/55713 this (as + // of TypeScript 5.5.2) actually narrows the type of value to + // never - but that still allows the following assignment to + // succeed. + if (typeof value === 'string') continue; + (this as unknown as Record)['ORDER_' + key] = value; } // List of illegal variable names. This is not intended to be a @@ -105,55 +116,52 @@ export class PythonGenerator extends CodeGenerator { // 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' + '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} + * + * @param workspace Workspace to generate code from. */ - init(workspace) { + init(workspace: Workspace) { super.init(workspace); - /** - * Empty loops or conditionals are not allowed in Python. - */ this.PASS = this.INDENT + 'pass\n'; if (!this.nameDB_) { @@ -171,16 +179,15 @@ export class PythonGenerator extends CodeGenerator { 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'); + 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.getVariableName(variables[i].getId()) + - ' = None'); + defvars.push(this.getVariableName(variables[i].getId()) + ' = None'); } this.definitions_['variables'] = defvars.join('\n'); @@ -189,10 +196,11 @@ export class PythonGenerator extends CodeGenerator { /** * Prepend the generated code with import statements and variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. + * + * @param code Generated code. + * @returns Completed code. */ - finish(code) { + finish(code: string): string { // Convert the definitions dictionary into a list. const imports = []; const definitions = []; @@ -208,7 +216,7 @@ export class PythonGenerator extends CodeGenerator { code = super.finish(code); this.isInitialized = false; - this.nameDB_.reset(); + 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; } @@ -216,28 +224,30 @@ export class PythonGenerator extends CodeGenerator { /** * 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. + * + * @param line Line of generated code. + * @returns Legal line of code. */ - scrubNakedValue(line) { + scrubNakedValue(line: string): string { return line + '\n'; } /** * Encode a string as a properly escaped Python string, complete with quotes. - * @param {string} string Text to encode. - * @return {string} Python string. + * + * @param string Text to encode. + * @returns Python string. */ - quote_(string) { + quote_(string: string): 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) { + let quote = "'"; + if (string.indexOf("'") !== -1) { if (string.indexOf('"') === -1) { quote = '"'; } else { - string = string.replace(/'/g, '\\\''); + string = string.replace(/'/g, "\\'"); } } return quote + string + quote; @@ -246,27 +256,29 @@ export class PythonGenerator extends CodeGenerator { /** * Encode a string as a properly escaped multiline Python string, complete * with quotes. - * @param {string} string Text to encode. - * @return {string} Python string. + * + * @param string Text to encode. + * @returns Python string. */ - multiline_quote_(string) { + multiline_quote_(string: string): string { const lines = string.split(/\n/g).map(this.quote_); // Join with the following, plus a newline: // + '\n' + - return lines.join(' + \'\\n\' + \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 + * + * @param block The current block. + * @param code The Python code created for this block. + * @param thisOnly True to generate code for only this statement. + * @returns Python code with comments and subsequent blocks added. + */ - scrub_(block, code, opt_thisOnly) { + scrub_(block: Block, code: string, thisOnly = false): string { let commentCode = ''; // Only collect comments for blocks that aren't inline. if (!block.outputConnection || !block.outputConnection.targetConnection) { @@ -280,7 +292,7 @@ export class PythonGenerator extends CodeGenerator { // Don't collect comments for nested statements. for (let i = 0; i < block.inputList.length; i++) { if (block.inputList[i].type === inputTypes.VALUE) { - const childBlock = block.inputList[i].connection.targetBlock(); + const childBlock = block.inputList[i].connection!.targetBlock(); if (childBlock) { comment = this.allNestedComments(childBlock); if (comment) { @@ -290,33 +302,40 @@ export class PythonGenerator extends CodeGenerator { } } } - const nextBlock = block.nextConnection && block.nextConnection.targetBlock(); - const nextCode = opt_thisOnly ? '' : this.blockToCode(nextBlock); + const nextBlock = + block.nextConnection && block.nextConnection.targetBlock(); + const nextCode = 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} + * + * @param block The block. + * @param atId The ID of the input block to get (and adjust) the value of. + * @param delta Value to add. + * @param negate Whether to negate the value. + * @returns The adjusted value or code that evaluates to it. */ - getAdjustedInt(block, atId, opt_delta, opt_negate) { - let delta = opt_delta || 0; + getAdjustedInt( + block: Block, + atId: string, + delta = 0, + negate = false, + ): string | number { 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; + const atOrder = delta ? Order.ADDITIVE : Order.NONE; + let at: string | number = + 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) { + if (negate) { at = -at; } } else { @@ -328,7 +347,7 @@ export class PythonGenerator extends CodeGenerator { } else { at = 'int(' + at + ')'; } - if (opt_negate) { + if (negate) { at = '-' + at; } } diff --git a/generators/python/text.js b/generators/python/text.ts similarity index 62% rename from generators/python/text.js rename to generators/python/text.ts index f4cd181f3..5d93e91ed 100644 --- a/generators/python/text.js +++ b/generators/python/text.ts @@ -5,29 +5,36 @@ */ /** - * @fileoverview Generating Python for text blocks. + * @file Generating Python for text blocks. */ // Former goog.module ID: Blockly.Python.texts import * as stringUtils from '../../core/utils/string.js'; +import type {Block} from '../../core/block.js'; +import type {JoinMutatorBlock} from '../../blocks/text.js'; import {NameType} from '../../core/names.js'; import {Order} from './python_generator.js'; +import type {PythonGenerator} from './python_generator.js'; - -export function text(block, generator) { +export function text( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Text value. const code = generator.quote_(block.getFieldValue('TEXT')); return [code, Order.ATOMIC]; -}; +} -export function text_multiline(block, generator) { +export function text_multiline( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Text value. const code = generator.multiline_quote_(block.getFieldValue('TEXT')); - const order = - code.indexOf('+') !== -1 ? Order.ADDITIVE : Order.ATOMIC; + const order = code.indexOf('+') !== -1 ? Order.ADDITIVE : Order.ATOMIC; return [code, order]; -}; +} /** * Regular expression to detect a single-quoted string literal. @@ -37,95 +44,113 @@ const strRegExp = /^\s*'([^']|\\')*'\s*$/; /** * Enclose the provided value in 'str(...)' function. * Leave string literals alone. - * @param {string} value Code evaluating to a value. - * @return {Array} Array containing code evaluating to a string + * + * @param value Code evaluating to a value. + * @returns Array containing code evaluating to a string * and * the order of the returned code.[string, number] */ -const forceString = function(value) { +const forceString = function (value: string): [string, Order] { if (strRegExp.test(value)) { return [value, Order.ATOMIC]; } return ['str(' + value + ')', Order.FUNCTION_CALL]; }; -export function text_join(block, generator) { +export function text_join( + block: Block, + generator: PythonGenerator, +): [string, Order] { // 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_) { + const joinBlock = block as JoinMutatorBlock; + switch (joinBlock.itemCount_) { case 0: return ["''", Order.ATOMIC]; case 1: { - const element = - generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; + const element = generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; const codeAndOrder = forceString(element); return codeAndOrder; } case 2: { - const element0 = - generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; - const element1 = - generator.valueToCode(block, 'ADD1', Order.NONE) || "''"; + const element0 = generator.valueToCode(block, 'ADD0', Order.NONE) || "''"; + const element1 = generator.valueToCode(block, 'ADD1', Order.NONE) || "''"; const code = forceString(element0)[0] + ' + ' + forceString(element1)[0]; return [code, Order.ADDITIVE]; } default: { const elements = []; - for (let i = 0; i < block.itemCount_; i++) { + for (let i = 0; i < joinBlock.itemCount_; i++) { elements[i] = - generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; + generator.valueToCode(block, 'ADD' + i, Order.NONE) || "''"; } - const tempVar = - generator.nameDB_.getDistinctName('x', NameType.VARIABLE); - const code = '\'\'.join([str(' + tempVar + ') for ' + tempVar + ' in [' + - elements.join(', ') + ']])'; + const tempVar = generator.nameDB_!.getDistinctName( + 'x', + NameType.VARIABLE, + ); + const code = + "''.join([str(" + + tempVar + + ') for ' + + tempVar + + ' in [' + + elements.join(', ') + + ']])'; return [code, Order.FUNCTION_CALL]; } } -}; +} -export function text_append(block, generator) { +export function text_append(block: Block, generator: PythonGenerator) { // Append to a variable in place. - const varName = - generator.getVariableName(block.getFieldValue('VAR')); + const varName = generator.getVariableName(block.getFieldValue('VAR')); const value = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return varName + ' = str(' + varName + ') + ' + forceString(value)[0] + '\n'; -}; +} -export function text_length(block, generator) { +export function text_length( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Is the string null or array empty? const text = generator.valueToCode(block, 'VALUE', Order.NONE) || "''"; return ['len(' + text + ')', Order.FUNCTION_CALL]; -}; +} -export function text_isEmpty(block, generator) { +export function text_isEmpty( + block: Block, + generator: PythonGenerator, +): [string, Order] { // 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]; -}; +} -export function text_indexOf(block, generator) { +export function text_indexOf( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Search the text for a substring. // Should we allow for non-case sensitive??? const operator = block.getFieldValue('END') === 'FIRST' ? 'find' : 'rfind'; - const substring = - generator.valueToCode(block, 'FIND', Order.NONE) || "''"; - const text = - generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; + const substring = generator.valueToCode(block, 'FIND', Order.NONE) || "''"; + const text = generator.valueToCode(block, 'VALUE', Order.MEMBER) || "''"; const code = text + '.' + operator + '(' + substring + ')'; if (block.workspace.options.oneBasedIndex) { return [code + ' + 1', Order.ADDITIVE]; } return [code, Order.FUNCTION_CALL]; -}; +} -export function text_charAt(block, generator) { +export function text_charAt( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Get letter at index. // Note: Until January 2013 this block did not have the WHERE input. const where = block.getFieldValue('WHERE') || 'FROM_START'; - const textOrder = - (where === 'RANDOM') ? Order.NONE : Order.MEMBER; + const textOrder = where === 'RANDOM' ? Order.NONE : Order.MEMBER; const text = generator.valueToCode(block, 'VALUE', textOrder) || "''"; switch (where) { case 'FIRST': { @@ -147,26 +172,33 @@ export function text_charAt(block, generator) { return [code, Order.MEMBER]; } case 'RANDOM': { - generator.definitions_['import_random'] = 'import random'; - const functionName = - generator.provideFunction_('text_random_letter', ` + // TODO(#7600): find better approach than casting to any to override + // CodeGenerator declaring .definitions protected (here and below). + (generator as AnyDuringMigration).definitions_['import_random'] = + 'import random'; + const functionName = generator.provideFunction_( + 'text_random_letter', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(text): x = int(random.random() * len(text)) return text[x] -`); +`, + ); const code = functionName + '(' + text + ')'; return [code, Order.FUNCTION_CALL]; } } throw Error('Unhandled option (text_charAt).'); -}; +} -export function text_getSubstring(block, generator) { +export function text_getSubstring( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Get substring. const where1 = block.getFieldValue('WHERE1'); const where2 = block.getFieldValue('WHERE2'); - const text = - generator.valueToCode(block, 'STRING', Order.MEMBER) || "''"; + const text = generator.valueToCode(block, 'STRING', Order.MEMBER) || "''"; let at1; switch (where1) { case 'FROM_START': @@ -195,7 +227,8 @@ export function text_getSubstring(block, generator) { // Ensure that if the result calculated is 0 that sub-sequence will // include all elements as expected. if (!stringUtils.isNumber(String(at2))) { - generator.definitions_['import_sys'] = 'import sys'; + (generator as AnyDuringMigration).definitions_['import_sys'] = + 'import sys'; at2 += ' or sys.maxsize'; } else if (at2 === 0) { at2 = ''; @@ -209,49 +242,63 @@ export function text_getSubstring(block, generator) { } const code = text + '[' + at1 + ' : ' + at2 + ']'; return [code, Order.MEMBER]; -}; +} -export function text_changeCase(block, generator) { +export function text_changeCase( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Change capitalization. const OPERATORS = { 'UPPERCASE': '.upper()', 'LOWERCASE': '.lower()', - 'TITLECASE': '.title()' + 'TITLECASE': '.title()', }; - const operator = OPERATORS[block.getFieldValue('CASE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('CASE') as OperatorOption]; const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const code = text + operator; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_trim(block, generator) { +export function text_trim( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Trim spaces. const OPERATORS = { 'LEFT': '.lstrip()', 'RIGHT': '.rstrip()', - 'BOTH': '.strip()' + 'BOTH': '.strip()', }; - const operator = OPERATORS[block.getFieldValue('MODE')]; + type OperatorOption = keyof typeof OPERATORS; + const operator = OPERATORS[block.getFieldValue('MODE') as OperatorOption]; const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const code = text + operator; return [code, Order.FUNCTION_CALL]; -}; +} -export function text_print(block, generator) { +export function text_print(block: Block, generator: PythonGenerator) { // Print statement. const msg = generator.valueToCode(block, 'TEXT', Order.NONE) || "''"; return 'print(' + msg + ')\n'; -}; +} -export function text_prompt_ext(block, generator) { +export function text_prompt_ext( + block: Block, + generator: PythonGenerator, +): [string, Order] { // Prompt function. - const functionName = generator.provideFunction_('text_prompt', ` + const functionName = generator.provideFunction_( + 'text_prompt', + ` def ${generator.FUNCTION_NAME_PLACEHOLDER_}(msg): try: return raw_input(msg) except NameError: return input(msg) -`); +`, + ); let msg; if (block.getField('TEXT')) { // Internal message. @@ -266,27 +313,36 @@ def ${generator.FUNCTION_NAME_PLACEHOLDER_}(msg): code = 'float(' + code + ')'; } return [code, Order.FUNCTION_CALL]; -}; +} export const text_prompt = text_prompt_ext; -export function text_count(block, generator) { +export function text_count( + block: Block, + generator: PythonGenerator, +): [string, Order] { 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]; -}; +} -export function text_replace(block, generator) { +export function text_replace( + block: Block, + generator: PythonGenerator, +): [string, Order] { const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''"; const from = generator.valueToCode(block, 'FROM', Order.NONE) || "''"; const to = generator.valueToCode(block, 'TO', Order.NONE) || "''"; const code = text + '.replace(' + from + ', ' + to + ')'; return [code, Order.MEMBER]; -}; +} -export function text_reverse(block, generator) { +export function text_reverse( + block: Block, + generator: PythonGenerator, +): [string, Order] { 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 deleted file mode 100644 index 0908cd2ed..000000000 --- a/generators/python/variables.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Generating Python for variable blocks. - */ - -// Former goog.module ID: Blockly.Python.variables - -import {Order} from './python_generator.js'; - - -export function variables_get(block, generator) { - // Variable getter. - const code = - generator.getVariableName(block.getFieldValue('VAR')); - return [code, Order.ATOMIC]; -}; - -export function variables_set(block, generator) { - // Variable setter. - const argument0 = - generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; - const varName = - generator.getVariableName(block.getFieldValue('VAR')); - return varName + ' = ' + argument0 + '\n'; -}; diff --git a/generators/python/variables.ts b/generators/python/variables.ts new file mode 100644 index 000000000..a323181f1 --- /dev/null +++ b/generators/python/variables.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2012 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file Generating Python for variable blocks. + */ + +// Former goog.module ID: Blockly.Python.variables + +import type {Block} from '../../core/block.js'; +import type {PythonGenerator} from './python_generator.js'; +import {Order} from './python_generator.js'; + +export function variables_get( + block: Block, + generator: PythonGenerator, +): [string, Order] { + // Variable getter. + const code = generator.getVariableName(block.getFieldValue('VAR')); + return [code, Order.ATOMIC]; +} + +export function variables_set(block: Block, generator: PythonGenerator) { + // Variable setter. + const argument0 = generator.valueToCode(block, 'VALUE', Order.NONE) || '0'; + const varName = generator.getVariableName(block.getFieldValue('VAR')); + return varName + ' = ' + argument0 + '\n'; +} diff --git a/generators/python/variables_dynamic.js b/generators/python/variables_dynamic.ts similarity index 82% rename from generators/python/variables_dynamic.js rename to generators/python/variables_dynamic.ts index 36c56ca06..884700160 100644 --- a/generators/python/variables_dynamic.js +++ b/generators/python/variables_dynamic.ts @@ -5,12 +5,11 @@ */ /** - * @fileoverview Generating Python for dynamic variable blocks. + * @file Generating Python for dynamic variable blocks. */ // Former goog.module ID: Blockly.Python.variablesDynamic - // generator is dynamically typed. export { variables_get as variables_get_dynamic, diff --git a/package-lock.json b/package-lock.json index 63bf4b96c..7340e04fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "markdown-tables-to-json": "^0.1.7", "mocha": "^10.0.0", "patch-package": "^8.0.0", - "prettier": "3.0.3", + "prettier": "3.1.0", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.0.2", @@ -62,184 +62,6 @@ "node": ">=0.10.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz", - "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/runtime": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", @@ -253,9 +75,9 @@ } }, "node_modules/@blockly/block-test": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-5.0.0.tgz", - "integrity": "sha512-TASw4N3FQpJekOJTAm8ubGXOqf1pzVRBM64uqE6g+PtOgGGKw9G7F+Fs+OccS96mjwPEg1e+Eh6rsLRdV5kMqA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@blockly/block-test/-/block-test-5.0.3.tgz", + "integrity": "sha512-iY8aIh+rTJGZMT/mvEeqjRXAlN7jOso/kQsQ5BrdyH+8UEQ+K4IJdjyFou18ydktnfpeXnBkqw6P4wzc/0jtcA==", "dev": true, "engines": { "node": ">=8.17.0" @@ -265,16 +87,16 @@ } }, "node_modules/@blockly/dev-tools": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-7.0.3.tgz", - "integrity": "sha512-IyupTGAx0yMQImOKyS0xnqe6WKYwwh9B+4EHT5VlNORrku4TxfJfM0rsVyeacOvAG6hBHAE9J9VqYih79WNFfA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@blockly/dev-tools/-/dev-tools-7.1.3.tgz", + "integrity": "sha512-KdC9ZUwnuzqYNaxAHgj2pFEgtCMhf/w8u5wNHUScReLWXPqc9wQSIUCq6QsDbN1EiT6BaL8dYwqcHoLl+fgUXA==", "dev": true, "dependencies": { - "@blockly/block-test": "^5.0.0", - "@blockly/theme-dark": "^6.0.1", - "@blockly/theme-deuteranopia": "^5.0.1", - "@blockly/theme-highcontrast": "^5.0.1", - "@blockly/theme-tritanopia": "^5.0.1", + "@blockly/block-test": "^5.0.3", + "@blockly/theme-dark": "^6.0.4", + "@blockly/theme-deuteranopia": "^5.0.4", + "@blockly/theme-highcontrast": "^5.0.4", + "@blockly/theme-tritanopia": "^5.0.4", "chai": "^4.2.0", "dat.gui": "^0.7.7", "lodash.assign": "^4.2.0", @@ -290,9 +112,9 @@ } }, "node_modules/@blockly/theme-dark": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-6.0.1.tgz", - "integrity": "sha512-fZa834SKstG31PNkoZ26DLIpevNVBWLDwDT/g99a6EtwqnkZg2VjCjbJDLA0xzrCmxN8AH6zmZU/lAdMoBH8sw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@blockly/theme-dark/-/theme-dark-6.0.4.tgz", + "integrity": "sha512-G0A+9cMlMiv0/H6/FjQTK+tsqXLmR/FGAfO4gByaTt/ocBmOFeoSlQZdTI23L9ZEvnfO11ZaqH6pqUxY0GSusw==", "dev": true, "engines": { "node": ">=8.17.0" @@ -302,9 +124,9 @@ } }, "node_modules/@blockly/theme-deuteranopia": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-5.0.1.tgz", - "integrity": "sha512-xGBYGkr170VzODnio41UOZwDSjUofbNWiJwTSA5rm3jbWyeeERL8raCPJN+AB3kZ8Vfvt6BSUBobYGKcsxFGNw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@blockly/theme-deuteranopia/-/theme-deuteranopia-5.0.4.tgz", + "integrity": "sha512-QzJN/2MFtXAdeGFT77fVnQAHYVAn3+HBPI9UPvzhu2l6Tod9v6pnrQ51+8Xiw/wGTklFeZzModJkajCnb6RDCg==", "dev": true, "engines": { "node": ">=8.17.0" @@ -314,9 +136,9 @@ } }, "node_modules/@blockly/theme-highcontrast": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-5.0.1.tgz", - "integrity": "sha512-090W+FL4mCwf3fEasERHmypJbYdoP1zjlPnnDsrvkXw5EoSEY6b8PhIgzD63JeZMjWOFjWyCgoZWsBvI9IbVCA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@blockly/theme-highcontrast/-/theme-highcontrast-5.0.4.tgz", + "integrity": "sha512-JFSYmEzGeh3KdUqZLScktdFRTxgYoTqLWIeBLOWEU1AFoFMUbKN8G2ChW3Rzq5snbu3pHNBsisswMEp/vuRbUw==", "dev": true, "engines": { "node": ">=8.17.0" @@ -326,9 +148,9 @@ } }, "node_modules/@blockly/theme-modern": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-5.0.1.tgz", - "integrity": "sha512-88XtPw1cNVFTg3LDUeoERc9LZk6wZ7iFJYklPgbWUL/Dnq7/WOuB+kYy2nb7jBPReKchInNjdHM09tWQdKi+eQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@blockly/theme-modern/-/theme-modern-5.0.4.tgz", + "integrity": "sha512-AamkRgc5XDvENdEBol8GVUebBooAaHP/yGfixVQ3Oj48xErKisUbLuCpZ4emvahewGghJ55HVXSJKdLQ2n0h8w==", "dev": true, "engines": { "node": ">=8.17.0" @@ -338,9 +160,9 @@ } }, "node_modules/@blockly/theme-tritanopia": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-5.0.1.tgz", - "integrity": "sha512-0cfmzjb/2aCga16CO1QDjKxoOwucU29v9P+Dwgr16j8IoNKpFOUnnN2xt7yYw4CeJaOVGZEcHljjzgU52oRPXA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@blockly/theme-tritanopia/-/theme-tritanopia-5.0.4.tgz", + "integrity": "sha512-ex4WlFLidLNY0Zpvf18ZOnBrQ7HRnupvumpajVmpEVhnm1O7h6H6IdHCgBvL4hFb72q+7/xKxAkxG0+lSfih1w==", "dev": true, "engines": { "node": ">=8.17.0" @@ -350,12 +172,12 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", - "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, "dependencies": { - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "esquery": "^1.5.0", "jsdoc-type-pratt-parser": "~4.0.0" }, @@ -388,9 +210,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -411,14 +233,23 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -517,12 +348,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -544,9 +375,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@hyperjump/json-pointer": { @@ -563,9 +394,9 @@ } }, "node_modules/@hyperjump/json-schema": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.5.2.tgz", - "integrity": "sha512-sjYufclp9Qa3o7MbDSbBoxi7TzWy0N11sRj1HyF/oU1G/TvPqrxd7mIvqUiaN3yLXd4EeparDSs3SGLZSrBVKQ==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.6.6.tgz", + "integrity": "sha512-LuZ+erTRmAzppvBT3cx7orfK5AF7wVxdHd850KM06i3RZbcDX+EWD65+YIojQ7uNbHeiqCdcQmIxhNNUN9riNQ==", "dev": true, "dependencies": { "@hyperjump/json-pointer": "^1.0.0", @@ -673,9 +504,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "dependencies": { "ansi-regex": "^6.0.1" @@ -705,15 +536,15 @@ } }, "node_modules/@microsoft/api-documenter": { - "version": "7.22.33", - "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.22.33.tgz", - "integrity": "sha512-9UUe5Z/vbTlMQy3kbSqPaJN7e3CT4LsfNFyP8szfdQEHXiO6BV2ZwwYIUtbQObgaKt7foL4A/cqRMKqhRIi7VQ==", + "version": "7.23.12", + "resolved": "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.23.12.tgz", + "integrity": "sha512-ZFQGHNs8fSe3KoSCNa+jt/HLTN8IdTRGd0TZqmSeHpz2cSvUYHJeyQKhv8s7yi2flr1LezBq5/ig65ITZPSSqw==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.27.6", + "@microsoft/api-extractor-model": "7.28.2", "@microsoft/tsdoc": "0.14.2", - "@rushstack/node-core-library": "3.59.7", - "@rushstack/ts-command-line": "4.15.2", + "@rushstack/node-core-library": "3.61.0", + "@rushstack/ts-command-line": "4.17.1", "colors": "~1.2.1", "js-yaml": "~3.13.1", "resolve": "~1.22.1" @@ -722,52 +553,6 @@ "api-documenter": "bin/api-documenter" } }, - "node_modules/@microsoft/api-documenter/node_modules/@microsoft/api-extractor-model": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.27.6.tgz", - "integrity": "sha512-eiCnlayyum1f7fS2nA9pfIod5VCNR1G+Tq84V/ijDrKrOFVa598BLw145nCsGDMoFenV6ajNi2PR5WCwpAxW6Q==", - "dev": true, - "dependencies": { - "@microsoft/tsdoc": "0.14.2", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.59.7" - } - }, - "node_modules/@microsoft/api-documenter/node_modules/@rushstack/node-core-library": { - "version": "3.59.7", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.59.7.tgz", - "integrity": "sha512-ln1Drq0h+Hwa1JVA65x5mlSgUrBa1uHL+V89FqVWQgXd1vVIMhrtqtWGQrhTnFHxru5ppX+FY39VWELF/FjQCw==", - "dev": true, - "dependencies": { - "colors": "~1.2.1", - "fs-extra": "~7.0.1", - "import-lazy": "~4.0.0", - "jju": "~1.4.0", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "z-schema": "~5.0.2" - }, - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@microsoft/api-documenter/node_modules/@rushstack/ts-command-line": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.15.2.tgz", - "integrity": "sha512-5+C2uoJY8b+odcZD6coEe2XNC4ZjGB4vCMESbqW/8DHRWC/qIHfANdmN9F1wz/lAgxz72i7xRoVtPY2j7e4gpQ==", - "dev": true, - "dependencies": { - "@types/argparse": "1.0.38", - "argparse": "~1.0.9", - "colors": "~1.2.1", - "string-argv": "~0.3.1" - } - }, "node_modules/@microsoft/api-documenter/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -777,20 +562,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/@microsoft/api-documenter/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, "node_modules/@microsoft/api-documenter/node_modules/js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -804,55 +575,22 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@microsoft/api-documenter/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@microsoft/api-documenter/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@microsoft/api-documenter/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/@microsoft/api-extractor": { - "version": "7.35.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.35.4.tgz", - "integrity": "sha512-E/DIIlgu1ZW+AD+Y0UuVe/30rO+Km0CRkDU1aOKQntwKtv/+FodtRAUvzU/vAxq5lQHSsy6jJErLiXR6G3fupA==", + "version": "7.38.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.38.3.tgz", + "integrity": "sha512-xt9iYyC5f39281j77JTA9C3ISJpW1XWkCcnw+2vM78CPnro6KhPfwQdPDfwS5JCPNuq0grm8cMdPUOPvrchDWw==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.27.3", + "@microsoft/api-extractor-model": "7.28.2", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.59.4", - "@rushstack/rig-package": "0.3.21", - "@rushstack/ts-command-line": "4.15.1", + "@rushstack/node-core-library": "3.61.0", + "@rushstack/rig-package": "0.5.1", + "@rushstack/ts-command-line": "4.17.1", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "~5.0.4" }, @@ -861,14 +599,14 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.27.3.tgz", - "integrity": "sha512-fSFvw7otYHduOkyshjTbapKKgwF8bgquVHvgF8VgeKtMYvqXkoaj7W6VcM7PNY7E2bbblhUgC4XNdqZLD4SJGw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.2.tgz", + "integrity": "sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.59.4" + "@rushstack/node-core-library": "3.61.0" } }, "node_modules/@microsoft/api-extractor/node_modules/source-map": { @@ -1031,9 +769,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "3.59.4", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.59.4.tgz", - "integrity": "sha512-YAKJDC6Mz/KA1D7bvB88WaRX3knt/ZuLzkRu5G9QADGSjLtvTWzCNCytRF2PCSaaHOZaZsWul4F1KQdgFgUDqA==", + "version": "3.61.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.61.0.tgz", + "integrity": "sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -1041,7 +779,7 @@ "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", - "semver": "~7.3.0", + "semver": "~7.5.4", "z-schema": "~5.0.2" }, "peerDependencies": { @@ -1053,42 +791,10 @@ } } }, - "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@rushstack/node-core-library/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/@rushstack/rig-package": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.21.tgz", - "integrity": "sha512-6KPBuZYP/b9U0Qwy1J4vjYtXvLavdmVT7mMelErfqqZ3P/ywoxlFITGr9ZbqD1zmfIVrIfC2jrM6gfm7OHPRhQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", + "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", "dev": true, "dependencies": { "resolve": "~1.22.1", @@ -1096,9 +802,9 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.15.1.tgz", - "integrity": "sha512-EL4jxZe5fhb1uVL/P/wQO+Z8Rc8FMiWJ1G7VgnPDvdIt5GVjRfK7vwzder1CZQiX3x0PY6uxENYLNGTFd1InRQ==", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz", + "integrity": "sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==", "dev": true, "dependencies": { "@types/argparse": "1.0.38", @@ -1217,15 +923,15 @@ "dev": true }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/node": { @@ -1234,16 +940,10 @@ "integrity": "sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ==", "dev": true }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, "node_modules/@types/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "dev": true }, "node_modules/@types/vinyl": { @@ -1263,9 +963,9 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" @@ -1282,16 +982,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", - "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/type-utils": "6.6.0", - "@typescript-eslint/utils": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1317,13 +1017,13 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", - "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1334,9 +1034,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", - "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1347,12 +1047,12 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", - "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/types": "6.10.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1363,21 +1063,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/parser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.1.0.tgz", @@ -1426,13 +1111,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", - "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.6.0", - "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1453,9 +1138,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", - "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1466,13 +1151,13 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", - "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1493,12 +1178,12 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", - "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/types": "6.10.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1509,21 +1194,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/types": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.1.0.tgz", @@ -1566,34 +1236,18 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", - "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.6.0", - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", "semver": "^7.5.4" }, "engines": { @@ -1608,13 +1262,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", - "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1625,9 +1279,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", - "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1638,13 +1292,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", - "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", - "@typescript-eslint/visitor-keys": "6.6.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1665,12 +1319,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", - "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/types": "6.10.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1681,21 +1335,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz", @@ -1714,20 +1353,25 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@wdio/config": { - "version": "8.16.7", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.16.7.tgz", - "integrity": "sha512-cdUQ7Hy3608b9w2pbrm6Qagv7r8JpxSLzg7X7LEXQTAD0bS8w6VuA9DzI2GVcUXLijLUlAfHwpJUlcL5Yt3R/w==", + "version": "8.24.6", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.24.6.tgz", + "integrity": "sha512-ZFmd6rB1kgL4k/SjLXbtFTCxvxSf1qzdt/losiTqkqFBYznkTRUBGSoGaVTlkMtHAReiVSK92sICc15JWaCdEA==", "dev": true, "dependencies": { - "@wdio/logger": "8.11.0", - "@wdio/types": "8.16.7", - "@wdio/utils": "8.16.7", + "@wdio/logger": "8.16.17", + "@wdio/types": "8.24.2", + "@wdio/utils": "8.24.6", "decamelize": "^6.0.0", "deepmerge-ts": "^5.0.0", "glob": "^10.2.2", - "import-meta-resolve": "^3.0.0", - "read-pkg-up": "^10.0.0" + "import-meta-resolve": "^3.0.0" }, "engines": { "node": "^16.13 || >=18" @@ -1746,9 +1390,9 @@ } }, "node_modules/@wdio/logger": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.11.0.tgz", - "integrity": "sha512-IsuKSaYi7NKEdgA57h8muzlN/MVp1dQG+V4C//7g4m03YJUnNQLvDhJzLjdeNTfvZy61U7foQSyt+3ktNzZkXA==", + "version": "8.16.17", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.16.17.tgz", + "integrity": "sha512-zeQ41z3T+b4IsrriZZipayXxLNDuGsm7TdExaviNGojPVrIsQUCSd/FvlLHM32b7ZrMyInHenu/zx1cjAZO71g==", "dev": true, "dependencies": { "chalk": "^5.1.2", @@ -1800,15 +1444,15 @@ } }, "node_modules/@wdio/protocols": { - "version": "8.16.5", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.16.5.tgz", - "integrity": "sha512-u9I57hIqmcOgrDH327ZCc2GTXv2YFN5bg6UaA3OUoJU7eJgGYHFB6RrjiNjLXer68iIx07wwVM70V/1xzijd3Q==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.23.0.tgz", + "integrity": "sha512-2XTzD+lqQP3g8BWn+Bn5BTFzjHqzZNwq7DjlYrb27Bq8nOA+1DEcj3WzQ6V6CktTnKI/LAYKA1IFAF//Azrp/Q==", "dev": true }, "node_modules/@wdio/repl": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.10.1.tgz", - "integrity": "sha512-VZ1WFHTNKjR8Ga97TtV2SZM6fvRjWbYI2i/f4pJB4PtusorKvONAMJf2LQcUBIyzbVobqr7KSrcjmSwRolI+yw==", + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.23.1.tgz", + "integrity": "sha512-u6zG2cgBm67V5/WlQzadWqLGXs3moH8MOsgoljULQncelSBBZGZ5DyLB4p7jKcUAsKtMjgmFQmIvpQoqmyvdfg==", "dev": true, "dependencies": { "@types/node": "^20.1.0" @@ -1818,9 +1462,9 @@ } }, "node_modules/@wdio/types": { - "version": "8.16.7", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.16.7.tgz", - "integrity": "sha512-s7fSO5CbrFdM5gsLgXzdCbZykJzZ7bpfxdW3v7NT0IQMABlv4cYpesK66DHQGjSG8Rr3LEqsMmpkt2TlMiwWVQ==", + "version": "8.24.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.24.2.tgz", + "integrity": "sha512-x7iWF5NM8NfVxziGwLdQ+3sstgSxRoqfmmFEDTDps0oFrN5CgkqcoLkqXJ5u166gvpxpEq0gxZwxkbPC/Lp0cw==", "dev": true, "dependencies": { "@types/node": "^20.1.0" @@ -1830,14 +1474,14 @@ } }, "node_modules/@wdio/utils": { - "version": "8.16.7", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.16.7.tgz", - "integrity": "sha512-uId5CapZSGCIqXjdOB8nuzd9WgHNkay2Imw3u0faOn0iyPVw3pF4r41xEBqtDkAnrOcaxmLvvyjuhEl6PvTm+w==", + "version": "8.24.6", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.24.6.tgz", + "integrity": "sha512-qwcshLH9iKnhK0jXoXjPw3G02UhyShT0I+ljC0hMybJEBsra92TYFa47Cp6n1fdvM3+/BTuhsgtzRz0anObicQ==", "dev": true, "dependencies": { "@puppeteer/browsers": "^1.6.0", - "@wdio/logger": "8.11.0", - "@wdio/types": "8.16.7", + "@wdio/logger": "8.16.17", + "@wdio/types": "8.24.2", "decamelize": "^6.0.0", "deepmerge-ts": "^5.1.0", "edgedriver": "^5.3.5", @@ -1847,6 +1491,7 @@ "import-meta-resolve": "^3.0.0", "locate-app": "^2.1.0", "safaridriver": "^0.1.0", + "split2": "^4.2.0", "wait-port": "^1.0.4" }, "engines": { @@ -1854,9 +1499,9 @@ } }, "node_modules/@wdio/utils/node_modules/@puppeteer/browsers": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.7.1.tgz", - "integrity": "sha512-nIb8SOBgDEMFY2iS2MdnUZOg2ikcYchRrBoF+wtdjieRFKR2uGRipHY/oFLo+2N6anDualyClPzGywTHRGrLfw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.8.0.tgz", + "integrity": "sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==", "dev": true, "dependencies": { "debug": "4.3.4", @@ -1865,7 +1510,7 @@ "proxy-agent": "6.3.1", "tar-fs": "3.0.4", "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "yargs": "17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" @@ -1886,20 +1531,6 @@ "node": ">= 14" } }, - "node_modules/@wdio/utils/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@wdio/utils/node_modules/decamelize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", @@ -1966,24 +1597,6 @@ "node": ">= 14" } }, - "node_modules/@wdio/utils/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -1996,9 +1609,9 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2611,9 +2224,9 @@ } }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "dev": true, "engines": { "node": ">=0.6" @@ -2775,18 +2388,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2826,12 +2427,12 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dev": true, "dependencies": { - "@types/http-cache-semantics": "^4.0.1", + "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", @@ -2878,18 +2479,18 @@ } }, "node_modules/chai": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", - "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -2924,10 +2525,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -3229,9 +2833,9 @@ } }, "node_modules/comment-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", - "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -3313,9 +2917,9 @@ } }, "node_modules/concurrently": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.1.tgz", - "integrity": "sha512-nVraf3aXOpIcNud5pB9M82p1tynmZkrSGQ1p6X/VY8cJ+2LMVqAgXsJxYYefACSHbTYlm92O1xuhdGTjwoEvbQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", "dev": true, "dependencies": { "chalk": "^4.1.2", @@ -3614,9 +3218,9 @@ } }, "node_modules/deep-eql": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz", - "integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -3736,9 +3340,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1188743", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1188743.tgz", - "integrity": "sha512-FZDQC58vLiGR2mjSgsMzU8aEJieovMonIyxf38b775eYdIfAYgSzyAWnDf0Eq6ouF/L9qcbqR8jcQeIC34jp/w==", + "version": "0.0.1213968", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1213968.tgz", + "integrity": "sha512-o4n/beY+3CcZwFctYapjGelKptR4AuQT5gXS1Kvgbig+ArwkxK7f8wDVuD1wsoswiJWCwV6OK+Qb7vhNzNmABQ==", "dev": true }, "node_modules/dir-glob": { @@ -3881,13 +3485,13 @@ } }, "node_modules/edgedriver": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.3.6.tgz", - "integrity": "sha512-AvrkKsaMx8X5M64NVgPTfA+XTnOv6bvxH1Cp1m8cBQQVD0HEaC9OJMwPV9Kmqnxh0fCL7VJiBKZH5YOfikbB0g==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.3.8.tgz", + "integrity": "sha512-FWLPDuwJDeGGgtmlqTXb4lQi/HV9yylLo1F9O1g9TLqSemA5T6xH28seUIfyleVirLFtDQyKNUxKsMhMT4IfnA==", "dev": true, "hasInstallScript": true, "dependencies": { - "@wdio/logger": "^8.11.0", + "@wdio/logger": "^8.16.17", "decamelize": "^6.0.0", "edge-paths": "^3.0.5", "node-fetch": "^3.3.2", @@ -4093,18 +3697,19 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -4171,14 +3776,14 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.6.0.tgz", - "integrity": "sha512-T/1gzsvnX45qABzyPEonEhFDttkTn7Igm/X89TXIkTLBOsNl2GYtyBqQPZGXZZ8J5VBzEhiCMvI2P2kXX4dnFw==", + "version": "46.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.0.tgz", + "integrity": "sha512-UQuEtbqLNkPf5Nr/6PPRCtr9xypXY+g8y/Q7gPa0YK7eDhh0y2lWprXRnaYbW7ACgIUvpDKy9X2bZqxtGzBG9Q==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.40.1", + "@es-joy/jsdoccomment": "~0.41.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.0", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "esquery": "^1.5.0", @@ -4193,21 +3798,6 @@ "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5002,6 +4592,38 @@ "node": ">=0.10.0" } }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -5233,9 +4855,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -5339,19 +4961,19 @@ } }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", + "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5737,9 +5359,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6622,27 +6244,6 @@ "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -6715,9 +6316,9 @@ } }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, "dependencies": { "quick-lru": "^5.1.1", @@ -6794,9 +6395,9 @@ } }, "node_modules/import-meta-resolve": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.0.0.tgz", - "integrity": "sha512-4IwhLhNNA8yy445rPjD/lWh++7hMDOml2eHtd58eG7h+qK3EryMuuRbsHGPikCoAgIkkDnckKfWSk2iDla/ejg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.1.1.tgz", + "integrity": "sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==", "dev": true, "funding": { "type": "github", @@ -7228,9 +6829,9 @@ } }, "node_modules/jackspeak": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.0.tgz", - "integrity": "sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -7251,12 +6852,6 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -7372,15 +6967,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7457,9 +7043,9 @@ "dev": true }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -7603,15 +7189,6 @@ "node": ">=0.10.0" } }, - "node_modules/lines-and-columns": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", - "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -7775,12 +7352,12 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.3.tgz", - "integrity": "sha512-krIV4Cf1BIGIx2t1e6tucThhrBemUnIUjMtD2vN4mrMxnxpBvrcosBSpooqunBqP/hOEEV1w/Cr1YskGtqw5Jg==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/lowercase-keys": { @@ -8470,21 +8047,6 @@ } } }, - "node_modules/normalize-package-data": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", - "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8950,37 +8512,6 @@ "node": ">=0.8" } }, - "node_modules/parse-json": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.0.tgz", - "integrity": "sha512-ihtdrgbqdONYD156Ap6qTcaGcGdkdAxodO1wLqQ/j7HP1u2sFYppINiq4jyC8F+Nm+4fVufylCV00QmkTHkSUg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.21.4", - "error-ex": "^1.3.2", - "json-parse-even-better-errors": "^3.0.0", - "lines-and-columns": "^2.0.3", - "type-fest": "^3.8.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-json/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -9096,21 +8627,6 @@ "rimraf": "bin.js" } }, - "node_modules/patch-package/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/patch-package/node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -9401,9 +8917,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -9653,147 +9169,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/read-pkg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", - "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^6.0.0", - "parse-json": "^7.0.0", - "type-fest": "^4.2.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", - "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^8.1.0", - "type-fest": "^4.2.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz", - "integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz", - "integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10155,15 +9530,15 @@ "dev": true }, "node_modules/rimraf": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", - "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, "dependencies": { - "glob": "^10.2.5" + "glob": "^10.3.7" }, "bin": { - "rimraf": "dist/cjs/src/bin.js" + "rimraf": "dist/esm/bin.mjs" }, "engines": { "node": ">=14" @@ -10259,9 +9634,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -10779,6 +10154,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10938,15 +10322,6 @@ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/streamx": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", @@ -11519,12 +10894,12 @@ "dev": true }, "node_modules/undici": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", - "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "version": "5.26.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.3.tgz", + "integrity": "sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==", "dev": true, "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" @@ -11910,9 +11285,9 @@ } }, "node_modules/wait-port": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.0.4.tgz", - "integrity": "sha512-w8Ftna3h6XSFWWc2JC5gZEgp64nz8bnaTp5cvzbJSZ53j+omktWTDdwXxEF0jM8YveviLgFWvNGrSvRHnkyHyw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "dev": true, "dependencies": { "chalk": "^4.1.2", @@ -11936,18 +11311,18 @@ } }, "node_modules/webdriver": { - "version": "8.16.7", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.16.7.tgz", - "integrity": "sha512-YQDYLIfx5zppkKSDHtcBNwQ0VccThGdrIHKVF1m4mOOsw422MYeoUzoKCRWrzH2Osh25zB34DrDhHVKq4q5APg==", + "version": "8.24.6", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.24.6.tgz", + "integrity": "sha512-k5XI2/SHd/14h4ElPQH8EzSUXujZIGbBEi+3dTS2H457KFR5Q8QYfIazDs/YnEdooOp8b6Oe9N7qI99LP8K6bQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "8.16.7", - "@wdio/logger": "8.11.0", - "@wdio/protocols": "8.16.5", - "@wdio/types": "8.16.7", - "@wdio/utils": "8.16.7", + "@wdio/config": "8.24.6", + "@wdio/logger": "8.16.17", + "@wdio/protocols": "8.23.0", + "@wdio/types": "8.24.2", + "@wdio/utils": "8.24.6", "deepmerge-ts": "^5.1.0", "got": "^ 12.6.1", "ky": "^0.33.0", @@ -11995,23 +11370,23 @@ } }, "node_modules/webdriverio": { - "version": "8.16.7", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.16.7.tgz", - "integrity": "sha512-Idom8HYyDoZ02kcc53/XnY/3yZnpub8fxRAwNZDshTvdji7C7fiS/nEYuqxYIVG7bkACrGGkyzHshXhsMriybQ==", + "version": "8.24.6", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.24.6.tgz", + "integrity": "sha512-gJMAJiErbXe/oFJbV+H9lXp9GPxnUgHrbtxkG6SCKQlk1zPFho9FZ3fQWl/ty84w5n9ZMhAdnQIfZM9aytxIBQ==", "dev": true, "dependencies": { "@types/node": "^20.1.0", - "@wdio/config": "8.16.7", - "@wdio/logger": "8.11.0", - "@wdio/protocols": "8.16.5", - "@wdio/repl": "8.10.1", - "@wdio/types": "8.16.7", - "@wdio/utils": "8.16.7", + "@wdio/config": "8.24.6", + "@wdio/logger": "8.16.17", + "@wdio/protocols": "8.23.0", + "@wdio/repl": "8.23.1", + "@wdio/types": "8.24.2", + "@wdio/utils": "8.24.6", "archiver": "^6.0.0", "aria-query": "^5.0.0", "css-shorthand-properties": "^1.1.1", "css-value": "^0.0.1", - "devtools-protocol": "^0.0.1188743", + "devtools-protocol": "^0.0.1213968", "grapheme-splitter": "^1.0.2", "import-meta-resolve": "^3.0.0", "is-plain-obj": "^4.1.0", @@ -12023,7 +11398,7 @@ "resq": "^1.9.1", "rgb2hex": "0.2.5", "serialize-error": "^11.0.1", - "webdriver": "8.16.7" + "webdriver": "8.24.6" }, "engines": { "node": "^16.13 || >=18" diff --git a/package.json b/package.json index 230be1593..8f972c170 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "build:compressed": "exit 1 # Deprecated; use \"npm run minify\" instead.", "build:js": "exit 1 # Deprecated; use \"npm run tsc\" instead.", "build:langfiles": "exit 1 # Deprecated; use \"npm run langfiles\" instead.", - "bump": "npm --no-git-tag-version version 4.$(date +'%Y%m%d').0", "clean": "gulp clean", "deployDemos": "npm ci && gulp deployDemos", "deployDemos:beta": "npm ci && gulp deployDemosBeta", @@ -51,7 +50,7 @@ "test": "gulp test", "test:browser": "cd tests/browser && npx mocha", "test:generators": "gulp testGenerators", - "test:mocha:interactive": "http-server ./ -o /tests/mocha/index.html -c-1", + "test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -o /tests/mocha/index.html -c-1\"", "test:compile:advanced": "gulp buildAdvancedCompilationTest --debug", "updateGithubPages": "npm ci && gulp gitUpdateGithubPages" }, @@ -98,7 +97,7 @@ "markdown-tables-to-json": "^0.1.7", "mocha": "^10.0.0", "patch-package": "^8.0.0", - "prettier": "3.0.3", + "prettier": "3.1.0", "readline-sync": "^1.4.10", "rimraf": "^5.0.0", "typescript": "^5.0.2", diff --git a/scripts/gulpfiles/appengine_tasks.js b/scripts/gulpfiles/appengine_tasks.js index 1f08ca3d5..ddbd2f45f 100644 --- a/scripts/gulpfiles/appengine_tasks.js +++ b/scripts/gulpfiles/appengine_tasks.js @@ -51,7 +51,7 @@ function copyStaticSrc(done) { * Prerequisite: clean, build. */ function copyBuilt(done) { - return gulp.src(['build/msg/*', 'dist/*_compressed.js*'], {base: '.'}) + return gulp.src(['build/msg/*', 'dist/*_compressed.js*', 'build/*.loader.mjs'], {base: '.'}) .pipe(gulp.dest(demoStaticTmpDir)); } diff --git a/scripts/gulpfiles/git_tasks.js b/scripts/gulpfiles/git_tasks.js index ebf9bb356..7c320cd87 100644 --- a/scripts/gulpfiles/git_tasks.js +++ b/scripts/gulpfiles/git_tasks.js @@ -25,6 +25,7 @@ const EXTRAS = [ 'build/msg', 'dist/*_compressed.js*', 'node_modules/@blockly', + 'build/*.loader.mjs', ]; let upstream = null; diff --git a/scripts/migration/js2ts b/scripts/migration/js2ts index a8d941e01..068871df5 100755 --- a/scripts/migration/js2ts +++ b/scripts/migration/js2ts @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); -const filenames = process.argv.slice(2); // Trim off node and script name. +const filenames = process.argv.slice(2); // Trim off node and script name. ////////////////////////////////////////////////////////////////////// // Load deps files via require (since they're executalbe .js files). @@ -40,7 +40,7 @@ globalThis.goog = {}; * to {'module': 'goog'} for backwards-compatibility. Valid properties * and values include {'module': 'goog'} and {'lang': 'es6'}. */ -goog.addDependency = function(relPath, provides, _requires, opt_loadFlags) { +goog.addDependency = function (relPath, provides, _requires, opt_loadFlags) { // Ignore any non-ESM files, as they can't be imported. if (opt_loadFlags?.module !== 'es6') return; @@ -61,7 +61,7 @@ require(path.resolve(__dirname, '../../build/deps.js')); /** RegExp matching goog.require statements. */ const requireRE = - /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s+=\s+)?goog.require(Type)?\('([^']+)'\);/mg; + /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s+=\s+)?goog.require(Type)?\('([^']+)'\);/gm; /** RegExp matching key: value pairs in destructuring assignments. */ const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g; @@ -80,72 +80,82 @@ for (const filename of filenames) { contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, ''); // Migrate from goog.module to goog.declareModuleId. - const closurePathRelative = - path.relative(path.dirname(path.resolve(filename)), closurePath); + const closurePathRelative = path.relative( + path.dirname(path.resolve(filename)), + closurePath, + ); contents = contents.replace( - /^goog.module\('([$\w.]+)'\);$/m, - `import * as goog from '${closurePathRelative}/goog.js';\n` + - `goog.declareModuleId('$1');`); + /^goog.module\('([$\w.]+)'\);$/m, + `import * as goog from '${closurePathRelative}/goog.js';\n` + + `goog.declareModuleId('$1');`, + ); // Migrate from goog.require to import. contents = contents.replace( - requireRE, - function( - orig, // Whole statement to be replaced. - name, // Name of named import of whole module (if applicable). - names, // {}-enclosed list of destructured imports. - type, // If truthy, it is a requireType not require. - moduleId, // goog.module ID that was goog.require()d. - ) { - const importPath = modulePaths[moduleId]; - type = type ? ' type' : ''; - if (!importPath) { - console.warn(`Unable to migrate goog.require('${ - moduleId}') as no ES module path known.`); - return orig; - } - let relativePath = - path.relative(path.dirname(path.resolve(filename)), importPath); - if (relativePath[0] !== '.') relativePath = './' + relativePath; - if (name) { - return `import${type} * as ${name} from '${relativePath}';`; - } else if (names) { - names = names.replace(keyValueRE, '$1 as $2'); - return `import${type} ${names} from '${relativePath}';`; - } else { // Side-effect only require. - return `import${type} '${relativePath}';`; - } - }); + requireRE, + function ( + orig, // Whole statement to be replaced. + name, // Name of named import of whole module (if applicable). + names, // {}-enclosed list of destructured imports. + type, // If truthy, it is a requireType not require. + moduleId, // goog.module ID that was goog.require()d. + ) { + const importPath = modulePaths[moduleId]; + type = type ? ' type' : ''; + if (!importPath) { + console.warn( + `Unable to migrate goog.require('${moduleId}') as no ES module path known.`, + ); + return orig; + } + let relativePath = path.relative( + path.dirname(path.resolve(filename)), + importPath, + ); + if (relativePath[0] !== '.') relativePath = './' + relativePath; + if (name) { + return `import${type} * as ${name} from '${relativePath}';`; + } else if (names) { + names = names.replace(keyValueRE, '$1 as $2'); + return `import${type} ${names} from '${relativePath}';`; + } else { + // Side-effect only require. + return `import${type} '${relativePath}';`; + } + }, + ); // Find and update or remove old-style export assignemnts. /** @type {!Array<{name: string, re: RegExp>}>} */ const easyExports = []; contents = contents.replace( - /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm, - function( - orig, // Whole statement to be replaced. - exportName, // Name to export item as. - declName, // Already-declared name for item being exported. - ) { - // Renamed exports have to be transalted as-is. - if (exportName !== declName) { - return `export {${declName} as ${exportName}};\n`; - } - // OK, we're doing "export.foo = foo;". Can we update the - // declaration? We can't actualy modify it yet as we're in - // the middle of a search-and-replace on contents already, but - // we can delete the old export and later update the - // declaration into an export. - const declRE = new RegExp( - `^(\\s*)((?:const|let|var|function|class)\\s+${declName})\\b`, - 'gm'); - if (contents.match(declRE)) { - easyExports.push({exportName, declRE}); - return ''; // Delete existing export assignment. - } else { - return `export ${exportName};\n`; // Safe fallback. - } - }); + /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm, + function ( + orig, // Whole statement to be replaced. + exportName, // Name to export item as. + declName, // Already-declared name for item being exported. + ) { + // Renamed exports have to be transalted as-is. + if (exportName !== declName) { + return `export {${declName} as ${exportName}};\n`; + } + // OK, we're doing "export.foo = foo;". Can we update the + // declaration? We can't actualy modify it yet as we're in + // the middle of a search-and-replace on contents already, but + // we can delete the old export and later update the + // declaration into an export. + const declRE = new RegExp( + `^(\\s*)((?:const|let|var|function|class)\\s+${declName})\\b`, + 'gm', + ); + if (contents.match(declRE)) { + easyExports.push({exportName, declRE}); + return ''; // Delete existing export assignment. + } else { + return `export ${exportName};\n`; // Safe fallback. + } + }, + ); // Add 'export' to existing declarations where appropriate. for (const {exportName, declRE} of easyExports) { contents = contents.replace(declRE, '$1export $2'); diff --git a/scripts/package/README.md b/scripts/package/README.md index 378eb3373..9e8ab6e35 100644 --- a/scripts/package/README.md +++ b/scripts/package/README.md @@ -5,26 +5,10 @@ blocks together to build programs. All code is free and open source. The source for this module is in the [Blockly repo](http://github.com/google/blockly). -## Installation - -You can install this package either via `npm` or `unpkg`. - -### npm - -```bash -npm install blockly -``` - -### unpkg - -```html - -``` - ## Example Usage ```js -import Blockly from 'blockly'; +import * as Blockly from 'blockly/core'; Blockly.inject('blocklyDiv', { ... }) @@ -34,46 +18,76 @@ Blockly.inject('blocklyDiv', { For samples on how to integrate Blockly into your project, view the list of samples at [blockly-samples](https://github.com/google/blockly-samples). -### Importing Blockly +## Installation -When you import Blockly with `import * as Blockly from 'blockly';` you'll get the default modules: -Blockly core, Blockly built-in blocks, the JavaScript generator and the English lang files. +You can install this package either via `npm` or `unpkg`. -If you need more flexibility, you'll want to define your imports more carefully: +### unpkg -#### Blockly Core +```html + +``` + +When importing from unpkg, you can access imports from the global namespace. ```js +// Access Blockly. +Blockly.thing; +// Access the default blocks. +Blockly.Blocks['block_type']; +// Access the javascript generator. +javascript.javascriptGenerator; +``` + +### npm + +```bash +npm install blockly +``` + +## Imports + +Note: Using import of our package targets requires you to use a bundler (like webpack), since Blockly is packaged as a UMD, rather than an ESM. + +```js +// Import Blockly core. import * as Blockly from 'blockly/core'; -``` - -#### Blockly built in blocks - -```js +// Import the default blocks. import * as libraryBlocks from 'blockly/blocks'; +// Import a generator. +import {javascriptGenerator} from 'blockly/javascript'; +// Import a message file. +import * as En from 'blockly/msg/en'; ``` -#### Blockly Generators - -If your application needs to generate code from the Blockly blocks, you'll want to include a generator. +## Requires ```js -import {pythonGenerator} from 'blockly/python'; +// Require Blockly core. +const Blockly = require('blockly/core'); +// Require the default blocks. +const libraryBlocks = require('blockly/blocks'); +// Require a generator. +const {javascriptGenerator} = require('blockly/javascript'); +// Require a message file. +const En = require('blockly/msg/en'); ``` -to include the Python generator. You can also import `{javascriptGenerator} from 'blockly/javascript'`, `{phpGenerator} from 'blockly/php'`, `{dartGenerator} from 'blockly/dart'` and `{luaGenerator} from 'blockly/lua'`. +## Applying messages -#### Blockly Languages +Once you have the message files, you also need to apply them. ```js -import * as Fr from 'blockly/msg/fr'; -Blockly.setLocale(Fr); +Blockly.setLocal(En); ``` -To import the French lang files. Once you've imported the specific lang module, you'll also want to set the locale in Blockly. - For a full list of supported Blockly locales, see: [https://github.com/google/blockly/tree/master/msg/js](https://github.com/google/blockly/tree/master/msg/js) +## Docs + +For more information about how to use Blockly, check out our +[devsite](https://developers.google.com/blockly/guides/overview). + ## License Apache 2.0 diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 2cf50fea0..f5b2e6a82 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -2242,21 +2242,151 @@ suite('Blocks', function () { chai.assert.isTrue(blockB.disabled); }); test('Disabled blocks from JSON should have proper disabled status', function () { + // Nested c-shaped blocks, inner block is disabled const blockJson = { 'type': 'controls_if', - 'enabled': false, + 'inputs': { + 'DO0': { + 'block': { + 'type': 'controls_if', + 'enabled': false, + }, + }, + }, }; Blockly.serialization.blocks.append(blockJson, this.workspace); - const block = this.workspace.getTopBlocks(false)[0]; + const innerBlock = this.workspace + .getTopBlocks(false)[0] + .getChildren()[0]; chai.assert.isTrue( - block.visuallyDisabled, + innerBlock.visuallyDisabled, 'block should have visuallyDisabled set because it is disabled', ); chai.assert.isFalse( - block.isEnabled(), + innerBlock.isEnabled(), 'block should be marked disabled because enabled json property was set to false', ); }); + test('Disabled blocks from XML should have proper disabled status', function () { + // Nested c-shaped blocks, inner block is disabled + const blockXml = ` + + + + + + `; + Blockly.Xml.domToWorkspace( + Blockly.utils.xml.textToDom(blockXml), + this.workspace, + ); + const innerBlock = this.workspace + .getTopBlocks(false)[0] + .getChildren()[0]; + chai.assert.isTrue( + innerBlock.visuallyDisabled, + 'block should have visuallyDisabled set because it is disabled', + ); + chai.assert.isFalse( + innerBlock.isEnabled(), + 'block should be marked disabled because enabled xml property was set to false', + ); + }); + suite('Disabling blocks with children and neighbors', function () { + setup(function () { + // c-shape block with a stack of 4 blocks in the input + const blockJson = { + 'type': 'controls_if', + 'id': 'parent', + 'inputs': { + 'DO0': { + 'block': { + 'type': 'controls_repeat_ext', + 'id': 'child1', + 'next': { + 'block': { + 'type': 'controls_for', + 'id': 'child2', + 'enabled': false, + 'next': { + 'block': { + 'type': 'controls_whileUntil', + 'id': 'child3', + 'next': { + 'block': { + 'type': 'controls_forEach', + 'id': 'child4', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + Blockly.serialization.blocks.append(blockJson, this.workspace); + this.parent = this.workspace.getBlockById('parent'); + this.child1 = this.workspace.getBlockById('child1'); + this.child2 = this.workspace.getBlockById('child2'); + this.child3 = this.workspace.getBlockById('child3'); + this.child4 = this.workspace.getBlockById('child4'); + }); + test('Disabling parent block visually disables all descendants', async function () { + this.parent.setEnabled(false); + await Blockly.renderManagement.finishQueuedRenders(); + for (const child of this.parent.getDescendants(false)) { + chai.assert.isTrue( + child.visuallyDisabled, + `block ${child.id} should be visually disabled`, + ); + } + }); + test('Child blocks regain original status after parent is re-enabled', async function () { + this.parent.setEnabled(false); + await Blockly.renderManagement.finishQueuedRenders(); + this.parent.setEnabled(true); + await Blockly.renderManagement.finishQueuedRenders(); + + // child2 is disabled, rest should be enabled + chai.assert.isTrue( + this.child1.isEnabled(), + 'child1 should be enabled', + ); + chai.assert.isFalse( + this.child1.visuallyDisabled, + 'child1 should not be visually disabled', + ); + + chai.assert.isFalse( + this.child2.isEnabled(), + 'child2 should be disabled', + ); + chai.assert.isTrue( + this.child2.visuallyDisabled, + 'child2 should be visually disabled', + ); + + chai.assert.isTrue( + this.child3.isEnabled(), + 'child3 should be enabled', + ); + chai.assert.isFalse( + this.child3.visuallyDisabled, + 'child3 should not be visually disabled', + ); + + chai.assert.isTrue( + this.child4.isEnabled(), + 'child34 should be enabled', + ); + chai.assert.isFalse( + this.child4.visuallyDisabled, + 'child4 should not be visually disabled', + ); + }); + }); }); }); diff --git a/tests/mocha/event_block_field_intermediate_change_test.js b/tests/mocha/event_block_field_intermediate_change_test.js index e8a71effb..4d441e5d6 100644 --- a/tests/mocha/event_block_field_intermediate_change_test.js +++ b/tests/mocha/event_block_field_intermediate_change_test.js @@ -35,4 +35,38 @@ suite('Field Intermediate Change Event', function () { chai.assert.deepEqual(newEvent, origEvent); }); }); + + suite('Change Value', function () { + test("running forward changes the block's value to new value", function () { + const block = this.workspace.newBlock('text', 'block_id'); + const origEvent = new Blockly.Events.BlockFieldIntermediateChange( + block, + 'TEXT', + 'old value', + 'new value', + ); + origEvent.run(true); + + chai.assert.deepEqual( + block.getField(origEvent.name).getValue(), + 'new value', + ); + }); + + test("running backward changes the block's value to old value", function () { + const block = this.workspace.newBlock('text', 'block_id'); + const origEvent = new Blockly.Events.BlockFieldIntermediateChange( + block, + 'TEXT', + 'old value', + 'new value', + ); + origEvent.run(false); + + chai.assert.deepEqual( + block.getField(origEvent.name).getValue(), + 'old value', + ); + }); + }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 1804a7504..6c4e5ad0c 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -100,6 +100,7 @@ import './jso_serialization_test.js'; import './json_test.js'; import './keydown_test.js'; + import './layering_test.js'; import './blocks/lists_test.js'; import './blocks/logic_ternary_test.js'; import './metrics_test.js'; diff --git a/tests/mocha/insertion_marker_manager_test.js b/tests/mocha/insertion_marker_manager_test.js index 26996fa95..75cad8ebc 100644 --- a/tests/mocha/insertion_marker_manager_test.js +++ b/tests/mocha/insertion_marker_manager_test.js @@ -177,6 +177,79 @@ suite('Insertion marker manager', function () { const markers = manager.getInsertionMarkers(); chai.assert.equal(markers.length, 2); }); + + suite('children being set as insertion markers', function () { + setup(function () { + Blockly.Blocks['shadows_in_init'] = { + init: function () { + this.appendValueInput('test').connection.setShadowState({ + 'type': 'math_number', + }); + this.setPreviousStatement(true); + }, + }; + + Blockly.Blocks['shadows_in_load'] = { + init: function () { + this.appendValueInput('test'); + this.setPreviousStatement(true); + }, + + loadExtraState: function () { + this.getInput('test').connection.setShadowState({ + 'type': 'math_number', + }); + }, + + saveExtraState: function () { + return true; + }, + }; + }); + + teardown(function () { + delete Blockly.Blocks['shadows_in_init']; + delete Blockly.Blocks['shadows_in_load']; + }); + + test('Shadows added in init are set as insertion markers', function () { + const state = { + 'blocks': { + 'blocks': [ + { + 'id': 'first', + 'type': 'shadows_in_init', + }, + ], + }, + }; + const manager = createBlocksAndManager(this.workspace, state); + const markers = manager.getInsertionMarkers(); + chai.assert.isTrue( + markers[0].getChildren()[0].isInsertionMarker(), + 'Expected the shadow block to be an insertion maker', + ); + }); + + test('Shadows added in `loadExtraState` are set as insertion markers', function () { + const state = { + 'blocks': { + 'blocks': [ + { + 'id': 'first', + 'type': 'shadows_in_load', + }, + ], + }, + }; + const manager = createBlocksAndManager(this.workspace, state); + const markers = manager.getInsertionMarkers(); + chai.assert.isTrue( + markers[0].getChildren()[0].isInsertionMarker(), + 'Expected the shadow block to be an insertion maker', + ); + }); + }); }); suite('Would delete block', function () { diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 500bb2011..895ff0be2 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -61,6 +61,12 @@ suite('JSO Serialization', function () { assertProperty(jso, 'id', 'id0'); }); + test('saveId false', function () { + const block = this.workspace.newBlock('row_block'); + const jso = Blockly.serialization.blocks.save(block, {saveIds: false}); + assertProperty(jso, 'id', undefined); + }); + suite('Attributes', function () { suite('Collapsed', function () { test('True', function () { diff --git a/tests/mocha/layering_test.js b/tests/mocha/layering_test.js new file mode 100644 index 000000000..a25f89374 --- /dev/null +++ b/tests/mocha/layering_test.js @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +suite('Layering', function () { + setup(function () { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + this.layerManager = this.workspace.getLayerManager(); + }); + + teardown(function () { + sharedTestTeardown.call(this); + }); + + function createRenderedElement() { + const g = Blockly.utils.dom.createSvgElement('g', {}); + return { + getSvgRoot: () => g, + }; + } + + suite('appending layers', function () { + test('layer is not appended if it already exists', function () { + const elem1 = createRenderedElement(); + const elem2 = createRenderedElement(); + this.layerManager.append(elem1, 999); + + const layerCount = this.layerManager.layers.size; + this.layerManager.append(elem2, 999); + + chai.assert.equal( + this.layerManager.layers.size, + layerCount, + 'Expected the element to be appended to the existing layer', + ); + }); + + test('more positive layers are appended after less positive layers', function () { + // Checks that if the element comes after all elements, its still gets + // appended. + + const elem1 = createRenderedElement(); + const elem2 = createRenderedElement(); + + this.layerManager.append(elem1, 1000); + this.layerManager.append(elem2, 1010); + + const layer1000 = this.layerManager.layers.get(1000); + const layer1010 = this.layerManager.layers.get(1010); + chai.assert.equal( + layer1000.nextSibling, + layer1010, + 'Expected layer 1000 to be direclty before layer 1010', + ); + }); + + test('less positive layers are appended before more positive layers', function () { + const elem1 = createRenderedElement(); + const elem2 = createRenderedElement(); + + this.layerManager.append(elem1, 1010); + this.layerManager.append(elem2, 1000); + + const layer1010 = this.layerManager.layers.get(1010); + const layer1000 = this.layerManager.layers.get(1000); + chai.assert.equal( + layer1000.nextSibling, + layer1010, + 'Expected layer 1000 to be direclty before layer 1010', + ); + }); + }); + + suite('dragging', function () { + test('moving an element to the drag layer adds it to the drag group', function () { + const elem = createRenderedElement(); + + this.layerManager.moveToDragLayer(elem); + + chai.assert.equal( + this.layerManager.dragLayer.firstChild, + elem.getSvgRoot(), + 'Expected the element to be the first element in the drag layer.', + ); + }); + }); +}); diff --git a/tests/mocha/metrics_test.js b/tests/mocha/metrics_test.js index 83fa605c6..ea2cba7ba 100644 --- a/tests/mocha/metrics_test.js +++ b/tests/mocha/metrics_test.js @@ -68,45 +68,93 @@ suite('Metrics', function () { 'getFlyout', ); }); - test('Toolbox at left', function () { - this.toolboxMetricsStub.returns({width: 107, height: 0, position: 2}); - this.flyoutMetricsStub.returns({}); + + test('left toolboxes with always open flyouts have both offsets', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); this.getToolboxStub.returns(true); - this.getFlyoutStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); - assertDimensionsMatch(absoluteMetrics, 107, 0); + assertDimensionsMatch(absoluteMetrics, 150, 0); }); - test('Toolbox at top', function () { - this.toolboxMetricsStub.returns({width: 0, height: 107, position: 0}); - this.flyoutMetricsStub.returns({}); + + test('top toolboxes with always open flyouts have both offsets', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); this.getToolboxStub.returns(true); - this.getFlyoutStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); - assertDimensionsMatch(absoluteMetrics, 0, 107); + assertDimensionsMatch(absoluteMetrics, 0, 150); }); - test('Flyout at left', function () { - this.toolboxMetricsStub.returns({}); - this.flyoutMetricsStub.returns({width: 107, height: 0, position: 2}); - this.getToolboxStub.returns(false); - this.getFlyoutStub.returns(true); + + test('left toolboxes with autoclosing flyouts only have a toolbox offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); + this.getToolboxStub.returns(true); + this.getFlyoutStub.returns({autoClose: true}); const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); - assertDimensionsMatch(absoluteMetrics, 107, 0); + assertDimensionsMatch(absoluteMetrics, 50, 0); }); - test('Flyout at top', function () { - this.toolboxMetricsStub.returns({}); - this.flyoutMetricsStub.returns({width: 0, height: 107, position: 0}); - this.getToolboxStub.returns(false); - this.getFlyoutStub.returns(true); + + test('top toolboxes with autoclosing flyouts only have a toolbox offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); + this.getToolboxStub.returns(true); + this.getFlyoutStub.returns({autoClose: true}); const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); - assertDimensionsMatch(absoluteMetrics, 0, 107); + assertDimensionsMatch(absoluteMetrics, 0, 50); + }); + + test('left always open flyouts have a flyout offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); + + const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); + + assertDimensionsMatch(absoluteMetrics, 100, 0); + }); + + test('top always open flyouts have a flyout offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); + + const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); + + assertDimensionsMatch(absoluteMetrics, 0, 100); + }); + + test('left autoclosing flyouts have no offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: true}); + + const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); + + assertDimensionsMatch(absoluteMetrics, 0, 0); + }); + + test('top autoclosing flyouts have no offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: true}); + + const absoluteMetrics = this.metricsManager.getAbsoluteMetrics(); + + assertDimensionsMatch(absoluteMetrics, 0, 0); }); }); @@ -132,50 +180,103 @@ suite('Metrics', function () { ); this.svgMetricsStub = sinon.stub(this.metricsManager, 'getSvgMetrics'); }); - test('Toolbox at left', function () { - this.toolboxMetricsStub.returns({width: 107, height: 0, position: 2}); - this.flyoutMetricsStub.returns({}); + + test('left toolboxes with always open flyouts have both offsets', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); this.svgMetricsStub.returns({width: 500, height: 500}); this.getToolboxStub.returns(true); - this.getFlyoutStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); const viewMetrics = this.metricsManager.getViewMetrics(); - assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 393, 500); + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 350, 500); }); - test('Toolbox at top', function () { - this.toolboxMetricsStub.returns({width: 0, height: 107, position: 0}); - this.flyoutMetricsStub.returns({}); + + test('top toolboxes with always open flyouts have both offsets', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); this.svgMetricsStub.returns({width: 500, height: 500}); this.getToolboxStub.returns(true); - this.getFlyoutStub.returns(false); + this.getFlyoutStub.returns({autoClose: false}); const viewMetrics = this.metricsManager.getViewMetrics(); - assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 393); + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 350); }); - test('Flyout at left', function () { - this.toolboxMetricsStub.returns({}); - this.flyoutMetricsStub.returns({width: 107, height: 0, position: 2}); + + test('left toolboxes with autoclosing flyouts only have a toolbox offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); + this.svgMetricsStub.returns({width: 500, height: 500}); + this.getToolboxStub.returns(true); + this.getFlyoutStub.returns({autoClose: true}); + + const viewMetrics = this.metricsManager.getViewMetrics(); + + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 450, 500); + }); + + test('top toolboxes with autoclosing flyouts only have a toolbox offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); + this.svgMetricsStub.returns({width: 500, height: 500}); + this.getToolboxStub.returns(true); + this.getFlyoutStub.returns({autoClose: true}); + + const viewMetrics = this.metricsManager.getViewMetrics(); + + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 450); + }); + + test('left always open flyouts have a flyout offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); this.svgMetricsStub.returns({width: 500, height: 500}); this.getToolboxStub.returns(false); - this.getFlyoutStub.returns(true); + this.getFlyoutStub.returns({autoClose: false}); const viewMetrics = this.metricsManager.getViewMetrics(); - assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 393, 500); + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 400, 500); }); - test('Flyout at top', function () { - this.toolboxMetricsStub.returns({}); - this.flyoutMetricsStub.returns({width: 0, height: 107, position: 0}); + + test('top always open flyouts have a flyout offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); this.svgMetricsStub.returns({width: 500, height: 500}); this.getToolboxStub.returns(false); - this.getFlyoutStub.returns(true); + this.getFlyoutStub.returns({autoClose: false}); const viewMetrics = this.metricsManager.getViewMetrics(); - assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 393); + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 400); }); + + test('left autoclosing flyouts have no offset', function () { + this.toolboxMetricsStub.returns({width: 50, height: 0, position: 2}); + this.flyoutMetricsStub.returns({width: 100, height: 0, position: 2}); + this.svgMetricsStub.returns({width: 500, height: 500}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: true}); + + const viewMetrics = this.metricsManager.getViewMetrics(); + + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 500); + }); + + test('top autoclosing flyouts have no offset', function () { + this.toolboxMetricsStub.returns({width: 0, height: 50, position: 0}); + this.flyoutMetricsStub.returns({width: 0, height: 100, position: 0}); + this.svgMetricsStub.returns({width: 500, height: 500}); + this.getToolboxStub.returns(false); + this.getFlyoutStub.returns({autoClose: true}); + + const viewMetrics = this.metricsManager.getViewMetrics(); + + assertDimensionsMatch(viewMetrics, -SCROLL_X, -SCROLL_Y, 500, 500); + }); + test('Get view metrics in workspace coordinates ', function () { const scale = 2; const getWorkspaceCoordinates = true; diff --git a/tests/mocha/render_management_test.js b/tests/mocha/render_management_test.js index 240a9dd15..d5d957df4 100644 --- a/tests/mocha/render_management_test.js +++ b/tests/mocha/render_management_test.js @@ -30,8 +30,8 @@ suite('Render Management', function () { getParent: () => null, getChildren: () => [], isDisposed: () => false, - getConnections_: () => [], getRelativeToSurfaceXY: () => ({x: 0, y: 0}), + updateComponentLocations: () => {}, workspace: { resizeContents: () => {}, }, diff --git a/tests/mocha/theme_test.js b/tests/mocha/theme_test.js index bd9091a37..6fab65bcc 100644 --- a/tests/mocha/theme_test.js +++ b/tests/mocha/theme_test.js @@ -126,7 +126,7 @@ suite('Theme', function () { try { const blockStyles = createBlockStyles(); const theme = new Blockly.Theme('themeName', blockStyles); - workspace = new Blockly.WorkspaceSvg(new Blockly.Options({})); + workspace = Blockly.inject('blocklyDiv', {}); const blockA = workspace.newBlock('stack_block'); blockA.setStyle = function () { diff --git a/tests/mocha/trashcan_test.js b/tests/mocha/trashcan_test.js index fd622c26e..14acd21b6 100644 --- a/tests/mocha/trashcan_test.js +++ b/tests/mocha/trashcan_test.js @@ -121,14 +121,14 @@ suite('Trashcan', function () { }); }); test('Click outside trashcan - fires trashcanClose', function () { - sinon.stub(this.trashcan.flyout, 'isVisible').returns(true); - // Stub flyout interaction. - const hideFlyoutStub = sinon.stub(this.trashcan.flyout, 'hide'); + this.trashcan.flyout.setVisible(true); simulateClick(this.workspace.svgGroup_); - sinon.assert.calledOnce(hideFlyoutStub); - + chai.assert.isFalse( + this.trashcan.flyout.isVisible(), + 'Expected flyout to be hidden', + ); assertEventFired( this.eventsFireStub, Blockly.Events.TrashcanOpen, diff --git a/tests/mocha/workspace_comment_test.js b/tests/mocha/workspace_comment_test.js index a1b0e38b5..f2126dea2 100644 --- a/tests/mocha/workspace_comment_test.js +++ b/tests/mocha/workspace_comment_test.js @@ -163,19 +163,6 @@ suite('Workspace comment', function () { // Nothing should go wrong the second time dispose is called. comment.dispose(); }); - - test('WorkspaceCommentSvg disposed', function () { - const comment = new Blockly.WorkspaceCommentSvg( - this.workspace, - 'comment text', - 0, - 0, - 'comment id', - ); - comment.dispose(); - // Nothing should go wrong the second time dispose is called. - comment.dispose(); - }); }); suite('Width and height', function () { diff --git a/tests/scripts/check_metadata.sh b/tests/scripts/check_metadata.sh index d9b7326f1..1accb7148 100755 --- a/tests/scripts/check_metadata.sh +++ b/tests/scripts/check_metadata.sh @@ -29,14 +29,15 @@ readonly RELEASE_DIR='dist' # Q3 2021 6.20210701.0 808807 (late-quarter goog.module conversion) # Q4 2021 7.20211209.0-beta.0 920002 # Q4 2021 7.20211209.0 929665 -# Q2 2022 8.0.0 928056 +# Q2 2022 8.0.0 928056 # Q3 2022 8.0.0 1040413 (mid-quarter typescript conversion) -# Q4 2022 8.0.0 870104 -# Q4 2022 9.1.1 903357 +# Q4 2022 8.0.0 870104 +# Q4 2022 9.1.1 903357 # Q1 2023 9.2.1 909181 # Q2 2023 9.3.3 887618 # Q3 2023 10.1.3 898859 -readonly BLOCKLY_SIZE_EXPECTED=898859 +# Q4 2023 10.2.2 903535 +readonly BLOCKLY_SIZE_EXPECTED=903535 # Size of blocks_compressed.js # Q2 2019 2.20190722.0 75618 @@ -51,14 +52,15 @@ readonly BLOCKLY_SIZE_EXPECTED=898859 # Q3 2021 6.20210701.0 76669 # Q4 2021 7.20211209.0-beta.0 82054 # Q4 2021 7.20211209.0 86966 -# Q2 2022 8.0.0 90769 +# Q2 2022 8.0.0 90769 # Q3 2022 8.0.0 102176 (mid-quarter typescript conversion) -# Q4 2022 8.0.0 102213 -# Q4 2022 9.1.1 102190 +# Q4 2022 8.0.0 102213 +# Q4 2022 9.1.1 102190 # Q1 2023 9.2.1 101114 # Q2 2023 9.3.3 91848 # Q3 2023 10.1.3 90150 -readonly BLOCKS_SIZE_EXPECTED=90150 +# Q4 2023 10.2.2 90269 +readonly BLOCKS_SIZE_EXPECTED=90269 # Size of blockly_compressed.js.gz # Q2 2019 2.20190722.0 180925 @@ -74,14 +76,15 @@ readonly BLOCKS_SIZE_EXPECTED=90150 # Q3 2021 6.20210701.0 152025 (late-quarter goog.module conversion) # Q4 2021 7.20211209.0-beta.0 169863 # Q4 2021 7.20211209.0 171759 -# Q2 2022 8.0.0 173997 +# Q2 2022 8.0.0 173997 # Q3 2022 8.0.0 185766 (mid-quarter typescript conversion) -# Q4 2022 8.0.0 175140 -# Q4 2022 9.1.1 179306 +# Q4 2022 8.0.0 175140 +# Q4 2022 9.1.1 179306 # Q1 2023 9.2.1 179814 # Q2 2023 9.3.3 175206 # Q3 2023 10.1.3 180553 -readonly BLOCKLY_GZ_SIZE_EXPECTED=180553 +# Q4 2023 10.2.2 181474 +readonly BLOCKLY_GZ_SIZE_EXPECTED=181474 # Size of blocks_compressed.js.gz # Q2 2019 2.20190722.0 14552 @@ -96,14 +99,15 @@ readonly BLOCKLY_GZ_SIZE_EXPECTED=180553 # Q3 2021 6.20210701.0 15284 # Q4 2021 7.20211209.0-beta.0 16616 # Q4 2021 7.20211209.0 15760 -# Q2 2022 8.0.0 16192 +# Q2 2022 8.0.0 16192 # Q3 2022 8.0.0 17016 (mid-quarter typescript conversion) -# Q4 2022 8.0.0 17188 -# Q4 2022 9.1.1 17182 +# Q4 2022 8.0.0 17188 +# Q4 2022 9.1.1 17182 # Q1 2023 9.2.1 17262 # Q2 2023 9.3.3 16736 # Q3 2023 10.1.3 16508 -readonly BLOCKS_GZ_SIZE_EXPECTED=16508 +# Q4 2023 10.2.2 16442 +readonly BLOCKS_GZ_SIZE_EXPECTED=16442 # ANSI colors readonly BOLD_GREEN='\033[1;32m'