From a95aea24c48f31f8e19120f4b57b722a32f3598c Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 31 Mar 2023 13:13:46 -0700 Subject: [PATCH 1/8] feat: Parse message newlines as endOfRow dummies. --- core/block.ts | 25 ++++++++--- core/input.ts | 31 ++++++++++++++ core/renderers/common/info.ts | 5 +++ core/renderers/geras/info.ts | 3 +- core/utils/parsing.ts | 13 +++++- tests/mocha/block_json_test.js | 78 ++++++++++++++++++++++++++++++++++ tests/mocha/block_test.js | 19 +++++++++ 7 files changed, 165 insertions(+), 9 deletions(-) diff --git a/core/block.ts b/core/block.ts index 33dc56af39b..340f14a0249 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1684,7 +1684,7 @@ export class Block implements IASTNodeLocation, IDeletable { * with fields or inputs defined in the args array. * @param args Array of arguments to be interpolated. * @param lastDummyAlign If a dummy input is added at the end, how should it - * be aligned? + * be aligned? Also affects dummies created from newline tokens. * @param warningPrefix Warning prefix string identifying block. */ private interpolate_( @@ -1775,11 +1775,21 @@ export class Block implements IASTNodeLocation, IDeletable { } // Args can be strings, which is why this isn't elseif. if (typeof element === 'string') { - // AnyDuringMigration because: Type '{ text: string; type: string; } | - // null' is not assignable to type 'string | number'. - element = this.stringToFieldJson_(element) as AnyDuringMigration; - if (!element) { - continue; + if (element === '\n') { + // Convert newline tokens to dummies with endOfRow enabled. + const newlineInput = {'type': 'input_dummy', 'endOfRow': true}; + // Treat these as the "last" dummy for alignment purposes. + if (lastDummyAlign) { + (newlineInput as AnyDuringMigration)['align'] = lastDummyAlign; + } + 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); @@ -1869,6 +1879,9 @@ export class Block implements IASTNodeLocation, IDeletable { input.setAlign(alignment); } } + if (element['endOfRow'] != undefined) { + input.setEndOfRow(!!element['endOfRow']); + } return input; } diff --git a/core/input.ts b/core/input.ts index 09a8a2dc625..fa9b1300d83 100644 --- a/core/input.ts +++ b/core/input.ts @@ -32,6 +32,12 @@ export class Input { fieldRow: Field[] = []; /** Alignment of input's fields (left, right or centre). */ align = Align.LEFT; + + /** + * If true, any following input should always be rendered on a new row even if + * this input is rendered inline. + */ + private endOfRow = false; /** Is the input visible? */ private visible = true; @@ -249,6 +255,31 @@ export class Input { return this; } + /** + * If true, any following input should always be rendered on a new row even if + * this input is rendered inline. + * + * @returns True if this input should be the last input on its row. + */ + isEndOfRow(): boolean { + return this.endOfRow; + } + + /** + * Set whether the input should be the last input on its row. + * + * @param endOfRow Whether the input should be the last input on its row. + * @returns The input being modified (to allow chaining). + */ + setEndOfRow(endOfRow: boolean): Input { + this.endOfRow = endOfRow; + if (this.sourceBlock.rendered) { + const sourceBlock = this.sourceBlock as BlockSvg; + sourceBlock.queueRender(); + } + return this; + } + /** * Changes the connection's shadow block. * diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index 1a162a676d1..9ed7d0df73d 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -345,6 +345,11 @@ export class RenderInfo { if (!lastInput) { return false; } + // If the last input was marked as the end of a row, then the next input + // should always be on the next row. + if (lastInput.isEndOfRow()) { + return true; + } // A statement input or an input following one always gets a new row. if (input.type === inputTypes.STATEMENT || lastInput.type === inputTypes.STATEMENT) { diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index 78262cdb9fb..f6b08cd251c 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -360,7 +360,8 @@ export class RenderInfo extends BaseRenderInfo { row.width < prevInput.width) { rowNextRightEdges.set(row, prevInput.width); } else { - nextRightEdge = row.width; + // To avoid jagged right edges, use the maximum width. + nextRightEdge = Math.max(nextRightEdge, row.width); } prevInput = row; } diff --git a/core/utils/parsing.ts b/core/utils/parsing.ts index 4badd77fff4..3fb38569cd8 100644 --- a/core/utils/parsing.ts +++ b/core/utils/parsing.ts @@ -48,6 +48,15 @@ function tokenizeInterpolationInternal( } buffer.length = 0; state = 1; + } else if (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 { buffer.push(c); // Regular char. } @@ -132,11 +141,11 @@ function tokenizeInterpolationInternal( tokens.push(text); } - // Merge adjacent text tokens into a single string. + // Merge adjacent text tokens into a single string (excluding newline tokens). const mergedTokens = []; buffer.length = 0; for (let i = 0; i < tokens.length; i++) { - if (typeof tokens[i] === 'string') { + if (typeof tokens[i] === 'string' && tokens[i] !== '\n') { buffer.push(tokens[i] as string); } else { text = buffer.join(''); diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index b2b15f2e068..bc219e727b1 100644 --- a/tests/mocha/block_json_test.js +++ b/tests/mocha/block_json_test.js @@ -65,6 +65,10 @@ suite('Block JSON initialization', function() { this.assertError(['test', 2, 'test'], 1, 'Block "test": Message index %2 out of range.'); }); + + test('Accepts newline token', function() { + this.assertNoError(['test', '\n', 'test'], 0); + }); }); suite('interpolateArguments_', function() { @@ -285,6 +289,80 @@ suite('Block JSON initialization', function() { }, ]); }); + + test('Includes endOfRow', function() { + this.assertInterpolation( + ['test1', {'type': 'input_dummy', 'endOfRow': true}, 'test2'], + [], + undefined, + [ + { + 'type': 'field_label', + 'text': 'test1', + }, + { + 'type': 'input_dummy', + 'endOfRow': true, + }, + { + 'type': 'field_label', + 'text': 'test2', + }, + { + 'type': 'input_dummy', + }, + ]); + }); + + test('Converts newline to endOfRow', function() { + this.assertInterpolation( + ['test1', '\n', 'test2'], + [], + undefined, + [ + { + 'type': 'field_label', + 'text': 'test1', + }, + { + 'type': 'input_dummy', + 'endOfRow': true, + }, + { + 'type': 'field_label', + 'text': 'test2', + }, + { + 'type': 'input_dummy', + }, + ]); + }); + + test('Aligns endOfRow like last dummy', function() { + this.assertInterpolation( + ['test1', '\n', 'test2'], + [], + 'CENTER', + [ + { + 'type': 'field_label', + 'text': 'test1', + }, + { + 'type': 'input_dummy', + 'endOfRow': true, + 'align': 'CENTER', + }, + { + 'type': 'field_label', + 'text': 'test2', + }, + { + 'type': 'input_dummy', + 'align': 'CENTER', + }, + ]); + }); }); suite('fieldFromJson_', function() { diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 093c8ebba8e..ba5b76ef1d8 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -2062,4 +2062,23 @@ suite('Blocks', function() { chai.assert.isTrue(initCalled, 'expected init function to be called'); }); }); + + suite('EndOfRow', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([ + { + "type": "endOfRow_test_block", + "message0": "Row1\nRow2", + 'inputsInline': true, + }, + ]); + }); + test('Converts newline to dummy with endOfRow', function() { + const block = this.workspace.newBlock('endOfRow_test_block'); + chai.assert.equal(block.inputList[0].fieldRow[0].getValue(), 'Row1'); + chai.assert.isTrue(block.inputList[0].isEndOfRow(), + 'newline should be converted to dummy with endOfRow'); + chai.assert.equal(block.inputList[1].fieldRow[0].getValue(), 'Row2'); + }); + }); }); From 4e98cf8c8c86f537c2c16e124b1b02c81eb50dfc Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 31 Mar 2023 18:49:37 -0700 Subject: [PATCH 2/8] Fix the multilineinput field test. --- core/utils/parsing.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/utils/parsing.ts b/core/utils/parsing.ts index 3fb38569cd8..c6f30f0dd1d 100644 --- a/core/utils/parsing.ts +++ b/core/utils/parsing.ts @@ -21,8 +21,8 @@ import * as colourUtils from './colour.js'; * * @param message Text which might contain string table references and * interpolation tokens. - * @param parseInterpolationTokens Option to parse numeric - * interpolation tokens (%1, %2, ...) when true. + * @param parseInterpolationTokens Option to parse numeric interpolation + * tokens (%1, %2, ...) and newline characters when true. * @returns Array of strings and numbers. */ function tokenizeInterpolationInternal( @@ -48,7 +48,7 @@ function tokenizeInterpolationInternal( } buffer.length = 0; state = 1; - } else if (c === '\n') { + } else if (parseInterpolationTokens && c === '\n') { // Output newline characters as single-character tokens, to be replaced // with endOfRow dummies during interpolation. const text = buffer.join(''); From 7da76e3bb7793a670a59d50ab3644b5d6dd472fe Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 7 Apr 2023 14:33:31 -0700 Subject: [PATCH 3/8] Addressing PR feedback. --- core/block.ts | 4 ++-- core/renderers/geras/info.ts | 5 ++++- core/utils/parsing.ts | 9 ++++++--- tests/mocha/block_json_test.js | 8 ++++---- tests/mocha/block_test.js | 2 +- tests/mocha/utils_test.js | 9 +++++++++ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/core/block.ts b/core/block.ts index 340f14a0249..0ad9623238d 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1784,8 +1784,8 @@ export class Block implements IASTNodeLocation, IDeletable { } element = newlineInput as AnyDuringMigration; } else { - // AnyDuringMigration because: Type '{ text: string; type: string; } | - // null' is not assignable to type 'string | number'. + // AnyDuringMigration because: Type '{ text: string; type: string; } + // | null' is not assignable to type 'string | number'. element = this.stringToFieldJson_(element) as AnyDuringMigration; if (!element) { continue; diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index f6b08cd251c..10c77bf9c40 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -359,8 +359,11 @@ export class RenderInfo extends BaseRenderInfo { if (prevInput && prevInput.hasStatement && row.width < prevInput.width) { rowNextRightEdges.set(row, prevInput.width); + } else if (row.hasStatement) { + nextRightEdge = row.width; } else { - // To avoid jagged right edges, use the maximum width. + // To avoid jagged right edges along consecutive non-statement rows, + // use the maximum width. nextRightEdge = Math.max(nextRightEdge, row.width); } prevInput = row; diff --git a/core/utils/parsing.ts b/core/utils/parsing.ts index c6f30f0dd1d..342c87a10d3 100644 --- a/core/utils/parsing.ts +++ b/core/utils/parsing.ts @@ -141,11 +141,13 @@ function tokenizeInterpolationInternal( tokens.push(text); } - // Merge adjacent text tokens into a single string (excluding newline tokens). + // Merge adjacent text tokens into a single string (but if newlines should be + // tokenized, don't merge those with adjacent text). const mergedTokens = []; buffer.length = 0; for (let i = 0; i < tokens.length; i++) { - if (typeof tokens[i] === 'string' && tokens[i] !== '\n') { + if (typeof tokens[i] === 'string' && + !(parseInterpolationTokens && tokens[i] == '\n')) { buffer.push(tokens[i] as string); } else { text = buffer.join(''); @@ -170,7 +172,8 @@ function tokenizeInterpolationInternal( * It will also replace string table references (e.g., %{bky_my_msg} and * %{BKY_MY_MSG} will both be replaced with the value in * 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 * interpolation tokens. diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index bc219e727b1..21bff7b011f 100644 --- a/tests/mocha/block_json_test.js +++ b/tests/mocha/block_json_test.js @@ -66,7 +66,7 @@ suite('Block JSON initialization', function() { 'Block "test": Message index %2 out of range.'); }); - test('Accepts newline token', function() { + test('Newline tokens are valid', function() { this.assertNoError(['test', '\n', 'test'], 0); }); }); @@ -290,7 +290,7 @@ suite('Block JSON initialization', function() { ]); }); - test('Includes endOfRow', function() { + test('endOfRow property is included in interpolation output', function() { this.assertInterpolation( ['test1', {'type': 'input_dummy', 'endOfRow': true}, 'test2'], [], @@ -314,7 +314,7 @@ suite('Block JSON initialization', function() { ]); }); - test('Converts newline to endOfRow', function() { + test('Newline is converted to dummy with endOfRow', function() { this.assertInterpolation( ['test1', '\n', 'test2'], [], @@ -338,7 +338,7 @@ suite('Block JSON initialization', function() { ]); }); - test('Aligns endOfRow like last dummy', function() { + test('Newline converted to dummy aligned like last dummy', function() { this.assertInterpolation( ['test1', '\n', 'test2'], [], diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index ba5b76ef1d8..935e40fd534 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -2073,7 +2073,7 @@ suite('Blocks', function() { }, ]); }); - test('Converts newline to dummy with endOfRow', function() { + test('Newline is converted to a dummy with endOfRow set', function() { const block = this.workspace.newBlock('endOfRow_test_block'); chai.assert.equal(block.inputList[0].fieldRow[0].getValue(), 'Row1'); chai.assert.isTrue(block.inputList[0].isEndOfRow(), diff --git a/tests/mocha/utils_test.js b/tests/mocha/utils_test.js index c70391d9b6d..5968ee4c0d4 100644 --- a/tests/mocha/utils_test.js +++ b/tests/mocha/utils_test.js @@ -49,6 +49,12 @@ suite('Utils', function() { Blockly.utils.parsing.tokenizeInterpolation('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() { @@ -198,6 +204,9 @@ suite('Utils', function() { resultString = Blockly.utils.parsing.replaceMessageReferences('%a'); chai.assert.equal(resultString, '%a', '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'); chai.assert.equal(resultString, '%1', 'Interpolation tokens ignored.'); resultString = Blockly.utils.parsing.replaceMessageReferences('%1 %2'); From ca6b0afc2f03aa71a874267570758205e250c9d5 Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 14 Apr 2023 12:38:38 -0700 Subject: [PATCH 4/8] Addressing PR feedback. --- core/block.ts | 31 +++++++++++++++++-------------- core/utils/parsing.ts | 23 ++++++++++++++--------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/core/block.ts b/core/block.ts index 0ad9623238d..b4eb06450a6 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1551,8 +1551,11 @@ export class Block implements IASTNodeLocation, IDeletable { let i = 0; while (json['message' + i] !== undefined) { this.interpolate_( - json['message' + i], json['args' + i] || [], - json['lastDummyAlign' + i], warningPrefix); + json['message' + i], + json['args' + i] || [], + // Backwards compatibility: lastDummyAlign aliases implicitDummyAlign. + json['lastDummyAlign' + i] || json['implicitDummyAlign' + i], + warningPrefix); i++; } @@ -1683,16 +1686,17 @@ export class Block implements IASTNodeLocation, IDeletable { * @param message Text contains interpolation tokens (%1, %2, ...) that match * with fields or inputs defined in the args array. * @param args Array of arguments to be interpolated. - * @param lastDummyAlign If a dummy input is added at the end, how should it - * be aligned? Also affects dummies created from newline tokens. + * @param implicitDummyAlign If an implicit dummy input is added at the end or + * in place of newline tokens, how should it be aligned? * @param warningPrefix Warning prefix string identifying block. */ private interpolate_( message: string, args: AnyDuringMigration[], - lastDummyAlign: string|undefined, warningPrefix: string) { + implicitDummyAlign: string|undefined, warningPrefix: string) { const tokens = parsing.tokenizeInterpolation(message); this.validateTokens_(tokens, args.length); - const elements = this.interpolateArguments_(tokens, args, lastDummyAlign); + const elements = this.interpolateArguments_( + tokens, args, implicitDummyAlign); // An array of [field, fieldName] tuples. const fieldStack = []; @@ -1760,13 +1764,13 @@ export class Block implements IASTNodeLocation, IDeletable { * * @param tokens The tokens to interpolate * @param args The arguments to insert. - * @param lastDummyAlign The alignment the added dummy input should have, if - * we are required to add one. + * @param implicitDummyAlign The alignment any added implicit dummies input + * should have, if we are required to add one. * @returns The JSON definitions of field and inputs to add to the block. */ private interpolateArguments_( tokens: Array, args: Array, - lastDummyAlign: string|undefined): AnyDuringMigration[] { + implicitDummyAlign: string|undefined): AnyDuringMigration[] { const elements = []; for (let i = 0; i < tokens.length; i++) { let element = tokens[i]; @@ -1778,9 +1782,8 @@ export class Block implements IASTNodeLocation, IDeletable { if (element === '\n') { // Convert newline tokens to dummies with endOfRow enabled. const newlineInput = {'type': 'input_dummy', 'endOfRow': true}; - // Treat these as the "last" dummy for alignment purposes. - if (lastDummyAlign) { - (newlineInput as AnyDuringMigration)['align'] = lastDummyAlign; + if (implicitDummyAlign) { + (newlineInput as AnyDuringMigration)['align'] = implicitDummyAlign; } element = newlineInput as AnyDuringMigration; } else { @@ -1800,8 +1803,8 @@ export class Block implements IASTNodeLocation, IDeletable { !this.isInputKeyword_( (elements as AnyDuringMigration)[length - 1]['type'])) { const dummyInput = {'type': 'input_dummy'}; - if (lastDummyAlign) { - (dummyInput as AnyDuringMigration)['align'] = lastDummyAlign; + if (implicitDummyAlign) { + (dummyInput as AnyDuringMigration)['align'] = implicitDummyAlign; } elements.push(dummyInput); } diff --git a/core/utils/parsing.ts b/core/utils/parsing.ts index 342c87a10d3..ef5553d3df2 100644 --- a/core/utils/parsing.ts +++ b/core/utils/parsing.ts @@ -22,11 +22,15 @@ import * as colourUtils from './colour.js'; * @param message Text which might contain string table references and * interpolation tokens. * @param parseInterpolationTokens Option to parse numeric interpolation - * tokens (%1, %2, ...) and newline characters when true. + * tokens (%1, %2, ...) when true. + * @param tokenizeNewlines Split individual newline characters into separate + * tokens when true. * @returns Array of strings and numbers. */ function tokenizeInterpolationInternal( - message: string, parseInterpolationTokens: boolean): (string|number)[] { + message: string, + parseInterpolationTokens: boolean, + tokenizeNewlines: boolean): (string|number)[] { const tokens = []; const chars = message.split(''); chars.push(''); // End marker. @@ -48,7 +52,7 @@ function tokenizeInterpolationInternal( } buffer.length = 0; state = 1; - } else if (parseInterpolationTokens && c === '\n') { + } else if (tokenizeNewlines && c === '\n') { // Output newline characters as single-character tokens, to be replaced // with endOfRow dummies during interpolation. const text = buffer.join(''); @@ -113,7 +117,7 @@ function tokenizeInterpolationInternal( Array.prototype.push.apply( tokens, tokenizeInterpolationInternal( - rawValue, parseInterpolationTokens)); + rawValue, parseInterpolationTokens, tokenizeNewlines)); } else if (parseInterpolationTokens) { // When parsing interpolation tokens, numbers are special // placeholders (%1, %2, etc). Make sure all other values are @@ -147,7 +151,7 @@ function tokenizeInterpolationInternal( buffer.length = 0; for (let i = 0; i < tokens.length; i++) { if (typeof tokens[i] === 'string' && - !(parseInterpolationTokens && tokens[i] == '\n')) { + !(tokenizeNewlines && tokens[i] === '\n')) { buffer.push(tokens[i] as string); } else { text = buffer.join(''); @@ -180,7 +184,7 @@ function tokenizeInterpolationInternal( * @returns Array of strings and numbers. */ export function tokenizeInterpolation(message: string): (string|number)[] { - return tokenizeInterpolationInternal(message, true); + return tokenizeInterpolationInternal(message, true, true); } /** @@ -196,9 +200,10 @@ export function replaceMessageReferences(message: string|any): string { if (typeof message !== 'string') { return message; } - const interpolatedResult = tokenizeInterpolationInternal(message, false); - // When parseInterpolationTokens === false, interpolatedResult should be at - // most length 1. + const interpolatedResult = + tokenizeInterpolationInternal(message, false, false); + // When parseInterpolationTokens and tokenizeNewlines are false, + // interpolatedResult should be at most length 1. return interpolatedResult.length ? String(interpolatedResult[0]) : ''; } From c52b7e1daf42fdec9ce72e870c3609d5a523f663 Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 4 Aug 2023 15:59:33 -0700 Subject: [PATCH 5/8] Newline parsing now uses a new custom input. --- core/block.ts | 61 ++++++++++++++++++++----------- core/inputs.ts | 3 +- core/inputs/end_row_input.ts | 31 ++++++++++++++++ core/inputs/input.ts | 31 ---------------- core/inputs/input_types.ts | 4 ++ core/renderers/common/info.ts | 9 +++-- core/renderers/geras/info.ts | 3 +- core/renderers/measurables/row.ts | 2 +- core/renderers/zelos/info.ts | 12 ++++-- core/xml.ts | 2 +- tests/mocha/block_json_test.js | 17 ++++----- tests/mocha/block_test.js | 11 +++--- 12 files changed, 108 insertions(+), 78 deletions(-) create mode 100644 core/inputs/end_row_input.ts diff --git a/core/block.ts b/core/block.ts index f738a02ba28..9cd22fa2354 100644 --- a/core/block.ts +++ b/core/block.ts @@ -48,6 +48,7 @@ import {Size} from './utils/size.js'; import type {VariableModel} from './variable_model.js'; import type {Workspace} from './workspace.js'; import {DummyInput} from './inputs/dummy_input.js'; +import {EndRowInput} from './inputs/end_row_input.js'; import {ValueInput} from './inputs/value_input.js'; import {StatementInput} from './inputs/statement_input.js'; import {IconType} from './icons/icon_types.js'; @@ -1339,6 +1340,12 @@ export class Block implements IASTNodeLocation, IDeletable { 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; } @@ -1560,6 +1567,17 @@ export class Block implements IASTNodeLocation, IDeletable { 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. * @@ -1628,8 +1646,8 @@ export class Block implements IASTNodeLocation, IDeletable { this.interpolate_( json['message' + i], json['args' + i] || [], - // Backwards compatibility: lastDummyAlign aliases implicitDummyAlign. - json['lastDummyAlign' + i] || json['implicitDummyAlign' + i], + // Backwards compatibility: lastDummyAlign aliases implicitAlign. + json['lastDummyAlign' + i] || json['implicitAlign' + i], warningPrefix, ); i++; @@ -1766,20 +1784,19 @@ export class Block implements IASTNodeLocation, IDeletable { * @param message Text contains interpolation tokens (%1, %2, ...) that match * with fields or inputs defined in the args array. * @param args Array of arguments to be interpolated. - * @param implicitDummyAlign If an implicit dummy input is added at the end or - * in place of newline tokens, how should it be aligned? + * @param implicitAlign If an implicit input is added at the end or in place + * of newline tokens, how should it be aligned? * @param warningPrefix Warning prefix string identifying block. */ private interpolate_( message: string, args: AnyDuringMigration[], - implicitDummyAlign: string | undefined, + implicitAlign: string | undefined, warningPrefix: string, ) { const tokens = parsing.tokenizeInterpolation(message); this.validateTokens_(tokens, args.length); - const elements = this.interpolateArguments_( - tokens, args, implicitDummyAlign); + const elements = this.interpolateArguments_(tokens, args, implicitAlign); // An array of [field, fieldName] tuples. const fieldStack = []; @@ -1857,19 +1874,20 @@ export class Block implements IASTNodeLocation, IDeletable { /** * 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 - * to the end of the elements. + * JSON that defines a label field. Newline characters are converted to + * 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 args The arguments to insert. - * @param implicitDummyAlign The alignment any added implicit dummies input - * should have, if we are required to add one. + * @param implicitAlign The alignment to use for any implicitly added end-row + * or dummy inputs, if necessary. * @returns The JSON definitions of field and inputs to add to the block. */ private interpolateArguments_( tokens: Array, args: Array, - implicitDummyAlign: string | undefined, + implicitAlign: string | undefined, ): AnyDuringMigration[] { const elements = []; for (let i = 0; i < tokens.length; i++) { @@ -1880,10 +1898,10 @@ export class Block implements IASTNodeLocation, IDeletable { // Args can be strings, which is why this isn't elseif. if (typeof element === 'string') { if (element === '\n') { - // Convert newline tokens to dummies with endOfRow enabled. - const newlineInput = {'type': 'input_dummy', 'endOfRow': true}; - if (implicitDummyAlign) { - (newlineInput as AnyDuringMigration)['align'] = implicitDummyAlign; + // Convert newline tokens to end-row inputs. + const newlineInput = {'type': 'input_end_row'}; + if (implicitAlign) { + (newlineInput as AnyDuringMigration)['align'] = implicitAlign; } element = newlineInput as AnyDuringMigration; } else { @@ -1906,8 +1924,8 @@ export class Block implements IASTNodeLocation, IDeletable { ) ) { const dummyInput = {'type': 'input_dummy'}; - if (implicitDummyAlign) { - (dummyInput as AnyDuringMigration)['align'] = implicitDummyAlign; + if (implicitAlign) { + (dummyInput as AnyDuringMigration)['align'] = implicitAlign; } elements.push(dummyInput); } @@ -1971,6 +1989,9 @@ export class Block implements IASTNodeLocation, IDeletable { case 'input_dummy': input = this.appendDummyInput(element['name']); break; + case 'input_end_row': + input = this.appendEndRowInput(element['name']); + break; default: { input = this.appendInputFromRegistry(element['type'], element['name']); break; @@ -1994,9 +2015,6 @@ export class Block implements IASTNodeLocation, IDeletable { input.setAlign(alignment); } } - if (element['endOfRow'] != undefined) { - input.setEndOfRow(!!element['endOfRow']); - } return input; } @@ -2012,6 +2030,7 @@ export class Block implements IASTNodeLocation, IDeletable { str === 'input_value' || str === 'input_statement' || str === 'input_dummy' || + str === 'input_end_row' || registry.hasItem(registry.Type.INPUT, str) ); } diff --git a/core/inputs.ts b/core/inputs.ts index 8bd23d7908e..48360d701bc 100644 --- a/core/inputs.ts +++ b/core/inputs.ts @@ -7,8 +7,9 @@ import {Align} from './inputs/align.js'; import {Input} from './inputs/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 {ValueInput} from './inputs/value_input.js'; import {inputTypes} from './inputs/input_types.js'; -export {Align, Input, DummyInput, StatementInput, ValueInput, inputTypes}; +export {Align, Input, DummyInput, EndRowInput, StatementInput, ValueInput, inputTypes}; diff --git a/core/inputs/end_row_input.ts b/core/inputs/end_row_input.ts new file mode 100644 index 00000000000..58227a09457 --- /dev/null +++ b/core/inputs/end_row_input.ts @@ -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); + } +} diff --git a/core/inputs/input.ts b/core/inputs/input.ts index d309c41e48f..31d3773b663 100644 --- a/core/inputs/input.ts +++ b/core/inputs/input.ts @@ -29,12 +29,6 @@ export class Input { fieldRow: Field[] = []; /** Alignment of input's fields (left, right or centre). */ align = Align.LEFT; - - /** - * If true, any following input should always be rendered on a new row even if - * this input is rendered inline. - */ - private endOfRow = false; /** Is the input visible? */ private visible = true; @@ -252,31 +246,6 @@ export class Input { return this; } - /** - * If true, any following input should always be rendered on a new row even if - * this input is rendered inline. - * - * @returns True if this input should be the last input on its row. - */ - isEndOfRow(): boolean { - return this.endOfRow; - } - - /** - * Set whether the input should be the last input on its row. - * - * @param endOfRow Whether the input should be the last input on its row. - * @returns The input being modified (to allow chaining). - */ - setEndOfRow(endOfRow: boolean): Input { - this.endOfRow = endOfRow; - if (this.sourceBlock.rendered) { - const sourceBlock = this.sourceBlock as BlockSvg; - sourceBlock.queueRender(); - } - return this; - } - /** * Changes the connection's shadow block. * diff --git a/core/inputs/input_types.ts b/core/inputs/input_types.ts index ff768deb266..a51537be792 100644 --- a/core/inputs/input_types.ts +++ b/core/inputs/input_types.ts @@ -21,4 +21,8 @@ export enum inputTypes { DUMMY = 5, // An unknown type of input defined by an external developer. 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, } diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index c22d36c063d..433b8578f77 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -14,6 +14,7 @@ import type {RenderedConnection} from '../../rendered_connection.js'; import type {Measurable} from '../measurables/base.js'; import {BottomRow} from '../measurables/bottom_row.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 {Field} from '../measurables/field.js'; import {Hat} from '../measurables/hat.js'; @@ -326,7 +327,7 @@ export class RenderInfo { } else if (input instanceof ValueInput) { activeRow.elements.push(new ExternalValueInput(this.constants_, input)); 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 // still important. activeRow.minHeight = Math.max( @@ -355,9 +356,9 @@ export class RenderInfo { if (!lastInput) { return false; } - // If the last input was marked as the end of a row, then the next input - // should always be on the next row. - if (lastInput.isEndOfRow()) { + // 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. diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index c600ee56122..9559facae1b 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -13,6 +13,7 @@ import {RenderInfo as BaseRenderInfo} from '../common/info.js'; import type {Measurable} from '../measurables/base.js'; import type {BottomRow} from '../measurables/bottom_row.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 type {Field} from '../measurables/field.js'; import {InRowSpacer} from '../measurables/in_row_spacer.js'; @@ -90,7 +91,7 @@ export class RenderInfo extends BaseRenderInfo { } else if (input instanceof ValueInput) { activeRow.elements.push(new ExternalValueInput(this.constants_, input)); 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 // still important. activeRow.minHeight = Math.max( diff --git a/core/renderers/measurables/row.ts b/core/renderers/measurables/row.ts index c8116a2fc88..e698688de87 100644 --- a/core/renderers/measurables/row.ts +++ b/core/renderers/measurables/row.ts @@ -89,7 +89,7 @@ export class Row { hasInlineInput = false; /** - * Whether the row has any dummy inputs. + * Whether the row has any dummy inputs or end-row inputs. */ hasDummyInput = false; diff --git a/core/renderers/zelos/info.ts b/core/renderers/zelos/info.ts index d806fb0b669..a82a1b80e8f 100644 --- a/core/renderers/zelos/info.ts +++ b/core/renderers/zelos/info.ts @@ -9,6 +9,7 @@ goog.declareModuleId('Blockly.zelos.RenderInfo'); import type {BlockSvg} from '../../block_svg.js'; import {DummyInput} from '../../inputs/dummy_input.js'; +import {EndRowInput} from '../../inputs/end_row_input.js'; import {FieldImage} from '../../field_image.js'; import {FieldLabel} from '../../field_label.js'; import {FieldTextInput} from '../../field_textinput.js'; @@ -124,6 +125,11 @@ export class RenderInfo extends BaseRenderInfo { if (!lastInput) { 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. if ( input instanceof StatementInput || @@ -267,9 +273,9 @@ export class RenderInfo extends BaseRenderInfo { override addInput_(input: Input, activeRow: Row) { // 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 - // padding later. + // padding later. An end-row input after a dummy input also counts. if ( - input instanceof DummyInput && + (input instanceof DummyInput || input instanceof EndRowInput) && activeRow.hasDummyInput && activeRow.align === Align.LEFT && input.align === Align.RIGHT @@ -502,7 +508,7 @@ export class RenderInfo extends BaseRenderInfo { const connectionWidth = this.outputConnection.width; const outerShape = this.outputConnection.shape.type; const constants = this.constants_; - if (this.isMultiRow && this.inputRows.length > 1) { + if (this.inputRows.length > 1) { switch (outerShape) { case constants.SHAPES.ROUND: { // Special case for multi-row round reporter blocks. diff --git a/core/xml.ts b/core/xml.ts index b74e6e71964..469f6cc450e 100644 --- a/core/xml.ts +++ b/core/xml.ts @@ -213,7 +213,7 @@ export function blockToDom( const input = block.inputList[i]; let container: Element; let empty = true; - if (input.type === inputTypes.DUMMY) { + if (input.type === inputTypes.DUMMY || input.type === inputTypes.END_ROW) { continue; } else { const childBlock = input.connection!.targetBlock(); diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index 61d81cd23be..0cd130e0086 100644 --- a/tests/mocha/block_json_test.js +++ b/tests/mocha/block_json_test.js @@ -317,9 +317,9 @@ suite('Block JSON initialization', function () { ]); }); - test('endOfRow property is included in interpolation output', function() { + test('interpolation output includes end-row inputs', function() { this.assertInterpolation( - ['test1', {'type': 'input_dummy', 'endOfRow': true}, 'test2'], + ['test1', {'type': 'input_end_row'}, 'test2'], [], undefined, [ @@ -328,8 +328,7 @@ suite('Block JSON initialization', function () { 'text': 'test1', }, { - 'type': 'input_dummy', - 'endOfRow': true, + 'type': 'input_end_row', }, { 'type': 'field_label', @@ -341,7 +340,7 @@ suite('Block JSON initialization', function () { ]); }); - test('Newline is converted to dummy with endOfRow', function() { + test('Newline is converted to end-row input', function() { this.assertInterpolation( ['test1', '\n', 'test2'], [], @@ -352,8 +351,7 @@ suite('Block JSON initialization', function () { 'text': 'test1', }, { - 'type': 'input_dummy', - 'endOfRow': true, + 'type': 'input_end_row', }, { 'type': 'field_label', @@ -365,7 +363,7 @@ suite('Block JSON initialization', function () { ]); }); - test('Newline converted to dummy aligned like last dummy', function() { + test('Newline converted to end-row aligned like last dummy', function() { this.assertInterpolation( ['test1', '\n', 'test2'], [], @@ -376,8 +374,7 @@ suite('Block JSON initialization', function () { 'text': 'test1', }, { - 'type': 'input_dummy', - 'endOfRow': true, + 'type': 'input_end_row', 'align': 'CENTER', }, { diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 8487423d3a0..4676de312ab 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -10,6 +10,7 @@ import {ConnectionType} from '../../build/src/core/connection_type.js'; import {createDeprecationWarningStub} from './test_helpers/warnings.js'; import {createRenderedBlock} from './test_helpers/block_definitions.js'; import * as eventUtils from '../../build/src/core/events/utils.js'; +import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js'; import { sharedTestSetup, sharedTestTeardown, @@ -2407,17 +2408,17 @@ suite('Blocks', function () { setup(function() { Blockly.defineBlocksWithJsonArray([ { - "type": "endOfRow_test_block", + "type": "end_row_test_block", "message0": "Row1\nRow2", 'inputsInline': true, }, ]); }); - test('Newline is converted to a dummy with endOfRow set', function() { - const block = this.workspace.newBlock('endOfRow_test_block'); + 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].isEndOfRow(), - 'newline should be converted to dummy with endOfRow'); + 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'); }); }); From e3a78c8e925ea8d88e4c9ba5970a4fcde992231a Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 4 Aug 2023 16:12:48 -0700 Subject: [PATCH 6/8] npm run format --- core/inputs.ts | 10 ++- core/utils/parsing.ts | 19 +++-- tests/mocha/block_json_test.js | 123 ++++++++++++++++----------------- tests/mocha/block_test.js | 16 +++-- tests/mocha/utils_test.js | 16 +++-- 5 files changed, 99 insertions(+), 85 deletions(-) diff --git a/core/inputs.ts b/core/inputs.ts index 48360d701bc..4b7bfa89750 100644 --- a/core/inputs.ts +++ b/core/inputs.ts @@ -12,4 +12,12 @@ import {StatementInput} from './inputs/statement_input.js'; import {ValueInput} from './inputs/value_input.js'; import {inputTypes} from './inputs/input_types.js'; -export {Align, Input, DummyInput, EndRowInput, StatementInput, ValueInput, inputTypes}; +export { + Align, + Input, + DummyInput, + EndRowInput, + StatementInput, + ValueInput, + inputTypes, +}; diff --git a/core/utils/parsing.ts b/core/utils/parsing.ts index e132d69080f..5d2c65dc1b3 100644 --- a/core/utils/parsing.ts +++ b/core/utils/parsing.ts @@ -118,9 +118,9 @@ function tokenizeInterpolationInternal( Array.prototype.push.apply( tokens, tokenizeInterpolationInternal( - rawValue, - parseInterpolationTokens, - tokenizeNewlines, + rawValue, + parseInterpolationTokens, + tokenizeNewlines, ), ); } else if (parseInterpolationTokens) { @@ -155,8 +155,10 @@ function tokenizeInterpolationInternal( const mergedTokens = []; buffer.length = 0; for (let i = 0; i < tokens.length; i++) { - if (typeof tokens[i] === 'string' && - !(tokenizeNewlines && tokens[i] === '\n')) { + if ( + typeof tokens[i] === 'string' && + !(tokenizeNewlines && tokens[i] === '\n') + ) { buffer.push(tokens[i] as string); } else { text = buffer.join(''); @@ -205,8 +207,11 @@ export function replaceMessageReferences(message: string | any): string { if (typeof message !== 'string') { return message; } - const interpolatedResult = - tokenizeInterpolationInternal(message, false, false); + const interpolatedResult = tokenizeInterpolationInternal( + message, + false, + false, + ); // When parseInterpolationTokens and tokenizeNewlines are false, // interpolatedResult should be at most length 1. return interpolatedResult.length ? String(interpolatedResult[0]) : ''; diff --git a/tests/mocha/block_json_test.js b/tests/mocha/block_json_test.js index 0cd130e0086..cd4337ef03f 100644 --- a/tests/mocha/block_json_test.js +++ b/tests/mocha/block_json_test.js @@ -93,7 +93,7 @@ suite('Block JSON initialization', function () { ); }); - test('Newline tokens are valid', function() { + test('Newline tokens are valid', function () { this.assertNoError(['test', '\n', 'test'], 0); }); }); @@ -317,75 +317,68 @@ suite('Block JSON initialization', function () { ]); }); - test('interpolation output includes end-row inputs', 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', - }, - ]); + ['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 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', - }, - ]); + 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', + }, + ]); }); }); diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 4676de312ab..902f345174e 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -2404,21 +2404,23 @@ suite('Blocks', function () { }); }); - suite('EndOfRow', function() { - setup(function() { + suite('EndOfRow', function () { + setup(function () { Blockly.defineBlocksWithJsonArray([ { - "type": "end_row_test_block", - "message0": "Row1\nRow2", + 'type': 'end_row_test_block', + 'message0': 'Row1\nRow2', 'inputsInline': true, }, ]); }); - test('Newline is converted to an end-row input', function() { + 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.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'); }); }); diff --git a/tests/mocha/utils_test.js b/tests/mocha/utils_test.js index d2daa86f123..0d2b96a6010 100644 --- a/tests/mocha/utils_test.js +++ b/tests/mocha/utils_test.js @@ -59,10 +59,11 @@ suite('Utils', function () { ); }); - test('Newlines are tokenized', function() { + test('Newlines are tokenized', function () { chai.assert.deepEqual( - Blockly.utils.parsing.tokenizeInterpolation('Hello\nWorld'), - ['Hello', '\n', 'World']); + Blockly.utils.parsing.tokenizeInterpolation('Hello\nWorld'), + ['Hello', '\n', 'World'], + ); }); }); @@ -237,8 +238,13 @@ suite('Utils', function () { '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('Hello\nWorld'); + chai.assert.equal( + resultString, + 'Hello\nWorld', + 'Newlines are not tokenized', + ); resultString = Blockly.utils.parsing.replaceMessageReferences('%1'); chai.assert.equal(resultString, '%1', 'Interpolation tokens ignored.'); From 02808cb139c8bb1c72de0032827501ac40f5a7d2 Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 4 Aug 2023 17:14:31 -0700 Subject: [PATCH 7/8] Added input_end_row to block factory. --- .../blockfactory/block_definition_extractor.js | 12 +++++++----- demos/blockfactory/blocks.js | 18 ++++++++++++++++++ demos/blockfactory/factory_utils.js | 9 ++++++--- demos/blockfactory/index.html | 1 + 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/demos/blockfactory/block_definition_extractor.js b/demos/blockfactory/block_definition_extractor.js index 35186ac845c..e6b1cedd619 100644 --- a/demos/blockfactory/block_definition_extractor.js +++ b/demos/blockfactory/block_definition_extractor.js @@ -287,14 +287,16 @@ BlockDefinitionExtractor.parseInputs_ = function(block) { * @private */ 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 = - isDummy ? 'input_dummy' : - (input.type === Blockly.INPUT_VALUE) ? 'input_value' : 'input_statement'; + input.type === Blockly.inputs.inputTypes.DUMMY ? 'input_dummy' : + input.type === Blockly.inputs.inputTypes.END_ROW ? 'input_end_row' : + input.type === Blockly.inputs.inputTypes.VALUE ? 'input_value' : + 'input_statement'; var inputDefBlock = BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr}); - if (!isDummy) { + if (hasConnector) { inputDefBlock.append(BlockDefinitionExtractor.newDomElement_( 'field', {name: 'INPUTNAME'}, input.name)); } @@ -307,7 +309,7 @@ BlockDefinitionExtractor.input_ = function(input, align) { fieldsDef.append(fieldsXml); inputDefBlock.append(fieldsDef); - if (!isDummy) { + if (hasConnector) { var typeValue = BlockDefinitionExtractor.newDomElement_( 'value', {name: 'TYPE'}); typeValue.append( diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index 8927e64531b..2bb431aaec5 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -228,6 +228,24 @@ Blockly.Blocks['input_dummy'] = { } }; +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 at the end of a row with no " + + "connections. Alignment options (left, right, centre) " + + "apply only to multi-line fields.", + "helpUrl": "https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks#block_inputs" + }); + } +}; + Blockly.Blocks['field_static'] = { // Text value. init: function() { diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index 0caf4b5aa5e..7612b996c93 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -177,7 +177,8 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { var input = {type: contentsBlock.type}; // 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'); } var check = JSON.parse( @@ -272,13 +273,15 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { // Generate inputs. var TYPES = {'input_value': 'appendValueInput', 'input_statement': 'appendStatementInput', - 'input_dummy': 'appendDummyInput'}; + 'input_dummy': 'appendDummyInput', + 'input_end_row': 'appendEndRowInput'}; var contentsBlock = rootBlock.getInputTargetBlock('INPUTS'); while (contentsBlock) { if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) { var name = ''; // 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 = JSON.stringify(contentsBlock.getFieldValue('INPUTNAME')); } diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html index 37c75236d78..14d84d73f7e 100644 --- a/demos/blockfactory/index.html +++ b/demos/blockfactory/index.html @@ -422,6 +422,7 @@

Generator stub: + From 78f999f1415ef3fdf7a59a850daa1ffe1c66b991 Mon Sep 17 00:00:00 2001 From: John Nesky Date: Fri, 11 Aug 2023 12:28:47 -0700 Subject: [PATCH 8/8] Addres feedback, fix endrow after external value. --- core/block.ts | 2 +- core/renderers/common/info.ts | 13 +++++++++---- core/renderers/geras/info.ts | 8 ++++---- core/renderers/zelos/info.ts | 8 ++++++-- demos/blockfactory/blocks.js | 13 +++++++------ demos/blockfactory/factory_utils.js | 2 +- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/core/block.ts b/core/block.ts index 4dfd2b141cd..ea85edaf8c3 100644 --- a/core/block.ts +++ b/core/block.ts @@ -1647,7 +1647,7 @@ export class Block implements IASTNodeLocation, IDeletable { json['message' + i], json['args' + i] || [], // Backwards compatibility: lastDummyAlign aliases implicitAlign. - json['lastDummyAlign' + i] || json['implicitAlign' + i], + json['implicitAlign' + i] || json['lastDummyAlign' + i], warningPrefix, ); i++; diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index 433b8578f77..c035d0e52e0 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -328,8 +328,8 @@ export class RenderInfo { activeRow.elements.push(new ExternalValueInput(this.constants_, input)); activeRow.hasExternalInput = true; } else if (input instanceof DummyInput || input instanceof EndRowInput) { - // Dummy inputs have no visual representation, but the information is - // still important. + // Dummy and end-row inputs have no visual representation, but the + // information is still important. activeRow.minHeight = Math.max( activeRow.minHeight, input.getSourceBlock() && input.getSourceBlock()!.isShadow() @@ -368,8 +368,13 @@ export class RenderInfo { ) { return true; } - // Value and dummy inputs get new row if inputs are not inlined. - if (input instanceof ValueInput || input instanceof DummyInput) { + // 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 + ) { return !this.isInline; } return false; diff --git a/core/renderers/geras/info.ts b/core/renderers/geras/info.ts index 9559facae1b..b3a681bbf1d 100644 --- a/core/renderers/geras/info.ts +++ b/core/renderers/geras/info.ts @@ -92,8 +92,8 @@ export class RenderInfo extends BaseRenderInfo { activeRow.elements.push(new ExternalValueInput(this.constants_, input)); activeRow.hasExternalInput = true; } else if (input instanceof DummyInput || input instanceof EndRowInput) { - // Dummy inputs have no visual representation, but the information is - // still important. + // Dummy and end-row inputs have no visual representation, but the + // information is still important. activeRow.minHeight = Math.max( activeRow.minHeight, this.constants_.DUMMY_INPUT_MIN_HEIGHT, @@ -383,8 +383,8 @@ export class RenderInfo extends BaseRenderInfo { } else if (row.hasStatement) { nextRightEdge = row.width; } else { - // To avoid jagged right edges along consecutive non-statement rows, - // use the maximum width. + // To keep right edges of consecutive non-statement rows aligned, use + // the maximum width. nextRightEdge = Math.max(nextRightEdge, row.width); } prevInput = row; diff --git a/core/renderers/zelos/info.ts b/core/renderers/zelos/info.ts index a82a1b80e8f..e2d491c90d8 100644 --- a/core/renderers/zelos/info.ts +++ b/core/renderers/zelos/info.ts @@ -137,8 +137,12 @@ export class RenderInfo extends BaseRenderInfo { ) { return true; } - // Value and dummy inputs get new row if inputs are not inlined. - if (input instanceof ValueInput || input instanceof DummyInput) { + // Value, dummy, and end-row inputs get new row if inputs are not inlined. + if ( + input instanceof ValueInput || + input instanceof DummyInput || + input instanceof EndRowInput + ) { return !this.isInline || this.isMultiRow; } return false; diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index 2bb431aaec5..70691e2649e 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -220,9 +220,9 @@ Blockly.Blocks['input_dummy'] = { "previousStatement": "Input", "nextStatement": "Input", "colour": 210, - "tooltip": "For adding fields on a separate row with no " + - "connections. Alignment options (left, right, centre) " + - "apply only to multi-line fields.", + "tooltip": "For adding fields without any block connections." + + "Alignment options (left, right, centre) only affect " + + "multi-row blocks.", "helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293" }); } @@ -238,9 +238,10 @@ Blockly.Blocks['input_end_row'] = { "previousStatement": "Input", "nextStatement": "Input", "colour": 210, - "tooltip": "For adding fields at the end of a row with no " + - "connections. Alignment options (left, right, centre) " + - "apply only to multi-line fields.", + "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" }); } diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index 7612b996c93..164b3357be0 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -203,7 +203,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) { if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() !== '') { var align = lastInput.getFieldValue('ALIGN'); if (align !== 'LEFT') { - JS.lastDummyAlign0 = align; + JS.implicitAlign0 = align; } args.pop(); message.pop();