release: Merge branch 'develop' into rc/v10.3.0

This commit is contained in:
Beka Westberg
2023-12-07 22:20:24 +00:00
129 changed files with 6885 additions and 4940 deletions

View File

@@ -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 {

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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"

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}

View File

@@ -325,8 +325,12 @@ export const loopTypes: Set<string> = 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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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);
}
}
/**

View File

@@ -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');
}
/**

View File

@@ -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.
*

View File

@@ -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_();
}

View File

@@ -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;
}
`;

View File

@@ -113,7 +113,7 @@ export interface PositionMetrics {
* @internal
*/
export function createDom() {
if (div) {
if (document.querySelector('.blocklyDropDownDiv')) {
return; // Already created.
}
div = document.createElement('div');

View File

@@ -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 {

View File

@@ -581,6 +581,20 @@ export abstract class Field<T = any>
);
}
/**
* 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<T = any>
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<T = any>
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.

View File

@@ -278,12 +278,8 @@ export class FieldDropdown extends Field<string> {
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);
}

View File

@@ -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<T extends InputTypes> 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<T extends InputTypes> 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<T extends InputTypes> 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<T extends InputTypes> 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<T extends InputTypes> 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;
}

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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}".`,
);
}

View File

@@ -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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -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<IIcon> {
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.
*

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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());

View File

@@ -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. */

View File

@@ -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;
}

154
core/layer_manager.ts Normal file
View File

@@ -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<number, SVGGElement>();
/** @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)!;
}
}

19
core/layers.ts Normal file
View File

@@ -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;

View File

@@ -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,

View File

@@ -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),
);
}
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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).

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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_;

View File

@@ -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;
}
/**
* <g class="blocklyWorkspace">
* <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
@@ -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. */

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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];
}

View File

@@ -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];
};
}

View File

@@ -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;
}
}

View File

@@ -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<string, Order>)['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;
}
}

View File

@@ -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];
};
}

View File

@@ -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];
};
}

View File

@@ -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.');
};
}

View File

@@ -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<string, [string | null, Order]> = {
'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<string, [string, Order]> = {
'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<string, [string | null, Order, Order]> = {
'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<num> 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<num> 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<num> 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,
];
};
}

View File

@@ -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;
};
}

View File

@@ -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<String> 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];
};
}

View File

@@ -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';
};

View File

@@ -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';
}

View File

@@ -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,

View File

@@ -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];
}

View File

@@ -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];
};
}

View File

@@ -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<!Array<number>>}
*/
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<string, Order>)['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;
}

View File

@@ -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];
};
}

View File

@@ -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];
};

View File

@@ -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];
}

View File

@@ -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.');
};
}

View File

@@ -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<string, [string | null, Order]> = {
'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<string, [string, Order]> = {
'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<string, [string | null, Order, Order]> = {
'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,
];
}

View File

@@ -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;
};
}

View File

@@ -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<string|number>} 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(/([-()\\[\\]{}+?*.$\\^|,:#<!\\\\])/g, '\\\\$1')
.replace(/\\x08/g, '\\\\x08');
return haystack.replace(new RegExp(needle, 'g'), replacement);
}
`);
`,
);
const code = functionName + '(' + text + ', ' + from + ', ' + to + ')';
return [code, Order.FUNCTION_CALL];
};
}
export function text_reverse(block, generator) {
const text = generator.valueToCode(block, 'TEXT',
Order.MEMBER) || "''";
export function text_reverse(
block: Block,
generator: JavascriptGenerator,
): [string, Order] {
const text = generator.valueToCode(block, 'TEXT', Order.MEMBER) || "''";
const code = text + ".split('').reverse().join('')";
return [code, Order.FUNCTION_CALL];
};
}

View File

@@ -5,24 +5,28 @@
*/
/**
* @fileoverview Generating JavaScript for variable blocks.
* @file Generating JavaScript for variable blocks.
*/
// Former goog.module ID: Blockly.JavaScript.variables
import type {Block} from '../../core/block.js';
import type {JavascriptGenerator} from './javascript_generator.js';
import {Order} from './javascript_generator.js';
export function variables_get(block, generator) {
export function variables_get(
block: Block,
generator: JavascriptGenerator,
): [string, Order] {
// Variable getter.
const code = generator.getVariableName(block.getFieldValue('VAR'));
return [code, Order.ATOMIC];
};
}
export function variables_set(block, generator) {
export function variables_set(block: Block, generator: JavascriptGenerator) {
// Variable setter.
const argument0 = generator.valueToCode(
block, 'VALUE', Order.ASSIGNMENT) || '0';
const argument0 =
generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '0';
const varName = generator.getVariableName(block.getFieldValue('VAR'));
return varName + ' = ' + argument0 + ';\n';
};
}

View File

@@ -5,12 +5,11 @@
*/
/**
* @fileoverview Generating JavaScript for dynamic variable blocks.
* @file Generating JavaScript for dynamic variable blocks.
*/
// Former goog.module ID: Blockly.JavaScript.variablesDynamic
// JavaScript is dynamically typed.
export {
variables_get as variables_get_dynamic,

View File

@@ -5,9 +5,8 @@
*/
/**
* @fileoverview Complete helper functions for generating Lua for
* @file Complete helper functions for generating Lua for
* blocks. This is the entrypoint for lua_compressed.js.
* @suppress {extraRequire}
*/
// Former goog.module ID: Blockly.Lua.all
@@ -27,13 +26,21 @@ export * from './lua/lua_generator.js';
/**
* Lua code generator instance.
* @type {!LuaGenerator}
*/
export const luaGenerator = new LuaGenerator();
// Install per-block-type generator functions:
Object.assign(
luaGenerator.forBlock,
colour, lists, logic, loops, math, procedures,
text, variables, variablesDynamic
);
const generators: typeof luaGenerator.forBlock = {
...colour,
...lists,
...logic,
...loops,
...math,
...procedures,
...text,
...variables,
...variablesDynamic,
};
for (const name in generators) {
luaGenerator.forBlock[name] = generators[name];
}

View File

@@ -5,46 +5,64 @@
*/
/**
* @fileoverview Generating Lua for colour blocks.
* @file Generating Lua for colour blocks.
*/
// Former goog.module ID: Blockly.Lua.colour
import type {Block} from '../../core/block.js';
import type {LuaGenerator} from './lua_generator.js';
import {Order} from './lua_generator.js';
export function colour_picker(block, generator) {
export function colour_picker(
block: Block,
generator: LuaGenerator,
): [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: LuaGenerator,
): [string, Order] {
// Generate a random colour.
const code = 'string.format("#%06x", math.random(0, 2^24 - 1))';
return [code, Order.HIGH];
};
}
export function colour_rgb(block, generator) {
export function colour_rgb(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Compose a colour from RGB components expressed as percentages.
const functionName = generator.provideFunction_('colour_rgb', `
const functionName = generator.provideFunction_(
'colour_rgb',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(r, g, b)
r = math.floor(math.min(100, math.max(0, r)) * 2.55 + .5)
g = math.floor(math.min(100, math.max(0, g)) * 2.55 + .5)
b = math.floor(math.min(100, math.max(0, b)) * 2.55 + .5)
return string.format("#%02x%02x%02x", r, g, b)
end
`);
`,
);
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.HIGH];
};
}
export function colour_blend(block, generator) {
export function colour_blend(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Blend two colours together.
const functionName = generator.provideFunction_('colour_blend', `
const functionName = generator.provideFunction_(
'colour_blend',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio)
local r1 = tonumber(string.sub(colour1, 2, 3), 16)
local r2 = tonumber(string.sub(colour2, 2, 3), 16)
@@ -58,13 +76,14 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(colour1, colour2, ratio)
local b = math.floor(b1 * (1 - ratio) + b2 * ratio + .5)
return string.format("#%02x%02x%02x", r, g, b)
end
`);
`,
);
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.HIGH];
};
}

View File

@@ -5,34 +5,48 @@
*/
/**
* @fileoverview Generating Lua for list blocks.
* @file Generating Lua for list blocks.
*/
// Former goog.module ID: Blockly.Lua.lists
import type {Block} from '../../core/block.js';
import type {CreateWithBlock} from '../../blocks/lists.js';
import type {LuaGenerator} from './lua_generator.js';
import {NameType} from '../../core/names.js';
import {Order} from './lua_generator.js';
export function lists_create_empty(block, generator) {
export function lists_create_empty(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Create an empty list.
return ['{}', Order.HIGH];
};
}
export function lists_create_with(block, generator) {
export function lists_create_with(
block: Block,
generator: LuaGenerator,
): [string, Order] {
const createWithBlock = block as CreateWithBlock;
// Create a list with any number of elements of any type.
const elements = new Array(block.itemCount_);
for (let i = 0; i < block.itemCount_; i++) {
const elements = new Array(createWithBlock.itemCount_);
for (let i = 0; i < createWithBlock.itemCount_; i++) {
elements[i] =
generator.valueToCode(block, 'ADD' + i, Order.NONE) || 'None';
generator.valueToCode(createWithBlock, 'ADD' + i, Order.NONE) || 'None';
}
const code = '{' + elements.join(', ') + '}';
return [code, Order.HIGH];
};
}
export function lists_repeat(block, generator) {
export function lists_repeat(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Create a list with one element repeated.
const functionName = generator.provideFunction_('create_list_repeated', `
const functionName = generator.provideFunction_(
'create_list_repeated',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(item, count)
local t = {}
for i = 1, count do
@@ -40,33 +54,45 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(item, count)
end
return t
end
`);
`,
);
const element = generator.valueToCode(block, 'ITEM', Order.NONE) || 'None';
const repeatCount = generator.valueToCode(block, 'NUM', Order.NONE) || '0';
const code = functionName + '(' + element + ', ' + repeatCount + ')';
return [code, Order.HIGH];
};
}
export function lists_length(block, generator) {
export function lists_length(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// String or array length.
const list = generator.valueToCode(block, 'VALUE', Order.UNARY) || '{}';
return ['#' + list, Order.UNARY];
};
}
export function lists_isEmpty(block, generator) {
export function lists_isEmpty(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Is the string null or array empty?
const list = generator.valueToCode(block, 'VALUE', Order.UNARY) || '{}';
const code = '#' + list + ' == 0';
return [code, Order.RELATIONAL];
};
}
export function lists_indexOf(block, generator) {
export function lists_indexOf(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Find an item in the list.
const item = generator.valueToCode(block, 'FIND', Order.NONE) || "''";
const list = generator.valueToCode(block, 'VALUE', Order.NONE) || '{}';
let functionName;
if (block.getFieldValue('END') === 'FIRST') {
functionName = generator.provideFunction_('first_index', `
functionName = generator.provideFunction_(
'first_index',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t, elem)
for k, v in ipairs(t) do
if v == elem then
@@ -75,9 +101,12 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t, elem)
end
return 0
end
`);
`,
);
} else {
functionName = generator.provideFunction_('last_index', `
functionName = generator.provideFunction_(
'last_index',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t, elem)
for i = #t, 1, -1 do
if t[i] == elem then
@@ -86,20 +115,26 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(t, elem)
end
return 0
end
`);
`,
);
}
const code = functionName + '(' + list + ', ' + item + ')';
return [code, Order.HIGH];
};
}
/**
* 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 getListIndex = function(listName, where, opt_at) {
const getListIndex = function (
listName: string,
where: string,
opt_at: string,
): string {
if (where === 'FIRST') {
return '1';
} else if (where === 'FROM_END') {
@@ -113,7 +148,10 @@ const getListIndex = function(listName, where, opt_at) {
}
};
export function lists_getIndex(block, generator) {
export function lists_getIndex(
block: Block,
generator: LuaGenerator,
): [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';
@@ -122,19 +160,30 @@ export function lists_getIndex(block, generator) {
// If `list` would be evaluated more than once (which is the case for LAST,
// FROM_END, and RANDOM) and is non-trivial, make sure to access it only once.
if ((where === 'LAST' || where === 'FROM_END' || where === 'RANDOM') &&
!list.match(/^\w+$/)) {
if (
(where === 'LAST' || where === 'FROM_END' || where === 'RANDOM') &&
!list.match(/^\w+$/)
) {
// `list` is an expression, so we may not evaluate it more than once.
if (mode === 'REMOVE') {
// We can use multiple statements.
const atOrder =
(where === 'FROM_END') ? Order.ADDITIVE : Order.NONE;
const atOrder = where === 'FROM_END' ? Order.ADDITIVE : Order.NONE;
let at = generator.valueToCode(block, 'AT', atOrder) || '1';
const listVar =
generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE);
const listVar = generator.nameDB_!.getDistinctName(
'tmp_list',
NameType.VARIABLE,
);
at = getListIndex(listVar, where, at);
const code = listVar + ' = ' + list + '\n' +
'table.remove(' + listVar + ', ' + at + ')\n';
const code =
listVar +
' = ' +
list +
'\n' +
'table.remove(' +
listVar +
', ' +
at +
')\n';
return code;
} else {
// We need to create a procedure to avoid reevaluating values.
@@ -142,41 +191,49 @@ export function lists_getIndex(block, generator) {
let functionName;
if (mode === 'GET') {
functionName = generator.provideFunction_(
'list_get_' + where.toLowerCase(), [
'function ' + generator.FUNCTION_NAME_PLACEHOLDER_ + '(t' +
'list_get_' + where.toLowerCase(),
[
'function ' +
generator.FUNCTION_NAME_PLACEHOLDER_ +
'(t' +
// The value for 'FROM_END' and'FROM_START' depends on `at` so
// we add it as a parameter.
((where === 'FROM_END' || where === 'FROM_START') ? ', at)' :
')'),
' return t[' + getListIndex('t', where, 'at') + ']', 'end'
]);
} else { // `mode` === 'GET_REMOVE'
functionName =
generator.provideFunction_(
'list_remove_' + where.toLowerCase(), [
'function ' + generator.FUNCTION_NAME_PLACEHOLDER_ + '(t' +
// The value for 'FROM_END' and'FROM_START' depends on `at` so
// we add it as a parameter.
((where === 'FROM_END' || where === 'FROM_START') ? ', at)' :
')'),
' return table.remove(t, ' + getListIndex('t', where, 'at') +
')',
'end'
]);
(where === 'FROM_END' || where === 'FROM_START' ? ', at)' : ')'),
' return t[' + getListIndex('t', where, 'at') + ']',
'end',
],
);
} else {
// `mode` === 'GET_REMOVE'
functionName = generator.provideFunction_(
'list_remove_' + where.toLowerCase(),
[
'function ' +
generator.FUNCTION_NAME_PLACEHOLDER_ +
'(t' +
// The value for 'FROM_END' and'FROM_START' depends on `at` so
// we add it as a parameter.
(where === 'FROM_END' || where === 'FROM_START' ? ', at)' : ')'),
' return table.remove(t, ' + getListIndex('t', where, 'at') + ')',
'end',
],
);
}
const code = functionName + '(' + list +
// The value for 'FROM_END' and 'FROM_START' depends on `at` so we
// pass it.
((where === 'FROM_END' || where === 'FROM_START') ? ', ' + at : '') +
')';
const code =
functionName +
'(' +
list +
// The value for 'FROM_END' and 'FROM_START' depends on `at` so we
// pass it.
(where === 'FROM_END' || where === 'FROM_START' ? ', ' + at : '') +
')';
return [code, Order.HIGH];
}
} else {
// Either `list` is a simple variable, or we only need to refer to `list`
// once.
const atOrder = (mode === 'GET' && where === 'FROM_END') ?
Order.ADDITIVE :
Order.NONE;
const atOrder =
mode === 'GET' && where === 'FROM_END' ? Order.ADDITIVE : Order.NONE;
let at = generator.valueToCode(block, 'AT', atOrder) || '1';
at = getListIndex(list, where, at);
if (mode === 'GET') {
@@ -186,14 +243,15 @@ export function lists_getIndex(block, generator) {
const code = 'table.remove(' + list + ', ' + at + ')';
if (mode === 'GET_REMOVE') {
return [code, Order.HIGH];
} else { // `mode` === 'REMOVE'
} else {
// `mode` === 'REMOVE'
return code + '\n';
}
}
}
};
}
export function lists_setIndex(block, generator) {
export function lists_setIndex(block: Block, generator: LuaGenerator): string {
// Set element at index.
// Note: Until February 2013 this block did not have MODE or WHERE inputs.
let list = generator.valueToCode(block, 'LIST', Order.HIGH) || '{}';
@@ -205,28 +263,41 @@ export function lists_setIndex(block, generator) {
let code = '';
// If `list` would be evaluated more than once (which is the case for LAST,
// FROM_END, and RANDOM) and is non-trivial, make sure to access it only once.
if ((where === 'LAST' || where === 'FROM_END' || where === 'RANDOM') &&
!list.match(/^\w+$/)) {
if (
(where === 'LAST' || where === 'FROM_END' || where === 'RANDOM') &&
!list.match(/^\w+$/)
) {
// `list` is an expression, so we may not evaluate it more than once.
// We can use multiple statements.
const listVar =
generator.nameDB_.getDistinctName('tmp_list', NameType.VARIABLE);
const listVar = generator.nameDB_!.getDistinctName(
'tmp_list',
NameType.VARIABLE,
);
code = listVar + ' = ' + list + '\n';
list = listVar;
}
if (mode === 'SET') {
code += list + '[' + getListIndex(list, where, at) + '] = ' + value;
} else { // `mode` === 'INSERT'
} else {
// `mode` === 'INSERT'
// LAST is a special case, because we want to insert
// *after* not *before*, the existing last element.
code += 'table.insert(' + list + ', ' +
(getListIndex(list, where, at) + (where === 'LAST' ? ' + 1' : '')) +
', ' + value + ')';
code +=
'table.insert(' +
list +
', ' +
(getListIndex(list, where, at) + (where === 'LAST' ? ' + 1' : '')) +
', ' +
value +
')';
}
return code + '\n';
};
}
export function lists_getSublist(block, generator) {
export function lists_getSublist(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Get sublist.
const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}';
const where1 = block.getFieldValue('WHERE1');
@@ -237,11 +308,12 @@ export function lists_getSublist(block, generator) {
// 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_(
'list_sublist_' + where1.toLowerCase() + '_' + where2.toLowerCase(), `
'list_sublist_' + where1.toLowerCase() + '_' + where2.toLowerCase(),
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(source${at1Param}${at2Param})
local t = {}
local start = ${getListIndex('source', where1, 'at1')}
@@ -251,23 +323,32 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(source${at1Param}${at2Param})
end
return t
end
`);
const 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 : '') +
')';
`,
);
const 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.HIGH];
};
}
export function lists_sort(block, generator) {
export function lists_sort(
block: Block,
generator: LuaGenerator,
): [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 functionName = generator.provideFunction_('list_sort', `
const functionName = generator.provideFunction_(
'list_sort',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, typev, direction)
local t = {}
for n,v in pairs(list) do table.insert(t, v) end
@@ -288,25 +369,30 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(list, typev, direction)
table.sort(t, compare)
return t
end
`);
`,
);
const code =
functionName + '(' + list + ',"' + type + '", ' + direction + ')';
functionName + '(' + list + ',"' + type + '", ' + direction + ')';
return [code, Order.HIGH];
};
}
export function lists_split(block, generator) {
export function lists_split(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Block for splitting text into a list, or joining a list into text.
let input = generator.valueToCode(block, 'INPUT', Order.NONE);
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') {
if (!input) {
input = "''";
}
functionName = generator.provideFunction_('list_string_split', `
functionName = generator.provideFunction_(
'list_string_split',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(input, delim)
local t = {}
local pos = 1
@@ -322,7 +408,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(input, delim)
end
return t
end
`);
`,
);
} else if (mode === 'JOIN') {
if (!input) {
input = '{}';
@@ -333,12 +420,17 @@ end
}
const code = functionName + '(' + input + ', ' + delimiter + ')';
return [code, Order.HIGH];
};
}
export function lists_reverse(block, generator) {
export function lists_reverse(
block: Block,
generator: LuaGenerator,
): [string, Order] {
// Block for reversing a list.
const list = generator.valueToCode(block, 'LIST', Order.NONE) || '{}';
const functionName = generator.provideFunction_('list_reverse', `
const functionName = generator.provideFunction_(
'list_reverse',
`
function ${generator.FUNCTION_NAME_PLACEHOLDER_}(input)
local reversed = {}
for i = #input, 1, -1 do
@@ -346,7 +438,8 @@ function ${generator.FUNCTION_NAME_PLACEHOLDER_}(input)
end
return reversed
end
`);
`,
);
const code = functionName + '(' + list + ')';
return [code, Order.HIGH];
};
}

View File

@@ -5,15 +5,16 @@
*/
/**
* @fileoverview Generating Lua for logic blocks.
* @file Generating Lua for logic blocks.
*/
// Former goog.module ID: Blockly.Lua.logic
import type {Block} from '../../core/block.js';
import type {LuaGenerator} from './lua_generator.js';
import {Order} from './lua_generator.js';
export function controls_if(block, generator) {
export function controls_if(block: Block, generator: LuaGenerator): string {
// If/elseif/else condition.
let n = 0;
let code = '';
@@ -23,15 +24,17 @@ export function controls_if(block, generator) {
}
do {
const conditionCode =
generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false';
generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false';
let branchCode = generator.statementToCode(block, 'DO' + n);
if (generator.STATEMENT_SUFFIX) {
branchCode = generator.prefixLines(
branchCode =
generator.prefixLines(
generator.injectId(generator.STATEMENT_SUFFIX, block),
generator.INDENT) + branchCode;
generator.INDENT,
) + branchCode;
}
code +=
(n > 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];
};
}

View File

@@ -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 <pre>continue</pre>.
* It is also used to recognise <pre>continue</pre>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.');
};
}

View File

@@ -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<string, Order>)['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;
};
}
}

View File

@@ -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<string, [string, Order]> = {
'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<string, [string, Order]> = {
'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<string, [string | null, Order, Order]> = {
'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,
];
};
}

View File

@@ -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;
};
}

View File

@@ -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];
};
}

View File

@@ -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';
};

View File

@@ -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';
}

View File

@@ -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,

View File

@@ -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];
}

View File

@@ -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];
};
}

View File

@@ -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];
};
}

View File

@@ -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];
};
}

View File

@@ -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.');
};
}

View File

@@ -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<string, [string, Order]> = {
'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<string, [string, Order]> = {
'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,
];
};
}

View File

@@ -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<!Array<number>>}
*/
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;
};
}

View File

@@ -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<string, Order>)['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;
}
}

View File

@@ -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;
};
}

View File

@@ -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];
};
}

View File

@@ -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';
};

View File

@@ -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';
}

View File

@@ -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,

View File

@@ -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];
}

View File

@@ -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];
};
}

Some files were not shown because too many files have changed in this diff Show More