-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #17 from Amberg/workitems/SubTemplates
Markdown formatter
- Loading branch information
Showing
40 changed files
with
3,187 additions
and
1,916 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
Oops, something went wrong.