mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
feat: Parse newlines in JSON message as row separators. (#6944)
* feat: Parse message newlines as endOfRow dummies. * Fix the multilineinput field test. * Addressing PR feedback. * Addressing PR feedback. * Newline parsing now uses a new custom input. * npm run format * Added input_end_row to block factory. * Addres feedback, fix endrow after external value.
This commit is contained in:
@@ -48,6 +48,7 @@ import {Size} from './utils/size.js';
|
|||||||
import type {VariableModel} from './variable_model.js';
|
import type {VariableModel} from './variable_model.js';
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
import {DummyInput} from './inputs/dummy_input.js';
|
import {DummyInput} from './inputs/dummy_input.js';
|
||||||
|
import {EndRowInput} from './inputs/end_row_input.js';
|
||||||
import {ValueInput} from './inputs/value_input.js';
|
import {ValueInput} from './inputs/value_input.js';
|
||||||
import {StatementInput} from './inputs/statement_input.js';
|
import {StatementInput} from './inputs/statement_input.js';
|
||||||
import {IconType} from './icons/icon_types.js';
|
import {IconType} from './icons/icon_types.js';
|
||||||
@@ -1339,6 +1340,12 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let i = 0; i < this.inputList.length; i++) {
|
||||||
|
if (this.inputList[i] instanceof EndRowInput) {
|
||||||
|
// A row-end input is present. Inline value inputs.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1560,6 +1567,17 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
return this.appendInput(new DummyInput(name, this));
|
return this.appendInput(new DummyInput(name, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an input that ends the row.
|
||||||
|
*
|
||||||
|
* @param name Optional language-neutral identifier which may used to find
|
||||||
|
* this input again. Should be unique to this block.
|
||||||
|
* @returns The input object created.
|
||||||
|
*/
|
||||||
|
appendEndRowInput(name = ''): Input {
|
||||||
|
return this.appendInput(new EndRowInput(name, this));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the given input row.
|
* Appends the given input row.
|
||||||
*
|
*
|
||||||
@@ -1628,7 +1646,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
this.interpolate_(
|
this.interpolate_(
|
||||||
json['message' + i],
|
json['message' + i],
|
||||||
json['args' + i] || [],
|
json['args' + i] || [],
|
||||||
json['lastDummyAlign' + i],
|
// Backwards compatibility: lastDummyAlign aliases implicitAlign.
|
||||||
|
json['implicitAlign' + i] || json['lastDummyAlign' + i],
|
||||||
warningPrefix,
|
warningPrefix,
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
@@ -1765,19 +1784,19 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
* @param message Text contains interpolation tokens (%1, %2, ...) that match
|
* @param message Text contains interpolation tokens (%1, %2, ...) that match
|
||||||
* with fields or inputs defined in the args array.
|
* with fields or inputs defined in the args array.
|
||||||
* @param args Array of arguments to be interpolated.
|
* @param args Array of arguments to be interpolated.
|
||||||
* @param lastDummyAlign If a dummy input is added at the end, how should it
|
* @param implicitAlign If an implicit input is added at the end or in place
|
||||||
* be aligned?
|
* of newline tokens, how should it be aligned?
|
||||||
* @param warningPrefix Warning prefix string identifying block.
|
* @param warningPrefix Warning prefix string identifying block.
|
||||||
*/
|
*/
|
||||||
private interpolate_(
|
private interpolate_(
|
||||||
message: string,
|
message: string,
|
||||||
args: AnyDuringMigration[],
|
args: AnyDuringMigration[],
|
||||||
lastDummyAlign: string | undefined,
|
implicitAlign: string | undefined,
|
||||||
warningPrefix: string,
|
warningPrefix: string,
|
||||||
) {
|
) {
|
||||||
const tokens = parsing.tokenizeInterpolation(message);
|
const tokens = parsing.tokenizeInterpolation(message);
|
||||||
this.validateTokens_(tokens, args.length);
|
this.validateTokens_(tokens, args.length);
|
||||||
const elements = this.interpolateArguments_(tokens, args, lastDummyAlign);
|
const elements = this.interpolateArguments_(tokens, args, implicitAlign);
|
||||||
|
|
||||||
// An array of [field, fieldName] tuples.
|
// An array of [field, fieldName] tuples.
|
||||||
const fieldStack = [];
|
const fieldStack = [];
|
||||||
@@ -1855,19 +1874,20 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts args in place of numerical tokens. String args are converted to
|
* Inserts args in place of numerical tokens. String args are converted to
|
||||||
* JSON that defines a label field. If necessary an extra dummy input is added
|
* JSON that defines a label field. Newline characters are converted to
|
||||||
* to the end of the elements.
|
* end-row inputs, and if necessary an extra dummy input is added to the end
|
||||||
|
* of the elements.
|
||||||
*
|
*
|
||||||
* @param tokens The tokens to interpolate
|
* @param tokens The tokens to interpolate
|
||||||
* @param args The arguments to insert.
|
* @param args The arguments to insert.
|
||||||
* @param lastDummyAlign The alignment the added dummy input should have, if
|
* @param implicitAlign The alignment to use for any implicitly added end-row
|
||||||
* we are required to add one.
|
* or dummy inputs, if necessary.
|
||||||
* @returns The JSON definitions of field and inputs to add to the block.
|
* @returns The JSON definitions of field and inputs to add to the block.
|
||||||
*/
|
*/
|
||||||
private interpolateArguments_(
|
private interpolateArguments_(
|
||||||
tokens: Array<string | number>,
|
tokens: Array<string | number>,
|
||||||
args: Array<AnyDuringMigration | string>,
|
args: Array<AnyDuringMigration | string>,
|
||||||
lastDummyAlign: string | undefined,
|
implicitAlign: string | undefined,
|
||||||
): AnyDuringMigration[] {
|
): AnyDuringMigration[] {
|
||||||
const elements = [];
|
const elements = [];
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
@@ -1877,11 +1897,20 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
}
|
}
|
||||||
// Args can be strings, which is why this isn't elseif.
|
// Args can be strings, which is why this isn't elseif.
|
||||||
if (typeof element === 'string') {
|
if (typeof element === 'string') {
|
||||||
// AnyDuringMigration because: Type '{ text: string; type: string; } |
|
if (element === '\n') {
|
||||||
// null' is not assignable to type 'string | number'.
|
// Convert newline tokens to end-row inputs.
|
||||||
element = this.stringToFieldJson_(element) as AnyDuringMigration;
|
const newlineInput = {'type': 'input_end_row'};
|
||||||
if (!element) {
|
if (implicitAlign) {
|
||||||
continue;
|
(newlineInput as AnyDuringMigration)['align'] = implicitAlign;
|
||||||
|
}
|
||||||
|
element = newlineInput as AnyDuringMigration;
|
||||||
|
} else {
|
||||||
|
// AnyDuringMigration because: Type '{ text: string; type: string; }
|
||||||
|
// | null' is not assignable to type 'string | number'.
|
||||||
|
element = this.stringToFieldJson_(element) as AnyDuringMigration;
|
||||||
|
if (!element) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elements.push(element);
|
elements.push(element);
|
||||||
@@ -1895,8 +1924,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const dummyInput = {'type': 'input_dummy'};
|
const dummyInput = {'type': 'input_dummy'};
|
||||||
if (lastDummyAlign) {
|
if (implicitAlign) {
|
||||||
(dummyInput as AnyDuringMigration)['align'] = lastDummyAlign;
|
(dummyInput as AnyDuringMigration)['align'] = implicitAlign;
|
||||||
}
|
}
|
||||||
elements.push(dummyInput);
|
elements.push(dummyInput);
|
||||||
}
|
}
|
||||||
@@ -1960,6 +1989,9 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
case 'input_dummy':
|
case 'input_dummy':
|
||||||
input = this.appendDummyInput(element['name']);
|
input = this.appendDummyInput(element['name']);
|
||||||
break;
|
break;
|
||||||
|
case 'input_end_row':
|
||||||
|
input = this.appendEndRowInput(element['name']);
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
input = this.appendInputFromRegistry(element['type'], element['name']);
|
input = this.appendInputFromRegistry(element['type'], element['name']);
|
||||||
break;
|
break;
|
||||||
@@ -1998,6 +2030,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
|||||||
str === 'input_value' ||
|
str === 'input_value' ||
|
||||||
str === 'input_statement' ||
|
str === 'input_statement' ||
|
||||||
str === 'input_dummy' ||
|
str === 'input_dummy' ||
|
||||||
|
str === 'input_end_row' ||
|
||||||
registry.hasItem(registry.Type.INPUT, str)
|
registry.hasItem(registry.Type.INPUT, str)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,17 @@
|
|||||||
import {Align} from './inputs/align.js';
|
import {Align} from './inputs/align.js';
|
||||||
import {Input} from './inputs/input.js';
|
import {Input} from './inputs/input.js';
|
||||||
import {DummyInput} from './inputs/dummy_input.js';
|
import {DummyInput} from './inputs/dummy_input.js';
|
||||||
|
import {EndRowInput} from './inputs/end_row_input.js';
|
||||||
import {StatementInput} from './inputs/statement_input.js';
|
import {StatementInput} from './inputs/statement_input.js';
|
||||||
import {ValueInput} from './inputs/value_input.js';
|
import {ValueInput} from './inputs/value_input.js';
|
||||||
import {inputTypes} from './inputs/input_types.js';
|
import {inputTypes} from './inputs/input_types.js';
|
||||||
|
|
||||||
export {Align, Input, DummyInput, StatementInput, ValueInput, inputTypes};
|
export {
|
||||||
|
Align,
|
||||||
|
Input,
|
||||||
|
DummyInput,
|
||||||
|
EndRowInput,
|
||||||
|
StatementInput,
|
||||||
|
ValueInput,
|
||||||
|
inputTypes,
|
||||||
|
};
|
||||||
|
|||||||
31
core/inputs/end_row_input.ts
Normal file
31
core/inputs/end_row_input.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {Block} from '../block.js';
|
||||||
|
import {Input} from './input.js';
|
||||||
|
import {inputTypes} from './input_types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an input on a block that is always the last input in the row. Any
|
||||||
|
* following input will be rendered on the next row even if the block has inline
|
||||||
|
* inputs. Any newline character in a JSON block definition's message will
|
||||||
|
* automatically be parsed as an end-row input.
|
||||||
|
*/
|
||||||
|
export class EndRowInput extends Input {
|
||||||
|
readonly type = inputTypes.END_ROW;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name Language-neutral identifier which may used to find this input
|
||||||
|
* again.
|
||||||
|
* @param block The block containing this input.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
block: Block,
|
||||||
|
) {
|
||||||
|
super(name, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,8 @@ export enum inputTypes {
|
|||||||
DUMMY = 5,
|
DUMMY = 5,
|
||||||
// An unknown type of input defined by an external developer.
|
// An unknown type of input defined by an external developer.
|
||||||
CUSTOM = 6,
|
CUSTOM = 6,
|
||||||
|
// An input with no connections that is always the last input of a row. Any
|
||||||
|
// subsequent input will be rendered on the next row. Any newline character in
|
||||||
|
// a JSON block definition's message will be parsed as an end-row input.
|
||||||
|
END_ROW = 7,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {RenderedConnection} from '../../rendered_connection.js';
|
|||||||
import type {Measurable} from '../measurables/base.js';
|
import type {Measurable} from '../measurables/base.js';
|
||||||
import {BottomRow} from '../measurables/bottom_row.js';
|
import {BottomRow} from '../measurables/bottom_row.js';
|
||||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||||
|
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||||
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
||||||
import {Field} from '../measurables/field.js';
|
import {Field} from '../measurables/field.js';
|
||||||
import {Hat} from '../measurables/hat.js';
|
import {Hat} from '../measurables/hat.js';
|
||||||
@@ -326,9 +327,9 @@ export class RenderInfo {
|
|||||||
} else if (input instanceof ValueInput) {
|
} else if (input instanceof ValueInput) {
|
||||||
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
||||||
activeRow.hasExternalInput = true;
|
activeRow.hasExternalInput = true;
|
||||||
} else if (input instanceof DummyInput) {
|
} else if (input instanceof DummyInput || input instanceof EndRowInput) {
|
||||||
// Dummy inputs have no visual representation, but the information is
|
// Dummy and end-row inputs have no visual representation, but the
|
||||||
// still important.
|
// information is still important.
|
||||||
activeRow.minHeight = Math.max(
|
activeRow.minHeight = Math.max(
|
||||||
activeRow.minHeight,
|
activeRow.minHeight,
|
||||||
input.getSourceBlock() && input.getSourceBlock()!.isShadow()
|
input.getSourceBlock() && input.getSourceBlock()!.isShadow()
|
||||||
@@ -355,6 +356,11 @@ export class RenderInfo {
|
|||||||
if (!lastInput) {
|
if (!lastInput) {
|
||||||
return false;
|
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) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// A statement input or an input following one always gets a new row.
|
// A statement input or an input following one always gets a new row.
|
||||||
if (
|
if (
|
||||||
input instanceof StatementInput ||
|
input instanceof StatementInput ||
|
||||||
@@ -362,8 +368,13 @@ export class RenderInfo {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Value and dummy inputs get new row if inputs are not inlined.
|
// Value inputs, dummy inputs, and any input following an external value
|
||||||
if (input instanceof ValueInput || input instanceof DummyInput) {
|
// input get a new row if inputs are not inlined.
|
||||||
|
if (
|
||||||
|
input instanceof ValueInput ||
|
||||||
|
input instanceof DummyInput ||
|
||||||
|
lastInput instanceof ValueInput
|
||||||
|
) {
|
||||||
return !this.isInline;
|
return !this.isInline;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
|||||||
import type {Measurable} from '../measurables/base.js';
|
import type {Measurable} from '../measurables/base.js';
|
||||||
import type {BottomRow} from '../measurables/bottom_row.js';
|
import type {BottomRow} from '../measurables/bottom_row.js';
|
||||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||||
|
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||||
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
||||||
import type {Field} from '../measurables/field.js';
|
import type {Field} from '../measurables/field.js';
|
||||||
import {InRowSpacer} from '../measurables/in_row_spacer.js';
|
import {InRowSpacer} from '../measurables/in_row_spacer.js';
|
||||||
@@ -90,9 +91,9 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
} else if (input instanceof ValueInput) {
|
} else if (input instanceof ValueInput) {
|
||||||
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
||||||
activeRow.hasExternalInput = true;
|
activeRow.hasExternalInput = true;
|
||||||
} else if (input instanceof DummyInput) {
|
} else if (input instanceof DummyInput || input instanceof EndRowInput) {
|
||||||
// Dummy inputs have no visual representation, but the information is
|
// Dummy and end-row inputs have no visual representation, but the
|
||||||
// still important.
|
// information is still important.
|
||||||
activeRow.minHeight = Math.max(
|
activeRow.minHeight = Math.max(
|
||||||
activeRow.minHeight,
|
activeRow.minHeight,
|
||||||
this.constants_.DUMMY_INPUT_MIN_HEIGHT,
|
this.constants_.DUMMY_INPUT_MIN_HEIGHT,
|
||||||
@@ -379,8 +380,12 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
row.width < prevInput.width
|
row.width < prevInput.width
|
||||||
) {
|
) {
|
||||||
rowNextRightEdges.set(row, prevInput.width);
|
rowNextRightEdges.set(row, prevInput.width);
|
||||||
} else {
|
} else if (row.hasStatement) {
|
||||||
nextRightEdge = row.width;
|
nextRightEdge = row.width;
|
||||||
|
} else {
|
||||||
|
// To keep right edges of consecutive non-statement rows aligned, use
|
||||||
|
// the maximum width.
|
||||||
|
nextRightEdge = Math.max(nextRightEdge, row.width);
|
||||||
}
|
}
|
||||||
prevInput = row;
|
prevInput = row;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export class Row {
|
|||||||
hasInlineInput = false;
|
hasInlineInput = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the row has any dummy inputs.
|
* Whether the row has any dummy inputs or end-row inputs.
|
||||||
*/
|
*/
|
||||||
hasDummyInput = false;
|
hasDummyInput = false;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ goog.declareModuleId('Blockly.zelos.RenderInfo');
|
|||||||
|
|
||||||
import type {BlockSvg} from '../../block_svg.js';
|
import type {BlockSvg} from '../../block_svg.js';
|
||||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||||
|
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||||
import {FieldImage} from '../../field_image.js';
|
import {FieldImage} from '../../field_image.js';
|
||||||
import {FieldLabel} from '../../field_label.js';
|
import {FieldLabel} from '../../field_label.js';
|
||||||
import {FieldTextInput} from '../../field_textinput.js';
|
import {FieldTextInput} from '../../field_textinput.js';
|
||||||
@@ -124,6 +125,11 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
if (!lastInput) {
|
if (!lastInput) {
|
||||||
return false;
|
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) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// A statement input or an input following one always gets a new row.
|
// A statement input or an input following one always gets a new row.
|
||||||
if (
|
if (
|
||||||
input instanceof StatementInput ||
|
input instanceof StatementInput ||
|
||||||
@@ -131,8 +137,12 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Value and dummy inputs get new row if inputs are not inlined.
|
// Value, dummy, and end-row inputs get new row if inputs are not inlined.
|
||||||
if (input instanceof ValueInput || input instanceof DummyInput) {
|
if (
|
||||||
|
input instanceof ValueInput ||
|
||||||
|
input instanceof DummyInput ||
|
||||||
|
input instanceof EndRowInput
|
||||||
|
) {
|
||||||
return !this.isInline || this.isMultiRow;
|
return !this.isInline || this.isMultiRow;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -267,9 +277,9 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
override addInput_(input: Input, activeRow: Row) {
|
override addInput_(input: Input, activeRow: Row) {
|
||||||
// If we have two dummy inputs on the same row, one aligned left and the
|
// If we have two dummy inputs on the same row, one aligned left and the
|
||||||
// other right, keep track of the right aligned dummy input so we can add
|
// other right, keep track of the right aligned dummy input so we can add
|
||||||
// padding later.
|
// padding later. An end-row input after a dummy input also counts.
|
||||||
if (
|
if (
|
||||||
input instanceof DummyInput &&
|
(input instanceof DummyInput || input instanceof EndRowInput) &&
|
||||||
activeRow.hasDummyInput &&
|
activeRow.hasDummyInput &&
|
||||||
activeRow.align === Align.LEFT &&
|
activeRow.align === Align.LEFT &&
|
||||||
input.align === Align.RIGHT
|
input.align === Align.RIGHT
|
||||||
@@ -502,7 +512,7 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
const connectionWidth = this.outputConnection.width;
|
const connectionWidth = this.outputConnection.width;
|
||||||
const outerShape = this.outputConnection.shape.type;
|
const outerShape = this.outputConnection.shape.type;
|
||||||
const constants = this.constants_;
|
const constants = this.constants_;
|
||||||
if (this.isMultiRow && this.inputRows.length > 1) {
|
if (this.inputRows.length > 1) {
|
||||||
switch (outerShape) {
|
switch (outerShape) {
|
||||||
case constants.SHAPES.ROUND: {
|
case constants.SHAPES.ROUND: {
|
||||||
// Special case for multi-row round reporter blocks.
|
// Special case for multi-row round reporter blocks.
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ import * as colourUtils from './colour.js';
|
|||||||
*
|
*
|
||||||
* @param message Text which might contain string table references and
|
* @param message Text which might contain string table references and
|
||||||
* interpolation tokens.
|
* interpolation tokens.
|
||||||
* @param parseInterpolationTokens Option to parse numeric
|
* @param parseInterpolationTokens Option to parse numeric interpolation
|
||||||
* interpolation tokens (%1, %2, ...) when true.
|
* tokens (%1, %2, ...) when true.
|
||||||
|
* @param tokenizeNewlines Split individual newline characters into separate
|
||||||
|
* tokens when true.
|
||||||
* @returns Array of strings and numbers.
|
* @returns Array of strings and numbers.
|
||||||
*/
|
*/
|
||||||
function tokenizeInterpolationInternal(
|
function tokenizeInterpolationInternal(
|
||||||
message: string,
|
message: string,
|
||||||
parseInterpolationTokens: boolean,
|
parseInterpolationTokens: boolean,
|
||||||
|
tokenizeNewlines: boolean,
|
||||||
): (string | number)[] {
|
): (string | number)[] {
|
||||||
const tokens = [];
|
const tokens = [];
|
||||||
const chars = message.split('');
|
const chars = message.split('');
|
||||||
@@ -47,6 +50,15 @@ function tokenizeInterpolationInternal(
|
|||||||
}
|
}
|
||||||
buffer.length = 0;
|
buffer.length = 0;
|
||||||
state = 1;
|
state = 1;
|
||||||
|
} else if (tokenizeNewlines && c === '\n') {
|
||||||
|
// Output newline characters as single-character tokens, to be replaced
|
||||||
|
// with endOfRow dummies during interpolation.
|
||||||
|
const text = buffer.join('');
|
||||||
|
if (text) {
|
||||||
|
tokens.push(text);
|
||||||
|
}
|
||||||
|
buffer.length = 0;
|
||||||
|
tokens.push(c);
|
||||||
} else {
|
} else {
|
||||||
buffer.push(c); // Regular char.
|
buffer.push(c); // Regular char.
|
||||||
}
|
}
|
||||||
@@ -108,6 +120,7 @@ function tokenizeInterpolationInternal(
|
|||||||
tokenizeInterpolationInternal(
|
tokenizeInterpolationInternal(
|
||||||
rawValue,
|
rawValue,
|
||||||
parseInterpolationTokens,
|
parseInterpolationTokens,
|
||||||
|
tokenizeNewlines,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (parseInterpolationTokens) {
|
} else if (parseInterpolationTokens) {
|
||||||
@@ -137,11 +150,15 @@ function tokenizeInterpolationInternal(
|
|||||||
tokens.push(text);
|
tokens.push(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge adjacent text tokens into a single string.
|
// Merge adjacent text tokens into a single string (but if newlines should be
|
||||||
|
// tokenized, don't merge those with adjacent text).
|
||||||
const mergedTokens = [];
|
const mergedTokens = [];
|
||||||
buffer.length = 0;
|
buffer.length = 0;
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
if (typeof tokens[i] === 'string') {
|
if (
|
||||||
|
typeof tokens[i] === 'string' &&
|
||||||
|
!(tokenizeNewlines && tokens[i] === '\n')
|
||||||
|
) {
|
||||||
buffer.push(tokens[i] as string);
|
buffer.push(tokens[i] as string);
|
||||||
} else {
|
} else {
|
||||||
text = buffer.join('');
|
text = buffer.join('');
|
||||||
@@ -166,14 +183,15 @@ function tokenizeInterpolationInternal(
|
|||||||
* It will also replace string table references (e.g., %{bky_my_msg} and
|
* It will also replace string table references (e.g., %{bky_my_msg} and
|
||||||
* %{BKY_MY_MSG} will both be replaced with the value in
|
* %{BKY_MY_MSG} will both be replaced with the value in
|
||||||
* Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
|
* Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
|
||||||
* (e.g., '%%').
|
* (e.g., '%%'). Newline characters will also be output as string tokens
|
||||||
|
* containing a single newline character.
|
||||||
*
|
*
|
||||||
* @param message Text which might contain string table references and
|
* @param message Text which might contain string table references and
|
||||||
* interpolation tokens.
|
* interpolation tokens.
|
||||||
* @returns Array of strings and numbers.
|
* @returns Array of strings and numbers.
|
||||||
*/
|
*/
|
||||||
export function tokenizeInterpolation(message: string): (string | number)[] {
|
export function tokenizeInterpolation(message: string): (string | number)[] {
|
||||||
return tokenizeInterpolationInternal(message, true);
|
return tokenizeInterpolationInternal(message, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,9 +207,13 @@ export function replaceMessageReferences(message: string | any): string {
|
|||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
const interpolatedResult = tokenizeInterpolationInternal(message, false);
|
const interpolatedResult = tokenizeInterpolationInternal(
|
||||||
// When parseInterpolationTokens === false, interpolatedResult should be at
|
message,
|
||||||
// most length 1.
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// When parseInterpolationTokens and tokenizeNewlines are false,
|
||||||
|
// interpolatedResult should be at most length 1.
|
||||||
return interpolatedResult.length ? String(interpolatedResult[0]) : '';
|
return interpolatedResult.length ? String(interpolatedResult[0]) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export function blockToDom(
|
|||||||
const input = block.inputList[i];
|
const input = block.inputList[i];
|
||||||
let container: Element;
|
let container: Element;
|
||||||
let empty = true;
|
let empty = true;
|
||||||
if (input.type === inputTypes.DUMMY) {
|
if (input.type === inputTypes.DUMMY || input.type === inputTypes.END_ROW) {
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
const childBlock = input.connection!.targetBlock();
|
const childBlock = input.connection!.targetBlock();
|
||||||
|
|||||||
@@ -287,14 +287,16 @@ BlockDefinitionExtractor.parseInputs_ = function(block) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
BlockDefinitionExtractor.input_ = function(input, align) {
|
BlockDefinitionExtractor.input_ = function(input, align) {
|
||||||
var isDummy = (input.type === Blockly.DUMMY_INPUT);
|
var hasConnector = (input.type === Blockly.inputs.inputTypes.VALUE || input.type === Blockly.inputs.inputTypes.STATEMENT);
|
||||||
var inputTypeAttr =
|
var inputTypeAttr =
|
||||||
isDummy ? 'input_dummy' :
|
input.type === Blockly.inputs.inputTypes.DUMMY ? 'input_dummy' :
|
||||||
(input.type === Blockly.INPUT_VALUE) ? 'input_value' : 'input_statement';
|
input.type === Blockly.inputs.inputTypes.END_ROW ? 'input_end_row' :
|
||||||
|
input.type === Blockly.inputs.inputTypes.VALUE ? 'input_value' :
|
||||||
|
'input_statement';
|
||||||
var inputDefBlock =
|
var inputDefBlock =
|
||||||
BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr});
|
BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr});
|
||||||
|
|
||||||
if (!isDummy) {
|
if (hasConnector) {
|
||||||
inputDefBlock.append(BlockDefinitionExtractor.newDomElement_(
|
inputDefBlock.append(BlockDefinitionExtractor.newDomElement_(
|
||||||
'field', {name: 'INPUTNAME'}, input.name));
|
'field', {name: 'INPUTNAME'}, input.name));
|
||||||
}
|
}
|
||||||
@@ -307,7 +309,7 @@ BlockDefinitionExtractor.input_ = function(input, align) {
|
|||||||
fieldsDef.append(fieldsXml);
|
fieldsDef.append(fieldsXml);
|
||||||
inputDefBlock.append(fieldsDef);
|
inputDefBlock.append(fieldsDef);
|
||||||
|
|
||||||
if (!isDummy) {
|
if (hasConnector) {
|
||||||
var typeValue = BlockDefinitionExtractor.newDomElement_(
|
var typeValue = BlockDefinitionExtractor.newDomElement_(
|
||||||
'value', {name: 'TYPE'});
|
'value', {name: 'TYPE'});
|
||||||
typeValue.append(
|
typeValue.append(
|
||||||
|
|||||||
@@ -220,14 +220,33 @@ Blockly.Blocks['input_dummy'] = {
|
|||||||
"previousStatement": "Input",
|
"previousStatement": "Input",
|
||||||
"nextStatement": "Input",
|
"nextStatement": "Input",
|
||||||
"colour": 210,
|
"colour": 210,
|
||||||
"tooltip": "For adding fields on a separate row with no " +
|
"tooltip": "For adding fields without any block connections." +
|
||||||
"connections. Alignment options (left, right, centre) " +
|
"Alignment options (left, right, centre) only affect " +
|
||||||
"apply only to multi-line fields.",
|
"multi-row blocks.",
|
||||||
"helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293"
|
"helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Blockly.Blocks['input_end_row'] = {
|
||||||
|
// End-row input.
|
||||||
|
init: function() {
|
||||||
|
this.jsonInit({
|
||||||
|
"message0": "end-row input",
|
||||||
|
"message1": FIELD_MESSAGE,
|
||||||
|
"args1": FIELD_ARGS,
|
||||||
|
"previousStatement": "Input",
|
||||||
|
"nextStatement": "Input",
|
||||||
|
"colour": 210,
|
||||||
|
"tooltip": "For adding fields without any block connections that will " +
|
||||||
|
"be rendered on a separate row from any following inputs. " +
|
||||||
|
"Alignment options (left, right, centre) only affect " +
|
||||||
|
"multi-row blocks.",
|
||||||
|
"helpUrl": "https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks#block_inputs"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Blockly.Blocks['field_static'] = {
|
Blockly.Blocks['field_static'] = {
|
||||||
// Text value.
|
// Text value.
|
||||||
init: function() {
|
init: function() {
|
||||||
|
|||||||
@@ -177,7 +177,8 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
|
|||||||
|
|
||||||
var input = {type: contentsBlock.type};
|
var input = {type: contentsBlock.type};
|
||||||
// Dummy inputs don't have names. Other inputs do.
|
// Dummy inputs don't have names. Other inputs do.
|
||||||
if (contentsBlock.type !== 'input_dummy') {
|
if (contentsBlock.type !== 'input_dummy' &&
|
||||||
|
contentsBlock.type !== 'input_end_row') {
|
||||||
input.name = contentsBlock.getFieldValue('INPUTNAME');
|
input.name = contentsBlock.getFieldValue('INPUTNAME');
|
||||||
}
|
}
|
||||||
var check = JSON.parse(
|
var check = JSON.parse(
|
||||||
@@ -202,7 +203,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
|
|||||||
if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() !== '') {
|
if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() !== '') {
|
||||||
var align = lastInput.getFieldValue('ALIGN');
|
var align = lastInput.getFieldValue('ALIGN');
|
||||||
if (align !== 'LEFT') {
|
if (align !== 'LEFT') {
|
||||||
JS.lastDummyAlign0 = align;
|
JS.implicitAlign0 = align;
|
||||||
}
|
}
|
||||||
args.pop();
|
args.pop();
|
||||||
message.pop();
|
message.pop();
|
||||||
@@ -272,13 +273,15 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) {
|
|||||||
// Generate inputs.
|
// Generate inputs.
|
||||||
var TYPES = {'input_value': 'appendValueInput',
|
var TYPES = {'input_value': 'appendValueInput',
|
||||||
'input_statement': 'appendStatementInput',
|
'input_statement': 'appendStatementInput',
|
||||||
'input_dummy': 'appendDummyInput'};
|
'input_dummy': 'appendDummyInput',
|
||||||
|
'input_end_row': 'appendEndRowInput'};
|
||||||
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
|
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
|
||||||
while (contentsBlock) {
|
while (contentsBlock) {
|
||||||
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
|
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
|
||||||
var name = '';
|
var name = '';
|
||||||
// Dummy inputs don't have names. Other inputs do.
|
// Dummy inputs don't have names. Other inputs do.
|
||||||
if (contentsBlock.type !== 'input_dummy') {
|
if (contentsBlock.type !== 'input_dummy' &&
|
||||||
|
contentsBlock.type !== 'input_end_row') {
|
||||||
name =
|
name =
|
||||||
JSON.stringify(contentsBlock.getFieldValue('INPUTNAME'));
|
JSON.stringify(contentsBlock.getFieldValue('INPUTNAME'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,7 @@
|
|||||||
</value>
|
</value>
|
||||||
</block>
|
</block>
|
||||||
<block type="input_dummy"></block>
|
<block type="input_dummy"></block>
|
||||||
|
<block type="input_end_row"></block>
|
||||||
</category>
|
</category>
|
||||||
<category name="Field">
|
<category name="Field">
|
||||||
<block type="field_static"></block>
|
<block type="field_static"></block>
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ suite('Block JSON initialization', function () {
|
|||||||
'Block "test": Message index %2 out of range.',
|
'Block "test": Message index %2 out of range.',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Newline tokens are valid', function () {
|
||||||
|
this.assertNoError(['test', '\n', 'test'], 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('interpolateArguments_', function () {
|
suite('interpolateArguments_', function () {
|
||||||
@@ -312,6 +316,70 @@ suite('Block JSON initialization', function () {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('interpolation output includes end-row inputs', function () {
|
||||||
|
this.assertInterpolation(
|
||||||
|
['test1', {'type': 'input_end_row'}, 'test2'],
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_end_row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_dummy',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Newline is converted to end-row input', function () {
|
||||||
|
this.assertInterpolation(['test1', '\n', 'test2'], [], undefined, [
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_end_row',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_dummy',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Newline converted to end-row aligned like last dummy', function () {
|
||||||
|
this.assertInterpolation(['test1', '\n', 'test2'], [], 'CENTER', [
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_end_row',
|
||||||
|
'align': 'CENTER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'field_label',
|
||||||
|
'text': 'test2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'input_dummy',
|
||||||
|
'align': 'CENTER',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('fieldFromJson_', function () {
|
suite('fieldFromJson_', function () {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {ConnectionType} from '../../build/src/core/connection_type.js';
|
|||||||
import {createDeprecationWarningStub} from './test_helpers/warnings.js';
|
import {createDeprecationWarningStub} from './test_helpers/warnings.js';
|
||||||
import {createRenderedBlock} from './test_helpers/block_definitions.js';
|
import {createRenderedBlock} from './test_helpers/block_definitions.js';
|
||||||
import * as eventUtils from '../../build/src/core/events/utils.js';
|
import * as eventUtils from '../../build/src/core/events/utils.js';
|
||||||
|
import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js';
|
||||||
import {
|
import {
|
||||||
sharedTestSetup,
|
sharedTestSetup,
|
||||||
sharedTestTeardown,
|
sharedTestTeardown,
|
||||||
@@ -2494,4 +2495,25 @@ suite('Blocks', function () {
|
|||||||
chai.assert.isTrue(initCalled, 'expected init function to be called');
|
chai.assert.isTrue(initCalled, 'expected init function to be called');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('EndOfRow', function () {
|
||||||
|
setup(function () {
|
||||||
|
Blockly.defineBlocksWithJsonArray([
|
||||||
|
{
|
||||||
|
'type': 'end_row_test_block',
|
||||||
|
'message0': 'Row1\nRow2',
|
||||||
|
'inputsInline': true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test('Newline is converted to an end-row input', function () {
|
||||||
|
const block = this.workspace.newBlock('end_row_test_block');
|
||||||
|
chai.assert.equal(block.inputList[0].fieldRow[0].getValue(), 'Row1');
|
||||||
|
chai.assert.isTrue(
|
||||||
|
block.inputList[0] instanceof EndRowInput,
|
||||||
|
'newline should be converted to an end-row input',
|
||||||
|
);
|
||||||
|
chai.assert.equal(block.inputList[1].fieldRow[0].getValue(), 'Row2');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ suite('Utils', function () {
|
|||||||
['Hello%World'],
|
['Hello%World'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Newlines are tokenized', function () {
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
Blockly.utils.parsing.tokenizeInterpolation('Hello\nWorld'),
|
||||||
|
['Hello', '\n', 'World'],
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('Number interpolation', function () {
|
suite('Number interpolation', function () {
|
||||||
@@ -231,6 +238,14 @@ suite('Utils', function () {
|
|||||||
'Unrecognized % escape code treated as literal',
|
'Unrecognized % escape code treated as literal',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
resultString =
|
||||||
|
Blockly.utils.parsing.replaceMessageReferences('Hello\nWorld');
|
||||||
|
chai.assert.equal(
|
||||||
|
resultString,
|
||||||
|
'Hello\nWorld',
|
||||||
|
'Newlines are not tokenized',
|
||||||
|
);
|
||||||
|
|
||||||
resultString = Blockly.utils.parsing.replaceMessageReferences('%1');
|
resultString = Blockly.utils.parsing.replaceMessageReferences('%1');
|
||||||
chai.assert.equal(resultString, '%1', 'Interpolation tokens ignored.');
|
chai.assert.equal(resultString, '%1', 'Interpolation tokens ignored.');
|
||||||
resultString = Blockly.utils.parsing.replaceMessageReferences('%1 %2');
|
resultString = Blockly.utils.parsing.replaceMessageReferences('%1 %2');
|
||||||
|
|||||||
Reference in New Issue
Block a user