From df29763019ff865949deb9989e79714dc0a7fd67 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Mon, 10 Jun 2024 08:11:48 +0200 Subject: [PATCH 1/4] Allow & and | in conditions!! --- DocxTemplater.Test/PatternMatcherTest.cs | 1 + DocxTemplater/PatterMatcher.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/DocxTemplater.Test/PatternMatcherTest.cs b/DocxTemplater.Test/PatternMatcherTest.cs index 94286c2..4c702ab 100644 --- a/DocxTemplater.Test/PatternMatcherTest.cs +++ b/DocxTemplater.Test/PatternMatcherTest.cs @@ -40,6 +40,7 @@ static IEnumerable TestPatternMatch_Cases() yield return new TestCaseData("{?{!MyBool}}").Returns(new[] { PatternType.Condition }); yield return new TestCaseData("{ ? { MyBool}}").Returns(new[] { PatternType.Condition }); yield return new TestCaseData("{?{ a / 20 >= 12 }}").Returns(new[] { PatternType.Condition }); + yield return new TestCaseData("{?{ a / 20 >= 12 && a.b == true || !b.c }}").Returns(new[] { PatternType.Condition }); yield return new TestCaseData("{{var}:F(d)}").Returns(new[] { PatternType.Variable }); yield return new TestCaseData("{{ds.foo.var}:f('HH : mm : s')}").Returns(new[] { PatternType.Variable }).SetName("Format with date pattern"); yield return new TestCaseData("{{ds.foo.var}:f(HH:mm)}").Returns(new[] { PatternType.Variable }).SetName("Format with date pattern"); diff --git a/DocxTemplater/PatterMatcher.cs b/DocxTemplater/PatterMatcher.cs index 1485d1c..ba38c21 100644 --- a/DocxTemplater/PatterMatcher.cs +++ b/DocxTemplater/PatterMatcher.cs @@ -25,7 +25,7 @@ internal static class PatternMatcher (?:\s*s\s*:) | (?(?:else)|:) | (?(condMarker) # if condition marker is set, we expect a condition - (?[a-zA-Z0-9+\-*\/><=\s\.\!]+)? #condition name (without brackets) + (?[a-zA-Z0-9+\-*\/><=\s\.\!&\|]+)? #condition name (without brackets) | (?: (?[\/\#])?(?[a-zA-Z0-9\._]+)? #variable name From 159d6d485ced20a8cd82d5fbf5c61be727356ef6 Mon Sep 17 00:00:00 2001 From: Manuel Amstutz Date: Tue, 11 Jun 2024 13:34:48 +0200 Subject: [PATCH 2/4] Markdown Formatter --- .../DocxTemplater.Markdown.csproj | 15 + .../MarkDownFormatterConfiguration.cs | 66 ++ DocxTemplater.Markdown/MarkdownFormatter.cs | 52 + .../MarkdownToOpenXmlRenderer.cs | 183 ++++ .../Renderer/HeadingRenderer.cs | 21 + .../Inlines/EmphasisInlineRenderer.cs | 26 + .../Renderer/Inlines/LineBreakLineRenderer.cs | 16 + .../Renderer/Inlines/LiteralInlineRenderer.cs | 12 + .../Renderer/ListRenderer.cs | 200 ++++ .../Renderer/OpenXmlObjectRenderer.cs | 9 + .../Renderer/ParagraphRenderer.cs | 18 + .../Renderer/TableRenderer.cs | 85 ++ .../Renderer/ThematicBreakRenderer.cs | 33 + DocxTemplater.Test/DocxTemplater.Test.csproj | 140 +-- DocxTemplater.Test/MarkdownRendererTest.cs | 300 ++++++ .../Resources/MarkdownTestTemplate.docx | Bin 0 -> 15412 bytes DocxTemplater.Test/Resources/TestMarkDown.md | 65 ++ DocxTemplater.sln | 112 +- DocxTemplater/OpenXmlHelper.cs | 958 +++++++++--------- 19 files changed, 1736 insertions(+), 575 deletions(-) create mode 100644 DocxTemplater.Markdown/DocxTemplater.Markdown.csproj create mode 100644 DocxTemplater.Markdown/MarkDownFormatterConfiguration.cs create mode 100644 DocxTemplater.Markdown/MarkdownFormatter.cs create mode 100644 DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/HeadingRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/Inlines/EmphasisInlineRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/Inlines/LineBreakLineRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/Inlines/LiteralInlineRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/ListRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/OpenXmlObjectRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/ParagraphRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/TableRenderer.cs create mode 100644 DocxTemplater.Markdown/Renderer/ThematicBreakRenderer.cs create mode 100644 DocxTemplater.Test/MarkdownRendererTest.cs create mode 100644 DocxTemplater.Test/Resources/MarkdownTestTemplate.docx create mode 100644 DocxTemplater.Test/Resources/TestMarkDown.md diff --git a/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj b/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj new file mode 100644 index 0000000..4f8a152 --- /dev/null +++ b/DocxTemplater.Markdown/DocxTemplater.Markdown.csproj @@ -0,0 +1,15 @@ + + + True + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/DocxTemplater.Markdown/MarkDownFormatterConfiguration.cs b/DocxTemplater.Markdown/MarkDownFormatterConfiguration.cs new file mode 100644 index 0000000..2d43a4b --- /dev/null +++ b/DocxTemplater.Markdown/MarkDownFormatterConfiguration.cs @@ -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(); + UnorderedListLevelConfiguration = new List(); + + 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 OrderedListLevelConfiguration + { + get; + } + + public List UnorderedListLevelConfiguration + { + get; + } + + /// + /// Name of a table style in the template document applied to tables. + /// + public string TableStyle { get; set; } + } +} diff --git a/DocxTemplater.Markdown/MarkdownFormatter.cs b/DocxTemplater.Markdown/MarkdownFormatter.cs new file mode 100644 index 0000000..82dacf2 --- /dev/null +++ b/DocxTemplater.Markdown/MarkdownFormatter.cs @@ -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(); + } + } +} diff --git a/DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs b/DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs new file mode 100644 index 0000000..846de3a --- /dev/null +++ b/DocxTemplater.Markdown/MarkdownToOpenXmlRenderer.cs @@ -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 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(); + 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 content) + { + if (!content.IsEmpty) + { + var newRun = new Run(new Text(content.ToString()){ Space = SpaceProcessingModeValues.Preserve }); + 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 m_formatStack; + public FormatScope(Stack formatStack, bool bold, bool italic, string style) + { + m_formatStack = formatStack; + m_formatStack.Push(new Format(bold, italic, style)); + } + + public void Dispose() + { + m_formatStack.Pop(); + } + } + } + +} diff --git a/DocxTemplater.Markdown/Renderer/HeadingRenderer.cs b/DocxTemplater.Markdown/Renderer/HeadingRenderer.cs new file mode 100644 index 0000000..480f143 --- /dev/null +++ b/DocxTemplater.Markdown/Renderer/HeadingRenderer.cs @@ -0,0 +1,21 @@ +using DocumentFormat.OpenXml.Wordprocessing; +using Markdig.Syntax; + +namespace DocxTemplater.Markdown.Renderer +{ + internal sealed class HeadingRenderer : OpenXmlObjectRenderer + { + 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(); + } + } +} diff --git a/DocxTemplater.Markdown/Renderer/Inlines/EmphasisInlineRenderer.cs b/DocxTemplater.Markdown/Renderer/Inlines/EmphasisInlineRenderer.cs new file mode 100644 index 0000000..529058c --- /dev/null +++ b/DocxTemplater.Markdown/Renderer/Inlines/EmphasisInlineRenderer.cs @@ -0,0 +1,26 @@ +using Markdig.Syntax.Inlines; + +namespace DocxTemplater.Markdown.Renderer.Inlines +{ + internal sealed class EmphasisInlineRenderer : OpenXmlObjectRenderer + { + 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); + } + } +} \ No newline at end of file diff --git a/DocxTemplater.Markdown/Renderer/Inlines/LineBreakLineRenderer.cs b/DocxTemplater.Markdown/Renderer/Inlines/LineBreakLineRenderer.cs new file mode 100644 index 0000000..7d19232 --- /dev/null +++ b/DocxTemplater.Markdown/Renderer/Inlines/LineBreakLineRenderer.cs @@ -0,0 +1,16 @@ +using Markdig.Syntax.Inlines; + +namespace DocxTemplater.Markdown.Renderer.Inlines +{ + internal sealed class LineBreakLineRenderer : OpenXmlObjectRenderer + { + protected override void Write(MarkdownToOpenXmlRenderer renderer, LineBreakInline obj) + { + if (renderer.IsLastInContainer) + { + return; + } + renderer.NewLine(); + } + } +} diff --git a/DocxTemplater.Markdown/Renderer/Inlines/LiteralInlineRenderer.cs b/DocxTemplater.Markdown/Renderer/Inlines/LiteralInlineRenderer.cs new file mode 100644 index 0000000..8f5d626 --- /dev/null +++ b/DocxTemplater.Markdown/Renderer/Inlines/LiteralInlineRenderer.cs @@ -0,0 +1,12 @@ +using Markdig.Syntax.Inlines; + +namespace DocxTemplater.Markdown.Renderer.Inlines +{ + internal sealed class LiteralInlineRenderer : OpenXmlObjectRenderer + { + protected override void Write(MarkdownToOpenXmlRenderer renderer, LiteralInline obj) + { + renderer.Write(ref obj.Content); + } + } +} \ No newline at end of file diff --git a/DocxTemplater.Markdown/Renderer/ListRenderer.cs b/DocxTemplater.Markdown/Renderer/ListRenderer.cs new file mode 100644 index 0000000..d8c5772 --- /dev/null +++ b/DocxTemplater.Markdown/Renderer/ListRenderer.cs @@ -0,0 +1,200 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using Markdig.Syntax; +using System.Linq; + +namespace DocxTemplater.Markdown.Renderer +{ + internal sealed class ListRenderer : OpenXmlObjectRenderer + { + private int m_level = -1; + private int m_levelWithSameOrdering = -1; + private bool? m_lastLevelOrdered; + + private readonly MainDocumentPart m_mainDocumentPart; + private readonly MarkDownFormatterConfiguration m_configuration; + private AbstractNum m_currentAbstractNumNotOrdered; + private AbstractNum m_currentAbstractNumOrdered; + private NumberingInstance m_currentNumberingInstanceOrdered; + private NumberingInstance m_currentNumberingInstanceNotOrdered; + private NumberingDefinitionsPart m_numberingDefinitionsPart; + private string m_listParagraphStyle; + + public ListRenderer(MainDocumentPart mainDocumentPart, MarkDownFormatterConfiguration configuration) + { + m_mainDocumentPart = mainDocumentPart; + m_configuration = configuration; + } + + protected override void Write(MarkdownToOpenXmlRenderer renderer, ListBlock listBlock) + { + + StartListLevel(listBlock.IsOrdered); + try + { + var numberingInstance = listBlock.IsOrdered ? m_currentNumberingInstanceOrdered : m_currentNumberingInstanceNotOrdered; + + foreach (var item in listBlock) + { + var numberingProps = + new NumberingProperties( + new NumberingLevelReference() { Val = m_levelWithSameOrdering }, + new NumberingId() { Val = numberingInstance.NumberID } + ); + var listItem = (ListItemBlock)item; + var listParagraph = new Paragraph(); + var paragraphProperties = new ParagraphProperties(numberingProps) + { + ParagraphStyleId = new ParagraphStyleId() { Val = m_listParagraphStyle } + }; + listParagraph.ParagraphProperties = paragraphProperties; + renderer.ReplaceIfCurrentParagraphIsEmpty(listParagraph); + renderer.ExplicitParagraph = true; + renderer.WriteChildren(listItem); + } + } + finally + { + EndListLevel(); + } + + if (m_level == -1) + { + renderer.AddParagraph(); + } + } + + private void CrateListParagraphStyleIfNotExistent() + { + if (m_listParagraphStyle == null) + { + var part = m_mainDocumentPart.StyleDefinitionsPart; + if (part == null) + { + part = m_mainDocumentPart.AddNewPart(); + part.Styles = new Styles(); + } + + var style = part.Styles?.Elements