From e5d25bb686abc131fcdcc290fdea113949e27580 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Tue, 15 Nov 2016 15:51:51 -0800 Subject: [PATCH] String reference in JSON string messages (#741) * Adds message references to message string interpolation, in the form of %{BKY_STRING}. * Re-adding CONTROLS_IFELSE block using the new syntax, referencing to CONTROL_IF equivalents. --- blocks/logic.js | 42 +++++++++++++++++++++ core/utils.js | 69 +++++++++++++++++++++++++++++++++- generators/dart/logic.js | 2 + generators/javascript/logic.js | 2 + generators/lua/logic.js | 2 + generators/php/logic.js | 2 + generators/python/logic.js | 2 + tests/jsunit/utils_test.js | 50 ++++++++++++++++++++++++ tests/playground.html | 2 +- 9 files changed, 170 insertions(+), 3 deletions(-) diff --git a/blocks/logic.js b/blocks/logic.js index 57af601a9..3a74e2872 100644 --- a/blocks/logic.js +++ b/blocks/logic.js @@ -265,6 +265,48 @@ Blockly.Blocks['controls_if_else'] = { } }; +Blockly.Blocks['controls_ifelse'] = { + /** + * If/else block that does not use a mutator. + */ + init: function() { + this.jsonInit({ + "message0": "%{BKY_CONTROLS_IF_MSG_IF} %1", + "args0": [ + { + "type": "input_value", + "name": "IF0", + "check": "Boolean", + "align": "RIGHT" + } + ], + "message1": "%{BKY_CONTROLS_IF_MSG_THEN} %1", + "args1": [ + { + "type": "input_statement", + "name": "DO0", + "check": "Boolean", + "align": "RIGHT" + } + ], + "message2": "%{BKY_CONTROLS_IF_MSG_ELSE} %1", + "args2": [ + { + "type": "input_statement", + "name": "ELSE", + "check": "Boolean", + "align": "RIGHT" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": Blockly.Blocks.logic.HUE, + "tooltip": Blockly.Msg.CONTROLS_IF_TOOLTIP_2, + "helpUrl": Blockly.Msg.CONTROLS_IF_HELPURL + }); + } +}; + Blockly.Blocks['logic_compare'] = { /** * Block for comparison operator. diff --git a/core/utils.js b/core/utils.js index 949eed2bd..ca2c9c641 100644 --- a/core/utils.js +++ b/core/utils.js @@ -323,6 +323,7 @@ Blockly.utils.tokenizeInterpolation = function(message) { // 0 - Base case. // 1 - % found. // 2 - Digit found. + // 3 - Message ref found var state = 0; var buffer = []; var number = null; @@ -330,6 +331,11 @@ Blockly.utils.tokenizeInterpolation = function(message) { var c = chars[i]; if (state == 0) { if (c == '%') { + var text = buffer.join(''); + if (text) { + tokens.push(text); + } + buffer.length = 0; state = 1; // Start escape. } else { buffer.push(c); // Regular char. @@ -346,8 +352,10 @@ Blockly.utils.tokenizeInterpolation = function(message) { tokens.push(text); } buffer.length = 0; + } else if (c == '{') { + state = 3; } else { - buffer.push('%', c); // Not an escape: %a + buffer.push('%', c); // Not recognized. Return as literal. state = 0; } } else if (state == 2) { @@ -358,13 +366,70 @@ Blockly.utils.tokenizeInterpolation = function(message) { i--; // Parse this char again. state = 0; } + } else if (state == 3) { // String table reference + if (c == '') { + // Premature end before closing '}' + buffer.splice(0, 0, '%{'); // Re-insert leading delimiter + i--; // Parse this char again. + state = 0; // and parse as string literal. + } else if (c != '}') { + buffer.push(c); + } else { + var rawKey = buffer.join(''); + if (/[a-zA-Z][a-zA-Z0-9_]*/.test(rawKey)) { // Strict matching + // Found a valid string key. Attempt case insensitive match. + var keyUpper = rawKey.toUpperCase(); + + // BKY_ is the prefix used to namespace the strings used in Blockly + // core files and the predefined blocks in ../blocks/. These strings + // are defined in ../msgs/ files. + var bklyKey = goog.string.startsWith(keyUpper, 'BKY_') ? + keyUpper.substring(4) : null; + if (bklyKey && bklyKey in Blockly.Msg) { + var rawValue = Blockly.Msg[bklyKey]; + var subTokens = Blockly.utils.tokenizeInterpolation(rawValue); + tokens = tokens.concat(subTokens); + } else { + // No entry found in the string table. Pass reference as string. + tokens.push('%{' + rawKey + '}'); + } + buffer.length = 0; // Clear the array + state = 0; + } else { + tokens.push('%{' + rawKey + '}'); + buffer.length = 0; + state = 0; // and parse as string literal. + } + } } } var text = buffer.join(''); if (text) { tokens.push(text); } - return tokens; + + // Merge adjacent text tokens into a single string. + var mergedTokens = []; + buffer.length = 0; + for (var i = 0; i < tokens.length; ++i) { + if (typeof tokens[i] == 'string') { + buffer.push(tokens[i]); + } else { + text = buffer.join(''); + if (text) { + mergedTokens.push(text); + } + buffer.length = 0; + mergedTokens.push(tokens[i]); + } + } + text = buffer.join(''); + if (text) { + mergedTokens.push(text); + } + buffer.length = 0; + + return mergedTokens; }; /** diff --git a/generators/dart/logic.js b/generators/dart/logic.js index 0861edc74..620b55da8 100644 --- a/generators/dart/logic.js +++ b/generators/dart/logic.js @@ -50,6 +50,8 @@ Blockly.Dart['controls_if'] = function(block) { return code + '\n'; }; +Blockly.Dart['controls_ifelse'] = Blockly.Dart['controls_if']; + Blockly.Dart['logic_compare'] = function(block) { // Comparison operator. var OPERATORS = { diff --git a/generators/javascript/logic.js b/generators/javascript/logic.js index 46b8974f7..1c4d4b620 100644 --- a/generators/javascript/logic.js +++ b/generators/javascript/logic.js @@ -50,6 +50,8 @@ Blockly.JavaScript['controls_if'] = function(block) { return code + '\n'; }; +Blockly.JavaScript['controls_ifelse'] = Blockly.JavaScript['controls_if']; + Blockly.JavaScript['logic_compare'] = function(block) { // Comparison operator. var OPERATORS = { diff --git a/generators/lua/logic.js b/generators/lua/logic.js index 74eba62ed..033c52e33 100644 --- a/generators/lua/logic.js +++ b/generators/lua/logic.js @@ -50,6 +50,8 @@ Blockly.Lua['controls_if'] = function(block) { return code + 'end\n'; }; +Blockly.Lua['controls_ifelse'] = Blockly.Lua['controls_if']; + Blockly.Lua['logic_compare'] = function(block) { // Comparison operator. var OPERATORS = { diff --git a/generators/php/logic.js b/generators/php/logic.js index f51767034..475d2f703 100644 --- a/generators/php/logic.js +++ b/generators/php/logic.js @@ -50,6 +50,8 @@ Blockly.PHP['controls_if'] = function(block) { return code + '\n'; }; +Blockly.PHP['controls_ifelse'] = Blockly.PHP['controls_if']; + Blockly.PHP['logic_compare'] = function(block) { // Comparison operator. var OPERATORS = { diff --git a/generators/python/logic.js b/generators/python/logic.js index a1e79eafd..981ab3696 100644 --- a/generators/python/logic.js +++ b/generators/python/logic.js @@ -51,6 +51,8 @@ Blockly.Python['controls_if'] = function(block) { return code; }; +Blockly.Python['controls_ifelse'] = Blockly.Python['controls_if']; + Blockly.Python['logic_compare'] = function(block) { // Comparison operator. var OPERATORS = { diff --git a/tests/jsunit/utils_test.js b/tests/jsunit/utils_test.js index 3d6086522..a96cec57b 100644 --- a/tests/jsunit/utils_test.js +++ b/tests/jsunit/utils_test.js @@ -115,16 +115,66 @@ function test_commonWordSuffix() { function test_tokenizeInterpolation() { var tokens = Blockly.utils.tokenizeInterpolation(''); assertArrayEquals('Null interpolation', [], tokens); + tokens = Blockly.utils.tokenizeInterpolation('Hello'); assertArrayEquals('No interpolation', ['Hello'], tokens); + tokens = Blockly.utils.tokenizeInterpolation('Hello%World'); assertArrayEquals('Unescaped %.', ['Hello%World'], tokens); + tokens = Blockly.utils.tokenizeInterpolation('Hello%%World'); assertArrayEquals('Escaped %.', ['Hello%World'], tokens); + tokens = Blockly.utils.tokenizeInterpolation('Hello %1 World'); assertArrayEquals('Interpolation.', ['Hello ', 1, ' World'], tokens); + tokens = Blockly.utils.tokenizeInterpolation('%123Hello%456World%789'); assertArrayEquals('Interpolations.', [123, 'Hello', 456, 'World', 789], tokens); + tokens = Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%'); assertArrayEquals('Torture interpolations.', ['%%x%0', 0, 1, '%'], tokens); + + Blockly.Msg = Blockly.Msg || {}; + + Blockly.Msg.STRING_REF = 'test string'; + tokens = Blockly.utils.tokenizeInterpolation('%{bky_string_ref}'); + assertArrayEquals('String table reference, lowercase', ['test string'], tokens); + tokens = Blockly.utils.tokenizeInterpolation('%{BKY_STRING_REF}'); + assertArrayEquals('String table reference, uppercase', ['test string'], tokens); + + Blockly.Msg.WITH_PARAM = 'before %1 after'; + tokens = Blockly.utils.tokenizeInterpolation('%{bky_with_param}'); + assertArrayEquals('String table reference, with parameter', ['before ', 1, ' after'], tokens); + + Blockly.Msg.RECURSE = 'before %{bky_string_ref} after'; + tokens = Blockly.utils.tokenizeInterpolation('%{bky_recurse}'); + assertArrayEquals('String table reference, with subreference', ['before test string after'], tokens); + + // Error cases... + tokens = Blockly.utils.tokenizeInterpolation('%{bky_undefined}'); + assertArrayEquals('Undefined string table reference', ['%{bky_undefined}'], tokens); + + Blockly.Msg['1'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{1} after'); + assertArrayEquals('Invalid initial digit in string table reference', ['before %{1} after'], tokens); + + Blockly.Msg['TWO WORDS'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{two words} after'); + assertArrayEquals('Invalid character in string table reference: space', ['before %{two words} after'], tokens); + + Blockly.Msg['TWO-WORDS'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{two-words} after'); + assertArrayEquals('Invalid character in string table reference: dash', ['before %{two-words} after'], tokens); + + Blockly.Msg['TWO.WORDS'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{two.words} after'); + assertArrayEquals('Invalid character in string table reference: period', ['before %{two.words} after'], tokens); + + Blockly.Msg['AB&C'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{ab&c} after'); + assertArrayEquals('Invalid character in string table reference: &', ['before %{ab&c} after'], tokens); + + Blockly.Msg['UNCLOSED'] = 'Will not match'; + tokens = Blockly.utils.tokenizeInterpolation('before %{unclosed'); + assertArrayEquals('String table reference, with parameter', ['before %{unclosed'], tokens); } diff --git a/tests/playground.html b/tests/playground.html index 6270af20c..8a7435915 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -379,7 +379,7 @@ h1 {