Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,37 @@ public static HtmlAttributeCollection ParseStyle(string? htmlStyles)
/// <summary>
/// Gets the named attribute.
/// </summary>
public string? this[string name]
public ReadOnlySpan<char> 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 [];
}
}

/// <summary>
/// Determines whether the collection contains the specified key.
/// </summary>
public bool ContainsKey(string name)
{
return attributes.ContainsKey(name);
}

/// <summary>
/// Efficient way to determine if a style is equals to a value.
/// </summary>
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;
}

/// <summary>
/// Gets an attribute representing a color (named color, hexadecimal or hexadecimal
/// without the preceding # character).
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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));

Expand Down
10 changes: 5 additions & 5 deletions src/Html2OpenXml/Expressions/BlockElementExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ protected override IEnumerable<OpenXmlElement> 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(
Expand All @@ -102,7 +102,7 @@ protected override IEnumerable<OpenXmlElement> 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 }));
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions src/Html2OpenXml/Expressions/BodyExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ protected override void ComposeStyles(ParsingContext context)

// Unsupported W3C attribute but claimed by users. Specified at <body> 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<SectionProperties>();
if (sectionProperties == null || sectionProperties.GetFirstChild<PageSize>() == null)
Expand Down
2 changes: 1 addition & 1 deletion src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down
5 changes: 3 additions & 2 deletions src/Html2OpenXml/Expressions/Table/TableCaptionExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ public override IEnumerable<OpenXmlElement> 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)
Expand Down
31 changes: 15 additions & 16 deletions src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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 };
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Html2OpenXml/Expressions/Table/TableColExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public override IEnumerable<OpenXmlElement> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/Html2OpenXml/Expressions/Table/TableExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/Html2OpenXml/Primitives/HtmlColor.Named.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ private static HtmlColor GetNamedColor (ReadOnlySpan<char> 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<char> loweredValue = name.Length <= 128 ? stackalloc char[name.Length] : new char[name.Length];

name.ToLowerInvariant(loweredValue);

namedColors.TryGetValue(loweredValue.ToString(), out var color);
Expand Down
8 changes: 5 additions & 3 deletions src/Html2OpenXml/Utilities/AngleSharpExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,11 @@ public static string CollapseLineBreaks(this string str)
/// Determines whether the layout mode is inline vs block or flex.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInlineLayout(string? displayMode, string defaultLayout)
public static bool IsInlineLayout(ReadOnlySpan<char> displayMode, string defaultLayout)
{
return (displayMode ?? defaultLayout)
.StartsWith("inline", StringComparison.OrdinalIgnoreCase) == true;
if (displayMode.IsEmpty)
displayMode = defaultLayout.AsSpan();

return displayMode.StartsWith("inline".AsSpan(), StringComparison.InvariantCultureIgnoreCase);
}
}
24 changes: 9 additions & 15 deletions src/Html2OpenXml/Utilities/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ static class Converter
/// <summary>
/// Convert the Html text align attribute (horizontal alignement) to its corresponding OpenXml value.
/// </summary>
public static JustificationValues? ToParagraphAlign(string? htmlAlign)
public static JustificationValues? ToParagraphAlign(ReadOnlySpan<char> span)
{
if (htmlAlign == null) return null;
return htmlAlign.ToLowerInvariant() switch
Span<char> 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,
Expand All @@ -38,10 +39,11 @@ static class Converter
/// <summary>
/// Convert the Html vertical-align attribute to its corresponding OpenXml value.
/// </summary>
public static TableVerticalAlignmentValues? ToVAlign(string? htmlAlign)
public static TableVerticalAlignmentValues? ToVAlign(ReadOnlySpan<char> span)
{
if (htmlAlign == null) return null;
return htmlAlign.ToLowerInvariant() switch
Span<char> loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
span.ToLowerInvariant(loweredValue);
return loweredValue switch
{
"top" => TableVerticalAlignmentValues.Top,
"middle" => TableVerticalAlignmentValues.Center,
Expand Down Expand Up @@ -159,14 +161,6 @@ public static BorderValues ToBorderStyle(ReadOnlySpan<char> span)
};
}

public static PageOrientationValues ToPageOrientation(string? orientation)
{
if ( "landscape".Equals(orientation,StringComparison.OrdinalIgnoreCase))
return PageOrientationValues.Landscape;

return PageOrientationValues.Portrait;
}

public static ICollection<TextDecoration> ToTextDecoration(ReadOnlySpan<char> values)
{
// this style could take multiple values separated by a space
Expand Down
13 changes: 7 additions & 6 deletions test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ namespace HtmlToOpenXml.Tests.Primitives
public class StyleParserTests
{
[TestCase("text-decoration:underline; color: red ")]
[TestCase("text-decoration&#58;underline&#59;color:red")]
[TestCase("text-decoration &#58; underline &#59;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;")]
Expand All @@ -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]
Expand Down
10 changes: 5 additions & 5 deletions test/HtmlToOpenXml.Tests/StyleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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&#58;underline&#59;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"));
}
}
}