# Receipt Template Language (RTL) - Project Specification --- # Phase 1: Core Template Engine ## 1. Overview A file-based template system for configuring receipt layouts per receipt type and printer model. Replaces hardcoded C# converters with editable `.template` files. ### Deliverables - New project: `Inspectron.Epson.Templates` - Template language lexer, parser, and interpreter - File-based configuration loader - Integration with existing `IReceiptConverterFactory` - Fallback template for undefined combinations - Unit tests --- ## 2. Project Structure ``` Inspectron.Epson.Templates/ ├── Inspectron.Epson.Templates.csproj ├── Language/ │ ├── Lexer.cs │ ├── Parser.cs │ ├── Tokens.cs │ └── Nodes/ │ ├── TemplateNode.cs │ ├── TextNode.cs │ ├── StyledTextNode.cs │ ├── BindingNode.cs │ ├── SeparatorNode.cs │ ├── RowNode.cs │ ├── ColumnNode.cs │ ├── IfNode.cs │ ├── ForeachNode.cs │ └── EmptyLineNode.cs ├── Interpreter/ │ ├── TemplateInterpreter.cs │ ├── DataContext.cs │ └── InterpreterException.cs ├── Configuration/ │ ├── TemplateConfiguration.cs │ ├── TemplateAssignment.cs │ ├── PrinterProfile.cs │ ├── PrinterProfileRegistry.cs │ └── TemplateResolver.cs ├── Storage/ │ ├── ITemplateStorage.cs │ └── FileTemplateStorage.cs ├── TemplateEngine.cs ├── TemplateReceiptConverter.cs └── TemplateReceiptConverterFactory.cs ``` --- ## 3. File System Layout ``` templates/ ├── kitchen-default.template # Default kitchen receipt ├── kitchen-u220.template # Kitchen receipt for TM-U220II ├── bar-default.template # Bar receipt ├── final-receipt.template # Customer receipt ├── orders-overview.template # Orders overview ├── fallback.template # Used when no match found ├── assignments.json # Maps (receiptType, printer) → template └── samples/ # Sample data for preview/testing ├── kitchen-default.json ├── bar-default.json ├── final-receipt.json └── orders-overview.json ``` --- ## 4. Template Language Specification ### 4.1 Line Types | Line Type | Syntax | Example | |-----------|--------|---------| | Styled Text | `#styles# content` | `#bold,center# {title}` | | Plain Text | `content` | `Hello World` | | Separator | `---`, `===`, `***`, `~~~` | `---` | | Row Start | `@row ratios [styles]` | `@row 60:40 bold` | | Row Content | `content \| content` | `{name} \| >{price}` | | Row End | `@endrow` | `@endrow` | | Condition | `@if condition` | `@if comment` | | Loop | `@foreach item in collection` | `@foreach dish in dishes` | | Block End | `@end` | `@end` | | Empty | *(blank line)* | | ### 4.2 Styles | Style | Description | |-------|-------------| | `bold` | Bold text | | `big` | Large font (2x magnification) | | `red` | Red color (two-color printers only) | | `center` | Center-aligned | | `right` | Right-aligned | | `spacing:N` | Set line spacing (0-255) | | `spacing:default` | Reset to default spacing | **Syntax:** `#style1,style2,...# content` **Examples:** ``` #bold,center,big# {title} #red# WARNING #spacing:0,bold# Compact line ``` ### 4.3 Data Binding **Syntax:** `{path}` or `{path:format}` **Path navigation:** ``` {title} // Root property {order.date} // Nested property {items.count} // Collection count ``` **Format specifiers:** ``` {price:F2} // Decimal, 2 places → "12.50" {date:dd-MMM-yy} // Date → "25-Jan-26" {date:HH:mm} // Time → "14:30" {number:D4} // Padded integer → "0042" ``` **Missing data behavior:** Returns empty string (silent). ### 4.4 Separators ``` --- // Dashed line (fills line width) === // Equals line *** // Asterisk line ~~~ // Tilde line ``` Minimum 3 characters. Fills `PrinterProfile.LineWidth`. ### 4.5 Rows (Columns) **Multi-line syntax:** ``` @row 60:40 {leftContent} | >{rightContent} @endrow ``` **Single-line shorthand:** ``` @row 60:40 | {left} | >{right} | ``` **Ratios:** ``` @row 50:50 // Two equal columns @row 60:30:10 // Three columns (percentages) @row *:20 // First fills remainder, second is 20% @row 30:*:20 // Middle fills remainder ``` **Column alignment (prefix in content):** ``` {text} // Left-aligned (default) >{text} // Right-aligned ^{text} // Center-aligned ``` **Row styles (optional, applied to all columns):** ``` @row 50:50 bold @row 70:30 big,red ``` ### 4.6 Conditionals **Syntax:** ``` @if condition content @end ``` **Conditions:** ``` @if propertyName // Truthy (not null, not empty, not false, not 0) @if !propertyName // Falsy @if items.count > 0 // Comparison @if status == "active" // String equality @if total >= 100 // Numeric comparison ``` **Operators:** `==`, `!=`, `>`, `<`, `>=`, `<=` ### 4.7 Loops **Syntax:** ``` @foreach item in collection content using {item.property} @end ``` **Loop variables:** ``` {item} // Current item (if primitive) {item.property} // Item property {item._index} // 0-based index {item._number} // 1-based number {item._first} // true if first iteration {item._last} // true if last iteration ``` **Nesting:** ``` @foreach gang in gangs #red# {gang.name} @foreach dish in gang.dishes {dish.number}x {dish.name} @end @end ``` --- ## 5. Configuration ### 5.1 assignments.json ```json { "assignments": [ { "receiptType": "Receipt", "printerProfile": null, "templateFile": "final-receipt.template" }, { "receiptType": "WorkareaTicket", "printerProfile": null, "templateFile": "kitchen-default.template" }, { "receiptType": "WorkareaTicket", "printerProfile": "tm-u220ii", "templateFile": "kitchen-u220.template" }, { "receiptType": "NextCourse", "printerProfile": null, "templateFile": "kitchen-default.template" }, { "receiptType": "OrdersOverview", "printerProfile": null, "templateFile": "orders-overview.template" } ], "fallbackTemplate": "fallback.template" } ``` ### 5.2 Printer Profiles (built-in) | Profile ID | Printer | LineWidth | BigLineWidth | SupportsRed | |------------|---------|-----------|--------------|-------------| | `tm-t30iii` | TM-T30III | 48 | 24 | false | | `tm-u220ii` | TM-U220II | 33 | 20 | true | Mapped from printer ID byte: - `0x01` → `tm-t30iii` - `0x13` → `tm-u220ii` ### 5.3 Resolution Priority 1. Exact match: `receiptType` + `printerProfile` 2. Type only: `receiptType` + `printerProfile: null` 3. Fallback: `fallbackTemplate` --- ## 6. Public API ### 6.1 TemplateEngine ```csharp namespace Inspectron.Epson.Templates; public class TemplateEngine { public TemplateEngine(string templatesDirectory); /// /// Renders a template with the given data. /// public List Render( string templateContent, string jsonData, PrinterProfile profile); /// /// Parses template and returns errors/warnings (for validation). /// public TemplateValidationResult Validate(string templateContent); } public class TemplateValidationResult { public bool IsValid { get; } public List Errors { get; } public List Warnings { get; } } public class TemplateError { public int Line { get; } public int Column { get; } public string Message { get; } public string Code { get; } // e.g., "RTL001" } public class TemplateWarning { public int Line { get; } public int Column { get; } public string Message { get; } public string Code { get; } // e.g., "RTL100" } ``` ### 6.2 TemplateReceiptConverterFactory ```csharp namespace Inspectron.Epson.Templates; public class TemplateReceiptConverterFactory : IReceiptConverterFactory { public TemplateReceiptConverterFactory(string templatesDirectory); /// /// Creates a converter for the given receipt type and printer. /// Falls back to fallback template if no match found. /// public IReceiptConverter Create(int receiptType, byte printerId); /// /// Reloads templates and assignments from disk. /// public void Reload(); } ``` ### 6.3 Integration Example ```csharp // In EpsonPrintService startup var converterFactory = new TemplateReceiptConverterFactory("./templates"); // In DI registration (Ninject) kernel.Bind() .ToConstant(converterFactory) .InSingletonScope(); // Usage remains unchanged var converter = converterFactory.Create(receiptType, printerId); var commands = converter.Convert(jsonContent); ``` --- ## 7. Error Codes ### Errors (prevent rendering) | Code | Message | |------|---------| | RTL001 | Unexpected token '{token}' at line {line} | | RTL002 | Unclosed block '@{block}' started at line {line} | | RTL003 | '@end' without matching '@if' or '@foreach' | | RTL004 | Invalid style '{style}' | | RTL005 | Invalid ratio format '{ratio}' | | RTL006 | Column count mismatch: expected {expected}, got {actual} | | RTL007 | Invalid condition syntax | | RTL008 | Invalid binding path '{path}' | | RTL009 | Unclosed binding '{' at line {line} | | RTL010 | Unclosed style marker '#' at line {line} | ### Warnings (render anyway) | Code | Message | |------|---------| | RTL100 | Unused loop variable '{variable}' | | RTL101 | Empty @if block | | RTL102 | Empty @foreach block | | RTL103 | Style '{style}' has no effect on this printer | --- ## 8. Default Templates Provide default templates matching current hardcoded behavior: - `kitchen-default.template` - matches `KitchenReceiptConverter` - `kitchen-u220.template` - compact version for narrow printers - `bar-default.template` - matches `BarReceiptConverter` - `final-receipt.template` - matches `FinalReceiptConverter` - `orders-overview.template` - matches `OrderItemsReceiptConverter` - `fallback.template` - minimal template that dumps JSON fields --- ## 9. Testing ### Unit Tests ``` Inspectron.Epson.Templates.Tests/ ├── Lexer/ │ ├── LexerBasicTests.cs │ ├── LexerStyleTests.cs │ ├── LexerBindingTests.cs │ └── LexerControlFlowTests.cs ├── Parser/ │ ├── ParserBasicTests.cs │ ├── ParserRowTests.cs │ ├── ParserNestingTests.cs │ └── ParserErrorTests.cs ├── Interpreter/ │ ├── InterpreterTextTests.cs │ ├── InterpreterBindingTests.cs │ ├── InterpreterConditionTests.cs │ ├── InterpreterLoopTests.cs │ └── InterpreterRowTests.cs ├── Configuration/ │ ├── TemplateResolverTests.cs │ └── PrinterProfileTests.cs └── Integration/ ├── KitchenReceiptTests.cs ├── FinalReceiptTests.cs └── FallbackTests.cs ``` ### Test Coverage Goals - Lexer: All token types, edge cases, error handling - Parser: All node types, nesting, malformed input - Interpreter: All features, format specifiers, missing data - Integration: Match output of existing converters --- ## 10. Migration Plan 1. Create `Inspectron.Epson.Templates` project 2. Implement and test template engine 3. Create default templates matching current output 4. Add `TemplateReceiptConverterFactory` to DI as alternative 5. Integration test: compare output of both factories 6. Switch DI registration to use template factory 7. Remove legacy converter code (optional, can keep as reference) --- # Phase 2: Template Editor (VS Code Extension + C# Language Server) ## 1. Overview VS Code extension for creating and editing receipt templates with live preview, syntax highlighting, autocomplete, and validation. The extension uses a **C# Language Server** (LSP) that reuses the lexer, parser, and interpreter from Phase 1. The VS Code extension is a thin TypeScript client that connects to the language server. ### Architecture ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ VS Code │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ vscode-rtl (TypeScript) │ │ │ │ - Language client (connects to LSP) │ │ │ │ - Preview webview (renders HTML) │ │ │ │ - Assignments webview │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ LSP (JSON-RPC over stdio) │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Inspectron.Epson.Templates.LanguageServer (C#) │ │ │ │ - Semantic tokens (syntax highlighting) │ │ │ │ - Completion provider │ │ │ │ - Diagnostics (validation) │ │ │ │ - Hover provider │ │ │ │ - Folding ranges │ │ │ │ - Document symbols │ │ │ │ - Custom requests (preview rendering) │ │ │ │ │ │ │ │ References: Inspectron.Epson.Templates (Phase 1) │ │ │ └───────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Deliverables - C# Language Server: `Inspectron.Epson.Templates.LanguageServer` - VS Code extension: `vscode-rtl` (thin TypeScript client) - Language registration for `.template` files - Semantic token provider for syntax highlighting (via LSP) - Completion provider (via LSP) - Diagnostic provider for real-time validation (via LSP) - Webview panel for live preview with sample data - Webview panel for template assignment management ### Advantages - **No code duplication** - reuses Phase 1 lexer, parser, interpreter - **Single source of truth** - language behavior defined once in C# - **Mature editing features** - VS Code provides find/replace, multi-cursor, undo/redo, git - **Cross-platform** - .NET 8 runs on Windows, macOS, Linux - **Easy distribution** - VSIX bundles extension + language server executable --- ## 2. Project Structure ### 2.1 Language Server (C#) ``` Inspectron.Epson.Templates.LanguageServer/ ├── Inspectron.Epson.Templates.LanguageServer.csproj ├── Program.cs # Entry point, stdio server setup ├── RtlLanguageServer.cs # Main server class ├── Handlers/ │ ├── TextDocumentSyncHandler.cs # Document open/change/close │ ├── SemanticTokensHandler.cs # Syntax highlighting │ ├── CompletionHandler.cs # Autocomplete │ ├── DiagnosticHandler.cs # Validation errors/warnings │ ├── HoverHandler.cs # Hover information │ ├── FoldingRangeHandler.cs # Code folding │ ├── DocumentSymbolHandler.cs # Outline/breadcrumbs │ └── CustomRequestHandlers.cs # Preview rendering, schema extraction ├── Services/ │ ├── DocumentManager.cs # Track open documents │ ├── DiagnosticService.cs # Run validation, publish diagnostics │ ├── CompletionService.cs # Build completion items │ ├── SemanticTokenService.cs # Generate semantic tokens │ ├── SchemaService.cs # Extract schema from sample JSON │ └── ScopeTracker.cs # Track @foreach loop variables └── Models/ ├── RtlSemanticTokenTypes.cs # Token type definitions └── RtlSemanticTokenModifiers.cs # Token modifier definitions ``` ### 2.2 VS Code Extension (TypeScript - thin client) ``` vscode-rtl/ ├── package.json # Extension manifest ├── tsconfig.json # TypeScript configuration ├── webpack.config.js # Bundler configuration ├── .vscodeignore # Files to exclude from package ├── language-configuration.json # Bracket matching, auto-closing pairs ├── snippets/ │ └── rtl.snippets.json # Code snippets (@if, @foreach, @row) ├── src/ │ ├── extension.ts # Extension entry point │ ├── languageClient.ts # LSP client setup and management │ ├── preview/ │ │ ├── previewPanel.ts # Webview panel management │ │ └── previewStyles.css # Receipt visual styling │ ├── assignments/ │ │ └── assignmentsPanel.ts # Assignments editor webview │ └── utils/ │ └── serverPath.ts # Locate language server executable ├── media/ │ ├── preview.css # Receipt preview styling │ ├── assignments.css # Assignments editor styling │ └── icons/ │ ├── template.svg # File icon │ └── preview.svg # Preview button icon ├── server/ # Bundled language server (published .exe/.dll) │ └── (built from C# project) └── test/ └── extension.test.ts # Basic extension activation tests ``` --- ## 3. UI Layout The extension integrates with VS Code's native UI: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ File Edit Selection View Go Run Terminal Help │ ├─────────────────────────────────────────────────────────────────────────────┤ │ EXPLORER │ kitchen-default.template │ RTL Preview │ │ ┌─────────────────┐ │ ─────────────────────────────────────│───────────────│ │ │ ▼ TEMPLATES │ │ 1│#bold,center,big# {title} │┌─────────────┐│ │ │ kitchen-def...│ │ 2│--- ││ Warme Küche ││ │ │ kitchen-u220 │ │ 3│ ││─────────────││ │ │ bar-default │ │ 4│@row 60:40 ││ ││ │ │ final-receipt │ │ 5│{date:dd-MMM} | >{orderNumber} ││25-Jan Nr:42 ││ │ │ orders-overv..│ │ 6│@endrow ││ ││ │ │ fallback │ │ 7│#center# {waiter} ││Max Musterma.││ │ │ │ │ 8│ ││ ││ │ │ ▼ SAMPLES │ │ 9│@foreach dish in dishes ││Tisch: 12 ││ │ │ kitchen-def...│ │ 10│ {dish.quantity}x {dish.name} ││─────────────││ │ │ bar-default...│ │ 11│@end ││2x Tomatensup││ │ │ │ │ ││1x Schnitzel ││ │ │ ▼ RTL ASSIGNMENTS│ │ ││ - Pommes ││ │ │ Receipt → ...│ │ ││ + Reis ││ │ │ WorkareaTic..│ │ │└─────────────┘│ │ │ NextCourse →.│ │ │ │ │ └─────────────────┘ │ │Printer: ▼ │ │ │ │ TM-T30III │ ├─────────────────────┴──────────────────────────────────────┴───────────────┤ │ PROBLEMS OUTPUT DEBUG CONSOLE TERMINAL │ │ ┌──────────────────────────────────────────────────────────────────────────┐│ │ │ ⚠ kitchen-default.template [2] ││ │ │ ⚠ Line 10: Unused loop variable 'dish' in nested scope [RTL100] ││ │ │ ❌ fallback.template [1] ││ │ │ ❌ Line 5: Unclosed '@foreach' block started at line 3 [RTL002] ││ │ └──────────────────────────────────────────────────────────────────────────┘│ ├─────────────────────────────────────────────────────────────────────────────┤ │ RTL │ Ln 7, Col 12 │ UTF-8 │ CRLF │ {} kitchen-default.json │ ✓ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### UI Components | Component | VS Code Feature | Implementation | |-----------|-----------------|----------------| | Template list | Explorer tree view | Native file explorer or custom TreeDataProvider | | Sample data list | Explorer tree view | Custom TreeDataProvider | | Assignments list | Explorer tree view | Custom TreeDataProvider with inline editing | | Editor | Native text editor | Language client connects to C# language server | | Syntax highlighting | Semantic tokens | Language server provides tokens via LSP | | Autocomplete | Completion items | Language server provides completions via LSP | | Validation | Problems panel | Language server publishes diagnostics via LSP | | Preview | Webview panel | TypeScript requests rendered HTML from language server | | Status bar | Status bar items | Shows current sample file, printer profile | --- ## 4. Features ### 4.1 Language Registration (VS Code Extension) **package.json contribution:** ```json { "contributes": { "languages": [{ "id": "rtl", "aliases": ["Receipt Template Language", "RTL"], "extensions": [".template"], "configuration": "./language-configuration.json", "icon": { "light": "./media/icons/template.svg", "dark": "./media/icons/template.svg" } }] } } ``` **Note:** No TextMate grammar needed - syntax highlighting is provided via LSP semantic tokens. **language-configuration.json:** ```json { "comments": { "lineComment": "//" }, "brackets": [["@if", "@end"], ["@foreach", "@end"], ["@row", "@endrow"]], "autoClosingPairs": [ { "open": "{", "close": "}" }, { "open": "#", "close": "#" } ], "surroundingPairs": [ { "open": "{", "close": "}" }, { "open": "#", "close": "#" } ] } ``` ### 4.2 Syntax Highlighting (LSP Semantic Tokens) The language server provides semantic tokens via the `textDocument/semanticTokens/full` LSP method. **C# Semantic Token Types (SemanticTokensHandler.cs):** ```csharp public static class RtlSemanticTokenTypes { public const string Keyword = "keyword"; // @if, @foreach, @end, @row, @endrow public const string Variable = "variable"; // {binding} public const string Property = "property"; // {object.property} public const string Decorator = "decorator"; // #style# public const string String = "string"; // Style names: bold, center public const string Number = "number"; // Ratios: 60:40 public const string Operator = "operator"; // ==, !=, >, <, |, >, ^ public const string Comment = "comment"; // Separators: ---, === public const string Type = "type"; // Format specifiers: F2, dd-MMM } ``` **Token mapping:** | Element | Token Type | Default Color | |---------|------------|---------------| | `@if`, `@foreach`, `@end` | `keyword` | Purple | | `@row`, `@endrow` | `keyword` | Purple | | `#...#` markers | `decorator` | Blue | | Style names `bold`, `center` | `string` | Green | | `{...}` bindings | `variable` | Teal | | Property paths `.property` | `property` | Teal | | Format specifiers `:F2` | `type` | Orange | | Ratios `60:40` | `number` | Light Green | | Separators `---` | `comment` | Gray | | Column delimiter `\|` | `operator` | Yellow | | Alignment `>`, `^` | `operator` | Orange | | Comparison `==`, `!=` | `operator` | Red | ### 4.3 Code Folding (LSP Folding Ranges) **FoldingRangeHandler.cs:** ```csharp public class FoldingRangeHandler : IFoldingRangeHandler { public Task?> Handle(FoldingRangeRequestParam request, CancellationToken ct) { var ranges = new List(); // Parse document, find @if...@end, @foreach...@end, @row...@endrow blocks // Add FoldingRange for each block return Task.FromResult?>(new Container(ranges)); } } ``` Supports: - Fold `@if...@end` blocks - Fold `@foreach...@end` blocks - Fold `@row...@endrow` blocks - Nested folding ### 4.4 Autocomplete (LSP Completion) **CompletionHandler.cs triggers:** | Trigger | Context | Completions | |---------|---------|-------------| | `#` | Line start or after `#` | Style keywords: `bold`, `big`, `red`, `center`, `right`, `spacing:` | | `,` | Inside `#...#` | Style keywords | | `@` | Line start | Control flow: `if`, `foreach`, `end`, `row`, `endrow` | | `{` | Anywhere | Root data properties from sample JSON + active loop variables | | `.` | Inside `{...}` | Child properties based on path + loop metadata (`_index`, `_first`, etc.) | | `:` | Inside `{...}` after property | Format specifiers: `F2`, `D4`, `dd-MMM-yy`, `HH:mm` | | `in ` | After `@foreach x` | Collection properties from schema | **CompletionService.cs features:** - Context-aware (detects binding, style, control flow context) - Completion item kinds (Property, Keyword, Snippet, etc.) - Documentation for format specifiers - Loop variable scope tracking via ScopeTracker ```csharp public class CompletionService { private readonly SchemaService _schemaService; private readonly ScopeTracker _scopeTracker; public List GetCompletions(DocumentUri uri, Position position) { var context = DetectContext(uri, position); return context switch { CompletionContext.Style => GetStyleCompletions(), CompletionContext.ControlFlow => GetControlFlowCompletions(), CompletionContext.Binding => GetBindingCompletions(uri, position), CompletionContext.BindingProperty => GetPropertyCompletions(uri, position), CompletionContext.BindingFormat => GetFormatCompletions(), _ => new List() }; } } ``` ### 4.5 Diagnostics (LSP PublishDiagnostics) **DiagnosticService.cs:** - Validates on document open and change (debounced) - Uses Phase 1 `TemplateEngine.Validate()` method - Publishes diagnostics via `textDocument/publishDiagnostics` ```csharp public class DiagnosticService { private readonly TemplateEngine _templateEngine; private readonly ILanguageServerFacade _server; public void ValidateDocument(DocumentUri uri, string content) { var result = _templateEngine.Validate(content); var diagnostics = result.Errors .Select(e => new Diagnostic { Range = new Range(e.Line - 1, e.Column - 1, e.Line - 1, e.Column + 10), Severity = DiagnosticSeverity.Error, Code = e.Code, Message = e.Message, Source = "rtl" }) .Concat(result.Warnings.Select(w => new Diagnostic { Range = new Range(w.Line - 1, w.Column - 1, w.Line - 1, w.Column + 10), Severity = DiagnosticSeverity.Warning, Code = w.Code, Message = w.Message, Source = "rtl" })) .ToList(); _server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams { Uri = uri, Diagnostics = new Container(diagnostics) }); } } ``` ### 4.6 Hover Provider (LSP Hover) **HoverHandler.cs:** - Hover over style → shows description - Hover over binding → shows data type from schema - Hover over control flow → shows syntax help - Hover over format specifier → shows example output ```csharp public class HoverHandler : IHoverHandler { public Task Handle(HoverParams request, CancellationToken ct) { var content = GetHoverContent(request.TextDocument.Uri, request.Position); if (content == null) return Task.FromResult(null); return Task.FromResult(new Hover { Contents = new MarkedStringsOrMarkupContent(new MarkupContent { Kind = MarkupKind.Markdown, Value = content }) }); } } ``` ### 4.7 Code Snippets (VS Code Extension) **snippets/rtl.snippets.json:** ```json { "If Block": { "prefix": "@if", "body": ["@if ${1:condition}", " $0", "@end"], "description": "Conditional block" }, "Foreach Loop": { "prefix": "@foreach", "body": ["@foreach ${1:item} in ${2:collection}", " $0", "@end"], "description": "Loop over collection" }, "Row": { "prefix": "@row", "body": ["@row ${1:60}:${2:40}", "${3:left} | >${4:right}", "@endrow"], "description": "Two-column row" }, "Styled Text": { "prefix": "#style", "body": ["#${1|bold,big,red,center,right|}# ${0:text}"], "description": "Styled text line" } } ``` ### 4.8 Preview Panel (Webview + Custom LSP Request) The preview uses a **custom LSP request** to get rendered HTML from the language server. **Custom request (Language Server):** ```csharp // CustomRequestHandlers.cs [Method("rtl/renderPreview")] public record RenderPreviewParams { public DocumentUri Uri { get; init; } public string PrinterProfile { get; init; } // "tm-t30iii" or "tm-u220ii" public string? SampleDataPath { get; init; } } public record RenderPreviewResult { public string Html { get; init; } // Rendered receipt as HTML public string? Error { get; init; } // Render error message if any } public class CustomRequestHandlers : IJsonRpcRequestHandler { public Task Handle(RenderPreviewParams request, CancellationToken ct) { // 1. Load template content // 2. Load sample data JSON // 3. Get printer profile // 4. Call TemplateEngine.Render() // 5. Convert PrintCommand[] to HTML // 6. Return HTML or error } } ``` **VS Code Extension (previewPanel.ts):** ```typescript async function updatePreview() { const result = await client.sendRequest('rtl/renderPreview', { uri: activeDocument.uri.toString(), printerProfile: selectedPrinter, sampleDataPath: selectedSamplePath }); if (result.error) { panel.webview.html = renderError(result.error); } else { panel.webview.html = wrapInReceiptStyle(result.html); } } ``` **Features:** - Side-by-side preview (like Markdown preview) - Printer profile selector (TM-T30III, TM-U220II) - Live updates as template changes (debounced 300ms) - Shows render errors inline ### 4.9 Sample Data Integration **Custom LSP requests:** ```csharp // Get schema from sample JSON (for autocomplete) [Method("rtl/getSchema")] public record GetSchemaParams { public string SampleDataPath { get; init; } } public record GetSchemaResult { public SchemaNode Schema { get; init; } } // List available sample files [Method("rtl/listSamples")] public record ListSamplesResult { public List SampleFiles { get; init; } } ``` **Features:** - Status bar item shows current sample file - Click status bar → quick pick to change sample - Sample JSON files in `samples/` directory - Automatic schema extraction for autocomplete - Sample file association by naming convention ### 4.10 Tree Views (VS Code Extension) **Templates Tree View:** - Lists all `.template` files - Context menu: Rename, Delete, Duplicate, New - Click to open **Samples Tree View:** - Lists all `.json` sample files - Shows which template they're associated with - Click to open **Assignments Tree View:** - Shows current assignments from `assignments.json` - Inline display: `Receipt → final-receipt.template` - Context menu: Edit, Delete - "Add Assignment" button ### 4.11 Assignments Editor (Webview) **Webview panel for editing assignments.json:** ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Template Assignments [Save] │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Receipt Type Printer Template Actions │ │ ─────────────────────────────────────────────────────────────────── │ │ [Receipt ▼] [(Any) ▼] [final-receipt ▼] [Delete] │ │ [WorkareaTicket ▼] [(Any) ▼] [kitchen-default ▼] [Delete] │ │ [WorkareaTicket ▼] [TM-U220II ▼] [kitchen-u220 ▼] [Delete] │ │ [NextCourse ▼] [(Any) ▼] [kitchen-default ▼] [Delete] │ │ [OrdersOverview ▼] [(Any) ▼] [orders-overview ▼] [Delete] │ │ │ │ [+ Add Assignment] │ │ │ │ Fallback Template: [fallback.template ▼] │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 5. Data Flow ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ VS Code │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────────────────────┐ │ │ │ vscode-rtl Extension (TypeScript) │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ │ │ Language │ │ Preview │ │ Assignments │ │ │ │ │ │ Client │ │ Webview │ │ Webview │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └────────────┬─────────────┘ │ │ │ │ │ │ │ │ │ │ └─────────┼───────────────────┼─────────────────────────┼───────────────┘ │ │ │ LSP │ Custom Request │ File I/O │ │ │ (JSON-RPC) │ rtl/renderPreview │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Language Server (C# - stdio process) │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Handlers │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ │ │ Semantic │ │ Completion │ │ Diagnostic │ │ Custom │ │ │ │ │ │ │ │ Tokens │ │ Handler │ │ Handler │ │ Requests │ │ │ │ │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ └───────┼──────────────┼──────────────┼──────────────┼────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Services │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ │ │ Document │ │ Schema │ │ Scope │ │ Diagnostic │ │ │ │ │ │ │ │ Manager │ │ Service │ │ Tracker │ │ Service │ │ │ │ │ │ │ └────────────┘ └─────┬──────┘ └────────────┘ └─────┬──────┘ │ │ │ │ │ └──────────────────────┼─────────────────────────────┼────────┘ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Inspectron.Epson.Templates (Phase 1 - Referenced) │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ │ │ Lexer │ │ Parser │ │ Template │ │ │ │ │ │ │ │ │ │ │ │ Engine │ │ │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` **Key points:** - Language Server is a standalone C# console application communicating via stdio - All language features (highlighting, completion, validation) reuse Phase 1 code - Custom LSP requests handle preview rendering and schema extraction - VS Code extension is a thin client (~500 lines of TypeScript) --- ## 6. Language Server Implementation Details ### 6.1 Context Detection (CompletionService.cs) ```csharp public enum CompletionContext { None, Style, // Inside #...# ControlFlow, // After @ at line start Binding, // Inside {...} BindingProperty, // After . inside {...} BindingFormat, // After : inside {...} ForeachCollection, // After "in" in @foreach RowContent // Inside @row...@endrow } public class CompletionService { public CompletionContext DetectContext(string line, int column) { var textBefore = line[..column]; // Check for binding context var lastOpenBrace = textBefore.LastIndexOf('{'); var lastCloseBrace = textBefore.LastIndexOf('}'); if (lastOpenBrace > lastCloseBrace) { var bindingText = textBefore[(lastOpenBrace + 1)..]; if (bindingText.Contains(':')) return CompletionContext.BindingFormat; if (bindingText.Contains('.')) return CompletionContext.BindingProperty; return CompletionContext.Binding; } // Check for style context var hashCount = textBefore.Count(c => c == '#'); if (hashCount % 2 == 1) return CompletionContext.Style; // Check for control flow if (Regex.IsMatch(textBefore, @"^\s*@\w*$")) return CompletionContext.ControlFlow; // Check for foreach collection if (Regex.IsMatch(textBefore, @"@foreach\s+\w+\s+in\s+\w*$")) return CompletionContext.ForeachCollection; return CompletionContext.None; } } ``` ### 6.2 Schema Extraction (SchemaService.cs) ```csharp public abstract record SchemaNode(string Name); public record ObjectNode(string Name, List Properties) : SchemaNode(Name); public record ArrayNode(string Name, SchemaNode? ItemType) : SchemaNode(Name); public record ValueNode(string Name, JsonValueKind ValueKind) : SchemaNode(Name); public class SchemaService { private readonly Dictionary _schemaCache = new(); public SchemaNode ExtractSchema(string jsonContent) { using var doc = JsonDocument.Parse(jsonContent); return AnalyzeElement("root", doc.RootElement); } private SchemaNode AnalyzeElement(string name, JsonElement element) { return element.ValueKind switch { JsonValueKind.Object => new ObjectNode(name, element.EnumerateObject() .Select(p => AnalyzeElement(p.Name, p.Value)) .ToList()), JsonValueKind.Array => new ArrayNode(name, element.GetArrayLength() > 0 ? AnalyzeElement("item", element[0]) : null), _ => new ValueNode(name, element.ValueKind) }; } public SchemaNode? ResolvePropertyPath(SchemaNode root, string path) { var parts = path.Split('.'); var current = root; foreach (var part in parts) { current = current switch { ObjectNode obj => obj.Properties.FirstOrDefault(p => p.Name == part), ArrayNode arr => arr.ItemType, _ => null }; if (current == null) return null; } return current; } } ``` ### 6.3 Scope Tracking (ScopeTracker.cs) Track active `@foreach` loops to resolve loop variable bindings: ```csharp public record LoopScope( string VariableName, string CollectionPath, SchemaNode? ItemSchema, int StartLine, int EndLine); public class ScopeTracker { private readonly SchemaService _schemaService; public List GetActiveScopes(string documentContent, int line, SchemaNode? rootSchema) { var lines = documentContent.Split('\n'); var stack = new Stack<(string Variable, string Collection, int Line)>(); var scopes = new List(); for (int i = 0; i <= line && i < lines.Length; i++) { var currentLine = lines[i]; var foreachMatch = Regex.Match(currentLine, @"@foreach\s+(\w+)\s+in\s+(\S+)"); if (foreachMatch.Success) { stack.Push((foreachMatch.Groups[1].Value, foreachMatch.Groups[2].Value, i)); } else if (Regex.IsMatch(currentLine, @"@end\b") && stack.Count > 0) { // Only pop if this @end is before or at the cursor line if (i < line) { stack.Pop(); } } } // Convert stack to scopes with schema info foreach (var (variable, collection, startLine) in stack) { var collectionSchema = rootSchema != null ? _schemaService.ResolvePropertyPath(rootSchema, collection) : null; var itemSchema = collectionSchema is ArrayNode arr ? arr.ItemType : null; scopes.Add(new LoopScope(variable, collection, itemSchema, startLine, -1)); } return scopes; } public List GetLoopVariableCompletions(List scopes) { var items = new List(); foreach (var scope in scopes) { // Add the loop variable itself items.Add(new CompletionItem { Label = scope.VariableName, Kind = CompletionItemKind.Variable, Detail = $"Loop variable from @foreach {scope.VariableName} in {scope.CollectionPath}" }); // Add loop metadata items.AddRange(new[] { new CompletionItem { Label = $"{scope.VariableName}._index", Kind = CompletionItemKind.Property, Detail = "0-based index" }, new CompletionItem { Label = $"{scope.VariableName}._number", Kind = CompletionItemKind.Property, Detail = "1-based number" }, new CompletionItem { Label = $"{scope.VariableName}._first", Kind = CompletionItemKind.Property, Detail = "true if first iteration" }, new CompletionItem { Label = $"{scope.VariableName}._last", Kind = CompletionItemKind.Property, Detail = "true if last iteration" } }); // Add item properties if schema is available if (scope.ItemSchema is ObjectNode obj) { foreach (var prop in obj.Properties) { items.Add(new CompletionItem { Label = $"{scope.VariableName}.{prop.Name}", Kind = CompletionItemKind.Property, Detail = GetPropertyTypeDescription(prop) }); } } } return items; } } ``` ### 6.4 Semantic Tokens (SemanticTokenService.cs) ```csharp public class SemanticTokenService { // Token types must match SemanticTokensLegend sent during initialization private static readonly string[] TokenTypes = new[] { "keyword", // 0: @if, @foreach, @end, @row "variable", // 1: {binding} "property", // 2: .property "decorator", // 3: #style# "string", // 4: style names "number", // 5: ratios "operator", // 6: |, >, ^, == "comment", // 7: separators "type" // 8: format specifiers }; public SemanticTokens GetSemanticTokens(string content) { var tokens = new List(); // [deltaLine, deltaStart, length, tokenType, tokenModifiers] var lexer = new Lexer(content); var allTokens = lexer.Tokenize(); int prevLine = 0; int prevChar = 0; foreach (var token in allTokens) { var tokenType = MapTokenType(token.Type); if (tokenType < 0) continue; var deltaLine = token.Line - 1 - prevLine; var deltaStart = deltaLine == 0 ? token.Column - 1 - prevChar : token.Column - 1; tokens.Add(deltaLine); tokens.Add(deltaStart); tokens.Add(token.Length); tokens.Add(tokenType); tokens.Add(0); // No modifiers prevLine = token.Line - 1; prevChar = token.Column - 1; } return new SemanticTokens { Data = tokens.ToImmutableArray() }; } private int MapTokenType(TokenType type) => type switch { TokenType.If or TokenType.Foreach or TokenType.End or TokenType.Row or TokenType.EndRow => 0, // keyword TokenType.Binding => 1, // variable TokenType.PropertyPath => 2, // property TokenType.StyleMarker => 3, // decorator TokenType.StyleName => 4, // string TokenType.Ratio => 5, // number TokenType.Operator or TokenType.ColumnDelimiter or TokenType.AlignmentMarker => 6, // operator TokenType.Separator => 7, // comment TokenType.FormatSpecifier => 8, // type _ => -1 // skip }; } ``` --- ## 7. Commands and Keybindings ### Commands (package.json) ```json { "contributes": { "commands": [ { "command": "rtl.openPreview", "title": "Open Preview", "category": "RTL", "icon": "$(open-preview)" }, { "command": "rtl.openPreviewToSide", "title": "Open Preview to the Side", "category": "RTL", "icon": "$(open-preview)" }, { "command": "rtl.refreshPreview", "title": "Refresh Preview", "category": "RTL" }, { "command": "rtl.selectSampleData", "title": "Select Sample Data", "category": "RTL" }, { "command": "rtl.editSampleData", "title": "Edit Sample Data", "category": "RTL" }, { "command": "rtl.editAssignments", "title": "Edit Assignments", "category": "RTL" }, { "command": "rtl.newTemplate", "title": "New Template", "category": "RTL", "icon": "$(new-file)" }, { "command": "rtl.duplicateTemplate", "title": "Duplicate Template", "category": "RTL" }, { "command": "rtl.restartServer", "title": "Restart Language Server", "category": "RTL" } ] } } ``` ### Keybindings | Shortcut | Command | When | |----------|---------|------| | `Ctrl+Shift+V` | `rtl.openPreview` | `editorLangId == rtl` | | `Ctrl+K V` | `rtl.openPreviewToSide` | `editorLangId == rtl` | | `Ctrl+Shift+R` | `rtl.refreshPreview` | `editorLangId == rtl` | **Note:** Standard VS Code shortcuts work automatically: - `Ctrl+S` - Save - `Ctrl+Space` - Trigger autocomplete - `F8` / `Shift+F8` - Navigate problems - `Ctrl+G` - Go to line - `Ctrl+F` / `Ctrl+H` - Find / Replace --- ## 8. Configuration ### Extension Settings (contributes.configuration) ```json { "contributes": { "configuration": { "title": "RTL - Receipt Template Language", "properties": { "rtl.templatesDirectory": { "type": "string", "default": "./templates", "description": "Path to templates directory" }, "rtl.samplesDirectory": { "type": "string", "default": "./templates/samples", "description": "Path to sample data directory" }, "rtl.defaultPrinterProfile": { "type": "string", "enum": ["tm-t30iii", "tm-u220ii"], "default": "tm-t30iii", "description": "Default printer profile for preview" }, "rtl.preview.debounceMs": { "type": "number", "default": 300, "description": "Delay before updating preview after changes" }, "rtl.server.path": { "type": "string", "default": "", "description": "Path to language server executable (uses bundled server if empty)" }, "rtl.trace.server": { "type": "string", "enum": ["off", "messages", "verbose"], "default": "off", "description": "Traces communication between VS Code and the language server" } } } } } ``` ### Workspace Settings Example ```json // .vscode/settings.json { "rtl.templatesDirectory": "./templates", "rtl.samplesDirectory": "./templates/samples", "rtl.defaultPrinterProfile": "tm-t30iii" } ``` --- ## 9. Dependencies ### 9.1 Language Server (C#) **Inspectron.Epson.Templates.LanguageServer.csproj:** ```xml Exe net8.0 enable enable true true ``` ### 9.2 VS Code Extension (TypeScript) **package.json:** ```json { "name": "vscode-rtl", "displayName": "RTL - Receipt Template Language", "description": "Syntax highlighting, autocomplete, and preview for Receipt Template Language", "version": "1.0.0", "publisher": "inspectron", "engines": { "vscode": "^1.85.0" }, "categories": ["Programming Languages", "Formatters"], "activationEvents": [ "onLanguage:rtl", "workspaceContains:**/*.template" ], "main": "./dist/extension.js", "dependencies": { "vscode-languageclient": "^9.0.0" }, "devDependencies": { "@types/vscode": "^1.85.0", "@types/node": "^20.x", "typescript": "^5.3.0", "webpack": "^5.x", "webpack-cli": "^5.x", "ts-loader": "^9.x", "@vscode/test-electron": "^2.x" }, "scripts": { "vscode:prepublish": "npm run package && npm run build-server", "compile": "webpack", "watch": "webpack --watch", "package": "webpack --mode production --devtool hidden-source-map", "build-server": "dotnet publish ../Inspectron.Epson.Templates.LanguageServer -c Release -o ./server", "test": "node ./out/test/runTest.js" } } ``` --- ## 10. Testing ### 10.1 Language Server Tests (C# - xUnit) ``` Inspectron.Epson.Templates.LanguageServer.Tests/ ├── Handlers/ │ ├── SemanticTokensHandlerTests.cs │ ├── CompletionHandlerTests.cs │ ├── DiagnosticHandlerTests.cs │ └── HoverHandlerTests.cs ├── Services/ │ ├── CompletionServiceTests.cs │ ├── SchemaServiceTests.cs │ ├── ScopeTrackerTests.cs │ └── SemanticTokenServiceTests.cs └── Fixtures/ ├── valid-template.template ├── invalid-template.template └── sample-data.json ``` ### Test Cases **1. Semantic Tokens Tests** - Tokenize style markers `#bold,center#` → decorator + string - Tokenize bindings `{property.path}` → variable + property - Tokenize control flow → keyword - Tokenize separators → comment - Tokenize row syntax `@row 60:40` → keyword + number **2. Completion Tests** - Complete styles after `#` - Complete properties after `{` - Complete child properties after `.` - Complete format specifiers after `:` - Complete loop variables in scope - Complete loop metadata (`_index`, `_first`, etc.) **3. Diagnostics Tests** - Error on unclosed `@foreach` - Error on unclosed `{` binding - Warning on unused loop variable - Error on invalid style name **4. Schema Tests** - Extract schema from nested JSON - Resolve property paths - Handle arrays correctly **5. Scope Tracker Tests** - Track single @foreach scope - Track nested @foreach scopes - Close scope after @end ### 10.2 VS Code Extension Tests (TypeScript) ``` vscode-rtl/test/ ├── extension.test.ts # Extension activation tests ├── languageClient.test.ts # LSP client connection tests └── fixtures/ └── test-workspace/ ├── test.template └── test.json ``` ### Manual Test Checklist - [ ] Install extension in VS Code - [ ] Open `.template` file - syntax highlighting works (semantic tokens) - [ ] Type `@` at line start - autocomplete shows control flow - [ ] Type `{` - autocomplete shows data properties - [ ] Type `{item.` inside foreach - autocomplete shows item properties + `_index`, `_first`, etc. - [ ] Add syntax error - red squiggle appears, Problems panel updates - [ ] Fix error - squiggle disappears - [ ] Hover over style - shows description - [ ] Hover over binding - shows type from schema - [ ] Open preview (`Ctrl+Shift+V`) - receipt preview appears - [ ] Edit template - preview updates live - [ ] Change printer in preview - width adjusts - [ ] Open assignments editor - table displays correctly - [ ] Add/edit/delete assignment - saves to JSON - [ ] Restart Language Server command works --- ## 11. Distribution ### Build Process ```bash # 1. Build language server for all platforms dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r win-x64 -o vscode-rtl/server/win-x64 dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r linux-x64 -o vscode-rtl/server/linux-x64 dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r osx-x64 -o vscode-rtl/server/osx-x64 # 2. Build VS Code extension cd vscode-rtl npm install npm run package # 3. Create VSIX npx @vscode/vsce package # Creates: vscode-rtl-1.0.0.vsix ``` ### VSIX Contents ``` vscode-rtl-1.0.0.vsix ├── extension/ │ ├── dist/ │ │ └── extension.js # Bundled TypeScript │ ├── server/ │ │ ├── win-x64/ # Windows language server │ │ ├── linux-x64/ # Linux language server │ │ └── osx-x64/ # macOS language server │ ├── media/ │ ├── snippets/ │ ├── language-configuration.json │ └── package.json └── [Content_Types].xml ``` ### Installation Options 1. **Local VSIX install:** ```bash code --install-extension vscode-rtl-1.0.0.vsix ``` 2. **VS Code Marketplace:** (optional, for public distribution) ```bash npx @vscode/vsce publish ``` 3. **Manual installation:** - Copy extracted folder to: - Windows: `%USERPROFILE%\.vscode\extensions\` - macOS/Linux: `~/.vscode/extensions/` --- ## 12. Timeline ### Phase 1: Core Engine (Foundation) - C# 1. Project setup and structure 2. Lexer implementation + tests 3. Parser implementation + tests 4. Interpreter implementation + tests 5. Configuration and resolver 6. File storage 7. Factory integration 8. Default templates 9. Integration testing 10. Migration and deployment ### Phase 2: VS Code Extension + Language Server (After Phase 1 Complete) **Language Server (C#):** 1. Project setup with OmniSharp LSP 2. Server initialization and stdio transport 3. TextDocumentSyncHandler (open/change/close) 4. SemanticTokensHandler (syntax highlighting) 5. DiagnosticService + PublishDiagnostics 6. FoldingRangeHandler 7. HoverHandler 8. CompletionHandler - styles and control flow 9. SchemaService and ScopeTracker 10. CompletionHandler - data bindings with scope awareness 11. Custom request: rtl/renderPreview 12. Custom request: rtl/getSchema, rtl/listSamples 13. Unit tests **VS Code Extension (TypeScript):** 14. Extension project setup (package.json, webpack) 15. Language client setup and server spawning 16. Language registration and configuration 17. Preview webview - calls rtl/renderPreview 18. Preview webview - printer selector, live updates 19. Tree views (templates, samples) - optional 20. Assignments editor webview - optional 21. Code snippets 22. Integration testing 23. Multi-platform build and VSIX packaging 24. Documentation and distribution