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:

+
    +
  1. Item 1
  2. +
  3. Item 2
  4. +
+

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 @@ - - - -
    -
  1. This is H1 -
      -
    1. This is H2
    2. -
    3. Another H2
    4. -
    -
  2. -
  3. This is another H1 -
      -
    1. This is H2
    2. -
    3. Another H2
    4. -
    -
  4. -
  5. Last H1 -
      -
    1. This is H2
    2. -
    3. Another H2
    4. -
    -
  6. -
- -
- - 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:

-
    -
  1. Coffee
  2. -
  3. Tea
  4. -
  5. Milk - -
  6. -
  7. Wine
  8. -
- -

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:

+
    +
  1. Item 1
  2. +
  3. Item 2
  4. +
+

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(@"
+
Header placeholder
+

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(@" +
+

Placeholder +

+

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

placeholder

  1. 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( @"
  1. Item 1
    1. Item 1.1
  2. 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("
  1. 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( @"
    1. Item 1
      1. Item 1.1
      @@ -339,6 +345,7 @@ public void DecimalTieredStyle_ReturnsListWithTieredNumbering()
    2. 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(@"
      • 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; @@ -418,12 +432,13 @@ public void RomanList_ReturnsListWithCustomStyle() .Elements