();
\ 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 d0b3a6f8..beda5174 100644
--- a/examples/Demo/Demo.csproj
+++ b/examples/Demo/Demo.csproj
@@ -3,10 +3,11 @@
net8.0
Exe
+ true
-
+
diff --git a/examples/Demo/Program.cs b/examples/Demo/Program.cs
index fc993be1..3b2479e1 100644
--- a/examples/Demo/Program.cs
+++ b/examples/Demo/Program.cs
@@ -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..1ea4cc03 100755
--- a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
+++ b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
using DocumentFormat.OpenXml.Wordprocessing;
namespace HtmlToOpenXml;
@@ -18,27 +16,90 @@ namespace HtmlToOpenXml;
///
/// Represents the collection of attributes present in the current html tag.
///
-sealed class HtmlAttributeCollection
+readonly struct 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 +109,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 +123,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 +134,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 +146,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 +193,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/Collections/OpenXmlDocumentStyleCollection.cs b/src/Html2OpenXml/Collections/OpenXmlDocumentStyleCollection.cs
index 7e879a9e..c7e81e8e 100755
--- a/src/Html2OpenXml/Collections/OpenXmlDocumentStyleCollection.cs
+++ b/src/Html2OpenXml/Collections/OpenXmlDocumentStyleCollection.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using DocumentFormat.OpenXml.Wordprocessing;
namespace HtmlToOpenXml;
diff --git a/src/Html2OpenXml/Collections/RowSpanCollection.cs b/src/Html2OpenXml/Collections/RowSpanCollection.cs
index 9b34ee55..cb1ab488 100644
--- a/src/Html2OpenXml/Collections/RowSpanCollection.cs
+++ b/src/Html2OpenXml/Collections/RowSpanCollection.cs
@@ -9,9 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using System.Collections;
-using System.Collections.Generic;
namespace HtmlToOpenXml;
diff --git a/src/Html2OpenXml/Expressions/AbbreviationExpression.cs b/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
index d0dcca86..92130128 100644
--- a/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
+++ b/src/Html2OpenXml/Expressions/AbbreviationExpression.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Text.RegularExpressions;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
@@ -25,6 +22,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 +131,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 00ddff6d..bf5de423 100644
--- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
@@ -9,10 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -267,7 +264,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
@@ -276,7 +273,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/BlockQuoteExpression.cs b/src/Html2OpenXml/Expressions/BlockQuoteExpression.cs
index 3fa3d431..adeb706d 100644
--- a/src/Html2OpenXml/Expressions/BlockQuoteExpression.cs
+++ b/src/Html2OpenXml/Expressions/BlockQuoteExpression.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
-using System.Linq;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
diff --git a/src/Html2OpenXml/Expressions/BodyExpression.cs b/src/Html2OpenXml/Expressions/BodyExpression.cs
index 8444acfa..462d375d 100644
--- a/src/Html2OpenXml/Expressions/BodyExpression.cs
+++ b/src/Html2OpenXml/Expressions/BodyExpression.cs
@@ -9,9 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
diff --git a/src/Html2OpenXml/Expressions/FigureCaptionExpression.cs b/src/Html2OpenXml/Expressions/FigureCaptionExpression.cs
index 68a7b926..c17c7eaa 100644
--- a/src/Html2OpenXml/Expressions/FigureCaptionExpression.cs
+++ b/src/Html2OpenXml/Expressions/FigureCaptionExpression.cs
@@ -1,125 +1,123 @@
-/* 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.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using AngleSharp.Html.Dom;
-using DocumentFormat.OpenXml;
-using DocumentFormat.OpenXml.Wordprocessing;
-
-namespace HtmlToOpenXml.Expressions;
-
-///
-/// Process the parsing of a figcaption element, which is used to describe an image.
-///
-sealed class FigureCaptionExpression(IHtmlElement node) : BlockElementExpression(node)
-{
-
- ///
- public override IEnumerable Interpret (ParsingContext context)
- {
- ComposeStyles(context);
- var childElements = Interpret(context.CreateChild(this), node.ChildNodes);
-
- var figNumRef = new List() {
- new Run(
- new Text("Figure ") { Space = SpaceProcessingModeValues.Preserve }
- ),
- new SimpleField(
- new Run(
- new Text(AddFigureCaption(context).ToString(CultureInfo.InvariantCulture)))
- ) { Instruction = " SEQ Figure \\* ARABIC " }
- };
-
-
- if (!childElements.Any())
- {
- return [new Paragraph(figNumRef) {
- ParagraphProperties = new ParagraphProperties {
- ParagraphStyleId = context.DocumentStyle.GetParagraphStyle(context.DocumentStyle.DefaultStyles.CaptionStyle),
- KeepNext = DetermineKeepNext(node),
- }
- }];
- }
-
- //Add the figure number references to the start of the first paragraph.
- if(childElements.First() is Paragraph p)
- {
- var properties = p.GetFirstChild();
- p.InsertAfter(new Run(
- new Text(" ") { Space = SpaceProcessingModeValues.Preserve }
- ), properties);
- p.InsertAfter(figNumRef[1], properties);
- p.InsertAfter(figNumRef[0], properties);
- }
- else
- {
- // The first child of the figure caption is a table or something.
- // Just prepend a new paragraph with the figure number reference.
- childElements = [
- new Paragraph(figNumRef),
- ..childElements
- ];
- }
-
- foreach (var paragraph in childElements.OfType())
- {
- paragraph.ParagraphProperties ??= new ParagraphProperties();
- paragraph.ParagraphProperties.ParagraphStyleId ??= context.DocumentStyle.GetParagraphStyle(context.DocumentStyle.DefaultStyles.CaptionStyle);
- //Keep caption paragraphs together.
- paragraph.ParagraphProperties.KeepNext = new KeepNext();
- }
-
- if(childElements.OfType().LastOrDefault() is Paragraph lastPara)
- {
- lastPara.ParagraphProperties!.KeepNext = DetermineKeepNext(node);
- }
-
- return childElements;
- }
-
- ///
- /// Add a new figure caption to the document.
- ///
- /// Returns the id of the new figure caption.
- private static int AddFigureCaption(ParsingContext context)
- {
- var figCaptionRef = context.Properties("figCaptionRef");
- if (!figCaptionRef.HasValue)
- {
- figCaptionRef = 0;
- foreach (var p in context.MainPart.Document.Descendants())
- {
- if (p.Instruction == " SEQ Figure \\* ARABIC ")
- figCaptionRef++;
- }
- }
- figCaptionRef++;
-
- context.Properties("figCaptionRef", figCaptionRef);
- return figCaptionRef.Value;
- }
-
- ///
- /// Determines whether the KeepNext property should apply this this caption.
- ///
- /// A new or null.
- private static KeepNext? DetermineKeepNext(IHtmlElement node)
- {
- // A caption at the end of a figure will have no next sibling.
- if(node.NextElementSibling is null)
- {
- return null;
- }
- return new();
- }
-}
+/* 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.Globalization;
+using AngleSharp.Html.Dom;
+using DocumentFormat.OpenXml;
+using DocumentFormat.OpenXml.Wordprocessing;
+
+namespace HtmlToOpenXml.Expressions;
+
+///
+/// Process the parsing of a figcaption element, which is used to describe an image.
+///
+sealed class FigureCaptionExpression(IHtmlElement node) : BlockElementExpression(node)
+{
+
+ ///
+ public override IEnumerable Interpret (ParsingContext context)
+ {
+ ComposeStyles(context);
+ var childElements = Interpret(context.CreateChild(this), node.ChildNodes);
+
+ var figNumRef = new List() {
+ new Run(
+ new Text("Figure ") { Space = SpaceProcessingModeValues.Preserve }
+ ),
+ new SimpleField(
+ new Run(
+ new Text(AddFigureCaption(context).ToString(CultureInfo.InvariantCulture)))
+ ) { Instruction = " SEQ Figure \\* ARABIC " }
+ };
+
+
+ if (!childElements.Any())
+ {
+ return [new Paragraph(figNumRef) {
+ ParagraphProperties = new ParagraphProperties {
+ ParagraphStyleId = context.DocumentStyle.GetParagraphStyle(context.DocumentStyle.DefaultStyles.CaptionStyle),
+ KeepNext = DetermineKeepNext(node),
+ }
+ }];
+ }
+
+ //Add the figure number references to the start of the first paragraph.
+ if(childElements.First() is Paragraph p)
+ {
+ var properties = p.GetFirstChild();
+ p.InsertAfter(new Run(
+ new Text(" ") { Space = SpaceProcessingModeValues.Preserve }
+ ), properties);
+ p.InsertAfter(figNumRef[1], properties);
+ p.InsertAfter(figNumRef[0], properties);
+ }
+ else
+ {
+ // The first child of the figure caption is a table or something.
+ // Just prepend a new paragraph with the figure number reference.
+ childElements = [
+ new Paragraph(figNumRef),
+ ..childElements
+ ];
+ }
+
+ foreach (var paragraph in childElements.OfType())
+ {
+ paragraph.ParagraphProperties ??= new ParagraphProperties();
+ paragraph.ParagraphProperties.ParagraphStyleId ??= context.DocumentStyle.GetParagraphStyle(context.DocumentStyle.DefaultStyles.CaptionStyle);
+ //Keep caption paragraphs together.
+ paragraph.ParagraphProperties.KeepNext = new KeepNext();
+ }
+
+ if(childElements.OfType().LastOrDefault() is Paragraph lastPara)
+ {
+ lastPara.ParagraphProperties!.KeepNext = DetermineKeepNext(node);
+ }
+
+ return childElements;
+ }
+
+ ///
+ /// Add a new figure caption to the document.
+ ///
+ /// Returns the id of the new figure caption.
+ private static int AddFigureCaption(ParsingContext context)
+ {
+ var figCaptionRef = context.Properties("figCaptionRef");
+ if (!figCaptionRef.HasValue)
+ {
+ figCaptionRef = 0;
+ foreach (var p in context.MainPart.Document.Descendants())
+ {
+ if (p.Instruction == " SEQ Figure \\* ARABIC ")
+ figCaptionRef++;
+ }
+ }
+ figCaptionRef++;
+
+ context.Properties("figCaptionRef", figCaptionRef);
+ return figCaptionRef.Value;
+ }
+
+ ///
+ /// Determines whether the KeepNext property should apply this this caption.
+ ///
+ /// A new or null.
+ private static KeepNext? DetermineKeepNext(IHtmlElement node)
+ {
+ // A caption at the end of a figure will have no next sibling.
+ if(node.NextElementSibling is null)
+ {
+ return null;
+ }
+ return new();
+ }
+}
diff --git a/src/Html2OpenXml/Expressions/FontElementExpression.cs b/src/Html2OpenXml/Expressions/FontElementExpression.cs
index 88f47b8d..f95cf824 100644
--- a/src/Html2OpenXml/Expressions/FontElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/FontElementExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using System.Globalization;
using AngleSharp.Html.Dom;
@@ -27,7 +26,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/HorizontalLineExpression.cs b/src/Html2OpenXml/Expressions/HorizontalLineExpression.cs
index 49b7ec0c..88e8513a 100644
--- a/src/Html2OpenXml/Expressions/HorizontalLineExpression.cs
+++ b/src/Html2OpenXml/Expressions/HorizontalLineExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -35,23 +34,15 @@ public override IEnumerable Interpret (ParsingContext context)
// If the previous paragraph contains a bottom border or is a Table, we add some spacing between the
// and the previous element or Word will display only the last border.
// (see Remarks: http://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.bottomborder%28office.14%29.aspx)
- var addSpacing = false;
+ var shouldAddSpacing = previousElement is IHtmlTableElement
+ || (
+ !(styleAttributes = previousElement.GetStyles()).IsEmpty
+ && !(border = styleAttributes.GetBorders()).IsEmpty
+ && border.Bottom.IsValid
+ && border.Bottom.Width.ValueInDxa > 0
+ );
- if (previousElement is IHtmlTableElement)
- {
- addSpacing = true;
- }
- else
- {
- styleAttributes = previousElement.GetStyles();
- border = styleAttributes.GetBorders();
- if (border.Bottom.IsValid && border.Bottom.Width.ValueInDxa > 0)
- {
- addSpacing = true;
- }
- }
-
- if (addSpacing)
+ if (shouldAddSpacing)
{
paragraph.ParagraphProperties = new ParagraphProperties {
SpacingBetweenLines = new() { Before = "240" }
@@ -64,7 +55,9 @@ public override IEnumerable Interpret (ParsingContext context)
paragraph.Append(new Run());
styleAttributes = node.GetStyles();
- border = styleAttributes.GetBorders();
+ border = new HtmlBorder();
+ if (!styleAttributes.IsEmpty)
+ border = styleAttributes.GetBorders();
// Get style from border (only top) or use Default style
TopBorder? hrBorderStyle;
diff --git a/src/Html2OpenXml/Expressions/HtmlDomExpression.cs b/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
index 3efc82ef..5fab9ed7 100644
--- a/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
+++ b/src/Html2OpenXml/Expressions/HtmlDomExpression.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
diff --git a/src/Html2OpenXml/Expressions/HyperlinkExpression.cs b/src/Html2OpenXml/Expressions/HyperlinkExpression.cs
index 79fd8bee..a34c826f 100644
--- a/src/Html2OpenXml/Expressions/HyperlinkExpression.cs
+++ b/src/Html2OpenXml/Expressions/HyperlinkExpression.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Linq;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
diff --git a/src/Html2OpenXml/Expressions/Image/ImageExpression.cs b/src/Html2OpenXml/Expressions/Image/ImageExpression.cs
index 27c861c0..fe03d36d 100644
--- a/src/Html2OpenXml/Expressions/Image/ImageExpression.cs
+++ b/src/Html2OpenXml/Expressions/Image/ImageExpression.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Threading;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Svg.Dom;
@@ -163,7 +161,7 @@ private static int GetDimension(HtmlAttributeCollection styles, string primarySt
if (unit.IsValid)
{
- return unit.Type == UnitMetric.Percent ?
+ return unit.Metric == UnitMetric.Percent ?
(int)(unit.Value * percentageBase / 100) :
unit.ValueInPx;
}
diff --git a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
index 06fb089a..88b76451 100644
--- a/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Image/ImageExpressionBase.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
-using System.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -65,7 +63,7 @@ private void ComposeStyles ()
}
else
{
- var borderWidth = Unit.Parse(node.GetAttribute("border"));
+ var borderWidth = Unit.Parse(node.GetAttribute("border").AsSpan());
if (borderWidth.IsValid)
{
border.Val = BorderValues.Single;
@@ -81,8 +79,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/Image/SvgExpression.cs b/src/Html2OpenXml/Expressions/Image/SvgExpression.cs
index 7bb9a7f6..4082bfe1 100644
--- a/src/Html2OpenXml/Expressions/Image/SvgExpression.cs
+++ b/src/Html2OpenXml/Expressions/Image/SvgExpression.cs
@@ -34,7 +34,7 @@ sealed class SvgExpression(ISvgSvgElement node) : ImageExpressionBase(node)
protected override Drawing? CreateDrawing(ParsingContext context)
{
var imgPart = context.MainPart.AddImagePart(ImagePartType.Svg);
- using var stream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(svgNode.OuterHtml), writable: false);
+ using var stream = new MemoryStream(Encoding.UTF8.GetBytes(svgNode.OuterHtml), writable: false);
imgPart.FeedData(stream);
var imagePartId = context.MainPart.GetIdOfPart(imgPart);
return CreateSvgDrawing(context, svgNode, imagePartId, Size.Empty);
@@ -42,8 +42,8 @@ sealed class SvgExpression(ISvgSvgElement node) : ImageExpressionBase(node)
internal static Drawing CreateSvgDrawing(ParsingContext context, ISvgSvgElement svgNode, string imagePartId, Size preferredSize)
{
- var width = Unit.Parse(svgNode.GetAttribute("width"));
- var height = Unit.Parse(svgNode.GetAttribute("height"));
+ var width = Unit.Parse(svgNode.GetAttribute("width").AsSpan());
+ var height = Unit.Parse(svgNode.GetAttribute("height").AsSpan());
long widthInEmus, heightInEmus;
if (width.IsValid && height.IsValid)
{
diff --git a/src/Html2OpenXml/Expressions/LineBreakExpression.cs b/src/Html2OpenXml/Expressions/LineBreakExpression.cs
index 749b14c9..ec49ffee 100644
--- a/src/Html2OpenXml/Expressions/LineBreakExpression.cs
+++ b/src/Html2OpenXml/Expressions/LineBreakExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
diff --git a/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs b/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
index 24b34ad7..452ad1dc 100644
--- a/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/HeadingElementExpression.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Text.RegularExpressions;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
@@ -100,7 +97,6 @@ private static bool IsNumbering(OpenXmlElement runElement)
return false;
}
-
// Make sure we only grab the heading if it starts with a number
if (regexMatch.Success && headingText.Length > regexMatch.Groups["number"].Length)
{
diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
index ed7af86d..2d071b43 100644
--- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Linq;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
diff --git a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
index 1e0e1586..881d3ba1 100644
--- a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
@@ -9,8 +9,9 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
-using System.Linq;
+#if NET5_0_OR_GREATER
+using System.Collections.Frozen;
+#endif
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -26,7 +27,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 +219,7 @@ private void InitNumberingIds(ParsingContext context)
///
/// Predefined template of lists.
///
- private static Dictionary InitKnownLists()
+ private static IReadOnlyDictionary InitKnownLists()
{
var knownAbstractNums = new Dictionary();
@@ -292,6 +293,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 ec489ef4..0796f046 100644
--- a/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/PhrasingElementExpression.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Globalization;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
@@ -28,7 +26,7 @@ class PhrasingElementExpression(IHtmlElement node, OpenXmlLeafElement? styleProp
private readonly OpenXmlLeafElement? defaultStyleProperty = styleProperty;
protected readonly RunProperties runProperties = new();
- protected HtmlAttributeCollection? styleAttributes;
+ protected HtmlAttributeCollection styleAttributes;
protected IHtmlElement node = node;
@@ -117,7 +115,7 @@ protected virtual void ComposeStyles (ParsingContext context)
}
var colorValue = styleAttributes.GetColor("color");
- if (colorValue.IsEmpty) colorValue = HtmlColor.Parse(node.GetAttribute("color"));
+ if (colorValue.IsEmpty) colorValue = HtmlColor.Parse(node.GetAttribute("color").AsSpan());
if (!colorValue.IsEmpty)
runProperties.Color = new Color { Val = colorValue.ToHexString() };
@@ -130,7 +128,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/PreElementExpression.cs b/src/Html2OpenXml/Expressions/PreElementExpression.cs
index b466acc4..c5369fab 100644
--- a/src/Html2OpenXml/Expressions/PreElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/PreElementExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
diff --git a/src/Html2OpenXml/Expressions/QuoteElementExpression.cs b/src/Html2OpenXml/Expressions/QuoteElementExpression.cs
index 45265f67..ab5a14eb 100644
--- a/src/Html2OpenXml/Expressions/QuoteElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/QuoteElementExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
diff --git a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
index f9d60817..6a4f94cb 100644
--- a/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableCellExpression.cs
@@ -9,9 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -70,7 +68,7 @@ protected override void ComposeStyles(ParsingContext context)
var widthValue = cellNode.GetAttribute("width");
if (!string.IsNullOrEmpty(widthValue))
{
- width = Unit.Parse(widthValue);
+ width = Unit.Parse(widthValue.AsSpan());
}
}
@@ -78,8 +76,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/TableColExpression.cs b/src/Html2OpenXml/Expressions/Table/TableColExpression.cs
index 46b5a804..c51269de 100644
--- a/src/Html2OpenXml/Expressions/Table/TableColExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableColExpression.cs
@@ -44,7 +44,7 @@ public override IEnumerable Interpret(ParsingContext context)
// If this attribute is omitted, then the last saved width of the grid column is assumed to be zero.
column.Width = Math.Round(width.ValueInPoint * 20).ToString(CultureInfo.InvariantCulture);
}
- else if (width.Type == UnitMetric.Percent)
+ else if (width.Metric == UnitMetric.Percent)
{
var maxWidth = context.IsLandscape ? MaxTableLandscapeWidth : MaxTablePortraitWidth;
percentWidth = Math.Max(0, Math.Min(100, width.Value));
diff --git a/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs b/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs
index dc4b1d0e..27bef6f4 100644
--- a/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableElementExpressionBase.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -83,7 +82,7 @@ protected override void ComposeStyles(ParsingContext context)
cellProperties.TableCellVerticalAlignment = new() { Val = valign };
var bgcolor = styleAttributes.GetColor("background-color");
- if (bgcolor.IsEmpty) bgcolor = HtmlColor.Parse(node.GetAttribute("bgcolor"));
+ if (bgcolor.IsEmpty) bgcolor = HtmlColor.Parse(node.GetAttribute("bgcolor").AsSpan());
if (bgcolor.IsEmpty) bgcolor = styleAttributes.GetColor("background");
if (!bgcolor.IsEmpty)
{
diff --git a/src/Html2OpenXml/Expressions/Table/TableExpression.cs b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
index 3faea0ff..dd0e54db 100644
--- a/src/Html2OpenXml/Expressions/Table/TableExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableExpression.cs
@@ -9,10 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -169,10 +166,10 @@ protected override void ComposeStyles (ParsingContext context)
styleAttributes = tableNode.GetStyles();
var width = styleAttributes.GetUnit("width", UnitMetric.Pixel);
- if (!width.IsValid) width = Unit.Parse(tableNode.GetAttribute("width"), UnitMetric.Pixel);
+ if (!width.IsValid) width = Unit.Parse(tableNode.GetAttribute("width").AsSpan(), UnitMetric.Pixel);
if (!width.IsValid) width = new Unit(UnitMetric.Percent, 100);
- switch (width.Type)
+ switch (width.Metric)
{
case UnitMetric.Percent:
tableProperties.TableWidth = new TableWidth
@@ -286,9 +283,9 @@ protected override void ComposeStyles (ParsingContext context)
if (!align.HasValue)
{
var margin = styleAttributes.GetMargin("margin");
- if (margin.Left.Type == UnitMetric.Auto)
+ if (margin.Left.Metric == UnitMetric.Auto)
{
- if (margin.Right.Type == UnitMetric.Auto)
+ if (margin.Right.Metric == UnitMetric.Auto)
align = JustificationValues.Center;
else
align = JustificationValues.Right;
diff --git a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
index d0f04ff8..25c7ce1d 100644
--- a/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
+++ b/src/Html2OpenXml/Expressions/Table/TableRowExpression.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -103,9 +102,9 @@ protected override void ComposeStyles(ParsingContext context)
base.ComposeStyles(context);
Unit unit = styleAttributes!.GetUnit("height", UnitMetric.Pixel);
- if (!unit.IsValid) unit = Unit.Parse(rowNode.GetAttribute("height"), UnitMetric.Pixel);
+ if (!unit.IsValid) unit = Unit.Parse(rowNode.GetAttribute("height").AsSpan(), 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/Expressions/TextExpression.cs b/src/Html2OpenXml/Expressions/TextExpression.cs
index 3d09679c..4bca0c20 100644
--- a/src/Html2OpenXml/Expressions/TextExpression.cs
+++ b/src/Html2OpenXml/Expressions/TextExpression.cs
@@ -9,11 +9,9 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
#if NET5_0_OR_GREATER
using System.Collections.Frozen;
#endif
-using System.Collections.Generic;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Text;
diff --git a/src/Html2OpenXml/HtmlConverter.cs b/src/Html2OpenXml/HtmlConverter.cs
index 8e0664ed..25abf358 100755
--- a/src/Html2OpenXml/HtmlConverter.cs
+++ b/src/Html2OpenXml/HtmlConverter.cs
@@ -9,11 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Html.Dom;
using DocumentFormat.OpenXml;
diff --git a/src/Html2OpenXml/HtmlToOpenXml.csproj b/src/Html2OpenXml/HtmlToOpenXml.csproj
index 76c5227f..dcc8093c 100644
--- a/src/Html2OpenXml/HtmlToOpenXml.csproj
+++ b/src/Html2OpenXml/HtmlToOpenXml.csproj
@@ -9,13 +9,13 @@
HtmlToOpenXml
HtmlToOpenXml
HtmlToOpenXml.dll
- 3.2.8
+ 3.3.0
icon.png
Copyright 2009-$([System.DateTime]::Now.Year) Olivier Nizet
See changelog https://github.com/onizet/html2openxml/blob/master/CHANGELOG.md
README.md
office openxml netcore html
- 3.2.8
+ 3.3.0
MIT
https://github.com/onizet/html2openxml
https://github.com/onizet/html2openxml
@@ -25,6 +25,7 @@
true
embedded
$(NoWarn);CS8981
+ enable
@@ -44,8 +45,8 @@
-
-
+
+
@@ -64,5 +65,15 @@
true
+
+
+
+
+
+
+
+ @(ReleaseNoteLines, '%0a')
+
+
\ No newline at end of file
diff --git a/src/Html2OpenXml/IO/DataUri.cs b/src/Html2OpenXml/IO/DataUri.cs
index 394ffaf4..64c5fa03 100755
--- a/src/Html2OpenXml/IO/DataUri.cs
+++ b/src/Html2OpenXml/IO/DataUri.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using System.Text;
using System.Text.RegularExpressions;
@@ -21,9 +20,11 @@ namespace HtmlToOpenXml.IO;
[System.Diagnostics.DebuggerDisplay("{Mime,nq}")]
public sealed class DataUri
{
- private readonly static Regex dataUriRegex = new Regex(
+ // mime-type can be: svg+xml, x-png
+ private readonly static Regex dataUriRegex = new(
@"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/IO/DefaultWebRequest.cs b/src/Html2OpenXml/IO/DefaultWebRequest.cs
index 9125c7db..9be1fe0e 100644
--- a/src/Html2OpenXml/IO/DefaultWebRequest.cs
+++ b/src/Html2OpenXml/IO/DefaultWebRequest.cs
@@ -9,12 +9,8 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Net;
using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace HtmlToOpenXml.IO;
diff --git a/src/Html2OpenXml/Primitives/HtmlImageInfo.cs b/src/Html2OpenXml/IO/HtmlImageInfo.cs
similarity index 95%
rename from src/Html2OpenXml/Primitives/HtmlImageInfo.cs
rename to src/Html2OpenXml/IO/HtmlImageInfo.cs
index aacf58c8..a5b20df5 100755
--- a/src/Html2OpenXml/Primitives/HtmlImageInfo.cs
+++ b/src/Html2OpenXml/IO/HtmlImageInfo.cs
@@ -11,7 +11,7 @@
*/
using DocumentFormat.OpenXml.Packaging;
-namespace HtmlToOpenXml;
+namespace HtmlToOpenXml.IO;
///
/// Represents an image and its metadata.
diff --git a/src/Html2OpenXml/IO/IWebRequest.cs b/src/Html2OpenXml/IO/IWebRequest.cs
index 463fcde3..3cc218e7 100644
--- a/src/Html2OpenXml/IO/IWebRequest.cs
+++ b/src/Html2OpenXml/IO/IWebRequest.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Threading;
-using System.Threading.Tasks;
namespace HtmlToOpenXml.IO;
diff --git a/src/Html2OpenXml/IO/ImageHeader.cs b/src/Html2OpenXml/IO/ImageHeader.cs
index 94a1a518..851bfad6 100755
--- a/src/Html2OpenXml/IO/ImageHeader.cs
+++ b/src/Html2OpenXml/IO/ImageHeader.cs
@@ -13,10 +13,6 @@
* http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
* EMF Specifications: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-emf/ae7e7437-cfe5-485e-84ea-c74b51b000be
*/
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Text;
using System.Xml.XPath;
@@ -46,6 +42,7 @@ public enum FileType { Unrecognized, Bitmap, Gif, Png, Jpeg, Emf, Xml }
{ Encoding.UTF8.GetBytes(" x.Length).First().Length;
@@ -277,8 +274,8 @@ private static Size DecodeXml(Stream stream)
nav = nav.SelectSingleNode("/*[local-name() = 'svg']");
if (nav is not null)
{
- var width = Unit.Parse(nav.GetAttribute("width", string.Empty));
- var height = Unit.Parse(nav.GetAttribute("height", string.Empty));
+ var width = Unit.Parse(nav.GetAttribute("width", string.Empty).AsSpan());
+ var height = Unit.Parse(nav.GetAttribute("height", string.Empty).AsSpan());
if (width.IsValid && height.IsValid)
return new Size(width.ValueInPx, height.ValueInPx);
}
diff --git a/src/Html2OpenXml/IO/ImagePrefetcher.cs b/src/Html2OpenXml/IO/ImagePrefetcher.cs
index 9a2e4fda..5ead754d 100644
--- a/src/Html2OpenXml/IO/ImagePrefetcher.cs
+++ b/src/Html2OpenXml/IO/ImagePrefetcher.cs
@@ -1,331 +1,326 @@
-/* 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.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using DocumentFormat.OpenXml.Packaging;
-
-namespace HtmlToOpenXml.IO;
-
-interface IImageLoader
-{
- ///
- /// Download the remote or local image located at the specified url.
- ///
- Task Download(string imageUri, CancellationToken cancellationToken);
-}
-
-///
-/// Download and provison the metadata of a requested image.
-///
-sealed class ImagePrefetcher : IImageLoader
- where T: OpenXmlPartContainer, ISupportedRelationship
-{
- // Map extension to PartTypeInfo
- private static readonly Dictionary knownExtensions = new(StringComparer.OrdinalIgnoreCase) {
- { ".gif", ImagePartType.Gif },
- { ".bmp", ImagePartType.Bmp },
- { ".emf", ImagePartType.Emf },
- { ".ico", ImagePartType.Icon },
- { ".jp2", ImagePartType.Jp2 },
- { ".jpeg", ImagePartType.Jpeg },
- { ".jpg", ImagePartType.Jpeg },
- { ".jpe", ImagePartType.Jpeg },
- { ".pcx", ImagePartType.Pcx },
- { ".png", ImagePartType.Png },
- { ".svg", ImagePartType.Svg },
- { ".tif", ImagePartType.Tif },
- { ".tiff", ImagePartType.Tiff },
- { ".wmf", ImagePartType.Wmf }
- };
- private readonly T hostingPart;
- private readonly IWebRequest resourceLoader;
- private readonly HtmlImageInfoCollection prefetchedImages;
- private readonly object lockObject = new();
- private readonly ImageProcessingMode processingMode;
-
-
- ///
- /// Constructor.
- ///
- /// The image will be linked to that hosting part.
- /// Images are not shared between header, footer and body.
- /// Service to resolve an image.
- /// Specifies how images should be processed (embed, link, or data URI only).
- public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessingMode processingMode = ImageProcessingMode.Embed)
- {
- this.hostingPart = hostingPart;
- this.resourceLoader = resourceLoader;
- this.processingMode = processingMode;
- this.prefetchedImages = [];
- }
-
- //____________________________________________________________________
- //
- // Public Functionality
-
- ///
- /// Download the remote or local image located at the specified url.
- ///
- public async Task Download(string imageUri, CancellationToken cancellationToken)
- {
- // Check if image is already cached using thread-safe operation
- lock (lockObject)
- {
- if (prefetchedImages.Contains(imageUri))
- return prefetchedImages[imageUri];
- }
-
- HtmlImageInfo? iinfo;
- if (DataUri.IsWellFormed(imageUri)) // data inline, encoded in base64
- {
- iinfo = ReadDataUri(imageUri);
- }
- else
- {
- // Handle external images based on processing mode
- if (processingMode == ImageProcessingMode.EmbedDataUriOnly)
- {
- // Skip external images entirely
- return null;
- }
- else if (processingMode == ImageProcessingMode.LinkExternal)
- {
- // Create external link without downloading
- iinfo = CreateExternalImageLink(imageUri);
- }
- else
- {
- // Default: Download and embed
- iinfo = await DownloadRemoteImage(imageUri, cancellationToken).ConfigureAwait(false);
- }
- }
-
- // Add to cache using thread-safe operation
- if (iinfo != null)
- {
- lock (lockObject)
- {
- // Double-check pattern to prevent duplicate adds during concurrent access
- if (!prefetchedImages.Contains(imageUri))
- {
- prefetchedImages.Add(iinfo);
- }
- }
- }
-
- return iinfo;
- }
-
- ///
- /// Download the image and try to find its format type.
- ///
- private async Task DownloadRemoteImage(string src, CancellationToken cancellationToken)
- {
- Uri imageUri = new(src, UriKind.RelativeOrAbsolute);
- if (imageUri.IsAbsoluteUri && !resourceLoader.SupportsProtocol(imageUri.Scheme))
- return null;
-
- using var response = await resourceLoader.FetchAsync(imageUri, cancellationToken).ConfigureAwait(false);
- if (response?.Content == null || !response.Content.CanRead)
- return null;
-
- // For requested url with no filename, we need to read the media mime type if provided
- response.Headers.TryGetValue("Content-Type", out var mime);
- if (!TryInspectMimeType(mime, out PartTypeInfo type)
- && !TryGuessTypeFromUri(imageUri, out type)
- && !TryGuessTypeFromStream(response.Content, out type)
- )
- {
- return null;
- }
-
- return SaveImageAssert(src, type, response.Content.CopyTo);
- }
-
- ///
- /// Create an external relationship to an image without downloading it.
- ///
- private HtmlImageInfo? CreateExternalImageLink(string src)
- {
- Uri imageUri = new(src, UriKind.RelativeOrAbsolute);
-
- // Resolve relative URIs if possible (only for DefaultWebRequest which has BaseImageUrl)
- if (!imageUri.IsAbsoluteUri && resourceLoader is DefaultWebRequest defaultWebRequest
- && defaultWebRequest.BaseImageUrl != null)
- {
- string url1 = defaultWebRequest.BaseImageUrl.AbsoluteUri.TrimEnd('/', '\\');
- string path = src.TrimStart('/', '\\');
- imageUri = new Uri(string.Format("{0}/{1}", url1, path), UriKind.Absolute);
- }
-
- // Only create external links for absolute URIs with supported protocols
- if (!imageUri.IsAbsoluteUri || !resourceLoader.SupportsProtocol(imageUri.Scheme))
- return null;
-
- // Generate a unique GUID-based relationship ID for the external relationship
- string relationshipId = "imgext_" + Guid.NewGuid().ToString("N");
-
- // Create external relationship
- lock (lockObject)
- {
- hostingPart.AddExternalRelationship(
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
- imageUri,
- relationshipId);
- }
-
- // Return image info with external flag set
- // Note: Size will be empty as we don't download the image
- return new HtmlImageInfo(src, relationshipId) {
- IsExternal = true,
- Size = Size.Empty,
- TypeInfo = ImagePartType.Png // Default type, actual type doesn't matter for external links
- };
- }
-
- ///
- /// Parse the Data inline image.
- ///
- private HtmlImageInfo? ReadDataUri(string src)
- {
- if (DataUri.TryCreate(src, out var dataUri))
- {
- knownContentType.TryGetValue(dataUri!.Mime, out PartTypeInfo type);
-
- return SaveImageAssert(src, type, stream => stream.Write(dataUri.Data, 0, dataUri.Data.Length));
- }
-
- return null;
- }
-
- private HtmlImageInfo SaveImageAssert(string src, PartTypeInfo type, Action writeImage)
- {
- ImagePart ipart;
- string relationshipId = "img_" + Guid.NewGuid().ToString("N");
- lock (lockObject)
- {
- ipart = hostingPart.AddImagePart(type, relationshipId);
- }
-
- Size originalSize;
- using (var outputStream = ipart.GetStream(FileMode.Create))
- {
- writeImage(outputStream);
- outputStream.Seek(0L, SeekOrigin.Begin);
- originalSize = GetImageSize(outputStream);
- }
-
- string partId = hostingPart.GetIdOfPart(ipart);
- return new HtmlImageInfo(src, partId)
- {
- TypeInfo = type,
- Size = originalSize
- };
- }
-
- //____________________________________________________________________
- //
- // Private Implementation
-
- // http://stackoverflow.com/questions/58510/using-net-how-can-you-find-the-mime-type-of-a-file-based-on-the-file-signature
- private static readonly Dictionary knownContentType = new(StringComparer.OrdinalIgnoreCase) {
- { "image/gif", ImagePartType.Gif },
- { "image/pjpeg", ImagePartType.Jpeg },
- { "image/jp2", ImagePartType.Jp2 },
- { "image/jpg", ImagePartType.Jpeg },
- { "image/jpeg", ImagePartType.Jpeg },
- { "image/x-png", ImagePartType.Png },
- { "image/png", ImagePartType.Png },
- { "image/tiff", ImagePartType.Tiff },
- { "image/emf", ImagePartType.Emf },
- { "image/x-emf", ImagePartType.Emf },
- { "image/vnd.microsoft.icon", ImagePartType.Icon },
- // these icons mime type are wrong but we should nevertheless take care (http://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type)
- { "image/x-icon", ImagePartType.Icon },
- { "image/icon", ImagePartType.Icon },
- { "image/ico", ImagePartType.Icon },
- { "text/ico", ImagePartType.Icon },
- { "text/application-ico", ImagePartType.Icon },
- { "image/bmp", ImagePartType.Bmp },
- { "image/svg+xml", ImagePartType.Svg },
- };
-
- ///
- /// Inspect the response headers of a web request and decode the mime type if provided
- ///
- /// Returns the extension of the image if provideds.
- private static bool TryInspectMimeType(string? contentType, out PartTypeInfo type)
- {
- // can be null when the protocol used doesn't allow response headers
- if (contentType != null &&
- knownContentType.TryGetValue(contentType, out type))
- return true;
-
- type = default;
- return false;
- }
-
- ///
- /// Gets the OpenXml PartTypeInfo associated to an image.
- ///
- private static bool TryGuessTypeFromUri(Uri uri, out PartTypeInfo type)
- {
- string extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.Segments[uri.Segments.Length - 1] : uri.OriginalString);
- if (knownExtensions.TryGetValue(extension, out type)) return true;
-
- // extension not recognized, try with checking the query string. Expecting to resolve something like:
- // ./image.axd?picture=img1.jpg
- extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString());
- if (knownExtensions.TryGetValue(extension, out type)) return true;
-
- return false;
- }
-
- ///
- /// Gets the OpenXml PartTypeInfo associated to an image.
- ///
- private static bool TryGuessTypeFromStream(Stream stream, out PartTypeInfo type)
- {
- if (ImageHeader.TryDetectFileType(stream, out ImageHeader.FileType guessType))
- {
- switch (guessType)
- {
- case ImageHeader.FileType.Bitmap: type = ImagePartType.Bmp; return true;
- case ImageHeader.FileType.Emf: type = ImagePartType.Emf; return true;
- case ImageHeader.FileType.Gif: type = ImagePartType.Gif; return true;
- case ImageHeader.FileType.Jpeg: type = ImagePartType.Jpeg; return true;
- case ImageHeader.FileType.Png: type = ImagePartType.Png; return true;
- }
- }
- type = ImagePartType.Bmp;
- return false;
- }
-
- ///
- /// Loads an image from a stream and grab its size.
- ///
- private static Size GetImageSize(Stream imageStream)
- {
- // Read only the size of the image
- try
- {
- return ImageHeader.GetDimensions(imageStream);
- }
- catch (ArgumentException)
- {
- return Size.Empty;
- }
- }
-}
+/* 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 DocumentFormat.OpenXml.Packaging;
+
+namespace HtmlToOpenXml.IO;
+
+interface IImageLoader
+{
+ ///
+ /// Download the remote or local image located at the specified url.
+ ///
+ Task Download(string imageUri, CancellationToken cancellationToken);
+}
+
+///
+/// Download and provison the metadata of a requested image.
+///
+sealed class ImagePrefetcher : IImageLoader
+ where T: OpenXmlPartContainer, ISupportedRelationship
+{
+ // Map extension to PartTypeInfo
+ private static readonly Dictionary knownExtensions = new(StringComparer.OrdinalIgnoreCase) {
+ { ".gif", ImagePartType.Gif },
+ { ".bmp", ImagePartType.Bmp },
+ { ".emf", ImagePartType.Emf },
+ { ".ico", ImagePartType.Icon },
+ { ".jp2", ImagePartType.Jp2 },
+ { ".jpeg", ImagePartType.Jpeg },
+ { ".jpg", ImagePartType.Jpeg },
+ { ".jpe", ImagePartType.Jpeg },
+ { ".pcx", ImagePartType.Pcx },
+ { ".png", ImagePartType.Png },
+ { ".svg", ImagePartType.Svg },
+ { ".tif", ImagePartType.Tif },
+ { ".tiff", ImagePartType.Tiff },
+ { ".wmf", ImagePartType.Wmf }
+ };
+ private readonly T hostingPart;
+ private readonly IWebRequest resourceLoader;
+ private readonly HtmlImageInfoCollection prefetchedImages;
+ private readonly object lockObject = new();
+ private readonly ImageProcessingMode processingMode;
+
+
+ ///
+ /// Constructor.
+ ///
+ /// The image will be linked to that hosting part.
+ /// Images are not shared between header, footer and body.
+ /// Service to resolve an image.
+ /// Specifies how images should be processed (embed, link, or data URI only).
+ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessingMode processingMode = ImageProcessingMode.Embed)
+ {
+ this.hostingPart = hostingPart;
+ this.resourceLoader = resourceLoader;
+ this.processingMode = processingMode;
+ this.prefetchedImages = [];
+ }
+
+ //____________________________________________________________________
+ //
+ // Public Functionality
+
+ ///
+ /// Download the remote or local image located at the specified url.
+ ///
+ public async Task Download(string imageUri, CancellationToken cancellationToken)
+ {
+ // Check if image is already cached using thread-safe operation
+ lock (lockObject)
+ {
+ if (prefetchedImages.Contains(imageUri))
+ return prefetchedImages[imageUri];
+ }
+
+ HtmlImageInfo? iinfo;
+ if (DataUri.IsWellFormed(imageUri)) // data inline, encoded in base64
+ {
+ iinfo = ReadDataUri(imageUri);
+ }
+ else
+ {
+ // Handle external images based on processing mode
+ if (processingMode == ImageProcessingMode.EmbedDataUriOnly)
+ {
+ // Skip external images entirely
+ return null;
+ }
+ else if (processingMode == ImageProcessingMode.LinkExternal)
+ {
+ // Create external link without downloading
+ iinfo = CreateExternalImageLink(imageUri);
+ }
+ else
+ {
+ // Default: Download and embed
+ iinfo = await DownloadRemoteImage(imageUri, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ // Add to cache using thread-safe operation
+ if (iinfo != null)
+ {
+ lock (lockObject)
+ {
+ // Double-check pattern to prevent duplicate adds during concurrent access
+ if (!prefetchedImages.Contains(imageUri))
+ {
+ prefetchedImages.Add(iinfo);
+ }
+ }
+ }
+
+ return iinfo;
+ }
+
+ ///
+ /// Download the image and try to find its format type.
+ ///
+ private async Task DownloadRemoteImage(string src, CancellationToken cancellationToken)
+ {
+ Uri imageUri = new(src, UriKind.RelativeOrAbsolute);
+ if (imageUri.IsAbsoluteUri && !resourceLoader.SupportsProtocol(imageUri.Scheme))
+ return null;
+
+ using var response = await resourceLoader.FetchAsync(imageUri, cancellationToken).ConfigureAwait(false);
+ if (response?.Content == null || !response.Content.CanRead)
+ return null;
+
+ // For requested url with no filename, we need to read the media mime type if provided
+ response.Headers.TryGetValue("Content-Type", out var mime);
+ if (!TryInspectMimeType(mime, out PartTypeInfo type)
+ && !TryGuessTypeFromUri(imageUri, out type)
+ && !TryGuessTypeFromStream(response.Content, out type)
+ )
+ {
+ return null;
+ }
+
+ return SaveImageAssert(src, type, response.Content.CopyTo);
+ }
+
+ ///
+ /// Create an external relationship to an image without downloading it.
+ ///
+ private HtmlImageInfo? CreateExternalImageLink(string src)
+ {
+ Uri imageUri = new(src, UriKind.RelativeOrAbsolute);
+
+ // Resolve relative URIs if possible (only for DefaultWebRequest which has BaseImageUrl)
+ if (!imageUri.IsAbsoluteUri && resourceLoader is DefaultWebRequest defaultWebRequest
+ && defaultWebRequest.BaseImageUrl != null)
+ {
+ string url1 = defaultWebRequest.BaseImageUrl.AbsoluteUri.TrimEnd('/', '\\');
+ string path = src.TrimStart('/', '\\');
+ imageUri = new Uri(string.Format("{0}/{1}", url1, path), UriKind.Absolute);
+ }
+
+ // Only create external links for absolute URIs with supported protocols
+ if (!imageUri.IsAbsoluteUri || !resourceLoader.SupportsProtocol(imageUri.Scheme))
+ return null;
+
+ // Generate a unique GUID-based relationship ID for the external relationship
+ string relationshipId = "imgext_" + Guid.NewGuid().ToString("N");
+
+ // Create external relationship
+ lock (lockObject)
+ {
+ hostingPart.AddExternalRelationship(
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
+ imageUri,
+ relationshipId);
+ }
+
+ // Return image info with external flag set
+ // Note: Size will be empty as we don't download the image
+ return new HtmlImageInfo(src, relationshipId) {
+ IsExternal = true,
+ Size = Size.Empty,
+ TypeInfo = ImagePartType.Png // Default type, actual type doesn't matter for external links
+ };
+ }
+
+ ///
+ /// Parse the Data inline image.
+ ///
+ private HtmlImageInfo? ReadDataUri(string src)
+ {
+ if (DataUri.TryCreate(src, out var dataUri))
+ {
+ knownContentType.TryGetValue(dataUri!.Mime, out PartTypeInfo type);
+
+ return SaveImageAssert(src, type, stream => stream.Write(dataUri.Data, 0, dataUri.Data.Length));
+ }
+
+ return null;
+ }
+
+ private HtmlImageInfo SaveImageAssert(string src, PartTypeInfo type, Action writeImage)
+ {
+ ImagePart ipart;
+ string relationshipId = "img_" + Guid.NewGuid().ToString("N");
+ lock (lockObject)
+ {
+ ipart = hostingPart.AddImagePart(type, relationshipId);
+ }
+
+ Size originalSize;
+ using (var outputStream = ipart.GetStream(FileMode.Create))
+ {
+ writeImage(outputStream);
+ outputStream.Seek(0L, SeekOrigin.Begin);
+ originalSize = GetImageSize(outputStream);
+ }
+
+ string partId = hostingPart.GetIdOfPart(ipart);
+ return new HtmlImageInfo(src, partId)
+ {
+ TypeInfo = type,
+ Size = originalSize
+ };
+ }
+
+ //____________________________________________________________________
+ //
+ // Private Implementation
+
+ // http://stackoverflow.com/questions/58510/using-net-how-can-you-find-the-mime-type-of-a-file-based-on-the-file-signature
+ private static readonly Dictionary knownContentType = new(StringComparer.OrdinalIgnoreCase) {
+ { "image/gif", ImagePartType.Gif },
+ { "image/pjpeg", ImagePartType.Jpeg },
+ { "image/jp2", ImagePartType.Jp2 },
+ { "image/jpg", ImagePartType.Jpeg },
+ { "image/jpeg", ImagePartType.Jpeg },
+ { "image/x-png", ImagePartType.Png },
+ { "image/png", ImagePartType.Png },
+ { "image/tiff", ImagePartType.Tiff },
+ { "image/emf", ImagePartType.Emf },
+ { "image/x-emf", ImagePartType.Emf },
+ { "image/vnd.microsoft.icon", ImagePartType.Icon },
+ // these icons mime type are wrong but we should nevertheless take care (http://en.wikipedia.org/wiki/ICO_%28file_format%29#MIME_type)
+ { "image/x-icon", ImagePartType.Icon },
+ { "image/icon", ImagePartType.Icon },
+ { "image/ico", ImagePartType.Icon },
+ { "text/ico", ImagePartType.Icon },
+ { "text/application-ico", ImagePartType.Icon },
+ { "image/bmp", ImagePartType.Bmp },
+ { "image/svg+xml", ImagePartType.Svg },
+ };
+
+ ///
+ /// Inspect the response headers of a web request and decode the mime type if provided
+ ///
+ /// Returns the extension of the image if provideds.
+ private static bool TryInspectMimeType(string? contentType, out PartTypeInfo type)
+ {
+ // can be null when the protocol used doesn't allow response headers
+ if (contentType != null &&
+ knownContentType.TryGetValue(contentType, out type))
+ return true;
+
+ type = default;
+ return false;
+ }
+
+ ///
+ /// Gets the OpenXml PartTypeInfo associated to an image.
+ ///
+ private static bool TryGuessTypeFromUri(Uri uri, out PartTypeInfo type)
+ {
+ string extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.Segments[uri.Segments.Length - 1] : uri.OriginalString);
+ if (knownExtensions.TryGetValue(extension, out type)) return true;
+
+ // extension not recognized, try with checking the query string. Expecting to resolve something like:
+ // ./image.axd?picture=img1.jpg
+ extension = Path.GetExtension(uri.IsAbsoluteUri ? uri.AbsoluteUri : uri.ToString());
+ if (knownExtensions.TryGetValue(extension, out type)) return true;
+
+ return false;
+ }
+
+ ///
+ /// Gets the OpenXml PartTypeInfo associated to an image.
+ ///
+ private static bool TryGuessTypeFromStream(Stream stream, out PartTypeInfo type)
+ {
+ if (ImageHeader.TryDetectFileType(stream, out ImageHeader.FileType guessType))
+ {
+ switch (guessType)
+ {
+ case ImageHeader.FileType.Bitmap: type = ImagePartType.Bmp; return true;
+ case ImageHeader.FileType.Emf: type = ImagePartType.Emf; return true;
+ case ImageHeader.FileType.Gif: type = ImagePartType.Gif; return true;
+ case ImageHeader.FileType.Jpeg: type = ImagePartType.Jpeg; return true;
+ case ImageHeader.FileType.Png: type = ImagePartType.Png; return true;
+ }
+ }
+ type = ImagePartType.Bmp;
+ return false;
+ }
+
+ ///
+ /// Loads an image from a stream and grab its size.
+ ///
+ private static Size GetImageSize(Stream imageStream)
+ {
+ // Read only the size of the image
+ try
+ {
+ return ImageHeader.GetDimensions(imageStream);
+ }
+ catch (ArgumentException)
+ {
+ return Size.Empty;
+ }
+ }
+}
diff --git a/src/Html2OpenXml/IO/Resource.cs b/src/Html2OpenXml/IO/Resource.cs
index f0110619..bdf30e49 100755
--- a/src/Html2OpenXml/IO/Resource.cs
+++ b/src/Html2OpenXml/IO/Resource.cs
@@ -9,9 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.IO;
using System.Net;
namespace HtmlToOpenXml.IO;
diff --git a/src/Html2OpenXml/IO/SequentialBinaryReader.cs b/src/Html2OpenXml/IO/SequentialBinaryReader.cs
index efec891d..c912218a 100644
--- a/src/Html2OpenXml/IO/SequentialBinaryReader.cs
+++ b/src/Html2OpenXml/IO/SequentialBinaryReader.cs
@@ -12,7 +12,6 @@
* Inspiration from Metadata Extractor (Drew Noakes):
* https://github.com/drewnoakes/metadata-extractor-dotnet
*/
-using System.IO;
namespace HtmlToOpenXml.IO;
diff --git a/src/Html2OpenXml/ParsingContext.cs b/src/Html2OpenXml/ParsingContext.cs
index c75a3a65..b168810e 100644
--- a/src/Html2OpenXml/ParsingContext.cs
+++ b/src/Html2OpenXml/ParsingContext.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System.Collections.Generic;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using HtmlToOpenXml.Expressions;
diff --git a/src/Html2OpenXml/Primitives/HtmlColor.Named.cs b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs
old mode 100644
new mode 100755
index 37a42fb9..b8301893
--- a/src/Html2OpenXml/Primitives/HtmlColor.Named.cs
+++ b/src/Html2OpenXml/Primitives/HtmlColor.Named.cs
@@ -1,8 +1,6 @@
-using System;
#if NET5_0_OR_GREATER
using System.Collections.Frozen;
#endif
-using System.Collections.Generic;
namespace HtmlToOpenXml;
diff --git a/src/Html2OpenXml/Primitives/HtmlColor.cs b/src/Html2OpenXml/Primitives/HtmlColor.cs
index 2879aede..1076280b 100755
--- a/src/Html2OpenXml/Primitives/HtmlColor.cs
+++ b/src/Html2OpenXml/Primitives/HtmlColor.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using System.Globalization;
namespace HtmlToOpenXml;
@@ -44,78 +43,116 @@ 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
+ span = span.Trim();
+ if (span.Length < 3)
+ return Empty;
+
try
{
- if (htmlColor!.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
+ // Is it in hexa? Note: we no more accept hexa value without preceding the '#'
+ if (span[0] == '#')
{
- 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 ParseHexa(span);
}
- // HSL or HSLA
- if (htmlColor.StartsWith("hsl", StringComparison.OrdinalIgnoreCase))
+ // RGB or RGBA
+ if (span.StartsWith(['r','g','b'], StringComparison.OrdinalIgnoreCase))
{
- 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])
- );
- }
+ return ParseRgb(span);
}
- // Is it in hexa? Note: we no more accept hexa value without preceding the '#'
- if (htmlColor[0] == '#' && (htmlColor.Length == 7 || htmlColor.Length == 4))
+ // HSL or HSLA
+ if (span.StartsWith(['h','s','l'], StringComparison.OrdinalIgnoreCase))
{
- 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
- 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));
+ return ParseHsl(span);
}
}
catch (Exception exc)
{
if (exc is FormatException || exc is OverflowException || exc is ArgumentOutOfRangeException)
- return HtmlColor.Empty;
+ return Empty;
throw;
}
- return GetNamedColor(htmlColor.AsSpan());
+ return GetNamedColor(span);
+ }
+
+ private static HtmlColor ParseHexa(ReadOnlySpan span)
+ {
+ if (span.Length == 7)
+ {
+ return FromArgb(
+ span.Slice(1, 2).AsByte(NumberStyles.HexNumber),
+ span.Slice(3, 2).AsByte(NumberStyles.HexNumber),
+ span.Slice(5, 2).AsByte(NumberStyles.HexNumber));
+ }
+ if (span.Length == 4)
+ {
+ // #0FF --> #00FFFF
+ ReadOnlySpan r = [span[1], span[1]];
+ ReadOnlySpan g = [span[2], span[2]];
+ ReadOnlySpan b = [span[3], span[3]];
+ return FromArgb(
+ r.AsByte(NumberStyles.HexNumber),
+ g.AsByte(NumberStyles.HexNumber),
+ b.AsByte(NumberStyles.HexNumber));
+ }
+ return Empty;
+ }
+
+ private static HtmlColor ParseRgb(ReadOnlySpan span)
+ {
+ 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
+ };
+ }
+
+ private static HtmlColor ParseHsl(ReadOnlySpan span)
+ {
+ 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
+ };
}
///
@@ -220,21 +257,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..4fb945e3 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,114 @@ 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;
- // 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 +141,7 @@ public string? Family
}
///
- /// Gest or sets the style for the text.
+ /// Gest the style for the text.
///
public FontStyle? Style
{
@@ -102,7 +149,7 @@ public FontStyle? Style
}
///
- /// Gets or sets the variation of the characters.
+ /// Gets the variation of the characters.
///
public FontVariant? Variant
{
@@ -110,7 +157,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 +165,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..5d562648 100755
--- a/src/Html2OpenXml/Primitives/Margin.cs
+++ b/src/Html2OpenXml/Primitives/Margin.cs
@@ -13,16 +13,53 @@
namespace HtmlToOpenXml;
///
-/// Represents a Html Unit (ie: 120px, 10em, ...).
+/// Represents a Html Margin.
///
struct Margin
{
- private Unit[] sides;
+ /// Represents an empty margin (not defined).
+ public static readonly Margin Empty = new();
+ private Unit top;
+ private Unit right;
+ private Unit bottom;
+ private Unit left;
+
+ /// Apply to all four sides.
+ public Margin(Unit all)
+ {
+ this.top = all;
+ this.right = all;
+ this.bottom = all;
+ this.left = all;
+ }
+
+ /// Top and bottom | left and right.
+ public Margin(Unit topAndBottom, Unit leftAndRight)
+ {
+ this.top = topAndBottom;
+ this.bottom = topAndBottom;
+ this.left = leftAndRight;
+ this.right = leftAndRight;
+ }
+
+ /// Top | left and right | bottom.
+ public Margin(Unit top, Unit leftAndRight, Unit bottom)
+ {
+ this.top = top;
+ this.right = leftAndRight;
+ this.bottom = bottom;
+ this.left = leftAndRight;
+ }
+
+ /// Top | right | bottom | left.
public Margin(Unit top, Unit right, Unit bottom, Unit left)
{
- this.sides = [top, right, bottom, left];
+ this.top = top;
+ this.right = right;
+ this.bottom = bottom;
+ this.left = left;
}
///
@@ -48,47 +85,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 +118,8 @@ private void EnsureSides()
///
public Unit Bottom
{
- readonly get { return sides == null ? Unit.Empty : sides[2]; }
- set { EnsureSides(); sides[2] = value; }
+ readonly get => bottom;
+ set => bottom = value;
}
///
@@ -108,8 +127,8 @@ public Unit Bottom
///
public Unit Left
{
- readonly get { return sides == null ? Unit.Empty : sides[3]; }
- set { EnsureSides(); sides[3] = value; }
+ readonly get => left;
+ set => left = value;
}
///
@@ -117,8 +136,8 @@ public Unit Left
///
public Unit Top
{
- readonly get { return sides == null ? Unit.Empty : sides[0]; }
- set { EnsureSides(); sides[0] = value; }
+ readonly get => top;
+ set => top = value;
}
///
@@ -126,13 +145,13 @@ public Unit Top
///
public Unit Right
{
- readonly get { return sides == null ? Unit.Empty : sides[1]; }
- set { EnsureSides(); sides[1] = value; }
+ readonly get => right;
+ set => right = 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 +159,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..ef94da00 100755
--- a/src/Html2OpenXml/Primitives/SideBorder.cs
+++ b/src/Html2OpenXml/Primitives/SideBorder.cs
@@ -9,15 +9,12 @@
* 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;
///
-/// 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)
{
@@ -28,19 +25,20 @@ readonly struct SideBorder(BorderValues style, HtmlColor color, Unit size)
private readonly HtmlColor color = color;
private readonly Unit size = size;
- public static SideBorder Parse(string? str)
+ public static SideBorder Parse(ReadOnlySpan span)
{
- if (str == null) return SideBorder.Empty;
-
// 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 +46,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 +84,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..e54b1161 100755
--- a/src/Html2OpenXml/Primitives/Unit.cs
+++ b/src/Html2OpenXml/Primitives/Unit.cs
@@ -9,15 +9,13 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* 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 +23,98 @@ 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;
- }
- catch (FormatException)
- {
- return Unit.Empty;
+ return Empty;
}
- catch (ArithmeticException)
+ catch (Exception)
{
- 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);
}
///
/// 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 +141,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 +157,7 @@ public Double Value
///
/// Gets the value expressed in English Metrics Unit.
///
- public Int64 ValueInEmus
+ public long ValueInEmus
{
get { return valueInEmus; }
}
@@ -154,9 +165,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 +175,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 +183,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 +201,7 @@ public double ValueInEighthPoint
///
public bool IsValid
{
- get { return this.Type != UnitMetric.Unknown; }
+ get { return this.Metric != UnitMetric.Unknown; }
}
///
@@ -198,6 +209,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/Properties/FxCopRules.cs b/src/Html2OpenXml/Properties/FxCopRules.cs
index 9f1ca2b6..1d7e1bfe 100755
--- a/src/Html2OpenXml/Properties/FxCopRules.cs
+++ b/src/Html2OpenXml/Properties/FxCopRules.cs
@@ -3,5 +3,3 @@
[module: SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Scope = "member", Target = "HtmlToOpenXml.UnitMetric.#Ex", Justification = "Ex stands for a W3C compliant unit metric")]
[module: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Scope = "member", Target = "HtmlToOpenXml.Unit.#ValueInDxa", MessageId = "Dxa", Justification = "Dxa is a standardized unit metric")]
[module: SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Scope = "member", Target = "HtmlToOpenXml.Unit.#ValueInPx", MessageId = "Px", Justification = "Px is a standardized unit metric")]
-
-[module: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Scope = "member", Target = "HtmlToOpenXml.HtmlConverter.#ConsiderDivAsParagraph", MessageId = "DivAs", Justification = "DivAs stands for 'Html Div as'")]
diff --git a/src/Html2OpenXml/StyleEventArgs.cs b/src/Html2OpenXml/StyleEventArgs.cs
index 11580821..9f090952 100755
--- a/src/Html2OpenXml/StyleEventArgs.cs
+++ b/src/Html2OpenXml/StyleEventArgs.cs
@@ -9,7 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using DocumentFormat.OpenXml.Wordprocessing;
namespace HtmlToOpenXml;
diff --git a/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs b/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs
index 085f7d4c..4caed8b9 100644
--- a/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs
+++ b/src/Html2OpenXml/Utilities/AngleSharpExtensions.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Runtime.CompilerServices;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
@@ -84,7 +82,7 @@ public static bool IsPrecededByListElement(this INode child, out IElement? prece
/// Inline data in would returns .
public static bool TryParseUrl(string? uriString, UriKind uriKind,
#if NET5_0_OR_GREATER
- [System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
+ [System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
#endif
out Uri? result)
{
diff --git a/src/Html2OpenXml/Utilities/CollectionExtensions.cs b/src/Html2OpenXml/Utilities/CollectionExtensions.cs
index 42aa8d2a..1c878bfb 100644
--- a/src/Html2OpenXml/Utilities/CollectionExtensions.cs
+++ b/src/Html2OpenXml/Utilities/CollectionExtensions.cs
@@ -9,10 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
namespace HtmlToOpenXml;
diff --git a/src/Html2OpenXml/Utilities/Converter.cs b/src/Html2OpenXml/Utilities/Converter.cs
index daf75fb7..9d5907d2 100755
--- a/src/Html2OpenXml/Utilities/Converter.cs
+++ b/src/Html2OpenXml/Utilities/Converter.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using System.Globalization;
using DocumentFormat.OpenXml.Wordprocessing;
@@ -55,42 +53,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 +98,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 +112,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 +127,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 +159,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 +167,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/HttpUtility.cs b/src/Html2OpenXml/Utilities/HttpUtility.cs
index 13d6df2c..c8de0424 100755
--- a/src/Html2OpenXml/Utilities/HttpUtility.cs
+++ b/src/Html2OpenXml/Utilities/HttpUtility.cs
@@ -9,11 +9,8 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
using System.Text;
-using System.IO;
using System.Globalization;
-using System.Collections.Generic;
namespace HtmlToOpenXml;
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..55078a5c
--- /dev/null
+++ b/src/Html2OpenXml/Utilities/SpanExtensions.cs
@@ -0,0 +1,221 @@
+/* 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.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/src/Html2OpenXml/WordDocumentStyle.cs b/src/Html2OpenXml/WordDocumentStyle.cs
index dd974c4d..3059efca 100755
--- a/src/Html2OpenXml/WordDocumentStyle.cs
+++ b/src/Html2OpenXml/WordDocumentStyle.cs
@@ -9,8 +9,6 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
-using System;
-using System.Collections.Generic;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
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 31c2bd53..3c5106d9 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 efb64094..470338de 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].Elements().Count, Is.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);
}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs b/test/HtmlToOpenXml.Tests/Primitives/ColorTests.cs
index 2b891847..5ad852d6 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);
+ var color = HtmlColor.Parse(htmlColor.AsSpan());
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.AsSpan());
+ 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..8e7a7fe9
--- /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.AsSpan());
+ 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.AsSpan());
+ 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".AsSpan());
+ 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".AsSpan());
+ 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".AsSpan());
+ 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..1429af54 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/MarginTests.cs
@@ -12,12 +12,13 @@ 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);
+ var margin = Margin.Parse(html.AsSpan());
Assert.Multiple(() => {
- Assert.That(margin.IsValid, Is.EqualTo(true));
+ Assert.That(margin.IsValid, Is.True);
Assert.That(margin.Top.ValueInPx, Is.EqualTo(top));
Assert.That(margin.Right.ValueInPx, Is.EqualTo(right));
Assert.That(margin.Bottom.ValueInPx, Is.EqualTo(bottom));
@@ -28,25 +29,25 @@ public void ParseHtmlString_ShouldSucceed (string html, int top, int right, int
[Test]
public void ParseWithFloat_ShouldSucceed ()
{
- var margin = Margin.Parse("0 50% 9.5pt .00001pt");
+ var margin = Margin.Parse("0 50% 9.5pt .00001pt".AsSpan());
Assert.Multiple(() => {
- Assert.That(margin.IsValid, Is.EqualTo(true));
+ Assert.That(margin.IsValid, Is.True);
- Assert.That(margin.Top.Value, Is.EqualTo(0));
- Assert.That(margin.Top.Type, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(margin.Top.Value, Is.Zero);
+ 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"));
});
@@ -55,19 +56,19 @@ public void ParseWithFloat_ShouldSucceed ()
[Test]
public void ParseWithAuto_ShouldSucceed ()
{
- var margin = Margin.Parse("0 auto");
+ var margin = Margin.Parse("0 auto".AsSpan());
Assert.Multiple(() => {
- Assert.That(margin.IsValid, Is.EqualTo(true));
+ Assert.That(margin.IsValid, Is.True);
- Assert.That(margin.Top.Value, Is.EqualTo(0));
- Assert.That(margin.Top.Type, Is.EqualTo(UnitMetric.Pixel));
+ Assert.That(margin.Top.Value, Is.Zero);
+ 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.Value, Is.Zero);
+ 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/SideBorderTests.cs b/test/HtmlToOpenXml.Tests/Primitives/SideBorderTests.cs
index 4c1cd5fa..b404eea8 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/SideBorderTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/SideBorderTests.cs
@@ -14,7 +14,7 @@ public class SideBorderTests
[TestCase("thin dotted white", "dotted", 255, 255, 255)]
public void ParseHtmlBorder_ShouldSucceed(string htmlBorder, string borderStyle, byte red, byte green, byte blue)
{
- var border = SideBorder.Parse(htmlBorder);
+ var border = SideBorder.Parse(htmlBorder.AsSpan());
Assert.Multiple(() => {
Assert.That(border.IsValid, Is.True);
@@ -29,14 +29,14 @@ public void ParseHtmlBorder_ShouldSucceed(string htmlBorder, string borderStyle,
[TestCase("abc")]
public void InvalidBorder_ShouldFail(string htmlBorder)
{
- var border = SideBorder.Parse(htmlBorder);
+ var border = SideBorder.Parse(htmlBorder.AsSpan());
Assert.That(border.IsValid, Is.False);
}
[Test]
public void Border_ShouldSucceed()
{
- var border = SideBorder.Parse("3px solid black");
+ var border = SideBorder.Parse("3px solid black".AsSpan());
Assert.That(border.IsValid, Is.True);
Assert.That(border.Width.ValueInPx, Is.EqualTo(3));
Assert.That(border.Width.ValueInPoint, Is.EqualTo(2.25));
diff --git a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
new file mode 100644
index 00000000..73f02e82
--- /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([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..26d90392
--- /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.AsSpan());
+
+ 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.AsSpan());
+ Assert.That(unit.IsValid, Is.False);
+ }
+ }
+}
diff --git a/test/HtmlToOpenXml.Tests/StyleTests.cs b/test/HtmlToOpenXml.Tests/StyleTests.cs
index d9228a26..855b071c 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 = HtmlColor.Parse("red").ToHexString() }
+ Color = new() { Val = HtmlColor.Parse("red".AsSpan()).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);