diff --git a/Service/Options/IStylerOptions.cs b/Service/Options/IStylerOptions.cs index add56d1..27f9f40 100644 --- a/Service/Options/IStylerOptions.cs +++ b/Service/Options/IStylerOptions.cs @@ -15,6 +15,8 @@ public interface IStylerOptions bool KeepFirstAttributeOnSameLine { get; set; } + string FirstLineAttributes { get; set; } + int MaxAttributeCharatersPerLine { get; set; } int MaxAttributesPerLine { get; set; } diff --git a/Service/Options/StylerOptions.cs b/Service/Options/StylerOptions.cs index bc1a61a..6f6d1b2 100644 --- a/Service/Options/StylerOptions.cs +++ b/Service/Options/StylerOptions.cs @@ -164,6 +164,13 @@ public string SerializedAttributeOrderingRuleGroups } } + [Category("Attribute Reordering")] + [DisplayName("First Line Attributes")] + [JsonProperty("FirstLineAttributes", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [Description("Defines a list of attributes which should always appear on the same line as the element's start tag. Attribute reordering must be enabled for this setting to take effect.\r\n\r\nDefault Value: None")] + [DefaultValue("")] + public string FirstLineAttributes { get; set; } + [Category("Attribute Reordering")] [DisplayName("Order Attributes By Name")] [JsonProperty("OrderAttributesByName", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] diff --git a/Service/Options/defaultSettings.json b/Service/Options/defaultSettings.json index 44c55ab..97e3c85 100644 --- a/Service/Options/defaultSettings.json +++ b/Service/Options/defaultSettings.json @@ -20,6 +20,7 @@ "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText" ], + "FirstLineAttributes": "", "OrderAttributesByName": true, "PutEndingBracketOnNewLine": false, "RemoveEndingTagOfEmptyElement": true, diff --git a/Service/StylerService.cs b/Service/StylerService.cs index cf87549..7fb791d 100644 --- a/Service/StylerService.cs +++ b/Service/StylerService.cs @@ -23,6 +23,8 @@ public class StylerService : XmlEscapingService private IList NewlineExemptionElementsList { get; set; } + private IList FirstLineAttributes { get; set; } + private IList NoNewLineMarkupExtensionsList { get; set; } private AttributeOrderRules OrderRules { get; set; } @@ -98,6 +100,7 @@ public static StylerService CreateInstance(IStylerOptions options) { var stylerServiceInstance = new StylerService { Options = options }; stylerServiceInstance.NewlineExemptionElementsList = stylerServiceInstance.Options.NewlineExemptionElements.ToList(); + stylerServiceInstance.FirstLineAttributes = stylerServiceInstance.Options.FirstLineAttributes.ToList(); stylerServiceInstance.NoNewLineMarkupExtensionsList = stylerServiceInstance.Options.NoNewLineMarkupExtensions.ToList(); stylerServiceInstance.OrderRules = new AttributeOrderRules(options); stylerServiceInstance.ElementProcessStatusStack.Clear(); @@ -278,6 +281,11 @@ private bool IsNoLineBreakElement(string elementName) return this.NewlineExemptionElementsList.Contains(elementName); } + private bool IsFirstLineAttribute(string attributeName) + { + return this.FirstLineAttributes.Contains(attributeName); + } + private void ProcessXMLRoot(XmlReader xmlReader, StringBuilder output) { output.Append(""); @@ -393,6 +401,7 @@ private void ProcessElement(XmlReader xmlReader, StringBuilder output) bool isEmptyElement = xmlReader.IsEmptyElement; bool hasPutEndingBracketOnNewLine = false; var list = new List(xmlReader.AttributeCount); + var firstLineList = new List(xmlReader.AttributeCount); if (xmlReader.HasAttributes) { @@ -401,7 +410,15 @@ private void ProcessElement(XmlReader xmlReader, StringBuilder output) string attributeName = xmlReader.Name; string attributeValue = xmlReader.Value; AttributeOrderRule orderRule = OrderRules.GetRuleFor(attributeName); - list.Add(new AttributeInfo(attributeName, attributeValue, orderRule)); + + var attribute = new AttributeInfo(attributeName, attributeValue, orderRule); + list.Add(attribute); + + // Maintain separate list of first line attributes. + if (this.Options.EnableAttributeReordering && this.IsFirstLineAttribute(attribute.Name)) + { + firstLineList.Add(attribute); + } // Check for xml:space as defined in http://www.w3.org/TR/2008/REC-xml-20081126/#sec-white-space if (xmlReader.IsXmlSpaceAttribute()) @@ -413,6 +430,7 @@ private void ProcessElement(XmlReader xmlReader, StringBuilder output) if (this.Options.EnableAttributeReordering) { list.Sort(AttributeInfoComparison); + firstLineList.Sort(AttributeInfoComparison); } currentIndentString = this.GetIndentString(xmlReader.Depth); @@ -459,8 +477,27 @@ private void ProcessElement(XmlReader xmlReader, StringBuilder output) int attributeCountInCurrentLineBuffer = 0; AttributeInfo lastAttributeInfo = null; + + // Process first line attributes. + string firstLine = String.Empty; + foreach (var attrInfo in firstLineList) + { + firstLine = $"{firstLine} {attrInfo.ToSingleLineString()}"; + } + + if (firstLine.Length > 0) + { + attributeLines.Add(firstLine); + } + foreach (AttributeInfo attrInfo in list) { + // Skip attributes already added to first line. + if (firstLineList.Contains(attrInfo)) + { + continue; + } + // Attributes with markup extension, always put on new line if (attrInfo.IsMarkupExtension && this.Options.FormatMarkupExtension) { @@ -537,7 +574,7 @@ private void ProcessElement(XmlReader xmlReader, StringBuilder output) for (int i = 0; i < attributeLines.Count; i++) { - if ((i == 0) && this.Options.KeepFirstAttributeOnSameLine) + if ((i == 0) && (this.Options.KeepFirstAttributeOnSameLine || (firstLineList.Count > 0))) { output.Append(' ').Append(attributeLines[i].Trim()); diff --git a/UnitTests/Configurations/Default.json b/UnitTests/Configurations/Default.json index 44c55ab..97e3c85 100644 --- a/UnitTests/Configurations/Default.json +++ b/UnitTests/Configurations/Default.json @@ -20,6 +20,7 @@ "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText" ], + "FirstLineAttributes": "", "OrderAttributesByName": true, "PutEndingBracketOnNewLine": false, "RemoveEndingTagOfEmptyElement": true, diff --git a/UnitTests/TestConfigurations/AllDifferent.json b/UnitTests/TestConfigurations/AllDifferent.json index 91ad9df..be42c9c 100644 --- a/UnitTests/TestConfigurations/AllDifferent.json +++ b/UnitTests/TestConfigurations/AllDifferent.json @@ -8,6 +8,7 @@ "SeparateByGroups": true, "EnableAttributeReordering": false, "AttributeOrderingRuleGroups": [ "x:Class" ], + "FirstLineAttributes": "x:Name", "OrderAttributesByName": false, "PutEndingBracketOnNewLine": true, "RemoveEndingTagOfEmptyElement": false, diff --git a/UnitTests/TestConfigurations/BadSetting.json b/UnitTests/TestConfigurations/BadSetting.json index 2fde4fc..4dd47e8 100644 --- a/UnitTests/TestConfigurations/BadSetting.json +++ b/UnitTests/TestConfigurations/BadSetting.json @@ -20,6 +20,7 @@ "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText" ], + "FirstLineAttributes": "", "OrderAttributesByName": true, "PutEndingBracketOnNewLine": false, "RemoveEndingTagOfEmptyElement": true, diff --git a/UnitTests/TestConfigurations/Default.json b/UnitTests/TestConfigurations/Default.json index 44c55ab..97e3c85 100644 --- a/UnitTests/TestConfigurations/Default.json +++ b/UnitTests/TestConfigurations/Default.json @@ -20,6 +20,7 @@ "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText" ], + "FirstLineAttributes": "", "OrderAttributesByName": true, "PutEndingBracketOnNewLine": false, "RemoveEndingTagOfEmptyElement": true, diff --git a/UnitTests/TestFiles/TestKeepSelectAttributesOnFirstLine.expected b/UnitTests/TestFiles/TestKeepSelectAttributesOnFirstLine.expected new file mode 100644 index 0000000..94e8c53 --- /dev/null +++ b/UnitTests/TestFiles/TestKeepSelectAttributesOnFirstLine.expected @@ -0,0 +1,22 @@ + + +