1749 lines
64 KiB
Markdown
1749 lines
64 KiB
Markdown
# 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);
|
|
|
|
/// <summary>
|
|
/// Renders a template with the given data.
|
|
/// </summary>
|
|
public List<PrintCommand> Render(
|
|
string templateContent,
|
|
string jsonData,
|
|
PrinterProfile profile);
|
|
|
|
/// <summary>
|
|
/// Parses template and returns errors/warnings (for validation).
|
|
/// </summary>
|
|
public TemplateValidationResult Validate(string templateContent);
|
|
}
|
|
|
|
public class TemplateValidationResult
|
|
{
|
|
public bool IsValid { get; }
|
|
public List<TemplateError> Errors { get; }
|
|
public List<TemplateWarning> 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);
|
|
|
|
/// <summary>
|
|
/// Creates a converter for the given receipt type and printer.
|
|
/// Falls back to fallback template if no match found.
|
|
/// </summary>
|
|
public IReceiptConverter Create(int receiptType, byte printerId);
|
|
|
|
/// <summary>
|
|
/// Reloads templates and assignments from disk.
|
|
/// </summary>
|
|
public void Reload();
|
|
}
|
|
```
|
|
|
|
### 6.3 Integration Example
|
|
|
|
```csharp
|
|
// In EpsonPrintService startup
|
|
var converterFactory = new TemplateReceiptConverterFactory("./templates");
|
|
|
|
// In DI registration (Ninject)
|
|
kernel.Bind<IReceiptConverterFactory>()
|
|
.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<Container<FoldingRange>?> Handle(FoldingRangeRequestParam request, CancellationToken ct)
|
|
{
|
|
var ranges = new List<FoldingRange>();
|
|
// Parse document, find @if...@end, @foreach...@end, @row...@endrow blocks
|
|
// Add FoldingRange for each block
|
|
return Task.FromResult<Container<FoldingRange>?>(new Container<FoldingRange>(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<CompletionItem> 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<CompletionItem>()
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### 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<Diagnostic>(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<Hover?> Handle(HoverParams request, CancellationToken ct)
|
|
{
|
|
var content = GetHoverContent(request.TextDocument.Uri, request.Position);
|
|
if (content == null) return Task.FromResult<Hover?>(null);
|
|
|
|
return Task.FromResult<Hover?>(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<RenderPreviewParams, RenderPreviewResult>
|
|
{
|
|
public Task<RenderPreviewResult> 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<RenderPreviewResult>('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<string> 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<SchemaNode> 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<string, SchemaNode> _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<LoopScope> 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<LoopScope>();
|
|
|
|
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<CompletionItem> GetLoopVariableCompletions(List<LoopScope> scopes)
|
|
{
|
|
var items = new List<CompletionItem>();
|
|
|
|
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<int>(); // [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
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<OutputType>Exe</OutputType>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
<Nullable>enable</Nullable>
|
|
<PublishSingleFile>true</PublishSingleFile>
|
|
<SelfContained>true</SelfContained>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<!-- OmniSharp Language Server Protocol -->
|
|
<PackageReference Include="OmniSharp.Extensions.LanguageServer" Version="0.19.*" />
|
|
<PackageReference Include="OmniSharp.Extensions.LanguageProtocol" Version="0.19.*" />
|
|
|
|
<!-- Project Reference to Phase 1 -->
|
|
<ProjectReference Include="../Inspectron.Epson.Templates/Inspectron.Epson.Templates.csproj" />
|
|
</ItemGroup>
|
|
</Project>
|
|
```
|
|
|
|
### 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
|