From e5ea593cfad4e2f24abd821aa273af4f0dea8683 Mon Sep 17 00:00:00 2001 From: Ayush Hiremani Date: Mon, 12 Oct 2026 12:31:27 +0530 Subject: [PATCH 1/5] fix, unordered list utilize type attribute --- .../Expressions/Numbering/ListExpression.cs | 41 +++++++++++------ test/HtmlToOpenXml.Tests/NumberingTests.cs | 46 +++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs index f192e1a..d26f3af 100644 --- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs +++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs @@ -207,10 +207,9 @@ private static string GetListName(IElement listNode, string? parentName = null) bool orderedList = listNode.NodeName.Equals("ol", StringComparison.OrdinalIgnoreCase); string? type = styleAttributes["list-style-type"].ToString(); - if(orderedList && string.IsNullOrEmpty(type)) - { - type = ListTypeToListStyleType(listNode.GetAttribute("type")); - } + // parse node "type" attribute, if inline style "list-style-type" attribute is null. + if (string.IsNullOrEmpty(type)) + type = ListTypeToListStyleType(listNode.GetAttribute("type"), orderedList); if (string.IsNullOrEmpty(type) || !supportedListTypes.Contains(type!)) { @@ -233,18 +232,32 @@ private static string GetListName(IElement listNode, string? parentName = null) /// /// Map ordered list style attribute values to css list-style-type. - /// Valid types are "1|a|A|i|I": https://w3schools.com/tags/att_ol_type.asp + /// Ordered lists types are "1|a|A|i|I|α" (https://w3schools.com/tags/att_ol_type.asp). + /// Unordered lists types are "disc|circle|square" (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ul#type). /// - private static string? ListTypeToListStyleType(string? type) => type switch + private static string? ListTypeToListStyleType(string? type, bool orderedList) { - "1" => "decimal", - "a" => "lower-alpha", - "A" => "upper-alpha", - "i" => "lower-roman", - "I" => "upper-roman", - "lower-greek" or "upper-greek" => type, - _ => null - }; + if (string.IsNullOrEmpty(type)) + return null; + + if (!orderedList) + return type!.ToLowerInvariant() switch + { + "disc" or "circle" or "square" => type.ToLowerInvariant(), + _ => null + }; + + return type switch + { + "1" => "decimal", + "a" => "lower-alpha", + "A" => "upper-alpha", + "i" => "lower-roman", + "I" => "upper-roman", + "lower-greek" or "upper-greek" => type, + _ => null + }; + } /// /// Resolve the of a list element node, diff --git a/test/HtmlToOpenXml.Tests/NumberingTests.cs b/test/HtmlToOpenXml.Tests/NumberingTests.cs index 61a69d1..891d995 100644 --- a/test/HtmlToOpenXml.Tests/NumberingTests.cs +++ b/test/HtmlToOpenXml.Tests/NumberingTests.cs @@ -731,5 +731,51 @@ await converter.ParseBody(@"
    AssertThatOpenXmlDocumentIsValid(); } + + [Test(Description = "A nested ul type attribute must use its own bullet, not the parent marker")] + public async Task NestedUnorderedList_TypeAttribute_UsesOwnBullet() + { + await converter.ParseBody(@" +
      +
    • Ingredients +
        +
      • Rice
      • +
      +
    • +
    +
      +
    • Preparation +
        +
      • Wash
      • +
      +
    • +
    "); + + var paragraphs = mainPart.Document!.Body!.Elements().ToList(); + Assert.That(paragraphs, Has.Count.EqualTo(4)); + Assert.That(paragraphs.Select(GetBulletText), Is.EqualTo(new[] { "•", "▪", "•", "▪" })); + AssertThatOpenXmlDocumentIsValid(); + } + + [Test(Description = "CSS list-style-type takes precedence over the ul type attribute")] + public async Task UnorderedList_CssListStyleType_OverridesTypeAttribute() + { + await converter.ParseBody(@"
    • Item
    "); + + var paragraph = mainPart.Document!.Body!.Elements().Single(); + Assert.That(GetBulletText(paragraph), Is.EqualTo("o")); + AssertThatOpenXmlDocumentIsValid(); + } + + private string? GetBulletText(Paragraph paragraph) + { + var numberingProperties = paragraph.ParagraphProperties?.NumberingProperties; + var numId = numberingProperties?.NumberingId?.Val?.Value; + var level = numberingProperties?.NumberingLevelReference?.Val?.Value; + var numbering = mainPart.NumberingDefinitionsPart!.Numbering!; + var instance = numbering.Elements().Single(i => i.NumberID?.Value == numId); + var abstractNum = numbering.Elements().Single(a => a.AbstractNumberId?.Value == instance.AbstractNumId!.Val!.Value); + return abstractNum.Elements().First(l => l.LevelIndex?.Value == level).LevelText?.Val?.Value; + } } } From effe83449c6e1daab20efeb711730cfec8f61301 Mon Sep 17 00:00:00 2001 From: Ayush Hiremani Date: Mon, 12 Oct 2026 14:23:39 +0530 Subject: [PATCH 2/5] resolve comments --- .../Expressions/Numbering/ListExpression.cs | 31 +++++++++---------- test/HtmlToOpenXml.Tests/NumberingTests.cs | 26 +++++++++++----- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs index d26f3af..d100fc5 100644 --- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs +++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs @@ -237,24 +237,21 @@ private static string GetListName(IElement listNode, string? parentName = null) ///
private static string? ListTypeToListStyleType(string? type, bool orderedList) { - if (string.IsNullOrEmpty(type)) - return null; - - if (!orderedList) - return type!.ToLowerInvariant() switch - { - "disc" or "circle" or "square" => type.ToLowerInvariant(), - _ => null - }; - - return type switch + return (orderedList, type) switch { - "1" => "decimal", - "a" => "lower-alpha", - "A" => "upper-alpha", - "i" => "lower-roman", - "I" => "upper-roman", - "lower-greek" or "upper-greek" => type, + // un-ordered list marker style resolution + (false, string t) when t.Equals("disc", StringComparison.OrdinalIgnoreCase) => "disc", + (false, string t) when t.Equals("circle", StringComparison.OrdinalIgnoreCase) => "circle", + (false, string t) when t.Equals("square", StringComparison.OrdinalIgnoreCase) => "square", + + // ordered list marker style resolution + (true, "1") => "decimal", + (true, "a") => "lower-alpha", + (true, "A") => "upper-alpha", + (true, "i") => "lower-roman", + (true, "I") => "upper-roman", + (true, "lower-greek" or "upper-greek") => type, + _ => null }; } diff --git a/test/HtmlToOpenXml.Tests/NumberingTests.cs b/test/HtmlToOpenXml.Tests/NumberingTests.cs index 891d995..89d408b 100644 --- a/test/HtmlToOpenXml.Tests/NumberingTests.cs +++ b/test/HtmlToOpenXml.Tests/NumberingTests.cs @@ -733,19 +733,21 @@ await converter.ParseBody(@"