diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 85a9e28b..282d4cf3 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -5,14 +5,19 @@ name: 'dotnet.yml'
on:
push:
- branches: [ "dev" ]
+ branches:
+ - dev
paths-ignore:
- 'docs/**'
- '**/*.md'
pull_request:
+ branches:
+ - master
+ - dev
jobs:
- net5_above:
+ net:
+ if: github.event_name != 'push' || github.event.pull_request == null
runs-on: ubuntu-latest
defaults:
run:
@@ -26,7 +31,7 @@ jobs:
uses: actions/setup-dotnet@v5
with:
dotnet-version: ${{ matrix.dotnet-version }}.x
- - uses: actions/cache@v4
+ - uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.sln') }}
@@ -39,7 +44,8 @@ jobs:
- name: Run tests
run: dotnet test --framework net${{ matrix.dotnet-version }} --configuration Release --no-build --verbosity normal
- net462:
+ netfx462:
+ if: github.event_name != 'push' || github.event.pull_request == null
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
diff --git a/.gitignore b/.gitignore
index 8dd4607a..31b666d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -383,6 +383,7 @@ FodyWeavers.xsd
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
+*.csproj.lscache
# Local History for Visual Studio Code
.history/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a551e9f8..24566d87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 3.4.0
+
+- Numbering list now support `list-style-type: dash`
+- Add support for custom bullet symbols `list-style-type: '👍'` thanks to @AlexAbd1990
+- Minor internal optimisations
+
## 3.3.2
- Supports Greek numbering in ordered lists (upper-greek / lower-greek) #227
diff --git a/Directory.Build.props b/Directory.Build.props
index 4a5bbb38..df3b3bed 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,10 +4,10 @@
enable
latest
enable
- 3.3.2
+ 3.4.0
- 3.4.1
+ 3.5.1
\ No newline at end of file
diff --git a/HtmlToOpenXml.sln b/HtmlToOpenXml.sln
deleted file mode 100644
index aa68e297..00000000
--- a/HtmlToOpenXml.sln
+++ /dev/null
@@ -1,54 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.8.34511.84
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlToOpenXml", "src\Html2OpenXml\HtmlToOpenXml.csproj", "{EF700F30-C9BB-49A6-912C-E3B77857B514}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{58520A98-BA53-4BA4-AAE3-786AA21331D6}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{84EA02ED-2E97-47D2-992E-32CC104A3A7A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "examples\Demo\Demo.csproj", "{A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlToOpenXml.Tests", "test\HtmlToOpenXml.Tests\HtmlToOpenXml.Tests.csproj", "{CA0A68E0-45A0-4A01-A061-F951D93D6906}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "examples\Benchmark\Benchmark.csproj", "{143A3684-FAEB-43D0-A895-09BE5FDF85F6}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {EF700F30-C9BB-49A6-912C-E3B77857B514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EF700F30-C9BB-49A6-912C-E3B77857B514}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EF700F30-C9BB-49A6-912C-E3B77857B514}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EF700F30-C9BB-49A6-912C-E3B77857B514}.Release|Any CPU.Build.0 = Release|Any CPU
- {A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F}.Release|Any CPU.Build.0 = Release|Any CPU
- {CA0A68E0-45A0-4A01-A061-F951D93D6906}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CA0A68E0-45A0-4A01-A061-F951D93D6906}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CA0A68E0-45A0-4A01-A061-F951D93D6906}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CA0A68E0-45A0-4A01-A061-F951D93D6906}.Release|Any CPU.Build.0 = Release|Any CPU
- {143A3684-FAEB-43D0-A895-09BE5FDF85F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {143A3684-FAEB-43D0-A895-09BE5FDF85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {143A3684-FAEB-43D0-A895-09BE5FDF85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {143A3684-FAEB-43D0-A895-09BE5FDF85F6}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {EF700F30-C9BB-49A6-912C-E3B77857B514} = {58520A98-BA53-4BA4-AAE3-786AA21331D6}
- {A1ECC760-B9F7-4A00-AF5F-568B5FD6F09F} = {84EA02ED-2E97-47D2-992E-32CC104A3A7A}
- {CA0A68E0-45A0-4A01-A061-F951D93D6906} = {84EA02ED-2E97-47D2-992E-32CC104A3A7A}
- {143A3684-FAEB-43D0-A895-09BE5FDF85F6} = {84EA02ED-2E97-47D2-992E-32CC104A3A7A}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {14EE1026-6507-4295-9FEE-67A55C3849CE}
- SolutionGuid = {194D4CBE-A20A-4E32-967B-E1BBD3922C29}
- EndGlobalSection
-EndGlobal
diff --git a/HtmlToOpenXml.slnx b/HtmlToOpenXml.slnx
new file mode 100644
index 00000000..f1a0880a
--- /dev/null
+++ b/HtmlToOpenXml.slnx
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Demo/Program.cs b/examples/Demo/Program.cs
index d1ee82e7..fccd4693 100644
--- a/examples/Demo/Program.cs
+++ b/examples/Demo/Program.cs
@@ -12,7 +12,7 @@ static class Program
static async Task Main(string[] args)
{
const string filename = "test.docx";
- string html = ResourceHelper.GetString("Resources.CompleteRunTest.html");
+ string html = ResourceHelper.GetString("Resources.UlStyles.html");
if (File.Exists(filename)) File.Delete(filename);
using (MemoryStream generatedDocument = new())
diff --git a/examples/Demo/Resources/UlStyles.html b/examples/Demo/Resources/UlStyles.html
new file mode 100644
index 00000000..bfe2fc69
--- /dev/null
+++ b/examples/Demo/Resources/UlStyles.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
index f1492673..340ae61d 100755
--- a/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
+++ b/src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
@@ -73,7 +73,7 @@ public static HtmlAttributeCollection ParseStyle(string? htmlStyles)
}
else if (separator == ':' && !foundKey)
{
- key = span.Slice(0, index).Trim().ToString();
+ key = span.Slice(0, index).Trim().ToString().ToLowerInvariant();
foundKey = true;
index++;
}
@@ -87,7 +87,7 @@ public static HtmlAttributeCollection ParseStyle(string? htmlStyles)
}
else if (!foundKey && span.Slice(index).StartsWith(['&','#','5','8',';']))
{
- key = span.Slice(0, index).Trim().ToString();
+ key = span.Slice(0, index).Trim().ToString().ToLowerInvariant();
foundKey = true;
index += 5; // length of ":"
}
@@ -120,9 +120,15 @@ public ReadOnlySpan this[string name]
///
/// Determines whether the collection contains the specified key.
///
- public bool ContainsKey(string name)
+ public bool TryGetValue(string name, out ReadOnlySpan value)
{
- return attributes.ContainsKey(name);
+ if (attributes.TryGetValue(name, out var range))
+ {
+ value = rawValue.AsSpan().Slice(range).Trim();
+ return true;
+ }
+ value = default;
+ return false;
}
///
@@ -168,6 +174,7 @@ public Unit GetUnit(string name, UnitMetric defaultMetric = UnitMetric.Unitless)
public Margin GetMargin(string name)
{
Margin margin = Margin.Empty;
+ // shortcut to avoid resolving each individual side when we know this collection is empty
if (IsEmpty) return margin;
if (attributes.TryGetValue(name, out var range))
diff --git a/src/Html2OpenXml/Expressions/BodyExpression.cs b/src/Html2OpenXml/Expressions/BodyExpression.cs
index 4a1803ef..5b643c3b 100644
--- a/src/Html2OpenXml/Expressions/BodyExpression.cs
+++ b/src/Html2OpenXml/Expressions/BodyExpression.cs
@@ -69,10 +69,10 @@ protected override void ComposeStyles(ParsingContext context)
// Unsupported W3C attribute but claimed by users. Specified at level, the page
// orientation is applied on the whole document
- if (styleAttributes.ContainsKey("page-orientation"))
+ if (styleAttributes.TryGetValue("page-orientation", out var attrValue))
{
PageOrientationValues orientation = PageOrientationValues.Portrait;
- if (styleAttributes.HasKeyEqualsTo("page-orientation", "landscape"))
+ if ("landscape".Equals(attrValue, StringComparison.InvariantCultureIgnoreCase))
orientation = PageOrientationValues.Landscape;
var sectionProperties = mainPart.Document!.Body!.GetFirstChild();
diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
index 9ce33e33..199d0aea 100644
--- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
@@ -38,7 +38,7 @@ readonly struct ListContext(string listName, int absNumId, int instanceId, int l
// https://answers.microsoft.com/en-us/msoffice/forum/all/custom-list-number-style/21a54399-4404-4c37-8843-2ccaaf827485
// Image bullet: http://officeopenxml.com/WPnumbering-imagesAsSymbol.php
private static readonly HashSet supportedListTypes =
- ["disc", "decimal", "square", "circle",
+ ["disc", "decimal", "square", "circle", "dash",
"lower-alpha", "upper-alpha", "lower-latin", "upper-latin",
"lower-greek", "upper-greek",
"lower-roman", "upper-roman",
@@ -227,7 +227,15 @@ private static string GetListName(IElement listNode, string? parentName = null)
if (parentName != null && IsCascadingStyle(parentName))
return parentName!;
- type = orderedList? "decimal" : "disc";
+ // If a specific list-style-type is provided for an unordered list (e.g., a custom string like "'-'"),
+ // we strip the surrounding quotes to extract the raw symbol.
+ // Otherwise, we fallback to the default "decimal" or "disc" styles.
+ if (!orderedList && !string.IsNullOrEmpty(type))
+ {
+ return type!.Trim('\'', '\"');
+ }
+
+ return orderedList ? "decimal" : "disc";
}
return type!;
diff --git a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
index b41c4ee8..95c953c0 100644
--- a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
@@ -50,8 +50,20 @@ protected int GetOrCreateListTemplate(ParsingContext context, string listName)
Numbering numberingPart = context.MainPart.NumberingDefinitionsPart!.Numbering!;
+ AbstractNum abstractNum;
+
// at this stage, we have sanitized the list style so it's safe to grab them from the predefined template lists
- var abstractNum = predefinedNumberingLists[listName];
+ if (predefinedNumberingLists.TryGetValue(listName, out var predefined))
+ {
+ // If it's a predefined style, we clone it as usual
+ abstractNum = (AbstractNum)predefined.CloneNode(true);
+ }
+ else
+ {
+ // If not found in predefined lists, listName contains a custom bullet character (e.g., "-")
+ abstractNum = CreateCustomBulletAbstractNum(listName);
+ }
+
abstractNum = (AbstractNum) abstractNum.CloneNode(true);
abstractNum.AbstractNumberId = IncrementAbstractNumId(context, numberingPart);
var level1 = abstractNum.GetFirstChild()!;
@@ -216,6 +228,38 @@ private void InitNumberingIds(ParsingContext context)
isInitialized = true;
}
+ ///
+ /// Generates a custom abstract numbering definition for unordered lists using a specific symbol.
+ ///
+ /// The custom character or string to be used as the list bullet.
+ /// An instance configured with the custom bullet symbol across all levels.
+ private AbstractNum CreateCustomBulletAbstractNum(string customSymbol)
+ {
+ var abstractNum = new AbstractNum {
+ AbstractNumDefinitionName = new() { Val = customSymbol },
+ MultiLevelType = new() { Val = MultiLevelValues.HybridMultilevel }
+ };
+
+ for (var lvlIndex = 0; lvlIndex <= MaxLevel; lvlIndex++)
+ {
+ abstractNum.Append(new Level {
+ StartNumberingValue = new() { Val = 1 },
+ NumberingFormat = new() { Val = NumberFormatValues.Bullet },
+ LevelIndex = lvlIndex,
+ LevelText = new() { Val = string.Format(customSymbol, lvlIndex+1) },
+ LevelJustification = new() { Val = LevelJustificationValues.Left },
+ PreviousParagraphProperties = new() {
+ Indentation = new() {
+ Left = ((lvlIndex + 1) * Indentation * 2).ToString(),
+ Hanging = Indentation.ToString()
+ }
+ }
+ });
+ }
+
+ return abstractNum;
+ }
+
///
/// Predefined template of lists.
///
@@ -230,6 +274,7 @@ private static IReadOnlyDictionary InitKnownLists()
("disc", NumberFormatValues.Bullet, "•"),
("square", NumberFormatValues.Bullet, "▪"),
("circle", NumberFormatValues.Bullet, "o"),
+ ("dash", NumberFormatValues.Bullet, "-"),
("upper-alpha", NumberFormatValues.UpperLetter, "%{0}."),
("lower-alpha", NumberFormatValues.LowerLetter, "%{0}."),
("upper-roman", NumberFormatValues.UpperRoman, "%{0}."),
@@ -299,4 +344,4 @@ private static IReadOnlyDictionary InitKnownLists()
return knownAbstractNums;
#endif
}
-}
\ No newline at end of file
+}
diff --git a/src/Html2OpenXml/IO/HtmlImageInfo.cs b/src/Html2OpenXml/IO/HtmlImageInfo.cs
index a5b20df5..0ed2c27f 100755
--- a/src/Html2OpenXml/IO/HtmlImageInfo.cs
+++ b/src/Html2OpenXml/IO/HtmlImageInfo.cs
@@ -16,13 +16,8 @@ namespace HtmlToOpenXml.IO;
///
/// Represents an image and its metadata.
///
-sealed class HtmlImageInfo(string source, string partId)
+sealed class HtmlImageInfo(string partId)
{
- ///
- /// The URI identifying this cached image information.
- ///
- public string Source { get; set; } = source;
-
///
/// The Unique identifier of the ImagePart in the .
///
@@ -44,14 +39,3 @@ sealed class HtmlImageInfo(string source, string partId)
///
public bool IsExternal { get; set; }
}
-
-///
-/// Typed dictionary of where the Source URI is the identifier.
-///
-sealed class HtmlImageInfoCollection : System.Collections.ObjectModel.KeyedCollection
-{
- protected override string GetKeyForItem(HtmlImageInfo item)
- {
- return item.Source;
- }
-}
diff --git a/src/Html2OpenXml/IO/ImageHeader.cs b/src/Html2OpenXml/IO/ImageHeader.cs
index 851bfad6..013da2ad 100755
--- a/src/Html2OpenXml/IO/ImageHeader.cs
+++ b/src/Html2OpenXml/IO/ImageHeader.cs
@@ -57,7 +57,11 @@ public static bool TryDetectFileType(Stream stream, out FileType type)
{
using var reader = new SequentialBinaryReader(stream, leaveOpen: true);
type = DetectFileType(reader);
- stream.Seek(0L, SeekOrigin.Begin);
+ if (type != FileType.Unrecognized)
+ {
+ stream.Seek(0L, SeekOrigin.Begin);
+ }
+
return type != FileType.Unrecognized;
}
@@ -71,6 +75,10 @@ public static Size GetDimensions(Stream stream)
{
using var reader = new SequentialBinaryReader(stream, leaveOpen: true);
FileType type = DetectFileType(reader);
+
+ if (type == FileType.Unrecognized)
+ return Size.Empty;
+
stream.Seek(0L, SeekOrigin.Begin);
return type switch
{
@@ -91,19 +99,29 @@ public static Size KeepAspectRatio(Size actualSize, Size preferredSize)
{
int width, height;
+ // Handle edge cases where dimensions are zero or negative
+ if (actualSize.Width <= 0 || actualSize.Height <= 0)
+ return Size.Empty;
+
// Resize by the highest difference ratio between constrained dimension and real one.
- bool forceResizeByWidth = preferredSize.Height <= 0 && preferredSize.Width > 0;
- bool forceResizeByHeight = preferredSize.Width <= 0 && preferredSize.Height > 0;
- if (forceResizeByWidth || (!forceResizeByHeight &&
- Math.Abs(preferredSize.Width - actualSize.Width) > Math.Abs(preferredSize.Height - actualSize.Height)))
+ bool scaleByWidth = preferredSize.Height <= 0 && preferredSize.Width > 0;
+ bool scaleByHeight = preferredSize.Width <= 0 && preferredSize.Height > 0;
+ if (!scaleByHeight && !scaleByWidth)
+ {
+ int widthDiff = Math.Abs(preferredSize.Width - actualSize.Width);
+ int heightDiff = Math.Abs(preferredSize.Height - actualSize.Height);
+ scaleByWidth = widthDiff >= heightDiff;
+ }
+
+ if (scaleByWidth)
{
width = preferredSize.Width;
- height = (int) ((float) actualSize.Height / actualSize.Width * width);
+ height = actualSize.Height * width / actualSize.Width;
}
else
{
height = preferredSize.Height;
- width = (int) ((float) actualSize.Width / actualSize.Height * height);
+ width = actualSize.Width * height / actualSize.Height;
}
return new Size(width, height);
diff --git a/src/Html2OpenXml/IO/ImagePrefetcher.cs b/src/Html2OpenXml/IO/ImagePrefetcher.cs
index 5ead754d..36f21d33 100644
--- a/src/Html2OpenXml/IO/ImagePrefetcher.cs
+++ b/src/Html2OpenXml/IO/ImagePrefetcher.cs
@@ -9,6 +9,7 @@
* IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
* PARTICULAR PURPOSE.
*/
+using System.Collections.Concurrent;
using DocumentFormat.OpenXml.Packaging;
namespace HtmlToOpenXml.IO;
@@ -46,7 +47,7 @@ sealed class ImagePrefetcher : IImageLoader
};
private readonly T hostingPart;
private readonly IWebRequest resourceLoader;
- private readonly HtmlImageInfoCollection prefetchedImages;
+ private readonly ConcurrentDictionary prefetchedImages;
private readonly object lockObject = new();
private readonly ImageProcessingMode processingMode;
@@ -76,13 +77,9 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessin
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];
- }
+ if (prefetchedImages.TryGetValue(imageUri, out var iinfo))
+ return iinfo;
- HtmlImageInfo? iinfo;
if (DataUri.IsWellFormed(imageUri)) // data inline, encoded in base64
{
iinfo = ReadDataUri(imageUri);
@@ -110,14 +107,8 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessin
// 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);
- }
- }
+ // Double-check pattern to prevent duplicate adds during concurrent access
+ prefetchedImages.TryAdd(imageUri, iinfo);
}
return iinfo;
@@ -183,7 +174,7 @@ public ImagePrefetcher(T hostingPart, IWebRequest resourceLoader, ImageProcessin
// Return image info with external flag set
// Note: Size will be empty as we don't download the image
- return new HtmlImageInfo(src, relationshipId) {
+ return new HtmlImageInfo(relationshipId) {
IsExternal = true,
Size = Size.Empty,
TypeInfo = ImagePartType.Png // Default type, actual type doesn't matter for external links
@@ -223,7 +214,7 @@ private HtmlImageInfo SaveImageAssert(string src, PartTypeInfo type, Action
static class HttpUtility
{
- /// The common characters considered as white space.
- internal static readonly char[] WhiteSpaces = [' ', '\t', '\r', '\u00A0', '\u0085'];
+#if !NET5_0_OR_GREATER
private static readonly char[] entityEndingChars = [';', '&'];
-
static class HtmlEntities
{
private static readonly string[] entitiesList = [
@@ -146,6 +144,7 @@ private static void HtmlDecode(string? s, TextWriter output)
}
}
}
+#endif
///
/// Converts a string that represents an Html-encoded URL to a decoded string.
diff --git a/src/Html2OpenXml/Utilities/SpanExtensions.cs b/src/Html2OpenXml/Utilities/SpanExtensions.cs
index fdaa5289..1af88e11 100644
--- a/src/Html2OpenXml/Utilities/SpanExtensions.cs
+++ b/src/Html2OpenXml/Utilities/SpanExtensions.cs
@@ -160,7 +160,7 @@ public static int SplitCompositeAttribute(this ReadOnlySpan span, Span ResizedImageTestCases()
+ {
+ yield return (new Size(255, 0), new Size(255, 255), Size.Empty);
+ yield return (new Size(255, 255), new Size(255, 255), new Size(255, 255));
+ yield return (new Size(500, 255), new Size(125, 255), new Size(125, 63));
+ yield return (new Size(500, 255), new Size(255, 125), new Size(255, 130));
+ yield return (new Size(255, 500), new Size(255, 125), new Size(63, 125));
+ yield return (new Size(500, 255), new Size(500, 750), new Size(1470, 750));
+ yield return (new Size(9999, 7499), new Size(100, 75), new Size(100, 74));
+ yield return (new Size(1000, 1498), new Size(0, 642), new Size(428, 642));
+ }
}
}
\ No newline at end of file
diff --git a/test/HtmlToOpenXml.Tests/NumberingTests.cs b/test/HtmlToOpenXml.Tests/NumberingTests.cs
index f80a0a6c..db0907ec 100644
--- a/test/HtmlToOpenXml.Tests/NumberingTests.cs
+++ b/test/HtmlToOpenXml.Tests/NumberingTests.cs
@@ -698,5 +698,38 @@ await converter.ParseBody(@"
Assert.That(level.LevelText?.Val?.Value, Is.EqualTo("%1."));
}
}
+
+ [Test]
+ public async Task CustomBulletList_ReturnsListWithCustomStyle()
+ {
+ await converter.ParseBody(@"");
+
+ var elements = mainPart.Document!.Body!.ChildElements;
+ Assert.That(elements, Is.Not.Empty);
+ Assert.That(elements, Is.All.TypeOf());
+ var numId = ((Paragraph) elements[0]).ParagraphProperties?.NumberingProperties?.NumberingId?.Val?.Value;
+ Assert.That(numId, Is.Not.Null);
+
+ var numInst = mainPart.NumberingDefinitionsPart!.Numbering!
+ .Elements()
+ .Single(i => i.NumberID?.Value == numId);
+ Assert.That(numInst.AbstractNumId?.Val?.Value, Is.Not.Null);
+
+ var absNums = mainPart.NumberingDefinitionsPart.Numbering!
+ .Elements();
+ var absNum = absNums.FirstOrDefault(a => a.AbstractNumberId == numInst.AbstractNumId.Val);
+ Assert.That(absNum, Is.Not.Null);
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(absNum.AbstractNumDefinitionName?.Val?.Value, Is.EqualTo("😀"));
+ Assert.That(absNum.MultiLevelType?.Val?.InnerText, Is.AnyOf("hybridMultilevel", "multilevel"));
+ Assert.That(absNum.Elements().Count(), Is.AtLeast(2), "At least 2 level registred");
+ Assert.That(absNum.GetFirstChild()?.NumberingFormat?.Val?.Value, Is.EqualTo(NumberFormatValues.Bullet));
+ }
+
+ AssertThatOpenXmlDocumentIsValid();
+ }
}
}
diff --git a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
index 72704f70..febbd71b 100644
--- a/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
+++ b/test/HtmlToOpenXml.Tests/Primitives/StyleParserTests.cs
@@ -35,7 +35,7 @@ public void InvalidStyle_ShouldBeEmpty(string htmlStyle)
{
var styles = HtmlAttributeCollection.ParseStyle(htmlStyle);
Assert.That(styles.IsEmpty, Is.True);
- Assert.That(styles.ContainsKey("color"), Is.False);
+ Assert.That(styles.TryGetValue("color", out var _), Is.False);
}
[Test]
diff --git a/test/HtmlToOpenXml.Tests/StyleTests.cs b/test/HtmlToOpenXml.Tests/StyleTests.cs
index 0db55684..3c1dd2eb 100644
--- a/test/HtmlToOpenXml.Tests/StyleTests.cs
+++ b/test/HtmlToOpenXml.Tests/StyleTests.cs
@@ -178,7 +178,7 @@ public void EncodedStyle_ShouldSucceed()
public void EmptyStyle_ShouldBeIgnored()
{
var styleAttributes = HtmlAttributeCollection.ParseStyle("text-decoration;color:red");
- Assert.That(styleAttributes.ContainsKey("text-decoration"), Is.False);
+ Assert.That(styleAttributes.TryGetValue("text-decoration", out var _), Is.False);
Assert.That(styleAttributes["color"].ToString(), Is.EqualTo("red"));
}
}