diff --git a/csharp-version/src/MarkdownToDocx.Core/Models/HeadingStyle.cs b/csharp-version/src/MarkdownToDocx.Core/Models/HeadingStyle.cs
index 0a1f0b2..c1788d3 100644
--- a/csharp-version/src/MarkdownToDocx.Core/Models/HeadingStyle.cs
+++ b/csharp-version/src/MarkdownToDocx.Core/Models/HeadingStyle.cs
@@ -88,4 +88,23 @@ public sealed record HeadingStyle
/// H2 directly follows H1.
///
public int? SuppressPageBreakIfPrevHeadingLevel { get; init; }
+
+ ///
+ /// Border line style applied to all border positions on this heading.
+ /// Supported values: "single" (default), "double", "thick", "dotted", "dashed",
+ /// "dotDash", "wave", "triple".
+ ///
+ public string BorderStyle { get; init; } = "single";
+
+ ///
+ /// Unicode character (or short string) prepended to the heading text as a decorative prefix.
+ /// For example "◆", "★", "▶", or "01". Null or empty means no prefix.
+ ///
+ public string? IconPrefix { get; init; }
+
+ ///
+ /// Color of the icon prefix in hexadecimal format (e.g., "E91E8C").
+ /// When null, the heading's main Color is used.
+ ///
+ public string? IconPrefixColor { get; init; }
}
diff --git a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
index 07fdefd..e741203 100644
--- a/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
+++ b/csharp-version/src/MarkdownToDocx.Core/OpenXml/OpenXmlDocumentBuilder.cs
@@ -129,6 +129,21 @@ private ParagraphProperties CreateBaseParagraphProperties()
return props;
}
+ ///
+ /// Converts a border style name to the corresponding BorderValues enum value.
+ ///
+ private static BorderValues ParseBorderStyle(string borderStyle) => borderStyle.ToLowerInvariant() switch
+ {
+ "double" => BorderValues.Double,
+ "thick" => BorderValues.Thick,
+ "dotted" => BorderValues.Dotted,
+ "dashed" => BorderValues.Dashed,
+ "dotdash" => BorderValues.DotDash,
+ "wave" => BorderValues.Wave,
+ "triple" => BorderValues.Triple,
+ _ => BorderValues.Single
+ };
+
///
/// Creates a ParagraphBorders element from a comma-separated position string
///
@@ -136,22 +151,24 @@ private static ParagraphBorders CreateBordersFromPositions(
string borderPosition,
string borderColor,
uint borderSize,
- uint borderSpace)
+ uint borderSpace,
+ string borderStyle = "single")
{
var positions = borderPosition
.ToLowerInvariant()
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+ var borderVal = ParseBorderStyle(borderStyle);
var paragraphBorders = new ParagraphBorders();
foreach (var pos in positions)
{
OpenXmlElement border = pos switch
{
- "left" => new LeftBorder { Val = BorderValues.Single, Color = borderColor, Size = borderSize, Space = borderSpace },
- "right" => new RightBorder { Val = BorderValues.Single, Color = borderColor, Size = borderSize, Space = borderSpace },
- "top" => new TopBorder { Val = BorderValues.Single, Color = borderColor, Size = borderSize, Space = borderSpace },
- _ => new BottomBorder { Val = BorderValues.Single, Color = borderColor, Size = borderSize, Space = borderSpace }
+ "left" => new LeftBorder { Val = borderVal, Color = borderColor, Size = borderSize, Space = borderSpace },
+ "right" => new RightBorder { Val = borderVal, Color = borderColor, Size = borderSize, Space = borderSpace },
+ "top" => new TopBorder { Val = borderVal, Color = borderColor, Size = borderSize, Space = borderSpace },
+ _ => new BottomBorder { Val = borderVal, Color = borderColor, Size = borderSize, Space = borderSpace }
};
paragraphBorders.AppendChild(border);
}
@@ -516,6 +533,15 @@ private void AddHeadingSingleParagraph(int level, string text, HeadingStyle styl
var paragraphProps = CreateHeadingParagraphProperties(level, style);
paragraph.AppendChild(paragraphProps);
+ // Icon prefix run
+ if (!string.IsNullOrEmpty(style.IconPrefix))
+ {
+ var iconColor = string.IsNullOrEmpty(style.IconPrefixColor) ? style.Color : style.IconPrefixColor;
+ var iconRun = paragraph.AppendChild(new Run());
+ iconRun.AppendChild(CreateBaseRunProperties(style.FontSize, iconColor, bold: style.Bold));
+ iconRun.AppendChild(new Text(style.IconPrefix + " ") { Space = SpaceProcessingModeValues.Preserve });
+ }
+
var run = paragraph.AppendChild(new Run());
run.AppendChild(CreateBaseRunProperties(style.FontSize, style.Color, bold: style.Bold));
run.AppendChild(new Text(text) { Space = SpaceProcessingModeValues.Preserve });
@@ -565,7 +591,8 @@ private void AddHeadingWithSpacers(int level, string text, HeadingStyle style)
style.BorderPosition,
style.BorderColor ?? "3498db",
style.BorderSize,
- style.BorderSpace));
+ style.BorderSpace,
+ style.BorderStyle));
// Background shading
if (!string.IsNullOrEmpty(style.BackgroundColor))
@@ -590,6 +617,15 @@ private void AddHeadingWithSpacers(int level, string text, HeadingStyle style)
mainParagraph.AppendChild(mainProps);
+ // Icon prefix run
+ if (!string.IsNullOrEmpty(style.IconPrefix))
+ {
+ var iconColor = string.IsNullOrEmpty(style.IconPrefixColor) ? style.Color : style.IconPrefixColor;
+ var iconRun = mainParagraph.AppendChild(new Run());
+ iconRun.AppendChild(CreateBaseRunProperties(style.FontSize, iconColor, bold: style.Bold));
+ iconRun.AppendChild(new Text(style.IconPrefix + " ") { Space = SpaceProcessingModeValues.Preserve });
+ }
+
var run = mainParagraph.AppendChild(new Run());
run.AppendChild(CreateBaseRunProperties(style.FontSize, style.Color, bold: style.Bold));
run.AppendChild(new Text(text) { Space = SpaceProcessingModeValues.Preserve });
@@ -635,7 +671,8 @@ private ParagraphProperties CreateHeadingParagraphProperties(int level, HeadingS
style.BorderPosition,
style.BorderColor ?? "3498db",
style.BorderSize,
- style.BorderSpace));
+ style.BorderSpace,
+ style.BorderStyle));
}
// Background shading
diff --git a/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs b/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
index edb9340..c86b2be 100644
--- a/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
+++ b/csharp-version/src/MarkdownToDocx.Styling/Models/StyleConfiguration.cs
@@ -166,6 +166,25 @@ public sealed record HeadingStyleConfig
/// at the specified level (e.g., set to 1 on H2 to prevent a page break after H1).
///
public int? SuppressPageBreakIfPrevHeadingLevel { get; init; }
+
+ ///
+ /// Border line style for all border positions on this heading.
+ /// Supported values: "single" (default), "double", "thick", "dotted", "dashed",
+ /// "dotDash", "wave", "triple".
+ ///
+ public string BorderStyle { get; init; } = "single";
+
+ ///
+ /// Unicode character (or short string) prepended to the heading text as a decorative prefix.
+ /// For example "◆", "★", "▶", or "01". Null or empty means no prefix.
+ ///
+ public string? IconPrefix { get; init; }
+
+ ///
+ /// Color of the icon prefix in hexadecimal format (e.g., "E91E8C").
+ /// When null or empty, the heading's main Color is used.
+ ///
+ public string? IconPrefixColor { get; init; }
}
///
diff --git a/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs b/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
index 822599c..c771643 100644
--- a/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
+++ b/csharp-version/src/MarkdownToDocx.Styling/Styling/StyleApplicator.cs
@@ -47,7 +47,10 @@ public HeadingStyle ApplyHeadingStyle(int level, StyleConfiguration config)
SpaceAfter = headingConfig.SpaceAfter,
BorderExtent = headingConfig.BorderExtent,
LeftIndent = headingConfig.LeftIndent,
- SuppressPageBreakIfPrevHeadingLevel = headingConfig.SuppressPageBreakIfPrevHeadingLevel
+ SuppressPageBreakIfPrevHeadingLevel = headingConfig.SuppressPageBreakIfPrevHeadingLevel,
+ BorderStyle = headingConfig.BorderStyle,
+ IconPrefix = headingConfig.IconPrefix,
+ IconPrefixColor = headingConfig.IconPrefixColor
};
}
diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
index 83d62f4..d4cba83 100644
--- a/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
+++ b/csharp-version/tests/MarkdownToDocx.Tests/Unit/OpenXmlDocumentBuilderTests.cs
@@ -2326,6 +2326,174 @@ public void AddHeading_WithBottomBorderOnly_ShouldNotAddNilBorders()
borders.GetFirstChild().Should().BeNull();
}
+ [Fact]
+ public void AddHeading_WithBorderStyleDouble_ShouldRenderDoubleBorder()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with
+ {
+ ShowBorder = true,
+ BorderPosition = "bottom",
+ BorderColor = "3498db",
+ BorderSize = 12,
+ BorderSpace = 2,
+ BorderStyle = "double"
+ };
+
+ // Act
+ builder.AddHeading(1, "Double border heading", style);
+ builder.Save();
+
+ // Assert: bottom border uses Double style
+ _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.Double);
+ }
+
+ [Fact]
+ public void AddHeading_WithBorderStyleWave_ShouldRenderWaveBorder()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with
+ {
+ ShowBorder = true,
+ BorderPosition = "bottom",
+ BorderColor = "3498db",
+ BorderSize = 12,
+ BorderSpace = 2,
+ BorderStyle = "wave"
+ };
+
+ // Act
+ builder.AddHeading(2, "Wave border heading", style);
+ builder.Save();
+
+ // Assert: bottom border uses Wave style
+ _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.Wave);
+ }
+
+ [Fact]
+ public void AddHeading_WithIconPrefix_ShouldPrependIconRun()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with
+ {
+ IconPrefix = "◆"
+ };
+
+ // Act
+ builder.AddHeading(1, "Test Heading", style);
+ builder.Save();
+
+ // Assert: paragraph has two runs — icon prefix run and text run
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var paragraph = doc.MainDocumentPart!.Document.Body!
+ .Descendants()
+ .First(p => p.Descendants().Any());
+
+ var runs = paragraph.Descendants().ToList();
+ runs.Should().HaveCount(2);
+ runs[0].InnerText.Should().StartWith("◆");
+ runs[1].InnerText.Should().Be("Test Heading");
+ }
+
+ [Fact]
+ public void AddHeading_WithIconPrefixColor_ShouldUseCustomColor()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with
+ {
+ IconPrefix = "★",
+ IconPrefixColor = "E91E8C"
+ };
+
+ // Act
+ builder.AddHeading(1, "Colored Icon Heading", style);
+ builder.Save();
+
+ // Assert: icon run uses custom color, text run uses heading color
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var paragraph = doc.MainDocumentPart!.Document.Body!
+ .Descendants()
+ .First(p => p.Descendants().Any());
+
+ var runs = paragraph.Descendants().ToList();
+ runs.Should().HaveCount(2);
+ var iconColor = runs[0].RunProperties!.GetFirstChild()!.Val!.Value;
+ var textColor = runs[1].RunProperties!.GetFirstChild()!.Val!.Value;
+ iconColor.Should().Be("E91E8C");
+ textColor.Should().Be("2c3e50");
+ }
+
+ [Fact]
+ public void AddHeading_WithIconPrefixNoColor_ShouldUseHeadingColor()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with
+ {
+ IconPrefix = "▶",
+ IconPrefixColor = null
+ };
+
+ // Act
+ builder.AddHeading(1, "Default Color Icon", style);
+ builder.Save();
+
+ // Assert: icon run uses heading color (same as text run color)
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var paragraph = doc.MainDocumentPart!.Document.Body!
+ .Descendants()
+ .First(p => p.Descendants().Any());
+
+ var runs = paragraph.Descendants().ToList();
+ runs.Should().HaveCount(2);
+ var iconColor = runs[0].RunProperties!.GetFirstChild()!.Val!.Value;
+ var textColor = runs[1].RunProperties!.GetFirstChild()!.Val!.Value;
+ iconColor.Should().Be("2c3e50");
+ textColor.Should().Be("2c3e50");
+ }
+
+ [Fact]
+ public void AddHeading_WithNoIconPrefix_ShouldHaveSingleRun()
+ {
+ // Arrange
+ using var builder = new OpenXmlDocumentBuilder(_stream, _horizontalProvider);
+ var style = CreateDefaultHeadingStyle() with { IconPrefix = null };
+
+ // Act
+ builder.AddHeading(1, "Plain Heading", style);
+ builder.Save();
+
+ // Assert: only one run (no icon prefix)
+ _stream.Position = 0;
+ using var doc = WordprocessingDocument.Open(_stream, false);
+ var paragraph = doc.MainDocumentPart!.Document.Body!
+ .Descendants()
+ .First(p => p.Descendants().Any());
+
+ paragraph.Descendants().Should().HaveCount(1);
+ }
+
public void Dispose()
{
_stream?.Dispose();
diff --git a/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs b/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
index 60809d0..9fadbdc 100644
--- a/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
+++ b/csharp-version/tests/MarkdownToDocx.Tests/Unit/StyleApplicatorTests.cs
@@ -589,6 +589,74 @@ public void ApplyHeadingStyle_WithDefaultSuppressPageBreak_ShouldBeNull()
style.SuppressPageBreakIfPrevHeadingLevel.Should().BeNull();
}
+ [Fact]
+ public void ApplyHeadingStyle_WithBorderStyle_ShouldMapCorrectly()
+ {
+ // Arrange
+ var config = new StyleConfiguration
+ {
+ H1 = new HeadingStyleConfig
+ {
+ Size = 20,
+ Bold = true,
+ Color = "000000",
+ ShowBorder = true,
+ BorderStyle = "double"
+ }
+ };
+
+ // Act
+ var style = _applicator.ApplyHeadingStyle(1, config);
+
+ // Assert
+ style.BorderStyle.Should().Be("double");
+ }
+
+ [Fact]
+ public void ApplyHeadingStyle_WithDefaultBorderStyle_ShouldBeSingle()
+ {
+ // Act
+ var style = _applicator.ApplyHeadingStyle(1, _testConfig);
+
+ // Assert
+ style.BorderStyle.Should().Be("single");
+ }
+
+ [Fact]
+ public void ApplyHeadingStyle_WithIconPrefix_ShouldMapCorrectly()
+ {
+ // Arrange
+ var config = new StyleConfiguration
+ {
+ H2 = new HeadingStyleConfig
+ {
+ Size = 18,
+ Bold = true,
+ Color = "000000",
+ IconPrefix = "◆",
+ IconPrefixColor = "E91E8C"
+ }
+ };
+
+ // Act
+ var style = _applicator.ApplyHeadingStyle(2, config);
+
+ // Assert
+ style.IconPrefix.Should().Be("◆");
+ style.IconPrefixColor.Should().Be("E91E8C");
+ }
+
+ [Fact]
+ public void ApplyHeadingStyle_WithDefaultIconPrefix_ShouldBeNull()
+ {
+ // Act
+ var style = _applicator.ApplyHeadingStyle(1, _testConfig);
+
+ // Assert
+ style.IconPrefix.Should().BeNull();
+ style.IconPrefixColor.Should().BeNull();
+ }
+
[Fact]
public void ApplyParagraphStyle_ShouldMapInlineCodeFont()
{