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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions Docxodus.Tests/HtmlConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<StyleDefinitionsPart>();
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<DocumentSettingsPart>();
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<StyleDefinitionsPart>();
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<DocumentSettingsPart>();
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()
{
Expand Down
46 changes: 46 additions & 0 deletions Docxodus/WmlToHtmlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading