diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b8626..419c498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ All notable changes to this project will be documented in this file. - CSS-based label filtering enables responsive toggle without any re-rendering ### Fixed +- **Table container missing top margin (Issue #108)** - Tables preceded by paragraphs with no after-spacing now get a default `margin-top: 7.5pt` for visual separation + - Also handles floating table spacing from `w:tblpPr` (`topFromText`/`bottomFromText` attributes) + - Tables preceded by paragraphs with explicit after-spacing correctly skip the default margin - **Move markup Word compatibility (Issue #96)** - Documents with move operations no longer cause Word "unreadable content" warnings - Root cause: `FixUpRevMarkIds()` was overwriting IDs of `w:del`/`w:ins` after `FixUpRevisionIds()` had already assigned unique IDs, causing collisions with move element IDs - Fix: Removed redundant `FixUpRevMarkIds()` call - `FixUpRevisionIds()` already handles all revision element IDs correctly diff --git a/Docxodus.Tests/HtmlConverterTests.cs b/Docxodus.Tests/HtmlConverterTests.cs index 5b2ab4d..11586e3 100644 --- a/Docxodus.Tests/HtmlConverterTests.cs +++ b/Docxodus.Tests/HtmlConverterTests.cs @@ -2904,6 +2904,121 @@ public void HC041_TablePercentageWidth_RendersCorrectly() } } + [Fact] + public void HC056_TablePrecededByParagraph_HasTopMargin() + { + // Test that a table preceded by a paragraph with no after-spacing gets a default margin-top + using (MemoryStream ms = new MemoryStream()) + { + using (WordprocessingDocument wDoc = WordprocessingDocument.Create(ms, DocumentFormat.OpenXml.WordprocessingDocumentType.Document)) + { + var mainPart = wDoc.AddMainDocumentPart(); + + var stylesPart = mainPart.AddNewPart(); + stylesPart.Styles = new Styles( + new DocDefaults( + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + new RunFonts { Ascii = "Times New Roman" }, + new FontSize { Val = "24" })))); + stylesPart.Styles.Save(); + + var settingsPart = mainPart.AddNewPart(); + settingsPart.Settings = new Settings(); + settingsPart.Settings.Save(); + + // Create a paragraph followed by a table, with no explicit spacing + mainPart.Document = new Document( + new Body( + new Paragraph( + new Run(new Text("Text before table"))), + new DocumentFormat.OpenXml.Wordprocessing.Table( + new TableProperties( + new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }), + new DocumentFormat.OpenXml.Wordprocessing.TableRow( + new DocumentFormat.OpenXml.Wordprocessing.TableCell( + new TableCellProperties( + new TableCellWidth { Width = "5000", Type = TableWidthUnitValues.Pct }), + new Paragraph( + new Run(new Text("Table cell")))))))); + + mainPart.Document.Save(); + + var settings = new WmlToHtmlConverterSettings + { + PageTitle = "Table Spacing Test" + }; + + var html = WmlToHtmlConverter.ConvertToHtml(wDoc, settings); + var htmlString = html.ToString(); + + // Table should have a margin-top for visual separation + Assert.Contains("margin-top: 7.5pt", htmlString); + Assert.Contains("Text before table", htmlString); + Assert.Contains("Table cell", htmlString); + } + } + } + + [Fact] + public void HC057_TableWithParagraphSpacing_NoExtraMargin() + { + // Test that when the preceding paragraph has after-spacing, no extra margin-top is added + using (MemoryStream ms = new MemoryStream()) + { + using (WordprocessingDocument wDoc = WordprocessingDocument.Create(ms, DocumentFormat.OpenXml.WordprocessingDocumentType.Document)) + { + var mainPart = wDoc.AddMainDocumentPart(); + + var stylesPart = mainPart.AddNewPart(); + stylesPart.Styles = new Styles( + new DocDefaults( + new ParagraphPropertiesDefault( + new ParagraphPropertiesBaseStyle( + new SpacingBetweenLines { After = "200" })), + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + new RunFonts { Ascii = "Times New Roman" }, + new FontSize { Val = "24" })))); + stylesPart.Styles.Save(); + + var settingsPart = mainPart.AddNewPart(); + settingsPart.Settings = new Settings(); + settingsPart.Settings.Save(); + + // Create a paragraph with after-spacing followed by a table + mainPart.Document = new Document( + new Body( + new Paragraph( + new Run(new Text("Spaced paragraph"))), + new DocumentFormat.OpenXml.Wordprocessing.Table( + new TableProperties( + new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }), + new DocumentFormat.OpenXml.Wordprocessing.TableRow( + new DocumentFormat.OpenXml.Wordprocessing.TableCell( + new TableCellProperties( + new TableCellWidth { Width = "5000", Type = TableWidthUnitValues.Pct }), + new Paragraph( + new Run(new Text("Table cell")))))))); + + mainPart.Document.Save(); + + var settings = new WmlToHtmlConverterSettings + { + PageTitle = "Table Spacing Test" + }; + + var html = WmlToHtmlConverter.ConvertToHtml(wDoc, settings); + var htmlString = html.ToString(); + + // Table should NOT have the default 7.5pt margin-top since paragraph has spacing + Assert.DoesNotContain("margin-top: 7.5pt", htmlString); + Assert.Contains("Spaced paragraph", htmlString); + Assert.Contains("Table cell", htmlString); + } + } + } + [Fact] public void HC042_UnknownSerifFont_GetsSerifFallback() { diff --git a/Docxodus/WmlToHtmlConverter.cs b/Docxodus/WmlToHtmlConverter.cs index 08ac1bc..3eff9fa 100644 --- a/Docxodus/WmlToHtmlConverter.cs +++ b/Docxodus/WmlToHtmlConverter.cs @@ -4464,6 +4464,52 @@ private static object ProcessTable(WordprocessingDocument wordDoc, WmlToHtmlConv } } } + // Handle table spacing from w:tblpPr (floating table positioning properties) + var tblpPr = element.Elements(W.tblPr).Elements(W.tblpPr).FirstOrDefault(); + if (tblpPr != null) + { + var topFromText = (decimal?)tblpPr.Attribute(W.topFromText); + if (topFromText != null && topFromText > 0) + style.AddIfMissing("margin-top", + string.Format(NumberFormatInfo.InvariantInfo, "{0}pt", topFromText / 20m)); + + var bottomFromText = (decimal?)tblpPr.Attribute(W.bottomFromText); + if (bottomFromText != null && bottomFromText > 0) + style.AddIfMissing("margin-bottom", + string.Format(NumberFormatInfo.InvariantInfo, "{0}pt", bottomFromText / 20m)); + } + + // Look for spacing from the preceding paragraph's w:spacing w:after. + // If the preceding sibling is a paragraph with no after-spacing (or zero), + // the table needs its own top margin for visual separation. + // Word applies implicit spacing between paragraphs and tables; replicate + // that by examining the preceding element's spacing. + if (!style.ContainsKey("margin-top")) + { + var precedingSibling = element.ElementsBeforeSelf().LastOrDefault(); + bool needsDefaultTopMargin = false; + + if (precedingSibling != null && precedingSibling.Name == W.p) + { + var precedingPPr = precedingSibling.Element(W.pPr); + var precedingSpacing = precedingPPr?.Element(W.spacing); + var afterVal = (decimal?)precedingSpacing?.Attribute(W.after); + + // If preceding paragraph has no spacing-after or zero, table needs top margin + if (precedingSpacing == null || afterVal == null || afterVal == 0) + needsDefaultTopMargin = true; + } + else if (precedingSibling != null) + { + // Non-paragraph preceding element (e.g., another table) also needs separation + needsDefaultTopMargin = true; + } + + if (needsDefaultTopMargin) + style.AddIfMissing("margin-top", "7.5pt"); + } + style.AddIfMissing("margin-top", ".001pt"); + var tableDirection = bidiVisual != null ? new XAttribute("dir", "rtl") : new XAttribute("dir", "ltr"); style.AddIfMissing("margin-bottom", ".001pt"); var table = new XElement(Xhtml.table,