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
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@
// Adding indent equal to BorderSpace * 20 twips anchors the border at the margin boundary.
if (style.BorderSpace > 0)
{
string indentTwips = (style.BorderSpace * 20).ToString();

Check warning on line 875 in csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs

View workflow job for this annotation

GitHub Actions / Build and Test

The behavior of 'uint.ToString()' could vary based on the current user's locale settings. Replace this call in 'OpenXmlDocumentBuilder.CreateCodeBlockParagraphProperties(CodeBlockStyle)' with a call to 'uint.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

Check warning on line 875 in csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs

View workflow job for this annotation

GitHub Actions / Build and Test

The behavior of 'uint.ToString()' could vary based on the current user's locale settings. Replace this call in 'OpenXmlDocumentBuilder.CreateCodeBlockParagraphProperties(CodeBlockStyle)' with a call to 'uint.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
props.AppendChild(new Indentation { Left = indentTwips, Right = indentTwips });
}

Expand Down Expand Up @@ -941,7 +941,11 @@
int cellWidthTwips = Math.Max(0, fullTextAreaTwips - leftIndentTwips);

// Spacer paragraph before the quote (mirrors AddTable spacing semantics).
AddTableSpacer(style.SpaceBefore);
// Collapsing semantics: when the previous body element is already an empty
// spacer paragraph (e.g., the after-spacer of an immediately preceding
// quote-table or table), do not stack a second spacer — keep the larger
// gap of the two so adjacent ::: note blocks don't accumulate whitespace.
AddCollapsingBeforeSpacer(style.SpaceBefore);

var table = _body.AppendChild(new Table());

Expand Down Expand Up @@ -1276,7 +1280,7 @@
var indentation = new Indentation { Left = style.LeftIndent };
if (hasPadding && style.PaddingSpace > 0)
{
indentation.Right = (style.PaddingSpace * 20).ToString();

Check warning on line 1283 in csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs

View workflow job for this annotation

GitHub Actions / Build and Test

The behavior of 'uint.ToString()' could vary based on the current user's locale settings. Replace this call in 'OpenXmlDocumentBuilder.CreateQuoteParagraphProperties(QuoteStyle)' with a call to 'uint.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

Check warning on line 1283 in csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs

View workflow job for this annotation

GitHub Actions / Build and Test

The behavior of 'uint.ToString()' could vary based on the current user's locale settings. Replace this call in 'OpenXmlDocumentBuilder.CreateQuoteParagraphProperties(QuoteStyle)' with a call to 'uint.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
}
props.AppendChild(indentation);

Expand Down Expand Up @@ -1305,8 +1309,9 @@
- pageConfig.GutterMargin;
int textAreaTwips = (int)(fullTextAreaTwips * style.WidthPercent / 100.0);

// Spacer paragraph before table
AddTableSpacer(style.SpaceBefore);
// Spacer paragraph before table (collapses with a preceding spacer paragraph
// — see AddCollapsingBeforeSpacer for the margin-collapse rationale).
AddCollapsingBeforeSpacer(style.SpaceBefore);

var table = _body.AppendChild(new Table());
table.AppendChild(CreateTableProperties(style));
Expand Down Expand Up @@ -1334,6 +1339,49 @@
spacer.AppendChild(spacerProps);
}

/// <summary>
/// Adds a "before" spacer in front of a block element (table or quote-table)
/// while collapsing against an immediately preceding spacer paragraph. When
/// the previous body element is already an empty spacer paragraph (typically
/// the after-spacer of a sibling table/quote-table), the larger of the two
/// requested gaps is preserved instead of stacking two paragraphs. This
/// prevents the visible whitespace doubling reported when adjacent ::: note
/// blocks render as single-cell tables.
/// </summary>
private void AddCollapsingBeforeSpacer(string spacing)
{
if (string.IsNullOrEmpty(spacing) || spacing == "0") return;

if (_body.LastChild is Paragraph prev && IsBodySpacerParagraph(prev))
{
var existingSpacing = prev.ParagraphProperties!.GetFirstChild<SpacingBetweenLines>()!;
int existing = ParseTwipsOrZero(existingSpacing.After?.Value);
int desired = ParseTwipsOrZero(spacing);
if (desired > existing)
{
existingSpacing.After = desired.ToString(CultureInfo.InvariantCulture);
}
return;
}

AddTableSpacer(spacing);
}

/// <summary>
/// Returns true if the paragraph is one of our body-level spacer paragraphs:
/// no runs/hyperlinks and a SpacingBetweenLines element with Before="0" and a
/// non-empty After value. Heading after-spacers also match this shape, which
/// is intentional — collapsing against them preserves the larger gap.
/// </summary>
private static bool IsBodySpacerParagraph(Paragraph p)
{
if (p.Elements<Run>().Any()) return false;
if (p.Elements<Hyperlink>().Any()) return false;
var spacing = p.ParagraphProperties?.GetFirstChild<SpacingBetweenLines>();
if (spacing == null) return false;
return spacing.Before?.Value == "0" && !string.IsNullOrEmpty(spacing.After?.Value);
}

/// <summary>
/// Creates TableProperties with percentage-based width (overflow safety net) and
/// fixed layout so Word honours the tblGrid column widths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,127 @@ public void AddQuote_WithValidParameters_ShouldAddQuote()
textContent.Should().Contain("This is a quoted text.");
}

[Fact]
public void AddQuote_AdjacentTableQuotes_ShouldCollapseSpacersToSingleParagraph()
{
// Arrange: BG + PaddingSpace forces table-mode (issue #76 path).
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var style = CreateDefaultQuoteStyle() with
{
BackgroundColor = "fdf0ed",
PaddingSpace = 4,
SpaceBefore = "160",
SpaceAfter = "160"
};

// Act: render three adjacent ::: note blocks back-to-back.
builder.AddQuote(ToQuoteContent("first"), style);
builder.AddQuote(ToQuoteContent("second"), style);
builder.AddQuote(ToQuoteContent("third"), style);
builder.Save();

// Assert: between each pair of <w:tbl> there is exactly one empty
// spacer paragraph (the previous after-spacer), not two.
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
var bodyChildren = doc.MainDocumentPart!.Document.Body!.ChildElements.ToList();

var tableIndices = bodyChildren
.Select((el, i) => (el, i))
.Where(t => t.el is Table)
.Select(t => t.i)
.ToList();

tableIndices.Count.Should().Be(3, "three quote-tables were added");

// Between consecutive tables, only one body element (the spacer) should sit.
for (int k = 0; k < tableIndices.Count - 1; k++)
{
int gap = tableIndices[k + 1] - tableIndices[k] - 1;
gap.Should().Be(1,
$"adjacent quote-tables should have exactly one spacer paragraph between them, but found {gap}");
}
}

[Fact]
public void AddQuote_AdjacentTableQuotes_ShouldKeepLargerCollapsedSpacing()
{
// Arrange: differing SpaceAfter / SpaceBefore values should collapse to max,
// matching CSS-like margin-collapse semantics.
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var firstStyle = CreateDefaultQuoteStyle() with
{
BackgroundColor = "fdf0ed",
PaddingSpace = 4,
SpaceBefore = "100",
SpaceAfter = "100"
};
var secondStyle = CreateDefaultQuoteStyle() with
{
BackgroundColor = "fdf0ed",
PaddingSpace = 4,
SpaceBefore = "400",
SpaceAfter = "100"
};

// Act
builder.AddQuote(ToQuoteContent("first"), firstStyle);
builder.AddQuote(ToQuoteContent("second"), secondStyle);
builder.Save();

// Assert
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
var bodyChildren = doc.MainDocumentPart!.Document.Body!.ChildElements.ToList();

var firstTableIdx = bodyChildren.FindIndex(c => c is Table);
var secondTableIdx = bodyChildren.FindIndex(firstTableIdx + 1, c => c is Table);

secondTableIdx.Should().Be(firstTableIdx + 2, "exactly one collapsed spacer between tables");

var spacer = (Paragraph)bodyChildren[firstTableIdx + 1];
var spacing = spacer.ParagraphProperties!.GetFirstChild<SpacingBetweenLines>()!;
spacing.After!.Value.Should().Be("400",
"the gap should collapse to max(prev.SpaceAfter, next.SpaceBefore)");
}

[Fact]
public void AddTable_AdjacentTables_ShouldCollapseSpacersToSingleParagraph()
{
// Arrange: same collapse rule applies to plain Markdown tables — guard so
// the rare case of consecutive tables doesn't regress either.
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var tableData = new TableData
{
ColumnCount = 1,
Rows =
[
new TableRowData
{
IsHeader = false,
Cells = [new TableCellData { Runs = [new InlineRun { Text = "x" }], Alignment = "left" }]
}
]
};
var style = CreateDefaultTableStyle();

// Act
builder.AddTable(tableData, style);
builder.AddTable(tableData, style);
builder.Save();

// Assert
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
var bodyChildren = doc.MainDocumentPart!.Document.Body!.ChildElements.ToList();

var firstTableIdx = bodyChildren.FindIndex(c => c is Table);
var secondTableIdx = bodyChildren.FindIndex(firstTableIdx + 1, c => c is Table);

(secondTableIdx - firstTableIdx).Should().Be(2,
"exactly one spacer paragraph should sit between adjacent tables");
}

[Fact]
public void AddThematicBreak_ShouldAddHorizontalLine()
{
Expand Down
Loading