From 994f5cf46221191ebe142007a4a87a4b859e42d9 Mon Sep 17 00:00:00 2001 From: Olivier Nizet Date: Tue, 15 Oct 2024 22:46:21 +0200 Subject: [PATCH] Bordered container now use table instead of paragraphs with no spacing between lines #168 --- .../Expressions/BlockElementExpression.cs | 90 ++++++++++++++----- test/HtmlToOpenXml.Tests/DivTests.cs | 43 +++++++-- 2 files changed, 102 insertions(+), 31 deletions(-) diff --git a/src/Html2OpenXml/Expressions/BlockElementExpression.cs b/src/Html2OpenXml/Expressions/BlockElementExpression.cs index 2091ea5..aad5a06 100644 --- a/src/Html2OpenXml/Expressions/BlockElementExpression.cs +++ b/src/Html2OpenXml/Expressions/BlockElementExpression.cs @@ -27,9 +27,9 @@ class BlockElementExpression: PhrasingElementExpression { private readonly OpenXmlLeafElement[]? defaultStyleProperties; protected readonly ParagraphProperties paraProperties = new(); - // some style attributes, such as borders, must be applied on multiple paragraphs - // in order to render as one single block of content. - protected bool renderAsOneBlock; + // 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) @@ -58,20 +58,69 @@ public override IEnumerable Interpret (ParsingContext context) p.AppendChild(new BookmarkEnd() { Id = bookmarkId }); } - if (!renderAsOneBlock || childElements.Count() < 2) + if (!renderAsFramed) return childElements; - // to group together those paragraphs, we must force some indentation requirement - foreach (var p in childElements.OfType()) + var paragraphs = childElements.OfType(); + if (!paragraphs.Any()) return childElements; + + // if we have only 1 paragraph, just inline the styles + if (paragraphs.Count() == 1) { - p.ParagraphProperties ??= new(); - // do not override indentation if `text-indent` was previously set - if ((p.ParagraphProperties.Indentation?.FirstLine?.HasValue) != true) + var p = paragraphs.First(); + + if (!styleBorder.IsEmpty && p.ParagraphProperties?.ParagraphBorders is null) { - p.ParagraphProperties.Indentation = new() { Right = "0" }; + 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)]; + } + + /// + /// 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.Auto, Width = "0" } // 100% + }, + new TableGrid( + new GridColumn() { Width = "5610" }), + 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 childElements; + + return framedTable; } protected override IEnumerable Interpret ( @@ -163,18 +212,10 @@ 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; - renderAsOneBlock = true; + renderAsFramed = true; } foreach (string className in node.ClassList) @@ -187,8 +228,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) @@ -264,6 +305,9 @@ protected override void ComposeStyles (ParsingContext context) }; } } + + if (runProperties.Shading != null) + renderAsFramed = true; } /// diff --git a/test/HtmlToOpenXml.Tests/DivTests.cs b/test/HtmlToOpenXml.Tests/DivTests.cs index 2fa421c..25e8098 100644 --- a/test/HtmlToOpenXml.Tests/DivTests.cs +++ b/test/HtmlToOpenXml.Tests/DivTests.cs @@ -149,9 +149,9 @@ public void WithOnlyLineBreak_ReturnsEmptyRun() } [Test(Description = "Border defined on container should render its content with one bordered frame #168")] - public async Task WithBorders_ReturnsAsOneFramedBlock() + public async Task WithBorders_MultipleParagraphs_ReturnsAsOneFramedBlock() { - await converter.ParseBody(@"
+ await converter.ParseBody(@"

Header placeholder:

    @@ -164,19 +164,46 @@ public async Task WithBorders_ReturnsAsOneFramedBlock() AssertThatOpenXmlDocumentIsValid(); var paragraphs = mainPart.Document.Body!.Elements(); - Assert.That(paragraphs, Is.Not.Empty); - Assert.That(paragraphs.Select(p => p.ParagraphProperties?.ParagraphBorders), Has.All.Not.Empty); - Assert.That(paragraphs.SelectMany(p => p.ParagraphProperties?.ParagraphBorders!.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)); - Assert.That(paragraphs.Take(paragraphs.Count() - 1) - .Select(p => p.ParagraphProperties?.Indentation?.Right?.Value), Has.All.EqualTo("0"), - "Assert that all paragraphs right indentation is reset"); + 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