Tokenizer processes text in two phases: compilation (parsing a template pattern into an internal representation) and tokenization (matching input text against a compiled template to extract values).
Template patterns are compiled through a multi-stage pipeline:
pattern string
-> TemplateLexer (character scanning -> LexerTokens)
-> TemplateParser (LexerTokens -> AST: TemplateDocument/TemplateNodes)
-> AstTemplateDefinitionParser (AST -> Template definition)
-> FrontMatterBinder (extracts YAML config from --- markers)
-> TemplateCompiler (orchestrates the full pipeline)
| Stage | Location | Responsibility |
|---|---|---|
| TemplateLexer | Compilation/Lexer/ |
Character-by-character scanning, produces LexerTokens with FileLocation tracking |
| TemplateParser | Compilation/Parsing/ |
Converts lexer tokens into an AST (TemplateDocument with TemplateNodes) |
| AstTemplateDefinitionParser | Compilation/Definitions/ |
Transforms AST into Template definition objects |
| FrontMatterBinder | Compilation/Binders/ |
Extracts YAML front matter configuration from between --- markers |
| TemplateCompiler | Compilation/TemplateCompiler.cs |
Orchestrates the full compilation pipeline |
| DecoratorRegistry | Compilation/DecoratorRegistry.cs |
Discovers built-in transformers/validators via assembly reflection, merges custom registrations from TokenizerOptions |
Compiled templates are cached internally by pattern string, so repeated calls to Tokenize(pattern, input) only compile once.
Once compiled, templates extract data from input text:
| Component | Location | Responsibility |
|---|---|---|
| TokenizationEngine | Tokenization/TokenizationEngine.cs |
Core processing: matches input against template tokens sequentially |
| HintProcessor | Tokenization/HintProcessor.cs |
Pre-filters templates by checking if hint strings exist in the input before full tokenization |
| ResultBuilder | Tokenization/ResultBuilder.cs |
Aggregates matched/unmatched tokens into TokenizeResult |
| TokenizationContext | Tokenization/TokenizationContext.cs |
Maintains state (position, matches so far) during a tokenization pass |
The engine walks the input text looking for each token's preamble (the literal text preceding the token). When found, it extracts the value up to the next preamble or terminator, runs validators, applies transformers, and records the match.
Transformers (Transformers/) modify extracted values before assignment. Implement ITokenTransformer:
bool TryTransform(object value, string[] args, out object transformed);Validators (Validators/) accept or reject extracted values. Implement ITokenValidator:
bool IsValid(object value, params string[] args);Register custom implementations via TokenizerOptions:
var options = new TokenizerOptions()
.WithTransformer<MyTransformer>()
.WithValidator<MyValidator>();The core compilation and tokenization logic is synchronous. Tokenizer and TemplateMatcher expose async overloads (CompileAsync, TokenizeAsync) for stream/reader-based I/O, using cooperative buffer refills via TokenEnumerator.FillBufferAsync. This allows tokenization of inputs larger than memory.
| Class | Purpose |
|---|---|
Tokenizer |
Single-template tokenization. Compile a pattern, tokenize input against it. |
TemplateMatcher |
Multi-template matching. Register multiple templates, find the best match for an input. |
Both are available via DI using services.AddTokenizer().
The diagnostics subsystem provides structured tracing at two levels: compilation (template construction) and tokenization (runtime matching). It's opt-in via TokenizerOptions.EnableDiagnostics and uses null-object collectors to avoid any allocation overhead when disabled.
Diagnostics are recorded through two collector interfaces, each with an active implementation and a no-op null singleton:
| Interface | Active Implementation | Null Implementation | Scope |
|---|---|---|---|
ICompilationDiagnosticCollector |
CompilationDiagnosticCollector |
NullCompilationDiagnosticCollector |
Template construction - hints, tokens, decorators |
ITokenizationDiagnosticCollector |
TokenizationDiagnosticCollector |
NullTokenizationDiagnosticCollector |
Runtime matching - preambles, values, validators |
Both expose bool IsEnabled, used as a guard at call sites to skip argument evaluation when diagnostics are off:
if (_collector.IsEnabled)
{
_collector.Record(TokenizationEventType.PreambleMatched, tokenName, ...);
}Events are stored as DiagnosticEvent<TType>, a generic container parameterised by event kind:
DiagnosticEvent<TType>
├── TType Type - event kind enum value
├── string? TokenName - token this event relates to
├── int? TokenId - unique token ID within template
├── FileLocation? Location - position in input/source
├── string? Value - value being tested/assigned
├── string? Detail - human-readable explanation
├── string? DecoratorName - validator/transformer name
└── string[]? DecoratorArgs - decorator parameters
Global type aliases simplify usage:
global using TokenizationEvent = DiagnosticEvent<TokenizationEventType>;
global using CompilationEvent = DiagnosticEvent<CompilationEventType>;CompilationEventType (8 events): HintAdded, TagAdded, TokenCreated, OptionApplied, DecoratorApplied, ConcatenationApplied, RepeatingTokenLinked, CompilationCompleted.
TokenizationEventType (20 events): TokenizationStarted, TokenizationCompleted, HintMatched, HintMissing, PreambleSearchStarted, PreambleMatched, PreambleNotFound, ValueAccumulated, TokenAssignmentAttempted, ValidatorPassed, ValidatorFailed, TransformerSucceeded, TransformerFailed, TokenAssigned, TokenAssignmentFailed, NewlineTerminatedTokenProcessed, BacktrackStarted, RepeatingTokenDisabled, SingleUseTokenRemoved, FrontMatterTokenAssigned, FrontMatterTokenFailed, TokenMissed.
Raw events are transformed into a per-token diagnostic view by TokenDiagnosticBuilder. Each token gets a TokenDiagnostic that tells its complete story:
TokenDiagnostic
├── TokenName, TokenId
├── TokenOutcome - Matched | Rejected | NeverFound | Blocked
├── TokenAttempt[] - every consideration during tokenization
│ ├── Location, Value
│ ├── AttemptOutcome - Assigned | ValidatorRejected | TransformerFailed | Backtracked
│ └── DecoratorName, Reason
├── AssignedValues[] - matched values (multiple for repeating tokens)
├── AssignedLocations[] - parallel to AssignedValues
├── BlockedBy - name of blocker token (Blocked outcome only)
└── DiagnosticIssue[] - problems with adaptive hints
├── Code - stable issue code (TK001–TK008)
├── DiagnosticIssueType
├── Description
├── Location
└── Hint - contextual suggestion from hint generators
The token-centric view is built lazily on first access and cached. Raw events remain available via RawEvents for low-level tracing.
TokenDiagnosticBuilder transforms raw TokenizationEvent lists into TokenDiagnostic arrays through four ordered phases:
-
CollectEvents - walks all raw events, builds per-token attempt lists, issue lists, assigned value entries, and context indexes for cross-referencing.
-
ClassifyOutcomes - creates
TokenDiagnosticobjects. Determines each token'sTokenOutcomebased on whether it was assigned, rejected, or missed. RunsValueMismatchdetection (checks whether a matched token's value contains the preamble of a missed token, suggesting greedy capture). -
ApplyBlockedAnnotations - causality analysis for ordered mode only. Finds the first non-optional unmatched token (the "blocker") and reclassifies subsequent
NeverFoundtokens asBlocked. -
BuildVerdict - generates a human-readable summary (e.g. "Matched 3 of 5 tokens (2 missed).").
Each DiagnosticIssue carries a stable code from IssueCodeMap for programmatic filtering and documentation linking:
| Code | DiagnosticIssueType | Meaning |
|---|---|---|
| TK001 | PreambleNeverFound |
Preamble text not found in input |
| TK002 | ValidatorRejection |
Validator rejected extracted value |
| TK003 | TransformerFailure |
Transformer conversion failed |
| TK004 | ValueMismatch |
Greedy capture consumed another token's preamble |
| TK005 | RepeatingTokenCutShort |
Repeating token disabled prematurely |
| TK006 | (reserved) | Formerly UnmatchedInputSection, reserved to prevent reuse |
| TK007 | HintMissing |
Required hint string not found in input |
| TK008 | Blocked |
Not searched due to prior required token failure |
IssueFactory chains IHintGenerator implementations to produce contextual suggestions for each issue. All generators are stateless and shared via a static default factory. Each one receives the source event and a BuildContext containing cross-token indexes (input lines, rejections per token, decorator successes, optional token names).
| Generator | Detects | Example Hint |
|---|---|---|
PreambleNearMissHintGenerator |
Case/whitespace near-matches in input | "Input contains 'name:' at line 3 (case difference)." |
ValidatorValueHintGenerator |
Known validator patterns (IsEmail, IsNumeric, etc.) | "Value 'abc' is not a valid number." |
DateFormatHintGenerator |
Failed date transformers, tries 18 common formats | "Value matches format 'dd/MM/yyyy'. Change transformer to use it." |
ChainedDecoratorHintGenerator |
Prior decorator succeeded, next failed | "Decorator chain: 'Trim' succeeded → 'ToInt' rejected value 'abc'." |
MultipleRejectionHintGenerator |
Token rejected 2+ times | "Token was rejected 3 times. Values tried: 'a', 'b', 'c'." |
OptionalTokenHintGenerator |
Optional token not found | "Token 'MiddleName' is optional - no action needed." |
RepeatingTokenHintGenerator |
Repeating token disabled early | "Repeating token disabled. Value 'x' failed IsNumeric validation." |
ValueMismatchHintGenerator |
Greedy capture swallowed another preamble | "Consider adding '$' to prevent greedy capture." |
BlockedTokenHintGenerator |
Token blocked by prior failure | "Fix 'FirstName' first - this token may match once resolved." |
Two renderers produce human-readable diagnostic output:
AlignmentRenderer (AlignmentRenderer.Render) - structured view of template-to-input mapping with sections for matched tokens, failures, unmatched tokens, and blocked tokens. Includes assigned values with line locations, decorator details, and hints.
ProcessingOrderRenderer (ProcessingOrderRenderer.Render) - chronological walk-through of every engine decision, showing event type, token name, location, value, decorator, and detail for each step.
Both are called automatically during FinalizeTokenization when diagnostics are enabled, with alignment logged at Warning level and processing order at Debug level.
TemplateCompiler.Compile()
├── Creates ICompilationDiagnosticCollector (real or null based on EnableDiagnostics)
├── Passes collector to HintBinder, TagBinder, TokenBinder, DecoratorPipeline
├── Records CompilationCompleted
└── Returns CompilationResult with CompilationDiagnostics
Tokenizer.Tokenize() / RunCoreAsync()
├── Creates ITokenizationDiagnosticCollector (real or null based on EnableDiagnostics)
├── Passes collector to TokenizationSession
│ ├── TokenizationSession records start/complete, delegates to sub-components
│ ├── CandidateProcessor records preamble matching, value accumulation
│ ├── DecoratorPipeline records validator/transformer results
│ └── TokenMatchRouter records assignment decisions
├── ResultBuilder.BuildUnmatchedTokens records TokenMissed events
├── collector.GetResult() → TokenizationDiagnostics (attached to TokenizeResult.Diagnostics)
└── Logs issues, alignment, and processing order via ILogger