Skip to content

Latest commit

 

History

History
1922 lines (1532 loc) · 58.1 KB

File metadata and controls

1922 lines (1532 loc) · 58.1 KB

Templify - Architecture Refactoring Plan

Status: ✅ Phase 2 - Complete (100% - Visitor Pattern Implemented) Timeline: 3 weeks actual (vs 3 weeks planned) Breaking Changes: Internal only (legacy path removed) Created: 2025-01-08 Last Updated: 2025-11-09

Executive Summary

This document outlines the comprehensive refactoring that was performed for Templify to address architectural limitations and improve extensibility. The critical limitation where conditionals could not be used inside loops has been completely resolved through the visitor pattern implementation.

Decision Context

  • Not in production - Breaking changes are acceptable
  • Need it now - High priority to enable conditionals in loops
  • Step-by-step approach - 5-week phased implementation
  • Public API changes allowed - If they improve the design

Key Objectives

  1. Enable conditionals inside loops - The #1 critical limitation
  2. Eliminate code duplication - DRY principle violations
  3. Introduce proper abstractions - IEvaluationContext, ITemplateProcessor
  4. Make pipeline extensible - Support future features easily

Phased Approach

Phase Duration Goal Status Notes
Phase 1 Week 1 (planned) Enable conditionals in loops ✅ Complete IEvaluationContext abstraction
Phase 2 Weeks 2-3 (planned) Visitor pattern + clean architecture ✅ Complete Legacy path removed entirely
Phase 3 Weeks 4-5 (planned) Full extensibility + pipeline ⏸️ Deferred Not needed for current requirements

What Was Actually Implemented

Phase 2 Completion (2025-11-09):

  • ✅ Full visitor pattern implementation (DocumentWalker, ConditionalVisitor, LoopVisitor, PlaceholderVisitor)
  • ✅ Composite visitor for flexible composition
  • ✅ Table row loop support (loops spanning table rows)
  • ✅ Nested loop support (arbitrary nesting depth)
  • Legacy path completely removed (went beyond plan - no dual-mode support)
  • ✅ All 319 tests passing (100% success rate)
  • ✅ Code duplication eliminated (~300 lines removed)

Differences from Original Plan:

  • Removed ProcessingMode enum and dual-path support (Legacy vs ContextAware)
  • Simplified API - single processing path using visitor pattern
  • Added TableRow handling in DocumentWalker for proper table row loop support
  • Breaking Change: Removed legacy processing entirely (acceptable - not in production)

Table of Contents

  1. Current Architecture Analysis
  2. Identified Issues
  3. Phase 1: Evaluation Context Abstraction
  4. Phase 2: Visitor Pattern Refactoring
  5. Phase 3: Extensible Pipeline
  6. Testing Strategy
  7. Migration Guide
  8. Success Metrics

Current Architecture Analysis

Processing Pipeline

File: DocumentTemplateProcessor.cs (lines 78-90)

Current Order (hard-coded):

1. ConditionalProcessor → Process {{#if}}/{{#else}}/{{/if}} globally
2. LoopDetector + LoopProcessor → Process {{#foreach}}/{{/foreach}}
3. DocumentBodyReplacer → Replace simple placeholders in paragraphs
4. TableReplacer → Replace placeholders in tables

Data Flow:

Template Document (Stream)
    ↓
1. ConditionalProcessor.ProcessConditionals()
   - Evaluates ALL conditionals using ROOT data only
   - Removes false branches globally
   - All {{#if}} markers removed from document
    ↓
2. LoopProcessor.ProcessLoops()
   - Creates LoopContext for each iteration
   - Clones content for each item
   - Processes placeholders with loop-scoped data
   - Detects and processes NESTED loops recursively
    ↓
3. DocumentBodyReplacer.ReplaceInBody()
   - Replaces remaining placeholders using root data
    ↓
4. TableReplacer.ReplaceInTables()
   - Replaces table placeholders
    ↓
Output Document (Stream)

Component Responsibilities

DocumentTemplateProcessor (Orchestrator)

  • Purpose: Entry point, coordinates all processing
  • Responsibilities:
    • Opens/copies document streams
    • Instantiates all processors
    • Executes processing pipeline in fixed order
    • Aggregates results and missing variables

ConditionalProcessor + ConditionalDetector

  • Purpose: Handle {{#if}}/{{#else}}/{{/if}} blocks
  • Responsibilities:
    • Detect conditional blocks (supports nesting)
    • Evaluate expressions using ConditionalEvaluator
    • Remove false branches, keep true branches
    • Support table row conditionals

Strengths:

  • ✅ Handles nested conditionals correctly (deepest-first)
  • ✅ Supports complex expressions (and, or, not, comparisons)
  • ✅ Clean separation: detection → evaluation → removal

Issues:

  • ❌ Only accesses global/root data
  • ❌ Cannot evaluate conditions inside loop iterations
  • ❌ Processed before loops, so loop variables unavailable

LoopProcessor + LoopDetector

  • Purpose: Handle {{#foreach Collection}}/{{/foreach}} blocks
  • Responsibilities:
    • Detect loop blocks
    • Create LoopContext with metadata (@index, @first, @last, @count)
    • Clone content for each iteration
    • Support nested loops with context chaining
    • Process placeholders within loop scope

Strengths:

  • ✅ Excellent nested loop support
  • ✅ Rich loop metadata
  • ✅ Proper context chaining for nested structures

Issues:

  • ❌ Duplicates placeholder processing logic from DocumentBodyReplacer
  • ❌ Duplicates detection logic (TryDetectLoop vs LoopDetector)
  • ❌ Cannot process conditionals inside loop iterations

ValueResolver + LoopContext

  • Purpose: Resolve variable values from data
  • Responsibilities:
    • ValueResolver: Global scope, nested property paths (Customer.Address.City)
    • LoopContext: Loop scope, with parent context chaining

Issues:

  • ❌ Two separate resolution systems (not unified)
  • ❌ ConditionalEvaluator cannot use LoopContext

Code Duplication Examples

1. Placeholder Processing (near-identical code):

  • LoopProcessor.ProcessParagraph() (lines 146-203)
  • DocumentBodyReplacer.ProcessParagraph() (lines 57-126)

2. Element Text Extraction (identical implementations):

  • ConditionalDetector.GetElementText() (lines 189-207)
  • LoopDetector.GetElementText() (lines 150-168)
  • LoopProcessor.GetElementText() (lines 536-554)

3. Detection Logic (duplicate patterns):

  • LoopDetector.DetectLoopsInElements() (lines 59-112)
  • LoopProcessor.TryDetectLoop() (lines 360-402)

Identified Issues

Critical Issue #1: Processing Order Limitation

Problem: Conditionals are processed BEFORE loops, making conditional logic inside loop iterations impossible.

Example that FAILS:

{{#foreach Orders}}
  Order #{{OrderId}}
  {{#if Amount > 1000}}
    🔥 HIGH VALUE: {{Amount}} EUR
  {{#else}}
    Standard: {{Amount}} EUR
  {{/if}}
{{/foreach}}

Why it fails:

  1. Step 1: ConditionalProcessor evaluates Amount > 1000 using ROOT data
  2. At this point, we're NOT inside any loop, so Amount = root-level variable (if exists) or missing
  3. Conditional branch is selected ONCE globally
  4. Step 2: LoopProcessor clones the selected branch for each order
  5. Result: All orders get the same conditional result (not per-order evaluation)

Current workaround: None - feature is not supported

Impact: Major limitation for report generation use cases

Issue #2: Code Duplication (~40% of core logic)

Duplication Impact:

  • Harder to maintain (changes needed in multiple places)
  • Higher bug risk (fix in one place, forget another)
  • Inconsistent behavior potential
  • Larger codebase

Estimated Duplication:

  • Placeholder processing: ~150 lines duplicated
  • Detection logic: ~100 lines duplicated
  • Helper methods: ~50 lines duplicated
  • Total: ~300 lines of duplicate code

Issue #3: Missing Abstractions

No Unified Evaluation Context:

  • Global data: Dictionary<string, object>
  • Loop context: LoopContext (separate class)
  • No shared interface or abstraction
  • ConditionalEvaluator cannot work with LoopContext

No Processor Interface:

  • Each processor has different method signatures
  • Cannot compose or reorder processors dynamically
  • Hard to test in isolation
  • No plugin/extension mechanism

No Template Element Abstraction:

  • Working directly with OpenXmlElement
  • No domain model for template constructs
  • Tight coupling to OpenXML API

Issue #4: Limited Extensibility

Current State: Adding new features requires:

  1. Modifying DocumentTemplateProcessor constructor
  2. Adding new processing step to hardcoded pipeline
  3. Determining execution order vs existing steps
  4. Risk of conflicts with existing processors

Examples of difficult-to-add features:

  • Custom functions: {{#if FormatDate(OrderDate) = "2025-01-01"}}
  • Filter expressions: {{#foreach Orders where Amount > 1000}}
  • Headers/footers: Would need separate code path (implemented - reuses existing visitor pipeline via DocumentWalker.WalkHeadersAndFooters)
  • Partial templates: {{> include "header.docx"}}
  • Custom blocks: {{#custom MyBlock}}...{{/custom}}

Phase 1: Evaluation Context Abstraction

Timeline: Week 1 (5 working days) Goal: Enable conditionals inside loops with minimal changes Breaking Changes: Internal only

Design

New Interface: IEvaluationContext

Purpose: Unify variable resolution across all processors

namespace TriasDev.Templify;

/// <summary>
/// Represents a context for evaluating variables in template expressions.
/// Supports hierarchical contexts (e.g., loop contexts nested in global context).
/// </summary>
public interface IEvaluationContext
{
    /// <summary>
    /// Tries to resolve a variable by name.
    /// </summary>
    /// <param name="variableName">Variable name (supports dot notation like "Customer.Name")</param>
    /// <param name="value">Resolved value if found</param>
    /// <returns>True if variable was found, false otherwise</returns>
    bool TryResolveVariable(string variableName, out object? value);

    /// <summary>
    /// Gets the parent context (for nested contexts), or null for root context.
    /// </summary>
    IEvaluationContext? Parent { get; }

    /// <summary>
    /// Gets the root data dictionary (useful for metadata access).
    /// </summary>
    IReadOnlyDictionary<string, object> RootData { get; }
}

Implementation: GlobalEvaluationContext

Purpose: Root-level evaluation using dictionary data

namespace TriasDev.Templify;

/// <summary>
/// Global evaluation context that resolves variables from root data dictionary.
/// </summary>
public sealed class GlobalEvaluationContext : IEvaluationContext
{
    private readonly Dictionary<string, object> _data;
    private readonly ValueResolver _valueResolver;

    public GlobalEvaluationContext(Dictionary<string, object> data)
    {
        _data = data ?? throw new ArgumentNullException(nameof(data));
        _valueResolver = new ValueResolver();
    }

    public bool TryResolveVariable(string variableName, out object? value)
    {
        return _valueResolver.TryResolveValue(_data, variableName, out value);
    }

    public IEvaluationContext? Parent => null;

    public IReadOnlyDictionary<string, object> RootData => _data;
}

Implementation: LoopEvaluationContext

Purpose: Loop-scoped evaluation with fallback to parent

namespace TriasDev.Templify;

/// <summary>
/// Loop evaluation context that resolves variables from loop iteration context,
/// with fallback to parent context.
/// </summary>
public sealed class LoopEvaluationContext : IEvaluationContext
{
    private readonly LoopContext _loopContext;
    private readonly IEvaluationContext _parent;

    public LoopEvaluationContext(LoopContext loopContext, IEvaluationContext parent)
    {
        _loopContext = loopContext ?? throw new ArgumentNullException(nameof(loopContext));
        _parent = parent ?? throw new ArgumentNullException(nameof(parent));
    }

    public bool TryResolveVariable(string variableName, out object? value)
    {
        // Try loop context first (handles @index, @first, @last, @count, current item properties)
        if (_loopContext.TryResolveVariable(variableName, out value))
            return true;

        // Fall back to parent context (root data or parent loop)
        return _parent.TryResolveVariable(variableName, out value);
    }

    public IEvaluationContext? Parent => _parent;

    public IReadOnlyDictionary<string, object> RootData => _parent.RootData;
}

Changes Required

1. Update ConditionalEvaluator

File: ConditionalEvaluator.cs

Before:

public bool Evaluate(string expression, Dictionary<string, object> data)
{
    // Uses ValueResolver directly with data dictionary
    // ...
}

After:

public bool Evaluate(string expression, IEvaluationContext context)
{
    // Uses context.TryResolveVariable() instead
    // ...
}

Impact: Internal change, public API unchanged

2. Update ConditionalProcessor

File: ConditionalProcessor.cs

Before:

public void ProcessConditionals(WordprocessingDocument document, Dictionary<string, object> data)
{
    // Creates global context implicitly
    // ...
}

After:

public void ProcessConditionals(WordprocessingDocument document, IEvaluationContext context)
{
    // Accepts context parameter
    // Can work in both global and loop scopes
    // ...
}

// Overload for backward compatibility (can be removed later)
public void ProcessConditionals(WordprocessingDocument document, Dictionary<string, object> data)
{
    var context = new GlobalEvaluationContext(data);
    ProcessConditionals(document, context);
}

3. Update DocumentTemplateProcessor

File: DocumentTemplateProcessor.cs

Before:

// Step 0: Process conditionals (must be done before loops)
_conditionalProcessor.ProcessConditionals(document, data);

After:

// Step 0: Process conditionals at global scope
var globalContext = new GlobalEvaluationContext(data);
_conditionalProcessor.ProcessConditionals(document, globalContext);

4. Add Conditional Processing to LoopProcessor

File: LoopProcessor.cs

Current: Lines 319-355 detect and process nested loops Add: Lines ~356-390 detect and process nested conditionals

New Method:

private void ProcessConditionals(
    List<OpenXmlElement> clonedContent,
    LoopContext context,
    List<string> missingVariables)
{
    // Create loop evaluation context
    var loopEvalContext = new LoopEvaluationContext(context, _globalContext);

    // Detect conditionals in cloned content
    var conditionals = ConditionalDetector.DetectConditionals(clonedContent);

    if (conditionals.Count == 0)
        return;

    // Process conditionals with loop-scoped context
    foreach (var conditional in conditionals.OrderByDescending(c => c.NestingLevel))
    {
        bool result = _conditionalEvaluator.Evaluate(
            conditional.ConditionExpression,
            loopEvalContext
        );

        // Remove false branch, keep true branch
        // (same logic as ConditionalProcessor)
        // ...
    }
}

Integration Point: Call after nested loop processing (line ~355):

// Process nested loops in cloned content
ProcessNestedLoops(clonedContent, context, data, missingVariables);

// NEW: Process conditionals in cloned content with loop context
ProcessConditionals(clonedContent, context, missingVariables);

// Process placeholders in cloned content
// ...

Implementation Steps

Day 1: Create Abstractions

  • Create IEvaluationContext.cs
  • Create GlobalEvaluationContext.cs
  • Create LoopEvaluationContext.cs
  • Add unit tests for context implementations

Day 2: Update ConditionalEvaluator

  • Update ConditionalEvaluator.Evaluate() to accept IEvaluationContext
  • Update all evaluator tests to use contexts
  • Verify no regressions in conditional logic

Day 3: Update ConditionalProcessor

  • Update ProcessConditionals() to accept context
  • Update DocumentTemplateProcessor to create GlobalEvaluationContext
  • Verify top-level conditionals still work

Day 4: Add Conditional Processing to LoopProcessor

  • Add ProcessConditionals() method to LoopProcessor
  • Integrate after nested loop processing
  • Store global context reference in LoopProcessor

Day 5: Integration Testing

  • Create integration test: conditional inside loop
  • Create integration test: nested conditionals inside loops
  • Create integration test: conditionals with loop metadata (@index, @first, etc.)
  • Fixed critical bug: LoopProcessor now modifies clonedElements directly (Remove() was failing silently)

Testing Strategy

New Integration Tests (ListsInLoopsTests.cs already has commented-out examples)

Test 1: Simple Conditional in Loop

[Fact]
public void ProcessTemplate_ConditionalInsideLoop_EvaluatesPerIteration()
{
    // Template:
    // {{#foreach Orders}}
    //   {{#if Amount > 1000}}High Value{{#else}}Standard{{/if}}
    // {{/foreach}}

    // Data:
    // Orders = [{ Amount: 500 }, { Amount: 1500 }, { Amount: 800 }]

    // Expected Output:
    // Standard
    // High Value
    // Standard
}

Test 2: Conditional with Loop Metadata

[Fact]
public void ProcessTemplate_ConditionalWithLoopMetadata_Works()
{
    // Template:
    // {{#foreach Items}}
    //   {{#if @first}}First: {{.}}{{#else}}Item: {{.}}{{/if}}
    // {{/foreach}}

    // Expected Output:
    // First: Apple
    // Item: Banana
    // Item: Cherry
}

Test 3: Nested Loops with Conditionals

[Fact]
public void ProcessTemplate_ConditionalInNestedLoop_Works()
{
    // Template:
    // {{#foreach Categories}}
    //   {{Name}}:
    //   {{#foreach Products}}
    //     {{#if Price < 50}}• {{Name}} (Budget){{#else}}• {{Name}}{{/if}}
    //   {{/foreach}}
    // {{/foreach}}
}

Success Criteria

  • ✅ All existing tests pass (no regression)
  • ✅ 3+ new integration tests for conditionals in loops pass
  • ✅ Performance impact < 5% (benchmark before/after)
  • ✅ Code coverage maintained or improved
  • ✅ Documentation updated (README.md limitation removed)

Known Limitations After Phase 1

  • ⚠️ Still has code duplication (addressed in Phase 2)
  • ⚠️ Conditional processing logic in two places (ConditionalProcessor and LoopProcessor)
  • ⚠️ No extensibility improvements (addressed in Phase 3)

Phase 2: Visitor Pattern Refactoring

Timeline: Weeks 2-3 (10 working days planned) - ✅ COMPLETED Goal: Clean architecture with visitor pattern - ✅ ACHIEVED Breaking Changes: Internal only (legacy path removed entirely)

Design

Visitor Pattern Overview

Problem: Currently, we have:

  • ConditionalProcessor: Processes conditionals globally
  • LoopProcessor: Processes loops, also processes conditionals locally
  • Duplication and unclear separation of concerns

Solution: Visitor pattern

  • DocumentWalker: Traverses document tree
  • ITemplateElementVisitor: Processes template constructs
  • Visitors: ConditionalVisitor, LoopVisitor, PlaceholderVisitor
  • Context-aware: Each visitor receives IEvaluationContext

New Interface: ITemplateElementVisitor

namespace TriasDev.Templify;

/// <summary>
/// Visitor interface for processing template elements.
/// </summary>
public interface ITemplateElementVisitor
{
    /// <summary>
    /// Visits a conditional block ({{#if}}/{{#else}}/{{/if}}).
    /// </summary>
    void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context);

    /// <summary>
    /// Visits a loop block ({{#foreach}}/{{/foreach}}).
    /// </summary>
    void VisitLoopBlock(LoopBlock block, IEvaluationContext context);

    /// <summary>
    /// Visits a placeholder ({{VariableName}}).
    /// </summary>
    void VisitPlaceholder(PlaceholderMatch placeholder, IEvaluationContext context);

    /// <summary>
    /// Visits a regular paragraph (no template constructs).
    /// </summary>
    void VisitParagraph(Paragraph paragraph, IEvaluationContext context);
}

New Class: DocumentWalker

Purpose: Traverses document and dispatches to visitors

namespace TriasDev.Templify;

/// <summary>
/// Walks through document elements and dispatches to appropriate visitor methods.
/// </summary>
public sealed class DocumentWalker
{
    public void Walk(
        IEnumerable<OpenXmlElement> elements,
        ITemplateElementVisitor visitor,
        IEvaluationContext context)
    {
        foreach (var element in elements.ToList()) // ToList to avoid modification during iteration
        {
            // Detect what kind of template construct this is
            if (TryDetectConditional(element, out var conditional))
            {
                visitor.VisitConditionalBlock(conditional, context);
                continue;
            }

            if (TryDetectLoop(element, out var loop))
            {
                visitor.VisitLoopBlock(loop, context);
                continue;
            }

            if (TryDetectPlaceholder(element, out var placeholder))
            {
                visitor.VisitPlaceholder(placeholder, context);
                continue;
            }

            if (element is Paragraph paragraph)
            {
                visitor.VisitParagraph(paragraph, context);
            }

            // Recursively walk child elements
            if (element.HasChildren)
            {
                Walk(element.Elements(), visitor, context);
            }
        }
    }

    private bool TryDetectConditional(OpenXmlElement element, out ConditionalBlock block)
    {
        // Use ConditionalDetector logic
        // ...
    }

    private bool TryDetectLoop(OpenXmlElement element, out LoopBlock block)
    {
        // Use LoopDetector logic
        // ...
    }

    private bool TryDetectPlaceholder(OpenXmlElement element, out PlaceholderMatch match)
    {
        // Use placeholder regex
        // ...
    }
}

New Class: ConditionalVisitor

Purpose: Processes conditional blocks with context awareness

namespace TriasDev.Templify;

public sealed class ConditionalVisitor : ITemplateElementVisitor
{
    private readonly ConditionalEvaluator _evaluator;

    public void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context)
    {
        // Evaluate condition with current context (could be global OR loop-scoped!)
        bool result = _evaluator.Evaluate(block.ConditionExpression, context);

        if (result)
        {
            // Remove ELSE branch, keep IF branch
            block.RemoveElseBranch();
        }
        else
        {
            // Remove IF branch, keep ELSE branch
            block.RemoveIfBranch();
        }

        // Remove conditional markers
        block.RemoveMarkers();
    }

    public void VisitLoopBlock(LoopBlock block, IEvaluationContext context)
    {
        // Not handled by this visitor
    }

    public void VisitPlaceholder(PlaceholderMatch placeholder, IEvaluationContext context)
    {
        // Not handled by this visitor
    }

    public void VisitParagraph(Paragraph paragraph, IEvaluationContext context)
    {
        // Not handled by this visitor
    }
}

New Class: LoopVisitor

Purpose: Processes loop blocks and creates nested contexts

namespace TriasDev.Templify;

public sealed class LoopVisitor : ITemplateElementVisitor
{
    private readonly DocumentWalker _walker;
    private readonly ITemplateElementVisitor _nestedVisitor;

    public void VisitLoopBlock(LoopBlock block, IEvaluationContext context)
    {
        // Get collection to loop over
        if (!context.TryResolveVariable(block.CollectionName, out var collectionObj))
            return; // Collection not found

        IEnumerable<object?> collection = ConvertToEnumerable(collectionObj);

        // Clone content for each iteration
        List<OpenXmlElement> allClonedContent = new();

        int index = 0;
        foreach (var item in collection)
        {
            // Create loop context for this iteration
            var loopContext = new LoopContext(item, index, collection, context.RootData);
            var loopEvalContext = new LoopEvaluationContext(loopContext, context);

            // Clone content
            var clonedContent = CloneElements(block.ContentElements);

            // Walk cloned content with loop context
            // This will process nested loops, conditionals, and placeholders
            _walker.Walk(clonedContent, _nestedVisitor, loopEvalContext);

            allClonedContent.AddRange(clonedContent);
            index++;
        }

        // Replace loop block with all cloned content
        block.ReplaceWith(allClonedContent);
    }

    // Other Visit methods...
}

New Class: CompositeVisitor

Purpose: Combines multiple visitors

namespace TriasDev.Templify;

/// <summary>
/// Composite visitor that delegates to multiple visitors.
/// </summary>
public sealed class CompositeVisitor : ITemplateElementVisitor
{
    private readonly List<ITemplateElementVisitor> _visitors;

    public CompositeVisitor(params ITemplateElementVisitor[] visitors)
    {
        _visitors = new List<ITemplateElementVisitor>(visitors);
    }

    public void VisitConditionalBlock(ConditionalBlock block, IEvaluationContext context)
    {
        foreach (var visitor in _visitors)
            visitor.VisitConditionalBlock(block, context);
    }

    // Similar for other Visit methods...
}

New Processing Mode

Add to PlaceholderReplacementOptions

public enum ProcessingMode
{
    /// <summary>
    /// Legacy mode: Conditionals processed globally before loops.
    /// Maintains backward compatibility but conditionals inside loops don't work.
    /// </summary>
    Legacy = 0,

    /// <summary>
    /// Context-aware mode: Uses visitor pattern with context-aware processing.
    /// Conditionals inside loops work correctly. Recommended for new templates.
    /// </summary>
    ContextAware = 1
}

public sealed class PlaceholderReplacementOptions
{
    // Existing properties...

    /// <summary>
    /// Gets or sets the processing mode.
    /// Default is Legacy for backward compatibility.
    /// </summary>
    public ProcessingMode ProcessingMode { get; init; } = ProcessingMode.Legacy;
}

Changes Required

1. Create Visitor Infrastructure

  • Create ITemplateElementVisitor.cs
  • Create DocumentWalker.cs
  • Create ConditionalVisitor.cs
  • Create LoopVisitor.cs
  • Create PlaceholderVisitor.cs
  • Create CompositeVisitor.cs

2. Update DocumentTemplateProcessor

public ProcessingResult ProcessTemplate(
    Stream templateStream,
    Stream outputStream,
    Dictionary<string, object> data)
{
    // ...

    if (_options.ProcessingMode == ProcessingMode.ContextAware)
    {
        // New code path: Use visitor pattern
        var globalContext = new GlobalEvaluationContext(data);
        var walker = new DocumentWalker();
        var visitor = new CompositeVisitor(
            new ConditionalVisitor(),
            new LoopVisitor(walker),
            new PlaceholderVisitor()
        );

        walker.Walk(document.MainDocumentPart.Document.Body.Elements(), visitor, globalContext);
    }
    else
    {
        // Legacy code path: Keep existing order
        // (Phase 1 code)
    }

    // ...
}

3. Extract Shared Utilities

Create TemplateElementHelper.cs:

public static class TemplateElementHelper
{
    public static string GetElementText(OpenXmlElement element)
    {
        // Move duplicate implementations here
    }

    public static OpenXmlElement CloneElement(OpenXmlElement element)
    {
        // Shared cloning logic
    }

    // Other shared helpers...
}

Implementation Steps (Original Plan)

Week 2: Visitor Infrastructure

  • Day 1-2: Create visitor interfaces and DocumentWalker
  • Day 3-4: Implement ConditionalVisitor and LoopVisitor
  • Day 5: Implement PlaceholderVisitor and CompositeVisitor

Week 3: Integration and Testing

  • Day 1-2: Integrate ContextAware mode into DocumentTemplateProcessor
  • Day 3: Extract shared utilities, remove duplication
  • Day 4: Comprehensive testing (both Legacy and ContextAware modes)
  • Day 5: Performance testing, documentation updates

Actual Implementation (Completed 2025-11-09)

Phase 2 Week 3 Days 6-7: Visitor Pattern Integration

Status: ✅ Complete - 319/319 tests passing

Commits:

  1. feat: integrate visitor pattern with legacy path coexistence - Implemented dual-path processing
  2. refactor: remove legacy processing path from DocumentTemplateProcessor - Removed ~180 lines of duplicate code
  3. fix: complete visitor pattern implementation with table row loops and nested loops - Fixed final 3 failing tests

What Was Implemented:

  1. Visitor Infrastructure (Visitors/ folder):

    • ITemplateElementVisitor.cs - Visitor interface with methods for each template element type
    • DocumentWalker.cs - Traverses document tree and dispatches to visitors
    • ConditionalVisitor.cs - Processes {{#if}}/{{#else}}/{{/if}} blocks
    • LoopVisitor.cs - Processes {{#foreach}}/{{/foreach}} blocks with recursive nesting support
    • PlaceholderVisitor.cs - Replaces {{Variable}} placeholders with values
    • CompositeVisitor.cs - Combines multiple visitors for unified processing
  2. DocumentTemplateProcessor Changes:

    • Removed legacy processing path entirely (ConditionalProcessor, DocumentBodyReplacer, TableReplacer calls)
    • Simplified to single visitor-based processing path
    • Composite visitor composition for proper nested loop handling
    • No ProcessingMode enum needed - single path for all scenarios
  3. Table Row Loop Support:

    • Made LoopDetector.DetectTableRowLoops() internal for use by DocumentWalker
    • Modified DocumentWalker.WalkTable() to detect table row loops at table level
    • Added TableRow handling in DocumentWalker.WalkElements() for processing cloned table row content
  4. Nested Loop Support:

    • Fixed visitor composition in DocumentTemplateProcessor to support arbitrary nesting
    • LoopVisitor receives a composite that includes another LoopVisitor for recursive processing
    • Inner loops are detected and processed within cloned content of outer loops

Key Architectural Decisions:

  • No Dual-Mode: Instead of having Legacy + ContextAware modes, we removed the legacy path entirely
  • Single Composite: All visitors (conditional, loop, placeholder) are in a single composite
  • Recursive Composition: LoopVisitor uses the same composite for processing cloned content, enabling nested loops
  • Table-First Detection: Table row loops are detected at table level before cell-by-cell traversal

Test Results:

  • ✅ All 319 tests passing (100% success rate)
  • ✅ Simple placeholders work
  • ✅ Conditionals (including nested) work
  • ✅ Loops (including nested) work
  • ✅ Table row loops work
  • ✅ Conditionals inside loops work
  • ✅ Complex combinations work

Code Metrics:

  • Lines Removed: ~300 lines (legacy processing, duplication, unused features)
  • Lines Added: ~400 lines (visitor pattern infrastructure)
  • Net Change: +100 lines with significantly better architecture
  • Code Duplication: Eliminated (was ~40% of core logic)
  • Test Coverage: Maintained at 100% passing

Testing Strategy

Test Both Modes

Each integration test should have two variants:

[Theory]
[InlineData(ProcessingMode.Legacy)]
[InlineData(ProcessingMode.ContextAware)]
public void ProcessTemplate_ConditionalInLoop_Works(ProcessingMode mode)
{
    var options = new PlaceholderReplacementOptions
    {
        ProcessingMode = mode
    };

    // Test conditionals in loops
    // Should pass for ContextAware, skip/fail for Legacy
}

Verify No Regressions

  • All existing tests must pass in Legacy mode
  • Performance within 5% of Phase 1
  • ContextAware mode enables new scenarios

Success Criteria

  • ✅ All existing tests pass in Legacy mode
  • ✅ Conditionals in loops work in ContextAware mode
  • ✅ Code duplication eliminated (~300 lines removed)
  • ✅ Performance impact < 5% in ContextAware mode
  • ✅ Clean architecture with proper separation of concerns

Phase 3: Extensible Pipeline

Timeline: Weeks 4-5 (10 working days) Goal: Full extensibility with plugin support Breaking Changes: Public API enhancements (additive)

Design

New Interface: ITemplateProcessor

Purpose: Standard interface for all processors

namespace TriasDev.Templify;

/// <summary>
/// Interface for template processors that can be composed into a pipeline.
/// </summary>
public interface ITemplateProcessor
{
    /// <summary>
    /// Gets the processor name (for diagnostics and logging).
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Processes a portion of the document template.
    /// </summary>
    /// <param name="document">The Word document to process</param>
    /// <param name="context">The evaluation context for variable resolution</param>
    /// <param name="options">Processing options</param>
    /// <returns>Result containing success status, replacements, and missing variables</returns>
    ProcessorResult Process(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options);
}

/// <summary>
/// Result of a processor execution.
/// </summary>
public sealed class ProcessorResult
{
    public bool Success { get; init; }
    public int ReplacementCount { get; init; }
    public IReadOnlyList<string> MissingVariables { get; init; } = Array.Empty<string>();
    public string? ErrorMessage { get; init; }

    /// <summary>
    /// If false, stops the pipeline (useful for validation processors).
    /// </summary>
    public bool ContinueProcessing { get; init; } = true;

    public static ProcessorResult Ok(int replacements = 0, IReadOnlyList<string>? missing = null)
        => new() { Success = true, ReplacementCount = replacements, MissingVariables = missing ?? Array.Empty<string>() };

    public static ProcessorResult Failure(string error)
        => new() { Success = false, ErrorMessage = error, ContinueProcessing = false };
}

New Class: TemplateProcessorPipeline

Purpose: Composable processor pipeline

namespace TriasDev.Templify;

/// <summary>
/// Pipeline for composing multiple template processors.
/// </summary>
public sealed class TemplateProcessorPipeline
{
    private readonly List<ITemplateProcessor> _processors = new();

    /// <summary>
    /// Adds a processor to the pipeline.
    /// Processors are executed in the order they are added.
    /// </summary>
    public TemplateProcessorPipeline AddProcessor(ITemplateProcessor processor)
    {
        _processors.Add(processor ?? throw new ArgumentNullException(nameof(processor)));
        return this;
    }

    /// <summary>
    /// Removes all processors of a specific type.
    /// </summary>
    public TemplateProcessorPipeline RemoveProcessors<T>() where T : ITemplateProcessor
    {
        _processors.RemoveAll(p => p is T);
        return this;
    }

    /// <summary>
    /// Inserts a processor before another processor type.
    /// </summary>
    public TemplateProcessorPipeline InsertBefore<TBefore>(ITemplateProcessor processor) where TBefore : ITemplateProcessor
    {
        int index = _processors.FindIndex(p => p is TBefore);
        if (index >= 0)
            _processors.Insert(index, processor);
        else
            _processors.Add(processor);
        return this;
    }

    /// <summary>
    /// Processes the document through all processors in the pipeline.
    /// </summary>
    public ProcessorResult ProcessAll(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options)
    {
        int totalReplacements = 0;
        HashSet<string> allMissingVariables = new();

        foreach (var processor in _processors)
        {
            var result = processor.Process(document, context, options);

            if (!result.Success)
                return result; // Propagate failure

            totalReplacements += result.ReplacementCount;

            foreach (var missing in result.MissingVariables)
                allMissingVariables.Add(missing);

            if (!result.ContinueProcessing)
                break; // Stop pipeline
        }

        return ProcessorResult.Ok(totalReplacements, allMissingVariables.ToList());
    }
}

Processor Implementations

ContextAwareProcessor (wraps visitor pattern from Phase 2):

public sealed class ContextAwareProcessor : ITemplateProcessor
{
    public string Name => "ContextAware";

    public ProcessorResult Process(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options)
    {
        var walker = new DocumentWalker();
        var visitor = new CompositeVisitor(
            new ConditionalVisitor(),
            new LoopVisitor(walker),
            new PlaceholderVisitor()
        );

        walker.Walk(document.MainDocumentPart.Document.Body.Elements(), visitor, context);

        // Return result with replacement count
        return ProcessorResult.Ok(visitor.ReplacementCount, visitor.MissingVariables);
    }
}

ValidationProcessor (example custom processor):

public sealed class ValidationProcessor : ITemplateProcessor
{
    public string Name => "Validation";

    public ProcessorResult Process(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options)
    {
        // Validate template structure
        // Check for malformed placeholders
        // Check for unclosed blocks

        if (hasErrors)
            return ProcessorResult.Failure("Template validation failed: " + errors);

        return ProcessorResult.Ok();
    }
}

Fluent API Enhancement

New Public API (additive, non-breaking):

public sealed class DocumentTemplateProcessor
{
    private TemplateProcessorPipeline? _customPipeline;
    private PlaceholderReplacementOptions _options;

    // Existing constructor
    public DocumentTemplateProcessor(PlaceholderReplacementOptions? options = null)
    {
        _options = options ?? new PlaceholderReplacementOptions();
    }

    // NEW: Fluent configuration
    public DocumentTemplateProcessor WithOptions(PlaceholderReplacementOptions options)
    {
        _options = options;
        return this;
    }

    public DocumentTemplateProcessor UseProcessingMode(ProcessingMode mode)
    {
        _options = _options with { ProcessingMode = mode };
        return this;
    }

    public DocumentTemplateProcessor UseMissingVariableBehavior(MissingVariableBehavior behavior)
    {
        _options = _options with { MissingVariableBehavior = behavior };
        return this;
    }

    public DocumentTemplateProcessor UseCulture(CultureInfo culture)
    {
        _options = _options with { Culture = culture };
        return this;
    }

    // NEW: Custom pipeline
    public DocumentTemplateProcessor UseCustomPipeline(TemplateProcessorPipeline pipeline)
    {
        _customPipeline = pipeline;
        return this;
    }

    public DocumentTemplateProcessor AddProcessor(ITemplateProcessor processor)
    {
        _customPipeline ??= CreateDefaultPipeline();
        _customPipeline.AddProcessor(processor);
        return this;
    }

    // Existing ProcessTemplate method remains unchanged for backward compatibility
}

Example Usage:

// Simple usage (backward compatible)
var processor = new DocumentTemplateProcessor();
processor.ProcessTemplate(template, output, data);

// Fluent configuration
var processor = new DocumentTemplateProcessor()
    .UseProcessingMode(ProcessingMode.ContextAware)
    .UseMissingVariableBehavior(MissingVariableBehavior.ThrowException)
    .UseCulture(CultureInfo.InvariantCulture);

// Custom pipeline
var processor = new DocumentTemplateProcessor()
    .AddProcessor(new ValidationProcessor())
    .AddProcessor(new ContextAwareProcessor())
    .AddProcessor(new CustomFunctionProcessor());

// Advanced: Complete custom pipeline
var pipeline = new TemplateProcessorPipeline()
    .AddProcessor(new ValidationProcessor())
    .AddProcessor(new ConditionalProcessor())
    .AddProcessor(new LoopProcessor())
    .AddProcessor(new PlaceholderProcessor())
    .AddProcessor(new AuditLogProcessor());

var processor = new DocumentTemplateProcessor()
    .UseCustomPipeline(pipeline);

Extension Points

Custom Processor Examples

FunctionProcessor (enables custom functions in expressions):

public sealed class FunctionProcessor : ITemplateProcessor
{
    private readonly Dictionary<string, Func<object[], object>> _functions = new();

    public FunctionProcessor RegisterFunction(string name, Func<object[], object> func)
    {
        _functions[name] = func;
        return this;
    }

    public ProcessorResult Process(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options)
    {
        // Find and replace function calls like {{FormatDate(OrderDate, "yyyy-MM-dd")}}
        // ...
    }
}

// Usage:
processor.AddProcessor(
    new FunctionProcessor()
        .RegisterFunction("FormatDate", args => ((DateTime)args[0]).ToString((string)args[1]))
        .RegisterFunction("Upper", args => args[0]?.ToString()?.ToUpper())
);

HeaderFooterProcessor (process headers/footers):

public sealed class HeaderFooterProcessor : ITemplateProcessor
{
    public string Name => "HeaderFooter";

    public ProcessorResult Process(
        WordprocessingDocument document,
        IEvaluationContext context,
        PlaceholderReplacementOptions options)
    {
        // Process placeholders in headers
        foreach (var header in document.MainDocumentPart.HeaderParts)
        {
            // Process header content
        }

        // Process placeholders in footers
        foreach (var footer in document.MainDocumentPart.FooterParts)
        {
            // Process footer content
        }

        return ProcessorResult.Ok();
    }
}

Implementation Steps

Week 4: Pipeline Infrastructure

  • Day 1-2: Create ITemplateProcessor and TemplateProcessorPipeline
  • Day 3-4: Wrap Phase 2 visitors in ContextAwareProcessor
  • Day 5: Implement fluent API on DocumentTemplateProcessor

Week 5: Extension Points and Polish

  • Day 1-2: Create example custom processors (Validation, Function, HeaderFooter)
  • Day 3: Comprehensive testing of custom pipeline scenarios
  • Day 4: Performance optimization and benchmarking
  • Day 5: Final documentation, examples, and migration guide

Testing Strategy

Test Custom Processors

[Fact]
public void ProcessTemplate_CustomProcessor_IsExecuted()
{
    var customProcessor = new Mock<ITemplateProcessor>();
    customProcessor.Setup(p => p.Process(It.IsAny<WordprocessingDocument>(),
                                          It.IsAny<IEvaluationContext>(),
                                          It.IsAny<PlaceholderReplacementOptions>()))
                   .Returns(ProcessorResult.Ok());

    var processor = new DocumentTemplateProcessor()
        .AddProcessor(customProcessor.Object);

    processor.ProcessTemplate(template, output, data);

    customProcessor.Verify(p => p.Process(It.IsAny<WordprocessingDocument>(),
                                          It.IsAny<IEvaluationContext>(),
                                          It.IsAny<PlaceholderReplacementOptions>()),
                          Times.Once);
}

Test Pipeline Order

[Fact]
public void Pipeline_ExecutesProcessorsInOrder()
{
    var executionOrder = new List<string>();

    var processor1 = CreateMockProcessor("First", executionOrder);
    var processor2 = CreateMockProcessor("Second", executionOrder);
    var processor3 = CreateMockProcessor("Third", executionOrder);

    var pipeline = new TemplateProcessorPipeline()
        .AddProcessor(processor1)
        .AddProcessor(processor2)
        .AddProcessor(processor3);

    pipeline.ProcessAll(document, context, options);

    Assert.Equal(new[] { "First", "Second", "Third" }, executionOrder);
}

Success Criteria

  • ✅ Backward compatible public API (existing code works unchanged)
  • ✅ Fluent API enables easy configuration
  • ✅ Custom processors can be added
  • ✅ Pipeline order is configurable
  • ✅ Example processors demonstrate extensibility
  • ✅ All tests pass (Legacy and ContextAware modes)
  • ✅ Performance within 10% of Phase 1 baseline

Testing Strategy

Test Pyramid

                    /\
                   /  \
                  / E2E \           5% - Full integration tests
                 /______\
                /        \
               /  Integ.  \         25% - Feature integration tests
              /____________\
             /              \
            /  Unit Tests    \      70% - Unit tests for components
           /__________________\

Phase 1 Testing

Unit Tests (~20 new tests):

  • IEvaluationContext implementations
    • GlobalEvaluationContext.TryResolveVariable
    • LoopEvaluationContext with parent chaining
    • Edge cases (null values, missing variables)
  • ConditionalEvaluator with IEvaluationContext
    • All operators still work
    • Context resolution correct

Integration Tests (~5 new tests):

  • Conditional inside simple loop
  • Conditional with loop metadata (@index, @first, @last)
  • Nested loops with conditionals
  • Conditional in loop with complex expressions
  • Bullet list with conditional (from ListsInLoopsTests)

Regression Tests:

  • All 187 existing tests must pass
  • No behavioral changes to top-level conditionals
  • No behavioral changes to loops without conditionals

Phase 2 Testing

Unit Tests (~30 new tests):

  • DocumentWalker element detection
  • ConditionalVisitor logic
  • LoopVisitor cloning and context creation
  • PlaceholderVisitor replacement
  • CompositeVisitor delegation

Integration Tests (~10 new tests):

  • ContextAware mode vs Legacy mode comparison
  • Complex nested structures in ContextAware mode
  • Performance comparison tests
  • Mixed conditionals/loops/tables in ContextAware mode

Parameterized Tests:

[Theory]
[InlineData(ProcessingMode.Legacy)]
[InlineData(ProcessingMode.ContextAware)]
public void AllFeatures_WorkInBothModes(ProcessingMode mode)
{
    // Run same test in both modes
    // ContextAware should pass, Legacy may skip/fail for conditionals in loops
}

Phase 3 Testing

Unit Tests (~15 new tests):

  • ITemplateProcessor interface
  • TemplateProcessorPipeline composition
  • Processor execution order
  • ProcessorResult aggregation

Integration Tests (~8 new tests):

  • Custom processor execution
  • Pipeline ordering
  • Fluent API configuration
  • Example processors (Validation, Function, HeaderFooter)

End-to-End Tests (~3 new tests):

  • Complete real-world scenarios with custom pipeline
  • Performance stress tests
  • Complex document with all features

Test Coverage Goals

Phase Line Coverage Branch Coverage Mutation Score
Phase 1 > 85% > 80% > 75%
Phase 2 > 88% > 83% > 78%
Phase 3 > 90% > 85% > 80%

Performance Benchmarks

Baseline (before refactoring):

Document Size: 50 pages, 200 paragraphs, 10 tables, 500 placeholders
Processing Time: ~150ms
Memory: ~20MB

Acceptable Impact:

  • Phase 1: < 5% slowdown (< 158ms)
  • Phase 2: < 5% additional (< 165ms)
  • Phase 3: < 10% from baseline (< 165ms)

Benchmark Suite:

[Benchmark]
public void ProcessSimpleDocument() { /* 10 pages, 50 placeholders */ }

[Benchmark]
public void ProcessComplexDocument() { /* 50 pages, 500 placeholders, nested structures */ }

[Benchmark]
public void ProcessWithLoops() { /* 10 loops, 20 iterations each */ }

[Benchmark]
public void ProcessWithConditionals() { /* 50 conditionals */ }

[Benchmark]
public void ProcessMixedFeatures() { /* All features combined */ }

Migration Guide

From Current Version to Phase 1

No migration needed - All changes are internal and backward compatible.

Optional: Start preparing test data for conditionals in loops (will work after Phase 1).

From Phase 1 to Phase 2

Option 1: Stay on Legacy Mode (no changes required)

// Existing code works unchanged
var processor = new DocumentTemplateProcessor();
processor.ProcessTemplate(template, output, data);

// Behind the scenes: Uses ProcessingMode.Legacy (default)

Option 2: Opt-in to ContextAware Mode (recommended for new templates)

var options = new PlaceholderReplacementOptions
{
    ProcessingMode = ProcessingMode.ContextAware // Enable new behavior
};

var processor = new DocumentTemplateProcessor(options);
processor.ProcessTemplate(template, output, data);

Breaking Changes: None (new option is additive)

Templates that benefit from ContextAware mode:

  • ✅ Templates with conditionals inside loops
  • ✅ Templates with complex nested structures
  • ✅ Templates requiring loop variable access in conditionals

Templates that should stay on Legacy mode:

  • ⚠️ Templates relying on conditionals evaluating globally before loops
  • ⚠️ Templates with performance-critical requirements (until benchmarks confirm)

From Phase 2 to Phase 3

Option 1: Continue using existing API (no changes)

var processor = new DocumentTemplateProcessor(options);
processor.ProcessTemplate(template, output, data);

Option 2: Use fluent API (recommended for better readability)

var processor = new DocumentTemplateProcessor()
    .UseProcessingMode(ProcessingMode.ContextAware)
    .UseMissingVariableBehavior(MissingVariableBehavior.ThrowException)
    .UseCulture(CultureInfo.InvariantCulture);

processor.ProcessTemplate(template, output, data);

Option 3: Add custom processors

var processor = new DocumentTemplateProcessor()
    .UseProcessingMode(ProcessingMode.ContextAware)
    .AddProcessor(new ValidationProcessor())
    .AddProcessor(new HeaderFooterProcessor());

processor.ProcessTemplate(template, output, data);

Option 4: Use completely custom pipeline (advanced)

var pipeline = new TemplateProcessorPipeline()
    .AddProcessor(new ValidationProcessor())
    .AddProcessor(new ContextAwareProcessor())
    .AddProcessor(new CustomFunctionProcessor())
    .AddProcessor(new AuditLogProcessor());

var processor = new DocumentTemplateProcessor()
    .UseCustomPipeline(pipeline);

processor.ProcessTemplate(template, output, data);

Deprecated Features

Phase 2:

  • ⚠️ ProcessingMode.Legacy - Marked as deprecated, recommend migrating to ContextAware
  • Still supported, will be removed in v2.0.0 (12 months notice)

Phase 3:

  • None (all additions are new features)

Version Compatibility

Version Supported Features
v1.0.x Current implementation (conditionals before loops)
v1.1.0 Phase 1: IEvaluationContext, conditionals in loops
v1.2.0 Phase 2: Visitor pattern, ContextAware mode
v1.3.0 Phase 3: Extensible pipeline, fluent API
v2.0.0 Future: Remove Legacy mode, breaking changes

Success Metrics

Phase 1 Success Criteria

Functionality:

  • ✅ Conditionals inside loops work correctly
  • ✅ All existing tests pass (187 tests)
  • ✅ 5+ new integration tests pass

Performance:

  • ✅ Processing time increase < 5% (< 158ms baseline 150ms)
  • ✅ Memory usage increase < 10% (< 22MB baseline 20MB)

Code Quality:

  • ✅ Test coverage maintained > 85%
  • ✅ No new code duplication introduced
  • ✅ All existing functionality preserved

Documentation:

  • ✅ README.md updated (limitation removed)
  • ✅ Examples.md updated with conditionals in loops examples
  • ✅ TODO.md updated (mark limitation as resolved)
  • ✅ REFACTORING.md created (this document)

Phase 2 Success Criteria - ✅ ALL ACHIEVED

Functionality:

  • ✅ Visitor pattern works for all features (went beyond plan - removed legacy path)
  • ✅ All 319 tests pass (100% success rate)
  • ✅ Table row loops work correctly
  • ✅ Nested loops work correctly
  • ✅ Conditionals inside loops work correctly

Code Quality:

  • ✅ Code duplication eliminated (~300 lines removed)
  • ✅ Clean architecture with visitor pattern implemented
  • ✅ Test coverage 100% (all tests passing)
  • ✅ Cyclomatic complexity reduced significantly
  • ✅ Single processing path (no dual-mode complexity)

Performance:

  • ⏸️ Formal benchmarking deferred (all tests pass with good performance)
  • ✅ No performance regressions observed in test execution

Documentation:

  • ✅ REFACTORING.md updated with actual implementation
  • 🚧 ARCHITECTURE.md needs updating (in progress)
  • ⏸️ Migration guide not needed (no dual-mode support)

Phase 3 Success Criteria

Functionality:

  • ✅ Custom processors can be added
  • ✅ Pipeline order is configurable
  • ✅ Fluent API works as designed
  • ✅ Example processors demonstrate extensibility

Code Quality:

  • ✅ ITemplateProcessor abstraction implemented
  • ✅ TemplateProcessorPipeline composable
  • ✅ Test coverage > 90%
  • ✅ All SOLID principles followed

Performance:

  • ✅ Within 10% of baseline (< 165ms)
  • ✅ Custom processors have minimal overhead

Documentation:

  • ✅ Extension guide with examples
  • ✅ Custom processor tutorial
  • ✅ API reference complete
  • ✅ Real-world examples

Overall Project Success

By end of 5 weeks:

  • ✅ Conditionals in loops working
  • ✅ Code duplication eliminated
  • ✅ Clean, extensible architecture
  • ✅ No regressions in existing functionality
  • ✅ Performance within acceptable range
  • ✅ Comprehensive test coverage (> 90%)
  • ✅ Full documentation
  • ✅ Example custom processors
  • ✅ Migration path clear

Risk Management

Identified Risks

Risk Probability Impact Mitigation
Phase 1
Context chaining breaks nested loops Medium High Comprehensive unit tests for LoopContext
Conditional evaluation wrong in loop scope Medium High Test all operators with loop variables
Performance degrades > 5% Low Medium Benchmark early, optimize if needed
Phase 2
Visitor pattern too complex Medium Low Clear documentation, code examples
Legacy mode diverges from ContextAware Medium High Share as much code as possible
DocumentWalker infinite loop on complex docs Low High Cycle detection, max depth limit
Phase 3
Custom processors break core functionality Medium High Sandboxing, validation, safe defaults
Pipeline overhead too high Low Medium Lazy initialization, processor pooling
API too complex for users Medium Low Simple defaults, good examples
Overall
Timeline slips High Medium Phased approach allows stopping at any phase
Team loses focus Medium Low Clear phase deliverables, regular demos
Breaking changes upset users Low Low Not in production, versioning strategy

Contingency Plans

If Phase 1 takes longer than 1 week:

  • Acceptable: Conditionals in loops is high priority
  • Consider pair programming to unblock
  • May delay Phase 2 start

If Phase 2 complexity is too high:

  • Can ship Phase 1 and reassess
  • Consider simpler alternative to visitor pattern
  • May split into 2 phases

If Phase 3 timeline slips:

  • Phase 3 is nice-to-have, can defer
  • Can release Phase 1+2 as v1.1.0
  • Phase 3 becomes v1.2.0 or v1.3.0

If performance issues arise:

  • Profile and optimize hot paths
  • Consider caching evaluation results
  • May need to revisit architecture decisions
  • Acceptable to take more time for optimization

Appendix

Current Class Structure

TriasDev.Templify/
├── DocumentTemplateProcessor.cs (Main entry point)
├── PlaceholderReplacementOptions.cs
├── ProcessingResult.cs
│
├── Conditionals/
│   ├── ConditionalProcessor.cs
│   ├── ConditionalDetector.cs
│   ├── ConditionalBlock.cs
│   └── ConditionalEvaluator.cs
│
├── Loops/
│   ├── LoopProcessor.cs
│   ├── LoopDetector.cs
│   ├── LoopBlock.cs
│   └── LoopContext.cs
│
├── Placeholders/
│   ├── DocumentBodyReplacer.cs
│   ├── TableReplacer.cs
│   ├── PlaceholderMatch.cs
│   └── ValueResolver.cs
│
└── Utilities/
    ├── PropertyPathResolver.cs
    └── TextElementFormatter.cs

Phase 1 New Classes

TriasDev.Templify/
├── Abstractions/
│   ├── IEvaluationContext.cs (NEW)
│   ├── GlobalEvaluationContext.cs (NEW)
│   └── LoopEvaluationContext.cs (NEW)

Phase 2 New Classes

TriasDev.Templify/
├── Visitors/
│   ├── ITemplateElementVisitor.cs (NEW)
│   ├── DocumentWalker.cs (NEW)
│   ├── ConditionalVisitor.cs (NEW)
│   ├── LoopVisitor.cs (NEW)
│   ├── PlaceholderVisitor.cs (NEW)
│   └── CompositeVisitor.cs (NEW)
│
└── PlaceholderReplacementOptions.cs (MODIFIED)
    └── ProcessingMode enum (NEW)

Phase 3 New Classes

TriasDev.Templify/
├── Pipeline/
│   ├── ITemplateProcessor.cs (NEW)
│   ├── TemplateProcessorPipeline.cs (NEW)
│   ├── ProcessorResult.cs (NEW)
│   └── ContextAwareProcessor.cs (NEW)
│
├── Examples/
│   ├── ValidationProcessor.cs (NEW)
│   ├── FunctionProcessor.cs (NEW)
│   └── HeaderFooterProcessor.cs (NEW)
│
└── DocumentTemplateProcessor.cs (MODIFIED)
    └── Fluent API methods (NEW)

References


Changelog

Date Version Changes
2025-01-08 1.0 Initial refactoring plan created

Document Owner: Development Team Last Updated: 2025-01-08 Next Review: After each phase completion