();
\ No newline at end of file
diff --git a/examples/Benchmark/README.md b/examples/Benchmark/README.md
new file mode 100644
index 00000000..e1576beb
--- /dev/null
+++ b/examples/Benchmark/README.md
@@ -0,0 +1,7 @@
+# Benchmarks
+
+How to run the benchmark tool.
+First build the project: `dotnet build -c Release`
+
+Then run the performance test targeting multiple runtimes:
+`dotnet run -c Release -f net8.0 --runtimes net48 net8.0`
diff --git a/examples/Benchmark/ResourceHelper.cs b/examples/Benchmark/ResourceHelper.cs
new file mode 100644
index 00000000..c2b14629
--- /dev/null
+++ b/examples/Benchmark/ResourceHelper.cs
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2017 Deal Stream sàrl. All rights reserved
+ */
+using System.IO;
+using System.Reflection;
+using System.Resources;
+
+///
+/// Helper class to get an embedded resources.
+///
+public static class ResourceHelper
+{
+ public static string GetString(string resourceName)
+ {
+ return GetString(typeof(ResourceHelper).GetTypeInfo().Assembly, resourceName);
+ }
+
+ public static string GetString(Assembly assembly, string resourceName)
+ {
+ using (var stream = GetStream(assembly, resourceName))
+ {
+ using (var reader = new StreamReader(stream))
+ return reader.ReadToEnd();
+ }
+ }
+
+ public static Stream GetStream(string resourceName)
+ {
+ return GetStream(typeof(ResourceHelper).GetTypeInfo().Assembly, resourceName);
+ }
+
+ public static Stream GetStream(Assembly assembly, string resourceName)
+ {
+ var stream = assembly.GetManifestResourceStream(assembly.GetName().Name + "." + resourceName);
+ if (stream == null)
+ throw new MissingManifestResourceException($"Requested resource `{resourceName}` was not found in the assembly `{assembly}`.");
+
+ return stream;
+ }
+}
diff --git a/examples/Benchmark/benchmark.html b/examples/Benchmark/benchmark.html
new file mode 100644
index 00000000..de904e0c
--- /dev/null
+++ b/examples/Benchmark/benchmark.html
@@ -0,0 +1,124 @@
+
+
+
+
+ Sample HTML Page
+
+
+
+
+Welcome to My Sample Page
+
+
+
+ This is a sample paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+Visit Example
+
+
+
+
+
+
+ - Item 1
+ - Item 2
+ - Item 3
+
+
+
+
+
+ Header 1 |
+ Header 2 |
+
+
+ Cell 1 |
+ Cell 2 |
+
+
+ Cell 3 |
+ Cell 4 |
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+ Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.
+
+
+ Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam.
+
+
+ Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat.
+
+
+ Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.
+
+
+ Fusce convallis, mauris imperdiet gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum ligula sed magna. Maecenas nisl est, ultrices nec, congue eget, auctor vitae, massa.
+
+
+ Fusce luctus vestibulum augue ut aliquet. Nunc sagittis dictum nisi. Sed id blandit purus. Proin quis orci. Quisque convallis libero in sapien pharetra tincidunt.
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+ Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.
+
+
+ Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam.
+
+
+ Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat.
+
+
+ Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.
+
+
+ Fusce convallis, mauris imperdiet gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum ligula sed magna. Maecenas nisl est, ultrices nec, congue eget, auctor vitae, massa.
+
+
+ Fusce luctus vestibulum augue ut aliquet. Nunc sagittis dictum nisi. Sed id blandit purus. Proin quis orci. Quisque convallis libero in sapien pharetra tincidunt.
+
+
+
+
diff --git a/examples/Demo/Demo.csproj b/examples/Demo/Demo.csproj
index 3e76b9dc..851f0dab 100644
--- a/examples/Demo/Demo.csproj
+++ b/examples/Demo/Demo.csproj
@@ -3,6 +3,7 @@
net8.0
Exe
+ true
diff --git a/examples/Demo/Program.cs b/examples/Demo/Program.cs
index e794af42..3b2479e1 100644
--- a/examples/Demo/Program.cs
+++ b/examples/Demo/Program.cs
@@ -24,7 +24,7 @@ static async Task Main(string[] args)
// instead of creating it from scratch.
using (var buffer = ResourceHelper.GetStream("Resources.template.docx"))
{
- buffer.CopyTo(generatedDocument);
+ await buffer.CopyToAsync(generatedDocument);
}
generatedDocument.Position = 0L;
@@ -47,7 +47,7 @@ static async Task Main(string[] args)
AssertThatOpenXmlDocumentIsValid(package);
}
- File.WriteAllBytes(filename, generatedDocument.ToArray());
+ await File.WriteAllBytesAsync(filename, generatedDocument.ToArray());
}
Process.Start(new ProcessStartInfo(filename) { UseShellExecute = true });
diff --git a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
index 0c1c686e..2de8867e 100755
--- a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
+++ b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
@@ -9,8 +9,8 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
+using System;
using System.Collections.Generic;
-using System.Text.RegularExpressions;
using DocumentFormat.OpenXml.Wordprocessing;
namespace HtmlToOpenXml;
@@ -18,27 +18,90 @@ namespace HtmlToOpenXml;
///
/// Represents the collection of attributes present in the current html tag.
///
-sealed class HtmlAttributeCollection
+sealed partial class HtmlAttributeCollection
{
- private static readonly Regex stripStyleAttributesRegex = new(@"(?[^;\s]+)\s?(&\#58;|:)\s?(?[^;&]+)\s?(;|&\#59;)*");
- private readonly Dictionary attributes = [];
+ // Style key associated with a pointer to rawValue.
+ private readonly Dictionary attributes = [];
+ private readonly string rawValue;
-
- private HtmlAttributeCollection()
+ private HtmlAttributeCollection(string htmlStyles)
{
+ rawValue = htmlStyles;
}
- public static HtmlAttributeCollection ParseStyle(string? htmlTag)
+ ///
+ /// Gets a value that indicates whether this collection is empty.
+ ///
+ public bool IsEmpty => attributes.Count == 0;
+
+ public static HtmlAttributeCollection ParseStyle(string? htmlStyles)
{
- var collection = new HtmlAttributeCollection();
- if (string.IsNullOrEmpty(htmlTag)) return collection;
+ var collection = new HtmlAttributeCollection(htmlStyles!);
+ if (string.IsNullOrWhiteSpace(htmlStyles)) return collection;
+
+ var span = htmlStyles.AsSpan();
+ int startIndex = 0;
+ bool foundKey = false;
+ string? key = null;
+
+ while (span.Length > 0)
+ {
+ // Encoded ':' and ';' characters are valid for browser
+ //
+ int index = span.IndexOfAny(';', '&', ':');
+ if (index == -1)
+ {
+ if (foundKey)
+ {
+ // process the last value
+ collection.attributes[key!] = new Range(startIndex, startIndex + span.Length);
+ }
+ break;
+ }
- // Encoded ':' and ';' characters are valid for browser but not handled by the regex (bug #13812 reported by robin391)
- // ex=
- MatchCollection matches = stripStyleAttributesRegex.Matches(htmlTag);
- foreach (Match m in matches)
- collection.attributes[m.Groups["name"].Value] = m.Groups["val"].Value;
+ var separator = span[index];
+ if (separator == ';' && foundKey)
+ {
+ if (index > 0)
+ collection.attributes[key!] = new Range(startIndex, startIndex + index);
+ foundKey = false;
+ index++;
+ }
+ else if (separator == ';' && !foundKey)
+ {
+ // unexpected semicolon (ie, key with no value) -> ignore this style
+ index++;
+ }
+ else if (separator == ':' && !foundKey)
+ {
+ key = span.Slice(0, index).Trim().ToString();
+ foundKey = true;
+ index++;
+ }
+ // html-encoded semicolon
+ else if (foundKey && span.Slice(index).StartsWith(['&','#','5','9',';']))
+ {
+ if (index > 0)
+ collection.attributes[key!] = new Range(startIndex, startIndex + index);
+ foundKey = false;
+ index += 5; // length of ":"
+ }
+ else if (!foundKey && span.Slice(index).StartsWith(['&','#','5','8',';']))
+ {
+ key = span.Slice(0, index).Trim().ToString();
+ foundKey = true;
+ index += 5; // length of ":"
+ }
+ else
+ {
+ span = span.Slice(index + 1);
+ continue;
+ }
+
+ span = span.Slice(index);
+ startIndex += index;
+ }
return collection;
}
@@ -48,7 +111,12 @@ public static HtmlAttributeCollection ParseStyle(string? htmlTag)
///
public string? this[string name]
{
- get => attributes.TryGetValue(name, out var value)? value : null;
+ get
+ {
+ if (attributes.TryGetValue(name, out var range))
+ return rawValue.AsSpan().Slice(range).ToString().Trim();
+ return null;
+ }
}
///
@@ -57,7 +125,9 @@ public string? this[string name]
///
public HtmlColor GetColor(string name)
{
- return HtmlColor.Parse(this[name]);
+ if (attributes.TryGetValue(name, out var range))
+ return HtmlColor.Parse(rawValue.AsSpan().Slice(range));
+ return HtmlColor.Empty;
}
///
@@ -66,7 +136,9 @@ public HtmlColor GetColor(string name)
/// If the attribute is misformed, the property is set to false.
public Unit GetUnit(string name, UnitMetric defaultMetric = UnitMetric.Unitless)
{
- return Unit.Parse(this[name], defaultMetric);
+ if (attributes.TryGetValue(name, out var range))
+ return Unit.Parse(rawValue.AsSpan().Slice(range), defaultMetric);
+ return Unit.Empty;
}
///
@@ -76,7 +148,10 @@ public Unit GetUnit(string name, UnitMetric defaultMetric = UnitMetric.Unitless)
/// If the attribute is misformed, the property is set to false.
public Margin GetMargin(string name)
{
- Margin margin = Margin.Parse(this[name]);
+ Margin margin = Margin.Empty;
+ if (attributes.TryGetValue(name, out var range))
+ margin = Margin.Parse(rawValue.AsSpan().Slice(range));
+
Unit u;
u = GetUnit(name + "-top", UnitMetric.Pixel);
@@ -120,58 +195,83 @@ public HtmlBorder GetBorders()
/// If the attribute is misformed, the property is set to false.
public SideBorder GetSideBorder(string name)
{
- var attrValue = this[name];
- SideBorder border = SideBorder.Parse(attrValue);
+ SideBorder border = SideBorder.Empty;
+ if (attributes.TryGetValue(name, out Range range))
+ border = SideBorder.Parse(rawValue.AsSpan().Slice(range));
// handle attributes specified individually.
- Unit width = SideBorder.ParseWidth(this[name + "-width"]);
- if (!width.IsValid) width = border.Width;
+ Unit width = border.Width;
+ if (attributes.TryGetValue(name + "-width", out range))
+ {
+ var w = SideBorder.ParseWidth(rawValue.AsSpan().Slice(range));
+ if (width.IsValid) width = w;
+ }
var color = GetColor(name + "-color");
if (color.IsEmpty) color = border.Color;
- var style = Converter.ToBorderStyle(this[name + "-style"]);
- if (style == BorderValues.Nil) style = border.Style;
+ BorderValues style = border.Style;
+ if (attributes.TryGetValue(name + "-style", out range))
+ {
+ var s = Converter.ToBorderStyle(rawValue.AsSpan().Slice(range));
+ if (s != BorderValues.Nil) style = s;
+ }
return new SideBorder(style, color, width);
}
///
- /// Gets the font attribute and combine with the style, size and family.
+ /// Gets the `font` attribute and combine with the style, size and family.
///
public HtmlFont GetFont(string name)
{
- HtmlFont font = HtmlFont.Parse(this[name]);
+ HtmlFont font = HtmlFont.Empty;
+ if (attributes.TryGetValue(name, out Range range))
+ font = HtmlFont.Parse(rawValue.AsSpan().Slice(range));
+
FontStyle? fontStyle = font.Style;
FontVariant? variant = font.Variant;
FontWeight? weight = font.Weight;
Unit fontSize = font.Size;
string? family = font.Family;
- var attrValue = this[name + "-style"];
- if (attrValue != null)
+ if (attributes.TryGetValue(name + "-style", out range))
{
- fontStyle = Converter.ToFontStyle(attrValue) ?? font.Style;
+ var s = Converter.ToFontStyle(rawValue.AsSpan().Slice(range));
+ if (s.HasValue) fontStyle = s;
}
- attrValue = this[name + "-variant"];
- if (attrValue != null)
+
+ if (attributes.TryGetValue(name + "-variant", out range))
{
- variant = Converter.ToFontVariant(attrValue) ?? font.Variant;
+ var v = Converter.ToFontVariant(rawValue.AsSpan().Slice(range));
+ if (v.HasValue) variant = v;
}
- attrValue = this[name + "-weight"];
- if (attrValue != null)
+
+ if (attributes.TryGetValue(name + "-weight", out range))
{
- weight = Converter.ToFontWeight(attrValue) ?? font.Weight;
+ var w = Converter.ToFontWeight(rawValue.AsSpan().Slice(range));
+ if (w.HasValue) weight = w;
}
- attrValue = this[name + "-family"];
- if (attrValue != null)
+
+ if (attributes.TryGetValue(name + "-family", out range))
{
- family = Converter.ToFontFamily(attrValue) ?? font.Family;
+ var f = Converter.ToFontFamily(rawValue.AsSpan().Slice(range));
+ if (f != null) family = f;
}
Unit unit = this.GetUnit(name + "-size");
if (unit.IsValid) fontSize = unit;
- return new HtmlFont(fontStyle, variant, weight, fontSize, family);
+ return new HtmlFont(fontSize, family, fontStyle, variant, weight, Unit.Empty);
+ }
+
+ ///
+ /// Gets the composite `text-decoration` style.
+ ///
+ public IEnumerable GetTextDecorations(string name)
+ {
+ if (attributes.TryGetValue(name, out Range range))
+ return Converter.ToTextDecoration(rawValue.AsSpan().Slice(range));
+ return [];
}
}
diff --git a/src/Html2OpenXml/Expressions/AbbreviationExpression.cs b/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
index d0dcca86..69815322 100644
--- a/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
+++ b/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
@@ -25,6 +25,8 @@ namespace HtmlToOpenXml.Expressions;
///
sealed class AbbreviationExpression(IHtmlElement node) : PhrasingElementExpression(node)
{
+ private static readonly Regex linkRegex = new(@"^((https?|ftps?|mailto|file)://|[\\]{2})(?:[\w][\w.-]?)", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
+
///
public override IEnumerable Interpret(ParsingContext context)
@@ -132,8 +134,16 @@ public static long AddFootnoteReference(ParsingContext context, string descripti
// Description in footnote reference can be plain text or a web protocols/file share (like \\server01)
- Regex linkRegex = new(@"^((https?|ftps?|mailto|file)://|[\\]{2})(?:[\w][\w.-]?)");
- if (linkRegex.IsMatch(description) && Uri.TryCreate(description, UriKind.Absolute, out var uriReference))
+ bool isValidLink;
+ try
+ {
+ isValidLink = linkRegex.IsMatch(description);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ isValidLink = false;
+ }
+ if (isValidLink && Uri.TryCreate(description, UriKind.Absolute, out var uriReference))
{
// when URI references a network server (ex: \\server01), System.IO.Packaging is not resolving the correct URI and this leads
// to a bad-formed XML not recognized by Word. To enforce the "original URI", a fresh new instance must be created
diff --git a/src/Html2OpenXml/Expressions/BlockElementExpression.cs b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
index 62bc8ced..1e37542c 100644
--- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
@@ -243,7 +243,7 @@ protected override void ComposeStyles (ParsingContext context)
if (lineHeight.IsValid)
{
- if (lineHeight.Type == UnitMetric.Unitless)
+ if (lineHeight.Metric == UnitMetric.Unitless)
{
// auto should be considered as 240ths of a line
// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.spacingbetweenlines.line?view=openxml-3.0.1
@@ -252,7 +252,7 @@ protected override void ComposeStyles (ParsingContext context)
Line = Math.Round(lineHeight.Value * 240).ToString(CultureInfo.InvariantCulture)
};
}
- else if (lineHeight.Type == UnitMetric.Percent)
+ else if (lineHeight.Metric == UnitMetric.Percent)
{
// percentage depends on the font size which is hard to determine here
// let's rely this to "auto" behaviour
diff --git a/src/Html2OpenXml/Expressions/FontElementExpression.cs b/src/Html2OpenXml/Expressions/FontElementExpression.cs
index 88f47b8d..2fc8c285 100644
--- a/src/Html2OpenXml/Expressions/FontElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/FontElementExpression.cs
@@ -27,7 +27,7 @@ protected override void ComposeStyles(ParsingContext context)
string? attrValue = node.GetAttribute("size");
if (!string.IsNullOrEmpty(attrValue))
{
- Unit fontSize = Converter.ToFontSize(attrValue);
+ Unit fontSize = Converter.ToFontSize(attrValue.AsSpan());
if (fontSize.IsFixed)
runProperties.FontSize = new() {
Val = Math.Round(fontSize.ValueInPoint * 2).ToString(CultureInfo.InvariantCulture) };
diff --git a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
index 06fb089a..20b94031 100644
--- a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
@@ -81,8 +81,8 @@ private void ComposeStyles ()
// if the layout is not inline and both left and right are auto, image appears centered
// https://developer.mozilla.org/en-US/docs/Web/CSS/margin-left
var margin = styleAttributes.GetMargin("margin");
- if (margin.Left.Type == UnitMetric.Auto
- && margin.Right.Type == UnitMetric.Auto
+ if (margin.Left.Metric == UnitMetric.Auto
+ && margin.Right.Metric == UnitMetric.Auto
&& !AngleSharpExtensions.IsInlineLayout(styleAttributes["display"], "inline-block"))
{
paraProperties.Justification = new() { Val = JustificationValues.Center };
diff --git a/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs b/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
index 9a9f6ffd..a02e6533 100644
--- a/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
@@ -9,6 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -23,7 +24,7 @@ namespace HtmlToOpenXml.Expressions;
///
sealed class HeadingElementExpression(IHtmlElement node) : NumberingExpressionBase(node)
{
- private static readonly Regex numberingRegex = new(@"^\s*(\d+\.?)*\s*");
+ private static readonly Regex numberingRegex = new(@"^\s*(\d+\.?)*\s*", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
///
public override IEnumerable Interpret (ParsingContext context)
@@ -67,7 +68,15 @@ private static bool IsNumbering(OpenXmlElement runElement)
{
// Check if the line starts with a number format (1., 1.1., 1.1.1.)
// If it does, make sure we make the heading a numbered item
- Match regexMatch = numberingRegex.Match(runElement.InnerText ?? string.Empty);
+ Match regexMatch;
+ try
+ {
+ regexMatch = numberingRegex.Match(runElement.InnerText ?? string.Empty);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return false;
+ }
// Make sure we only grab the heading if it starts with a number
if (regexMatch.Groups.Count > 1 && regexMatch.Groups[1].Captures.Count > 0)
diff --git a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
index 1e0e1586..059c3e52 100644
--- a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
@@ -9,6 +9,9 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
+#if NET5_0_OR_GREATER
+using System.Collections.Frozen;
+#endif
using System.Collections.Generic;
using System.Linq;
using AngleSharp.Html.Dom;
@@ -26,7 +29,7 @@ abstract class NumberingExpressionBase(IHtmlElement node) : BlockElementExpressi
public const int MaxLevel = 8;
protected const int Indentation = 360;
public const string HeadingNumberingName = "decimal-heading-multi";
- private static readonly IDictionary predefinedNumberingLists = InitKnownLists();
+ private static readonly IReadOnlyDictionary predefinedNumberingLists = InitKnownLists();
/// Contains the list of templated list along with the AbstractNumbId
private Dictionary? knownAbsNumIds;
/// Contains the list of numbering instance.
@@ -218,7 +221,7 @@ private void InitNumberingIds(ParsingContext context)
///
/// Predefined template of lists.
///
- private static Dictionary InitKnownLists()
+ private static IReadOnlyDictionary InitKnownLists()
{
var knownAbstractNums = new Dictionary();
@@ -292,6 +295,10 @@ private static Dictionary InitKnownLists()
knownAbstractNums.Add(listName, abstractNum);
}
+#if NET5_0_OR_GREATER
+ return knownAbstractNums.ToFrozenDictionary();
+#else
return knownAbstractNums;
+#endif
}
}
\ No newline at end of file
diff --git a/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs b/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs
index c7619824..86646fd7 100644
--- a/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs
@@ -131,7 +131,7 @@ protected virtual void ComposeStyles (ParsingContext context)
runProperties.Shading = new Shading { Val = ShadingPatternValues.Clear, Fill = bgcolor.ToHexString() };
}
- foreach (var decoration in Converter.ToTextDecoration(styleAttributes["text-decoration"]))
+ foreach (var decoration in styleAttributes.GetTextDecorations("text-decoration"))
{
switch (decoration)
{
diff --git a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
index 4e816f89..815fbdaa 100644
--- a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
@@ -74,8 +74,8 @@ protected override void ComposeStyles(ParsingContext context)
{
cellProperties.TableCellWidth = new TableCellWidth
{
- Type = width.Type == UnitMetric.Percent ? TableWidthUnitValues.Pct : TableWidthUnitValues.Dxa,
- Width = width.Type == UnitMetric.Percent
+ Type = width.Metric == UnitMetric.Percent ? TableWidthUnitValues.Pct : TableWidthUnitValues.Dxa,
+ Width = width.Metric == UnitMetric.Percent
? ((int) (width.Value * 50)).ToString(CultureInfo.InvariantCulture)
: width.ValueInDxa.ToString(CultureInfo.InvariantCulture)
};
diff --git a/src/Html2OpenXml/Expressions/Table/TableExpression.cs b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
index 84c1d029..d77d7086 100644
--- a/src/Html2OpenXml/Expressions/Table/TableExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
@@ -172,7 +172,7 @@ protected override void ComposeStyles (ParsingContext context)
if (!width.IsValid) width = Unit.Parse(tableNode.GetAttribute("width"), UnitMetric.Pixel);
if (!width.IsValid) width = new Unit(UnitMetric.Percent, 100);
- switch (width.Type)
+ switch (width.Metric)
{
case UnitMetric.Percent:
tableProperties.TableWidth = new TableWidth
diff --git a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
index d0f04ff8..1a209dfc 100644
--- a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
@@ -105,7 +105,7 @@ protected override void ComposeStyles(ParsingContext context)
Unit unit = styleAttributes!.GetUnit("height", UnitMetric.Pixel);
if (!unit.IsValid) unit = Unit.Parse(rowNode.GetAttribute("height"), UnitMetric.Pixel);
- switch (unit.Type)
+ switch (unit.Metric)
{
case UnitMetric.Point:
rowProperties.AddChild(new TableRowHeight() { HeightType = HeightRuleValues.AtLeast, Val = (uint) (unit.Value * 20) });
diff --git a/src/Html2OpenXml/HtmlToOpenXml.csproj b/src/Html2OpenXml/HtmlToOpenXml.csproj
index bfffc341..ba846a90 100644
--- a/src/Html2OpenXml/HtmlToOpenXml.csproj
+++ b/src/Html2OpenXml/HtmlToOpenXml.csproj
@@ -9,13 +9,13 @@
HtmlToOpenXml
HtmlToOpenXml
HtmlToOpenXml.dll
- 3.2.2
+ 3.3.0
icon.png
Copyright 2009-$([System.DateTime]::Now.Year) Olivier Nizet
(Please write the package release notes in CHANGELOG.md)
README.md
office openxml netcore html
- 3.2.2
+ 3.3.0
MIT
https://github.com/onizet/html2openxml
https://github.com/onizet/html2openxml
@@ -44,7 +44,7 @@
-
+
@@ -67,8 +67,8 @@
-
-
+
+
@(ReleaseNoteLines, '%0a')
diff --git a/src/Html2OpenXml/IO/DataUri.cs b/src/Html2OpenXml/IO/DataUri.cs
index 918783fa..3128855b 100755
--- a/src/Html2OpenXml/IO/DataUri.cs
+++ b/src/Html2OpenXml/IO/DataUri.cs
@@ -23,7 +23,8 @@ public sealed class DataUri
{
private readonly static Regex dataUriRegex = new Regex(
@"data\:(?\w+/\w+)?(?:;charset=(?[a-zA-Z_0-9-]+))?(?;base64)?,(?.*)",
- RegexOptions.IgnoreCase | RegexOptions.Singleline);
+ RegexOptions.IgnoreCase | RegexOptions.Singleline,
+ TimeSpan.FromMilliseconds(200));
private DataUri(string mime, byte[] data)
{
@@ -50,12 +51,17 @@ public static bool TryCreate(string uri, out DataUri? result)
// while Internet Explorer requires that the charset's specification must precede the base64 token.
// http://en.wikipedia.org/wiki/Data_URI_scheme
- // We will stick for IE compliance for the moment...
-
- Match match = dataUriRegex.Match(uri);
+ Match match;
result = null;
-
- if (!match.Success) return false;
+ try
+ {
+ match = dataUriRegex.Match(uri);
+ if (!match.Success) return false;
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return false;
+ }
byte[] rawData;
string mime;
diff --git a/src/Html2OpenXml/Primitives/HtmlImageInfo.cs b/src/Html2OpenXml/IO/HtmlImageInfo.cs
similarity index 100%
rename from src/Html2OpenXml/Primitives/HtmlImageInfo.cs
rename to src/Html2OpenXml/IO/HtmlImageInfo.cs
diff --git a/src/Html2OpenXml/IO/ImageHeader.cs b/src/Html2OpenXml/IO/ImageHeader.cs
index 858a923d..c65a0b7a 100755
--- a/src/Html2OpenXml/IO/ImageHeader.cs
+++ b/src/Html2OpenXml/IO/ImageHeader.cs
@@ -46,6 +46,7 @@ public enum FileType { Unrecognized, Bitmap, Gif, Png, Jpeg, Emf, Xml }
{ Encoding.UTF8.GetBytes(" x.Length).First().Length;
diff --git a/src/Html2OpenXml/IO/ImagePrefetcher.cs b/src/Html2OpenXml/IO/ImagePrefetcher.cs
index 9ff6dfc3..f074358b 100644
--- a/src/Html2OpenXml/IO/ImagePrefetcher.cs
+++ b/src/Html2OpenXml/IO/ImagePrefetcher.cs
@@ -125,7 +125,7 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader)
Size originalSize;
using (var outputStream = ipart.GetStream(FileMode.Create))
{
- response.Content.CopyTo(outputStream);
+ await response.Content.CopyToAsync(outputStream);
outputStream.Seek(0L, SeekOrigin.Begin);
originalSize = GetImageSize(outputStream);
diff --git a/src/Html2OpenXml/Primitives/HtmlColor.Named.cs b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs
new file mode 100755
index 00000000..2fbec91c
--- /dev/null
+++ b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs
@@ -0,0 +1,180 @@
+using System;
+#if NET5_0_OR_GREATER
+using System.Collections.Frozen;
+#endif
+using System.Collections.Generic;
+
+namespace HtmlToOpenXml;
+
+///
+/// Helper class to translate a named color to its ARGB representation.
+///
+partial struct HtmlColor
+{
+ private static readonly IReadOnlyDictionary namedColors = InitKnownColors();
+
+ 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);
+ return color;
+ }
+
+ private static IReadOnlyDictionary InitKnownColors()
+ {
+ var colors = new Dictionary()
+ {
+ { "black", Black },
+ { "white", FromArgb(255,255,255) },
+ { "aliceblue", FromArgb(240, 248, 255) },
+ { "lightsalmon", FromArgb(255, 160, 122) },
+ { "antiquewhite", FromArgb(250, 235, 215) },
+ { "lightseagreen", FromArgb(32, 178, 170) },
+ { "aqua", FromArgb(0, 255, 255) },
+ { "lightskyblue", FromArgb(135, 206, 250) },
+ { "aquamarine", FromArgb(127, 255, 212) },
+ { "lightslategray", FromArgb(119, 136, 153) },
+ { "azure", FromArgb(240, 255, 255) },
+ { "lightsteelblue", FromArgb(176, 196, 222) },
+ { "beige", FromArgb(245, 245, 220) },
+ { "lightyellow", FromArgb(255, 255, 224) },
+ { "bisque", FromArgb(255, 228, 196) },
+ { "lime", FromArgb(0, 255, 0) },
+ { "limegreen", FromArgb(50, 205, 50) },
+ { "blanchedalmond", FromArgb(255, 255, 205) },
+ { "linen", FromArgb(250, 240, 230) },
+ { "blue", FromArgb(0, 0, 255) },
+ { "magenta", FromArgb(255, 0, 255) },
+ { "blueviolet", FromArgb(138, 43, 226) },
+ { "maroon", FromArgb(128, 0, 0) },
+ { "brown", FromArgb(165, 42, 42) },
+ { "mediumaquamarine", FromArgb(102, 205, 170) },
+ { "burlywood", FromArgb(222, 184, 135) },
+ { "mediumblue", FromArgb(0, 0, 205) },
+ { "cadetblue", FromArgb(95, 158, 160) },
+ { "mediumprchid", FromArgb(186, 85, 211) },
+ { "chartreuse", FromArgb(127, 255, 0) },
+ { "mediumpurple", FromArgb(147, 112, 219) },
+ { "chocolate", FromArgb(210, 105, 30) },
+ { "mediumseagreen", FromArgb(60, 179, 113) },
+ { "coral", FromArgb(255, 127, 80) },
+ { "mediumslateblue", FromArgb(123, 104, 238) },
+ { "cornflowerblue", FromArgb(100, 149, 237) },
+ { "mediumspringbreen", FromArgb(0, 250, 154) },
+ { "cornsilk", FromArgb(255, 248, 220) },
+ { "mediumturquoise", FromArgb(72, 209, 204) },
+ { "crimson", FromArgb(220, 20, 60) },
+ { "mediumvioletred", FromArgb(199, 21, 112) },
+ { "cyan", FromArgb(0, 255, 255) },
+ { "midnightblue", FromArgb(25, 25, 112) },
+ { "darkblue", FromArgb(0, 0, 139) },
+ { "mintcream", FromArgb(245, 255, 250) },
+ { "darkcyan", FromArgb(0, 139, 139) },
+ { "mistyrose", FromArgb(255, 228, 225) },
+ { "darkgoldenrod", FromArgb(184, 134, 11) },
+ { "moccasin", FromArgb(255, 228, 181) },
+ { "darkgray", FromArgb(169, 169, 169) },
+ { "navajowhite", FromArgb(255, 222, 173) },
+ { "darkgreen", FromArgb(0, 100, 0) },
+ { "navy", FromArgb(0, 0, 128) },
+ { "darkkhaki", FromArgb(189, 183, 107) },
+ { "oldlace", FromArgb(253, 245, 230) },
+ { "darkmagenta", FromArgb(139, 0, 139) },
+ { "olive", FromArgb(128, 128, 0) },
+ { "darkolivegreen", FromArgb(85, 107, 47) },
+ { "olivedrab", FromArgb(107, 142, 45) },
+ { "darkorange", FromArgb(255, 140, 0) },
+ { "orange", FromArgb(255, 165, 0) },
+ { "darkorchid", FromArgb(153, 50, 204) },
+ { "orangered", FromArgb(255, 69, 0) },
+ { "darkred", FromArgb(139, 0, 0) },
+ { "orchid", FromArgb(218, 112, 214) },
+ { "darksalmon", FromArgb(233, 150, 122) },
+ { "palegoldenrod", FromArgb(238, 232, 170) },
+ { "darkseagreen", FromArgb(143, 188, 143) },
+ { "palegreen", FromArgb(152, 251, 152) },
+ { "darkslateblue", FromArgb(72, 61, 139) },
+ { "paleturquoise", FromArgb(175, 238, 238) },
+ { "darkslategray", FromArgb(40, 79, 79) },
+ { "palevioletred", FromArgb(219, 112, 147) },
+ { "darkturquoise", FromArgb(0, 206, 209) },
+ { "papayawhip", FromArgb(255, 239, 213) },
+ { "darkviolet", FromArgb(148, 0, 211) },
+ { "peachpuff", FromArgb(255, 218, 155) },
+ { "deeppink", FromArgb(255, 20, 147) },
+ { "peru", FromArgb(205, 133, 63) },
+ { "deepskyblue", FromArgb(0, 191, 255) },
+ { "pink", FromArgb(255, 192, 203) },
+ { "dimgray", FromArgb(105, 105, 105) },
+ { "plum", FromArgb(221, 160, 221) },
+ { "dodgerblue", FromArgb(30, 144, 255) },
+ { "powderblue", FromArgb(176, 224, 230) },
+ { "firebrick", FromArgb(178, 34, 34) },
+ { "purple", FromArgb(128, 0, 128) },
+ { "floralwhite", FromArgb(255, 250, 240) },
+ { "red", FromArgb(255, 0, 0) },
+ { "forestgreen", FromArgb(34, 139, 34) },
+ { "rosybrown", FromArgb(188, 143, 143) },
+ { "fuschia", FromArgb(255, 0, 255) },
+ { "royalblue", FromArgb(65, 105, 225) },
+ { "gainsboro", FromArgb(220, 220, 220) },
+ { "saddlebrown", FromArgb(139, 69, 19) },
+ { "ghostwhite", FromArgb(248, 248, 255) },
+ { "salmon", FromArgb(250, 128, 114) },
+ { "gold", FromArgb(255, 215, 0) },
+ { "sandybrown", FromArgb(244, 164, 96) },
+ { "goldenrod", FromArgb(218, 165, 32) },
+ { "seagreen", FromArgb(46, 139, 87) },
+ { "gray", FromArgb(128, 128, 128) },
+ { "seashell", FromArgb(255, 245, 238) },
+ { "green", FromArgb(0, 128, 0) },
+ { "sienna", FromArgb(160, 82, 45) },
+ { "greenyellow", FromArgb(173, 255, 47) },
+ { "silver", FromArgb(192, 192, 192) },
+ { "honeydew", FromArgb(240, 255, 240) },
+ { "skyblue", FromArgb(135, 206, 235) },
+ { "hotpink", FromArgb(255, 105, 180) },
+ { "slateblue", FromArgb(106, 90, 205) },
+ { "indianred", FromArgb(205, 92, 92) },
+ { "slategray", FromArgb(112, 128, 144) },
+ { "indigo", FromArgb(75, 0, 130) },
+ { "snow", FromArgb(255, 250, 250) },
+ { "ivory", FromArgb(255, 240, 240) },
+ { "springgreen", FromArgb(0, 255, 127) },
+ { "khaki", FromArgb(240, 230, 140) },
+ { "steelblue", FromArgb(70, 130, 180) },
+ { "lavender", FromArgb(230, 230, 250) },
+ { "tan", FromArgb(210, 180, 140) },
+ { "lavenderblush", FromArgb(255, 240, 245) },
+ { "teal", FromArgb(0, 128, 128) },
+ { "lawngreen", FromArgb(124, 252, 0) },
+ { "thistle", FromArgb(216, 191, 216) },
+ { "lemonchiffon", FromArgb(255, 250, 205) },
+ { "tomato", FromArgb(253, 99, 71) },
+ { "lightblue", FromArgb(173, 216, 230) },
+ { "turquoise", FromArgb(64, 224, 208) },
+ { "lightcoral", FromArgb(240, 128, 128) },
+ { "violet", FromArgb(238, 130, 238) },
+ { "lightcyan", FromArgb(224, 255, 255) },
+ { "wheat", FromArgb(245, 222, 179) },
+ { "lightgoldenrodyellow", FromArgb(250, 250, 210) },
+ { "lightgreen", FromArgb(144, 238, 144) },
+ { "whitesmoke", FromArgb(245, 245, 245) },
+ { "lightgray", FromArgb(211, 211, 211) },
+ { "yellow", FromArgb(255, 255, 0) },
+ { "Lightpink", FromArgb(255, 182, 193) },
+ { "yellowgreen", FromArgb(154, 205, 50) },
+ { "transparent", FromArgb(0, 0, 0, 0) }
+ };
+
+#if NET5_0_OR_GREATER
+ return colors.ToFrozenDictionary();
+#else
+ return colors;
+#endif
+ }
+}
\ No newline at end of file
diff --git a/src/Html2OpenXml/Primitives/HtmlColor.cs b/src/Html2OpenXml/Primitives/HtmlColor.cs
index 799063a6..e668a6cf 100755
--- a/src/Html2OpenXml/Primitives/HtmlColor.cs
+++ b/src/Html2OpenXml/Primitives/HtmlColor.cs
@@ -17,7 +17,7 @@ namespace HtmlToOpenXml;
///
/// Represents an ARGB color.
///
-readonly struct HtmlColor : IEquatable
+readonly partial struct HtmlColor : IEquatable
{
private static readonly char[] hexDigits = {
'0', '1', '2', '3', '4', '5', '6', '7',
@@ -44,93 +44,112 @@ public HtmlColor(double alpha, byte red, byte green, byte blue) : this()
///
/// Try to parse a value (RGB(A) or HSL(A), hexadecimal, or named color) to its RGB representation.
///
- /// The color to parse.
+ /// The color to parse.
/// Returns if parsing failed.
- public static HtmlColor Parse(string? htmlColor)
+ public static HtmlColor Parse(ReadOnlySpan span)
{
- if (string.IsNullOrEmpty(htmlColor))
- return HtmlColor.Empty;
-
- // Bug fixed by jairoXXX to support rgb(r,g,b) format
- // RGB or RGBA
- try
+ span = span.Trim();
+
+ // Is it in hexa? Note: we no more accept hexa value without preceding the '#'
+ if (span[0] == '#')
{
- if (htmlColor!.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
+ if (span.Length == 7)
{
- int startIndex = htmlColor.IndexOf('(', 3), endIndex = htmlColor.LastIndexOf(')');
- if (startIndex >= 3 && endIndex > -1)
- {
- var colorStringArray = htmlColor.Substring(startIndex + 1, endIndex - startIndex - 1).Split(',');
- if (colorStringArray.Length < 3) return HtmlColor.Empty;
-
- return FromArgb(
- colorStringArray.Length == 3 ? 1.0: double.Parse(colorStringArray[3], CultureInfo.InvariantCulture),
- Byte.Parse(colorStringArray[0], NumberStyles.Integer, CultureInfo.InvariantCulture),
- Byte.Parse(colorStringArray[1], NumberStyles.Integer, CultureInfo.InvariantCulture),
- Byte.Parse(colorStringArray[2], NumberStyles.Integer, CultureInfo.InvariantCulture)
- );
- }
+ return FromArgb(
+ span.Slice(1, 2).AsByte(NumberStyles.HexNumber),
+ span.Slice(3, 2).AsByte(NumberStyles.HexNumber),
+ span.Slice(5, 2).AsByte(NumberStyles.HexNumber));
}
-
- // HSL or HSLA
- if (htmlColor.StartsWith("hsl", StringComparison.OrdinalIgnoreCase))
+ if (span.Length == 4)
{
- int startIndex = htmlColor.IndexOf('(', 3), endIndex = htmlColor.LastIndexOf(')');
- if (startIndex >= 3 && endIndex > -1)
- {
- var colorStringArray = htmlColor.Substring(startIndex + 1, endIndex - startIndex - 1).Split(',');
- if (colorStringArray.Length < 3) return HtmlColor.Empty;
-
- return FromHsl(
- colorStringArray.Length == 3 ? 1d: double.Parse(colorStringArray[3], CultureInfo.InvariantCulture),
- double.Parse(colorStringArray[0], CultureInfo.InvariantCulture),
- ParsePercent(colorStringArray[1]),
- ParsePercent(colorStringArray[2])
- );
- }
- }
-
- // Is it in hexa? Note: we no more accept hexa value without preceding the '#'
- if (htmlColor[0] == '#' && (htmlColor.Length == 7 || htmlColor.Length == 4))
- {
- if (htmlColor.Length == 7)
- {
- return FromArgb(
- Convert.ToByte(htmlColor.Substring(1, 2), 16),
- Convert.ToByte(htmlColor.Substring(3, 2), 16),
- Convert.ToByte(htmlColor.Substring(5, 2), 16));
- }
-
// #0FF --> #00FFFF
+ ReadOnlySpan r = [span[1], span[1]];
+ ReadOnlySpan g = [span[2], span[2]];
+ ReadOnlySpan b = [span[3], span[3]];
return FromArgb(
- Convert.ToByte(new string(htmlColor[1], 2), 16),
- Convert.ToByte(new string(htmlColor[2], 2), 16),
- Convert.ToByte(new string(htmlColor[3], 2), 16));
+ r.AsByte(NumberStyles.HexNumber),
+ g.AsByte(NumberStyles.HexNumber),
+ b.AsByte(NumberStyles.HexNumber));
}
+ return Empty;
}
- catch (Exception exc)
+
+ // RGB or RGBA
+ if (span.StartsWith(['r','g','b'], StringComparison.OrdinalIgnoreCase))
{
- if (exc is FormatException || exc is OverflowException || exc is ArgumentOutOfRangeException)
- return HtmlColor.Empty;
- throw;
+ int startIndex = span.IndexOf('('), endIndex = span.LastIndexOf(')');
+ if (startIndex < 3 || endIndex == -1)
+ return Empty;
+
+ span = span.Slice(startIndex + 1, endIndex - startIndex - 1);
+ Span tokens = stackalloc Range[5];
+ var sep = span.IndexOf(',') > -1? ',' : ' ';
+ return span.Split(tokens, sep, StringSplitOptions.RemoveEmptyEntries) switch
+ {
+ 3 => FromArgb(1.0,
+ span.Slice(tokens[0]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[1]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[2]).AsByte(NumberStyles.Integer)),
+ 4 => FromArgb(span.Slice(tokens[3]).AsDouble(),
+ span.Slice(tokens[0]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[1]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[2]).AsByte(NumberStyles.Integer)),
+ // r g b / a
+ 5 => FromArgb(span.Slice(tokens[4]).AsDouble(),
+ span.Slice(tokens[0]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[1]).AsByte(NumberStyles.Integer),
+ span.Slice(tokens[2]).AsByte(NumberStyles.Integer)),
+ _ => Empty
+ };
}
- return HtmlColorTranslator.FromHtml(htmlColor);
+ // HSL or HSLA
+ if (span.StartsWith(['h','s','l'], StringComparison.OrdinalIgnoreCase))
+ {
+ int startIndex = span.IndexOf('('), endIndex = span.LastIndexOf(')');
+ if (startIndex < 3 || endIndex == -1)
+ return Empty;
+
+ span = span.Slice(startIndex + 1, endIndex - startIndex - 1);
+ Span tokens = stackalloc Range[5];
+ var sep = span.IndexOf(',') > -1? ',' : ' ';
+ return span.Split(tokens, sep, StringSplitOptions.RemoveEmptyEntries) switch
+ {
+ 3 => FromHsl(1.0,
+ span.Slice(tokens[0]).AsDouble(),
+ span.Slice(tokens[1]).AsPercent(),
+ span.Slice(tokens[2]).AsPercent()),
+ 4 => FromHsl(span.Slice(tokens[3]).AsDouble(),
+ span.Slice(tokens[0]).AsDouble(),
+ span.Slice(tokens[1]).AsPercent(),
+ span.Slice(tokens[2]).AsPercent()),
+ _ => Empty
+ };
+ }
+
+ return GetNamedColor(span);
}
///
- /// Convert a potential percentage value to its numeric representation.
- /// Saturation and Lightness can contains both a percentage value or a value comprised between 0.0 and 1.0.
+ /// Try to parse a value (RGB(A) or HSL(A), hexadecimal, or named color) to its RGB representation.
///
- private static double ParsePercent (string value)
+ /// The color to parse.
+ /// Returns if parsing failed.
+ public static HtmlColor Parse(string? htmlColor)
{
- double parsedValue;
- if (value.IndexOf('%') > -1)
- parsedValue = double.Parse(value.Replace('%', ' '), CultureInfo.InvariantCulture) / 100d;
- else
- parsedValue = double.Parse(value, CultureInfo.InvariantCulture);
+ if (string.IsNullOrEmpty(htmlColor))
+ return Empty;
- return Math.Min(1, Math.Max(0, parsedValue));
+ try
+ {
+ return Parse(htmlColor.AsSpan());
+ }
+ catch (Exception exc)
+ {
+ if (exc is FormatException || exc is OverflowException || exc is ArgumentOutOfRangeException)
+ return Empty;
+ throw;
+ }
}
///
@@ -220,21 +239,15 @@ public static HtmlColor FromHsl(double alpha, double hue, double saturation, dou
byte iMid = Convert.ToByte(fMid * 255);
byte iMin = Convert.ToByte(fMin * 255);
- switch (iSextant)
+ return iSextant switch
{
- case 1:
- return FromArgb(alpha, iMid, iMax, iMin);
- case 2:
- return FromArgb(alpha, iMin, iMax, iMid);
- case 3:
- return FromArgb(alpha, iMin, iMid, iMax);
- case 4:
- return FromArgb(alpha, iMid, iMin, iMax);
- case 5:
- return FromArgb(alpha, iMax, iMin, iMid);
- default:
- return FromArgb(alpha, iMax, iMid, iMin);
- }
+ 1 => FromArgb(alpha, iMid, iMax, iMin),
+ 2 => FromArgb(alpha, iMin, iMax, iMid),
+ 3 => FromArgb(alpha, iMin, iMid, iMax),
+ 4 => FromArgb(alpha, iMid, iMin, iMax),
+ 5 => FromArgb(alpha, iMax, iMin, iMid),
+ _ => FromArgb(alpha, iMax, iMid, iMin),
+ };
}
///
diff --git a/src/Html2OpenXml/Primitives/HtmlFont.cs b/src/Html2OpenXml/Primitives/HtmlFont.cs
index 87cebc77..fd8c02de 100755
--- a/src/Html2OpenXml/Primitives/HtmlFont.cs
+++ b/src/Html2OpenXml/Primitives/HtmlFont.cs
@@ -16,7 +16,8 @@ namespace HtmlToOpenXml;
///
/// Represents a Html font (15px arial,sans-serif).
///
-readonly struct HtmlFont(FontStyle? style, FontVariant? variant, FontWeight? weight, Unit? size, string? family)
+readonly struct HtmlFont(Unit size, string? family, FontStyle? style,
+ FontVariant? variant, FontWeight? weight, Unit lineHeight)
{
/// Represents an empty font (not defined).
public static readonly HtmlFont Empty = new ();
@@ -25,68 +26,122 @@ readonly struct HtmlFont(FontStyle? style, FontVariant? variant, FontWeight? wei
private readonly FontVariant? variant = variant;
private readonly string? family = family;
private readonly FontWeight? weight = weight;
- private readonly Unit size = size ?? Unit.Empty;
+ private readonly Unit size = size;
+ private readonly Unit lineHeight = lineHeight;
+
+ ///
public static HtmlFont Parse(string? str)
{
- if (str == null) return HtmlFont.Empty;
+ if (str == null)
+ return Empty;
+ return Parse(str.AsSpan());
+ }
- // The font shorthand property sets all the font properties in one declaration.
- // The properties that can be set, are (in order):
- // "font-style font-variant font-weight font-size/line-height font-family"
- // The font-size and font-family values are required.
- // If one of the other values are missing, the default values will be inserted, if any.
+ ///
+ /// Parse the font style attribute.
+ ///
+ ///
+ /// The font shorthand property sets all the font properties in one declaration.
+ /// The properties that can be set, are (in order):
+ /// "font-style font-variant font-weight font-size/line-height font-family"
+ /// The font-size and font-family values are required.
+ /// If one of the other values are missing, the default values will be inserted, if any.
+ /// ///
+ public static HtmlFont Parse(ReadOnlySpan span)
+ {
// http://www.w3schools.com/cssref/pr_font_font.asp
- // in order to split by white spaces, we remove any white spaces between 2 family names (ex: Verdana, Arial -> Verdana,Arial)
- str = System.Text.RegularExpressions.Regex.Replace(str, @",\s+?", ",");
+ if (span.IsEmpty || span.Length < 2) return Empty;
+
+ Span tokens = stackalloc Range[6];
+ var tokenCount = span.SplitCompositeAttribute(tokens, ' ', skipSeparatorIfPrecededBy: ',');
+ if (tokenCount == 0)
+ return Empty;
- var fontParts = str.Split(HttpUtility.WhiteSpaces, StringSplitOptions.RemoveEmptyEntries);
- if (fontParts.Length < 2) return HtmlFont.Empty;
-
+ // Initialize default values
FontStyle? style = null;
FontVariant? variant = null;
FontWeight? weight = null;
// % and ratio font-size/line-height are not supported
- Unit fontSize;
- string? family;
+ Unit fontSize = Unit.Empty, lineHeight = Unit.Empty;
+ string? fontFamily = null;
- if (fontParts.Length == 2) // 2=the minimal set of required parameters
+ if (tokenCount == 2) // 2=the minimal set of required parameters
{
// should be the size and the family (in that order). Others are set to their default values
- fontSize = Converter.ToFontSize(fontParts[0]);
- if (!fontSize.IsValid) fontSize = Unit.Empty;
- family = Converter.ToFontFamily(fontParts[1]);
- return new HtmlFont(style, variant, weight, fontSize, family);
+ fontSize = Converter.ToFontSize(span.Slice(tokens[0]));
+ if (!fontSize.IsValid) return Empty;
+ fontFamily = Converter.ToFontFamily(span.Slice(tokens[1]));
+ return new HtmlFont(fontSize, fontFamily, style, variant, weight, lineHeight);
+ }
+ else if (tokenCount > 10)
+ {
+ // safety check to avoid overflow with stackalloc in a loop
+ return Empty;
}
- int index = 0;
-
- style = Converter.ToFontStyle(fontParts[index]);
- if (style.HasValue) { index++; }
-
- if (index + 2 > fontParts.Length) return HtmlFont.Empty;
- variant = Converter.ToFontVariant(fontParts[index]);
- if (variant.HasValue) { index++; }
-
- if (index + 2 > fontParts.Length) return HtmlFont.Empty;
- weight = Converter.ToFontWeight(fontParts[index]);
- if (weight.HasValue) { index++; }
+ Span loweredValue = stackalloc char[128];
+ for (int i = 0; i < tokenCount; i++)
+ {
+ var token = span.Slice(tokens[i]).Trim();
+ token.ToLowerInvariant(loweredValue);
+
+ switch (loweredValue.Slice(0, token.Length))
+ {
+ case "italic" or "oblique": style = FontStyle.Italic; break;
+ case "normal":
+ style ??= FontStyle.Normal;
+ variant ??= FontVariant.Normal;
+ weight ??= FontWeight.Normal;
+ break;
+ case "small-caps": variant = FontVariant.SmallCaps; break;
+ case "700" or "bold": weight = FontWeight.Bold; break;
+ case "bolder": weight = FontWeight.Bolder; break;
+ case "400": weight = FontWeight.Normal; break;
+ case "xx-small": fontSize = new Unit(UnitMetric.Point, 10); break;
+ case "x-small": fontSize = new Unit(UnitMetric.Point, 15); break;
+ case "small": fontSize = new Unit(UnitMetric.Point, 20); break;
+ case "medium": fontSize = new Unit(UnitMetric.Point, 27); break;
+ case "large": fontSize = new Unit(UnitMetric.Point, 36); break;
+ case "x-large": fontSize = new Unit(UnitMetric.Point, 48); break;
+ case "xx-large": fontSize = new Unit(UnitMetric.Point, 72); break;
+ default:
+ {
+ if (fontSize.IsValid || !TryParseFontSize (token, out fontSize, out lineHeight))
+ {
+ fontFamily ??= Converter.ToFontFamily(token);
+ }
+
+ break;
+ }
+ }
+ }
- if (fontParts.Length - index < 2) return HtmlFont.Empty;
- fontSize = Converter.ToFontSize(fontParts[fontParts.Length - 2]);
- if (!fontSize.IsValid) return HtmlFont.Empty;
+ return new HtmlFont(fontSize, fontFamily, style, variant, weight, lineHeight);
+ }
- family = Converter.ToFontFamily(fontParts[fontParts.Length - 1]);
+ private static bool TryParseFontSize(ReadOnlySpan token, out Unit fontSize, out Unit lineHeight)
+ {
+ // Handle font-size/line-height
+ var slash = token.IndexOf('/');
+ if (slash > 0)
+ {
+ fontSize = Unit.Parse(token.Slice(0, slash));
+ lineHeight = Unit.Parse(token.Slice(slash + 1));
+ return fontSize.IsValid;
+ }
- return new HtmlFont(style, variant, weight, fontSize, family);
+ fontSize = Unit.Parse(token);
+ lineHeight = Unit.Empty;
+ return fontSize.IsValid;
}
//____________________________________________________________________
//
///
- /// Gets or sets the name of this font.
+ /// Gets the name of this font.
///
public string? Family
{
@@ -94,7 +149,7 @@ public string? Family
}
///
- /// Gest or sets the style for the text.
+ /// Gest the style for the text.
///
public FontStyle? Style
{
@@ -102,7 +157,7 @@ public FontStyle? Style
}
///
- /// Gets or sets the variation of the characters.
+ /// Gets the variation of the characters.
///
public FontVariant? Variant
{
@@ -110,7 +165,7 @@ public FontVariant? Variant
}
///
- /// Gets or sets the size of the font, expressed in half points.
+ /// Gets the size of the font, expressed in half points.
///
public Unit Size
{
@@ -118,10 +173,18 @@ public Unit Size
}
///
- /// Gets or sets the weight of the characters (thin or thick).
+ /// Gets the weight of the characters (thin or thick).
///
public FontWeight? Weight
{
get { return weight; }
}
+
+ ///
+ /// Gets the height of a line.
+ ///
+ public Unit LineHeight
+ {
+ get { return lineHeight; }
+ }
}
diff --git a/src/Html2OpenXml/Primitives/Margin.cs b/src/Html2OpenXml/Primitives/Margin.cs
index c0c48075..8cd53161 100755
--- a/src/Html2OpenXml/Primitives/Margin.cs
+++ b/src/Html2OpenXml/Primitives/Margin.cs
@@ -10,21 +10,52 @@
* PARTICULAR PURPOSE.
*/
+using System;
+
namespace HtmlToOpenXml;
///
-/// Represents a Html Unit (ie: 120px, 10em, ...).
+/// Represents a Html Margin.
///
struct Margin
{
+ /// Represents an empty margin (not defined).
+ public static readonly Margin Empty = new() { sides = new Unit[4] };
private Unit[] sides;
+ /// Apply to all four sides.
+ public Margin(Unit all)
+ {
+ this.sides = [all, all, all, all];
+ }
+
+ /// Top and bottom | left and right.
+ public Margin(Unit topAndBottom, Unit leftAndRight)
+ {
+ this.sides = [topAndBottom, leftAndRight, topAndBottom, leftAndRight];
+ }
+
+ /// Top | left and right | bottom.
+ public Margin(Unit top, Unit leftAndRight, Unit bottom)
+ {
+ this.sides = [top, leftAndRight, bottom, leftAndRight];
+ }
+
+ /// Top | right | bottom | left.
public Margin(Unit top, Unit right, Unit bottom, Unit left)
{
this.sides = [top, right, bottom, left];
}
+ ///
+ public static Margin Parse(string? str)
+ {
+ if (str == null)
+ return Empty;
+ return Parse(str.AsSpan());
+ }
+
///
/// Parse the margin style attribute.
///
@@ -48,47 +79,29 @@ public Margin(Unit top, Unit right, Unit bottom, Unit left)
/// margin:25px;
/// all four margins are 25px
///
- public static Margin Parse(string? str)
+ public static Margin Parse(ReadOnlySpan span)
{
- if (str == null) return new Margin();
+ if (span.IsEmpty || span.IsWhiteSpace())
+ return Empty;
- var parts = str.Split(HttpUtility.WhiteSpaces);
- switch (parts.Length)
+ Span tokens = stackalloc Range[5];
+ return span.SplitCompositeAttribute(tokens) switch
{
- case 1:
- {
- Unit all = Unit.Parse(parts[0], UnitMetric.Pixel);
- return new Margin(all, all, all, all);
- }
- case 2:
- {
- Unit u1 = Unit.Parse(parts[0], UnitMetric.Pixel);
- Unit u2 = Unit.Parse(parts[1], UnitMetric.Pixel);
- return new Margin(u1, u2, u1, u2);
- }
- case 3:
- {
- Unit u1 = Unit.Parse(parts[0], UnitMetric.Pixel);
- Unit u2 = Unit.Parse(parts[1], UnitMetric.Pixel);
- Unit u3 = Unit.Parse(parts[2], UnitMetric.Pixel);
- return new Margin(u1, u2, u3, u2);
- }
- case 4:
- {
- Unit u1 = Unit.Parse(parts[0], UnitMetric.Pixel);
- Unit u2 = Unit.Parse(parts[1], UnitMetric.Pixel);
- Unit u3 = Unit.Parse(parts[2], UnitMetric.Pixel);
- Unit u4 = Unit.Parse(parts[3], UnitMetric.Pixel);
- return new Margin(u1, u2, u3, u4);
- }
- }
-
- return new Margin();
- }
-
- private void EnsureSides()
- {
- if (this.sides == null) sides = new Unit[4];
+ 1 => new Margin(Unit.Parse(span.Slice(tokens[0]), UnitMetric.Pixel)),
+ 2 => new Margin(
+ Unit.Parse(span.Slice(tokens[0]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[1]), UnitMetric.Pixel)),
+ 3 => new Margin(
+ Unit.Parse(span.Slice(tokens[0]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[1]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[2]), UnitMetric.Pixel)),
+ 4 => new Margin(
+ Unit.Parse(span.Slice(tokens[0]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[1]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[2]), UnitMetric.Pixel),
+ Unit.Parse(span.Slice(tokens[3]), UnitMetric.Pixel)),
+ _ => Empty
+ };
}
//____________________________________________________________________
@@ -99,8 +112,8 @@ private void EnsureSides()
///
public Unit Bottom
{
- readonly get { return sides == null ? Unit.Empty : sides[2]; }
- set { EnsureSides(); sides[2] = value; }
+ readonly get => sides[2];
+ set { sides[2] = value; }
}
///
@@ -108,8 +121,8 @@ public Unit Bottom
///
public Unit Left
{
- readonly get { return sides == null ? Unit.Empty : sides[3]; }
- set { EnsureSides(); sides[3] = value; }
+ readonly get => sides[3];
+ set { sides[3] = value; }
}
///
@@ -117,8 +130,8 @@ public Unit Left
///
public Unit Top
{
- readonly get { return sides == null ? Unit.Empty : sides[0]; }
- set { EnsureSides(); sides[0] = value; }
+ readonly get => sides[0];
+ set { sides[0] = value; }
}
///
@@ -126,13 +139,13 @@ public Unit Top
///
public Unit Right
{
- readonly get { return sides == null ? Unit.Empty : sides[1]; }
- set { EnsureSides(); sides[1] = value; }
+ readonly get => sides[1];
+ set { sides[1] = value; }
}
public readonly bool IsValid
{
- get => sides != null && Left.IsValid && Right.IsValid && Bottom.IsValid && Top.IsValid;
+ get => Left.IsValid && Right.IsValid && Bottom.IsValid && Top.IsValid;
}
///
@@ -140,6 +153,6 @@ public readonly bool IsValid
///
public readonly bool IsEmpty
{
- get => sides == null || !(Left.IsValid || Right.IsValid || Bottom.IsValid || Top.IsValid);
+ get => !(Left.IsValid || Right.IsValid || Bottom.IsValid || Top.IsValid);
}
}
diff --git a/src/Html2OpenXml/Primitives/SideBorder.cs b/src/Html2OpenXml/Primitives/SideBorder.cs
index 67c5598d..9b545466 100755
--- a/src/Html2OpenXml/Primitives/SideBorder.cs
+++ b/src/Html2OpenXml/Primitives/SideBorder.cs
@@ -11,13 +11,13 @@
*/
using System;
using System.Collections.Generic;
-using System.Text.RegularExpressions;
+using System.Linq;
using DocumentFormat.OpenXml.Wordprocessing;
namespace HtmlToOpenXml;
///
-/// Represents a Html Unit (ie: 120px, 10em, ...).
+/// Represents a Html border (ie: 1.2px solid blue...).
///
readonly struct SideBorder(BorderValues style, HtmlColor color, Unit size)
{
@@ -30,17 +30,24 @@ readonly struct SideBorder(BorderValues style, HtmlColor color, Unit size)
public static SideBorder Parse(string? str)
{
- if (str == null) return SideBorder.Empty;
+ if (str == null) return Empty;
+ return Parse(str.AsSpan());
+ }
+ public static SideBorder Parse(ReadOnlySpan span)
+ {
// The properties of a border that can be set, are (in order): border-width, border-style, and border-color.
// It does not matter if one of the values above are missing, e.g. border:solid #ff0000; is allowed.
// The main problem for parsing this attribute is that the browsers allow any permutation of the values... meaning more coding :(
// http://www.w3schools.com/cssref/pr_border.asp
- // Remove the spaces that could appear in the color parameter: rgb(233, 233, 233) -> rgb(233,233,233)
- str = Regex.Replace(str, @",\s+?", ",");
- var borderParts = new List(str.Split(HttpUtility.WhiteSpaces, StringSplitOptions.RemoveEmptyEntries));
- if (borderParts.Count == 0) return SideBorder.Empty;
+ if (span.Length < 2)
+ return Empty;
+
+ Span tokens = stackalloc Range[6];
+ var tokenCount = span.SplitCompositeAttribute(tokens);
+ if (tokenCount == 0)
+ return Empty;
// Initialize default values
Unit borderWidth = Unit.Empty;
@@ -48,34 +55,35 @@ public static SideBorder Parse(string? str)
BorderValues borderStyle = BorderValues.Nil;
// Now try to guess the values with their permutation
+ var tokenIndexes = new List(Enumerable.Range(0, tokenCount));
// handle border style
- for (int i = 0; i < borderParts.Count; i++)
+ for (int i = 0; i < tokenIndexes.Count; i++)
{
- borderStyle = Converter.ToBorderStyle(borderParts[i]);
+ borderStyle = Converter.ToBorderStyle(span.Slice(tokens[tokenIndexes[i]]));
if (borderStyle != BorderValues.Nil)
{
- borderParts.RemoveAt(i); // no need to process this part anymore
+ tokenIndexes.RemoveAt(i); // no need to process this part anymore
break;
}
}
- for (int i = 0; i < borderParts.Count; i++)
+ for (int i = 0; i < tokenIndexes.Count; i++)
{
- borderWidth = ParseWidth(borderParts[i]);
+ borderWidth = ParseWidth(span.Slice(tokens[tokenIndexes[i]]));
if (borderWidth.IsValid)
{
- borderParts.RemoveAt(i); // no need to process this part anymore
+ tokenIndexes.RemoveAt(i); // no need to process this part anymore
break;
}
}
// find width
- if(borderParts.Count > 0)
- borderColor = HtmlColor.Parse(borderParts[0]);
+ if(tokenIndexes.Count > 0)
+ borderColor = HtmlColor.Parse(span.Slice(tokens[tokenIndexes[0]]));
if (borderColor.IsEmpty && !borderWidth.IsValid && borderStyle == BorderValues.Nil)
- return SideBorder.Empty;
+ return Empty;
// returns the instance with default value if needed.
// These value are the ones used by the browser, i.e: solid 3px black
@@ -85,25 +93,27 @@ public static SideBorder Parse(string? str)
borderWidth.IsFixed? borderWidth : new Unit(UnitMetric.Pixel, 4));
}
- internal static Unit ParseWidth(string? borderWidth)
+ internal static Unit ParseWidth(ReadOnlySpan borderWidth)
{
Unit bu = Unit.Parse(borderWidth, UnitMetric.Pixel);
if (bu.IsValid)
{
- if (bu.Value > 0 && bu.Type == UnitMetric.Pixel)
+ if (bu.Value > 0 && bu.Metric == UnitMetric.Pixel)
return bu;
+ return Unit.Empty;
}
else
{
- switch (borderWidth)
- {
- case "thin": return new Unit(UnitMetric.Pixel, 1);
- case "medium": return new Unit(UnitMetric.Pixel, 3);
- case "thick": return new Unit(UnitMetric.Pixel, 5);
- }
+ Span loweredValue = borderWidth.Length <= 128 ? stackalloc char[borderWidth.Length] : new char[borderWidth.Length];
+ borderWidth.ToLowerInvariant(loweredValue);
+
+ return loweredValue switch {
+ "thin" => new Unit(UnitMetric.Pixel, 1),
+ "medium" => new Unit(UnitMetric.Pixel, 3),
+ "thick" => new Unit(UnitMetric.Pixel, 5),
+ _ => Unit.Empty,
+ };
}
-
- return Unit.Empty;
}
//____________________________________________________________________
diff --git a/src/Html2OpenXml/Primitives/Unit.cs b/src/Html2OpenXml/Primitives/Unit.cs
index 938df509..e54768bb 100755
--- a/src/Html2OpenXml/Primitives/Unit.cs
+++ b/src/Html2OpenXml/Primitives/Unit.cs
@@ -10,14 +10,13 @@
* PARTICULAR PURPOSE.
*/
using System;
-using System.Globalization;
namespace HtmlToOpenXml;
///
/// Represents a Html Unit (ie: 120px, 10em, ...).
///
-[System.Diagnostics.DebuggerDisplay("Unit: {Value} {Type}")]
+[System.Diagnostics.DebuggerDisplay("Unit: {Value} {Metric}")]
readonly struct Unit
{
/// Represents an empty unit (not defined).
@@ -25,85 +24,106 @@ readonly struct Unit
/// Represents an Auto unit.
public static readonly Unit Auto = new Unit(UnitMetric.Auto, 0L);
- private readonly UnitMetric type;
+ private readonly UnitMetric metric;
private readonly double value;
private readonly long valueInEmus;
- public Unit(UnitMetric type, double value)
+ public Unit(UnitMetric metric, double value)
{
- this.type = type;
+ this.metric = metric;
this.value = value;
- this.valueInEmus = ComputeInEmus(type, value);
+ this.valueInEmus = ComputeInEmus(metric, value);
}
- public static Unit Parse(string? str, UnitMetric defaultMetric = UnitMetric.Unitless)
+ public static Unit Parse(ReadOnlySpan span, UnitMetric defaultMetric = UnitMetric.Unitless)
{
- if (str == null) return Unit.Empty;
-
- str = str.Trim().ToLowerInvariant();
- int length = str.Length;
- int digitLength = -1;
- for (int i = 0; i < length; i++)
+ span = span.Trim();
+ if (span.Length <= 1)
{
- char ch = str[i];
- if ((ch < '0' || ch > '9') && ch != '-' && ch != '.' && ch != ',')
- break;
-
- digitLength = i;
+ // either this is invalid or this is a single digit
+ if (span.Length == 0 || !char.IsDigit(span[0])) return Empty;
+ return new Unit(defaultMetric, span[0] - '0');
}
- if (digitLength == -1)
+
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+
+ // guess the unit first than use the native Double parsing
+ UnitMetric metric;
+ int metricSize = 2;
+ if (span[span.Length - 1] == '%')
{
- // No digits in the width, we ignore this style
- return str == "auto"? Unit.Auto : Unit.Empty;
+ metric = UnitMetric.Percent;
+ metricSize = 1;
}
-
- UnitMetric type;
- if (digitLength < length - 1)
- type = Converter.ToUnitMetric(str.Substring(digitLength + 1).Trim());
else
- type = defaultMetric;
+ {
+ var metricSpan = loweredValue.Slice(loweredValue.Length - 2, 2);
+ metric = metricSpan switch {
+ "in" => UnitMetric.Inch,
+ "cm" => UnitMetric.Centimeter,
+ "mm" => UnitMetric.Millimeter,
+ "em" => UnitMetric.EM,
+ "ex" => UnitMetric.Ex,
+ "pt" => UnitMetric.Point,
+ "pc" => UnitMetric.Pica,
+ "px" => UnitMetric.Pixel,
+ _ => UnitMetric.Unknown,
+ };
+
+ // not recognised but maybe this is unitless (only digits)
+ if (metric == UnitMetric.Unknown && (char.IsDigit(metricSpan[0]) || metricSpan[0] == '.'))
+ {
+ metric = UnitMetric.Unitless;
+ metricSize = 0;
+ }
+ }
- string v = str.Substring(0, digitLength + 1);
double value;
try
{
- value = Convert.ToDouble(v, CultureInfo.InvariantCulture);
+ value = span.Slice(0, span.Length - metricSize).AsDouble();
if (value < short.MinValue || value > short.MaxValue)
- return Unit.Empty;
+ return Empty;
}
- catch (FormatException)
+ catch (Exception)
{
- return Unit.Empty;
- }
- catch (ArithmeticException)
- {
- return Unit.Empty;
+ // No digits, we ignore this style
+ return loweredValue is "auto"? Auto : Empty;
}
- return new Unit(type, value);
+ return new Unit(metric, value);
+ }
+
+ public static Unit Parse(string? str, UnitMetric defaultMetric = UnitMetric.Unitless)
+ {
+ if (string.IsNullOrWhiteSpace(str))
+ return Empty;
+
+ return Parse(str.AsSpan(), defaultMetric);
}
///
/// Gets the value expressed in the English Metrics Units.
///
- private static long ComputeInEmus(UnitMetric type, double value)
+ private static long ComputeInEmus(UnitMetric metric, double value)
{
/* Compute width and height in English Metrics Units.
- * There are 360000 EMUs per centimeter, 914400 EMUs per inch, 12700 EMUs per point
- * widthInEmus = widthInPixels / HorizontalResolutionInDPI * 914400
- * heightInEmus = heightInPixels / VerticalResolutionInDPI * 914400
- *
- * According to 1 px ~= 9525 EMU -> 914400 EMU per inch / 9525 EMU = 96 dpi
- * So Word use 96 DPI printing which seems fair.
- * http://hastobe.net/blogs/stevemorgan/archive/2008/09/15/howto-insert-an-image-into-a-word-document-and-display-it-using-openxml.aspx
- * http://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
- *
- * The list of units supported are explained here: http://www.w3schools.com/css/css_units.asp
- */
-
- switch (type)
+ * There are 360000 EMUs per centimeter, 914400 EMUs per inch, 12700 EMUs per point
+ * widthInEmus = widthInPixels / HorizontalResolutionInDPI * 914400
+ * heightInEmus = heightInPixels / VerticalResolutionInDPI * 914400
+ *
+ * According to 1 px ~= 9525 EMU -> 914400 EMU per inch / 9525 EMU = 96 dpi
+ * So Word use 96 DPI printing which seems fair.
+ * http://hastobe.net/blogs/stevemorgan/archive/2008/09/15/howto-insert-an-image-into-a-word-document-and-display-it-using-openxml.aspx
+ * http://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
+ *
+ * The list of units supported are explained here: http://www.w3schools.com/css/css_units.asp
+ */
+
+ switch (metric)
{
case UnitMetric.Auto:
case UnitMetric.Unitless:
@@ -130,15 +150,15 @@ private static long ComputeInEmus(UnitMetric type, double value)
///
/// Gets the type of unit (pixel, percent, point, ...)
///
- public UnitMetric Type
+ public UnitMetric Metric
{
- get { return type; }
+ get { return metric; }
}
///
/// Gets the value of this unit.
///
- public Double Value
+ public double Value
{
get { return value; }
}
@@ -146,7 +166,7 @@ public Double Value
///
/// Gets the value expressed in English Metrics Unit.
///
- public Int64 ValueInEmus
+ public long ValueInEmus
{
get { return valueInEmus; }
}
@@ -154,9 +174,9 @@ public Int64 ValueInEmus
///
/// Gets the value expressed in Dxa unit.
///
- public Int64 ValueInDxa
+ public long ValueInDxa
{
- get { return (long) (((double) valueInEmus / 914400L) * 20 * 72); }
+ get { return (long) ((double) valueInEmus / 914400L * 20 * 72); }
}
///
@@ -164,7 +184,7 @@ public Int64 ValueInDxa
///
public int ValueInPx
{
- get { return (int) (type == UnitMetric.Pixel ? this.value : (float) valueInEmus / 914400L * 96); }
+ get { return (int) (metric == UnitMetric.Pixel ? this.value : (float) valueInEmus / 914400L * 96); }
}
///
@@ -172,7 +192,7 @@ public int ValueInPx
///
public double ValueInPoint
{
- get { return (double) (type == UnitMetric.Point ? this.value : (float) valueInEmus / 12700L); }
+ get { return metric == UnitMetric.Point ? this.value : (float) valueInEmus / 12700L; }
}
///
@@ -190,7 +210,7 @@ public double ValueInEighthPoint
///
public bool IsValid
{
- get { return this.Type != UnitMetric.Unknown; }
+ get { return this.Metric != UnitMetric.Unknown; }
}
///
@@ -198,6 +218,6 @@ public bool IsValid
///
public bool IsFixed
{
- get { return IsValid && Type != UnitMetric.Percent && Type != UnitMetric.Auto; }
+ get { return IsValid && Metric != UnitMetric.Percent && Metric != UnitMetric.Auto; }
}
}
diff --git a/src/Html2OpenXml/Utilities/Converter.cs b/src/Html2OpenXml/Utilities/Converter.cs
index 9481fb52..66ef3deb 100755
--- a/src/Html2OpenXml/Utilities/Converter.cs
+++ b/src/Html2OpenXml/Utilities/Converter.cs
@@ -55,42 +55,44 @@ static class Converter
///
/// Convert Html regular font-size to OpenXml font value (expressed in point).
///
- public static Unit ToFontSize(string? htmlSize)
+ public static Unit ToFontSize(ReadOnlySpan span)
{
- if (htmlSize == null) return Unit.Empty;
- switch (htmlSize.ToLowerInvariant())
+ if (span.IsEmpty) return Unit.Empty;
+
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+ var unit = loweredValue switch
+ {
+ "1" or "xx-small" => new Unit(UnitMetric.Point, 10),
+ "2" or "x-small" => new Unit(UnitMetric.Point, 15),
+ "3" or "small" => new Unit(UnitMetric.Point, 20),
+ "4" or "medium" => new Unit(UnitMetric.Point, 27),
+ "5" or "large" => new Unit(UnitMetric.Point, 36),
+ "6" or "x-large" => new Unit(UnitMetric.Point, 48),
+ "7" or "xx-large" => new Unit(UnitMetric.Point, 72),
+ _ => Unit.Empty
+ };
+
+ if (!unit.IsValid)
{
- case "1":
- case "xx-small": return new Unit(UnitMetric.Point, 10);
- case "2":
- case "x-small": return new Unit(UnitMetric.Point, 15);
- case "3":
- case "small": return new Unit(UnitMetric.Point, 20);
- case "4":
- case "medium": return new Unit(UnitMetric.Point, 27);
- case "5":
- case "large": return new Unit(UnitMetric.Point, 36);
- case "6":
- case "x-large": return new Unit(UnitMetric.Point, 48);
- case "7":
- case "xx-large": return new Unit(UnitMetric.Point, 72);
- default:
- // the font-size is specified in positive half-points
- Unit unit = Unit.Parse(htmlSize, UnitMetric.Pixel);
- if (!unit.IsValid || unit.Value <= 0)
- return Unit.Empty;
-
- // this is a rough conversion to support some percent size, considering 100% = 11 pt
- if (unit.Type == UnitMetric.Percent) unit = new Unit(UnitMetric.Point, unit.Value * 0.11);
- return unit;
+ // the font-size is specified in positive half-points
+ unit = Unit.Parse(loweredValue, UnitMetric.Pixel);
+ if (!unit.IsValid || unit.Value <= 0)
+ return Unit.Empty;
+
+ // this is a rough conversion to support some percent size, considering 100% = 11 pt
+ if (unit.Metric == UnitMetric.Percent) unit = new Unit(UnitMetric.Point, unit.Value * 0.11);
}
+ return unit;
}
- public static FontVariant? ToFontVariant(string? html)
+ public static FontVariant? ToFontVariant(ReadOnlySpan span)
{
- if (html == null) return null;
+ if (span.IsEmpty) return null;
- return html.ToLowerInvariant() switch
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+ return loweredValue switch
{
"small-caps" => FontVariant.SmallCaps,
"normal" => FontVariant.Normal,
@@ -98,10 +100,13 @@ public static Unit ToFontSize(string? htmlSize)
};
}
- public static FontStyle? ToFontStyle(string? html)
+ public static FontStyle? ToFontStyle(ReadOnlySpan span)
{
- if (html == null) return null;
- return html.ToLowerInvariant() switch
+ if (span.IsEmpty) return null;
+
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+ return loweredValue switch
{
"italic" or "oblique" => FontStyle.Italic,
"normal" => FontStyle.Normal,
@@ -109,10 +114,13 @@ public static Unit ToFontSize(string? htmlSize)
};
}
- public static FontWeight? ToFontWeight(string? html)
+ public static FontWeight? ToFontWeight(ReadOnlySpan span)
{
- if (html == null) return null;
- return html.ToLowerInvariant() switch
+ if (span.IsEmpty) return null;
+
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+ return loweredValue switch
{
"700" or "bold" => FontWeight.Bold,
"bolder" => FontWeight.Bolder,
@@ -121,33 +129,26 @@ public static Unit ToFontSize(string? htmlSize)
};
}
- public static string? ToFontFamily(string? str)
+ public static string? ToFontFamily(ReadOnlySpan span)
{
- if (str == null) return null;
+ if (span.IsEmpty) return null;
- var names = str.Split(',' );
- for (int i = 0; i < names.Length; i++)
- {
- string fontName = names[i];
- if (fontName.Length == 0) continue;
- try
- {
- if (fontName[0] == '\'' && fontName[fontName.Length-1] == '\'') fontName = fontName.Substring(1, fontName.Length - 2);
- return fontName;
- }
- catch (ArgumentException)
- {
- // the name is not a TrueType font or is not a font installed on this computer
- }
- }
-
- return null;
+ // return the first font name
+ Span tokens = stackalloc Range[1];
+ return span.SplitCompositeAttribute(tokens, ',') switch {
+ 1 => span.Slice(tokens[0]).ToString(),
+ _ => null
+ };
}
- public static BorderValues ToBorderStyle(string? borderStyle)
+ public static BorderValues ToBorderStyle(ReadOnlySpan span)
{
- if (borderStyle == null) return BorderValues.Nil;
- return borderStyle.ToLowerInvariant() switch
+ if (span.IsEmpty)
+ return BorderValues.Nil;
+
+ Span loweredValue = span.Length <= 128 ? stackalloc char[span.Length] : new char[span.Length];
+ span.ToLowerInvariant(loweredValue);
+ return loweredValue switch
{
"dotted" => BorderValues.Dotted,
"dashed" => BorderValues.Dashed,
@@ -160,24 +161,6 @@ public static BorderValues ToBorderStyle(string? borderStyle)
};
}
- public static UnitMetric ToUnitMetric(string? type)
- {
- if (type == null) return UnitMetric.Unitless;
- return type.ToLowerInvariant() switch
- {
- "%" => UnitMetric.Percent,
- "in" => UnitMetric.Inch,
- "cm" => UnitMetric.Centimeter,
- "mm" => UnitMetric.Millimeter,
- "em" => UnitMetric.EM,
- "ex" => UnitMetric.Ex,
- "pt" => UnitMetric.Point,
- "pc" => UnitMetric.Pica,
- "px" => UnitMetric.Pixel,
- _ => UnitMetric.Unknown,
- };
- }
-
public static PageOrientationValues ToPageOrientation(string? orientation)
{
if ( "landscape".Equals(orientation,StringComparison.OrdinalIgnoreCase))
@@ -186,17 +169,23 @@ public static PageOrientationValues ToPageOrientation(string? orientation)
return PageOrientationValues.Portrait;
}
- public static IEnumerable ToTextDecoration(string? html)
+ public static ICollection ToTextDecoration(ReadOnlySpan values)
{
// this style could take multiple values separated by a space
// ex: text-decoration: blink underline;
var decorations = new List();
+ if (values.IsEmpty) return decorations;
+
+ Span loweredValue = values.Length <= 128 ? stackalloc char[values.Length] : new char[values.Length];
+ values.ToLowerInvariant(loweredValue);
- if (html == null) return decorations;
- foreach (string part in html.ToLowerInvariant().Split(' '))
+ Span tokens = stackalloc Range[5];
+ ReadOnlySpan span = loweredValue;
+ var tokenCount = span.Split(tokens, ' ', StringSplitOptions.RemoveEmptyEntries);
+ for (int i = 0; i < tokenCount; i++)
{
- switch (part)
+ switch (span.Slice(tokens[i]))
{
case "underline": decorations.Add(TextDecoration.Underline); break;
case "line-through": decorations.Add(TextDecoration.LineThrough); break;
diff --git a/src/Html2OpenXml/Utilities/HtmlColorTranslator.cs b/src/Html2OpenXml/Utilities/HtmlColorTranslator.cs
deleted file mode 100755
index ff59cb92..00000000
--- a/src/Html2OpenXml/Utilities/HtmlColorTranslator.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace HtmlToOpenXml;
-
-///
-/// Helper class to translate a named color to its ARGB representation.
-///
-static class HtmlColorTranslator
-{
- private static readonly Dictionary namedColors = InitKnownColors();
-
- public static HtmlColor FromHtml (string htmlColor)
- {
- namedColors.TryGetValue(htmlColor, out var color);
- return color;
- }
-
- private static Dictionary InitKnownColors()
- {
- var colors = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- { "Black", HtmlColor.Black },
- { "White", HtmlColor.FromArgb(255,255,255) },
- { "AliceBlue", HtmlColor.FromArgb(240, 248, 255) },
- { "LightSalmon", HtmlColor.FromArgb(255, 160, 122) },
- { "AntiqueWhite", HtmlColor.FromArgb(250, 235, 215) },
- { "LightSeaGreen", HtmlColor.FromArgb(32, 178, 170) },
- { "Aqua", HtmlColor.FromArgb(0, 255, 255) },
- { "LightSkyBlue", HtmlColor.FromArgb(135, 206, 250) },
- { "Aquamarine", HtmlColor.FromArgb(127, 255, 212) },
- { "LightSlateGray", HtmlColor.FromArgb(119, 136, 153) },
- { "Azure", HtmlColor.FromArgb(240, 255, 255) },
- { "LightSteelBlue", HtmlColor.FromArgb(176, 196, 222) },
- { "Beige", HtmlColor.FromArgb(245, 245, 220) },
- { "LightYellow", HtmlColor.FromArgb(255, 255, 224) },
- { "Bisque", HtmlColor.FromArgb(255, 228, 196) },
- { "Lime", HtmlColor.FromArgb(0, 255, 0) },
- { "LimeGreen", HtmlColor.FromArgb(50, 205, 50) },
- { "BlanchedAlmond", HtmlColor.FromArgb(255, 255, 205) },
- { "Linen", HtmlColor.FromArgb(250, 240, 230) },
- { "Blue", HtmlColor.FromArgb(0, 0, 255) },
- { "Magenta", HtmlColor.FromArgb(255, 0, 255) },
- { "BlueViolet", HtmlColor.FromArgb(138, 43, 226) },
- { "Maroon", HtmlColor.FromArgb(128, 0, 0) },
- { "Brown", HtmlColor.FromArgb(165, 42, 42) },
- { "MediumAquamarine", HtmlColor.FromArgb(102, 205, 170) },
- { "BurlyWood", HtmlColor.FromArgb(222, 184, 135) },
- { "MediumBlue", HtmlColor.FromArgb(0, 0, 205) },
- { "CadetBlue", HtmlColor.FromArgb(95, 158, 160) },
- { "MediumOrchid", HtmlColor.FromArgb(186, 85, 211) },
- { "Chartreuse", HtmlColor.FromArgb(127, 255, 0) },
- { "MediumPurple", HtmlColor.FromArgb(147, 112, 219) },
- { "Chocolate", HtmlColor.FromArgb(210, 105, 30) },
- { "MediumSeaGreen", HtmlColor.FromArgb(60, 179, 113) },
- { "Coral", HtmlColor.FromArgb(255, 127, 80) },
- { "MediumSlateBlue", HtmlColor.FromArgb(123, 104, 238) },
- { "CornflowerBlue", HtmlColor.FromArgb(100, 149, 237) },
- { "MediumSpringGreen", HtmlColor.FromArgb(0, 250, 154) },
- { "Cornsilk", HtmlColor.FromArgb(255, 248, 220) },
- { "MediumTurquoise", HtmlColor.FromArgb(72, 209, 204) },
- { "Crimson", HtmlColor.FromArgb(220, 20, 60) },
- { "MediumVioletRed", HtmlColor.FromArgb(199, 21, 112) },
- { "Cyan", HtmlColor.FromArgb(0, 255, 255) },
- { "MidnightBlue", HtmlColor.FromArgb(25, 25, 112) },
- { "DarkBlue", HtmlColor.FromArgb(0, 0, 139) },
- { "MintCream", HtmlColor.FromArgb(245, 255, 250) },
- { "DarkCyan", HtmlColor.FromArgb(0, 139, 139) },
- { "MistyRose", HtmlColor.FromArgb(255, 228, 225) },
- { "DarkGoldenrod", HtmlColor.FromArgb(184, 134, 11) },
- { "Moccasin", HtmlColor.FromArgb(255, 228, 181) },
- { "DarkGray", HtmlColor.FromArgb(169, 169, 169) },
- { "NavajoWhite", HtmlColor.FromArgb(255, 222, 173) },
- { "DarkGreen", HtmlColor.FromArgb(0, 100, 0) },
- { "Navy", HtmlColor.FromArgb(0, 0, 128) },
- { "DarkKhaki", HtmlColor.FromArgb(189, 183, 107) },
- { "OldLace", HtmlColor.FromArgb(253, 245, 230) },
- { "DarkMagenta", HtmlColor.FromArgb(139, 0, 139) },
- { "Olive", HtmlColor.FromArgb(128, 128, 0) },
- { "DarkOliveGreen", HtmlColor.FromArgb(85, 107, 47) },
- { "OliveDrab", HtmlColor.FromArgb(107, 142, 45) },
- { "DarkOrange", HtmlColor.FromArgb(255, 140, 0) },
- { "Orange", HtmlColor.FromArgb(255, 165, 0) },
- { "DarkOrchid", HtmlColor.FromArgb(153, 50, 204) },
- { "OrangeRed", HtmlColor.FromArgb(255, 69, 0) },
- { "DarkRed", HtmlColor.FromArgb(139, 0, 0) },
- { "Orchid", HtmlColor.FromArgb(218, 112, 214) },
- { "DarkSalmon", HtmlColor.FromArgb(233, 150, 122) },
- { "PaleGoldenrod", HtmlColor.FromArgb(238, 232, 170) },
- { "DarkSeaGreen", HtmlColor.FromArgb(143, 188, 143) },
- { "PaleGreen", HtmlColor.FromArgb(152, 251, 152) },
- { "DarkSlateBlue", HtmlColor.FromArgb(72, 61, 139) },
- { "PaleTurquoise", HtmlColor.FromArgb(175, 238, 238) },
- { "DarkSlateGray", HtmlColor.FromArgb(40, 79, 79) },
- { "PaleVioletRed", HtmlColor.FromArgb(219, 112, 147) },
- { "DarkTurquoise", HtmlColor.FromArgb(0, 206, 209) },
- { "PapayaWhip", HtmlColor.FromArgb(255, 239, 213) },
- { "DarkViolet", HtmlColor.FromArgb(148, 0, 211) },
- { "PeachPuff", HtmlColor.FromArgb(255, 218, 155) },
- { "DeepPink", HtmlColor.FromArgb(255, 20, 147) },
- { "Peru", HtmlColor.FromArgb(205, 133, 63) },
- { "DeepSkyBlue", HtmlColor.FromArgb(0, 191, 255) },
- { "Pink", HtmlColor.FromArgb(255, 192, 203) },
- { "DimGray", HtmlColor.FromArgb(105, 105, 105) },
- { "Plum", HtmlColor.FromArgb(221, 160, 221) },
- { "DodgerBlue", HtmlColor.FromArgb(30, 144, 255) },
- { "PowderBlue", HtmlColor.FromArgb(176, 224, 230) },
- { "Firebrick", HtmlColor.FromArgb(178, 34, 34) },
- { "Purple", HtmlColor.FromArgb(128, 0, 128) },
- { "FloralWhite", HtmlColor.FromArgb(255, 250, 240) },
- { "Red", HtmlColor.FromArgb(255, 0, 0) },
- { "ForestGreen", HtmlColor.FromArgb(34, 139, 34) },
- { "RosyBrown", HtmlColor.FromArgb(188, 143, 143) },
- { "Fuschia", HtmlColor.FromArgb(255, 0, 255) },
- { "RoyalBlue", HtmlColor.FromArgb(65, 105, 225) },
- { "Gainsboro", HtmlColor.FromArgb(220, 220, 220) },
- { "SaddleBrown", HtmlColor.FromArgb(139, 69, 19) },
- { "GhostWhite", HtmlColor.FromArgb(248, 248, 255) },
- { "Salmon", HtmlColor.FromArgb(250, 128, 114) },
- { "Gold", HtmlColor.FromArgb(255, 215, 0) },
- { "SandyBrown", HtmlColor.FromArgb(244, 164, 96) },
- { "Goldenrod", HtmlColor.FromArgb(218, 165, 32) },
- { "SeaGreen", HtmlColor.FromArgb(46, 139, 87) },
- { "Gray", HtmlColor.FromArgb(128, 128, 128) },
- { "Seashell", HtmlColor.FromArgb(255, 245, 238) },
- { "Green", HtmlColor.FromArgb(0, 128, 0) },
- { "Sienna", HtmlColor.FromArgb(160, 82, 45) },
- { "GreenYellow", HtmlColor.FromArgb(173, 255, 47) },
- { "Silver", HtmlColor.FromArgb(192, 192, 192) },
- { "Honeydew", HtmlColor.FromArgb(240, 255, 240) },
- { "SkyBlue", HtmlColor.FromArgb(135, 206, 235) },
- { "HotPink", HtmlColor.FromArgb(255, 105, 180) },
- { "SlateBlue", HtmlColor.FromArgb(106, 90, 205) },
- { "IndianRed", HtmlColor.FromArgb(205, 92, 92) },
- { "SlateGray", HtmlColor.FromArgb(112, 128, 144) },
- { "Indigo", HtmlColor.FromArgb(75, 0, 130) },
- { "Snow", HtmlColor.FromArgb(255, 250, 250) },
- { "Ivory", HtmlColor.FromArgb(255, 240, 240) },
- { "SpringGreen", HtmlColor.FromArgb(0, 255, 127) },
- { "Khaki", HtmlColor.FromArgb(240, 230, 140) },
- { "SteelBlue", HtmlColor.FromArgb(70, 130, 180) },
- { "Lavender", HtmlColor.FromArgb(230, 230, 250) },
- { "Tan", HtmlColor.FromArgb(210, 180, 140) },
- { "LavenderBlush", HtmlColor.FromArgb(255, 240, 245) },
- { "Teal", HtmlColor.FromArgb(0, 128, 128) },
- { "LawnGreen", HtmlColor.FromArgb(124, 252, 0) },
- { "Thistle", HtmlColor.FromArgb(216, 191, 216) },
- { "LemonChiffon", HtmlColor.FromArgb(255, 250, 205) },
- { "Tomato", HtmlColor.FromArgb(253, 99, 71) },
- { "LightBlue", HtmlColor.FromArgb(173, 216, 230) },
- { "Turquoise", HtmlColor.FromArgb(64, 224, 208) },
- { "LightCoral", HtmlColor.FromArgb(240, 128, 128) },
- { "Violet", HtmlColor.FromArgb(238, 130, 238) },
- { "LightCyan", HtmlColor.FromArgb(224, 255, 255) },
- { "Wheat", HtmlColor.FromArgb(245, 222, 179) },
- { "LightGoldenrodYellow", HtmlColor.FromArgb(250, 250, 210) },
- { "LightGreen", HtmlColor.FromArgb(144, 238, 144) },
- { "WhiteSmoke", HtmlColor.FromArgb(245, 245, 245) },
- { "LightGray", HtmlColor.FromArgb(211, 211, 211) },
- { "Yellow", HtmlColor.FromArgb(255, 255, 0) },
- { "LightPink", HtmlColor.FromArgb(255, 182, 193) },
- { "YellowGreen", HtmlColor.FromArgb(154, 205, 50) },
- { "Transparent", HtmlColor.FromArgb(0, 0, 0, 0) }
- };
-
- return colors;
- }
-}
\ No newline at end of file
diff --git a/src/Html2OpenXml/Utilities/Range.cs b/src/Html2OpenXml/Utilities/Range.cs
new file mode 100644
index 00000000..7d89ba4a
--- /dev/null
+++ b/src/Html2OpenXml/Utilities/Range.cs
@@ -0,0 +1,37 @@
+/* Copyright (C) Olivier Nizet https://github.com/onizet/html2openxml - All Rights Reserved
+ *
+ * This source is subject to the Microsoft Permissive License.
+ * Please see the License.txt file for more information.
+ * All other rights reserved.
+ *
+ * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
+ * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ */
+#if !NET5_0_OR_GREATER
+namespace System;
+
+using System.Runtime.CompilerServices;
+
+readonly struct Range(int start, int end)
+{
+ /// Represent the inclusive start index of the Range.
+ public int Start { get; } = start;
+
+ /// Represent the exclusive end index of the Range.
+ public int End { get; } = end;
+
+ /// Calculate the start offset and length of range object using a collection length.
+ ///
+ /// For performance reason, we don't validate the input length parameter against negative values.
+ /// It is expected Range will be used with collections which always have non negative length/count.
+ /// We validate the range is inside the length scope though.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public (int Offset, int Length) GetOffsetAndLength(int _)
+ {
+ return (Start, End - Start);
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Html2OpenXml/Utilities/SpanExtensions.cs b/src/Html2OpenXml/Utilities/SpanExtensions.cs
new file mode 100644
index 00000000..03a26d47
--- /dev/null
+++ b/src/Html2OpenXml/Utilities/SpanExtensions.cs
@@ -0,0 +1,222 @@
+/* Copyright (C) Olivier Nizet https://github.com/onizet/html2openxml - All Rights Reserved
+ *
+ * This source is subject to the Microsoft Permissive License.
+ * Please see the License.txt file for more information.
+ * All other rights reserved.
+ *
+ * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
+ * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
+ * PARTICULAR PURPOSE.
+ */
+using System;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+
+namespace HtmlToOpenXml;
+
+///
+/// Polyfill helper class to provide extension methods for .
+///
+static class SpanExtensions
+{
+ ///
+ /// Shim method to convert to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte AsByte(this ReadOnlySpan span, NumberStyles style)
+ {
+#if NET5_0_OR_GREATER
+ return byte.Parse(span, style);
+#else
+ return byte.Parse(span.ToString(), style);
+#endif
+ }
+
+ ///
+ /// Shim method to convert to .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double AsDouble(this ReadOnlySpan span)
+ {
+#if NET5_0_OR_GREATER
+ return double.Parse(span, CultureInfo.InvariantCulture);
+#else
+ return double.Parse(span.ToString(), CultureInfo.InvariantCulture);
+#endif
+ }
+
+ ///
+ /// Convert a potential percentage value to its numeric representation.
+ /// Saturation and Lightness can contains both a percentage value or a value comprised between 0.0 and 1.0.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static double AsPercent (this ReadOnlySpan span)
+ {
+ int index = span.IndexOf('%');
+ if (index > -1)
+ {
+ double parsedValue = span.Slice(0, index).AsDouble() / 100d;
+ return Math.Min(1, Math.Max(0, parsedValue));
+ }
+
+ return span.AsDouble();
+ }
+
+ ///
+ /// Shim method to remain compliant with pre-NET 8 framework.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ReadOnlySpan Slice(this ReadOnlySpan span, Range range)
+ {
+#if NET5_0_OR_GREATER
+ return span[range];
+#else
+ var (start, length) = range.GetOffsetAndLength(span.Length);
+ return span.Slice(start, length);
+#endif
+ }
+
+#if !NET5_0_OR_GREATER
+ ///
+ /// Parses the source for the specified ,
+ /// populating the span with instances
+ /// representing the regions between the separators.
+ ///
+ /// The source span to parse.
+ /// The destination span into which the resulting ranges are written.
+ /// A character that delimits the regions in this instance.
+ /// A bitwise combination of the enumeration values that specifies whether to trim whitespace and include empty ranges.
+ /// The number of ranges written into .
+ public static int Split(this ReadOnlySpan span, Span destination,
+ char separator, StringSplitOptions options = StringSplitOptions.None)
+ {
+ // If the destination is empty, there's nothing to do.
+ if (destination.IsEmpty)
+ return 0;
+
+ int matches = 0;
+ int startIndex = 0;
+ while (span.Length > 0)
+ {
+ int index = span.IndexOf(separator);
+ if (index == -1) index = span.Length;
+ if (options == StringSplitOptions.RemoveEmptyEntries && index == 0)
+ {
+ span = span.Slice(1);
+ startIndex++;
+ continue;
+ }
+
+ destination[matches] = new Range(startIndex, startIndex + index);
+ matches++;
+
+ if (matches >= destination.Length || span.Length <= index)
+ break;
+
+ // move to next token
+ span = span.Slice(index + 1);
+ startIndex += index + 1;
+ }
+
+ return matches;
+ }
+#endif
+
+ ///
+ /// Parses the source for the specified style attribute separators,
+ /// populating the span with instances
+ /// representing the regions between the separators.
+ ///
+ /// The source span to parse.
+ /// The destination span into which the resulting ranges are written.
+ /// A character that delimits the regions in this instance.
+ /// If is preceded by this character, the separator will be treated as a normal character.
+ /// The number of ranges written into .
+ public static int SplitCompositeAttribute(this ReadOnlySpan span, Span destination,
+ char separator = ' ', char? skipSeparatorIfPrecededBy = null)
+ {
+ // If the destination is empty, there's nothing to do.
+ if (destination.IsEmpty)
+ return 0;
+
+ int matches = 0, startIndex = 0, offsetIndex = 0;
+ bool isEscaping = false;
+ char endEscapingChar = '\0';
+ ReadOnlySpan searchValues = [separator, '(', '\'', '"'];
+
+ while (span.Length > 0)
+ {
+ bool isPositiveMatch = true;
+
+ // Remove the spaces that could appear inside a token.
+ // Eg: rgb(233, 233, 233) -> rgb(233,233,233)
+ int index = isEscaping?
+ span.IndexOf(endEscapingChar) :
+ span.IndexOfAny(searchValues);
+
+ if (index == -1)
+ {
+ // process the last match
+ destination[matches] = new Range(startIndex, startIndex + offsetIndex + span.Length);
+ matches++;
+ break;
+ }
+
+ // we find the beginning of an escaping sequence
+ var ch = span[index];
+ if (ch != separator && !isEscaping)
+ {
+ if (ch == '(')
+ {
+ endEscapingChar = ')';
+ offsetIndex += index + 1;
+ }
+ else
+ {
+ endEscapingChar = ch; // ' or "
+ if (index == 0) startIndex++; // exclude the quote from the captured range
+ }
+ isEscaping = true;
+ isPositiveMatch = false;
+ }
+ // end of escaping sequence
+ else if (ch == endEscapingChar)
+ {
+ if (ch == ')') index++; // include that closing parenthesis in the range
+ isEscaping = false;
+ }
+ // this is a separator but maybe we will need to skip it
+ // eg: "Arial, Verdana bold 1em" -> the space after the comma must be skipped
+ else if (ch == separator && index > 0 &&
+ skipSeparatorIfPrecededBy.HasValue && span[index -1] == skipSeparatorIfPrecededBy)
+ {
+ index++;
+ offsetIndex += index + 1;
+ isPositiveMatch = false;
+ }
+ else if (index == 0) // empty token
+ {
+ startIndex++;
+ isPositiveMatch = false;
+ }
+
+ // index > 0 to exclude empty entries
+ if (!isEscaping && index > 0 && isPositiveMatch)
+ {
+ destination[matches] = new Range(startIndex, startIndex + offsetIndex + index);
+ matches++;
+ startIndex += index + offsetIndex + 1;
+ offsetIndex = 0;
+ }
+
+ if (matches >= destination.Length || span.Length <= index)
+ break;
+
+ // move to next token
+ span = span.Slice(index + 1);
+ }
+
+ return matches;
+ }
+}
diff --git a/test/HtmlToOpenXml.Tests/HrTests.cs b/test/HtmlToOpenXml.Tests/HrTests.cs
index b7fdc1d5..e83d16b8 100644
--- a/test/HtmlToOpenXml.Tests/HrTests.cs
+++ b/test/HtmlToOpenXml.Tests/HrTests.cs
@@ -14,6 +14,7 @@ public class HrTests : HtmlConverterTestBase
public void Standalone_ReturnsWithNoSpacing ()
{
var elements = converter.Parse("
");
+ TestContext.Out.WriteLine(elements[0]!.OuterXml);
AssertIsHr(elements[0], false);
}
diff --git a/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs b/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
index c58459de..2b56cac0 100644
--- a/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
+++ b/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
@@ -8,17 +8,17 @@ namespace HtmlToOpenXml.Tests
{
public abstract class HtmlConverterTestBase
{
- private System.IO.MemoryStream generatedDocument;
- private WordprocessingDocument package;
+ private MemoryStream generatedDocument = default!;
+ private WordprocessingDocument package = default!;
- protected HtmlConverter converter;
- protected MainDocumentPart mainPart;
+ protected HtmlConverter converter = default!;
+ protected MainDocumentPart mainPart = default!;
[SetUp]
public void Init ()
{
- generatedDocument = new System.IO.MemoryStream();
+ generatedDocument = new MemoryStream();
package = WordprocessingDocument.Create(generatedDocument, WordprocessingDocumentType.Document);
mainPart = package.MainDocumentPart!;
diff --git a/test/HtmlToOpenXml.Tests/HtmlToOpenXml.Tests.csproj b/test/HtmlToOpenXml.Tests/HtmlToOpenXml.Tests.csproj
index e781a424..434b85a5 100755
--- a/test/HtmlToOpenXml.Tests/HtmlToOpenXml.Tests.csproj
+++ b/test/HtmlToOpenXml.Tests/HtmlToOpenXml.Tests.csproj
@@ -16,11 +16,11 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
-
+
-
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/test/HtmlToOpenXml.Tests/ParserTests.cs b/test/HtmlToOpenXml.Tests/ParserTests.cs
index a3dcd8b4..aa685eff 100644
--- a/test/HtmlToOpenXml.Tests/ParserTests.cs
+++ b/test/HtmlToOpenXml.Tests/ParserTests.cs
@@ -89,7 +89,7 @@ public void ConsecutiveParagraph_WithClosedTags_ShouldNotContinueStyle()
Assert.That(elements[1].ChildElements, Has.Count.EqualTo(1));
Assert.That(elements[1].FirstChild, Is.TypeOf(typeof(Run)));
- var runProperties = elements[1].FirstChild.GetFirstChild();
+ var runProperties = elements[1].FirstChild!.GetFirstChild();
Assert.That(runProperties, Is.Null);
}
@@ -100,6 +100,7 @@ public void ConsecutiveParagraph_WithClosedTags_ShouldNotContinueStyle()
public int Newline_ReturnsRunCount (string html)
{
var elements = converter.Parse(html);
+ Assert.That(elements.Count, Is.EqualTo(1));
return elements[0].Count(c => c is Run);
}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs b/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs
index 2b891847..3848f148 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs
@@ -8,25 +8,23 @@ namespace HtmlToOpenXml.Tests.Primitives
[TestFixture]
public class ColorTests
{
- [TestCase("", 0, 0, 0, 0d)]
[TestCase("#F00", 255, 0, 0, 1d)]
[TestCase("#00FFFF", 0, 255, 255, 1d)]
[TestCase("red", 255, 0, 0, 1d)]
- [TestCase("rgb(106, 90, 205)", 106, 90, 205, 1d)]
+ [TestCase("rgb(106, 90, 205)", 106, 90, 205, 1d)]
[TestCase("rgba(106, 90, 205, 0.6)", 106, 90, 205, 0.6d)]
+ [TestCase("rgb(106 90 205)", 106, 90, 205, 1d)]
+ [TestCase("rgb(106 90 205 / 0.25)", 106, 90, 205, 0.25d)]
[TestCase("hsl(248, 53%, 58%)", 106, 91, 205, 1)]
[TestCase("hsla(9, 100%, 64%, 0.6)", 255, 99, 71, 0.6d)]
[TestCase("hsl(0, 100%, 50%)", 255, 0, 0, 1)]
- // Percentage not respected that should be maxed out
- [TestCase("hsl(0, 200%, 150%)", 255, 255, 255, 1)]
- // Failure that leads to empty
- [TestCase("rgba(1.06, 90, 205, 0.6)", 0, 0, 0, 0.0d)]
- [TestCase("rgba(a, r, g, b)", 0, 0, 0, 0.0d)]
+ [TestCase("hsl(0, 200%, 150%)", 255, 255, 255, 1, Description = "Percentage not respected that should be maxed out")]
public void ParseHtmlColor_ShouldSucceed(string htmlColor, byte red, byte green, byte blue, double alpha)
{
var color = HtmlColor.Parse(htmlColor);
Assert.Multiple(() => {
+ Assert.That(color.IsEmpty, Is.False);
Assert.That(color.R, Is.EqualTo(red));
Assert.That(color.B, Is.EqualTo(blue));
Assert.That(color.G, Is.EqualTo(green));
@@ -34,10 +32,22 @@ public void ParseHtmlColor_ShouldSucceed(string htmlColor, byte red, byte green,
});
}
+ // Failure that leads to empty
+ [TestCase("")]
+ [TestCase("rgba(1.06, 90, 205, 0.6)")]
+ [TestCase("rgba(a, r, g, b)")]
+ [TestCase("rgb")]
+ public void ParseInvalidHtmlColor_ReturnsEmpty(string htmlColor)
+ {
+ var color = HtmlColor.Parse(htmlColor);
+ Assert.That(color.IsEmpty, Is.True);
+ }
+
[TestCase(255, 0, 0, 0, ExpectedResult = "FF0000")]
public string ArgColor_ToHex_ShouldSucceed(byte red, byte green, byte blue, double alpha)
{
var color = HtmlColor.FromArgb(alpha, red, green, blue);
+ Assert.That(color.IsEmpty, Is.False);
return color.ToHexString();
}
@@ -45,6 +55,7 @@ public string ArgColor_ToHex_ShouldSucceed(byte red, byte green, byte blue, doub
public string HslColor_ToHex_ShouldSucceed(double alpha, double hue, double saturation, double luminosity)
{
var color = HtmlColor.FromHsl(alpha, hue, saturation, luminosity);
+ Assert.That(color.IsEmpty, Is.False);
return color.ToHexString();
}
}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/FontTests.cs b/test/HtmlToOpenXml.Tests/Primitives/FontTests.cs
new file mode 100644
index 00000000..1fa1d28a
--- /dev/null
+++ b/test/HtmlToOpenXml.Tests/Primitives/FontTests.cs
@@ -0,0 +1,80 @@
+using NUnit.Framework;
+
+namespace HtmlToOpenXml.Tests.Primitives
+{
+ ///
+ /// Tests Html font style attribute.
+ ///
+ [TestFixture]
+ public class FontTests
+ {
+ [TestCase("1.2em Verdana", ExpectedResult = true)]
+ [TestCase("Verdana 1.2em", ExpectedResult = false)]
+ [TestCase("italic Verdana", ExpectedResult = false)]
+ public bool WithMinimal_ReturnsValid (string html)
+ {
+ var font = HtmlFont.Parse(html);
+ Assert.Multiple(() => {
+ Assert.That(font.Style, Is.Null);
+ Assert.That(font.Weight, Is.Null);
+ });
+ return font.Size.IsValid;
+ }
+
+ [TestCase("italic BOLD 1.2em Verdana")]
+ [TestCase("Verdana 1.2em bold italic ")]
+ public void WithDisordered_ShouldSucceed (string html)
+ {
+ var font = HtmlFont.Parse(html);
+ Assert.Multiple(() => {
+ Assert.That(font.Style, Is.EqualTo(FontStyle.Italic));
+ Assert.That(font.Weight, Is.EqualTo(FontWeight.Bold));
+ Assert.That(font.Family, Is.EqualTo("Verdana"));
+ Assert.That(font.Size.Metric, Is.EqualTo(UnitMetric.EM));
+ Assert.That(font.Size.Value, Is.EqualTo(1.2));
+ });
+ }
+
+ [Test(Description = "Multiple font families must keep the first one")]
+ public void WithMultipleFamily_ShouldSucceed ()
+ {
+ var font = HtmlFont.Parse("Verdana, Arial bolder 1.2em");
+ Assert.Multiple(() => {
+ Assert.That(font.Style, Is.Null);
+ Assert.That(font.Weight, Is.EqualTo(FontWeight.Bolder));
+ Assert.That(font.Family, Is.EqualTo("Verdana"));
+ Assert.That(font.Size.Metric, Is.EqualTo(UnitMetric.EM));
+ Assert.That(font.Size.Value, Is.EqualTo(1.2));
+ });
+ }
+
+ [Test(Description = "Font families with quotes must unescape the first one")]
+ public void WithQuotedFamily_ShouldSucceed ()
+ {
+ var font = HtmlFont.Parse("'Times New Roman', Times, Verdana, Arial bolder 1.2em");
+ Assert.Multiple(() => {
+ Assert.That(font.Style, Is.Null);
+ Assert.That(font.Weight, Is.EqualTo(FontWeight.Bolder));
+ Assert.That(font.Family, Is.EqualTo("Times New Roman"));
+ Assert.That(font.Size.Metric, Is.EqualTo(UnitMetric.EM));
+ Assert.That(font.Size.Value, Is.EqualTo(1.2));
+ });
+ }
+
+ [Test]
+ public void WithFontSizeLineHeight_ShouldSucceed()
+ {
+ var font = HtmlFont.Parse("italic small-caps bold 12px/30px Georgia, serif");
+ Assert.Multiple(() => {
+ Assert.That(font.Variant, Is.EqualTo(FontVariant.SmallCaps));
+ Assert.That(font.Style, Is.EqualTo(FontStyle.Italic));
+ Assert.That(font.Weight, Is.EqualTo(FontWeight.Bold));
+ Assert.That(font.Family, Is.EqualTo("Georgia"));
+ Assert.That(font.Size.Metric, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(font.Size.Value, Is.EqualTo(12));
+ Assert.That(font.LineHeight.Metric, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(font.LineHeight.Value, Is.EqualTo(30));
+ });
+ }
+ }
+}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs b/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
index 12bebe28..4db60b8f 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
@@ -12,6 +12,7 @@ public class MarginTests
[TestCase("25px 50px 75px", 25, 50, 75, 50)]
[TestCase("25px 50px", 25, 50, 25, 50)]
[TestCase("25px", 25, 25, 25, 25)]
+ [TestCase("25px 75px", 25, 75, 25, 75)]
public void ParseHtmlString_ShouldSucceed (string html, int top, int right, int bottom, int left)
{
var margin = Margin.Parse(html);
@@ -34,19 +35,19 @@ public void ParseWithFloat_ShouldSucceed ()
Assert.That(margin.IsValid, Is.EqualTo(true));
Assert.That(margin.Top.Value, Is.EqualTo(0));
- Assert.That(margin.Top.Type, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(margin.Top.Metric, Is.EqualTo(UnitMetric.Pixel));
Assert.That(margin.Right.Value, Is.EqualTo(50));
- Assert.That(margin.Right.Type, Is.EqualTo(UnitMetric.Percent));
+ Assert.That(margin.Right.Metric, Is.EqualTo(UnitMetric.Percent));
Assert.That(margin.Bottom.Value, Is.EqualTo(9.5));
- Assert.That(margin.Bottom.Type, Is.EqualTo(UnitMetric.Point));
+ Assert.That(margin.Bottom.Metric, Is.EqualTo(UnitMetric.Point));
Assert.That(margin.Bottom.ValueInPoint, Is.EqualTo(9.5));
//size are half-point font size (OpenXml relies mostly on long value, not on float)
Assert.That(Math.Round(margin.Bottom.ValueInPoint * 2).ToString(), Is.EqualTo("19"));
Assert.That(margin.Left.Value, Is.EqualTo(.00001));
- Assert.That(margin.Left.Type, Is.EqualTo(UnitMetric.Point));
+ Assert.That(margin.Left.Metric, Is.EqualTo(UnitMetric.Point));
// but due to conversion: 0 (OpenXml relies mostly on long value, not on float)
Assert.That(Math.Round(margin.Left.ValueInPoint * 2).ToString(), Is.EqualTo("0"));
});
@@ -61,13 +62,13 @@ public void ParseWithAuto_ShouldSucceed ()
Assert.That(margin.IsValid, Is.EqualTo(true));
Assert.That(margin.Top.Value, Is.EqualTo(0));
- Assert.That(margin.Top.Type, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(margin.Top.Metric, Is.EqualTo(UnitMetric.Pixel));
Assert.That(margin.Bottom.Value, Is.EqualTo(0));
- Assert.That(margin.Bottom.Type, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(margin.Bottom.Metric, Is.EqualTo(UnitMetric.Pixel));
- Assert.That(margin.Left.Type, Is.EqualTo(UnitMetric.Auto));
- Assert.That(margin.Right.Type, Is.EqualTo(UnitMetric.Auto));
+ Assert.That(margin.Left.Metric, Is.EqualTo(UnitMetric.Auto));
+ Assert.That(margin.Right.Metric, Is.EqualTo(UnitMetric.Auto));
});
}
}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
new file mode 100644
index 00000000..1820a15a
--- /dev/null
+++ b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
@@ -0,0 +1,47 @@
+using NUnit.Framework;
+
+namespace HtmlToOpenXml.Tests.Primitives
+{
+ ///
+ /// Tests parsing the `style` attribute.
+ ///
+ [TestFixture]
+ public class StyleParserTests
+ {
+ [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"));
+ });
+ }
+
+ [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"));
+ }
+
+ [TestCase("color;color;")]
+ [TestCase(":;")]
+ [TestCase("color:;")]
+ public void InvalidStyle_ShouldBeEmpty(string htmlStyle)
+ {
+ var styles = HtmlAttributeCollection.ParseStyle(htmlStyle);
+ Assert.That(styles.IsEmpty, Is.True);
+ Assert.That(styles["color"], Is.Null);
+ }
+
+ [Test]
+ public void WithMultipleTextDecoration_ReturnsAllValues()
+ {
+ var styles = HtmlAttributeCollection.ParseStyle("text-decoration:underline dotted wavy");
+ var decorations = styles.GetTextDecorations("text-decoration");
+ Assert.That(decorations, Is.EquivalentTo(new [] { TextDecoration.Underline, TextDecoration.Dotted, TextDecoration.Wave }));
+ }
+ }
+}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/UnitTests.cs b/test/HtmlToOpenXml.Tests/Primitives/UnitTests.cs
new file mode 100644
index 00000000..ad84ad46
--- /dev/null
+++ b/test/HtmlToOpenXml.Tests/Primitives/UnitTests.cs
@@ -0,0 +1,38 @@
+using NUnit.Framework;
+
+namespace HtmlToOpenXml.Tests.Primitives
+{
+ ///
+ /// Tests Html color style attribute.
+ ///
+ [TestFixture]
+ class UnitTests
+ {
+ [TestCase("auto", 0, UnitMetric.Auto)]
+ [TestCase("AUTO", 0, UnitMetric.Auto, Description = "Should be case insensitive")]
+ [TestCase("5%", 5, UnitMetric.Percent)]
+ [TestCase(" 12 px", 12, UnitMetric.Pixel)]
+ [TestCase(" 12 ", 12, UnitMetric.Unitless)]
+ [TestCase("9", 9, UnitMetric.Unitless)]
+ public void ParseHtmlUnit_ShouldSucceed(string str, double value, UnitMetric metric)
+ {
+ var unit = Unit.Parse(str);
+
+ Assert.Multiple(() => {
+ Assert.That(unit.IsValid, Is.True);
+ Assert.That(unit.Metric, Is.EqualTo(metric));
+ Assert.That(unit.Value, Is.EqualTo(value));
+ });
+ }
+
+ [TestCase(" ")]
+ [TestCase("12zz")]
+ [TestCase("zz")]
+ [TestCase("%")]
+ public void ParseInvalidHtmlColor_ReturnsEmpty(string str)
+ {
+ var unit = Unit.Parse(str);
+ Assert.That(unit.IsValid, Is.False);
+ }
+ }
+}
diff --git a/test/HtmlToOpenXml.Tests/StyleTests.cs b/test/HtmlToOpenXml.Tests/StyleTests.cs
index 24afe765..beceff96 100644
--- a/test/HtmlToOpenXml.Tests/StyleTests.cs
+++ b/test/HtmlToOpenXml.Tests/StyleTests.cs
@@ -25,7 +25,7 @@ public void UseVariantStyle_ReturnsAppliedStyle()
Type = args.Type,
BasedOn = new BasedOn { Val = "Normal" },
StyleRunProperties = new() {
- Color = new() { Val = HtmlColorTranslator.FromHtml("red").ToHexString() }
+ Color = new() { Val = HtmlColor.Parse("red").ToHexString() }
}
});
};
@@ -175,7 +175,7 @@ public void EncodedStyle_ShouldSucceed()
}
[Test(Description = "Key style with no value should be ignored")]
- public void EmptyStyle_ShouldBeIgnoredd()
+ public void EmptyStyle_ShouldBeIgnored()
{
var styleAttributes = HtmlAttributeCollection.ParseStyle("text-decoration;color:red");
Assert.That(styleAttributes["text-decoration"], Is.Null);