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(@"
    +
  • Item 1
  • +
"); + + 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")); } }