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
28 changes: 19 additions & 9 deletions csharp-version/src/MarkdownToDocx.CLI/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,24 +139,34 @@ public static IReadOnlyList<InlineRun> GetParagraphRuns(ParagraphBlock paragraph
return runs;
}

public static IReadOnlyList<InlineRun> GetQuoteRuns(QuoteBlock block)
public static QuoteContent GetQuoteContent(QuoteBlock block)
{
var runs = new List<InlineRun>();
bool firstParagraph = true;
var blocks = new List<QuoteContentBlock>();

foreach (var child in block)
{
if (child is ParagraphBlock paragraph && paragraph.Inline != null)
switch (child)
{
if (!firstParagraph)
runs.Add(new InlineRun { Text = " " });
case ParagraphBlock paragraph:
var runs = new List<InlineRun>();
if (paragraph.Inline != null)
ExtractInlineRuns(paragraph.Inline, runs, bold: false, italic: false);
if (runs.Count > 0)
blocks.Add(new QuoteParagraph { Runs = runs });
break;

ExtractInlineRuns(paragraph.Inline, runs, bold: false, italic: false);
firstParagraph = false;
case ListBlock list:
var items = GetListItems(list).ToList();
if (items.Count > 0)
{
var startNumber = int.TryParse(list.OrderedStart, out var parsed) ? parsed : 1;
blocks.Add(new QuoteList { Items = items, IsOrdered = list.IsOrdered, StartNumber = startNumber });
}
break;
}
}

return runs;
return new QuoteContent { Blocks = blocks };
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions csharp-version/src/MarkdownToDocx.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@
break;

case QuoteBlock quote:
var quoteRuns = Helpers.GetQuoteRuns(quote);
var quoteContent = Helpers.GetQuoteContent(quote);
var quoteStyle = styleApplicator.ApplyQuoteStyle(config.Styles);
builder.AddQuote(quoteRuns, quoteStyle);
builder.AddQuote(quoteContent, quoteStyle);
break;

case Table tableBlock:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ public interface IDocumentBuilder : IDisposable
void AddCodeBlock(string code, string? language, CodeBlockStyle style);

/// <summary>
/// Adds a quote block to the document with inline formatting support
/// Adds a quote block to the document with structured content support.
/// Renders paragraphs and lists within the blockquote with quote styling.
/// </summary>
/// <param name="runs">Structured inline runs (bold, italic, code) extracted from the quote block</param>
/// <param name="content">Structured child blocks extracted from the blockquote</param>
/// <param name="style">Quote style configuration</param>
void AddQuote(IReadOnlyList<InlineRun> runs, QuoteStyle style);
void AddQuote(QuoteContent content, QuoteStyle style);

/// <summary>
/// Adds a title page with a cover image to the document.
Expand Down
50 changes: 50 additions & 0 deletions csharp-version/src/MarkdownToDocx.Core/Models/QuoteContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace MarkdownToDocx.Core.Models;

/// <summary>
/// Structured content extracted from a blockquote.
/// Contains an ordered list of typed child blocks ready for rendering.
/// </summary>
public sealed class QuoteContent
{
/// <summary>
/// Ordered sequence of child blocks within the blockquote.
/// </summary>
public IReadOnlyList<QuoteContentBlock> Blocks { get; init; } = [];
}

/// <summary>
/// Base type for all child blocks within a blockquote.
/// </summary>
public abstract class QuoteContentBlock { }

/// <summary>
/// A paragraph (with inline formatting) inside a blockquote.
/// </summary>
public sealed class QuoteParagraph : QuoteContentBlock
{
/// <summary>
/// Structured inline runs with bold/italic/code formatting.
/// </summary>
public IReadOnlyList<InlineRun> Runs { get; init; } = [];
}

/// <summary>
/// A list (ordered or unordered) inside a blockquote.
/// </summary>
public sealed class QuoteList : QuoteContentBlock
{
/// <summary>
/// List items.
/// </summary>
public IReadOnlyList<ListItem> Items { get; init; } = [];

/// <summary>
/// True for numbered list, false for bullet list.
/// </summary>
public bool IsOrdered { get; init; }

/// <summary>
/// First number for ordered lists.
/// </summary>
public int StartNumber { get; init; } = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,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 865 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 865 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 All @@ -885,11 +885,31 @@
}

/// <inheritdoc/>
public void AddQuote(IReadOnlyList<InlineRun> runs, QuoteStyle style)
public void AddQuote(QuoteContent content, QuoteStyle style)
{
ArgumentNullException.ThrowIfNull(runs);
ArgumentNullException.ThrowIfNull(content);
ArgumentNullException.ThrowIfNull(style);

foreach (var block in content.Blocks)
{
switch (block)
{
case QuoteParagraph p:
AddQuoteParagraph(p.Runs, style);
break;

case QuoteList l:
AddQuoteList(l, style);
break;
}
}
}

/// <summary>
/// Renders a paragraph inside a blockquote with quote styling.
/// </summary>
private void AddQuoteParagraph(IReadOnlyList<InlineRun> runs, QuoteStyle style)
{
var paragraph = _body.AppendChild(new Paragraph());
var paragraphProps = CreateQuoteParagraphProperties(style);
paragraph.AppendChild(paragraphProps);
Expand All @@ -913,6 +933,32 @@
}
}

/// <summary>
/// Renders a list inside a blockquote with quote styling on each item.
/// </summary>
private void AddQuoteList(QuoteList list, QuoteStyle style)
{
int itemNumber = list.StartNumber;
foreach (var item in list.Items)
{
var paragraph = _body.AppendChild(new Paragraph());
var paragraphProps = CreateQuoteParagraphProperties(style);
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}. " : "\u2022 ";
run.AppendChild(new Text(bullet + item.Text) { Space = SpaceProcessingModeValues.Preserve });

if (list.IsOrdered) itemNumber++;
}
}

/// <summary>
/// Creates paragraph properties for quote blocks
/// </summary>
Expand Down Expand Up @@ -969,7 +1015,7 @@
var indentation = new Indentation { Left = style.LeftIndent };
if (hasPadding && style.PaddingSpace > 0)
{
indentation.Right = (style.PaddingSpace * 20).ToString();

Check warning on line 1018 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 1018 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
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,53 @@ This is a paragraph.
result.Should().Contain(b => b is FencedCodeBlock);
result.Should().Contain(b => b is QuoteBlock);
}

[Fact]
public void Parse_WithListInsideQuote_ShouldContainListBlockChild()
{
// Arrange — the exact pattern from issue #70
var markdown = "> - Item 1\n> - Item 2\n> - Item 3";

// Act
var result = _parser.Parse(markdown);

// Assert
result.Should().HaveCount(1);
var quoteBlock = result[0].Should().BeOfType<QuoteBlock>().Subject;
quoteBlock.Should().Contain(child => child is ListBlock);
var list = quoteBlock.OfType<ListBlock>().Single();
list.Count.Should().Be(3);
list.IsOrdered.Should().BeFalse();
}

[Fact]
public void Parse_WithParagraphAndListInsideQuote_ShouldContainBoth()
{
// Arrange
var markdown = "> Summary text\n>\n> - Point A\n> - Point B";

// Act
var result = _parser.Parse(markdown);

// Assert
var quoteBlock = result.OfType<QuoteBlock>().Single();
quoteBlock.Should().Contain(child => child is ParagraphBlock);
quoteBlock.Should().Contain(child => child is ListBlock);
}

[Fact]
public void Parse_WithOrderedListInsideQuote_ShouldPreserveOrdering()
{
// Arrange
var markdown = "> 1. First\n> 2. Second";

// Act
var result = _parser.Parse(markdown);

// Assert
var quoteBlock = result.OfType<QuoteBlock>().Single();
var list = quoteBlock.OfType<ListBlock>().Single();
list.IsOrdered.Should().BeTrue();
list.Count.Should().Be(2);
}
}
Loading
Loading