diff --git a/src/wix/WixToolset.Converters/WixConverter.cs b/src/wix/WixToolset.Converters/WixConverter.cs index 2d75c5e5f..3fea44986 100644 --- a/src/wix/WixToolset.Converters/WixConverter.cs +++ b/src/wix/WixToolset.Converters/WixConverter.cs @@ -1033,7 +1033,7 @@ private void ConvertControlElement(XElement element) using (var lab = new ConversionLab(element)) { var xConditions = element.Elements(ConditionElementName).ToList(); - var context = new InnerTextContext(); + var collector = new InnerContentCollector(); var conditions = new List<KeyValuePair<string, string>>(); foreach (var xCondition in xConditions) @@ -1041,10 +1041,10 @@ private void ConvertControlElement(XElement element) var action = UppercaseFirstChar(xCondition.Attribute("Action")?.Value); if (!String.IsNullOrEmpty(action) && - TryGetInnerText(xCondition, context) && + collector.CollectInnerTextAndCommentsForAttributeValue(xCondition, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the '{1}Condition' attribute instead.", xCondition.Name.LocalName, action)) { - conditions.Add(new KeyValuePair<string, string>(action, context.Value)); + conditions.Add(new KeyValuePair<string, string>(action, value)); } } @@ -1063,7 +1063,7 @@ private void ConvertControlElement(XElement element) } lab.RemoveOrphanTextNodes(); - lab.AddCommentsAsSiblings(context.Comments); + lab.AddCommentsAsSiblings(collector.Comments); } } @@ -1081,17 +1081,17 @@ private void ConvertComponentElement(XElement element) var xCondition = element.Element(ConditionElementName); if (xCondition != null) { - var context = new InnerTextContext(); + var collector = new InnerContentCollector(); - if (TryGetInnerText(xCondition, context) && + if (collector.CollectInnerTextAndCommentsForAttributeValue(xCondition, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName)) { using (var lab = new ConversionLab(element)) { xCondition.Remove(); - element.Add(new XAttribute("Condition", context.Value)); + element.Add(new XAttribute("Condition", value)); lab.RemoveOrphanTextNodes(); - lab.AddCommentsAsSiblings(context.Comments); + lab.AddCommentsAsSiblings(collector.Comments); } } } @@ -1194,18 +1194,18 @@ private void ConvertFeatureElement(XElement element) if (xCondition != null) { var level = xCondition.Attribute("Level")?.Value; - var context = new InnerTextContext(); + var collector = new InnerContentCollector(); if (!String.IsNullOrEmpty(level) && - TryGetInnerText(xCondition, context) && + collector.CollectInnerTextAndCommentsForAttributeValue(xCondition, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Level' element instead.", xCondition.Name.LocalName)) { using (var lab = new ConversionLab(xCondition)) { lab.ReplaceTargetElement(new XElement(LevelElementName, new XAttribute("Value", level), - new XAttribute("Condition", context.Value))); - lab.AddCommentsAsSiblings(context.Comments); + new XAttribute("Condition", value))); + lab.AddCommentsAsSiblings(collector.Comments); } } } @@ -1240,23 +1240,23 @@ private void ConvertFileElement(XElement element) private void ConvertLaunchConditionElement(XElement element) { var message = element.Attribute("Message")?.Value; - var context = new InnerTextContext() { ReportWhitespace = true }; + var collector = new InnerContentCollector(); if (!String.IsNullOrEmpty(message) && - TryGetInnerText(element, context) && + collector.CollectInnerTextWithTrailingWhitespaceAndCommentsForAttributeValue(element, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Launch' element instead.", element.Name.LocalName)) { - if (String.IsNullOrWhiteSpace(context.Value)) + if (String.IsNullOrWhiteSpace(value)) { - context.Value = String.Empty; + value = String.Empty; } using (var lab = new ConversionLab(element)) { lab.ReplaceTargetElement(new XElement(LaunchElementName, - new XAttribute("Condition", context.Value), + new XAttribute("Condition", value), new XAttribute("Message", message))); - lab.AddCommentsAsSiblings(context.Comments); + lab.AddCommentsAsSiblings(collector.Comments); } } } @@ -1301,8 +1301,8 @@ private void ConvertPermissionExElement(XElement element) var xCondition = element.Element(ConditionElementName); if (xCondition != null) { - var context = new InnerTextContext(); - if (TryGetInnerText(xCondition, context) && + var collector = new InnerContentCollector(); + if (collector.CollectInnerTextAndCommentsForAttributeValue(xCondition, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName)) { using (var lab = new ConversionLab(xCondition)) @@ -1311,9 +1311,9 @@ private void ConvertPermissionExElement(XElement element) } using (var lab = new ConversionLab(element)) { - element.Add(new XAttribute("Condition", context.Value)); + element.Add(new XAttribute("Condition", value)); lab.RemoveOrphanTextNodes(); - lab.AddCommentsAsSiblings(context.Comments); + lab.AddCommentsAsSiblings(collector.Comments); } } } @@ -1681,35 +1681,35 @@ private void ConvertCustomActionRefElement(XElement element) private void ConvertPublishElement(XElement element) { - var context = new InnerTextContext(); + var collector = new InnerContentCollector(); - if (TryGetInnerText(element, context) && + if (collector.CollectInnerTextAndCommentsForAttributeValue(element, out string value) && this.OnInformation(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", element.Name.LocalName)) { using (var lab = new ConversionLab(element)) { - if ("1" == context.Value) + if ("1" == value) { this.OnInformation(ConverterTestType.PublishConditionOneUnnecessary, element, "Adding Condition='1' on {0} elements is no longer necessary. Remove the Condition attribute.", element.Name.LocalName); } else { - element.Add(new XAttribute("Condition", context.Value)); + element.Add(new XAttribute("Condition", value)); } lab.RemoveOrphanTextNodes(); - lab.AddCommentsAsSiblings(context.Comments); + lab.AddCommentsAsSiblings(collector.Comments); } } - var evnt = element.Attribute("Event")?.Value; - var value = element.Attribute("Value")?.Value; + var eventName = element.Attribute("Event")?.Value; + var eventValue = element.Attribute("Value")?.Value; - if (evnt?.Equals("DoAction", StringComparison.OrdinalIgnoreCase) == true - && value?.StartsWith("WixUI", StringComparison.OrdinalIgnoreCase) == true + if (eventName?.Equals("DoAction", StringComparison.OrdinalIgnoreCase) == true + && eventValue?.StartsWith("WixUI", StringComparison.OrdinalIgnoreCase) == true && this.OnInformation(ConverterTestType.CustomActionIdsIncludePlatformSuffix, element, "Custom action ids have changed in WiX v4 extensions to support platform-specific custom actions. For more information, see https://wixtoolset.org/docs/fourthree/#converting-custom-wixui-dialog-sets.")) { - element.Attribute("Value").Value = value + "_$(sys.BUILDARCHSHORT)"; + element.Attribute("Value").Value = eventValue + "_$(sys.BUILDARCHSHORT)"; } } @@ -1996,16 +1996,10 @@ private void ConvertCustomActionElement(XElement xCustomAction) } var xScript = xCustomAction.Attribute("Script"); - var context = new InnerTextContext() { CoalesceWhitespace = false }; + var collector = new InnerContentCollector(); - if (xScript != null && TryGetInnerText(xCustomAction, context)) + if (xScript != null && collector.CollectInnerTextWithTrailingWhitespaceAndCommentsForScriptFile(xCustomAction, out string value)) { - if (null != context.Value) - { - char[] whitespaceChars = { ' ', '\t', '\r', '\n' }; - context.Value = context.Value.Trim(whitespaceChars); - } - if (this.OnInformation(ConverterTestType.InnerTextDeprecated, xCustomAction, "Using {0} element text is deprecated. Extract the text to a file and use the 'ScriptSourceFile' attribute to reference it.", xCustomAction.Name.LocalName)) { var scriptFolder = Path.GetDirectoryName(this.SourceFile) ?? String.Empty; @@ -2013,12 +2007,12 @@ private void ConvertCustomActionElement(XElement xCustomAction) var ext = (xScript.Value == "jscript") ? ".js" : (xScript.Value == "vbscript") ? ".vbs" : ".txt"; var scriptFile = Path.Combine(scriptFolder, id + ext); - File.WriteAllText(scriptFile, context.Value); + File.WriteAllText(scriptFile, value); RemoveChildren(xCustomAction); xCustomAction.Add(new XAttribute("ScriptSourceFile", scriptFile)); - if (context.Comments.Any()) + if (collector.Comments.Any()) { var remainingNodes = xCustomAction.NodesAfterSelf().ToList(); var replacementNodes = remainingNodes.Where(e => XmlNodeType.Text != e.NodeType); @@ -2026,7 +2020,7 @@ private void ConvertCustomActionElement(XElement xCustomAction) { node.Remove(); } - foreach (var comment in context.Comments) + foreach (var comment in collector.Comments) { xCustomAction.Add(comment); xCustomAction.Add("\n"); @@ -2165,9 +2159,9 @@ private void ConvertWixLocalizationUIElement(XElement element) private void ConvertInnerTextToAttribute(XElement element, string attributeName) { - var context = new InnerTextContext(); + var collector = new InnerContentCollector(); - if (TryGetInnerText(element, context)) + if (collector.CollectInnerTextAndCommentsForAttributeValue(element, out string value)) { // If the target attribute already exists, error if we have anything more than whitespace. var attribute = element.Attribute(attributeName); @@ -2180,8 +2174,8 @@ private void ConvertInnerTextToAttribute(XElement element, string attributeName) using (var lab = new ConversionLab(element)) { lab.RemoveOrphanTextNodes(); - element.Add(new XAttribute(attributeName, context.Value)); - lab.AddCommentsAsSiblings(context.Comments); + element.Add(new XAttribute(attributeName, value)); + lab.AddCommentsAsSiblings(collector.Comments); } } } @@ -2874,142 +2868,6 @@ private static void RenameElementToStandardDirectory(XElement element) } } - // This function is used to simultaneously extract the inner text of, and any comments embedded in, an XElement. - // The comments are returned in a list, so there can be several of them. In addition, the code is designed so that - // it can be called in a loop, processing one element at a time, extracting the comments cumulatively, and returning - // the inner text separately with each call. - // - // This function's first parameter refers to an XElement containinng inner text and comments to be extracted. - // - // Other information is passed to and from TryGetInnerText() through the properties of an InnerTextContext object, - // which is passed to this function as its second parameter. - // - // On entry, the Comments property of the InnerTextContext object contains a List<XNode> erpresenting the comments - // collected during any previous calls to this function using this InnerTextCntext object. TryGetInnerText() adds - // any comments it finds to this List<XNode>. - // - // The inner text is returned through the Value property of the InnerTextContext object. - // - // The function, itself, returns true if inner text is found. - // - // There are two other properties of the InnerTextContext object that control the behavior of the function: The CoalesceWhitespace - // property, with a default value of true, causes contiguous whitespace to be coalesced into a single space character and leading and - // trailing whitespace to be trimmed. When this paarameter is false, the inner text is returned with all whitespace intact. - // - // The other property of the InnerTextContext object, ReportWhitespace, has a default value of "false". When this property is - // false, the text found is trimmed of leading and trailing whitespace before being returned and the function only returns true - // if the returned text is not empty. If it is true, the value returned by the function is true, not only when non-whitespace - // text is returned in the Value property, but also when only whitespace is found. In this case, the text returned is a single space. - // - // There is one other important case in which the function returns true: If a CDATA node is processed, even if all the whitespace - // removed causes the remaining text to be empty, the function returns true. If in fact, the remaining text is empty, the text - // returned is a single space. - private static bool TryGetInnerText(XElement element, InnerTextContext context) - { - context.Value = null; - char[] whitespaceChars = { ' ', '\t', '\r', '\n' }; - var nodes = element.Nodes().ToList(); - var inWhitespace = false; - var cDataFound = false; - var whitespaceFound = false; - var sb = new StringBuilder(); - - foreach (var node in nodes) - { - if (XmlNodeType.Comment == node.NodeType) - { - context.Comments.Add(node); - } - else if (XmlNodeType.CDATA == node.NodeType || XmlNodeType.Text == node.NodeType) - { - var isCData = XmlNodeType.CDATA == node.NodeType; - - if (isCData) - { - cDataFound = true; - } - - var text = (node as XText)?.Value; - - if (context.CoalesceWhitespace) - { - var nodeSB = new StringBuilder(); - - foreach (var c in text) - { - char? emit = c; - - // Replace contiguous whitespace with a single space. - if (' ' == c || '\r' == c || '\n' == c || '\t' == c) - { - if (!inWhitespace) - { - inWhitespace = true; - whitespaceFound = true; - emit = ' '; - } - else - { - emit = null; - } - } - else - { - inWhitespace = false; - } - - if (emit.HasValue) - { - nodeSB.Append(emit); - } - } - - text = nodeSB.ToString(); - } - - if (0 < text.Length) - { - text = text.Trim(whitespaceChars); - - if (0 == text.Length && context.ReportWhitespace) - { - text = " "; - } - } - - sb.Append(text); - } - } - - var found = false; - - context.Value = sb.ToString(); - - if (0 < context.Value.Length) - { - found = true; - } - - context.Value = context.Value.Trim(whitespaceChars); - - if (context.ReportWhitespace && whitespaceFound) - { - found = true; - } - - if (cDataFound) - { - found = true; - - if (0 == context.Value.Length) - { - context.Value = " "; - } - } - - return found; - } - private static bool IsTextNode(XNode node, out XText text) { text = null; @@ -3079,41 +2937,247 @@ private static bool WasImplicitlyStringTyped(string value) return true; } - // This class represents the context in which TryGetInnerText() is called. - // - // Upon return from TryGetInnerText(), the Value property represents a string comprisiing the non-comment portion of an XML element's inner text. - // - // The Comments property always returns a (possibly empty) List<XNode> representing any comments that have been accumulated from previous calls - // to TryGetInnerText(), using the same InnerTextContext object. Even if the intended call to TryGetInnerText is short-circuited, the Comments - // property still returns the intended List<XNode>. - // - // Upon entry to TryGetInnerText(), the CoalesceWhitespace and reportWhitespace properties contain values that control the behavior of TryGetInnerContext(), - // as explained in the introductory comments of that function. - private class InnerTextContext + // This class encapsulates methods for extraving text and comments from XElements. Multiple calls can be made to the collection + // methods to processs multiple XElements. The Comments property is used to extract the list of comments accumulated during the collection process. + private class InnerContentCollector { - internal string Value { get; set; } = null; - internal List<XNode> comments = null; + public InnerContentCollector() + { + this.Comments = new List<XNode>(); + } - internal List<XNode> Comments + public List<XNode> Comments { get; private set; } + + public bool CollectInnerTextAndCommentsForAttributeValue(XElement element, out string collectedText) { - get + char[] whitespaceChars = { ' ', '\t', '\r', '\n' }; + var nodes = element.Nodes().ToList(); + var inWhitespace = false; + var cDataFound = false; + var sb = new StringBuilder(); + + foreach (var node in nodes) { - if (null == this.comments) + if (XmlNodeType.Comment == node.NodeType) { - this.comments = new List<XNode>(); + this.Comments.Add(node); } + else if (XmlNodeType.CDATA == node.NodeType || XmlNodeType.Text == node.NodeType) + { + var isCData = XmlNodeType.CDATA == node.NodeType; + + if (isCData) + { + cDataFound = true; + } + + var text = node is XText xtext ? xtext.Value : String.Empty; + var nodeSB = new StringBuilder(); + + foreach (var c in text) + { + char? emit = c; + + // Replace contiguous whitespace with a single space. + if (' ' == c || '\r' == c || '\n' == c || '\t' == c) + { + if (!inWhitespace) + { + inWhitespace = true; + emit = ' '; + } + else + { + emit = null; + } + } + else + { + inWhitespace = false; + } - return this.comments; + if (emit.HasValue) + { + nodeSB.Append(emit); + } + } + + text = nodeSB.ToString().Trim(whitespaceChars); + sb.Append(text); + } } + var found = false; + collectedText = sb.ToString(); - set => this.comments = value; + if (0 < collectedText.Length) + { + found = true; + } + + collectedText = collectedText.Trim(whitespaceChars); + + if (cDataFound) + { + found = true; + + if (0 == collectedText.Length) + { + collectedText = " "; + } + } + + return found; } - internal bool CoalesceWhitespace { get; set; } = true; - internal bool ReportWhitespace { get; set; } = false; + public bool CollectInnerTextWithTrailingWhitespaceAndCommentsForAttributeValue(XElement element, out string collectedText) + { + char[] whitespaceChars = { ' ', '\t', '\r', '\n' }; + var nodes = element.Nodes().ToList(); + var inWhitespace = false; + var cDataFound = false; + var whitespaceFound = false; + var sb = new StringBuilder(); + + foreach (var node in nodes) + { + if (XmlNodeType.Comment == node.NodeType) + { + this.Comments.Add(node); + } + else if (XmlNodeType.CDATA == node.NodeType || XmlNodeType.Text == node.NodeType) + { + var isCData = XmlNodeType.CDATA == node.NodeType; + + if (isCData) + { + cDataFound = true; + } + + var text = node is XText xtext ? xtext.Value : String.Empty; + var nodeSB = new StringBuilder(); - internal InnerTextContext() + foreach (var c in text) + { + char? emit = c; + + // Replace contiguous whitespace with a single space. + if (' ' == c || '\r' == c || '\n' == c || '\t' == c) + { + if (!inWhitespace) + { + inWhitespace = true; + whitespaceFound = true; + emit = ' '; + } + else + { + emit = null; + } + } + else + { + inWhitespace = false; + } + + if (emit.HasValue) + { + nodeSB.Append(emit); + } + } + + text = nodeSB.ToString(); + + if (0 < text.Length) + { + text = text.Trim(whitespaceChars); + + if (0 == text.Length) + { + text = " "; + } + } + + sb.Append(text); + } + } + var found = false; + collectedText = sb.ToString(); + + if (0 < collectedText.Length) + { + found = true; + } + + collectedText = collectedText.Trim(whitespaceChars); + + if (whitespaceFound) + { + found = true; + } + + if (cDataFound) + { + found = true; + + if (0 == collectedText.Length) + { + collectedText = " "; + } + } + + return found; + } + + public bool CollectInnerTextWithTrailingWhitespaceAndCommentsForScriptFile(XElement element, out string collectedText) { + var value = String.Empty; + char[] whitespaceChars = { ' ', '\t', '\r', '\n' }; + var nodes = element.Nodes().ToList(); + var cDataFound = false; + var sb = new StringBuilder(); + + foreach (var node in nodes) + { + if (XmlNodeType.Comment == node.NodeType) + { + this.Comments.Add(node); + } + else if (XmlNodeType.CDATA == node.NodeType || XmlNodeType.Text == node.NodeType) + { + var isCData = XmlNodeType.CDATA == node.NodeType; + + if (isCData) + { + cDataFound = true; + } + + var text = node is XText xtext ? xtext.Value.Trim(whitespaceChars) : String.Empty; + sb.Append(text); + } + } + + var found = false; + + collectedText = sb.ToString(); + + if (0 < collectedText.Length) + { + found = true; + } + + collectedText = collectedText.Trim(whitespaceChars); + + if (cDataFound) + { + found = true; + + if (0 == collectedText.Length) + { + collectedText = " "; + } + } + + return found; } }