From a6df3484544402270452d201d2d1da6bab270043 Mon Sep 17 00:00:00 2001 From: forest6511 <20209757+forest6511@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:58:30 +0900 Subject: [PATCH] fix: force box-mode rendering for left-only paragraph borders (#61) When only a left border is specified in w:pBdr, Word anchors the border at the paragraph indent position (flush with text), ignoring BorderSpace as a horizontal gap. Adding Nil borders for the unspecified sides forces Word into box-mode rendering, which anchors the left border at the page content area edge so that LeftIndent creates a visible gap between the border line and the text. Add three unit tests covering: - left-only border produces Nil on top/bottom/right (box-mode forced) - all-four borders remain Single (no Nil added) - bottom-only border produces no Nil borders (box-mode not triggered) --- .../OpenXml/OpenXmlDocumentBuilder.cs | 15 +++ .../Unit/OpenXmlDocumentBuilderTests.cs | 97 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs index f47bd12..9584751 100644 --- a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs +++ b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs @@ -156,6 +156,21 @@ private static ParagraphBorders CreateBordersFromPositions( paragraphBorders.AppendChild(border); } + // When only "left" is specified, Word anchors the border at the paragraph indent (flush + // with text) instead of at the page content edge. Adding Nil borders for the unspecified + // sides forces Word into box-mode rendering, which anchors the left border at position 0 + // so that LeftIndent creates a visible gap between the border line and the text. + var specifiedPositions = new HashSet(positions); + if (specifiedPositions.Contains("left")) + { + if (!specifiedPositions.Contains("top")) + paragraphBorders.AppendChild(new TopBorder { Val = BorderValues.Nil }); + if (!specifiedPositions.Contains("bottom")) + paragraphBorders.AppendChild(new BottomBorder { Val = BorderValues.Nil }); + if (!specifiedPositions.Contains("right")) + paragraphBorders.AppendChild(new RightBorder { Val = BorderValues.Nil }); + } + return paragraphBorders; } diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs index c9f95ba..2ca4fcf 100644 --- a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs +++ b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs @@ -2229,6 +2229,103 @@ public void AddHeading_WithLeftIndentAndBorderExtentText_ShouldRenderIndentation indentation!.Left!.Value.Should().Be("400"); } + [Fact] + public void AddHeading_WithLeftBorderOnly_ShouldAddNilBordersForBoxMode() + { + // Arrange + using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider); + var style = CreateDefaultHeadingStyle() with + { + ShowBorder = true, + BorderPosition = "left", + BorderColor = "0066cc", + BorderSize = 24, + BorderSpace = 8, + LeftIndent = "200" + }; + + // Act + builder.AddHeading(3, "Section", style); + builder.Save(); + + // Assert: left border is Single; top/bottom/right are Nil to force box-mode rendering + _stream.Position = 0; + using var doc = WordprocessingDocument.Open(_stream, false); + var paragraph = doc.MainDocumentPart!.Document.Body! + .Descendants() + .First(p => p.ParagraphProperties?.ParagraphBorders != null); + + var borders = paragraph.ParagraphProperties!.ParagraphBorders!; + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil); + } + + [Fact] + public void AddHeading_WithAllFourBorders_ShouldNotAddNilBorders() + { + // Arrange + using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider); + var style = CreateDefaultHeadingStyle() with + { + ShowBorder = true, + BorderPosition = "left,top,right,bottom", + BorderColor = "0066cc", + BorderSize = 12, + BorderSpace = 4 + }; + + // Act + builder.AddHeading(2, "Section", style); + builder.Save(); + + // Assert: all four borders are Single, none are Nil + _stream.Position = 0; + using var doc = WordprocessingDocument.Open(_stream, false); + var paragraph = doc.MainDocumentPart!.Document.Body! + .Descendants() + .First(p => p.ParagraphProperties?.ParagraphBorders != null); + + var borders = paragraph.ParagraphProperties!.ParagraphBorders!; + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + } + + [Fact] + public void AddHeading_WithBottomBorderOnly_ShouldNotAddNilBorders() + { + // Arrange + using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider); + var style = CreateDefaultHeadingStyle() with + { + ShowBorder = true, + BorderPosition = "bottom", + BorderColor = "cccccc", + BorderSize = 8, + BorderSpace = 2 + }; + + // Act + builder.AddHeading(2, "Section", style); + builder.Save(); + + // Assert: only bottom border exists, no Nil borders added (box-mode only applies to left) + _stream.Position = 0; + using var doc = WordprocessingDocument.Open(_stream, false); + var paragraph = doc.MainDocumentPart!.Document.Body! + .Descendants() + .First(p => p.ParagraphProperties?.ParagraphBorders != null); + + var borders = paragraph.ParagraphProperties!.ParagraphBorders!; + borders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single); + borders.GetFirstChild().Should().BeNull(); + borders.GetFirstChild().Should().BeNull(); + borders.GetFirstChild().Should().BeNull(); + } + public void Dispose() { _stream?.Dispose();