Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown formatter #15

Closed
wants to merge 2 commits into from
Closed
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
15 changes: 15 additions & 0 deletions DocxTemplater.Markdown/DocxTemplater.Markdown.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Markdig" Version="0.37.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DocxTemplater\DocxTemplater.csproj" />
</ItemGroup>
</Project>
66 changes: 66 additions & 0 deletions DocxTemplater.Markdown/MarkDownFormatterConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using DocumentFormat.OpenXml.Wordprocessing;
using System.Collections.Generic;

namespace DocxTemplater.Markdown
{
public record ListLevelConfiguration(
string LevelText,
string FontOverride,
NumberFormatValues NumberingFormat,
int IndentPerLevel);

public class MarkDownFormatterConfiguration
{
public static readonly MarkDownFormatterConfiguration Default;

static MarkDownFormatterConfiguration()
{
Default = new MarkDownFormatterConfiguration();
}

public MarkDownFormatterConfiguration()
{
OrderedListLevelConfiguration = new List<ListLevelConfiguration>();
UnorderedListLevelConfiguration = new List<ListLevelConfiguration>();

OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%1.", null, NumberFormatValues.Decimal, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%2.", null, NumberFormatValues.LowerLetter, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%3.", null, NumberFormatValues.LowerRoman, 720));

OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%4.", null, NumberFormatValues.Decimal, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%5.", null, NumberFormatValues.LowerLetter, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%6.", null, NumberFormatValues.LowerRoman, 720));

OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%7.", null, NumberFormatValues.Decimal, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%8.", null, NumberFormatValues.LowerLetter, 720));
OrderedListLevelConfiguration.Add(new ListLevelConfiguration("%9.", null, NumberFormatValues.LowerRoman, 720));

UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0b7", "Symbol", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("o", "Courier New", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0a7", "Wingdings", NumberFormatValues.Bullet, 720));

UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0b7", "Symbol", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("o", "Courier New", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0a7", "Wingdings", NumberFormatValues.Bullet, 720));

UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0b7", "Symbol", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("o", "Courier New", NumberFormatValues.Bullet, 720));
UnorderedListLevelConfiguration.Add(new ListLevelConfiguration("\uf0a7", "Wingdings", NumberFormatValues.Bullet, 720));
}

public List<ListLevelConfiguration> OrderedListLevelConfiguration
{
get;
}

public List<ListLevelConfiguration> UnorderedListLevelConfiguration
{
get;
}

/// <summary>
/// Name of a table style in the template document applied to tables.
/// </summary>
public string TableStyle { get; set; }
}
}
52 changes: 52 additions & 0 deletions DocxTemplater.Markdown/MarkdownFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxTemplater.Formatter;
using Markdig.Parsers;
using System;
using DocumentFormat.OpenXml.Packaging;
using Markdig;

namespace DocxTemplater.Markdown
{
public class MarkdownFormatter : IFormatter
{
private readonly MarkDownFormatterConfiguration m_configuration;

public MarkdownFormatter(MarkDownFormatterConfiguration configuration = null)
{
m_configuration = configuration ?? MarkDownFormatterConfiguration.Default;
}

public bool CanHandle(Type type, string prefix)
{
string prefixUpper = prefix.ToUpper();
return prefixUpper is "MD" && type == typeof(string);
}

public void ApplyFormat(FormatterContext context, Text target)
{
if (context.Value is not string mdText)
{
return;
}

var root = target.GetRoot();
if (root is OpenXmlPartRootElement openXmlPartRootElement && openXmlPartRootElement.OpenXmlPart != null)
{

if (openXmlPartRootElement.OpenXmlPart is MainDocumentPart mainDocumentPart)
{
var pipeline = new MarkdownPipelineBuilder().UsePipeTables().Build();
var markdownDocument = MarkdownParser.Parse(mdText, pipeline);
var renderer = new MarkdownToOpenXmlRenderer(target, mainDocumentPart, m_configuration);
renderer.Render(markdownDocument);
}
else
{
throw new InvalidOperationException("Markdown currently only supported in MainDocument");
}
}
target.RemoveWithEmptyParent();
}
}
}
183 changes: 183 additions & 0 deletions DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxTemplater.Markdown.Renderer;
using DocxTemplater.Markdown.Renderer.Inlines;
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System;
using System.Collections.Generic;

namespace DocxTemplater.Markdown
{
internal sealed class MarkdownToOpenXmlRenderer : RendererBase
{
private sealed record Format(bool Bold, bool Italic, string Style);

private readonly Stack<Format> m_formatStack = new();
private OpenXmlCompositeElement m_parentElement;
private bool m_lastElemntWasNewLine;

public MarkdownToOpenXmlRenderer(
Text previousOpenXmlNode,
MainDocumentPart mainDocumentPart,
MarkDownFormatterConfiguration configuration)
{
m_lastElemntWasNewLine = true;
m_formatStack.Push(new Format(false, false, null));
m_parentElement = previousOpenXmlNode.GetFirstAncestor<Paragraph>();
ObjectRenderers.Add(new LiteralInlineRenderer());
ObjectRenderers.Add(new ParagraphRenderer());
ObjectRenderers.Add(new LineBreakLineRenderer());
ObjectRenderers.Add(new EmphasisInlineRenderer());
ObjectRenderers.Add(new TableRenderer(configuration, mainDocumentPart));
ObjectRenderers.Add(new ListRenderer(mainDocumentPart, configuration));
ObjectRenderers.Add(new HeadingRenderer());
ObjectRenderers.Add(new ThematicBreakRenderer());
}

public bool ExplicitParagraph { get; set; }

public Paragraph CurrentParagraph => m_parentElement as Paragraph;

public MarkdownToOpenXmlRenderer Write(ref StringSlice slice)
{
Write(slice.AsSpan());
return this;
}

public void Write(ReadOnlySpan<char> content)
{
if (!content.IsEmpty)
{
var newRun = new Run(new Text(content.ToString()));
var format = m_formatStack.Peek();
if (format.Bold || format.Italic || format.Style != null)
{
RunProperties run1Properties = new();
if (format.Bold)
{
run1Properties.Append(new Bold());
}
if (format.Italic)
{
run1Properties.Append(new Italic());
}
newRun.RunProperties = run1Properties;

//add style
if (format.Style != null)
{
var runStyle = new RunStyle { Val = format.Style };
newRun.RunProperties.Append(runStyle);
}
}
m_parentElement.Append(newRun);
m_lastElemntWasNewLine = false;
}
}

public override object Render(MarkdownObject markdownObject)
{
Write(markdownObject);
return null;
}

public void WriteLeafInline(LeafBlock leafBlock)
{
Inline inline = leafBlock.Inline;
while (inline != null)
{
Write(inline);
inline = inline.NextSibling;
}
}

public IDisposable PushFormat(bool? bold, bool? italic)
{
var currentStyle = m_formatStack.Peek();
bold ??= currentStyle.Bold;
italic ??= currentStyle.Italic;
return new FormatScope(m_formatStack, bold.Value, italic.Value, currentStyle.Style);
}

public IDisposable PushStyle(string style)
{
return new FormatScope(m_formatStack, m_formatStack.Peek().Bold, m_formatStack.Peek().Italic, style);
}

public void NewLine()
{
m_parentElement.Append(new Run(new Break()));
m_lastElemntWasNewLine = true;
}

public void EnsureNewLine()
{
if (!m_lastElemntWasNewLine)
{
NewLine();
}
}

public void ReplaceIfCurrentParagraphIsEmpty(Paragraph newParagraph)
{
var lastParagraph = CurrentParagraph;
AddParagraph(newParagraph);
if (lastParagraph != null && lastParagraph.ChildElements.Count == 0)
{
lastParagraph.Remove();
}
}

public void AddParagraph(OpenXmlCompositeElement paragraph = null)
{
paragraph ??= new Paragraph();
m_parentElement = m_parentElement.InsertAfterSelf(paragraph);
m_lastElemntWasNewLine = false;
}

public IDisposable PushParagraph(Paragraph paragraph)
{
m_lastElemntWasNewLine = true;
return new ParagraphScope(this, paragraph);
}

private sealed class ParagraphScope : IDisposable
{
private readonly MarkdownToOpenXmlRenderer m_renderer;
private readonly OpenXmlCompositeElement m_previousParagraph;

public ParagraphScope(MarkdownToOpenXmlRenderer renderer, Paragraph element)
{
m_renderer = renderer;
m_previousParagraph = m_renderer.m_parentElement;
m_renderer.m_parentElement = element;
}

public void Dispose()
{
m_renderer.m_parentElement = m_previousParagraph;
}
}

private sealed class FormatScope : IDisposable
{
private readonly Stack<Format> m_formatStack;
public FormatScope(Stack<Format> formatStack, bool bold, bool italic, string style)
{
m_formatStack = formatStack;
m_formatStack.Push(new Format(bold, italic, style));
}

public void Dispose()
{
m_formatStack.Pop();
}
}
}

}
21 changes: 21 additions & 0 deletions DocxTemplater.Markdown/Renderer/HeadingRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DocumentFormat.OpenXml.Wordprocessing;
using Markdig.Syntax;

namespace DocxTemplater.Markdown.Renderer
{
internal sealed class HeadingRenderer : OpenXmlObjectRenderer<HeadingBlock>
{
protected override void Write(MarkdownToOpenXmlRenderer renderer, HeadingBlock heading)
{
var headingParagraph = new Paragraph();
// add heading style
var headingStyle = new ParagraphStyleId() { Val = $"Heading{heading.Level}" };
var paragraphProps = new ParagraphProperties();
paragraphProps.Append(headingStyle);
headingParagraph.ParagraphProperties = paragraphProps;
renderer.AddParagraph(headingParagraph);
renderer.WriteLeafInline(heading);
renderer.AddParagraph();
}
}
}
26 changes: 26 additions & 0 deletions DocxTemplater.Markdown/Renderer/Inlines/EmphasisInlineRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Markdig.Syntax.Inlines;

namespace DocxTemplater.Markdown.Renderer.Inlines
{
internal sealed class EmphasisInlineRenderer : OpenXmlObjectRenderer<EmphasisInline>
{
protected override void Write(MarkdownToOpenXmlRenderer renderer, EmphasisInline obj)
{
bool? italic = null;
bool? bold = null;
if (obj.DelimiterChar is '_' or '*')
{
if (obj.DelimiterCount == 1)
{
italic = true;
}
else if (obj.DelimiterCount == 2)
{
bold = true;
}
}
using var format = renderer.PushFormat(bold, italic);
renderer.WriteChildren(obj);
}
}
}
16 changes: 16 additions & 0 deletions DocxTemplater.Markdown/Renderer/Inlines/LineBreakLineRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Markdig.Syntax.Inlines;

namespace DocxTemplater.Markdown.Renderer.Inlines
{
internal sealed class LineBreakLineRenderer : OpenXmlObjectRenderer<LineBreakInline>
{
protected override void Write(MarkdownToOpenXmlRenderer renderer, LineBreakInline obj)
{
if (renderer.IsLastInContainer)
{
return;
}
renderer.NewLine();
}
}
}
Loading
Loading