Skip to content

Commit

Permalink
Allow to pass Culture to apply culture specific formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
Amberg committed Jan 11, 2024
1 parent be8251e commit 165f3c3
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dotnet_diagnostic.IDE0290.severity = silent
# IDE0045: Convert to conditional expression
dotnet_diagnostic.IDE0045.severity = suggestion

# IDE0301: Simplify collection initialization
dotnet_diagnostic.IDE0301.severity = suggestion

[{*Test}/**.cs]
#Inline variable declaration (IDE0018)
Expand Down
3 changes: 1 addition & 2 deletions DocxTemplater.Images/ImageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ public void ApplyFormat(FormatterContext context, Text target)
}

// case 1. Image ist the only child element of a <wps:wsp> (TextBox)
if (TryHandleImageInWordprocessingShape(target, impagepartRelationShipId, image,
context.Args.FirstOrDefault(), maxPropertyId))
if (TryHandleImageInWordprocessingShape(target, impagepartRelationShipId, image, context.Args.FirstOrDefault() ?? string.Empty, maxPropertyId))
{
return;
}
Expand Down
45 changes: 44 additions & 1 deletion DocxTemplater.Test/DocxTemplateTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using DocumentFormat.OpenXml;
using System.Collections;
using System.Globalization;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using DocxTemplater.Images;
Expand Down Expand Up @@ -41,6 +43,47 @@ public void ReplaceTextBoldIsPreserved()
Assert.That(body.Descendants<Text>().Skip(1).First().Text, Is.EqualTo("Replaced"));
}

[Test, TestCaseSource(nameof(CultureIsAppliedTest_Cases))]
public string CultureIsAppliedTest(string formatter, CultureInfo culture, object value)
{
using var memStream = new MemoryStream();
using var wpDocument = WordprocessingDocument.Create(memStream, WordprocessingDocumentType.Document);

MainDocumentPart mainPart = wpDocument.AddMainDocumentPart();
mainPart.Document = new Document(new Body(new Paragraph(
new Run(new Text("Double without Formatter:")),
new Run(new Text($"{{{{var}}{formatter}}}")),
new Run(new Text("Double with formatter"))
)));
wpDocument.Save();
memStream.Position = 0;
var docTemplate = new DocxTemplate(memStream, new ProcessSettings() { Culture = culture });
docTemplate.BindModel("var", value);
var result = docTemplate.Process();
docTemplate.Validate();
Assert.IsNotNull(result);
result.Position = 0;

var document = WordprocessingDocument.Open(result, false);
var body = document.MainDocumentPart.Document.Body;
return body.Descendants<Text>().Skip(1).First().Text;
}

static IEnumerable CultureIsAppliedTest_Cases()
{
yield return new TestCaseData("", new CultureInfo("en-us"), new DateTime(2024, 11, 1)).Returns("11/1/2024 12:00:00 AM");
yield return new TestCaseData("", new CultureInfo("de-ch"), new DateTime(2024, 11, 1)).Returns("01.11.2024 00:00:00");
yield return new TestCaseData(":f(d)", new CultureInfo("en-us"), new DateTime(2024, 11, 1, 20, 22, 33)).Returns("11/1/2024");
yield return new TestCaseData(":FORMAT(D)", new CultureInfo("en-us"), new DateTime(2024, 11, 1, 20, 22, 33)).Returns("Friday, November 1, 2024");
yield return new TestCaseData(":F(yyyy MM dd - HH mm ss)", new CultureInfo("en-us"), new DateTime(2024, 11, 1, 20, 22, 33)).Returns("2024 11 01 - 20 22 33");
yield return new TestCaseData(":F(n)", new CultureInfo("en-us"), 50000.45).Returns("50,000.450");
yield return new TestCaseData(":F(c)", new CultureInfo("en-us"), 50000.45).Returns("$50,000.45");
yield return new TestCaseData(":F(n)", new CultureInfo("de"), 50000.45).Returns("50.000,450");
yield return new TestCaseData(":F(c)", new CultureInfo("de-ch"), 50000.45).Returns("CHF 50’000.45");

}


[Test]
public void BindCollection()
{
Expand Down
41 changes: 38 additions & 3 deletions DocxTemplater.Test/PatternMatcherTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,62 @@ public PatternType[] TestPatternMatch(string input)
Assert.That(match.Match.Value.First(), Is.EqualTo('{'));
Assert.That(match.Match.Value.Last(), Is.EqualTo('}'));
}

return matches.Select(x => x.Type).ToArray();
}

static IEnumerable TestPatternMatch_Cases()
{
yield return new TestCaseData("{{Foo}}").Returns(new[] { PatternType.Variable });
yield return new TestCaseData("{{Foo}:blupp()}").Returns(new[] { PatternType.Variable });


yield return new TestCaseData("{{/Items}}").Returns(new[] { PatternType.LoopEnd });
yield return new TestCaseData("{{#items}}").Returns(new[] { PatternType.LoopStart });
yield return new TestCaseData("{{/Items.InnerCollection}}").Returns(new[] { PatternType.LoopEnd });
yield return new TestCaseData("{{#items.InnerCollection}}").Returns(new[] { PatternType.LoopStart });
yield return new TestCaseData("{{a.foo > 5}}").Returns(new[] { PatternType.Condition });
yield return new TestCaseData("{{ a > 5 }}").Returns(new[] { PatternType.Condition });
yield return new TestCaseData("{{ a / 20 >= 12 }}").Returns(new[] { PatternType.Condition });
yield return new TestCaseData("{{var}:F(d)}").Returns(new[] { PatternType.Variable });
yield return new TestCaseData("{{var}:toupper}").Returns(new[] { PatternType.Variable });
yield return new TestCaseData("{{else}}").Returns(new[] { PatternType.ConditionElse });
yield return new TestCaseData("{{var}:format(a,b)}").Returns(new[] { PatternType.Variable })
.SetName("Multiple Arguments");
yield return new TestCaseData("{{/}}").Returns(new[] { PatternType.ConditionEnd });
yield return new TestCaseData("NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{else}}" +
"I'm here if if this is not the case{{/}}{{/ds.Items.InnerCollection}}{{/Items}}")
.Returns(new[] { PatternType.Variable, PatternType.ConditionElse, PatternType.ConditionEnd, PatternType.LoopEnd, PatternType.LoopEnd })
yield return new TestCaseData(
"NumericValue is greater than 0 - {{ds.Items.InnerCollection.InnerValue}:toupper()}{{else}}" +
"I'm here if if this is not the case{{/}}{{/ds.Items.InnerCollection}}{{/Items}}")
.Returns(new[]
{
PatternType.Variable, PatternType.ConditionElse, PatternType.ConditionEnd, PatternType.LoopEnd,
PatternType.LoopEnd
})
.SetName("Complex Match 1");
}

[Test, TestCaseSource(nameof(PatternMatcherArgumentParsingTest_Cases))]
public string[] PatternMatcherArgumentParsingTest(string syntax)
{
var match = PatternMatcher.FindSyntaxPatterns(syntax).First();
return match.Arguments;
}

static IEnumerable PatternMatcherArgumentParsingTest_Cases()
{
yield return new TestCaseData("{{Foo}}").Returns(Array.Empty<string>());
yield return new TestCaseData("{{Foo}:format}").Returns(Array.Empty<string>());
yield return new TestCaseData("{{Foo}:format()}").Returns(Array.Empty<string>());
yield return new TestCaseData("{{Foo}:format('')}").Returns(new[] { string.Empty });
yield return new TestCaseData("{{Foo}:format(a)}").Returns(new[] { "a" });
yield return new TestCaseData("{{Foo}:format(param)}").Returns(new[] { "param" });
yield return new TestCaseData("{{Foo}:format('param')}").Returns(new[] { "param" });
yield return new TestCaseData("{{Foo}:format(a,b)}").Returns(new[] { "a", "b" });
yield return new TestCaseData("{{Foo}:format(a,b,c)}").Returns(new[] { "a", "b", "c" });
yield return new TestCaseData("{{Foo}:format(a,'a b',c)}").Returns(new[] { "a", "a b", "c" });
yield return new TestCaseData("{{Foo}:format(a,b,'YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "b", "YYYY_MMM/DD FF", "d" });
yield return new TestCaseData("{{Foo}:format(a,'John Doe','YYYY_MMM/DD FF',d)}").Returns(new[] { "a", "John Doe", "YYYY_MMM/DD FF", "d" });
}
}
}

11 changes: 7 additions & 4 deletions DocxTemplater/DocxTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ public sealed class DocxTemplate : IDisposable
private readonly VariableReplacer m_variableReplacer;
private readonly ScriptCompiler m_scriptCompiler;

public DocxTemplate(Stream docXStream)
public DocxTemplate(Stream docXStream, ProcessSettings settings = null)
{
Settings = settings ?? ProcessSettings.Default;
m_stream = new MemoryStream();
docXStream.CopyTo(m_stream);
m_stream.Position = 0;
Expand All @@ -42,14 +43,16 @@ public DocxTemplate(Stream docXStream)
m_wpDocument = WordprocessingDocument.Open(m_stream, true, openSettings);
m_models = new ModelDictionary();
m_scriptCompiler = new ScriptCompiler(m_models);
m_variableReplacer = new VariableReplacer(m_models);
m_variableReplacer = new VariableReplacer(m_models, Settings);
Processed = false;
}

public static DocxTemplate Open(string pathToTemplate)
public ProcessSettings Settings { get; }

public static DocxTemplate Open(string pathToTemplate, ProcessSettings settings = null)
{
using var fileStream = new FileStream(pathToTemplate, FileMode.Open, FileAccess.Read);
return new DocxTemplate(fileStream);
return new DocxTemplate(fileStream, settings);
}

public IReadOnlyDictionary<string, object> Models => m_models.Models;
Expand Down
4 changes: 2 additions & 2 deletions DocxTemplater/Formatter/CaseFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public void ApplyFormat(FormatterContext context, Text target)
{
if (context.Formatter.Equals("toupper", StringComparison.InvariantCultureIgnoreCase))
{
target.Text = str.ToUpper();
target.Text = str.ToUpper(context.Culture);
}
else if (context.Formatter.Equals("tolower", StringComparison.InvariantCultureIgnoreCase))
{
target.Text = str.ToLower();
target.Text = str.ToLower(context.Culture);
}
}
else
Expand Down
2 changes: 1 addition & 1 deletion DocxTemplater/Formatter/FormatPatternFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void ApplyFormat(FormatterContext context, Text target)
}
if (context.Value is IFormattable formattable)
{
target.Text = formattable.ToString(context.Args[0], null);
target.Text = formattable.ToString(context.Args[0], context.Culture);
}
else
{
Expand Down
15 changes: 11 additions & 4 deletions DocxTemplater/Formatter/VariableReplacer.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
using DocumentFormat.OpenXml;
using System;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Wordprocessing;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace DocxTemplater.Formatter
{
internal class VariableReplacer
{
private readonly ModelDictionary m_models;
private readonly ProcessSettings m_processSettings;
private readonly List<IFormatter> m_formatters;

public VariableReplacer(ModelDictionary models)
public VariableReplacer(ModelDictionary models, ProcessSettings processSettings)
{
m_models = models;
m_processSettings = processSettings;
m_formatters = new List<IFormatter>();
m_formatters.Add(new FormatPatternFormatter());
m_formatters.Add(new HtmlFormatter());
Expand Down Expand Up @@ -42,12 +44,17 @@ public void ApplyFormatter(PatternMatch patternMatch, object value, Text target)
{
if (formatter.CanHandle(value.GetType(), patternMatch.Formatter))
{
var context = new FormatterContext(patternMatch.Variable, patternMatch.Formatter, patternMatch.Arguments, value, CultureInfo.CurrentUICulture);
var context = new FormatterContext(patternMatch.Variable, patternMatch.Formatter, patternMatch.Arguments, value, m_processSettings.Culture);
formatter.ApplyFormat(context, target);
return;
}
}
}
if (value is IFormattable formattable)
{
target.Text = formattable.ToString(null, m_processSettings.Culture);
return;
}
target.Text = value.ToString() ?? string.Empty;

}
Expand Down
19 changes: 16 additions & 3 deletions DocxTemplater/PatterMatcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace DocxTemplater
Expand Down Expand Up @@ -31,7 +33,16 @@ internal static class PatternMatcher
)
)
\}
(?::(?<formatter>[a-zA-z0-9]+)\((?<arg>[a-zA-Z0-9\,]*)\))?
(?::
(?<formatter>[a-zA-z0-9]+) #formatter
(?:\( #arguments with brackets
(?:
(?:,?
(?: (?<arg>[a-zA-Z0-9\s-\\/_-]+) | (?:'(?<arg>[a-zA-Z0-9\s-\\/_-]*)') )
)*
)
\))?
)?
\}
", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
public static IEnumerable<PatternMatch> FindSyntaxPatterns(string text)
Expand Down Expand Up @@ -67,7 +78,9 @@ public static IEnumerable<PatternMatch> FindSyntaxPatterns(string text)
}
else
{
result.Add(new PatternMatch(match, PatternType.Variable, null, null, match.Groups["varname"].Value, match.Groups["formatter"].Value, match.Groups["arg"].Value.Split(','), match.Index, match.Length));
var argGroup = match.Groups["arg"];
var arguments = argGroup.Success ? argGroup.Captures.Select(x => x.Value).ToArray() : Array.Empty<string>();
result.Add(new PatternMatch(match, PatternType.Variable, null, null, match.Groups["varname"].Value, match.Groups["formatter"].Value, arguments, match.Index, match.Length));
}
}
return result;
Expand Down
17 changes: 17 additions & 0 deletions DocxTemplater/ProcessSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Globalization;

namespace DocxTemplater
{
public class ProcessSettings
{
private CultureInfo m_culture;

public CultureInfo Culture
{
get => m_culture ?? CultureInfo.CurrentUICulture;
set => m_culture = value;
}

public static ProcessSettings Default { get; } = new ProcessSettings() { Culture = null }; // will use current ui culture
}
}

0 comments on commit 165f3c3

Please sign in to comment.