This is an automated email from the ASF dual-hosted git repository. joshtynjala pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/royale-compiler.git
commit edd4b5743ce31e9ba78a0496cd049b3f129f64b6 Author: Josh Tynjala <[email protected]> AuthorDate: Tue Dec 12 15:04:12 2023 -0800 MXMLDataBindingParser: fix using backslash as an escape character for { and } to make the compiler treat them as regular characters instead of binding delimiters (closes #110) Behavior updated to better match the Flex SDK compiler. If either the { or the } is escaped, but the other one isn't, they're both treated as part of the string, same as if both were escaped. All of these, except for the first, should be treated as regular strings: <js:Label text="{not_binding}"/> <js:Label text="\{not_binding}"/> <js:Label text="\{not_binding\}"/> <js:Label text="{not_binding\}"/> <js:Label text="{not_binding"/> --- .../mxml/royale/TestRoyaleMXMLApplication.java | 347 ++++++++++++++++++++- .../internal/tree/mxml/MXMLDataBindingParser.java | 8 +- .../internal/tree/mxml/MXMLTreeBuilder.java | 6 + 3 files changed, 358 insertions(+), 3 deletions(-) diff --git a/compiler-jx/src/test/java/org/apache/royale/compiler/internal/codegen/mxml/royale/TestRoyaleMXMLApplication.java b/compiler-jx/src/test/java/org/apache/royale/compiler/internal/codegen/mxml/royale/TestRoyaleMXMLApplication.java index 13442c7be..8a3f1eec9 100644 --- a/compiler-jx/src/test/java/org/apache/royale/compiler/internal/codegen/mxml/royale/TestRoyaleMXMLApplication.java +++ b/compiler-jx/src/test/java/org/apache/royale/compiler/internal/codegen/mxml/royale/TestRoyaleMXMLApplication.java @@ -235,8 +235,12 @@ public class TestRoyaleMXMLApplication extends RoyaleTestBase assertOutWithMetadata(outTemplate.replaceAll("AppName", appName)); } + // we previously had a test that checked if a backslash could escape another + // backslash, but the Flex SDK compiler treats that as two backslashes! + // the only characters that the Flex SDK compiler escapes with backslash + // are { and } curly braces (which are used for binding, if unescaped) @Test - public void testAlreadyEscapedBackslashStringAttribute() + public void testDoubleBackslashStringAttribute() { String code = "<basic:Application xmlns:fx=\"http://ns.adobe.com/mxml/2009\" xmlns:basic=\"library://ns.apache.org/royale/basic\">" + "<basic:beads><basic:TextPromptBead prompt=\"0-9\\\\\"/></basic:beads></basic:Application>"; @@ -299,7 +303,346 @@ public class TestRoyaleMXMLApplication extends RoyaleTestBase " '$ID_8_0',\n" + " 'prompt',\n" + " true,\n" + - " '0-9\\\\',\n" + + " '0-9\\\\\\\\',\n" + + " 0,\n" + + " 0,\n" + + " null\n" + + " ],\n" + + " 0,\n" + + " 0\n" + + " ]);\n" + + " \n" + + "};\n" + + "goog.inherits(AppName, org.apache.royale.core.Application);\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Metadata\n" + + " *\n" + + " * @type {Object.<string, Array.<Object>>}\n" + + " */\n" + + "AppName.prototype.ROYALE_CLASS_INFO = { names: [{ name: 'AppName', qName: 'AppName', kind: 'class' }] };\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Reflection\n" + + " *\n" + + " * @return {Object.<string, Function>}\n" + + " */\n" + + "AppName.prototype.ROYALE_REFLECTION_INFO = function () {\n" + + " return {\n" + + " methods: function () {\n" + + " return {\n" + + " 'AppName': { type: '', declaredBy: 'AppName'}\n"+ + " };\n" + + " }\n" + + " };\n" + + "};\n" + + "/**\n" + + " * @const\n" + + " * @type {number}\n" + + " */\n" + + "AppName.prototype.ROYALE_COMPILE_FLAGS = 9;\n" + + "\n" + + "\n"; + + assertOutWithMetadata(outTemplate.replaceAll("AppName", appName)); + } + + @Test + public void testBackslashOpenCurlyBraceStringAttribute() + { + String code = "<basic:Application xmlns:fx=\"http://ns.adobe.com/mxml/2009\" xmlns:basic=\"library://ns.apache.org/royale/basic\">" + + "<basic:beads><basic:TextPromptBead prompt=\"\\{0-9}\"/></basic:beads></basic:Application>"; + + IMXMLDocumentNode dnode = (IMXMLDocumentNode) getNode(code, + IMXMLDocumentNode.class, RoyaleTestBase.WRAP_LEVEL_NONE); + + ((JSRoyaleEmitter)(mxmlBlockWalker.getASEmitter())).getModel().setCurrentClass(dnode.getDefinition()); + mxmlBlockWalker.visitDocument(dnode); + String appName = dnode.getQualifiedName(); + String outTemplate = "/**\n" + + " * AppName\n" + + " *\n" + + " * @fileoverview\n" + + " *\n" + + " * @suppress {checkTypes|accessControls}\n" + + " */\n" + + "\n" + + "goog.provide('AppName');\n" + + "\n" + + "goog.require('org.apache.royale.core.Application');\n" + + "goog.require('org.apache.royale.html.accessories.TextPromptBead');\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * @constructor\n" + + " * @extends {org.apache.royale.core.Application}\n" + + " */\n" + + "AppName = function() {\n" + + " AppName.base(this, 'constructor');\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {org.apache.royale.html.accessories.TextPromptBead}\n" + + " */\n" + + " this.$ID_8_0;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldd;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldp;\n" + + "\n" + + " this.generateMXMLAttributes([\n" + + " 1,\n" + + " 'beads',\n" + + " null,\n" + + " [\n" + + " org.apache.royale.html.accessories.TextPromptBead,\n" + + " 2,\n" + + " '_id',\n" + + " true,\n" + + " '$ID_8_0',\n" + + " 'prompt',\n" + + " true,\n" + + " '{0-9}',\n" + + " 0,\n" + + " 0,\n" + + " null\n" + + " ],\n" + + " 0,\n" + + " 0\n" + + " ]);\n" + + " \n" + + "};\n" + + "goog.inherits(AppName, org.apache.royale.core.Application);\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Metadata\n" + + " *\n" + + " * @type {Object.<string, Array.<Object>>}\n" + + " */\n" + + "AppName.prototype.ROYALE_CLASS_INFO = { names: [{ name: 'AppName', qName: 'AppName', kind: 'class' }] };\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Reflection\n" + + " *\n" + + " * @return {Object.<string, Function>}\n" + + " */\n" + + "AppName.prototype.ROYALE_REFLECTION_INFO = function () {\n" + + " return {\n" + + " methods: function () {\n" + + " return {\n" + + " 'AppName': { type: '', declaredBy: 'AppName'}\n"+ + " };\n" + + " }\n" + + " };\n" + + "};\n" + + "/**\n" + + " * @const\n" + + " * @type {number}\n" + + " */\n" + + "AppName.prototype.ROYALE_COMPILE_FLAGS = 9;\n" + + "\n" + + "\n"; + + assertOutWithMetadata(outTemplate.replaceAll("AppName", appName)); + } + + @Test + public void testBackslashCloseCurlyBraceStringAttribute() + { + String code = "<basic:Application xmlns:fx=\"http://ns.adobe.com/mxml/2009\" xmlns:basic=\"library://ns.apache.org/royale/basic\">" + + "<basic:beads><basic:TextPromptBead prompt=\"{0-9\\}\"/></basic:beads></basic:Application>"; + + IMXMLDocumentNode dnode = (IMXMLDocumentNode) getNode(code, + IMXMLDocumentNode.class, RoyaleTestBase.WRAP_LEVEL_NONE); + + ((JSRoyaleEmitter)(mxmlBlockWalker.getASEmitter())).getModel().setCurrentClass(dnode.getDefinition()); + mxmlBlockWalker.visitDocument(dnode); + String appName = dnode.getQualifiedName(); + String outTemplate = "/**\n" + + " * AppName\n" + + " *\n" + + " * @fileoverview\n" + + " *\n" + + " * @suppress {checkTypes|accessControls}\n" + + " */\n" + + "\n" + + "goog.provide('AppName');\n" + + "\n" + + "goog.require('org.apache.royale.core.Application');\n" + + "goog.require('org.apache.royale.html.accessories.TextPromptBead');\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * @constructor\n" + + " * @extends {org.apache.royale.core.Application}\n" + + " */\n" + + "AppName = function() {\n" + + " AppName.base(this, 'constructor');\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {org.apache.royale.html.accessories.TextPromptBead}\n" + + " */\n" + + " this.$ID_8_0;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldd;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldp;\n" + + "\n" + + " this.generateMXMLAttributes([\n" + + " 1,\n" + + " 'beads',\n" + + " null,\n" + + " [\n" + + " org.apache.royale.html.accessories.TextPromptBead,\n" + + " 2,\n" + + " '_id',\n" + + " true,\n" + + " '$ID_8_0',\n" + + " 'prompt',\n" + + " true,\n" + + " '{0-9}',\n" + + " 0,\n" + + " 0,\n" + + " null\n" + + " ],\n" + + " 0,\n" + + " 0\n" + + " ]);\n" + + " \n" + + "};\n" + + "goog.inherits(AppName, org.apache.royale.core.Application);\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Metadata\n" + + " *\n" + + " * @type {Object.<string, Array.<Object>>}\n" + + " */\n" + + "AppName.prototype.ROYALE_CLASS_INFO = { names: [{ name: 'AppName', qName: 'AppName', kind: 'class' }] };\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * Reflection\n" + + " *\n" + + " * @return {Object.<string, Function>}\n" + + " */\n" + + "AppName.prototype.ROYALE_REFLECTION_INFO = function () {\n" + + " return {\n" + + " methods: function () {\n" + + " return {\n" + + " 'AppName': { type: '', declaredBy: 'AppName'}\n"+ + " };\n" + + " }\n" + + " };\n" + + "};\n" + + "/**\n" + + " * @const\n" + + " * @type {number}\n" + + " */\n" + + "AppName.prototype.ROYALE_COMPILE_FLAGS = 9;\n" + + "\n" + + "\n"; + + assertOutWithMetadata(outTemplate.replaceAll("AppName", appName)); + } + + @Test + public void testBackslashOpenAndCloseCurlyBraceStringAttribute() + { + String code = "<basic:Application xmlns:fx=\"http://ns.adobe.com/mxml/2009\" xmlns:basic=\"library://ns.apache.org/royale/basic\">" + + "<basic:beads><basic:TextPromptBead prompt=\"\\{0-9\\}\"/></basic:beads></basic:Application>"; + + IMXMLDocumentNode dnode = (IMXMLDocumentNode) getNode(code, + IMXMLDocumentNode.class, RoyaleTestBase.WRAP_LEVEL_NONE); + + ((JSRoyaleEmitter)(mxmlBlockWalker.getASEmitter())).getModel().setCurrentClass(dnode.getDefinition()); + mxmlBlockWalker.visitDocument(dnode); + String appName = dnode.getQualifiedName(); + String outTemplate = "/**\n" + + " * AppName\n" + + " *\n" + + " * @fileoverview\n" + + " *\n" + + " * @suppress {checkTypes|accessControls}\n" + + " */\n" + + "\n" + + "goog.provide('AppName');\n" + + "\n" + + "goog.require('org.apache.royale.core.Application');\n" + + "goog.require('org.apache.royale.html.accessories.TextPromptBead');\n" + + "\n" + + "\n" + + "\n" + + "/**\n" + + " * @constructor\n" + + " * @extends {org.apache.royale.core.Application}\n" + + " */\n" + + "AppName = function() {\n" + + " AppName.base(this, 'constructor');\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {org.apache.royale.html.accessories.TextPromptBead}\n" + + " */\n" + + " this.$ID_8_0;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldd;\n" + + " \n" + + " /**\n" + + " * @private\n" + + " * @type {Array}\n" + + " */\n" + + " this.mxmldp;\n" + + "\n" + + " this.generateMXMLAttributes([\n" + + " 1,\n" + + " 'beads',\n" + + " null,\n" + + " [\n" + + " org.apache.royale.html.accessories.TextPromptBead,\n" + + " 2,\n" + + " '_id',\n" + + " true,\n" + + " '$ID_8_0',\n" + + " 'prompt',\n" + + " true,\n" + + " '{0-9}',\n" + " 0,\n" + " 0,\n" + " null\n" + diff --git a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLDataBindingParser.java b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLDataBindingParser.java index 329ba167d..6408d58b8 100644 --- a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLDataBindingParser.java +++ b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLDataBindingParser.java @@ -189,7 +189,7 @@ class MXMLDataBindingParser case RIGHT_BRACE: { nesting--; - if (nesting == 0) + if (nesting == 0 && !escape && leftBraceCharIndex != -1) { // We've found the matching '}' for the '{'. // Record where the '{' and '}' are. @@ -197,6 +197,10 @@ class MXMLDataBindingParser result = LinkedListMultimap.<ISourceFragment, Integer> create(); result.put(leftBraceFragment, leftBraceCharIndex); result.put(fragment, i); + // clear the left brace, so that we can start + // searching for a new one + leftBraceFragment = null; + leftBraceCharIndex = -1; } escape = false; break; @@ -351,6 +355,8 @@ class MXMLDataBindingParser { ISourceFragment[] fragments = fragmentList.toArray(new ISourceFragment[0]); String text = SourceFragmentsReader.concatLogicalText(fragments); + // the { and } characters may be escaped with backslash, so remove it + text = text.replaceAll("\\\\\\{", "{").replaceAll("\\\\\\}", "}"); // LiteralNode automatically strips out quote characters at the // beginning and end of the string. diff --git a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLTreeBuilder.java b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLTreeBuilder.java index 6916a3716..979ea2655 100644 --- a/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLTreeBuilder.java +++ b/compiler/src/main/java/org/apache/royale/compiler/internal/tree/mxml/MXMLTreeBuilder.java @@ -537,6 +537,12 @@ public class MXMLTreeBuilder { text = SourceFragmentsReader.concatPhysicalText(fragments); } + if (flags.contains(TextParsingFlags.ALLOW_BINDING)) + { + // if binding is allowed, then the { and } characters may be + // escaped with backslash, so remove the backslash + text = text.replaceAll("\\\\\\{", "{").replaceAll("\\\\\\}", "}"); + } value = parseValue(propertyNode, type, text, flags, defaultValue, isAttribute);
