Skip to content

Commit

Permalink
Merge pull request #17 from Amberg/workitems/SubTemplates
Browse files Browse the repository at this point in the history
Markdown formatter
  • Loading branch information
Amberg authored Jun 12, 2024
2 parents 1edeb93 + d055a81 commit e1cfd74
Show file tree
Hide file tree
Showing 40 changed files with 3,187 additions and 1,916 deletions.
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; }
}
}
108 changes: 108 additions & 0 deletions DocxTemplater.Markdown/MarkdownFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxTemplater.Formatter;
using Markdig;
using Markdig.Parsers;
using System;

namespace DocxTemplater.Markdown
{
public class MarkdownFormatter : IFormatter, IFormatterInitialization
{
private readonly MarkDownFormatterConfiguration m_configuration;
private IModelLookup m_modelLookup;
private ProcessSettings m_processSettings;
private IVariableReplacer m_variableReplacer;
private IScriptCompiler m_scriptCompiler;
private int m_nestingDepth;

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;
}
if (m_nestingDepth > 3)
{
throw new OpenXmlTemplateException("Markdown nesting depth exceeded");
}
m_nestingDepth++;


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);
var firstParagraph = renderer.CurrentParagraph;
renderer.Render(markdownDocument);
var lastParagraph = renderer.CurrentParagraph;
try
{
target.RemoveWithEmptyParent();
DoVariableReplacementInParagraphs(firstParagraph, lastParagraph);
}
catch (Exception e)
{
throw new OpenXmlTemplateException("Variable Replacement in markdown failed", e);
}
}
else
{
throw new OpenXmlTemplateException("Markdown currently only supported in MainDocument");
}
}
m_nestingDepth--;
target.RemoveWithEmptyParent();
}

private void DoVariableReplacementInParagraphs(Paragraph firstParagraph, Paragraph lastParagraph)
{
var currentParagraph = firstParagraph;
do
{
if (currentParagraph.InnerText.Contains('{'))
{
var processor = new XmlNodeTemplate(currentParagraph, m_processSettings, m_modelLookup, m_variableReplacer, m_scriptCompiler);
processor.Process();
}
currentParagraph = currentParagraph.NextSibling<Paragraph>();
if (currentParagraph == null)
{
break;
}
}
while (currentParagraph != lastParagraph);
}

public void Initialize(IModelLookup modelLookup, ProcessSettings processSettings)
{
m_modelLookup = modelLookup;
m_processSettings = processSettings;
}

public void Initialize(IModelLookup modelLookup, IScriptCompiler scriptCompiler, IVariableReplacer variableReplacer,
ProcessSettings processSettings)
{
m_modelLookup = modelLookup;
m_processSettings = processSettings;
m_variableReplacer = variableReplacer;
m_scriptCompiler = scriptCompiler;
}
}
}
182 changes: 182 additions & 0 deletions DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@

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 text = new Text(content.ToString());
if (string.IsNullOrWhiteSpace(text.Text))
{
text.Space = SpaceProcessingModeValues.Preserve;
}
var newRun = new Run(text);
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 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();
}
}
}
Loading

0 comments on commit e1cfd74

Please sign in to comment.