diff --git a/CHANGELOG.md b/CHANGELOG.md
index a56d874d..ef44323f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 3.2.1
+
+- Fix indentation of numbering list #166
+- Bordered container must render its content with one bordered frame #168
+- Fix serialisation of the "Harvard" style for lower-roman list
+- Fix ParseHeader/Footer where input with multiple paragraphs output only the latest
+- Ensure to apply default style for paragraphs, to avoid a paragraph between 2 list is mis-guessed
+
## 3.2.0
- Add new public API to allow parsing into Header and Footer #162. Some API methods as been flagged as obsolete with a clear message of what to use instead.
diff --git a/HtmlToOpenXml.sln b/HtmlToOpenXml.sln
index 18814542..d702dedb 100644
--- a/HtmlToOpenXml.sln
+++ b/HtmlToOpenXml.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 17
+# 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}"
diff --git a/examples/Demo/Program.cs b/examples/Demo/Program.cs
index 47c0124b..0620d54f 100644
--- a/examples/Demo/Program.cs
+++ b/examples/Demo/Program.cs
@@ -15,7 +15,7 @@ static class Program
static async Task Main(string[] args)
{
const string filename = "test.docx";
- string html = ResourceHelper.GetString("Resources.AdvancedTable.html");
+ string html = ResourceHelper.GetString("Resources.CompleteRunTest.html");
if (File.Exists(filename)) File.Delete(filename);
using (MemoryStream generatedDocument = new MemoryStream())
@@ -28,8 +28,8 @@ static async Task Main(string[] args)
}
generatedDocument.Position = 0L;
- using (WordprocessingDocument package = WordprocessingDocument.Open(generatedDocument, true))
- //using (WordprocessingDocument package = WordprocessingDocument.Create(generatedDocument, WordprocessingDocumentType.Document))
+ //using (WordprocessingDocument package = WordprocessingDocument.Open(generatedDocument, true))
+ using (WordprocessingDocument package = WordprocessingDocument.Create(generatedDocument, WordprocessingDocumentType.Document))
{
MainDocumentPart mainPart = package.MainDocumentPart;
if (mainPart == null)
@@ -38,12 +38,11 @@ static async Task Main(string[] args)
new Document(new Body()).Save(mainPart);
}
- HtmlConverter converter = new HtmlConverter(mainPart);
+ HtmlConverter converter = new(mainPart, new HtmlToOpenXml.IO.DefaultWebRequest(){
+ BaseImageUrl = new Uri(Path.Combine(Environment.CurrentDirectory, "images"))
+ });
converter.RenderPreAsTable = true;
- Body body = mainPart.Document.Body;
-
await converter.ParseBody(html);
- mainPart.Document.Save();
AssertThatOpenXmlDocumentIsValid(package);
}
diff --git a/examples/Demo/Resources/CompleteRunTest.html b/examples/Demo/Resources/CompleteRunTest.html
index c36816a4..0976ee14 100644
--- a/examples/Demo/Resources/CompleteRunTest.html
+++ b/examples/Demo/Resources/CompleteRunTest.html
@@ -161,7 +161,19 @@
Heading 5
line below!
-
+
+
+
+
Header placeholder:
+
+ - Item 1
+ - Item 2
+
+
Footer Placeholder
+
+
+
+ Lorem Ipsum
diff --git a/examples/Demo/Resources/Demo.html b/examples/Demo/Resources/Demo.html
deleted file mode 100644
index 906b7e4c..00000000
--- a/examples/Demo/Resources/Demo.html
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
- - This is H1
-
- - This is H2
- - Another H2
-
-
- - This is another H1
-
- - This is H2
- - Another H2
-
-
- - Last H1
-
- - This is H2
- - Another H2
-
-
-
-
-
-
- Looks how cool is Open Xml.
- Now with HtmlToOpenXml, it nevers been so easy to convert html.
-
- If you like it, add me a rating on codeplex
-
-
- public void SetContentType(System.Web.HttpRequest request, System.Web.HttpResponse response, String reportName)
-{
- if (request.Browser.Browser.Contains("IE"))
- {
- // Replace the %20 to obtain a clean name when saving the file from Word.
- encodedFilename =
- Uri.EscapeDataString(Path.GetFileNameWithoutExtension(encodedFilename)).Replace("%20", " ")
- + Path.GetExtension(encodedFilename);
- }
-}
-
-
-
- An ordered list:
-
- - Coffee
- - Tea
- - Milk
-
- - Coffee
- - Tea
-
- - Earl Grey
- - Green
- - Rosboh
-
-
- - Milk
-
-
- - Wine
-
-
- An unordered list:
-
diff --git a/examples/Demo/Resources/Demo2.html b/examples/Demo/Resources/Demo2.html
deleted file mode 100644
index 50436c08..00000000
--- a/examples/Demo/Resources/Demo2.html
+++ /dev/null
@@ -1,57 +0,0 @@
-$#Candidate Code#$
-
-Hello dude
-bonjour je suis du texte en mauve.
-
-
-
-| saa |
- fdgdfg |
- fdg |
- fdgfdg |
- |
-
-
-| |
- |
- |
- fdg |
- |
-
-
-| |
- fd |
- fdggfdg |
- |
- gfdgfd |
-
-
-
-
-
-
-
-
-
-| saa |
- fdgdfg |
- fdg |
- fdgfdg |
- |
-
-
-| |
- |
- |
- fdg |
- |
-
-
-| |
- fd |
- fdggfdg |
- |
- gfdgfd |
-
-
-
\ No newline at end of file
diff --git a/src/Html2OpenXml/Expressions/BlockElementExpression.cs b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
index e15db508..7ae7026f 100644
--- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs
+++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs
@@ -23,28 +23,68 @@ namespace HtmlToOpenXml.Expressions;
/// Process the parsing of block contents (like p, span, heading).
/// A block-level element always starts on a new line, and the browsers automatically add some space (a margin) before and after the element.
///
-class BlockElementExpression(IHtmlElement node, params OpenXmlLeafElement[]? styleProperty) : PhrasingElementExpression(node)
+class BlockElementExpression: PhrasingElementExpression
{
- private readonly OpenXmlLeafElement[]? defaultStyleProperties = styleProperty;
+ private readonly OpenXmlLeafElement[]? defaultStyleProperties;
protected readonly ParagraphProperties paraProperties = new();
+ // some style attributes, such as borders or bgcolor, will convert this node to a framed container
+ protected bool renderAsFramed;
+ private HtmlBorder styleBorder;
+
+
+ public BlockElementExpression(IHtmlElement node, OpenXmlLeafElement? styleProperty) : base(node)
+ {
+ if (styleProperty is not null)
+ defaultStyleProperties = [styleProperty];
+ }
+ public BlockElementExpression(IHtmlElement node, params OpenXmlLeafElement[]? styleProperty) : base(node)
+ {
+ defaultStyleProperties = styleProperty;
+ }
///
public override IEnumerable Interpret (ParsingContext context)
{
- var elements = base.Interpret(context);
+ var childElements = base.Interpret(context);
var bookmarkTarget = node.GetAttribute(InternalNamespaceUri, "bookmark");
if (bookmarkTarget is not null)
{
var bookmarkId = IncrementBookmarkId(context).ToString(CultureInfo.InvariantCulture);
- var p = elements.First();
+ var p = childElements.First();
// need to be inserted after pPr to avoid schema warning
p.InsertAfter(new BookmarkStart() { Id = bookmarkId, Name = bookmarkTarget }, p.GetFirstChild());
p.AppendChild(new BookmarkEnd() { Id = bookmarkId });
}
- return elements;
+ if (!renderAsFramed)
+ return childElements;
+
+ var paragraphs = childElements.OfType();
+ if (!paragraphs.Any()) return childElements;
+
+ // if we have only 1 paragraph, just inline the styles
+ if (paragraphs.Count() == 1)
+ {
+ var p = paragraphs.First();
+
+ if (!styleBorder.IsEmpty && p.ParagraphProperties?.ParagraphBorders is null)
+ {
+ p.ParagraphProperties ??= new();
+ p.ParagraphProperties!.ParagraphBorders = new ParagraphBorders {
+ LeftBorder = Converter.ToBorder(styleBorder.Left),
+ RightBorder = Converter.ToBorder(styleBorder.Right),
+ TopBorder = Converter.ToBorder(styleBorder.Top),
+ BottomBorder = Converter.ToBorder(styleBorder.Bottom)
+ };
+ }
+
+ return childElements;
+ }
+
+ // if we have 2+ paragraphs, we will embed them inside a stylised table
+ return [CreateFrame(childElements)];
}
protected override IEnumerable Interpret (
@@ -136,17 +176,11 @@ protected override void ComposeStyles (ParsingContext context)
}
- var styleBorder = styleAttributes.GetBorders();
+ styleBorder = styleAttributes.GetBorders();
if (!styleBorder.IsEmpty)
{
- var borders = new ParagraphBorders {
- LeftBorder = Converter.ToBorder(styleBorder.Left),
- RightBorder = Converter.ToBorder(styleBorder.Right),
- TopBorder = Converter.ToBorder(styleBorder.Top),
- BottomBorder = Converter.ToBorder(styleBorder.Bottom)
- };
-
- paraProperties.ParagraphBorders = borders;
+ renderAsFramed = true;
+ runProperties.Border = null;
}
foreach (string className in node.ClassList)
@@ -159,8 +193,8 @@ protected override void ComposeStyles (ParsingContext context)
}
}
- Margin margin = styleAttributes.GetMargin("margin");
- Indentation? indentation = null;
+ var margin = styleAttributes.GetMargin("margin");
+ Indentation? indentation = null;
if (!margin.IsEmpty)
{
if (margin.Top.IsFixed || margin.Bottom.IsFixed)
@@ -236,6 +270,9 @@ protected override void ComposeStyles (ParsingContext context)
};
}
}
+
+ if (runProperties.Shading != null)
+ renderAsFramed = true;
}
///
@@ -322,6 +359,43 @@ private static Paragraph CreateParagraph(ParsingContext context, IList
+ /// Group all the paragraph inside a framed table.
+ ///
+ private Table CreateFrame(IEnumerable childElements)
+ {
+ TableCell cell;
+ TableProperties tableProperties;
+ Table framedTable = new(
+ tableProperties = new TableProperties {
+ TableWidth = new() { Type = TableWidthUnitValues.Pct, Width = "5000" } // 100%
+ },
+ new TableGrid(
+ new GridColumn() { Width = "9442" }),
+ new TableRow(
+ cell = new TableCell(childElements)
+ )
+ );
+
+ if (!styleBorder.IsEmpty)
+ {
+ tableProperties.TableBorders = new TableBorders {
+ LeftBorder = Converter.ToBorder(styleBorder.Left),
+ RightBorder = Converter.ToBorder(styleBorder.Right),
+ TopBorder = Converter.ToBorder(styleBorder.Top),
+ BottomBorder = Converter.ToBorder(styleBorder.Bottom)
+ };
+ }
+
+ if (runProperties.Shading != null)
+ {
+ cell.TableCellProperties = new() { Shading = (Shading?) runProperties.Shading.Clone() };
+ }
+
+ return framedTable;
+ }
+
///
/// Resolve the next available (they must be unique).
///
diff --git a/src/Html2OpenXml/Expressions/BodyExpression.cs b/src/Html2OpenXml/Expressions/BodyExpression.cs
index 7ff5ee16..1abd147a 100644
--- a/src/Html2OpenXml/Expressions/BodyExpression.cs
+++ b/src/Html2OpenXml/Expressions/BodyExpression.cs
@@ -23,7 +23,8 @@ namespace HtmlToOpenXml.Expressions;
/// Top parent expression, processing the body tag,
/// even if it is not directly specified in the provided Html.
///
-sealed class BodyExpression(IHtmlElement node) : BlockElementExpression(node)
+sealed class BodyExpression(IHtmlElement node, ParagraphStyleId? defaultStyle)
+ : BlockElementExpression(node, defaultStyle)
{
private bool shouldRegisterTopBookmark;
diff --git a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
index 8bb2a369..811976c7 100644
--- a/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/ListExpression.cs
@@ -83,7 +83,6 @@ public override IEnumerable Interpret(ParsingContext context)
p.ParagraphProperties ??= new();
p.ParagraphProperties.ParagraphStyleId = GetStyleIdForListItem(context.DocumentStyle, liNode);
- p.ParagraphProperties.Indentation = level < 2? null : new() { Left = (level * Indentation).ToString() };
p.ParagraphProperties.NumberingProperties = new NumberingProperties {
NumberingLevelReference = new() { Val = level - 1 },
NumberingId = new() { Val = listContext.InstanceId }
diff --git a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
index a282b17f..1e0e1586 100644
--- a/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
+++ b/src/Html2OpenXml/Expressions/Numbering/NumberingExpressionBase.cs
@@ -80,8 +80,8 @@ protected int GetOrCreateListTemplate(ParsingContext context, string listName)
abstractNum.StyleLink = new StyleLink { Val = "Harvard" };
context.DocumentStyle.AddStyle("Harvard", new Style (
- new Name { Val = "Harvard" },
- new ParagraphProperties(
+ new StyleName { Val = "Harvard" },
+ new StyleParagraphProperties(
new NumberingProperties() { NumberingId = new() { Val = abstractNum.AbstractNumberId } }
)) {
Type = StyleValues.Numbering,
@@ -252,7 +252,10 @@ private static Dictionary InitKnownLists()
LevelText = new() { Val = string.Format(text, lvlIndex+1) },
LevelJustification = new() { Val = LevelJustificationValues.Left },
PreviousParagraphProperties = new() {
- Indentation = new() { Left = Indentation.ToString(), Hanging = Indentation.ToString() }
+ Indentation = new() {
+ Left = ((lvlIndex + 1) * Indentation * 2).ToString(),
+ Hanging = Indentation.ToString()
+ }
},
NumberingSymbolRunProperties = useSymbol? new () {
RunFonts = new() { Ascii = "Symbol", Hint = FontTypeHintValues.Default }
@@ -280,7 +283,10 @@ private static Dictionary InitKnownLists()
StartNumberingValue = new() { Val = 1 },
NumberingFormat = new() { Val = NumberFormatValues.Decimal },
LevelIndex = lvlIndex,
- LevelText = new() { Val = lvlText.ToString() }
+ LevelText = new() { Val = lvlText.ToString() },
+ PreviousParagraphProperties = new() {
+ Indentation = new() { Left = "0", Hanging = Indentation.ToString() }
+ }
});
}
knownAbstractNums.Add(listName, abstractNum);
diff --git a/src/Html2OpenXml/HtmlConverter.cs b/src/Html2OpenXml/HtmlConverter.cs
index 8531f7ed..7cb7a380 100755
--- a/src/Html2OpenXml/HtmlConverter.cs
+++ b/src/Html2OpenXml/HtmlConverter.cs
@@ -127,8 +127,7 @@ public async Task ParseHeader(string html, HeaderFooterValues? headerType = null
new ParallelOptions() { CancellationToken = cancellationToken },
htmlStyles.GetParagraphStyle(htmlStyles.DefaultStyles.HeaderStyle));
- foreach (var p in paragraphs)
- headerPart.Header.AddChild(p);
+ headerPart.Header.Append(paragraphs);
}
///
@@ -152,8 +151,7 @@ public async Task ParseFooter(string html, HeaderFooterValues? footerType = null
new ParallelOptions() { CancellationToken = cancellationToken },
htmlStyles.GetParagraphStyle(htmlStyles.DefaultStyles.FooterStyle));
- foreach (var p in paragraphs)
- footerPart.Footer.AddChild(p);
+ footerPart.Footer.Append(paragraphs);
}
///
@@ -166,7 +164,8 @@ public async Task ParseBody(string html, CancellationToken cancellationToken = d
{
bodyImageLoader ??= new ImagePrefetcher(mainPart, webRequester);
var paragraphs = await ParseCoreAsync(html, mainPart, bodyImageLoader,
- new ParallelOptions() { CancellationToken = cancellationToken });
+ new ParallelOptions() { CancellationToken = cancellationToken },
+ htmlStyles.GetParagraphStyle(htmlStyles.DefaultStyles.Paragraph));
if (!paragraphs.Any())
return;
@@ -263,11 +262,9 @@ private async Task> ParseCoreAsync(string h
Expressions.HtmlDomExpression expression;
if (hostingPart is MainDocumentPart)
- expression = new Expressions.BodyExpression(htmlDocument.Body!);
- else if (defaultParagraphStyleId?.Val?.HasValue == true)
- expression = new Expressions.BlockElementExpression(htmlDocument.Body!, defaultParagraphStyleId);
+ expression = new Expressions.BodyExpression(htmlDocument.Body!, defaultParagraphStyleId);
else
- expression = new Expressions.BlockElementExpression(htmlDocument.Body!);
+ expression = new Expressions.BlockElementExpression(htmlDocument.Body!, defaultParagraphStyleId);
var parsingContext = new ParsingContext(this, hostingPart, imageLoader);
var paragraphs = expression.Interpret(parsingContext);
diff --git a/src/Html2OpenXml/HtmlToOpenXml.csproj b/src/Html2OpenXml/HtmlToOpenXml.csproj
index b9475508..03daeffc 100644
--- a/src/Html2OpenXml/HtmlToOpenXml.csproj
+++ b/src/Html2OpenXml/HtmlToOpenXml.csproj
@@ -9,13 +9,13 @@
HtmlToOpenXml
HtmlToOpenXml
HtmlToOpenXml.dll
- 3.2.0
+ 3.2.1
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.0
+ 3.2.1
MIT
https://github.com/onizet/html2openxml
https://github.com/onizet/html2openxml
diff --git a/src/Html2OpenXml/IO/DataUri.cs b/src/Html2OpenXml/IO/DataUri.cs
index 8b0736e2..918783fa 100755
--- a/src/Html2OpenXml/IO/DataUri.cs
+++ b/src/Html2OpenXml/IO/DataUri.cs
@@ -84,7 +84,11 @@ public static bool TryCreate(string uri, out DataUri? result)
if (match.Groups["base64"].Length > 0)
{
// be careful that the raw data is encoded for url (standard %xx hex encoding)
+#if NET5_0_OR_GREATER
+ string base64 = System.Web.HttpUtility.HtmlDecode(match.Groups["data"].Value);
+#else
string base64 = HttpUtility.HtmlDecode(match.Groups["data"].Value);
+#endif
try
{
diff --git a/src/Html2OpenXml/PredefinedStyles.cs b/src/Html2OpenXml/PredefinedStyles.cs
index a7cf2a5a..993307ac 100755
--- a/src/Html2OpenXml/PredefinedStyles.cs
+++ b/src/Html2OpenXml/PredefinedStyles.cs
@@ -24,6 +24,7 @@ internal class PredefinedStyles
public const string TableGrid = "TableGrid";
public const string Header = "Header";
public const string Footer = "Footer";
+ public const string Paragraph = "Normal";
diff --git a/src/Html2OpenXml/PredefinedStyles.resx b/src/Html2OpenXml/PredefinedStyles.resx
index 75d3120f..f917f681 100755
--- a/src/Html2OpenXml/PredefinedStyles.resx
+++ b/src/Html2OpenXml/PredefinedStyles.resx
@@ -359,12 +359,20 @@
-
-
+
-
+
+
+
+
+]]>
+
+
+
+
+
]]>
diff --git a/src/Html2OpenXml/Primitives/DefaultStyles.cs b/src/Html2OpenXml/Primitives/DefaultStyles.cs
index 44d2a1ce..013adb3a 100644
--- a/src/Html2OpenXml/Primitives/DefaultStyles.cs
+++ b/src/Html2OpenXml/Primitives/DefaultStyles.cs
@@ -101,4 +101,10 @@ public class DefaultStyles
///
/// Footer
public string FooterStyle { get; set; } = PredefinedStyles.Footer;
+
+ ///
+ /// Default style for body paragraph.
+ ///
+ /// Normal
+ public string Paragraph { get; set; } = PredefinedStyles.Paragraph;
}
\ No newline at end of file
diff --git a/src/Html2OpenXml/WordDocumentStyle.cs b/src/Html2OpenXml/WordDocumentStyle.cs
index 6f7ee269..dd974c4d 100755
--- a/src/Html2OpenXml/WordDocumentStyle.cs
+++ b/src/Html2OpenXml/WordDocumentStyle.cs
@@ -53,7 +53,8 @@ internal WordDocumentStyle(MainDocumentPart mainPart)
PredefinedStyles.ListParagraph,
PredefinedStyles.Quote,
PredefinedStyles.QuoteChar,
- PredefinedStyles.TableGrid
+ PredefinedStyles.TableGrid,
+ PredefinedStyles.Paragraph
];
this.mainPart = mainPart;
}
diff --git a/test/HtmlToOpenXml.Tests/BodyTests.cs b/test/HtmlToOpenXml.Tests/BodyTests.cs
index 2baeb46c..d6da93d5 100644
--- a/test/HtmlToOpenXml.Tests/BodyTests.cs
+++ b/test/HtmlToOpenXml.Tests/BodyTests.cs
@@ -12,9 +12,11 @@ public class BodyTests : HtmlConverterTestBase
{
[TestCase("landscape", ExpectedResult = true)]
[TestCase("portrait", ExpectedResult = false)]
- public bool PageOrientation_ReturnsLandscapeDimension(string orientation)
+ public async Task PageOrientation_ReturnsLandscapeDimension(string orientation)
{
- var _ = converter.Parse($@"");
+ await converter.ParseBody($@"");
+ AssertThatOpenXmlDocumentIsValid();
+
var sectionProperties = mainPart.Document.Body!.GetFirstChild();
Assert.That(sectionProperties, Is.Not.Null);
var pageSize = sectionProperties.GetFirstChild();
@@ -24,7 +26,7 @@ public bool PageOrientation_ReturnsLandscapeDimension(string orientation)
[TestCase("portrait", ExpectedResult = true)]
[TestCase("landscape", ExpectedResult = false)]
- public bool PageOrientation_OverrideExistingLayout_ReturnsLandscapeDimension(string orientation)
+ public async Task PageOrientation_OverrideExistingLayout_ReturnsLandscapeDimension(string orientation)
{
using var generatedDocument = new MemoryStream();
using (var buffer = ResourceHelper.GetStream("Resources.DocWithLandscape.docx"))
@@ -35,7 +37,9 @@ public bool PageOrientation_OverrideExistingLayout_ReturnsLandscapeDimension(str
MainDocumentPart mainPart = package.MainDocumentPart!;
HtmlConverter converter = new(mainPart);
- var _ = converter.Parse($@"");
+ await converter.ParseBody($@"");
+ AssertThatOpenXmlDocumentIsValid();
+
var sectionProperties = mainPart.Document.Body!.GetFirstChild();
Assert.That(sectionProperties, Is.Not.Null);
var pageSize = sectionProperties.GetFirstChild();
diff --git a/test/HtmlToOpenXml.Tests/DivTests.cs b/test/HtmlToOpenXml.Tests/DivTests.cs
index 383ae07b..25e8098b 100644
--- a/test/HtmlToOpenXml.Tests/DivTests.cs
+++ b/test/HtmlToOpenXml.Tests/DivTests.cs
@@ -12,15 +12,18 @@ public class DivTests : HtmlConverterTestBase
[Test]
public void StyleAttribute_WithMultipleValues_ShouldBeAllApplied()
{
- var elements = converter.Parse(@"Lorem
");
+ var elements = converter.Parse(@"Lorem
");
Assert.That(elements, Has.Count.EqualTo(1));
Assert.That(elements, Has.All.TypeOf());
var p = (Paragraph) elements[0];
+ Assert.That(p.ParagraphProperties, Is.Not.Null);
Assert.Multiple(() =>
{
- Assert.That(p.ParagraphProperties?.Indentation?.FirstLine?.HasValue, Is.True);
- Assert.That(p.ParagraphProperties?.ParagraphBorders, Is.Not.Null);
- Assert.That(p.ParagraphProperties?.Justification?.Val?.Value, Is.EqualTo(JustificationValues.Center));
+ Assert.That(p.ParagraphProperties.Indentation?.FirstLine?.HasValue, Is.True);
+ Assert.That(p.ParagraphProperties.ParagraphBorders, Is.Not.Null);
+ Assert.That(p.ParagraphProperties.Justification?.Val?.Value, Is.EqualTo(JustificationValues.Center));
+ Assert.That(p.ParagraphProperties.SpacingBetweenLines?.Line?.Value, Is.EqualTo("600"));
+ Assert.That(p.ParagraphProperties.Indentation?.Right?.Value, Is.EqualTo("239"));
});
var borders = p.ParagraphProperties?.ParagraphBorders?.Elements();
@@ -144,5 +147,63 @@ public void WithOnlyLineBreak_ReturnsEmptyRun()
});
Assert.That(((Text)lastRun.LastChild).Text, Is.Empty);
}
+
+ [Test(Description = "Border defined on container should render its content with one bordered frame #168")]
+ public async Task WithBorders_MultipleParagraphs_ReturnsAsOneFramedBlock()
+ {
+ await converter.ParseBody(@"
+
+
Header placeholder:
+
+ - Item 1
+ - Item 2
+
+
Footer Placeholder
+
+
");
+ AssertThatOpenXmlDocumentIsValid();
+
+ var paragraphs = mainPart.Document.Body!.Elements();
+ Assert.That(paragraphs, Is.Empty, "Assert that all the paragraphs stand inside the framed table");
+
+ var framedTable = mainPart.Document.Body!.Elements().FirstOrDefault();
+ Assert.That(framedTable, Is.Not.Null);
+
+ var borders = framedTable.GetFirstChild()?.TableBorders;
+ Assert.That(borders, Is.Not.Null, "Assert that border is applied on table scope");
+ Assert.That(borders.Elements()!
+ .Select(b => b.Val?.Value),
+ Has.All.EqualTo(BorderValues.Dashed));
+
+ var cell = framedTable.GetFirstChild()?.GetFirstChild();
+ Assert.That(cell, Is.Not.Null);
+ paragraphs = cell.Elements();
+ Assert.That(paragraphs, Is.Not.Empty);
+
+ Assert.That(paragraphs.Last().ParagraphProperties?.Indentation?.FirstLine?.Value, Is.EqualTo("1080"),
+ "Assert that paragraph with text-indent is preserved");
+ Assert.That(paragraphs.Last().ParagraphProperties?.Indentation?.Right, Is.Null,
+ "Assert that paragraph with right indentation is preserved");
+ }
+
+ [Test(Description = "Background color defined on container should render its content with one bordered frame")]
+ public async Task WithBgcolor_MultipleParagraphs_ReturnsAsOneFramedBlock()
+ {
+ await converter.ParseBody(@"
+
+ Body Placeholder
+ ");
+ AssertThatOpenXmlDocumentIsValid();
+
+ var paragraphs = mainPart.Document.Body!.Elements();
+ Assert.That(paragraphs, Is.Empty, "Assert that all the paragraphs stand inside the framed table");
+
+ var framedTable = mainPart.Document.Body!.Elements().FirstOrDefault();
+ Assert.That(framedTable, Is.Not.Null);
+
+ var shading = framedTable.GetFirstChild()?.GetFirstChild()?.TableCellProperties?.Shading;
+ Assert.That(shading, Is.Not.Null, "Assert that background-color is applied on table scope");
+ Assert.That(shading.Fill?.Value, Is.EqualTo("FFA500"));
+ }
}
}
\ No newline at end of file
diff --git a/test/HtmlToOpenXml.Tests/HeaderFooterTests.cs b/test/HtmlToOpenXml.Tests/HeaderFooterTests.cs
index 1f399e6b..8bb71936 100644
--- a/test/HtmlToOpenXml.Tests/HeaderFooterTests.cs
+++ b/test/HtmlToOpenXml.Tests/HeaderFooterTests.cs
@@ -109,5 +109,48 @@ public async Task WithExistingHeader_Even_ReturnsAnotherHeaderPart()
});
AssertThatOpenXmlDocumentIsValid();
}
+
+ [Test]
+ public async Task Header_ReturnsStyleParagraphs()
+ {
+ await converter.ParseHeader(@"
+
+ ");
+
+ var header = mainPart.HeaderParts.FirstOrDefault()?.Header;
+ Assert.That(header, Is.Not.Null);
+ var paragraphs = header.Elements();
+ Assert.That(paragraphs.Count(), Is.EqualTo(3));
+ Assert.That(paragraphs.First().ParagraphProperties?.ParagraphStyleId?.Val?.Value,
+ Is.EqualTo(converter.HtmlStyles.DefaultStyles.HeaderStyle));
+ Assert.That(paragraphs.Skip(1).Select(p => p.ParagraphProperties?.ParagraphStyleId?.Val?.Value),
+ Has.All.EqualTo(converter.HtmlStyles.DefaultStyles.ListParagraphStyle));
+ }
+
+ [Test]
+ public async Task Footer_ReturnsStyleParagraphs()
+ {
+ await converter.ParseFooter(@"
+
+ ");
+
+ var footer = mainPart.FooterParts.FirstOrDefault()?.Footer;
+ Assert.That(footer, Is.Not.Null);
+ var paragraphs = footer.Elements();
+ Assert.That(paragraphs.Count(), Is.EqualTo(2));
+ Assert.That(paragraphs.Select(p => p.ParagraphProperties?.ParagraphStyleId?.Val?.Value),
+ Has.All.EqualTo(converter.HtmlStyles.DefaultStyles.FooterStyle));
+ }
}
}
\ No newline at end of file
diff --git a/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs b/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
index 3e056798..c58459de 100644
--- a/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
+++ b/test/HtmlToOpenXml.Tests/HtmlConverterTestBase.cs
@@ -49,6 +49,8 @@ protected void AssertThatOpenXmlDocumentIsValid()
foreach (ValidationErrorInfo error in errors)
{
TestContext.Error.Write("{0}\n\t{1}\n", error.Path?.XPath, error.Description);
+ if (error.Node is not null)
+ TestContext.Error.WriteLine("\n\t{0}", error.Node.OuterXml);
}
Assert.Fail("The document isn't conformant with Office 2021");
diff --git a/test/HtmlToOpenXml.Tests/ImgTests.cs b/test/HtmlToOpenXml.Tests/ImgTests.cs
index 64d97ae6..15e5596d 100644
--- a/test/HtmlToOpenXml.Tests/ImgTests.cs
+++ b/test/HtmlToOpenXml.Tests/ImgTests.cs
@@ -80,6 +80,22 @@ public void ManualProvisioning_WithNoContent_ShouldBeIgnored()
Assert.That(elements, Is.Empty);
}
+ [Test(Description = "Reading image from a local base image url.")]
+ public async Task FileSystem_LocalImage_WithBaseUri_ShouldSucceed()
+ {
+ string baseUri = TestContext.CurrentContext.WorkDirectory;
+
+ using var resourceStream = ResourceHelper.GetStream("Resources.html2openxml.jpg");
+ using (var fileStream = File.OpenWrite(Path.Combine(baseUri, "html2openxml.jpg")))
+ await resourceStream.CopyToAsync(fileStream);
+
+ converter = new(mainPart, new IO.DefaultWebRequest { BaseImageUrl = new Uri(baseUri) });
+
+ var elements = await converter.ParseAsync("
");
+ Assert.That(elements.Count(), Is.EqualTo(1));
+ AssertIsImg(mainPart, elements.First());
+ }
+
[Test(Description = "Reading local file containing a space in the name")]
public async Task FileSystem_LocalImage_WithSpaceInName_ShouldSucceed()
{
diff --git a/test/HtmlToOpenXml.Tests/NumberingTests.cs b/test/HtmlToOpenXml.Tests/NumberingTests.cs
index 5288765e..a7b914e2 100644
--- a/test/HtmlToOpenXml.Tests/NumberingTests.cs
+++ b/test/HtmlToOpenXml.Tests/NumberingTests.cs
@@ -10,6 +10,9 @@ namespace HtmlToOpenXml.Tests
[TestFixture]
public class NumberingTests : HtmlConverterTestBase
{
+ const int maxLevel = 8;
+
+
[Test(Description = "Skip any elements that is not a `li` tag")]
public void NonLiElement_ShouldBeIgnored()
{
@@ -27,12 +30,13 @@ public void NonLiElement_ShouldBeIgnored()
}
[Test(Description = "Two consecutive lists should restart numbering to 1")]
- public void ConsecutiveList_ReturnsList_RestartingOrder()
+ public async Task ConsecutiveList_ReturnsList_RestartingOrder()
{
- var elements = converter.Parse(@"
+ await converter.ParseBody(@"
- Item 1.1
placeholder
- Item 2.1
");
+ var elements = mainPart.Document.Body!.ChildElements;
Assert.Multiple(() => {
Assert.That(elements, Has.Count.EqualTo(3));
Assert.That(elements, Is.All.TypeOf());
@@ -59,18 +63,20 @@ public void ConsecutiveList_ReturnsList_RestartingOrder()
Is.Not.EqualTo(p2.ParagraphProperties?.NumberingProperties?.NumberingId?.Val?.Value),
"Expected two different list instances");
});
+ AssertThatOpenXmlDocumentIsValid();
}
[Test]
- public void NestedNumberList_ReturnsMultilevelList()
+ public async Task NestedNumberList_ReturnsMultilevelList()
{
- var elements = converter.Parse(
+ await converter.ParseBody(
@"
- Item 1
- Item 1.1
- Item 2
");
+ var elements = mainPart.Document.Body!.ChildElements;
Assert.Multiple(() => {
Assert.That(elements, Has.Count.EqualTo(3));
Assert.That(elements, Is.All.TypeOf());
@@ -113,6 +119,7 @@ public void NestedNumberList_ReturnsMultilevelList()
Assert.That(p1_1.ParagraphProperties?.NumberingProperties?.NumberingLevelReference?.Val?.Value, Is.EqualTo(1));
Assert.That(p2.ParagraphProperties?.NumberingProperties?.NumberingLevelReference?.Val?.Value, Is.EqualTo(0));
});
+ AssertThatOpenXmlDocumentIsValid();
}
[Test(Description = "Empty list should not be registred")]
@@ -177,7 +184,6 @@ public void WithExistingNumbering_ReturnsUniqueInstanceId()
[Test(Description = "Word doesn't display more than 8 deep levels.")]
public void MaxNumberingLevel_ShouldBeIgnored()
{
- const int maxLevel = 8;
var sb = new System.Text.StringBuilder();
for (int i = 0; i <= maxLevel; i++)
sb.AppendFormat("- Item {0}", i+1);
@@ -329,9 +335,9 @@ public async Task DisableContinueNumbering_ReturnsSecondList_RestartingOrder()
/// Tiered numbering such as: 1, 1.1, 1.1.1
///
[Test(Description = "Nested numbering (issue #81)")]
- public void DecimalTieredStyle_ReturnsListWithTieredNumbering()
+ public async Task DecimalTieredStyle_ReturnsListWithTieredNumbering()
{
- var elements = converter.Parse(
+ await converter.ParseBody(
@"
- Item 1
- Item 1.1
@@ -339,6 +345,7 @@ public void DecimalTieredStyle_ReturnsListWithTieredNumbering()
- Item 2
");
+ var elements = mainPart.Document.Body!.ChildElements;
var absNum = mainPart.NumberingDefinitionsPart?.Numbering
.Elements()
.SingleOrDefault();
@@ -347,10 +354,15 @@ public void DecimalTieredStyle_ReturnsListWithTieredNumbering()
var instances = mainPart.NumberingDefinitionsPart?.Numbering
.Elements().Where(i => i.AbstractNumId!.Val == absNum.AbstractNumberId);
Assert.That(instances, Is.Not.Null);
+ var levels = absNum.Elements();
Assert.Multiple(() =>
{
Assert.That(instances.Count(), Is.EqualTo(1));
Assert.That(instances.Select(i => i.NumberID?.HasValue), Has.All.True);
+ Assert.That(levels.Count(), Is.EqualTo(maxLevel + 1));
+ Assert.That(levels.Select(l => l.NumberingFormat?.Val?.Value), Has.All.EqualTo(NumberFormatValues.Decimal));
+ Assert.That(levels.Select(l => l.PreviousParagraphProperties?.Indentation?.Left?.Value), Has.All.EqualTo("0"),
+ "Decimal Tiered style must all be aligned on left with no indent");
});
Assert.That(elements, Is.Not.Empty);
@@ -359,6 +371,7 @@ public void DecimalTieredStyle_ReturnsListWithTieredNumbering()
e.ParagraphProperties?.NumberingProperties?.NumberingId?.Val?.Value),
Has.All.EqualTo(instances.First().NumberID!.Value),
"All paragraphs are linked to the same list instance");
+ AssertThatOpenXmlDocumentIsValid();
}
[Test(Description = "Allow to specify another start value for the first item of a `ol` list")]
@@ -392,12 +405,13 @@ public void OverrideStartNumber_WithUl_ShouldBeIgnored()
}
[Test]
- public void RomanList_ReturnsListWithCustomStyle()
+ public async Task RomanList_ReturnsListWithCustomStyle()
{
- var elements = converter.Parse(@"
+ 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;
@@ -418,12 +432,13 @@ public void RomanList_ReturnsListWithCustomStyle()
.Elements