diff --git a/csharp-version/src/MarkdownToDocx.Core/Models/QuoteStyle.cs b/csharp-version/src/MarkdownToDocx.Core/Models/QuoteStyle.cs
index ff5d205..431302d 100644
--- a/csharp-version/src/MarkdownToDocx.Core/Models/QuoteStyle.cs
+++ b/csharp-version/src/MarkdownToDocx.Core/Models/QuoteStyle.cs
@@ -66,13 +66,62 @@ public sealed record QuoteStyle
public string SpaceAfter { get; init; } = "120";
///
- /// Internal padding on top, right, and bottom in points.
- /// Creates invisible borders matching the background color to produce
- /// spacing between the background fill and the text content.
+ /// Top inner padding in twips (1/20 pt). Effective when BackgroundColor is set.
+ /// 0 means: fall back to (points * 20) if it is set.
+ ///
+ public uint PaddingTop { get; init; } = 0;
+
+ ///
+ /// Right inner padding in twips (1/20 pt). See .
+ ///
+ public uint PaddingRight { get; init; } = 0;
+
+ ///
+ /// Bottom inner padding in twips (1/20 pt). See .
+ ///
+ public uint PaddingBottom { get; init; } = 0;
+
+ ///
+ /// Left inner padding in twips (1/20 pt). See .
+ ///
+ public uint PaddingLeft { get; init; } = 0;
+
+ ///
+ /// Deprecated uniform padding in points. When any per-side
+ /// (, , ,
+ /// ) is zero and this is non-zero, that side falls back
+ /// to PaddingSpace * 20 twips. Prefer the per-side properties for new presets.
/// Only effective when BackgroundColor is set.
///
public uint PaddingSpace { get; init; } = 0;
+ ///
+ /// True if any padding side or PaddingSpace is non-zero.
+ ///
+ public bool HasAnyPadding =>
+ PaddingTop > 0 || PaddingRight > 0 || PaddingBottom > 0
+ || PaddingLeft > 0 || PaddingSpace > 0;
+
+ ///
+ /// Effective top padding in twips, with PaddingSpace fallback applied.
+ ///
+ public uint EffectivePaddingTop => PaddingTop > 0 ? PaddingTop : PaddingSpace * 20;
+
+ ///
+ /// Effective right padding in twips, with PaddingSpace fallback applied.
+ ///
+ public uint EffectivePaddingRight => PaddingRight > 0 ? PaddingRight : PaddingSpace * 20;
+
+ ///
+ /// Effective bottom padding in twips, with PaddingSpace fallback applied.
+ ///
+ public uint EffectivePaddingBottom => PaddingBottom > 0 ? PaddingBottom : PaddingSpace * 20;
+
+ ///
+ /// Effective left padding in twips, with PaddingSpace fallback applied.
+ ///
+ public uint EffectivePaddingLeft => PaddingLeft > 0 ? PaddingLeft : PaddingSpace * 20;
+
///
/// Monospace font family for inline code (ASCII characters)
///
diff --git a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
index afa6a16..d617f7d 100644
--- a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
+++ b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
@@ -890,6 +890,16 @@ public void AddQuote(QuoteContent content, QuoteStyle style)
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(style);
+ // Cell mode: when a background color is set together with any padding,
+ // wrap the content in a single-cell borderless table so w:tcMar can
+ // produce true 4-sided padding (see ADR-0004).
+ bool useCellMode = !string.IsNullOrEmpty(style.BackgroundColor) && style.HasAnyPadding;
+ if (useCellMode)
+ {
+ AddQuoteAsTable(content, style);
+ return;
+ }
+
foreach (var block in content.Blocks)
{
switch (block)
@@ -905,6 +915,247 @@ public void AddQuote(QuoteContent content, QuoteStyle style)
}
}
+ ///
+ /// Renders a quote block as a single-cell borderless table with w:tcMar padding
+ /// and w:shd background. Used when BackgroundColor + any padding is set.
+ ///
+ private void AddQuoteAsTable(QuoteContent content, QuoteStyle style)
+ {
+ // Compute cell width: full text area minus LeftIndent (which positions the cell).
+ var pageConfig = _textDirection.GetPageConfiguration();
+ int fullTextAreaTwips = (int)(uint)pageConfig.Width
+ - pageConfig.LeftMargin
+ - pageConfig.RightMargin
+ - pageConfig.GutterMargin;
+ int leftIndentTwips = ParseTwipsOrZero(style.LeftIndent);
+ int cellWidthTwips = Math.Max(0, fullTextAreaTwips - leftIndentTwips);
+
+ // Spacer paragraph before the quote (mirrors AddTable spacing semantics).
+ AddTableSpacer(style.SpaceBefore);
+
+ var table = _body.AppendChild(new Table());
+
+ // Table properties: width, fixed layout, indentation, borders.
+ var tableProps = new TableProperties();
+ tableProps.AppendChild(new TableWidth
+ {
+ Type = TableWidthUnitValues.Dxa,
+ Width = cellWidthTwips.ToString(CultureInfo.InvariantCulture)
+ });
+ tableProps.AppendChild(new TableLayout { Type = TableLayoutValues.Fixed });
+ if (leftIndentTwips > 0)
+ {
+ tableProps.AppendChild(new TableIndentation
+ {
+ Width = leftIndentTwips,
+ Type = TableWidthUnitValues.Dxa
+ });
+ }
+ tableProps.AppendChild(BuildQuoteCellTableBorders(style));
+ table.AppendChild(tableProps);
+
+ // Single-column grid spanning the full cell width.
+ var grid = table.AppendChild(new TableGrid());
+ grid.AppendChild(new GridColumn
+ {
+ Width = cellWidthTwips.ToString(CultureInfo.InvariantCulture)
+ });
+
+ // Single row, single cell.
+ var row = table.AppendChild(new TableRow());
+ var cell = row.AppendChild(new TableCell());
+
+ var cellProps = new TableCellProperties();
+ cellProps.AppendChild(new TableCellWidth
+ {
+ Type = TableWidthUnitValues.Dxa,
+ Width = cellWidthTwips.ToString(CultureInfo.InvariantCulture)
+ });
+
+ // tcMar: true 4-sided padding in twips (sub-point precision, no 31pt cap).
+ // Order in the OOXML schema is top, left (start), bottom, right (end).
+ cellProps.AppendChild(new TableCellMargin(
+ new TopMargin
+ {
+ Width = style.EffectivePaddingTop.ToString(CultureInfo.InvariantCulture),
+ Type = TableWidthUnitValues.Dxa
+ },
+ new StartMargin
+ {
+ Width = style.EffectivePaddingLeft.ToString(CultureInfo.InvariantCulture),
+ Type = TableWidthUnitValues.Dxa
+ },
+ new BottomMargin
+ {
+ Width = style.EffectivePaddingBottom.ToString(CultureInfo.InvariantCulture),
+ Type = TableWidthUnitValues.Dxa
+ },
+ new EndMargin
+ {
+ Width = style.EffectivePaddingRight.ToString(CultureInfo.InvariantCulture),
+ Type = TableWidthUnitValues.Dxa
+ }
+ ));
+
+ // Cell shading fills to the cell border on all four sides.
+ if (!string.IsNullOrEmpty(style.BackgroundColor))
+ {
+ cellProps.AppendChild(new Shading
+ {
+ Val = ShadingPatternValues.Clear,
+ Color = "auto",
+ Fill = style.BackgroundColor
+ });
+ }
+
+ cell.AppendChild(cellProps);
+
+ // Render inner blocks as plain paragraphs inside the cell — visual chrome
+ // (background, borders, padding) is owned by the cell.
+ bool wroteAny = false;
+ foreach (var block in content.Blocks)
+ {
+ switch (block)
+ {
+ case QuoteParagraph p:
+ cell.AppendChild(BuildQuoteCellParagraph(p.Runs, style));
+ wroteAny = true;
+ break;
+
+ case QuoteList l:
+ foreach (var p in BuildQuoteCellListParagraphs(l, style))
+ {
+ cell.AppendChild(p);
+ wroteAny = true;
+ }
+ break;
+ }
+ }
+
+ // OOXML requires every cell to end with at least one paragraph.
+ if (!wroteAny)
+ {
+ cell.AppendChild(new Paragraph(CreateBaseParagraphProperties()));
+ }
+
+ AddTableSpacer(style.SpaceAfter);
+ }
+
+ ///
+ /// Builds table-level borders for the quote cell. Visible borders honour
+ /// and ;
+ /// remaining sides emit Nil borders so the cell stays visually clean.
+ ///
+ private static TableBorders BuildQuoteCellTableBorders(QuoteStyle style)
+ {
+ var top = new TopBorder { Val = BorderValues.Nil };
+ var bottom = new BottomBorder { Val = BorderValues.Nil };
+ var left = new LeftBorder { Val = BorderValues.Nil };
+ var right = new RightBorder { Val = BorderValues.Nil };
+
+ if (style.ShowBorder)
+ {
+ var positions = style.BorderPosition
+ .ToLowerInvariant()
+ .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+ foreach (var pos in positions)
+ {
+ switch (pos)
+ {
+ case "top":
+ top = new TopBorder { Val = BorderValues.Single, Color = style.BorderColor, Size = style.BorderSize, Space = 0U };
+ break;
+ case "bottom":
+ bottom = new BottomBorder { Val = BorderValues.Single, Color = style.BorderColor, Size = style.BorderSize, Space = 0U };
+ break;
+ case "right":
+ right = new RightBorder { Val = BorderValues.Single, Color = style.BorderColor, Size = style.BorderSize, Space = 0U };
+ break;
+ case "left":
+ default:
+ left = new LeftBorder { Val = BorderValues.Single, Color = style.BorderColor, Size = style.BorderSize, Space = 0U };
+ break;
+ }
+ }
+ }
+
+ return new TableBorders(
+ top, left, bottom, right,
+ new InsideHorizontalBorder { Val = BorderValues.Nil },
+ new InsideVerticalBorder { Val = BorderValues.Nil }
+ );
+ }
+
+ ///
+ /// Builds a paragraph for cell-mode quote rendering. Visual chrome lives on the
+ /// cell, so the paragraph carries only run-level styling (font/colour/italic) and
+ /// neutral spacing.
+ ///
+ private Paragraph BuildQuoteCellParagraph(IReadOnlyList runs, QuoteStyle style)
+ {
+ var paragraph = new Paragraph();
+ var paragraphProps = CreateBaseParagraphProperties();
+ paragraphProps.AppendChild(new SpacingBetweenLines { Before = "0", After = "0" });
+ paragraph.AppendChild(paragraphProps);
+
+ foreach (var inlineRun in runs)
+ {
+ var run = paragraph.AppendChild(new Run());
+ var runProps = CreateBaseRunProperties(
+ style.FontSize,
+ style.Color,
+ bold: inlineRun.Bold,
+ italic: inlineRun.IsCode ? false : (style.Italic || inlineRun.Italic));
+
+ if (inlineRun.IsCode)
+ {
+ ApplyInlineCodeFont(runProps, style.InlineCodeFontAscii, style.InlineCodeFontEastAsia);
+ }
+
+ run.AppendChild(runProps);
+ run.AppendChild(new Text(inlineRun.Text) { Space = SpaceProcessingModeValues.Preserve });
+ }
+
+ return paragraph;
+ }
+
+ ///
+ /// Builds list-item paragraphs for cell-mode quote rendering.
+ ///
+ private IEnumerable BuildQuoteCellListParagraphs(QuoteList list, QuoteStyle style)
+ {
+ int itemNumber = list.StartNumber;
+ foreach (var item in list.Items)
+ {
+ var paragraph = new Paragraph();
+ var paragraphProps = CreateBaseParagraphProperties();
+ paragraphProps.AppendChild(new SpacingBetweenLines { Before = "0", After = "0" });
+ paragraph.AppendChild(paragraphProps);
+
+ var run = paragraph.AppendChild(new Run());
+ var runProps = CreateBaseRunProperties(
+ style.FontSize,
+ style.Color,
+ italic: style.Italic);
+ run.AppendChild(runProps);
+
+ string bullet = list.IsOrdered ? $"{itemNumber}. " : "• ";
+ run.AppendChild(new Text(bullet + item.Text) { Space = SpaceProcessingModeValues.Preserve });
+
+ if (list.IsOrdered) itemNumber++;
+ yield return paragraph;
+ }
+ }
+
+ ///
+ /// Parses a twips string (e.g. "720") to an int, returning 0 on null/empty/invalid.
+ ///
+ private static int ParseTwipsOrZero(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) return 0;
+ return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n : 0;
+ }
+
///
/// Renders a paragraph inside a blockquote with quote styling.
///
diff --git a/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs b/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
index c86b2be..1811280 100644
--- a/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
+++ b/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
@@ -396,7 +396,29 @@ public sealed record QuoteStyleConfig
public string SpaceAfter { get; init; } = "120";
///
- /// Internal padding on top, right, and bottom in points (default: 0 = no padding).
+ /// Top inner padding in twips (1/20 pt). Only effective when BackgroundColor is set.
+ /// When 0, falls back to PaddingSpace * 20.
+ ///
+ public uint PaddingTop { get; init; } = 0;
+
+ ///
+ /// Right inner padding in twips (1/20 pt). See PaddingTop.
+ ///
+ public uint PaddingRight { get; init; } = 0;
+
+ ///
+ /// Bottom inner padding in twips (1/20 pt). See PaddingTop.
+ ///
+ public uint PaddingBottom { get; init; } = 0;
+
+ ///
+ /// Left inner padding in twips (1/20 pt). See PaddingTop.
+ ///
+ public uint PaddingLeft { get; init; } = 0;
+
+ ///
+ /// Deprecated uniform padding in points. Falls back to all four sides when no
+ /// per-side value is set. Prefer PaddingTop / PaddingRight / PaddingBottom / PaddingLeft.
/// Only effective when BackgroundColor is set.
///
public uint PaddingSpace { get; init; } = 0;
diff --git a/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs b/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
index c771643..befc6d7 100644
--- a/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
+++ b/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
@@ -127,6 +127,10 @@ public QuoteStyle ApplyQuoteStyle(StyleConfiguration config)
LeftIndent = config.Quote.LeftIndent,
SpaceBefore = config.Quote.SpaceBefore,
SpaceAfter = config.Quote.SpaceAfter,
+ PaddingTop = config.Quote.PaddingTop,
+ PaddingRight = config.Quote.PaddingRight,
+ PaddingBottom = config.Quote.PaddingBottom,
+ PaddingLeft = config.Quote.PaddingLeft,
PaddingSpace = config.Quote.PaddingSpace,
InlineCodeFontAscii = config.Quote.InlineCodeFontAscii,
InlineCodeFontEastAsia = config.Quote.InlineCodeFontEastAsia
diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
index 4bd3665..e50f49b 100644
--- a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
+++ b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
@@ -634,9 +634,10 @@ public void AddCodeBlock_WithZeroBorderSpace_ShouldNotAddIndentation()
}
[Fact]
- public void AddQuote_WithPaddingSpaceAndBackground_ShouldAddRightIndentation()
+ public void AddQuote_WithPaddingSpaceAndBackground_ShouldRenderAsCellWithTableIndentation()
{
- // Arrange: PaddingSpace = 4 pt → right indent must be 4 * 20 = 80 twips
+ // Arrange: BG + PaddingSpace triggers cell mode (ADR-0004). Table is offset
+ // by LeftIndent via w:tblInd; padding is applied via w:tcMar (all 4 sides).
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
var style = CreateDefaultQuoteStyle() with
{
@@ -652,14 +653,21 @@ public void AddQuote_WithPaddingSpaceAndBackground_ShouldAddRightIndentation()
// Assert
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
- var paragraph = doc.MainDocumentPart!.Document.Body!
- .Descendants()
- .First(p => p.ParagraphProperties?.ParagraphBorders != null);
+ var table = doc.MainDocumentPart!.Document.Body!.Elements().FirstOrDefault();
+ table.Should().NotBeNull("BG + padding should render as a single-cell table");
- var indent = paragraph.ParagraphProperties!.GetFirstChild();
- indent.Should().NotBeNull();
- indent!.Left?.Value.Should().Be("560");
- indent!.Right?.Value.Should().Be("80", "right indent = PaddingSpace * 20 twips prevents right border overflow");
+ var tableProps = table!.GetFirstChild();
+ var tableIndent = tableProps!.GetFirstChild();
+ tableIndent.Should().NotBeNull();
+ tableIndent!.Width!.Value.Should().Be(560, "table is offset from the left margin by LeftIndent");
+
+ var cell = table.Descendants().Single();
+ var cellMargins = cell.TableCellProperties!.GetFirstChild();
+ cellMargins.Should().NotBeNull();
+ cellMargins!.TopMargin!.Width!.Value.Should().Be("80", "PaddingSpace=4pt → 80 twips on top");
+ cellMargins.StartMargin!.Width!.Value.Should().Be("80", "left padding now matches other sides");
+ cellMargins.BottomMargin!.Width!.Value.Should().Be("80");
+ cellMargins.EndMargin!.Width!.Value.Should().Be("80");
}
[Fact]
@@ -1931,7 +1939,7 @@ public void AddQuote_WithCodeRun_ShouldRenderMonospaceFont()
}
[Fact]
- public void AddQuote_WithPaddingSpaceAndBackground_ShouldRenderInvisiblePaddingBorders()
+ public void AddQuote_WithPaddingSpaceAndBackground_ShouldRenderAsCellWithShadingAndBorder()
{
// Arrange
using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
@@ -1955,18 +1963,248 @@ public void AddQuote_WithPaddingSpaceAndBackground_ShouldRenderInvisiblePaddingB
builder.AddQuote(ToQuoteContent("Padded quote"), style);
builder.Save();
- // Assert: top/right/bottom invisible borders should be present with background color
+ // Assert: cell mode emits a single-cell table with shading + visible left border + tcMar.
_stream.Position = 0;
using var doc = WordprocessingDocument.Open(_stream, false);
- var paragraph = doc.MainDocumentPart!.Document.Body!.Elements().First();
- var borders = paragraph.ParagraphProperties?.ParagraphBorders;
- borders.Should().NotBeNull();
- borders!.GetFirstChild()!.Space!.Value.Should().Be(4U);
- borders.GetFirstChild()!.Color!.Value.Should().Be("f0f4f8");
- borders.GetFirstChild()!.Space!.Value.Should().Be(4U);
- borders.GetFirstChild()!.Space!.Value.Should().Be(4U);
- // Left border should remain the visible border
- borders.GetFirstChild()!.Color!.Value.Should().Be("3498db");
+ var table = doc.MainDocumentPart!.Document.Body!.Elements().Single();
+
+ var tableBorders = table.GetFirstChild()!.GetFirstChild();
+ tableBorders.Should().NotBeNull();
+ tableBorders!.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Single);
+ tableBorders.GetFirstChild()!.Color!.Value.Should().Be("3498db");
+ tableBorders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil);
+ tableBorders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil);
+ tableBorders.GetFirstChild()!.Val!.Value.Should().Be(BorderValues.Nil);
+
+ var cell = table.Descendants().Single();
+ var shading = cell.TableCellProperties!.GetFirstChild();
+ shading.Should().NotBeNull();
+ shading!.Fill!.Value.Should().Be("f0f4f8");
+
+ var cellMargins = cell.TableCellProperties.GetFirstChild();
+ cellMargins!.TopMargin!.Width!.Value.Should().Be("80");
+ cellMargins.StartMargin!.Width!.Value.Should().Be("80", "left side now has padding (the Issue #72 fix)");
+ cellMargins.BottomMargin!.Width!.Value.Should().Be("80");
+ cellMargins.EndMargin!.Width!.Value.Should().Be("80");
+ }
+
+ [Fact]
+ public void AddQuote_WithPerSidePaddingAndBackground_ShouldUseExactPerSideValues()
+ {
+ // Arrange: per-side padding overrides PaddingSpace fallback. Values are twips.
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "ffefe6",
+ PaddingTop = 60,
+ PaddingRight = 200,
+ PaddingBottom = 60,
+ PaddingLeft = 240,
+ PaddingSpace = 4 // ignored — per-side wins
+ };
+
+ // Act
+ builder.AddQuote(ToQuoteContent("custom"), style);
+ builder.Save();
+
+ // Assert
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var cellMargins = doc.MainDocumentPart!.Document.Body!
+ .Descendants().Single()
+ .TableCellProperties!
+ .GetFirstChild();
+
+ cellMargins!.TopMargin!.Width!.Value.Should().Be("60");
+ cellMargins.EndMargin!.Width!.Value.Should().Be("200");
+ cellMargins.BottomMargin!.Width!.Value.Should().Be("60");
+ cellMargins.StartMargin!.Width!.Value.Should().Be("240");
+ }
+
+ [Fact]
+ public void AddQuote_WithVerticalTextAndCellMode_ShouldEmitTableSuccessfully()
+ {
+ // Arrange: ADR-0004 claims vertical-text + cell-mode compatibility because
+ // Word auto-orients table cells via w:textDirection. Verify the call path
+ // produces a valid DOCX (no exceptions, table emitted, padding applied).
+ using var builder = new OpenXmlDocumentBuilder(_stream, _verticalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 4
+ };
+
+ // Act
+ builder.AddQuote(ToQuoteContent("vertical quote"), style);
+ builder.Save();
+
+ // Assert
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var table = doc.MainDocumentPart!.Document.Body!.Elements().Single();
+ var cellMargins = table.Descendants().Single()
+ .TableCellProperties!.GetFirstChild();
+ cellMargins!.TopMargin!.Width!.Value.Should().Be("80");
+ cellMargins.StartMargin!.Width!.Value.Should().Be("80");
+ }
+
+ [Fact]
+ public void AddQuote_WithListInCellMode_ShouldRenderListItemsInsideCell()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 4
+ };
+ var content = new QuoteContent
+ {
+ Blocks = [
+ new QuoteList
+ {
+ IsOrdered = false,
+ Items = [
+ new CoreListItem { Text = "first" },
+ new CoreListItem { Text = "second" }
+ ]
+ }
+ ]
+ };
+
+ // Act
+ builder.AddQuote(content, style);
+ builder.Save();
+
+ // Assert: cell contains one paragraph per list item, each with bullet prefix.
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var cell = doc.MainDocumentPart!.Document.Body!.Descendants().Single();
+ var paragraphs = cell.Elements().ToList();
+ paragraphs.Should().HaveCount(2);
+ paragraphs[0].InnerText.Should().StartWith("•").And.Contain("first");
+ paragraphs[1].InnerText.Should().StartWith("•").And.Contain("second");
+ }
+
+ [Fact]
+ public void AddQuote_CellModeWithMultipleBorderPositions_ShouldEmitVisibleBordersOnEachSide()
+ {
+ // Arrange: BorderPosition = "top,bottom,right" exercises all three non-left
+ // branches in BuildQuoteCellTableBorders.
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 4,
+ ShowBorder = true,
+ BorderPosition = "top,bottom,right",
+ BorderColor = "ff0000",
+ BorderSize = 12
+ };
+
+ // Act
+ builder.AddQuote(ToQuoteContent("test"), style);
+ builder.Save();
+
+ // Assert
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var borders = doc.MainDocumentPart!.Document.Body!
+ .Elements().Single()
+ .GetFirstChild()!
+ .GetFirstChild();
+ 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.Nil, "left was not requested");
+ }
+
+ [Fact]
+ public void AddQuote_CellModeWithInlineCode_ShouldApplyMonospaceFont()
+ {
+ // Arrange: code runs inside a cell-mode quote paragraph must apply the
+ // configured monospace fonts (ApplyInlineCodeFont branch).
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 4,
+ InlineCodeFontAscii = "Courier New",
+ InlineCodeFontEastAsia = "Noto Sans Mono CJK JP"
+ };
+ var runs = new List
+ {
+ new InlineRun { Text = "Use " },
+ new InlineRun { Text = "exit()", IsCode = true },
+ new InlineRun { Text = " to leave." }
+ };
+
+ // Act
+ builder.AddQuote(ToQuoteContent(runs), style);
+ builder.Save();
+
+ // Assert: the middle run carries Courier New; surrounding text runs do not.
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var paragraph = doc.MainDocumentPart!.Document.Body!
+ .Descendants().Single()
+ .Elements().Single();
+ var runElems = paragraph.Elements().ToList();
+ runElems.Should().HaveCount(3);
+ runElems[1].RunProperties!.RunFonts!.Ascii!.Value.Should().Be("Courier New");
+ runElems[0].RunProperties?.RunFonts.Should().BeNull();
+ runElems[2].RunProperties?.RunFonts.Should().BeNull();
+ }
+
+ [Fact]
+ public void AddQuote_EmptyContentInCellMode_ShouldEmitCellWithFallbackParagraph()
+ {
+ // Arrange: OOXML requires every cell to contain at least one paragraph.
+ // The cell-mode renderer must emit an empty paragraph when no blocks exist.
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 4
+ };
+ var emptyContent = new QuoteContent { Blocks = Array.Empty() };
+
+ // Act
+ builder.AddQuote(emptyContent, style);
+ builder.Save();
+
+ // Assert: cell exists and contains exactly one (fallback) paragraph
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var cell = doc.MainDocumentPart!.Document.Body!.Descendants().Single();
+ cell.Elements().Should().HaveCount(1, "OOXML schema requires >= 1 paragraph per cell");
+ }
+
+ [Fact]
+ public void AddQuote_WithBackgroundButZeroPadding_ShouldUseParagraphMode()
+ {
+ // Arrange: BG without padding stays in the lightweight paragraph path.
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultQuoteStyle() with
+ {
+ BackgroundColor = "f0f0f0",
+ PaddingSpace = 0,
+ PaddingTop = 0,
+ PaddingRight = 0,
+ PaddingBottom = 0,
+ PaddingLeft = 0
+ };
+
+ // Act
+ builder.AddQuote(ToQuoteContent("test"), style);
+ builder.Save();
+
+ // Assert: no Table emitted; paragraph still carries shading.
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var body = doc.MainDocumentPart!.Document.Body!;
+ body.Elements().Should().BeEmpty("paragraph mode is preserved when no padding is requested");
+ body.Elements().First().ParagraphProperties!.Shading.Should().NotBeNull();
}
[Fact]
diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
index 9fadbdc..60e85e8 100644
--- a/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
+++ b/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
@@ -703,6 +703,60 @@ public void ApplyQuoteStyle_ShouldMapInlineCodeFont()
style.InlineCodeFontEastAsia.Should().Be("MS Gothic");
}
+ [Fact]
+ public void ApplyQuoteStyle_ShouldMapPerSidePadding()
+ {
+ // Arrange: per-side padding fields should round-trip from config to domain model.
+ var config = new StyleConfiguration
+ {
+ Quote = new QuoteStyleConfig
+ {
+ Size = 11,
+ PaddingTop = 60,
+ PaddingRight = 200,
+ PaddingBottom = 60,
+ PaddingLeft = 240,
+ PaddingSpace = 4
+ }
+ };
+
+ // Act
+ var style = _applicator.ApplyQuoteStyle(config);
+
+ // Assert
+ style.PaddingTop.Should().Be(60u);
+ style.PaddingRight.Should().Be(200u);
+ style.PaddingBottom.Should().Be(60u);
+ style.PaddingLeft.Should().Be(240u);
+ style.PaddingSpace.Should().Be(4u);
+ style.EffectivePaddingTop.Should().Be(60u, "per-side wins over PaddingSpace fallback");
+ style.HasAnyPadding.Should().BeTrue();
+ }
+
+ [Fact]
+ public void ApplyQuoteStyle_PaddingSpaceOnly_ShouldFallBackToAllFourSides()
+ {
+ // Arrange: legacy PaddingSpace alone should populate all sides via EffectivePadding*.
+ var config = new StyleConfiguration
+ {
+ Quote = new QuoteStyleConfig
+ {
+ Size = 11,
+ PaddingSpace = 4 // 4pt → 80 twips fallback
+ }
+ };
+
+ // Act
+ var style = _applicator.ApplyQuoteStyle(config);
+
+ // Assert
+ style.PaddingTop.Should().Be(0u);
+ style.EffectivePaddingTop.Should().Be(80u);
+ style.EffectivePaddingRight.Should().Be(80u);
+ style.EffectivePaddingBottom.Should().Be(80u);
+ style.EffectivePaddingLeft.Should().Be(80u, "Issue #72: left now gets padding via PaddingSpace fallback");
+ }
+
[Fact]
public void StyleConfigurationRecords_WithSameProperties_ShouldBeEqual()
{
diff --git a/docs/decisions/ADR-0004-quote-block-padding.md b/docs/decisions/ADR-0004-quote-block-padding.md
new file mode 100644
index 0000000..f785046
--- /dev/null
+++ b/docs/decisions/ADR-0004-quote-block-padding.md
@@ -0,0 +1,364 @@
+# ADR-0004: Quote Block Padding via Single-Cell Table
+
+**Date**: 2026-05-01
+**Status**: Proposed
+**Decided by**: forest6511
+
+---
+
+## Context
+
+**Problem Statement**:
+
+- `QuoteStyle.PaddingSpace` only emits invisible `w:pBdr` borders on **top/right/bottom**;
+ the **left** side has no padding, so when `BackgroundColor` is set without a visible
+ left border the text touches the colored fill edge (Issue #72).
+- Extending the existing technique to the left side (Option 1 in Issue #72) is structurally
+ fragile: the OOXML `w:pBdr/@w:space` attribute is `ST_PointMeasure` (whole points,
+ range 0–31), and on the left/right axes a border's `space` only displaces the rendered
+ border line — it does **not** reflow the run content. A separate `w:ind/@w:left`
+ compensation is required, which interacts with author-set indentation and can clip at
+ the page margin when `LeftIndent` is small.
+
+**Current Situation**:
+
+- `OpenXmlDocumentBuilder.CreateQuoteParagraphProperties` (`OpenXmlDocumentBuilder.cs:993-1002`)
+ emits Top/Right/Bottom `w:pBdr` borders only.
+- `Indentation.Right = PaddingSpace * 20` compensates for the right border that extends
+ into the page margin (`OpenXmlDocumentBuilder.cs:1015-1019`).
+- 20 existing presets in `csharp-version/config/` use `padding_space` (`uint`, points).
+- Quote rendering is paragraph-based: `AddQuoteParagraph` and `AddQuoteList` each emit
+ one `w:p` per source block.
+
+**Constraints**:
+
+- YAML schema must remain backward-compatible (CLAUDE.md, 🔴 critical rule).
+- Existing 20 presets must keep producing equivalent or better output.
+- Vertical text mode (`VerticalTextProvider`) must continue to work.
+- Fenced div rendering (`AddFencedDiv*`) is on a separate code path and is out of scope here.
+
+---
+
+## Decision
+
+Render quote blocks that have a **background color** with non-zero padding inside a
+**single-cell, borderless `w:tbl`** that uses `w:tcMar` for true 4-sided padding and
+`w:shd` on `w:tcPr` for the background fill.
+
+Quote blocks without a background color keep the current paragraph-based rendering.
+
+**Key Points**:
+
+- Add four new properties to `QuoteStyle` and `QuoteStyleConfig`: `PaddingTop`,
+ `PaddingRight`, `PaddingBottom`, `PaddingLeft` — all `uint` in **twips**, matching
+ the existing `TableStyle.CellPadding{Top,Right,Bottom,Left}` convention.
+- `PaddingSpace` (uint, points) is retained as a **deprecated alias** that
+ populates all four sides (`twips = PaddingSpace * 20`) when no explicit
+ per-side value is provided.
+- A new private renderer `AddQuoteAsTable` emits the cell-wrapped form when
+ `BackgroundColor` is set and any padding side is positive.
+- The existing paragraph rendering path is preserved verbatim for the
+ no-background / no-padding case (zero behavior change for those presets).
+
+**Scope**:
+
+- Covers: `QuoteStyle` padding semantics, blockquote rendering (`AddQuote` and helpers),
+ YAML schema additions for `padding`.
+- Not covered: Fenced div rendering (separate code path, separate decision if needed),
+ table cell padding (already uses `w:tcMar` correctly), heading/paragraph padding.
+
+---
+
+## Consequences
+
+### Positive
+
+- ✅ True 4-sided padding with twip precision (no 31 pt cap).
+- ✅ Background shading fills cleanly to the cell edge on all four sides.
+- ✅ No collision with page margins on small `LeftIndent` values.
+- ✅ Existing 20 presets unchanged (no `padding` field added; `PaddingSpace` keeps working).
+- ✅ Aligns with the canonical Word approach used by Aspose.Words / docx4j for
+ "block with padding" semantics.
+
+### Negative
+
+- ❌ Cell rendering is structurally heavier than a paragraph (more XML).
+- ❌ A single-cell table introduces a `w:tbl` element where users may previously have
+ expected a flat paragraph stream — affects `Find/Replace` selection across boundaries
+ and some Word UI behaviors.
+- ❌ Long quotes spanning a page break behave slightly differently in cell form
+ (Word splits the cell unless `w:cantSplit` is set; we keep the default split-allowed
+ behavior to match prior paragraph behavior).
+
+### Neutral
+
+- ℹ️ `Padding` model is reusable; future work may apply it to fenced div / heading
+ styles, but those migrations are explicitly out of scope here.
+- ℹ️ Vertical text mode continues to work because Word auto-orients table cell content
+ via `w:textDirection`; no provider changes needed.
+
+---
+
+## Alternatives Considered
+
+### Alternative 1: Add `LeftBorder` to the existing `w:pBdr` hack
+
+**Description**: Mirror the Top/Right/Bottom invisible-border emission to the Left side
+and extend the indentation compensation to `Indentation.Left += PaddingSpace * 20`.
+This is Option 1 from Issue #72.
+
+**Pros**:
+
+- Minimal diff (~10 LOC).
+- Single-property API preserved (no new model).
+- Keeps the flat paragraph structure.
+
+**Cons**:
+
+- Inherits the 0–31 pt `ST_PointMeasure` cap on padding.
+- Requires careful arithmetic against author-set `LeftIndent`; presets with
+ `LeftIndent=0` would either clip into the page margin or need a per-preset patch.
+- Does not solve the "between border" merging behavior between adjacent same-styled
+ paragraphs (ECMA-376 §17.3.1.7).
+- Conflicts with our roadmap of adding richer padding control (per-side values,
+ sub-point precision).
+
+**Why rejected**: Solves the visible left-padding gap but leaves the underlying
+structural fragility in place. The OOXML `w:pBdr` mechanism is fundamentally a
+border-spec, not a content-padding spec.
+
+---
+
+### Alternative 2: Add `LeftPaddingSpace` (and friends) keeping `w:pBdr`
+
+**Description**: Extend `QuoteStyle` with `Padding{Top,Right,Bottom,Left}Space` fields,
+keep emitting the four invisible `w:pBdr` borders.
+
+**Pros**:
+
+- 4-sided control without introducing tables.
+- Backward compatible (`PaddingSpace` kept as alias for all four).
+
+**Cons**:
+
+- Same 31 pt cap and integer-points precision as Alternative 1.
+- Same indent-arithmetic / page-margin clipping issues on the left/right axes.
+- Doubles the API surface without solving the root cause.
+
+**Why rejected**: Better API ergonomics than Alternative 1, but does not address the
+fundamental OOXML semantics issue (borders ≠ padding on the horizontal axis).
+
+---
+
+### Alternative 3: Document the limitation; do nothing
+
+**Description**: Note in the preset reference that `PaddingSpace` does not affect the
+left side; users should use `LeftIndent` instead.
+
+**Pros**:
+
+- Zero code change.
+
+**Cons**:
+
+- `LeftIndent` shifts the entire box (including background shading) inward; it cannot
+ produce padding **between** the colored fill and the text.
+- Issue #72 use case (KDP A5 fenced-div decorative blocks with uniform inner margin)
+ remains unsolvable.
+
+**Why rejected**: The stated user need cannot be met by `LeftIndent`; this would
+ship a known incorrect-by-design feature.
+
+---
+
+## Implementation
+
+**Files affected**:
+
+- `csharp-version/src/MarkdownToDocx.Core/Models/QuoteStyle.cs` — add
+ `PaddingTop/Right/Bottom/Left` (uint, twips); keep `PaddingSpace` (uint, points)
+ as documented deprecated alias.
+- `csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs` —
+ mirror the new properties on `QuoteStyleConfig`.
+- `csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs` —
+ `ApplyQuoteStyle` passes per-side and `PaddingSpace` through unchanged; the
+ fallback (per-side > 0 wins, else `PaddingSpace * 20`) lives in the
+ `EffectivePadding{Top,Right,Bottom,Left}` getters on `QuoteStyle` so the
+ domain model is self-contained.
+- `csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs` —
+ `AddQuote` dispatches to `AddQuoteAsTable` when `BackgroundColor` set and any
+ `Padding{Top,Right,Bottom,Left}` is positive; `AddQuoteAsTable` builds a
+ single-cell `w:tbl` with `w:tblBorders` all `nil`, `w:tcMar`, `w:shd` on `w:tcPr`,
+ and renders inner blocks via existing helpers. Paragraph path unchanged.
+- `csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs` —
+ extend with cell-mode rendering assertions.
+- `csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs` —
+ cover the precedence rule.
+
+**Code references**:
+
+```csharp
+// QuoteStyle.cs (proposed additions)
+public sealed record QuoteStyle
+{
+ /// Top inner padding in twips (1/20 pt). 0 = use PaddingSpace fallback.
+ public uint PaddingTop { get; init; } = 0;
+ public uint PaddingRight { get; init; } = 0;
+ public uint PaddingBottom { get; init; } = 0;
+ public uint PaddingLeft { get; init; } = 0;
+
+ ///
+ /// Deprecated. Uniform padding in points; converted to twips and applied to all
+ /// four sides if no explicit per-side padding is set. Prefer PaddingTop/...Right/Bottom/Left.
+ ///
+ public uint PaddingSpace { get; init; } = 0;
+
+ public bool HasAnyPadding => PaddingTop > 0 || PaddingRight > 0
+ || PaddingBottom > 0 || PaddingLeft > 0
+ || PaddingSpace > 0;
+}
+
+// QuoteStyle.cs (model-level fallback)
+public uint EffectivePaddingTop => PaddingTop > 0 ? PaddingTop : PaddingSpace * 20;
+public uint EffectivePaddingRight => PaddingRight > 0 ? PaddingRight : PaddingSpace * 20;
+public uint EffectivePaddingBottom => PaddingBottom > 0 ? PaddingBottom : PaddingSpace * 20;
+public uint EffectivePaddingLeft => PaddingLeft > 0 ? PaddingLeft : PaddingSpace * 20;
+
+// StyleApplicator.cs simply passes values through
+return new QuoteStyle
+{
+ PaddingTop = config.Quote.PaddingTop,
+ PaddingRight = config.Quote.PaddingRight,
+ PaddingBottom = config.Quote.PaddingBottom,
+ PaddingLeft = config.Quote.PaddingLeft,
+ PaddingSpace = config.Quote.PaddingSpace,
+ // ... other fields
+};
+
+// OpenXmlDocumentBuilder.AddQuote dispatch
+public void AddQuote(QuoteContent content, QuoteStyle style)
+{
+ bool useCellMode = !string.IsNullOrEmpty(style.BackgroundColor) && style.HasAnyPadding;
+ if (useCellMode) AddQuoteAsTable(content, style);
+ else AddQuoteAsParagraphs(content, style); // current implementation
+}
+```
+
+**Migration path** (no breaking changes for existing users):
+
+1. Existing presets using `PaddingSpace: 8` continue to load. Visual output changes:
+ `business.yaml` (the only existing preset using this) gains a left padding it
+ previously lacked — this is the bug fix Issue #72 requests.
+2. New presets can use per-side fields: `PaddingTop: 80`, `PaddingLeft: 160`, etc. (twips).
+3. README + preset docs add a "Padding" section noting `PaddingSpace` is deprecated
+ but supported indefinitely.
+4. A future major version may remove `PaddingSpace` (separate ADR required).
+
+**Rollback plan**:
+
+If cell-based rendering causes regressions (e.g., page-break artifacts in printed books):
+
+1. `AddQuote` dispatch becomes `if (useCellMode && !DisableQuoteCellMode)` guarded by
+ a YAML flag `quote.rendering: paragraph|cell|auto` (default `auto` = current decision).
+2. Users can pin `rendering: paragraph` to revert behavior per preset.
+3. Worst case: revert `AddQuote` dispatch, keep `Padding` model as additive metadata.
+
+---
+
+## Validation
+
+**Success Criteria**:
+
+- [ ] `PaddingTop/Right/Bottom/Left` deserialize from YAML and override `PaddingSpace` per-side
+- [ ] `PaddingSpace: 8` (existing presets) still produces a working DOCX with non-zero padding on all four sides via the new cell renderer when `BackgroundColor` is set
+- [ ] Quote with no `BackgroundColor` produces byte-identical paragraph output to pre-change behavior
+- [ ] Quote with nested list (PR #70 scenario) renders correctly inside the cell
+- [ ] Vertical text mode integration test produces a valid DOCX
+- [ ] All 20 existing presets convert sample MDs without errors
+- [ ] Test coverage for `MarkdownToDocx.Core` does not regress below 90%
+
+**Testing Strategy**:
+
+- Unit: `Padding` shorthand parsing, `PaddingSpace` alias round-trip, `useCellMode`
+ dispatch logic.
+- Integration: real Markdown → DOCX with `background_color` + `padding` /
+ `padding_space`, with horizontal and vertical text providers.
+- Visual: manual diff of Word-rendered output for one preset per category
+ (blockquote with bg, blockquote without bg, blockquote with nested list,
+ vertical-text blockquote).
+
+**Monitoring** (post-implementation):
+
+- Issue tracker for regressions on existing presets.
+- User reports of page-break behavior changes in printed-book workflows.
+
+---
+
+## Related Decisions
+
+**Supersedes**: None.
+
+**Related to**:
+
+- ADR-0002: YAML Schema Design — extends schema with new `padding` field; backward
+ compat path defined here.
+- ADR-0003: Vertical Text Implementation — confirmed compatible (Word auto-orients
+ table cells via `w:textDirection`).
+
+**Depends on**: None.
+
+---
+
+## References
+
+**Specification**:
+
+- ECMA-376 Part 1, §17.3.1.7 (Paragraph Borders)
+- ECMA-376 Part 1, §17.4.43 (`w:tcMar`)
+- ECMA-376 Part 1, `ST_PointMeasure` (border `space` units)
+
+**Authoritative sources**:
+
+- [LeftBorder Class — Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.leftborder)
+- [officeopenxml.com — Paragraph Borders](http://officeopenxml.com/WPborders.php)
+- [officeopenxml.com — Table Cell Margins](http://officeopenxml.com/WPtableCellMargins.php)
+- [Aspose.Words `Cell.CellFormat` padding properties](https://reference.aspose.com/words/net/aspose.words.cellformat/)
+
+**Prior-art implementations**:
+
+- Pandoc — `src/Text/Pandoc/Writers/Docx/OpenXML.hs` BlockQuote handler (style delegation, no padding emulation).
+- docx4j — single-cell tables for "padded boxed" content.
+
+**Issues/PRs**:
+
+- Issue #72: Quote: PaddingSpace cannot express left-side padding
+
+---
+
+## Timeline
+
+| Date | Event |
+|------|-------|
+| 2026-05-01 | Issue #72 filed, research conducted, ADR proposed |
+| TBD | Approved by forest6511 |
+| TBD | Implementation started on `feature/quote-cell-padding` |
+| TBD | Tests + Codex review complete |
+| TBD | PR merged |
+
+---
+
+## Notes
+
+The research that drove this decision is summarized in
+`claudedocs/research_quote_padding_20260501.md` (gitignored Claude scratch).
+Key insight: industrial OpenXML libraries (Aspose.Words, docx4j) treat **paragraph
+borders** and **cell padding** as orthogonal concerns and expose both. The "invisible
+border as padding" trick popular in early DOCX generators is an upper-half-plane
+technique that does not generalize cleanly to the horizontal axis. Migrating to cell
+margins for the shaded case aligns md2docx with the established library convention
+without losing the lightweight paragraph path for the no-background case.
+
+---
+
+**Last Updated**: 2026-05-01
+**Next Review**: After PR merge (validate success criteria)