Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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') }}
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ FodyWeavers.xsd
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
*.csproj.lscache

# Local History for Visual Studio Code
.history/
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Version>3.3.2</Version>
<Version>3.4.0</Version>
</PropertyGroup>

<PropertyGroup>
<DocumentFormatOpenXmlPackageVersion>3.4.1</DocumentFormatOpenXmlPackageVersion>
<DocumentFormatOpenXmlPackageVersion>3.5.1</DocumentFormatOpenXmlPackageVersion>
</PropertyGroup>
</Project>
54 changes: 0 additions & 54 deletions HtmlToOpenXml.sln

This file was deleted.

10 changes: 10 additions & 0 deletions HtmlToOpenXml.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Html2OpenXml/HtmlToOpenXml.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="examples/Benchmark/Benchmark.csproj" />
<Project Path="examples/Demo/Demo.csproj" />
<Project Path="test/HtmlToOpenXml.Tests/HtmlToOpenXml.Tests.csproj" />
</Folder>
</Solution>
2 changes: 1 addition & 1 deletion examples/Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
38 changes: 38 additions & 0 deletions examples/Demo/Resources/UlStyles.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body>
<ul style="list-style-type: '-';">
<li style="text-align: justify;">test 2</li>
<li style="text-align: justify;">test 3</li>
<li style="text-align: justify;">&nbsp;</li>
</ul>
<p style="text-align: justify;">&nbsp;</p>
<ul style="list-style-type: disc;">
<li style="text-align: justify;">test 2</li>
<li style="text-align: justify;">test 3</li>
<li style="text-align: justify;">&nbsp;</li>
</ul>
<p style="text-align: justify;">&nbsp;</p>
<ul style="list-style-type: '-';">
<li style="text-align: justify;">test 2</li>
<li style="text-align: justify;">test 3</li>
<li style="text-align: justify;">&nbsp;</li>
</ul>
<p style="text-align: justify;">&nbsp;</p>
<ul style="list-style-type: '😀';">
<li style="text-align: justify;">test 2</li>
<li style="text-align: justify;">test 3</li>
<li style="text-align: justify;">&nbsp;</li>
</ul>

<p style="text-align: justify;">&nbsp;</p>
<ul style="list-style-type: dash;">
<li style="text-align: justify;">test 2</li>
<li style="text-align: justify;">test 3</li>
<li style="text-align: justify;">&nbsp;</li>
</ul>
</body>
</html>
15 changes: 11 additions & 4 deletions src/Html2OpenXml/Collections/HtmlAttributeCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
Expand All @@ -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 "&#58;"
}
Expand Down Expand Up @@ -120,9 +120,15 @@ public ReadOnlySpan<char> this[string name]
/// <summary>
/// Determines whether the collection contains the specified key.
/// </summary>
public bool ContainsKey(string name)
public bool TryGetValue(string name, out ReadOnlySpan<char> value)
{
return attributes.ContainsKey(name);
if (attributes.TryGetValue(name, out var range))
{
value = rawValue.AsSpan().Slice(range).Trim();
return true;
}
value = default;
return false;
}

/// <summary>
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions src/Html2OpenXml/Expressions/BodyExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ protected override void ComposeStyles(ParsingContext context)

// Unsupported W3C attribute but claimed by users. Specified at <body> 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<SectionProperties>();
Expand Down
12 changes: 10 additions & 2 deletions src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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",
Expand Down Expand Up @@ -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!;
Expand Down
49 changes: 47 additions & 2 deletions src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Level>()!;
Expand Down Expand Up @@ -216,6 +228,38 @@ private void InitNumberingIds(ParsingContext context)
isInitialized = true;
}

/// <summary>
/// Generates a custom abstract numbering definition for unordered lists using a specific symbol.
/// </summary>
/// <param name="customSymbol">The custom character or string to be used as the list bullet.</param>
/// <returns>An <see cref="AbstractNum"/> instance configured with the custom bullet symbol across all levels.</returns>
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;
}

/// <summary>
/// Predefined template of lists.
/// </summary>
Expand All @@ -230,6 +274,7 @@ private static IReadOnlyDictionary<string, AbstractNum> 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}."),
Expand Down Expand Up @@ -299,4 +344,4 @@ private static IReadOnlyDictionary<string, AbstractNum> InitKnownLists()
return knownAbstractNums;
#endif
}
}
}
18 changes: 1 addition & 17 deletions src/Html2OpenXml/IO/HtmlImageInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,8 @@ namespace HtmlToOpenXml.IO;
/// <summary>
/// Represents an image and its metadata.
/// </summary>
sealed class HtmlImageInfo(string source, string partId)
sealed class HtmlImageInfo(string partId)
{
/// <summary>
/// The URI identifying this cached image information.
/// </summary>
public string Source { get; set; } = source;

/// <summary>
/// The Unique identifier of the ImagePart in the <see cref="MainDocumentPart"/>.
/// </summary>
Expand All @@ -44,14 +39,3 @@ sealed class HtmlImageInfo(string source, string partId)
/// </summary>
public bool IsExternal { get; set; }
}

/// <summary>
/// Typed dictionary of <see cref="HtmlImageInfo"/> where the Source URI is the identifier.
/// </summary>
sealed class HtmlImageInfoCollection : System.Collections.ObjectModel.KeyedCollection<string, HtmlImageInfo>
{
protected override string GetKeyForItem(HtmlImageInfo item)
{
return item.Source;
}
}
Loading
Loading