diff --git a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs index 1ea4cc0..f149267 100755 --- a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs +++ b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs @@ -107,16 +107,37 @@ public static HtmlAttributeCollection ParseStyle(string? htmlStyles) /// /// Gets the named attribute. /// - public string? this[string name] + public ReadOnlySpan this[string name] { get { if (attributes.TryGetValue(name, out var range)) - return rawValue.AsSpan().Slice(range).ToString().Trim(); - return null; + return rawValue.AsSpan().Slice(range).Trim(); + return []; } } + /// + /// Determines whether the collection contains the specified key. + /// + public bool ContainsKey(string name) + { + return attributes.ContainsKey(name); + } + + /// + /// Efficient way to determine if a style is equals to a value. + /// + public bool HasKeyEqualsTo(string name, string value) + { + if (attributes.TryGetValue(name, out var range)) + { + var span = rawValue.AsSpan().Slice(range).Trim(); + return span.Equals(value.AsSpan(), StringComparison.InvariantCultureIgnoreCase); + } + return false; + } + /// /// Gets an attribute representing a color (named color, hexadecimal or hexadecimal /// without the preceding # character). @@ -147,6 +168,8 @@ public Unit GetUnit(string name, UnitMetric defaultMetric = UnitMetric.Unitless) public Margin GetMargin(string name) { Margin margin = Margin.Empty; + if (IsEmpty) return margin; + if (attributes.TryGetValue(name, out var range)) margin = Margin.Parse(rawValue.AsSpan().Slice(range)); @@ -194,6 +217,8 @@ public HtmlBorder GetBorders() public SideBorder GetSideBorder(string name) { SideBorder border = SideBorder.Empty; + if (IsEmpty) return border; + if (attributes.TryGetValue(name, out Range range)) border = SideBorder.Parse(rawValue.AsSpan().Slice(range)); @@ -224,6 +249,8 @@ public SideBorder GetSideBorder(string name) public HtmlFont GetFont(string name) { HtmlFont font = HtmlFont.Empty; + if (IsEmpty) return font; + if (attributes.TryGetValue(name, out Range range)) font = HtmlFont.Parse(rawValue.AsSpan().Slice(range)); diff --git a/src/Html2OpenXml/Expressions/BlockElementExpression.cs b/src/Html2OpenXml/Expressions/BlockElementExpression.cs index bf5de42..7973153 100644 --- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs +++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs @@ -90,7 +90,7 @@ protected override IEnumerable Interpret ( { return ComposeChildren(context, childNodes, paraProperties, (runs) => { - if ("always".Equals(styleAttributes!["page-break-before"], StringComparison.OrdinalIgnoreCase)) + if (styleAttributes.HasKeyEqualsTo("page-break-before", "always")) { runs.Add( new Run( @@ -102,7 +102,7 @@ protected override IEnumerable Interpret ( } }, (runs) => { - if ("always".Equals(styleAttributes!["page-break-after"], StringComparison.OrdinalIgnoreCase)) + if (styleAttributes.HasKeyEqualsTo("page-break-after", "always")) { runs.Add(new Run( new Break() { Type = BreakValues.Page })); @@ -186,8 +186,8 @@ protected override void ComposeStyles (ParsingContext context) }; } - JustificationValues? align = Converter.ToParagraphAlign(styleAttributes!["text-align"]); - if (!align.HasValue) align = Converter.ToParagraphAlign(node.GetAttribute("align")); + JustificationValues? align = Converter.ToParagraphAlign(styleAttributes["text-align"]); + if (!align.HasValue) align = Converter.ToParagraphAlign(node.GetAttribute("align").AsSpan()); if (!align.HasValue) align = Converter.ToParagraphAlign(styleAttributes["justify-content"]); if (align.HasValue) { @@ -256,7 +256,7 @@ protected override void ComposeStyles (ParsingContext context) var lineHeight = styleAttributes.GetUnit("line-height"); if (!lineHeight.IsValid - && "normal".Equals(styleAttributes["line-height"], StringComparison.OrdinalIgnoreCase)) + && styleAttributes.HasKeyEqualsTo("line-height", "normal")) { // if `normal` is specified, reset any values lineHeight = new Unit(UnitMetric.Unitless, 1); diff --git a/src/Html2OpenXml/Expressions/BodyExpression.cs b/src/Html2OpenXml/Expressions/BodyExpression.cs index 462d375..9b86a3a 100644 --- a/src/Html2OpenXml/Expressions/BodyExpression.cs +++ b/src/Html2OpenXml/Expressions/BodyExpression.cs @@ -69,10 +69,11 @@ protected override void ComposeStyles(ParsingContext context) // Unsupported W3C attribute but claimed by users. Specified at level, the page // orientation is applied on the whole document - string? attr = styleAttributes!["page-orientation"]; - if (attr != null) + if (styleAttributes.ContainsKey("page-orientation")) { - PageOrientationValues orientation = Converter.ToPageOrientation(attr); + PageOrientationValues orientation = PageOrientationValues.Portrait; + if (styleAttributes.HasKeyEqualsTo("page-orientation", "landscape")) + orientation = PageOrientationValues.Landscape; var sectionProperties = mainPart.Document.Body!.GetFirstChild(); if (sectionProperties == null || sectionProperties.GetFirstChild() == null) diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs index 2d071b4..548ed09 100644 --- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs +++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs @@ -214,7 +214,7 @@ private static string GetListName(IElement listNode, string? parentName = null) { var styleAttributes = listNode.GetStyles(); bool orderedList = listNode.NodeName.Equals("ol", StringComparison.OrdinalIgnoreCase); - string? type = styleAttributes["list-style-type"]; + string? type = styleAttributes["list-style-type"].ToString(); if(orderedList && string.IsNullOrEmpty(type)) { diff --git a/src/Html2OpenXml/Expressions/Table/TableCaptionExpression.cs b/src/Html2OpenXml/Expressions/Table/TableCaptionExpression.cs index 8a05e34..509be4c 100644 --- a/src/Html2OpenXml/Expressions/Table/TableCaptionExpression.cs +++ b/src/Html2OpenXml/Expressions/Table/TableCaptionExpression.cs @@ -54,8 +54,9 @@ public override IEnumerable Interpret (ParsingContext context) } p.Append(childElements); - string? att = styleAttributes!["text-align"] ?? node.GetAttribute("align"); - if (!string.IsNullOrEmpty(att)) + var att = styleAttributes["text-align"]; + if (att.IsEmpty) att = node.GetAttribute("align").AsSpan(); + if (!att.IsEmpty) { JustificationValues? align = Converter.ToParagraphAlign(att); if (align.HasValue) diff --git a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs index 6a4f94c..05c471f 100644 --- a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs +++ b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs @@ -62,7 +62,7 @@ protected override void ComposeStyles(ParsingContext context) { base.ComposeStyles(context); - Unit width = styleAttributes!.GetUnit("width"); + Unit width = styleAttributes.GetUnit("width"); if (!width.IsValid) { var widthValue = cellNode.GetAttribute("width"); @@ -84,23 +84,22 @@ protected override void ComposeStyles(ParsingContext context) } // Manage vertical text (only for table cell) - string? direction = styleAttributes!["writing-mode"]; - if (direction != null) + var direction = styleAttributes["writing-mode"]; + if (!direction.IsEmpty) { - switch (direction) + if (direction.Equals("tb-lr".AsSpan(), StringComparison.InvariantCultureIgnoreCase) || + direction.Equals("vertical-lr".AsSpan(), StringComparison.InvariantCultureIgnoreCase)) { - case "tb-lr": - case "vertical-lr": - cellProperties.TextDirection = new() { Val = TextDirectionValues.BottomToTopLeftToRight }; - cellProperties.TableCellVerticalAlignment = new() { Val = TableVerticalAlignmentValues.Center }; - paraProperties.Justification = new() { Val = JustificationValues.Center }; - break; - case "tb-rl": - case "vertical-rl": - cellProperties.TextDirection = new() { Val = TextDirectionValues.TopToBottomRightToLeft }; - cellProperties.TableCellVerticalAlignment = new() { Val = TableVerticalAlignmentValues.Center }; - paraProperties.Justification = new() { Val = JustificationValues.Center }; - break; + cellProperties.TextDirection = new() { Val = TextDirectionValues.BottomToTopLeftToRight }; + cellProperties.TableCellVerticalAlignment = new() { Val = TableVerticalAlignmentValues.Center }; + paraProperties.Justification = new() { Val = JustificationValues.Center }; + } + else if (direction.Equals("tb-rl".AsSpan(), StringComparison.InvariantCultureIgnoreCase) || + direction.Equals("vertical-rl".AsSpan(), StringComparison.InvariantCultureIgnoreCase)) + { + cellProperties.TextDirection = new() { Val = TextDirectionValues.TopToBottomRightToLeft }; + cellProperties.TableCellVerticalAlignment = new() { Val = TableVerticalAlignmentValues.Center }; + paraProperties.Justification = new() { Val = JustificationValues.Center }; } } } diff --git a/src/Html2OpenXml/Expressions/Table/TableColExpression.cs b/src/Html2OpenXml/Expressions/Table/TableColExpression.cs index c51269d..6edf0d1 100644 --- a/src/Html2OpenXml/Expressions/Table/TableColExpression.cs +++ b/src/Html2OpenXml/Expressions/Table/TableColExpression.cs @@ -35,7 +35,7 @@ public override IEnumerable Interpret(ParsingContext context) ComposeStyles(context); var column = new GridColumn(); - var width = styleAttributes!.GetUnit("width"); + var width = styleAttributes.GetUnit("width"); if (width.IsValid) { if (width.IsFixed) diff --git a/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs b/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs index 27bef6f..f7614ce 100644 --- a/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs +++ b/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs @@ -71,8 +71,8 @@ protected override void ComposeStyles(ParsingContext context) { base.ComposeStyles(context); - var valign = Converter.ToVAlign(styleAttributes!["vertical-align"]); - if (!valign.HasValue) valign = Converter.ToVAlign(node.GetAttribute("valign")); + var valign = Converter.ToVAlign(styleAttributes["vertical-align"]); + if (!valign.HasValue) valign = Converter.ToVAlign(node.GetAttribute("valign").AsSpan()); if (!valign.HasValue) { // in Html, table cell are vertically centered by default @@ -92,7 +92,7 @@ protected override void ComposeStyles(ParsingContext context) } var halign = Converter.ToParagraphAlign(styleAttributes["text-align"]); - if (!halign.HasValue) halign = Converter.ToParagraphAlign(node.GetAttribute("align")); + if (!halign.HasValue) halign = Converter.ToParagraphAlign(node.GetAttribute("align").AsSpan()); if (halign.HasValue) { paraProperties.KeepNext = new(); diff --git a/src/Html2OpenXml/Expressions/Table/TableExpression.cs b/src/Html2OpenXml/Expressions/Table/TableExpression.cs index dd0e54d..abcf010 100644 --- a/src/Html2OpenXml/Expressions/Table/TableExpression.cs +++ b/src/Html2OpenXml/Expressions/Table/TableExpression.cs @@ -278,7 +278,7 @@ protected override void ComposeStyles (ParsingContext context) } } - var align = Converter.ToParagraphAlign(tableNode.GetAttribute("align")) + var align = Converter.ToParagraphAlign(tableNode.GetAttribute("align").AsSpan()) ?? Converter.ToParagraphAlign(styleAttributes["justify-self"]); if (!align.HasValue) { diff --git a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs index 25c7ce1..8b1223b 100644 --- a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs +++ b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs @@ -101,7 +101,7 @@ protected override void ComposeStyles(ParsingContext context) { base.ComposeStyles(context); - Unit unit = styleAttributes!.GetUnit("height", UnitMetric.Pixel); + Unit unit = styleAttributes.GetUnit("height", UnitMetric.Pixel); if (!unit.IsValid) unit = Unit.Parse(rowNode.GetAttribute("height").AsSpan(), UnitMetric.Pixel); switch (unit.Metric) diff --git a/src/Html2OpenXml/Primitives/HtmlColor.Named.cs b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs index b830189..400463d 100755 --- a/src/Html2OpenXml/Primitives/HtmlColor.Named.cs +++ b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs @@ -15,7 +15,6 @@ private static HtmlColor GetNamedColor (ReadOnlySpan name) { // the longest built-in Color's name is much lower than this check, so we should not allocate here in a typical usage Span loweredValue = name.Length <= 128 ? stackalloc char[name.Length] : new char[name.Length]; - name.ToLowerInvariant(loweredValue); namedColors.TryGetValue(loweredValue.ToString(), out var color); diff --git a/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs b/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs index 4caed8b..31763e8 100644 --- a/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs +++ b/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs @@ -156,9 +156,11 @@ public static string CollapseLineBreaks(this string str) /// Determines whether the layout mode is inline vs block or flex. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsInlineLayout(string? displayMode, string defaultLayout) + public static bool IsInlineLayout(ReadOnlySpan displayMode, string defaultLayout) { - return (displayMode ?? defaultLayout) - .StartsWith("inline", StringComparison.OrdinalIgnoreCase) == true; + if (displayMode.IsEmpty) + displayMode = defaultLayout.AsSpan(); + + return displayMode.StartsWith("inline".AsSpan(), StringComparison.InvariantCultureIgnoreCase); } } \ No newline at end of file diff --git a/src/Html2OpenXml/Utilities/Converter.cs b/src/Html2OpenXml/Utilities/Converter.cs index 9d5907d..4d2290c 100755 --- a/src/Html2OpenXml/Utilities/Converter.cs +++ b/src/Html2OpenXml/Utilities/Converter.cs @@ -22,12 +22,13 @@ static class Converter /// /// Convert the Html text align attribute (horizontal alignement) to its corresponding OpenXml value. /// - public static JustificationValues? ToParagraphAlign(string? htmlAlign) + public static JustificationValues? ToParagraphAlign(ReadOnlySpan span) { - if (htmlAlign == null) return null; - return htmlAlign.ToLowerInvariant() switch + Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length]; + span.ToLowerInvariant(loweredValue); + return loweredValue switch { - "left" => JustificationValues.Left, + "left" => JustificationValues.Left, "right" => JustificationValues.Right, "center" => JustificationValues.Center, "justify" => JustificationValues.Both, @@ -38,10 +39,11 @@ static class Converter /// /// Convert the Html vertical-align attribute to its corresponding OpenXml value. /// - public static TableVerticalAlignmentValues? ToVAlign(string? htmlAlign) + public static TableVerticalAlignmentValues? ToVAlign(ReadOnlySpan span) { - if (htmlAlign == null) return null; - return htmlAlign.ToLowerInvariant() switch + Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length]; + span.ToLowerInvariant(loweredValue); + return loweredValue switch { "top" => TableVerticalAlignmentValues.Top, "middle" => TableVerticalAlignmentValues.Center, @@ -159,14 +161,6 @@ public static BorderValues ToBorderStyle(ReadOnlySpan span) }; } - public static PageOrientationValues ToPageOrientation(string? orientation) - { - if ( "landscape".Equals(orientation,StringComparison.OrdinalIgnoreCase)) - return PageOrientationValues.Landscape; - - return PageOrientationValues.Portrait; - } - public static ICollection ToTextDecoration(ReadOnlySpan values) { // this style could take multiple values separated by a space diff --git a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs index 73f02e8..ebdce13 100644 --- a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs +++ b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs @@ -9,21 +9,22 @@ namespace HtmlToOpenXml.Tests.Primitives public class StyleParserTests { [TestCase("text-decoration:underline; color: red ")] - [TestCase("text-decoration:underline;color:red")] + [TestCase("text-decoration : underline ;color :red")] public void ParseStyle_ShouldSucceed(string htmlStyle) { var styles = HtmlAttributeCollection.ParseStyle(htmlStyle); Assert.Multiple(() => { - Assert.That(styles["text-decoration"], Is.EqualTo("underline")); - Assert.That(styles["color"], Is.EqualTo("red")); + Assert.That(styles.HasKeyEqualsTo("text-decoration", "underline"), Is.True); + Assert.That(styles.HasKeyEqualsTo("color", "red"), Is.True); + Assert.That(styles["color"].ToString(), Is.EqualTo("red")); }); } [Test(Description = "Parser should consider the last occurence of a style")] public void DuplicateStyle_ReturnsLatter() { - var styleAttributes = HtmlAttributeCollection.ParseStyle("color:red;color:blue"); - Assert.That(styleAttributes["color"], Is.EqualTo("blue")); + var styleAttributes = HtmlAttributeCollection.ParseStyle("color:red;color:BLUE"); + Assert.That(styleAttributes.HasKeyEqualsTo("color", "blue"), Is.True); } [TestCase("color;color;")] @@ -33,7 +34,7 @@ public void InvalidStyle_ShouldBeEmpty(string htmlStyle) { var styles = HtmlAttributeCollection.ParseStyle(htmlStyle); Assert.That(styles.IsEmpty, Is.True); - Assert.That(styles["color"], Is.Null); + Assert.That(styles.ContainsKey("color"), Is.False); } [Test] diff --git a/test/HtmlToOpenXml.Tests/StyleTests.cs b/test/HtmlToOpenXml.Tests/StyleTests.cs index 855b071..61b1d0f 100644 --- a/test/HtmlToOpenXml.Tests/StyleTests.cs +++ b/test/HtmlToOpenXml.Tests/StyleTests.cs @@ -163,23 +163,23 @@ public void ManualAddStyle_ThenRefreshStyles_ShouldSucceed() public void DuplicateStyle_ReturnsLatter() { var styleAttributes = HtmlAttributeCollection.ParseStyle("color:red;color:blue"); - Assert.That(styleAttributes["color"], Is.EqualTo("blue")); + Assert.That(styleAttributes["color"].ToString(), Is.EqualTo("blue")); } [Test(Description = "Encoded ':' and ';' characters are valid")] public void EncodedStyle_ShouldSucceed() { var styleAttributes = HtmlAttributeCollection.ParseStyle("text-decoration:underline;color:red"); - Assert.That(styleAttributes["text-decoration"], Is.EqualTo("underline")); - Assert.That(styleAttributes["color"], Is.EqualTo("red")); + Assert.That(styleAttributes["text-decoration"].ToString(), Is.EqualTo("underline")); + Assert.That(styleAttributes["color"].ToString(), Is.EqualTo("red")); } [Test(Description = "Key style with no value should be ignored")] public void EmptyStyle_ShouldBeIgnored() { var styleAttributes = HtmlAttributeCollection.ParseStyle("text-decoration;color:red"); - Assert.That(styleAttributes["text-decoration"], Is.Null); - Assert.That(styleAttributes["color"], Is.EqualTo("red")); + Assert.That(styleAttributes.ContainsKey("text-decoration"), Is.False); + Assert.That(styleAttributes["color"].ToString(), Is.EqualTo("red")); } } }