diff --git a/TriasDev.Templify.Gui/Services/ITemplifyService.cs b/TriasDev.Templify.Gui/Services/ITemplifyService.cs index c418e1e..1d6029f 100644 --- a/TriasDev.Templify.Gui/Services/ITemplifyService.cs +++ b/TriasDev.Templify.Gui/Services/ITemplifyService.cs @@ -18,8 +18,12 @@ public interface ITemplifyService /// /// Path to the template file (.docx). /// Optional path to JSON data file for validation. + /// Enable HTML entity replacement (e.g., <br> to line break). /// Validation result with errors and warnings. - Task ValidateTemplateAsync(string templatePath, string? jsonPath = null); + Task ValidateTemplateAsync( + string templatePath, + string? jsonPath = null, + bool enableHtmlEntityReplacement = false); /// /// Processes a template with JSON data and generates output. @@ -27,11 +31,13 @@ public interface ITemplifyService /// Path to the template file (.docx). /// Path to JSON data file. /// Path for the output file. + /// Enable HTML entity replacement (e.g., <br> to line break). /// Optional progress reporter. /// Processing result with statistics and any errors. Task ProcessTemplateAsync( string templatePath, string jsonPath, string outputPath, + bool enableHtmlEntityReplacement = false, IProgress? progress = null); } diff --git a/TriasDev.Templify.Gui/Services/TemplifyService.cs b/TriasDev.Templify.Gui/Services/TemplifyService.cs index 8069ef7..62a2d4b 100644 --- a/TriasDev.Templify.Gui/Services/TemplifyService.cs +++ b/TriasDev.Templify.Gui/Services/TemplifyService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using TriasDev.Templify.Core; using TriasDev.Templify.Gui.Models; +using TriasDev.Templify.Replacements; using TriasDev.Templify.Utilities; namespace TriasDev.Templify.Gui.Services; @@ -20,14 +21,18 @@ public class TemplifyService : ITemplifyService /// /// Validates a template file with optional JSON data. /// - public async Task ValidateTemplateAsync(string templatePath, string? jsonPath = null) + public async Task ValidateTemplateAsync( + string templatePath, + string? jsonPath = null, + bool enableHtmlEntityReplacement = false) { return await Task.Run(() => { PlaceholderReplacementOptions options = new PlaceholderReplacementOptions { MissingVariableBehavior = MissingVariableBehavior.LeaveUnchanged, - Culture = CultureInfo.InvariantCulture + Culture = CultureInfo.InvariantCulture, + TextReplacements = enableHtmlEntityReplacement ? TextReplacements.HtmlEntities : null }; DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); @@ -57,6 +62,7 @@ public async Task ProcessTemplateAsync( string templatePath, string jsonPath, string outputPath, + bool enableHtmlEntityReplacement = false, IProgress? progress = null) { return await Task.Run(() => @@ -76,11 +82,12 @@ public async Task ProcessTemplateAsync( progress?.Report(0.3); - // Validate template first + // Configure options with optional HTML entity replacement PlaceholderReplacementOptions options = new PlaceholderReplacementOptions { MissingVariableBehavior = MissingVariableBehavior.LeaveUnchanged, - Culture = CultureInfo.InvariantCulture + Culture = CultureInfo.InvariantCulture, + TextReplacements = enableHtmlEntityReplacement ? TextReplacements.HtmlEntities : null }; DocumentTemplateProcessor processor = new DocumentTemplateProcessor(options); diff --git a/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs b/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs index 5a5a027..32ef7d6 100644 --- a/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs +++ b/TriasDev.Templify.Gui/ViewModels/MainWindowViewModel.cs @@ -48,6 +48,14 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private ObservableCollection _results = new(); + /// + /// Gets or sets whether HTML entity replacement is enabled. + /// When enabled, HTML entities like <br>, &nbsp;, etc. are converted + /// to their Word equivalents before processing. + /// + [ObservableProperty] + private bool _enableHtmlEntityReplacement; + public MainWindowViewModel( ITemplifyService templifyService, IFileDialogService fileDialogService) @@ -103,7 +111,10 @@ private async Task ValidateTemplateAsync() try { - ValidationResult validation = await _templifyService.ValidateTemplateAsync(TemplatePath, JsonPath); + ValidationResult validation = await _templifyService.ValidateTemplateAsync( + TemplatePath, + JsonPath, + EnableHtmlEntityReplacement); if (validation.IsValid) { @@ -177,6 +188,7 @@ private async Task ProcessTemplateAsync() TemplatePath, JsonPath, OutputPath, + EnableHtmlEntityReplacement, progressReporter); if (result.Success) diff --git a/TriasDev.Templify.Gui/Views/MainWindow.axaml b/TriasDev.Templify.Gui/Views/MainWindow.axaml index d61c6cf..a2a7baa 100644 --- a/TriasDev.Templify.Gui/Views/MainWindow.axaml +++ b/TriasDev.Templify.Gui/Views/MainWindow.axaml @@ -15,7 +15,7 @@ - + - + + + + + + - + public bool WarnOnEmptyLoopCollections { get; init; } = true; + /// + /// Gets or initializes a dictionary of text replacements to apply to variable values before processing. + /// Use this to convert HTML entities, custom placeholders, or other text patterns. + /// Default is null (no replacements). + /// + /// + /// + /// Replacements are applied after value conversion but before newline and markdown processing. + /// This allows HTML line breaks (e.g., <br>) to be converted to \n, which then gets + /// processed into Word line breaks. + /// + /// + /// Use the built-in preset for common HTML entities: + /// + /// + /// var options = new PlaceholderReplacementOptions + /// { + /// TextReplacements = TextReplacements.HtmlEntities + /// }; + /// + /// + /// Or define custom replacements: + /// + /// + /// var options = new PlaceholderReplacementOptions + /// { + /// TextReplacements = new Dictionary<string, string> + /// { + /// ["<br>"] = "\n", + /// ["COMPANY_NAME"] = "Acme Corp" + /// } + /// }; + /// + /// + public IReadOnlyDictionary? TextReplacements { get; init; } + /// /// Creates a new instance of with default settings. /// diff --git a/TriasDev.Templify/Replacements/TextReplacements.cs b/TriasDev.Templify/Replacements/TextReplacements.cs new file mode 100644 index 0000000..74dcdce --- /dev/null +++ b/TriasDev.Templify/Replacements/TextReplacements.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2025 TriasDev GmbH & Co. KG +// Licensed under the MIT License. See LICENSE file in the project root for full license information. + +using System.Collections.ObjectModel; + +namespace TriasDev.Templify.Replacements; + +/// +/// Provides text replacement functionality and built-in replacement presets. +/// +/// +/// +/// ADR: Lookup Table vs HTML Parser +/// +/// +/// Context: Need to transform HTML-like strings in template values to Word-compatible output. +/// +/// +/// Decision: Lookup Table approach using simple string-to-string replacement dictionary. +/// +/// +/// Rationale: +/// +/// Simplicity: No complex HTML parsing required, just string.Replace() operations +/// Flexibility: Users can define custom mappings beyond HTML +/// Predictable: No edge cases from malformed HTML +/// Extensible: Easy to add new replacements without code changes +/// Sufficient: Paired tags like <b>text</b> can use existing markdown support +/// +/// +/// +/// Trade-offs: +/// +/// Cannot support opening/closing tag pairs - users should use markdown instead +/// Each variation needs explicit entry (e.g., <br>, <br/>, <br /> are separate) +/// +/// +/// +public static class TextReplacements +{ + /// + /// Built-in preset for common HTML entities and line break tags. + /// Converts HTML tags and entities to their Word-compatible equivalents. + /// + /// + /// Includes: + /// + /// <br>, <br/>, <br /> → newline (\n) + /// &nbsp; → non-breaking space (U+00A0) + /// &lt; → < + /// &gt; → > + /// &amp; → & + /// &quot; → " + /// &apos; → ' + /// &mdash; → em dash (—) + /// &ndash; → en dash (–) + /// + /// + public static IReadOnlyDictionary HtmlEntities { get; } = new ReadOnlyDictionary( + new Dictionary + { + // Line break variations (lowercase and uppercase) + ["
"] = "\n", + ["
"] = "\n", + ["
"] = "\n", + ["
"] = "\n", + ["
"] = "\n", + ["
"] = "\n", + + // Common HTML entities (lowercase only - per HTML spec, entities are case-sensitive) + [" "] = "\u00A0", // Non-breaking space + ["<"] = "<", + [">"] = ">", + ["&"] = "&", + ["""] = "\"", + ["'"] = "'", + ["—"] = "\u2014", // Em dash (—) + ["–"] = "\u2013", // En dash (–) + }); + + /// + /// Applies text replacements to the input string. + /// + /// The input string to transform. If null, returns null. + /// Dictionary of text replacements to apply. If null or empty, returns input unchanged. + /// The transformed string with all replacements applied, or null if input was null. + /// + /// Replacements are applied in dictionary enumeration order. + /// For predictable behavior with overlapping patterns, consider using an ordered dictionary + /// or applying replacements in a specific sequence. + /// + public static string? Apply(string? input, IReadOnlyDictionary? replacements) + { + if (string.IsNullOrEmpty(input) || replacements == null || replacements.Count == 0) + { + return input; + } + + string result = input; + foreach (KeyValuePair replacement in replacements) + { + result = result.Replace(replacement.Key, replacement.Value); + } + + return result; + } +} diff --git a/TriasDev.Templify/Visitors/PlaceholderVisitor.cs b/TriasDev.Templify/Visitors/PlaceholderVisitor.cs index a4ab121..d075c83 100644 --- a/TriasDev.Templify/Visitors/PlaceholderVisitor.cs +++ b/TriasDev.Templify/Visitors/PlaceholderVisitor.cs @@ -9,6 +9,7 @@ using TriasDev.Templify.Loops; using TriasDev.Templify.Markdown; using TriasDev.Templify.Placeholders; +using TriasDev.Templify.Replacements; using TriasDev.Templify.Utilities; namespace TriasDev.Templify.Visitors; @@ -136,6 +137,11 @@ public void VisitPlaceholder(PlaceholderMatch placeholder, Paragraph paragraph, _options.Culture, placeholder.Format, _options.BooleanFormatterRegistry); + + // Apply text replacements (e.g., HTML entities to Word-compatible text) + // Note: Apply returns null only if input is null, which won't happen here + replacementValue = TextReplacements.Apply(replacementValue, _options.TextReplacements)!; + ReplacePlaceholderInParagraph(paragraph, placeholder, replacementValue); _replacementCount++; } diff --git a/docs/for-template-authors/placeholders.md b/docs/for-template-authors/placeholders.md index 335c867..38d51a0 100644 --- a/docs/for-template-authors/placeholders.md +++ b/docs/for-template-authors/placeholders.md @@ -543,6 +543,87 @@ Second line. All newline formats are supported: `\n` (Unix/Linux/macOS), `\r\n` (Windows), `\r` (legacy Mac). +## HTML Entity Replacement + +If your data comes from web applications or APIs, it may contain HTML tags and entities. Templify can automatically convert these to their Word equivalents when enabled. + +### Enabling HTML Entity Replacement + +In the **Templify GUI**, check the **"Enable HTML Entity Replacement"** checkbox before processing. + +In code, use the `TextReplacements` option: + +```csharp +var options = new PlaceholderReplacementOptions +{ + TextReplacements = TextReplacements.HtmlEntities +}; +``` + +### Supported HTML Tags and Entities + +| HTML | Converted To | Description | +|------|--------------|-------------| +| `
` | Line break | HTML line break | +| `
` | Line break | Self-closing line break | +| `
` | Line break | Self-closing with space | +| ` ` | Non-breaking space | Keeps words together | +| `<` | `<` | Less than | +| `>` | `>` | Greater than | +| `&` | `&` | Ampersand | +| `"` | `"` | Double quote | +| `'` | `'` | Single quote | +| `—` | `—` | Em dash | +| `–` | `–` | En dash | + +> **Note:** The `
` tag also supports uppercase variants (`
`, `
`, `
`). However, HTML entities like ` ` are case-sensitive per the HTML specification and only lowercase versions are supported. + +### Example + +**JSON (with HTML from a web form):** +```json +{ + "Description": "Product features:
- Fast
- Reliable
- Easy to use", + "Price": "Starting at <$100", + "Note": "Contact us at info@company.com — we're here to help!" +} +``` + +**Template:** +``` +{{Description}} + +Price: {{Price}} + +{{Note}} +``` + +**Output (with HTML Entity Replacement enabled):** +``` +Product features: +- Fast +- Reliable +- Easy to use + +Price: Starting at <$100 + +Contact us at info@company.com — we're here to help! +``` + +### When to Use HTML Entity Replacement + +Enable this option when your data: +- Comes from web forms or CMS systems +- Contains HTML line breaks (`
`) +- Includes HTML-encoded special characters (`<`, `>`, `&`) +- Was exported from web applications + +### Limitations + +- Only simple tags and entities are supported (see table above) +- Paired tags like `text` are **not** converted — use markdown (`**text**`) instead +- This is an opt-in feature (disabled by default) to avoid unexpected changes + **Combining with Markdown:** You can use both markdown formatting and line breaks together: