Refactor interpolate_ into multiple functions & remove direct new Field call (#4585)

* Refactor interpolation

* Added docs

* Add tests

* Add test for dummy input after field not prefixed with field_

* Fix typings

* Pr comments
This commit is contained in:
Beka Westberg
2021-01-22 12:44:50 -08:00
committed by GitHub
parent efdcb89b5c
commit 75f118b4f9
5 changed files with 905 additions and 141 deletions

View File

@@ -1629,111 +1629,201 @@ Blockly.Block.prototype.mixin = function(mixinObj, opt_disableCheck) {
Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign,
warningPrefix) {
var tokens = Blockly.utils.tokenizeInterpolation(message);
// Interpolate the arguments. Build a list of elements.
var indexDup = [];
var indexCount = 0;
var elements = [];
this.validateTokens_(tokens, args.length);
var elements = this.interpolateArguments_(tokens, args, lastDummyAlign);
// An array of [field, fieldName] tuples.
var fieldStack = [];
for (var i = 0, element; (element = elements[i]); i++) {
switch (element['type']) {
case 'input_value':
case 'input_statement':
case 'input_dummy':
var input = this.inputFromJson_(element, warningPrefix);
// Should never be null, but just in case.
if (input) {
for (var j = 0, tuple; (tuple = fieldStack[j]); j++) {
input.appendField(tuple[0], tuple[1]);
}
fieldStack.length = 0;
}
break;
// All other types, including ones starting with 'input_' get routed here.
default:
var field = this.fieldFromJson_(element);
if (field) {
fieldStack.push([field, element['name']]);
}
}
}
};
/**
* Validates that the tokens are within the correct bounds, with no duplicates,
* and that all of the arguments are referred to. Throws errors if any of these
* things are not true.
* @param {!Array<string|number>} tokens An array of tokens to validate
* @param {number} argsCount The number of args that need to be referred to.
* @private
*/
Blockly.Block.prototype.validateTokens_ = function(tokens, argsCount) {
var visitedArgsHash = [];
var visitedArgsCount = 0;
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (typeof token == 'number') {
if (token <= 0 || token > args.length) {
throw Error('Block "' + this.type + '": ' +
'Message index %' + token + ' out of range.');
}
if (indexDup[token]) {
throw Error('Block "' + this.type + '": ' +
'Message index %' + token + ' duplicated.');
}
indexDup[token] = true;
indexCount++;
elements.push(args[token - 1]);
} else {
token = token.trim();
if (token) {
elements.push(token);
}
if (typeof token != 'number') {
continue;
}
if (token < 1 || token > argsCount) {
throw Error('Block "' + this.type + '": ' +
'Message index %' + token + ' out of range.');
}
if (visitedArgsHash[token]) {
throw Error('Block "' + this.type + '": ' +
'Message index %' + token + ' duplicated.');
}
visitedArgsHash[token] = true;
visitedArgsCount++;
}
if (indexCount != args.length) {
if (visitedArgsCount != argsCount) {
throw Error('Block "' + this.type + '": ' +
'Message does not reference all ' + args.length + ' arg(s).');
'Message does not reference all ' + argsCount + ' arg(s).');
}
// Add last dummy input if needed.
if (elements.length && (typeof elements[elements.length - 1] == 'string' ||
Blockly.utils.string.startsWith(
elements[elements.length - 1]['type'], 'field_'))) {
var dummyInput = {type: 'input_dummy'};
if (lastDummyAlign) {
dummyInput['align'] = lastDummyAlign;
};
/**
* 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.
* @param {!Array<!string|number>} tokens The tokens to interpolate
* @param {!Array<!Object|string>} args The arguments to insert.
* @param {string|undefined} lastDummyAlign The alignment the added dummy input
* should have, if we are required to add one.
* @return {!Array<!Object>} The JSON definitions of field and inputs to add
* to the block.
* @private
*/
Blockly.Block.prototype.interpolateArguments_ =
function(tokens, args, lastDummyAlign) {
var elements = [];
for (var i = 0; i < tokens.length; i++) {
var element = tokens[i];
if (typeof element == 'number') {
element = args[element - 1];
}
// Args can be strings, which is why this isn't elseif.
if (typeof element == 'string') {
element = this.stringToFieldJson_(element);
if (!element) {
continue;
}
}
elements.push(element);
}
var length = elements.length;
var startsWith = Blockly.utils.string.startsWith;
// TODO: This matches the old behavior, but it doesn't work for fields
// that don't start with 'field_'.
if (length && startsWith(elements[length - 1]['type'], 'field_')) {
var dummyInput = {'type': 'input_dummy'};
if (lastDummyAlign) {
dummyInput['align'] = lastDummyAlign;
}
elements.push(dummyInput);
}
return elements;
};
/**
* Creates a field from the json definition of a field. If a field with the
* given type cannot be found, this attempts to create a different field using
* the 'alt' property of the json definition (if it exists).
* @param {{alt:(string|undefined)}} element The element to try to turn into a
* field.
* @return {?Blockly.Field} The field defined by the JSON, or null if one
* couldn't be created.
* @private
*/
Blockly.Block.prototype.fieldFromJson_ = function(element) {
var field = Blockly.fieldRegistry.fromJson(element);
if (!field && element['alt']) {
if (typeof element['alt'] == 'string') {
var json = this.stringToFieldJson_(element['alt']);
return json ? this.fieldFromJson_(json) : null;
}
elements.push(dummyInput);
return this.fieldFromJson_(element['alt']);
}
// Lookup of alignment constants.
return field;
};
/**
* Creates an input from the json definition of an input. Sets the input's check
* and alignment if they are provided.
* @param {!Object} element The JSON to turn into an input.
* @param {string} warningPrefix The prefix to add to warnings to help the
* developer debug.
* @return {?Blockly.Input} The input that has been created, or null if one
* could not be created for some reason (should never happen).
* @private
*/
Blockly.Block.prototype.inputFromJson_ = function(element, warningPrefix) {
var alignmentLookup = {
'LEFT': Blockly.ALIGN_LEFT,
'RIGHT': Blockly.ALIGN_RIGHT,
'CENTRE': Blockly.ALIGN_CENTRE,
'CENTER': Blockly.ALIGN_CENTRE
};
// Populate block with inputs and fields.
var fieldStack = [];
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (typeof element == 'string') {
fieldStack.push([element, undefined]);
} else {
var field = null;
var input = null;
do {
var altRepeat = false;
if (typeof element == 'string') {
field = new Blockly.FieldLabel(element);
} else {
switch (element['type']) {
case 'input_value':
input = this.appendValueInput(element['name']);
break;
case 'input_statement':
input = this.appendStatementInput(element['name']);
break;
case 'input_dummy':
input = this.appendDummyInput(element['name']);
break;
default:
// This should handle all field JSON parsing, including
// options that can be applied to any field type.
field = Blockly.fieldRegistry.fromJson(element);
// Unknown field.
if (!field && element['alt']) {
element = element['alt'];
altRepeat = true;
}
}
}
} while (altRepeat);
if (field) {
fieldStack.push([field, element['name']]);
} else if (input) {
if (element['check']) {
input.setCheck(element['check']);
}
if (element['align']) {
var alignment = alignmentLookup[element['align'].toUpperCase()];
if (alignment === undefined) {
console.warn(warningPrefix + 'Illegal align value: ',
element['align']);
} else {
input.setAlign(alignment);
}
}
for (var j = 0; j < fieldStack.length; j++) {
input.appendField(fieldStack[j][0], fieldStack[j][1]);
}
fieldStack.length = 0;
}
var input = null;
switch (element['type']) {
case 'input_value':
input = this.appendValueInput(element['name']);
break;
case 'input_statement':
input = this.appendStatementInput(element['name']);
break;
case 'input_dummy':
input = this.appendDummyInput(element['name']);
break;
}
// Should never be hit because of interpolate_'s checks, but just in case.
if (!input) {
return null;
}
if (element['check']) {
input.setCheck(element['check']);
}
if (element['align']) {
var alignment = alignmentLookup[element['align'].toUpperCase()];
if (alignment === undefined) {
console.warn(warningPrefix + 'Illegal align value: ',
element['align']);
} else {
input.setAlign(alignment);
}
}
return input;
};
/**
* Turns a string into the JSON definition of a label field. If the string
* becomes an empty string when trimmed, this returns null.
* @param {string} str String to turn into the JSON definition of a label field.
* @return {?{text: string, type: string}} The JSON definition or null.
* @private
*/
Blockly.Block.prototype.stringToFieldJson_ = function(str) {
str = str.trim();
if (str) {
return {
'type': 'field_label',
'text': str,
};
}
return null;
};
/**

7
package-lock.json generated
View File

@@ -8173,7 +8173,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
@@ -8397,6 +8397,11 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg=="
},
"typescript-closure-tools": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/typescript-closure-tools/-/typescript-closure-tools-0.0.7.tgz",

View File

@@ -0,0 +1,582 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
suite('Block JSON initialization', function() {
suite('validateTokens_', function() {
setup(function() {
this.assertError = function(tokens, count, error) {
var block = {
type: 'test',
validateTokens_: Blockly.Block.prototype.validateTokens_,
};
chai.assert.throws(function() {
block.validateTokens_(tokens, count);
}, error);
};
this.assertNoError = function(tokens, count) {
var block = {
type: 'test',
validateTokens_: Blockly.Block.prototype.validateTokens_,
};
chai.assert.doesNotThrow(function() {
block.validateTokens_(tokens, count);
});
};
});
test('0 args, 0 tokens', function() {
this.assertNoError(['test', 'test'], 0);
});
test('0 args, 1 token', function() {
this.assertError(['test', 1, 'test'], 0,
'Block "test": Message index %1 out of range.');
});
test('1 arg, 0 tokens', function() {
this.assertError(['test', 'test'], 1,
'Block "test": Message does not reference all 1 arg(s).');
});
test('1 arg, 1 token', function() {
this.assertNoError(['test', 1, 'test'], 1);
});
test('1 arg, 2 tokens', function() {
this.assertError(['test', 1, 1, 'test'], 1,
'Block "test": Message index %1 duplicated.');
});
test('Token out of lower bound', function() {
this.assertError(['test', 0, 'test'], 1,
'Block "test": Message index %0 out of range.');
});
test('Token out of upper bound', function() {
this.assertError(['test', 2, 'test'], 1,
'Block "test": Message index %2 out of range.');
});
});
suite('interpolateArguments_', function() {
setup(function() {
this.assertInterpolation = function(tokens, args, lastAlign, elements) {
var block = {
type: 'test',
interpolateArguments_: Blockly.Block.prototype.interpolateArguments_,
stringToFieldJson_: Blockly.Block.prototype.stringToFieldJson_,
};
chai.assert.deepEqual(
block.interpolateArguments_(tokens, args, lastAlign),
elements);
};
});
test('Strings to labels', function() {
this.assertInterpolation(
['test1', 'test2', 'test3', { 'type': 'input_dummy'}],
[],
undefined,
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'field_label',
'text': 'test2',
},
{
'type': 'field_label',
'text': 'test3',
},
{
'type': 'input_dummy',
}
]);
});
test('Ignore empty strings', function() {
this.assertInterpolation(
['test1', '', ' ', { 'type': 'input_dummy'}],
[],
undefined,
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'input_dummy',
}
]);
});
test('Insert args', function() {
this.assertInterpolation(
[1, 2, 3, { 'type': 'input_dummy'}],
[
{
'type': 'field_number',
'name': 'test1',
},
{
'type': 'field_number',
'name': 'test2',
},
{
'type': 'field_number',
'name': 'test3',
},
],
undefined,
[
{
'type': 'field_number',
'name': 'test1',
},
{
'type': 'field_number',
'name': 'test2',
},
{
'type': 'field_number',
'name': 'test3',
},
{
'type': 'input_dummy',
}
]);
});
test('String args to labels', function() {
this.assertInterpolation(
[1, 2, 3, { 'type': 'input_dummy'}],
['test1', 'test2', 'test3'],
undefined,
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'field_label',
'text': 'test2',
},
{
'type': 'field_label',
'text': 'test3',
},
{
'type': 'input_dummy',
}
]);
});
test('Ignore empty string args', function() {
this.assertInterpolation(
[1, 2, 3, { 'type': 'input_dummy'}],
['test1', ' ', ' '],
undefined,
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'input_dummy',
}
]);
});
test('Add last dummy', function() {
this.assertInterpolation(
['test1', 'test2', 'test3'],
[],
undefined,
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'field_label',
'text': 'test2',
},
{
'type': 'field_label',
'text': 'test3',
},
{
'type': 'input_dummy',
}
]);
});
test.skip('Add last dummy for no_field_prefix_field', function() {
this.assertInterpolation(
[
{
'type': 'no_field_prefix_field',
}
],
[],
undefined,
[
{
'type': 'no_field_prefix_field',
},
{
'type': 'input_dummy',
}
]);
});
test('Set last dummy alignment', function() {
this.assertInterpolation(
['test1', 'test2', 'test3'],
[],
'CENTER',
[
{
'type': 'field_label',
'text': 'test1',
},
{
'type': 'field_label',
'text': 'test2',
},
{
'type': 'field_label',
'text': 'test3',
},
{
'type': 'input_dummy',
'align': 'CENTER',
}
]);
});
});
suite('fieldFromJson_', function() {
setup(function() {
this.stub = sinon.stub(Blockly.fieldRegistry, 'fromJson')
.callsFake(function(elem) {
switch (elem['type']) {
case 'field_label':
return 'field_label';
case 'field_number':
return 'field_number';
case 'no_field_prefix_field':
return 'no_field_prefix_field';
case 'input_prefix_field':
return 'input_prefix_field';
default:
return null;
}
});
this.assertField = function(json, expectedType) {
var block = {
type: 'test',
fieldFromJson_: Blockly.Block.prototype.fieldFromJson_,
stringToFieldJson_: Blockly.Block.prototype.stringToFieldJson_,
};
chai.assert.strictEqual(block.fieldFromJson_(json), expectedType);
};
});
teardown(function() {
this.stub.restore();
});
test('Simple field', function() {
this.assertField({
'type': 'field_label',
'text': 'text',
}, 'field_label');
});
test('Bad field', function() {
this.assertField({
'type': 'field_bad',
}, null);
});
test('no_field_prefix_field', function() {
this.assertField({
'type': 'no_field_prefix_field',
}, 'no_field_prefix_field');
});
test('input_prefix_field', function() {
this.assertField({
'type': 'input_prefix_field',
}, 'input_prefix_field');
});
test('Alt string', function() {
this.assertField({
'type': 'field_undefined',
'alt': 'alt text',
}, 'field_label');
});
test('input_prefix_bad w/ alt string', function() {
this.assertField({
'type': 'input_prefix_bad',
'alt': 'alt string',
}, 'field_label');
});
test('Alt other field', function() {
this.assertField({
'type': 'field_undefined',
'alt': {
'type': 'field_number',
'name': 'FIELDNAME'
},
}, 'field_number');
});
test('Deep alt string', function() {
this.assertField({
'type': 'field_undefined1',
'alt': {
'type': 'field_undefined2',
'alt': {
'type': 'field_undefined3',
'alt': {
'type': 'field_undefined4',
'alt': {
'type': 'field_undefined5',
'alt': 'text',
},
},
},
},
}, 'field_label');
});
test('Deep alt other field', function() {
this.assertField({
'type': 'field_undefined1',
'alt': {
'type': 'field_undefined2',
'alt': {
'type': 'field_undefined3',
'alt': {
'type': 'field_undefined4',
'alt': {
'type': 'field_undefined5',
'alt': {
'type': 'field_number',
'name': 'FIELDNAME'
},
},
},
},
},
}, 'field_number');
});
test('No alt', function() {
this.assertField({
'type': 'field_undefined'
}, null);
});
test('Bad alt', function() {
this.assertField({
'type': 'field_undefined',
'alt': {
'type': 'field_undefined',
}
}, null);
});
test('Spaces string alt', function() {
this.assertField({
'type': 'field_undefined',
'alt': ' ',
}, null);
});
});
suite('inputFromJson_', function() {
setup(function() {
var Input = function(type) {
this.type = type;
this.setCheck = sinon.fake();
this.setAlign = sinon.fake();
};
var Block = function() {
this.type = 'test';
this.appendDummyInput = sinon.fake.returns(new Input());
this.appendValueInput = sinon.fake.returns(new Input());
this.appendStatementInput = sinon.fake.returns(new Input());
this.inputFromJson_ = Blockly.Block.prototype.inputFromJson_;
};
this.assertInput = function(json, type, check, align) {
var block = new Block();
var input = block.inputFromJson_(json);
switch (type) {
case 'input_dummy':
chai.assert.isTrue(block.appendDummyInput.calledOnce,
'Expected a dummy input to be created.');
break;
case 'input_value':
chai.assert.isTrue(block.appendValueInput.calledOnce,
'Expected a value input to be created.');
break;
case 'input_statement':
chai.assert.isTrue(block.appendStatementInput.calledOnce,
'Expected a statement input to be created.');
break;
default:
chai.assert.isNull(input, 'Expected input to be null');
chai.assert.isTrue(block.appendDummyInput.notCalled,
'Expected no input to be created');
chai.assert.isTrue(block.appendValueInput.notCalled,
'Expected no input to be created');
chai.assert.isTrue(block.appendStatementInput.notCalled,
'Expected no input to be created');
return;
}
if (check) {
chai.assert.isTrue(input.setCheck.calledWith(check),
'Expected setCheck to be called with', check);
} else {
chai.assert.isTrue(input.setCheck.notCalled,
'Expected setCheck to not be called');
}
if (align !== undefined) {
chai.assert.isTrue(input.setAlign.calledWith(align),
'Expected setAlign to be called with', align);
} else {
chai.assert.isTrue(input.setAlign.notCalled,
'Expected setAlign to not be called');
}
};
});
test('Dummy', function() {
this.assertInput(
{
'type': 'input_dummy',
},
'input_dummy');
});
test('Value', function() {
this.assertInput(
{
'type': 'input_value',
},
'input_value');
});
test('Statement', function() {
this.assertInput(
{
'type': 'input_statement',
},
'input_statement');
});
test('Bad input type', function() {
this.assertInput(
{
'type': 'input_bad',
},
'input_bad');
});
test('String Check', function() {
this.assertInput(
{
'type': 'input_dummy',
'check': 'Integer'
},
'input_dummy',
'Integer');
});
test('Array check', function() {
this.assertInput(
{
'type': 'input_dummy',
'check': ['Integer', 'Number']
},
'input_dummy',
['Integer', 'Number']);
});
test('Empty check', function() {
this.assertInput(
{
'type': 'input_dummy',
'check': ''
},
'input_dummy');
});
test('Null check', function() {
this.assertInput(
{
'type': 'input_dummy',
'check': null,
},
'input_dummy');
});
test('"Left" align', function() {
this.assertInput(
{
'type': 'input_dummy',
'align': 'LEFT',
},
'input_dummy',
undefined,
Blockly.ALIGN_LEFT);
});
test('"Right" align', function() {
this.assertInput(
{
'type': 'input_dummy',
'align': 'RIGHT',
},
'input_dummy',
undefined,
Blockly.ALIGN_RIGHT);
});
test('"Center" align', function() {
this.assertInput(
{
'type': 'input_dummy',
'align': 'CENTER',
},
'input_dummy',
undefined,
Blockly.ALIGN_CENTRE);
});
test('"Centre" align', function() {
this.assertInput(
{
'type': 'input_dummy',
'align': 'CENTRE',
},
'input_dummy',
undefined,
Blockly.ALIGN_CENTRE);
});
});
});

View File

@@ -52,6 +52,7 @@
<script src="toolbox_helper.js"></script>
<script src="astnode_test.js"></script>
<script src="block_json_test.js"></script>
<script src="block_test.js"></script>
<script src="comment_test.js"></script>
<script src="connection_db_test.js"></script>

View File

@@ -23,73 +23,159 @@ suite('Utils', function() {
});
suite('tokenizeInterpolation', function() {
test('Basic', function() {
var tokens = Blockly.utils.tokenizeInterpolation('');
chai.assert.deepEqual(tokens, [], 'Null interpolation');
suite('Basic', function() {
test('Empty string', function() {
chai.assert.deepEqual(Blockly.utils.tokenizeInterpolation(''), []);
});
tokens = Blockly.utils.tokenizeInterpolation('Hello');
chai.assert.deepEqual(tokens, ['Hello'], 'No interpolation');
test('No interpolation', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('Hello'), ['Hello']);
});
tokens = Blockly.utils.tokenizeInterpolation('Hello%World');
chai.assert.deepEqual(tokens, ['Hello%World'], 'Unescaped %.');
test('Unescaped %', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('Hello%World'),
['Hello%World']);
});
tokens = Blockly.utils.tokenizeInterpolation('Hello%%World');
chai.assert.deepEqual(tokens, ['Hello%World'], 'Escaped %.');
tokens = Blockly.utils.tokenizeInterpolation('Hello %1 World');
chai.assert.deepEqual(tokens, ['Hello ', 1, ' World'], 'Interpolation.');
tokens = Blockly.utils.tokenizeInterpolation('%123Hello%456World%789');
chai.assert.deepEqual(tokens, [123, 'Hello', 456, 'World', 789], 'Interpolations.');
tokens = Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%');
chai.assert.deepEqual(tokens, ['%%x%0', 0, 1, '%'], 'Torture interpolations.');
test('Escaped %', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('Hello%%World'),
['Hello%World']);
});
});
test('String table', function() {
Blockly.Msg = Blockly.Msg || {};
Blockly.Msg.STRING_REF = 'test string';
var tokens = Blockly.utils.tokenizeInterpolation('%{bky_string_ref}');
chai.assert.deepEqual(tokens, ['test string'], 'String table reference, lowercase');
tokens = Blockly.utils.tokenizeInterpolation('%{BKY_STRING_REF}');
chai.assert.deepEqual(tokens, ['test string'], 'String table reference, uppercase');
suite('Number interpolation', function() {
test('Single-digit number interpolation', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('Hello%1World'),
['Hello', 1, 'World']);
});
Blockly.Msg.WITH_PARAM = 'before %1 after';
tokens = Blockly.utils.tokenizeInterpolation('%{bky_with_param}');
chai.assert.deepEqual(tokens, ['before ', 1, ' after'], 'String table reference, with parameter');
test('Multi-digit number interpolation', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%123Hello%456World%789'),
[123, 'Hello', 456, 'World', 789]);
});
Blockly.Msg.RECURSE = 'before %{bky_string_ref} after';
tokens = Blockly.utils.tokenizeInterpolation('%{bky_recurse}');
chai.assert.deepEqual(tokens, ['before test string after'], 'String table reference, with subreference');
test('Escaped number', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('Hello %%1 World'),
['Hello %1 World']);
});
test('Crazy interpolation', function() {
// No idea what this is supposed to tell you if it breaks. But might
// as well keep it.
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%'),
['%%x%0', 0, 1, '%']);
});
});
test('Error cases', function() {
var tokens = Blockly.utils.tokenizeInterpolation('%{bky_undefined}');
chai.assert.deepEqual(tokens, ['%{bky_undefined}'], 'Undefined string table reference');
suite('String table interpolation', function() {
setup(function() {
Blockly.Msg = Blockly.Msg || { };
});
Blockly.Msg['1'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{1} after');
chai.assert.deepEqual(tokens, ['before %{1} after'], 'Invalid initial digit in string table reference');
test('Simple interpolation', function() {
Blockly.Msg.STRING_REF = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_string_ref}'),
['test string']);
});
Blockly.Msg['TWO WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two words} after');
chai.assert.deepEqual(tokens, ['before %{two words} after'], 'Invalid character in string table reference: space');
test('Case', function() {
Blockly.Msg.STRING_REF = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{BkY_StRiNg_ReF}'),
['test string']);
});
Blockly.Msg['TWO-WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two-words} after');
chai.assert.deepEqual(tokens, ['before %{two-words} after'], 'Invalid character in string table reference: dash');
test('Surrounding text', function() {
Blockly.Msg.STRING_REF = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation(
'before %{bky_string_ref} after'),
['before test string after']);
});
Blockly.Msg['TWO.WORDS'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{two.words} after');
chai.assert.deepEqual(tokens, ['before %{two.words} after'], 'Invalid character in string table reference: period');
test('With param', function() {
Blockly.Msg.WITH_PARAM = 'before %1 after';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_with_param}'),
['before ', 1, ' after']);
});
Blockly.Msg['AB&C'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{ab&c} after');
chai.assert.deepEqual(tokens, ['before %{ab&c} after'], 'Invalid character in string table reference: &');
test('Recursive reference', function() {
Blockly.Msg.STRING_REF = 'test string';
Blockly.Msg.RECURSE = 'before %{bky_string_ref} after';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_recurse}'),
['before test string after']);
});
Blockly.Msg['UNCLOSED'] = 'Will not match';
tokens = Blockly.utils.tokenizeInterpolation('before %{unclosed');
chai.assert.deepEqual(tokens, ['before %{unclosed'], 'String table reference, with parameter');
test('Number reference', function() {
Blockly.Msg['1'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_1}'), ['test string']);
});
test('Undefined reference', function() {
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_undefined}'),
['%{bky_undefined}']);
});
test('Not prefixed', function() {
Blockly.Msg.STRING_REF = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{string_ref}'),
['%{string_ref}']);
});
test('Not prefixed, number', function() {
Blockly.Msg['1'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{1}'),
['%{1}']);
});
test('Space in ref', function() {
Blockly.Msg['string ref'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_string ref}'),
['%{bky_string ref}']);
});
test('Dash in ref', function() {
Blockly.Msg['string-ref'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_string-ref}'),
['%{bky_string-ref}']);
});
test('Period in ref', function() {
Blockly.Msg['string.ref'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_string.ref}'),
['%{bky_string.ref}']);
});
test('Ampersand in ref', function() {
Blockly.Msg['string&ref'] = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_string&ref}'),
['%{bky_string&ref}']);
});
test('Unclosed reference', function() {
Blockly.Msg.UNCLOSED = 'test string';
chai.assert.deepEqual(
Blockly.utils.tokenizeInterpolation('%{bky_unclosed'),
['%{bky_unclosed']);
});
});
});