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 @@ -1280,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 @@ -1341,47 +1341,40 @@

/// <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.
/// while collapsing against the immediately preceding paragraph's SpaceAfter.
/// CSS-like margin-collapse: if the previous body element is any paragraph
/// that already supplies a positive trailing gap (the after-spacer of a
/// sibling table/quote-table, a heading rendered as a single paragraph, or
/// regular body text with SpaceAfter), the requested before-spacing is
/// folded into that paragraph as max(existing, desired) rather than stacked
/// as a second paragraph. This prevents an extra blank line between a
/// heading/text and a following table/quote-table.
/// </summary>
private void AddCollapsingBeforeSpacer(string spacing)
{
if (string.IsNullOrEmpty(spacing) || spacing == "0") return;

if (_body.LastChild is Paragraph prev && IsBodySpacerParagraph(prev))
if (_body.LastChild is Paragraph prev)
{
var existingSpacing = prev.ParagraphProperties!.GetFirstChild<SpacingBetweenLines>()!;
int existing = ParseTwipsOrZero(existingSpacing.After?.Value);
int desired = ParseTwipsOrZero(spacing);
if (desired > existing)
var existingSpacing = prev.ParagraphProperties?.GetFirstChild<SpacingBetweenLines>();
if (existingSpacing != null)
{
existingSpacing.After = desired.ToString(CultureInfo.InvariantCulture);
int existingAfter = ParseTwipsOrZero(existingSpacing.After?.Value);
if (existingAfter > 0)
{
int desired = ParseTwipsOrZero(spacing);
if (desired > existingAfter)
{
existingSpacing.After = desired.ToString(CultureInfo.InvariantCulture);
}
return;
}
}
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 @@ -831,6 +831,93 @@ public void AddQuote_AdjacentTableQuotes_ShouldKeepLargerCollapsedSpacing()
"the gap should collapse to max(prev.SpaceAfter, next.SpaceBefore)");
}

[Fact]
public void AddQuote_AfterHeading_ShouldCollapseAgainstHeadingSpaceAfter()
{
// Arrange: H2 single-paragraph (default ShowBorder=false → AddHeadingSingleParagraph)
// followed by a table-rendered quote (BG + PaddingSpace). Issue #78: heading's
// SpaceAfter and quote's SpaceBefore must collapse so no extra blank paragraph
// sits between them.
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var headingStyle = CreateDefaultHeadingStyle() with
{
SpaceBefore = "480",
SpaceAfter = "280"
};
var quoteStyle = CreateDefaultQuoteStyle() with
{
BackgroundColor = "fdf0ed",
PaddingSpace = 6,
SpaceBefore = "160",
SpaceAfter = "160"
};

// Act
builder.AddHeading(2, "Summary", headingStyle);
builder.AddQuote(ToQuoteContent("note"), quoteStyle);
builder.Save();

// Assert: body sequence is heading-paragraph, then table — no spurious spacer.
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
var bodyChildren = doc.MainDocumentPart!.Document.Body!.ChildElements.ToList();

var headingIdx = bodyChildren.FindIndex(c =>
c is Paragraph p && p.Descendants<Text>().Any(t => t.Text == "Summary"));
headingIdx.Should().BeGreaterOrEqualTo(0, "the heading paragraph should be in the body");

var tableIdx = bodyChildren.FindIndex(headingIdx + 1, c => c is Table);
tableIdx.Should().Be(headingIdx + 1,
"no spacer paragraph should be inserted between the heading and the quote-table");

// The heading's SpaceAfter must have absorbed (max) the quote's SpaceBefore.
var headingParagraph = (Paragraph)bodyChildren[headingIdx];
var headingSpacing = headingParagraph.ParagraphProperties!.GetFirstChild<SpacingBetweenLines>()!;
headingSpacing.After!.Value.Should().Be("280",
"max(headingSpaceAfter=280, quoteSpaceBefore=160) = 280");
}

[Fact]
public void AddQuote_AfterHeading_ShouldGrowHeadingSpaceAfterWhenQuoteWantsLargerGap()
{
// Arrange: quote demands a larger gap than the heading's own SpaceAfter — the
// heading paragraph's After should grow (not stack a new spacer).
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var headingStyle = CreateDefaultHeadingStyle() with
{
SpaceBefore = "240",
SpaceAfter = "100"
};
var quoteStyle = CreateDefaultQuoteStyle() with
{
BackgroundColor = "fdf0ed",
PaddingSpace = 6,
SpaceBefore = "320",
SpaceAfter = "160"
};

// Act
builder.AddHeading(3, "Section", headingStyle);
builder.AddQuote(ToQuoteContent("body"), quoteStyle);
builder.Save();

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

var headingIdx = bodyChildren.FindIndex(c =>
c is Paragraph p && p.Descendants<Text>().Any(t => t.Text == "Section"));
var tableIdx = bodyChildren.FindIndex(headingIdx + 1, c => c is Table);

tableIdx.Should().Be(headingIdx + 1, "still no spacer paragraph between");

var headingSpacing = ((Paragraph)bodyChildren[headingIdx])
.ParagraphProperties!.GetFirstChild<SpacingBetweenLines>()!;
headingSpacing.After!.Value.Should().Be("320",
"max(100, 320) = 320 — heading's After is grown to satisfy the quote's request");
}

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