editor first run
This commit is contained in:
@@ -7,7 +7,14 @@
|
||||
"Bash(tree:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(dotnet test:*)"
|
||||
"Bash(dotnet test:*)",
|
||||
"Bash(dotnet restore:*)",
|
||||
"Bash(dotnet workload:*)",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:github.com)",
|
||||
"Bash(curl:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
166
EpsonTest.Templates/Templates/FinalReceipt.json
Normal file
166
EpsonTest.Templates/Templates/FinalReceipt.json
Normal file
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"CompanyName": "Klingler Gastro AG",
|
||||
"Address1": "M\u00FCnzplatz 3",
|
||||
"Address2": "CH-8001 Z\u00FCrich",
|
||||
"Phone": "043 321 22 22",
|
||||
"ReceiptNumber": null,
|
||||
"DateTime": "2025-12-16T14:58:00",
|
||||
"Guests": 2,
|
||||
"Items": [
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "San Pellegrino 50cl",
|
||||
"UnitPrice": 6.50,
|
||||
"TotalPrice": 6.50,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "6.50 6.50 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "Panna 50cl",
|
||||
"UnitPrice": 6.50,
|
||||
"TotalPrice": 6.50,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "6.50 6.50 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "An item with really long description that should wrap",
|
||||
"UnitPrice": 5.50,
|
||||
"TotalPrice": 5.50,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "5.50 5.50 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Business Lunch Menu Seco",
|
||||
"UnitPrice": 44.00,
|
||||
"TotalPrice": 88.00,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": [
|
||||
"Tomato Soup",
|
||||
"Grilled Salmon",
|
||||
"Tiramisu"
|
||||
],
|
||||
"PriceDisplay": "44.00 88.00 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Brunello di Montalcino 1",
|
||||
"UnitPrice": 16.00,
|
||||
"TotalPrice": 32.00,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "16.00 32.00 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 2,
|
||||
"Description": "Espresso",
|
||||
"UnitPrice": 5.50,
|
||||
"TotalPrice": 11.00,
|
||||
"TaxCategory": "A",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "5.50 11.00 A"
|
||||
},
|
||||
{
|
||||
"Quantity": 1,
|
||||
"Description": "Tip",
|
||||
"UnitPrice": 10.50,
|
||||
"TotalPrice": 10.50,
|
||||
"TaxCategory": "B",
|
||||
"SubItems": null,
|
||||
"PriceDisplay": "10.50 10.50 B"
|
||||
}
|
||||
],
|
||||
"Total": 160.00,
|
||||
"Currency": "CHF",
|
||||
"TotalInAlternateCurrency": 172.80,
|
||||
"AlternateCurrency": "EUR",
|
||||
"SplitPayments": [
|
||||
{
|
||||
"PaymentMethod": "Cash",
|
||||
"Amount": 50.00,
|
||||
"Currency": "CHF"
|
||||
},
|
||||
{
|
||||
"PaymentMethod": "Card",
|
||||
"Amount": 110.00,
|
||||
"Currency": "CHF"
|
||||
}
|
||||
],
|
||||
"PaymentMethod": "MASTER",
|
||||
"PaymentAmount": 160.00,
|
||||
"TaxBreakdown": [
|
||||
{
|
||||
"Category": "A",
|
||||
"Rate": 8.1,
|
||||
"Gross": 149.50,
|
||||
"Net": 138.30,
|
||||
"TaxAmount": 11.20,
|
||||
"Currency": "CHF"
|
||||
},
|
||||
{
|
||||
"Category": "B",
|
||||
"Rate": 0,
|
||||
"Gross": 10.50,
|
||||
"Net": 10.50,
|
||||
"TaxAmount": 0.00,
|
||||
"Currency": "CHF"
|
||||
}
|
||||
],
|
||||
"IsDebtor": true,
|
||||
"WaiterName": "Yves",
|
||||
"Terminal": "Hauptkasse ZH",
|
||||
"TableNumber": "12",
|
||||
"VatNumber": "CHE-449.635.880 MWST",
|
||||
"ThankYouMessage": "Das Team bedankt sich herzlich f\u00FCr Ihren",
|
||||
"GoodbyeMessageLine1": "Besuch.",
|
||||
"GoodbyeMessageLine2": "Auf Wiedersehen.",
|
||||
"TerminalReceipts": [
|
||||
{
|
||||
"ReceiptType": "*** Kundenbeleg ***",
|
||||
"BookingType": "Buchung",
|
||||
"PaymentSystem": "TWINT",
|
||||
"TransactionNumber": "XXXXXXXXXXXXXXX1494",
|
||||
"TransactionDateTime": "2025-11-11T12:34:49",
|
||||
"TerminalId": "31108834",
|
||||
"AID": "A0000015749E",
|
||||
"TransactionSeqCount": "6127",
|
||||
"TransactionRefNo": "99036644599",
|
||||
"AuthCode": "0d3396",
|
||||
"AcquirerId": "2",
|
||||
"EftAmount": 67.00,
|
||||
"TipAmount": 6.70,
|
||||
"TotalEftAmount": 73.70,
|
||||
"Currency": "CHF"
|
||||
},
|
||||
{
|
||||
"ReceiptType": "*** Kundenbeleg ***",
|
||||
"BookingType": "Buchung",
|
||||
"PaymentSystem": "MASTER",
|
||||
"TransactionNumber": "XXXXXXXXXXXXXXX2587",
|
||||
"TransactionDateTime": "2025-11-11T12:36:12",
|
||||
"TerminalId": "31108834",
|
||||
"AID": "A0000000041010",
|
||||
"TransactionSeqCount": "6128",
|
||||
"TransactionRefNo": "99036644600",
|
||||
"AuthCode": "1a4b2c",
|
||||
"AcquirerId": "1",
|
||||
"EftAmount": 93.00,
|
||||
"TipAmount": 0.00,
|
||||
"TotalEftAmount": 93.00,
|
||||
"Currency": "CHF"
|
||||
}
|
||||
],
|
||||
"DiscountInfo": {
|
||||
"Description": "Fine Dine",
|
||||
"Amount": -50.00,
|
||||
"Currency": "CHF"
|
||||
},
|
||||
"ReceiptLabel": "",
|
||||
"HasSplitPayments": true,
|
||||
"HasTaxableCategories": true
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
<Solution>
|
||||
<Folder Name="/Utils/">
|
||||
<Project Path="EpsonTest/EpsonTest.csproj" />
|
||||
<Project Path="EpsonTest.Templates/EpsonTest.Templates.csproj" />
|
||||
<Project Path="EpsonTest/EpsonTest.csproj" />
|
||||
<Project Path="Inspectron.PrintServer.TemplateEditor/Inspectron.PrintServer.TemplateEditor.csproj" Id="253ec869-47ac-41d4-985a-5923006ef449" />
|
||||
</Folder>
|
||||
<Project Path="EpsonPrintService/EpsonPrintService.csproj" />
|
||||
<Project Path="Inspectron.Epson/Inspectron.Epson.csproj" />
|
||||
<Project Path="Inspectron.Epson.TemplateEngine/Inspectron.Epson.TemplateEngine.csproj" />
|
||||
<Project Path="Inspectron.Epson.TemplateEngine.Tests/Inspectron.Epson.TemplateEngine.Tests.csproj" />
|
||||
<Project Path="Inspectron.Epson.TemplateEngine/Inspectron.Epson.TemplateEngine.csproj" />
|
||||
<Project Path="Inspectron.Epson/Inspectron.Epson.csproj" />
|
||||
</Solution>
|
||||
|
||||
652
Inspectron.Epson/HtmlPrintBuilder.cs
Normal file
652
Inspectron.Epson/HtmlPrintBuilder.cs
Normal file
@@ -0,0 +1,652 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Inspectron.Epson;
|
||||
|
||||
/// <summary>
|
||||
/// HTML-based printer implementation that builds HTML in memory,
|
||||
/// simulating Epson thermal printer output for testing and preview purposes.
|
||||
/// No file I/O is performed; call <see cref="GetHtml"/> to retrieve the result.
|
||||
/// </summary>
|
||||
public class HtmlPrintBuilder : IEpsonPrinter
|
||||
{
|
||||
private readonly ILogger? _logger;
|
||||
private readonly byte _id;
|
||||
private readonly int _paperWidth;
|
||||
|
||||
private bool _isConnected;
|
||||
private readonly StringBuilder _content = new();
|
||||
|
||||
// Current styling state
|
||||
private EpsonCommands.PrinterFont _currentFont = EpsonCommands.PrinterFont.A;
|
||||
private int _fontWidthMultiplier = 1;
|
||||
private int _fontHeightMultiplier = 1;
|
||||
private bool _isEmphasized;
|
||||
private bool _isRedColor;
|
||||
private bool _isQuadrupleMode;
|
||||
private bool _isBiggerFontWidthTM220;
|
||||
private bool _isBiggerFontHeightTM220;
|
||||
private int _lineSpacing = 14;
|
||||
private int _absolutePosition;
|
||||
|
||||
// Loaded image buffer
|
||||
private Image<Rgba32>? _loadedImage;
|
||||
private int _loadedImageMaxWidth;
|
||||
|
||||
private static readonly Dictionary<EpsonCommands.PrinterFont, (int Width, int Height)> FontSizes = new()
|
||||
{
|
||||
{ EpsonCommands.PrinterFont.A, (6, 12) },
|
||||
{ EpsonCommands.PrinterFont.B, (5, 9) },
|
||||
{ EpsonCommands.PrinterFont.C, (5, 12) },
|
||||
{ EpsonCommands.PrinterFont.D, (5, 12) },
|
||||
{ EpsonCommands.PrinterFont.E, (6, 12) }
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected => _isConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of HtmlPrintBuilder
|
||||
/// </summary>
|
||||
/// <param name="paperWidth">Paper width in pixels (default 384 for 80mm thermal printer)</param>
|
||||
/// <param name="logger">Optional logger for diagnostic output</param>
|
||||
/// <param name="id">Printer ID byte</param>
|
||||
public HtmlPrintBuilder(int paperWidth = 384, ILogger? logger = null, byte id = 0x01)
|
||||
{
|
||||
_paperWidth = paperWidth;
|
||||
_logger = logger;
|
||||
_id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the generated HTML string.
|
||||
/// </summary>
|
||||
public string GetHtml()
|
||||
{
|
||||
return GenerateHtml();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 10)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Connecting (paper width: {Width}px)", _paperWidth);
|
||||
|
||||
_content.Clear();
|
||||
ResetStylingState();
|
||||
_isConnected = true;
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Connected successfully");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Disconnect()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Disconnecting");
|
||||
|
||||
_loadedImage?.Dispose();
|
||||
_loadedImage = null;
|
||||
_isConnected = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PrinterStatus> GetPrinterStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Returning mock printer status (always ready)");
|
||||
return Task.FromResult(new PrinterStatus
|
||||
{
|
||||
RawByte = 0x12,
|
||||
DrawerOpen = false,
|
||||
IsOnline = true,
|
||||
IsCoverClosed = true,
|
||||
PaperFeedButtonPressed = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<OfflineStatus> GetOfflineStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Returning mock offline status (no issues)");
|
||||
return Task.FromResult(new OfflineStatus
|
||||
{
|
||||
RawByte = 0x12,
|
||||
CoverOpen = false,
|
||||
PaperFeedButton = false,
|
||||
PaperEnd = false,
|
||||
ErrorOccurred = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ErrorStatus> GetErrorStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Returning mock error status (no errors)");
|
||||
return Task.FromResult(new ErrorStatus
|
||||
{
|
||||
RawByte = 0x12,
|
||||
RecoverableError = false,
|
||||
AutoCutterError = false,
|
||||
UnrecoverableError = false,
|
||||
AutoRecoveryError = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PaperSensorStatus> GetPaperSensorStatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Returning mock paper sensor status (paper present)");
|
||||
return Task.FromResult(new PaperSensorStatus
|
||||
{
|
||||
RawByte = 0x12,
|
||||
PaperNearEnd = false,
|
||||
PaperPresent = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<byte> GetTM220StatusAsync()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: Returning mock TM220 status");
|
||||
return Task.FromResult((byte)0x12);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<byte?> GetPrinterIdAsync()
|
||||
{
|
||||
return Task.FromResult<byte?>(_id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<OverallStatus> GetOverallStatusAsync()
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Returning mock overall status (ready)");
|
||||
return Task.FromResult(new OverallStatus
|
||||
{
|
||||
PrinterModel = "HtmlPrintBuilder",
|
||||
PrinterStatus = new PrinterStatus { RawByte = 0x12, IsOnline = true, IsCoverClosed = true },
|
||||
OfflineStatus = new OfflineStatus { RawByte = 0x12 },
|
||||
ErrorStatus = new ErrorStatus { RawByte = 0x12 },
|
||||
PaperStatus = new PaperSensorStatus { RawByte = 0x12, PaperPresent = true },
|
||||
StatusText = "READY - HTML Print Builder",
|
||||
StatusIcon = "🟢",
|
||||
IsReady = true,
|
||||
Recommendations = new List<string>()
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitAsync()
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Initializing (resetting styling state)");
|
||||
ResetStylingState();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetQuadrupleMode(bool enable)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: {Action} quadruple mode", enable ? "Enabling" : "Disabling");
|
||||
_isQuadrupleMode = enable;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetRedColor(bool enable)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: {Action} red color mode", enable ? "Enabling" : "Disabling");
|
||||
_isRedColor = enable;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetEmphasized(bool enable)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: {Action} emphasized mode", enable ? "Enabling" : "Disabling");
|
||||
_isEmphasized = enable;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SelectFont(EpsonCommands.PrinterFont font)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Selecting font {Font}", font);
|
||||
_currentFont = font;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetBiggerFontTM220(bool biggerWidth, bool biggerHeight = false, bool secondaryFont = true)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Setting bigger font mode: Width={BiggerWidth}, Height={BiggerHeight}, SecondaryFont={SecondaryFont}",
|
||||
biggerWidth, biggerHeight, secondaryFont);
|
||||
_isBiggerFontWidthTM220 = biggerWidth;
|
||||
_isBiggerFontHeightTM220 = biggerHeight;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PrintTextAsync(string text)
|
||||
{
|
||||
EnsureConnected();
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing text ({Length} characters)", text.Length);
|
||||
|
||||
var style = BuildCurrentStyle();
|
||||
var escapedText = EscapeHtml(text);
|
||||
|
||||
escapedText = escapedText.TrimEnd('\n').Replace("\n", "<br>");
|
||||
|
||||
var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : "";
|
||||
|
||||
_content.AppendLine($"<span style=\"{style}{positionStyle}\">{escapedText}</span>");
|
||||
|
||||
_absolutePosition = 0;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetFontSizeAsync(int width, int height)
|
||||
{
|
||||
if (width < 1 || width > 8 || height < 1 || height > 8)
|
||||
throw new ArgumentException("Size must be between 1 and 8");
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Setting font size to {Width}x{Height}", width, height);
|
||||
_fontWidthMultiplier = width;
|
||||
_fontHeightMultiplier = height;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PrintTextAndCutAsync(string text, int feedLines = 5)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing text with cut ({Length} characters, {FeedLines} feed lines)",
|
||||
text.Length, feedLines);
|
||||
|
||||
await InitAsync();
|
||||
await PrintTextAsync(text);
|
||||
await PrintTextAsync("\n");
|
||||
await FeedLinesAsync(feedLines);
|
||||
await CutAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PrintBuffer()
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: PrintBuffer (no-op)");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task FeedLinesAsync(int lines)
|
||||
{
|
||||
EnsureConnected();
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Feeding {Lines} lines", lines);
|
||||
|
||||
for (int i = 0; i < lines; i++)
|
||||
{
|
||||
_content.AppendLine($"<div style=\"height: {_lineSpacing}px;\"></div>");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CutAsync(bool fullCut = true)
|
||||
{
|
||||
EnsureConnected();
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Cutting paper");
|
||||
_content.AppendLine("<div class=\"cut\"><span class=\"scissors\">✂️</span></div>");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LoadImageAsync(string imagePath, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Loading image from file: {Path}", imagePath);
|
||||
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath);
|
||||
}
|
||||
|
||||
_loadedImage?.Dispose();
|
||||
_loadedImage = await Image.LoadAsync<Rgba32>(imagePath);
|
||||
_loadedImageMaxWidth = maxWidth;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task LoadImageAsync(Stream imageStream, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Loading image from stream");
|
||||
|
||||
_loadedImage?.Dispose();
|
||||
_loadedImage = await Image.LoadAsync<Rgba32>(imageStream);
|
||||
_loadedImageMaxWidth = maxWidth;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LoadImageAsync(Image<Rgba32> image, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Loading image from ImageSharp object");
|
||||
|
||||
_loadedImage?.Dispose();
|
||||
_loadedImage = image.Clone();
|
||||
_loadedImageMaxWidth = maxWidth;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PrintLoadedImage()
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
if (_loadedImage == null)
|
||||
{
|
||||
_logger?.LogWarning("HtmlPrintBuilder: No image loaded to print");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing loaded image");
|
||||
await RenderImageToHtml(_loadedImage, _loadedImageMaxWidth);
|
||||
|
||||
_loadedImage.Dispose();
|
||||
_loadedImage = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PrintImageBitModeAsync(string imagePath, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.SingleDensity8Dot, int maxWidth = 10)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) from file: {Path}", imagePath);
|
||||
|
||||
if (!File.Exists(imagePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath);
|
||||
}
|
||||
|
||||
using var image = await Image.LoadAsync<Rgba32>(imagePath);
|
||||
await PrintImageBitModeAsync(image, mode, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PrintImageBitModeAsync(Stream imageStream, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.DoubleDensity24Dot, int maxWidth = 384)
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) from stream");
|
||||
|
||||
using var image = await Image.LoadAsync<Rgba32>(imageStream);
|
||||
await PrintImageBitModeAsync(image, mode, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task PrintImageBitModeAsync(Image<Rgba32> image, EpsonCommands.BitImageMode mode, int maxWidth)
|
||||
{
|
||||
EnsureConnected();
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) {Width}x{Height}", image.Width, image.Height);
|
||||
|
||||
return RenderImageToHtml(image, maxWidth);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendRawCommandAsync(byte[] command)
|
||||
{
|
||||
_logger?.LogDebug("HtmlPrintBuilder: SendRawCommandAsync ignored ({Length} bytes)", command.Length);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetAbsolutePrintPosition(int x)
|
||||
{
|
||||
if (x < 0 || x > 65535)
|
||||
throw new ArgumentOutOfRangeException(nameof(x), "Position must be between 0 and 65535");
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Setting absolute print position to {X}", x);
|
||||
_absolutePosition = x;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetDefaultLineSpacing()
|
||||
{
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Setting default line spacing");
|
||||
_lineSpacing = 14;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetCustomLineSpacing(int spacing = 24)
|
||||
{
|
||||
if (spacing < 0 || spacing > 255)
|
||||
throw new ArgumentOutOfRangeException(nameof(spacing), "Spacing must be between 0 and 255");
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Setting custom line spacing to {Spacing}", spacing);
|
||||
_lineSpacing = (int)(spacing * 0.6);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Disconnect();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void ResetStylingState()
|
||||
{
|
||||
_currentFont = EpsonCommands.PrinterFont.A;
|
||||
_fontWidthMultiplier = 1;
|
||||
_fontHeightMultiplier = 1;
|
||||
_isEmphasized = false;
|
||||
_isRedColor = false;
|
||||
_isQuadrupleMode = false;
|
||||
_isBiggerFontWidthTM220 = false;
|
||||
_isBiggerFontHeightTM220 = false;
|
||||
_lineSpacing = 14;
|
||||
_absolutePosition = 0;
|
||||
}
|
||||
|
||||
private string BuildCurrentStyle()
|
||||
{
|
||||
var styles = new List<string>();
|
||||
|
||||
var (baseWidth, baseHeight) = FontSizes[_currentFont];
|
||||
|
||||
float effectiveWidthMultiplier = _fontWidthMultiplier;
|
||||
float effectiveHeightMultiplier = _fontHeightMultiplier;
|
||||
|
||||
if (_isQuadrupleMode)
|
||||
{
|
||||
effectiveWidthMultiplier *= 2;
|
||||
effectiveHeightMultiplier *= 2;
|
||||
}
|
||||
|
||||
if (_isBiggerFontWidthTM220)
|
||||
{
|
||||
effectiveWidthMultiplier *= 2f;
|
||||
}
|
||||
|
||||
if (_isBiggerFontHeightTM220)
|
||||
{
|
||||
effectiveHeightMultiplier *= 1.2f;
|
||||
}
|
||||
|
||||
int fontSize = (int)(baseHeight * effectiveHeightMultiplier);
|
||||
styles.Add($"font-size: {fontSize}px");
|
||||
|
||||
if (effectiveWidthMultiplier > 1)
|
||||
{
|
||||
int letterSpacing = (int)((effectiveWidthMultiplier) * baseWidth / 2);
|
||||
styles.Add($"letter-spacing: {letterSpacing}px");
|
||||
}
|
||||
|
||||
if (_isEmphasized)
|
||||
{
|
||||
styles.Add("font-weight: bold");
|
||||
}
|
||||
|
||||
if (_isRedColor)
|
||||
{
|
||||
styles.Add("color: #cc0000");
|
||||
}
|
||||
|
||||
styles.Add($"line-height: {_lineSpacing}px");
|
||||
|
||||
return string.Join("; ", styles) + ";";
|
||||
}
|
||||
|
||||
private async Task RenderImageToHtml(Image<Rgba32> sourceImage, int maxWidth)
|
||||
{
|
||||
using var processedImage = sourceImage.Clone();
|
||||
|
||||
if (processedImage.Width > maxWidth)
|
||||
{
|
||||
EpsonImageConverter.ResizeImage(processedImage, maxWidth);
|
||||
}
|
||||
|
||||
using var grayscaleImage = EpsonImageConverter.ConvertToGrayscale(processedImage);
|
||||
|
||||
EpsonImageConverter.ApplyFloydSteinbergDithering(grayscaleImage);
|
||||
|
||||
using var ditheredImage = new Image<Rgba32>(grayscaleImage.Width, grayscaleImage.Height);
|
||||
grayscaleImage.ProcessPixelRows(ditheredImage, (sourceAccessor, targetAccessor) =>
|
||||
{
|
||||
for (int y = 0; y < sourceAccessor.Height; y++)
|
||||
{
|
||||
var sourceRow = sourceAccessor.GetRowSpan(y);
|
||||
var targetRow = targetAccessor.GetRowSpan(y);
|
||||
|
||||
for (int x = 0; x < sourceAccessor.Width; x++)
|
||||
{
|
||||
byte value = sourceRow[x].PackedValue;
|
||||
targetRow[x] = new Rgba32(value, value, value, 255);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Embed image as base64 data URI instead of saving to file
|
||||
using var ms = new MemoryStream();
|
||||
await ditheredImage.SaveAsync(ms, new PngEncoder());
|
||||
var base64 = Convert.ToBase64String(ms.ToArray());
|
||||
var dataUri = $"data:image/png;base64,{base64}";
|
||||
|
||||
var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : "";
|
||||
_content.AppendLine($"<div class=\"image\" style=\"{positionStyle}\"><img src=\"{dataUri}\" alt=\"Printed image\"></div>");
|
||||
|
||||
_absolutePosition = 0;
|
||||
|
||||
_logger?.LogInformation("HtmlPrintBuilder: Image embedded as base64 ({Bytes} bytes)", ms.Length);
|
||||
}
|
||||
|
||||
private string GenerateHtml()
|
||||
{
|
||||
return $@"<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""UTF-8"">
|
||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||
<title>Epson Printer Output</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
background-color: #f0f0f0;
|
||||
padding: 20px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}}
|
||||
|
||||
.receipt {{
|
||||
width: {_paperWidth}px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}}
|
||||
|
||||
.receipt span {{
|
||||
display: inline;
|
||||
}}
|
||||
|
||||
.receipt .image {{
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
}}
|
||||
|
||||
.receipt .image img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}}
|
||||
|
||||
.cut {{
|
||||
position: relative;
|
||||
border-top: 2px dashed #333;
|
||||
margin: 20px 0;
|
||||
height: 0;
|
||||
}}
|
||||
|
||||
.cut .scissors {{
|
||||
position: absolute;
|
||||
left: -5px;
|
||||
top: -12px;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
padding: 0 3px;
|
||||
}}
|
||||
|
||||
/* Simulated paper texture */
|
||||
.receipt::before {{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
rgba(0,0,0,0.02) 1px,
|
||||
rgba(0,0,0,0.02) 2px
|
||||
);
|
||||
pointer-events: none;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""receipt"">
|
||||
{_content}
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
|
||||
private static string EscapeHtml(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("&", "&")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">")
|
||||
.Replace("\"", """)
|
||||
.Replace("'", "'");
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (!_isConnected)
|
||||
{
|
||||
throw new EpsonConnectionException("Not connected. Call ConnectAsync first.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -6,7 +6,6 @@ public interface IPrinter
|
||||
{
|
||||
public Task InitAsync();
|
||||
public Task PrintImageAsync(string path);
|
||||
public Task SetFontSizeAsync(int fontSize);
|
||||
public Task PrintAsync(List<PrintCommand> commands);
|
||||
public Task Cut();
|
||||
}
|
||||
17
Inspectron.PrintServer.TemplateEditor/App.xaml
Normal file
17
Inspectron.PrintServer.TemplateEditor/App.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Inspectron.PrintServer.TemplateEditor"
|
||||
x:Class="Inspectron.PrintServer.TemplateEditor.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
|
||||
<!--
|
||||
For information about styling .NET MAUI pages
|
||||
please refer to the documentation:
|
||||
https://go.microsoft.com/fwlink/?linkid=2282329
|
||||
-->
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
15
Inspectron.PrintServer.TemplateEditor/App.xaml.cs
Normal file
15
Inspectron.PrintServer.TemplateEditor/App.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Inspectron.PrintServer.TemplateEditor
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
return new Window(new MainPage()) { Title = "Inspectron.PrintServer.TemplateEditor" };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
@@ -0,0 +1,113 @@
|
||||
@page "/"
|
||||
@using BlazorMonaco
|
||||
@using BlazorMonaco.Editor
|
||||
@using BlazorMonaco.Languages
|
||||
@using Inspectron.PrintServer.TemplateEditor.Services
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn" title="New">New</button>
|
||||
<button class="toolbar-btn" title="Open">Open</button>
|
||||
<button class="toolbar-btn" title="Save">Save</button>
|
||||
<div class="toolbar-separator"></div>
|
||||
<button class="toolbar-btn" title="Load JSON data file for expression completions" @onclick="LoadJsonAsync">Load JSON</button>
|
||||
@if (!string.IsNullOrEmpty(_loadedJsonFile))
|
||||
{
|
||||
<span class="json-indicator" title="@_loadedJsonFile">@_loadedJsonFile</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="split-pane">
|
||||
<div class="pane pane-editor">
|
||||
<div class="pane-header">Editor</div>
|
||||
<StandaloneCodeEditor @ref="_editor" Id="my-editor-id"
|
||||
ConstructionOptions="EditorConstructionOptions"
|
||||
OnDidInit="OnEditorInitAsync"
|
||||
OnDidChangeModelContent="OnContentChanged" />
|
||||
</div>
|
||||
|
||||
<div class="pane pane-preview">
|
||||
<div class="pane-header">Preview</div>
|
||||
<div class="preview-content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private StandaloneCodeEditor _editor = null!;
|
||||
private string _editorContent = "";
|
||||
private string? _loadedJsonFile;
|
||||
private readonly TemplateCompletionProvider _completionProvider = new();
|
||||
|
||||
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
|
||||
{
|
||||
return new StandaloneEditorConstructionOptions
|
||||
{
|
||||
AutomaticLayout = true,
|
||||
Language = "receipt-xml",
|
||||
Theme = "receipt-dark"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnEditorInitAsync()
|
||||
{
|
||||
// Register custom language and theme
|
||||
await JSRuntime.InvokeVoidAsync("receiptLanguage.register");
|
||||
await JSRuntime.InvokeVoidAsync("receiptLanguage.defineTheme");
|
||||
|
||||
// Apply the theme now that it's defined
|
||||
await BlazorMonaco.Editor.Global.SetTheme(JSRuntime, "receipt-dark");
|
||||
|
||||
// Wire up content accessor for the completion provider
|
||||
_completionProvider.SetEditorContentAccessor(() => _editorContent);
|
||||
|
||||
// Register completion provider
|
||||
await BlazorMonaco.Languages.Global.RegisterCompletionItemProvider(
|
||||
JSRuntime,
|
||||
"receipt-xml",
|
||||
_completionProvider.ProvideCompletionItems,
|
||||
_completionProvider.ResolveCompletionItem);
|
||||
}
|
||||
|
||||
private async Task OnContentChanged(ModelContentChangedEvent e)
|
||||
{
|
||||
_editorContent = await _editor.GetValue();
|
||||
}
|
||||
|
||||
private async Task LoadJsonAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await FilePicker.Default.PickAsync(new PickOptions
|
||||
{
|
||||
PickerTitle = "Select a JSON data file",
|
||||
FileTypes = new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
|
||||
{
|
||||
{ DevicePlatform.WinUI, new[] { ".json" } },
|
||||
{ DevicePlatform.macOS, new[] { "json" } },
|
||||
{ DevicePlatform.iOS, new[] { "public.json" } },
|
||||
{ DevicePlatform.Android, new[] { "application/json" } },
|
||||
})
|
||||
});
|
||||
|
||||
if (result is null)
|
||||
return;
|
||||
|
||||
using var stream = await result.OpenReadAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
|
||||
var paths = JsonPathExtractor.Extract(json);
|
||||
_completionProvider.SetJsonPaths(paths);
|
||||
_loadedJsonFile = result.FileName;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// User cancelled or file read failed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(MauiProgram).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using Inspectron.PrintServer.TemplateEditor
|
||||
@using Inspectron.PrintServer.TemplateEditor.Components
|
||||
@using Inspectron.PrintServer.TemplateEditor.Components.Layout
|
||||
@@ -0,0 +1,63 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||
|
||||
<!-- Note for MacCatalyst:
|
||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
||||
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
|
||||
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
|
||||
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
|
||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
||||
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Inspectron.PrintServer.TemplateEditor</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnableDefaultCssItems>false</EnableDefaultCssItems>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>Inspectron.PrintServer.TemplateEditor</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.companyname.inspectron.printserver.templateeditor</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- App Icon -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||
|
||||
<!-- Splash Screen -->
|
||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
||||
|
||||
<!-- Images -->
|
||||
<MauiImage Include="Resources\Images\*" />
|
||||
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<MauiFont Include="Resources\Fonts\*" />
|
||||
|
||||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlazorMonaco" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
14
Inspectron.PrintServer.TemplateEditor/MainPage.xaml
Normal file
14
Inspectron.PrintServer.TemplateEditor/MainPage.xaml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Inspectron.PrintServer.TemplateEditor"
|
||||
xmlns:components="clr-namespace:Inspectron.PrintServer.TemplateEditor.Components"
|
||||
x:Class="Inspectron.PrintServer.TemplateEditor.MainPage">
|
||||
|
||||
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
|
||||
<BlazorWebView.RootComponents>
|
||||
<RootComponent Selector="#app" ComponentType="{x:Type components:Routes}" />
|
||||
</BlazorWebView.RootComponents>
|
||||
</BlazorWebView>
|
||||
|
||||
</ContentPage>
|
||||
10
Inspectron.PrintServer.TemplateEditor/MainPage.xaml.cs
Normal file
10
Inspectron.PrintServer.TemplateEditor/MainPage.xaml.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Inspectron.PrintServer.TemplateEditor
|
||||
{
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Inspectron.PrintServer.TemplateEditor/MauiProgram.cs
Normal file
27
Inspectron.PrintServer.TemplateEditor/MauiProgram.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Inspectron.PrintServer.TemplateEditor
|
||||
{
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.ConfigureFonts(fonts =>
|
||||
{
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
});
|
||||
|
||||
builder.Services.AddMauiBlazorWebView();
|
||||
|
||||
#if DEBUG
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
builder.Logging.AddDebug();
|
||||
#endif
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<maui:MauiWinUIApplication
|
||||
x:Class="Inspectron.PrintServer.TemplateEditor.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maui="using:Microsoft.Maui"
|
||||
xmlns:local="using:Inspectron.PrintServer.TemplateEditor.WinUI">
|
||||
|
||||
</maui:MauiWinUIApplication>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Inspectron.PrintServer.TemplateEditor.WinUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : MauiWinUIApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="A65AC8AF-0350-4DF9-840E-55DDB793477D" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>$placeholder$</DisplayName>
|
||||
<PublisherDisplayName>User Name</PublisherDisplayName>
|
||||
<Logo>$placeholder$.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate" />
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="$placeholder$"
|
||||
Description="$placeholder$"
|
||||
Square150x150Logo="$placeholder$.png"
|
||||
Square44x44Logo="$placeholder$.png"
|
||||
BackgroundColor="transparent">
|
||||
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
|
||||
<uap:SplashScreen Image="$placeholder$.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
|
||||
</Package>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Inspectron.PrintServer.TemplateEditor.WinUI.app"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M284.432 247.568L284.004 221.881C316.359 221.335 340.356 211.735 355.308 193.336C382.408 159.996 372.893 108.183 372.786 107.659L398.013 102.831C398.505 105.432 409.797 167.017 375.237 209.53C355.276 234.093 324.719 246.894 284.432 247.568Z" fill="#8A6FE8"/>
|
||||
<path d="M331.954 109.36L361.826 134.245C367.145 138.676 375.055 137.959 379.497 132.639C383.928 127.32 383.211 119.41 377.891 114.969L348.019 90.0842C342.7 85.6531 334.79 86.3702 330.348 91.6896C325.917 97.0197 326.634 104.929 331.954 109.36Z" fill="#8A6FE8"/>
|
||||
<path d="M407.175 118.062L417.92 94.2263C420.735 87.858 417.856 80.4087 411.488 77.5831C405.12 74.7682 397.67 77.6473 394.845 84.0156L383.831 108.461L407.175 118.062Z" fill="#8A6FE8"/>
|
||||
<path d="M401.363 105.175L401.234 69.117C401.181 62.1493 395.498 56.541 388.53 56.5945C381.562 56.648 375.954 62.3313 376.007 69.2989L376.018 96.11L401.363 105.175Z" fill="#8A6FE8"/>
|
||||
<path d="M386.453 109.071L378.137 73.9548C376.543 67.169 369.757 62.9628 362.971 64.5575C356.185 66.1523 351.979 72.938 353.574 79.7237L362.04 115.482L386.453 109.071Z" fill="#8A6FE8"/>
|
||||
<path d="M381.776 142.261C396.359 142.261 408.181 130.44 408.181 115.857C408.181 101.274 396.359 89.4527 381.776 89.4527C367.194 89.4527 355.372 101.274 355.372 115.857C355.372 130.44 367.194 142.261 381.776 142.261Z" fill="url(#paint0_radial)"/>
|
||||
<path d="M248.267 406.979C248.513 384.727 245.345 339.561 222.376 301.736L199.922 315.372C220.76 349.675 222.323 389.715 221.841 407.182C221.798 408.627 235.263 409.933 248.267 406.979Z" fill="url(#paint1_linear)"/>
|
||||
<path d="M221.841 406.936L242.637 406.84L262.052 518.065L220.311 518.258C217.132 518.269 214.724 515.711 214.938 512.532L221.841 406.936Z" fill="#522CD5"/>
|
||||
<path d="M306.566 488.814C310.173 491.661 310.109 495.782 309.831 500.127L308.964 513.452C308.803 515.839 306.727 517.798 304.34 517.809L260.832 518.012C258.125 518.023 256.08 515.839 256.262 513.142L256.551 499.335C256.883 494.315 255.192 492.474 251.307 487.744C244.649 479.663 224.967 435.62 226.84 406.925L248.256 406.829C249.691 423.858 272.167 461.682 306.566 488.814Z" fill="url(#paint2_linear)"/>
|
||||
<path d="M309.82 500.127C310.023 497.088 310.077 494.176 308.889 491.715L254.635 491.961C256.134 494.166 256.765 496.092 256.562 499.314L256.273 513.121C256.091 515.828 258.146 518.012 260.843 517.99L304.34 517.798C306.727 517.787 308.803 515.828 308.964 513.442L309.82 500.127Z" fill="url(#paint3_radial)"/>
|
||||
<path d="M133.552 407.471C133.103 385.22 135.864 340.021 158.49 301.993L181.073 315.425C160.545 349.921 159.346 389.972 159.989 407.428C160.042 408.884 146.578 410.318 133.552 407.471Z" fill="url(#paint4_linear)"/>
|
||||
<path d="M110.798 497.152C110.765 494.187 111.204 491.575 112.457 487.23C131.882 434.132 133.52 407.364 133.52 407.364L159.999 407.246C159.999 407.246 161.819 433.512 181.716 486.427C183.289 490.195 183.471 493.641 183.674 496.831L183.792 513.816C183.803 516.374 181.716 518.483 179.158 518.494L177.873 518.504L116.781 518.782L115.496 518.793C112.927 518.804 110.83 516.728 110.819 514.159L110.798 497.152Z" fill="url(#paint5_linear)"/>
|
||||
<path d="M110.798 497.152C110.798 496.67 110.808 496.199 110.83 495.739C110.969 494.262 111.643 492.603 114.875 492.582L180.207 492.282C182.561 492.367 183.343 494.176 183.589 495.311C183.621 495.814 183.664 496.328 183.696 496.82L183.813 513.806C183.824 515.411 183.011 516.824 181.769 517.669C181.031 518.172 180.132 518.472 179.179 518.483L177.895 518.494L116.802 518.772L115.528 518.782C114.244 518.793 113.077 518.269 112.232 517.434C111.386 516.599 110.862 515.432 110.851 514.148L110.798 497.152Z" fill="url(#paint6_radial)"/>
|
||||
<path d="M314.979 246.348C324.162 210.407 318.008 181.777 318.008 181.777L326.452 181.734L326.656 181.574C314.262 115.75 256.326 66.0987 186.949 66.4198C108.796 66.773 45.7233 130.424 46.0765 208.577C46.4297 286.731 110.08 349.803 188.234 349.45C249.905 349.172 302.178 309.474 321.304 254.343C321.872 251.999 321.797 247.804 314.979 246.348Z" fill="url(#paint7_radial)"/>
|
||||
<path d="M310.237 279.035L65.877 280.148C71.3998 289.428 77.95 298.012 85.3672 305.761L290.972 304.829C298.336 297.005 304.8 288.368 310.237 279.035Z" fill="#D8CFF7"/>
|
||||
<path d="M235.062 312.794L280.924 312.585L280.74 272.021L234.877 272.23L235.062 312.794Z" fill="#512BD4"/>
|
||||
<path d="M243.001 297.626C242.691 297.626 242.434 297.53 242.22 297.327C242.006 297.123 241.899 296.866 241.899 296.588C241.899 296.299 242.006 296.042 242.22 295.839C242.434 295.625 242.691 295.528 243.001 295.528C243.312 295.528 243.568 295.635 243.782 295.839C243.996 296.042 244.114 296.299 244.114 296.588C244.114 296.877 244.007 297.123 243.793 297.327C243.568 297.519 243.312 297.626 243.001 297.626Z" fill="white"/>
|
||||
<path d="M255.192 297.434H253.212L247.967 289.203C247.839 289 247.721 288.775 247.636 288.55H247.593C247.636 288.786 247.657 289.299 247.657 290.091L247.668 297.444H245.912L245.891 286.228H247.999L253.062 294.265C253.276 294.597 253.415 294.833 253.479 294.95H253.511C253.458 294.651 253.437 294.148 253.437 293.441L253.426 286.217H255.17L255.192 297.434Z" fill="white"/>
|
||||
<path d="M263.733 297.412L257.589 297.423L257.568 286.206L263.465 286.195V287.779L259.387 287.79L259.398 290.969L263.155 290.958V292.532L259.398 292.542L259.409 295.86L263.733 295.85V297.412Z" fill="white"/>
|
||||
<path d="M272.445 287.758L269.298 287.769L269.32 297.401H267.5L267.479 287.769L264.343 287.779V286.195L272.434 286.174L272.445 287.758Z" fill="white"/>
|
||||
<path d="M315.279 246.337C324.355 210.836 318.457 182.483 318.308 181.798L171.484 182.462C171.484 182.462 162.226 181.563 162.268 190.018C162.311 198.463 162.761 222.341 162.878 248.746C162.9 254.172 167.363 256.773 170.863 256.751C170.874 256.751 311.618 252.213 315.279 246.337Z" fill="url(#paint8_radial)"/>
|
||||
<path d="M227.685 246.798C227.685 246.798 250.183 228.827 254.571 225.499C258.959 222.17 262.812 221.977 266.869 225.445C270.925 228.913 293.616 246.498 293.616 246.498L227.685 246.798Z" fill="#A08BE8"/>
|
||||
<path d="M320.748 256.141C320.748 256.141 324.943 248.414 315.279 246.348C315.289 246.305 170.927 246.894 170.927 246.894C167.566 246.905 163.232 244.925 162.846 241.671C162.857 244.004 162.878 246.369 162.889 248.756C162.91 253.68 166.582 256.27 169.878 256.698C170.21 256.73 170.542 256.773 170.874 256.773L180.742 256.73L320.748 256.141Z" fill="#512BD4"/>
|
||||
<path d="M206.4 233.214C212.511 233.095 217.302 224.667 217.102 214.39C216.901 204.112 211.785 195.878 205.674 195.997C199.563 196.116 194.772 204.544 194.973 214.821C195.173 225.099 200.289 233.333 206.4 233.214Z" fill="#512BD4"/>
|
||||
<path d="M306.249 214.267C306.356 203.989 301.488 195.605 295.377 195.541C289.266 195.478 284.225 203.758 284.118 214.037C284.011 224.315 288.878 232.699 294.99 232.763C301.101 232.826 306.142 224.545 306.249 214.267Z" fill="#512BD4"/>
|
||||
<path d="M205.905 205.291C208.152 203.022 211.192 202.016 214.157 202.262C215.912 205.495 217.014 209.733 217.111 214.389C217.164 217.3 216.811 220.04 216.158 222.513C212.669 223.519 208.752 222.662 205.979 219.922C201.912 215.909 201.88 209.348 205.905 205.291Z" fill="#8065E0"/>
|
||||
<path d="M294.996 204.285C297.255 202.016 300.294 200.999 303.259 201.256C305.164 204.628 306.309 209.209 306.256 214.239C306.224 216.808 305.892 219.259 305.303 221.485C301.793 222.523 297.843 221.678 295.061 218.916C291.004 214.892 290.972 208.342 294.996 204.285Z" fill="#8065E0"/>
|
||||
<path d="M11.6342 357.017C10.9171 354.716 -5.72611 300.141 21.3204 258.903C36.9468 235.078 63.3083 221.035 99.6664 217.15L102.449 243.276C74.3431 246.273 54.4676 256.345 43.3579 273.202C23.0971 303.941 36.5722 348.733 36.7113 349.183L11.6342 357.017Z" fill="url(#paint9_linear)"/>
|
||||
<path d="M95.1498 252.802C109.502 252.802 121.137 241.167 121.137 226.815C121.137 212.463 109.502 200.828 95.1498 200.828C80.7976 200.828 69.1628 212.463 69.1628 226.815C69.1628 241.167 80.7976 252.802 95.1498 252.802Z" fill="url(#paint10_radial)"/>
|
||||
<path d="M72.0098 334.434L33.4683 329.307C26.597 328.397 20.2929 333.214 19.3725 340.085C18.4627 346.956 23.279 353.26 30.1504 354.181L68.6919 359.308C75.5632 360.217 81.8673 355.401 82.7878 348.53C83.6975 341.658 78.8705 335.344 72.0098 334.434Z" fill="#8A6FE8"/>
|
||||
<path d="M3.73535 367.185L7.35297 393.076C8.36975 399.968 14.7702 404.731 21.6629 403.725C28.5556 402.708 33.3185 396.308 32.3124 389.415L28.5984 362.861L3.73535 367.185Z" fill="#8A6FE8"/>
|
||||
<path d="M15.5194 374.988L34.849 405.427C38.6058 411.292 46.4082 413.005 52.2735 409.248C58.1387 405.491 59.8512 397.689 56.0945 391.823L41.7953 369.144L15.5194 374.988Z" fill="#8A6FE8"/>
|
||||
<path d="M26.0511 363.739L51.8026 389.019C56.7688 393.911 64.7532 393.846 69.6445 388.88C74.5358 383.914 74.4715 375.929 69.516 371.038L43.2937 345.297L26.0511 363.739Z" fill="#8A6FE8"/>
|
||||
<path d="M26.4043 381.912C40.987 381.912 52.8086 370.091 52.8086 355.508C52.8086 340.925 40.987 329.104 26.4043 329.104C11.8216 329.104 0 340.925 0 355.508C0 370.091 11.8216 381.912 26.4043 381.912Z" fill="url(#paint11_radial)"/>
|
||||
<path d="M184.73 63.6308L157.819 66.5892L158.561 38.5412L177.888 36.4178L184.73 63.6308Z" fill="#8A6FE8"/>
|
||||
<path d="M170.018 41.647C180.455 39.521 187.193 29.3363 185.067 18.8988C182.941 8.46126 172.757 1.72345 162.319 3.84944C151.882 5.97543 145.144 16.1601 147.27 26.5976C149.396 37.0351 159.58 43.773 170.018 41.647Z" fill="#D8CFF7"/>
|
||||
<path d="M196.885 79.385C198.102 79.2464 198.948 78.091 198.684 76.8997C195.851 64.2818 183.923 55.5375 170.773 56.9926C157.622 58.4371 147.886 69.5735 147.865 82.4995C147.863 83.7232 148.949 84.6597 150.168 84.5316L196.885 79.385Z" fill="url(#paint12_radial)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(382.004 103.457) scale(26.4058)">
|
||||
<stop stop-color="#8065E0"/>
|
||||
<stop offset="1" stop-color="#512BD4"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear" x1="214.439" y1="303.482" x2="236.702" y2="409.505" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#522CD5"/>
|
||||
<stop offset="0.4397" stop-color="#8A6FE8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear" x1="231.673" y1="404.144" x2="297.805" y2="522.048" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#522CD5"/>
|
||||
<stop offset="0.4397" stop-color="#8A6FE8"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(280.957 469.555) rotate(-0.260742) scale(45.8326)">
|
||||
<stop offset="0.034" stop-color="#522CD5"/>
|
||||
<stop offset="0.9955" stop-color="#8A6FE8"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint4_linear" x1="166.061" y1="303.491" x2="144.763" y2="409.709" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#522CD5"/>
|
||||
<stop offset="0.4397" stop-color="#8A6FE8"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear" x1="146.739" y1="407.302" x2="147.246" y2="518.627" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#522CD5"/>
|
||||
<stop offset="0.4397" stop-color="#8A6FE8"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(148.63 470.023) rotate(179.739) scale(50.2476)">
|
||||
<stop offset="0.034" stop-color="#522CD5"/>
|
||||
<stop offset="0.9955" stop-color="#8A6FE8"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(219.219 153.929) rotate(179.739) scale(140.935)">
|
||||
<stop offset="0.4744" stop-color="#A08BE8"/>
|
||||
<stop offset="0.8618" stop-color="#8065E0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(314.861 158.738) rotate(179.739) scale(146.053)">
|
||||
<stop offset="0.0933" stop-color="#E1DFDD"/>
|
||||
<stop offset="0.6573" stop-color="white"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint9_linear" x1="54.1846" y1="217.159" x2="54.1846" y2="357.022" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.3344" stop-color="#9780E6"/>
|
||||
<stop offset="0.8488" stop-color="#8A6FE8"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(90.3494 218.071) rotate(-0.260742) scale(25.9924)">
|
||||
<stop stop-color="#8065E0"/>
|
||||
<stop offset="1" stop-color="#512BD4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint11_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.805 345.043) scale(26.4106)">
|
||||
<stop stop-color="#8065E0"/>
|
||||
<stop offset="1" stop-color="#512BD4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint12_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(169.113 67.3662) rotate(-32.2025) scale(21.0773)">
|
||||
<stop stop-color="#8065E0"/>
|
||||
<stop offset="1" stop-color="#512BD4"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,15 @@
|
||||
Any raw assets you want to be deployed with your application can be placed in
|
||||
this directory (and child directories). Deployment of the asset to your application
|
||||
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
|
||||
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
|
||||
These files will be deployed with your package and will be accessible using Essentials:
|
||||
|
||||
async Task LoadMauiAsset()
|
||||
{
|
||||
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var contents = reader.ReadToEnd();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,94 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Inspectron.PrintServer.TemplateEditor.Services;
|
||||
|
||||
public record JsonPropertyInfo(string Path, string Type, bool IsArray);
|
||||
|
||||
public static class JsonPathExtractor
|
||||
{
|
||||
public static List<JsonPropertyInfo> Extract(string json)
|
||||
{
|
||||
var results = new List<JsonPropertyInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
WalkElement(doc.RootElement, "", results);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Invalid JSON - return empty list
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void WalkElement(JsonElement element, string prefix, List<JsonPropertyInfo> results)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
{
|
||||
var path = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}";
|
||||
|
||||
var type = GetTypeName(prop.Value.ValueKind);
|
||||
var isArray = prop.Value.ValueKind == JsonValueKind.Array;
|
||||
|
||||
results.Add(new JsonPropertyInfo(path, type, isArray));
|
||||
|
||||
if (prop.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
WalkElement(prop.Value, path, results);
|
||||
}
|
||||
else if (prop.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
WalkArrayElement(prop.Value, path, results);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void WalkArrayElement(JsonElement array, string arrayPath, List<JsonPropertyInfo> results)
|
||||
{
|
||||
// Inspect first element to discover item property paths
|
||||
foreach (var item in array.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
// Use "[item]" prefix to indicate these are array item properties
|
||||
foreach (var prop in item.EnumerateObject())
|
||||
{
|
||||
var itemPath = $"{arrayPath}[].{prop.Name}";
|
||||
var type = GetTypeName(prop.Value.ValueKind);
|
||||
var isArray = prop.Value.ValueKind == JsonValueKind.Array;
|
||||
|
||||
results.Add(new JsonPropertyInfo(itemPath, type, isArray));
|
||||
|
||||
if (prop.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
WalkElement(prop.Value, itemPath, results);
|
||||
}
|
||||
else if (prop.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
WalkArrayElement(prop.Value, itemPath, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break; // Only inspect the first element
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTypeName(JsonValueKind kind) => kind switch
|
||||
{
|
||||
JsonValueKind.String => "string",
|
||||
JsonValueKind.Number => "number",
|
||||
JsonValueKind.True or JsonValueKind.False => "boolean",
|
||||
JsonValueKind.Array => "array",
|
||||
JsonValueKind.Object => "object",
|
||||
JsonValueKind.Null => "null",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
using System.Text.Json;
|
||||
using BlazorMonaco;
|
||||
using BlazorMonaco.Languages;
|
||||
|
||||
namespace Inspectron.PrintServer.TemplateEditor.Services;
|
||||
|
||||
public class TemplateCompletionProvider
|
||||
{
|
||||
private List<JsonPropertyInfo> _jsonPaths = [];
|
||||
|
||||
public void SetJsonPaths(List<JsonPropertyInfo> paths) => _jsonPaths = paths;
|
||||
|
||||
public Task<CompletionList> ProvideCompletionItems(string modelUri, Position position, CompletionContext context)
|
||||
{
|
||||
var items = new List<CompletionItem>();
|
||||
var editorContent = _editorContentAccessor?.Invoke();
|
||||
|
||||
if (string.IsNullOrEmpty(editorContent))
|
||||
return Task.FromResult(new CompletionList { Suggestions = items });
|
||||
|
||||
var lines = editorContent.Split('\n');
|
||||
if (position.LineNumber < 1 || position.LineNumber > lines.Length)
|
||||
return Task.FromResult(new CompletionList { Suggestions = items });
|
||||
|
||||
var line = lines[position.LineNumber - 1];
|
||||
var col = Math.Min(position.Column - 1, line.Length);
|
||||
var textBefore = line[..col];
|
||||
|
||||
var completionContext = DetectContext(textBefore, lines, position.LineNumber);
|
||||
|
||||
switch (completionContext.Kind)
|
||||
{
|
||||
case ContextKind.ElementName:
|
||||
items = GetElementCompletions(completionContext.Prefix);
|
||||
break;
|
||||
case ContextKind.AttributeName:
|
||||
items = GetAttributeCompletions(completionContext.ElementName, completionContext.Prefix);
|
||||
break;
|
||||
case ContextKind.AttributeValue:
|
||||
items = GetAttributeValueCompletions(completionContext.ElementName, completionContext.AttributeName);
|
||||
break;
|
||||
case ContextKind.Expression:
|
||||
items = GetExpressionCompletions(completionContext.Prefix, lines, position.LineNumber);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.FromResult(new CompletionList { Suggestions = items });
|
||||
}
|
||||
|
||||
public Task<CompletionItem> ResolveCompletionItem(CompletionItem item) => Task.FromResult(item);
|
||||
|
||||
private Func<string>? _editorContentAccessor;
|
||||
|
||||
public void SetEditorContentAccessor(Func<string> accessor) => _editorContentAccessor = accessor;
|
||||
|
||||
#region Context Detection
|
||||
|
||||
private enum ContextKind { None, ElementName, AttributeName, AttributeValue, Expression }
|
||||
|
||||
private record CompletionContextInfo(ContextKind Kind, string Prefix = "", string ElementName = "", string AttributeName = "");
|
||||
|
||||
private static CompletionContextInfo DetectContext(string textBefore, string[] lines, int lineNumber)
|
||||
{
|
||||
// Inside {{ }} expression?
|
||||
var lastOpen = textBefore.LastIndexOf("{{", StringComparison.Ordinal);
|
||||
var lastClose = textBefore.LastIndexOf("}}", StringComparison.Ordinal);
|
||||
if (lastOpen >= 0 && lastOpen > lastClose)
|
||||
{
|
||||
var exprText = textBefore[(lastOpen + 2)..];
|
||||
return new CompletionContextInfo(ContextKind.Expression, exprText);
|
||||
}
|
||||
|
||||
// Inside attribute value (="...")?
|
||||
var inAttrValue = IsInsideAttributeValue(textBefore);
|
||||
if (inAttrValue.Inside)
|
||||
return new CompletionContextInfo(ContextKind.AttributeValue,
|
||||
ElementName: inAttrValue.ElementName, AttributeName: inAttrValue.AttributeName);
|
||||
|
||||
// After < for element name?
|
||||
var lastLt = textBefore.LastIndexOf('<');
|
||||
var lastGt = textBefore.LastIndexOf('>');
|
||||
if (lastLt >= 0 && lastLt > lastGt)
|
||||
{
|
||||
var afterLt = textBefore[(lastLt + 1)..];
|
||||
|
||||
// Closing tag </...
|
||||
if (afterLt.StartsWith("/"))
|
||||
{
|
||||
var prefix = afterLt[1..];
|
||||
return new CompletionContextInfo(ContextKind.ElementName, prefix);
|
||||
}
|
||||
|
||||
// Check if we're still on the element name or moved to attributes
|
||||
var spaceIdx = afterLt.IndexOf(' ');
|
||||
if (spaceIdx < 0)
|
||||
{
|
||||
// Still typing element name
|
||||
return new CompletionContextInfo(ContextKind.ElementName, afterLt);
|
||||
}
|
||||
|
||||
// After element name + space = attribute context
|
||||
var elemName = afterLt[..spaceIdx].Trim();
|
||||
var attrText = afterLt[spaceIdx..].TrimStart();
|
||||
// Get the last partial attribute name being typed
|
||||
var lastSpace = attrText.LastIndexOfAny([' ', '"', '\'']);
|
||||
var attrPrefix = lastSpace >= 0 ? attrText[(lastSpace + 1)..] : attrText;
|
||||
// Filter out if we're right after = or inside a value
|
||||
if (!attrPrefix.Contains('='))
|
||||
return new CompletionContextInfo(ContextKind.AttributeName, attrPrefix, elemName);
|
||||
}
|
||||
|
||||
return new CompletionContextInfo(ContextKind.None);
|
||||
}
|
||||
|
||||
private static (bool Inside, string ElementName, string AttributeName) IsInsideAttributeValue(string text)
|
||||
{
|
||||
// Find the last opening tag context
|
||||
var lastLt = text.LastIndexOf('<');
|
||||
if (lastLt < 0) return (false, "", "");
|
||||
|
||||
var tagContent = text[(lastLt + 1)..];
|
||||
var lastGt = tagContent.LastIndexOf('>');
|
||||
if (lastGt >= 0) return (false, "", "");
|
||||
|
||||
// Extract element name
|
||||
var spaceIdx = tagContent.IndexOf(' ');
|
||||
if (spaceIdx < 0) return (false, "", "");
|
||||
var elemName = tagContent[..spaceIdx];
|
||||
if (elemName.StartsWith("/")) return (false, "", "");
|
||||
|
||||
// Check if cursor is inside a quoted attribute value
|
||||
var attrPart = tagContent[spaceIdx..];
|
||||
var inDoubleQuote = false;
|
||||
var inSingleQuote = false;
|
||||
var lastAttrName = "";
|
||||
var i = 0;
|
||||
|
||||
while (i < attrPart.Length)
|
||||
{
|
||||
if (attrPart[i] == '"' && !inSingleQuote)
|
||||
{
|
||||
inDoubleQuote = !inDoubleQuote;
|
||||
}
|
||||
else if (attrPart[i] == '\'' && !inDoubleQuote)
|
||||
{
|
||||
inSingleQuote = !inSingleQuote;
|
||||
}
|
||||
else if (!inDoubleQuote && !inSingleQuote && char.IsLetter(attrPart[i]))
|
||||
{
|
||||
// Capture attribute name
|
||||
var start = i;
|
||||
while (i < attrPart.Length && (char.IsLetterOrDigit(attrPart[i]) || attrPart[i] == '-' || attrPart[i] == '_'))
|
||||
i++;
|
||||
lastAttrName = attrPart[start..i];
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (inDoubleQuote || inSingleQuote)
|
||||
return (true, elemName, lastAttrName);
|
||||
|
||||
return (false, "", "");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Element Schema
|
||||
|
||||
private static readonly Dictionary<string, ElementSchema> Schema = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["receipt"] = new("Required root element", []),
|
||||
["line"] = new("Single line of text", new()
|
||||
{
|
||||
["align"] = new(["left", "center", "right"], "Text alignment"),
|
||||
["bold"] = BoolAttr("Bold text"),
|
||||
["big"] = BoolAttr("Double-width font"),
|
||||
["tall"] = BoolAttr("Double-height font"),
|
||||
["red"] = BoolAttr("Red text (supported printers)"),
|
||||
["lineSpacing"] = new([], "Line spacing in dots"),
|
||||
["wrap"] = BoolAttr("Word-wrap text"),
|
||||
["wrapIndent"] = new([], "Indent for wrapped lines"),
|
||||
}),
|
||||
["columns"] = new("Two-column layout", new()
|
||||
{
|
||||
["left"] = new([], "Left column content"),
|
||||
["right"] = new([], "Right column content"),
|
||||
["bold"] = BoolAttr("Bold text"),
|
||||
["big"] = BoolAttr("Double-width font"),
|
||||
["tall"] = BoolAttr("Double-height font"),
|
||||
["red"] = BoolAttr("Red text"),
|
||||
["lineSpacing"] = new([], "Line spacing in dots"),
|
||||
["wrap"] = BoolAttr("Wrap left column"),
|
||||
["wrapIndent"] = new([], "Indent for wrapped lines"),
|
||||
}),
|
||||
["row"] = new("Multi-column layout with <col> children", new()
|
||||
{
|
||||
["bold"] = BoolAttr("Bold text"),
|
||||
["big"] = BoolAttr("Double-width font"),
|
||||
["tall"] = BoolAttr("Double-height font"),
|
||||
["red"] = BoolAttr("Red text"),
|
||||
["lineSpacing"] = new([], "Line spacing in dots"),
|
||||
}),
|
||||
["col"] = new("Column inside <row> or <table>", new()
|
||||
{
|
||||
["width"] = new([], "Column width (char count or percentage)"),
|
||||
["align"] = new(["left", "center", "right"], "Cell alignment"),
|
||||
["wrap"] = BoolAttr("Word-wrap cell text"),
|
||||
}),
|
||||
["separator"] = new("Repeated character line", new()
|
||||
{
|
||||
["char"] = new([], "Character to repeat (default: -)"),
|
||||
}),
|
||||
["cut"] = new("Paper cut command", []),
|
||||
["feed"] = new("Empty lines", new()
|
||||
{
|
||||
["lines"] = new([], "Number of empty lines"),
|
||||
}),
|
||||
["table"] = new("ASCII box table", new()
|
||||
{
|
||||
["items"] = new([], "JSON array path for data rows"),
|
||||
["var"] = new([], "Loop variable name"),
|
||||
["headerItems"] = new([], "JSON array path for header rows"),
|
||||
}),
|
||||
["foreach"] = new("Loop over array", new()
|
||||
{
|
||||
["items"] = new([], "Path to JSON array (required)"),
|
||||
["var"] = new([], "Loop variable name (required)"),
|
||||
}),
|
||||
["if"] = new("Conditional block", new()
|
||||
{
|
||||
["test"] = new([], "Condition expression (required)"),
|
||||
}),
|
||||
["else"] = new("Else branch (must follow <if>)", []),
|
||||
};
|
||||
|
||||
private static AttributeSchema BoolAttr(string desc) => new(["true", "false"], desc);
|
||||
|
||||
private record ElementSchema(string Description, Dictionary<string, AttributeSchema> Attributes);
|
||||
private record AttributeSchema(List<string> Values, string Description);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Completion Generators
|
||||
|
||||
private static List<CompletionItem> GetElementCompletions(string prefix)
|
||||
{
|
||||
var items = new List<CompletionItem>();
|
||||
var sortIndex = 0;
|
||||
|
||||
foreach (var (name, schema) in Schema)
|
||||
{
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = name,
|
||||
Kind = CompletionItemKind.Keyword,
|
||||
Detail = schema.Description,
|
||||
InsertText = GetElementSnippet(name, schema),
|
||||
InsertTextRules = CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
SortText = sortIndex++.ToString("D3"),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string GetElementSnippet(string name, ElementSchema schema)
|
||||
{
|
||||
return name switch
|
||||
{
|
||||
"line" => "line${1: align=\"${2:left}\"}>${3:$0}</line>",
|
||||
"columns" => "columns left=\"${1}\" right=\"${2}\" />",
|
||||
"row" => "row>\n\t<col align=\"${1:left}\">${2}</col>\n\t<col align=\"${3:right}\">${4}</col>\n</row>",
|
||||
"col" => "col${1: align=\"${2:left}\"}>${3:$0}</col>",
|
||||
"separator" => "separator />",
|
||||
"cut" => "cut />",
|
||||
"feed" => "feed${1: lines=\"${2:1}\"} />",
|
||||
"foreach" => "foreach items=\"${1}\" var=\"${2:item}\">\n\t$0\n</foreach>",
|
||||
"if" => "if test=\"${1}\">\n\t$0\n</if>",
|
||||
"else" => "else>\n\t$0\n</else>",
|
||||
"table" => "table items=\"${1}\" var=\"${2:item}\">\n\t<col width=\"${3}\" align=\"${4:left}\">${5:Header}</col>\n</table>",
|
||||
"receipt" => "receipt>\n\t$0\n</receipt>",
|
||||
_ => $"{name}>$0</{name}>"
|
||||
};
|
||||
}
|
||||
|
||||
private static List<CompletionItem> GetAttributeCompletions(string elementName, string prefix)
|
||||
{
|
||||
var items = new List<CompletionItem>();
|
||||
|
||||
if (!Schema.TryGetValue(elementName, out var schema))
|
||||
return items;
|
||||
|
||||
var sortIndex = 0;
|
||||
foreach (var (attrName, attrSchema) in schema.Attributes)
|
||||
{
|
||||
var insertText = attrSchema.Values.Count > 0
|
||||
? $"{attrName}=\"${{1|{string.Join(",", attrSchema.Values)}|}}\""
|
||||
: $"{attrName}=\"$1\"";
|
||||
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = attrName,
|
||||
Kind = CompletionItemKind.Property,
|
||||
Detail = attrSchema.Description,
|
||||
InsertText = insertText,
|
||||
InsertTextRules = CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
SortText = sortIndex++.ToString("D3"),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<CompletionItem> GetAttributeValueCompletions(string elementName, string attributeName)
|
||||
{
|
||||
var items = new List<CompletionItem>();
|
||||
|
||||
if (!Schema.TryGetValue(elementName, out var schema))
|
||||
return items;
|
||||
|
||||
if (!schema.Attributes.TryGetValue(attributeName, out var attrSchema))
|
||||
return items;
|
||||
|
||||
foreach (var value in attrSchema.Values)
|
||||
{
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = value,
|
||||
Kind = CompletionItemKind.Value,
|
||||
InsertText = value,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<CompletionItem> GetExpressionCompletions(string prefix, string[] lines, int lineNumber)
|
||||
{
|
||||
var items = new List<CompletionItem>();
|
||||
var sortIndex = 0;
|
||||
|
||||
// Loop variables
|
||||
var loopVars = FindLoopVariables(lines, lineNumber);
|
||||
|
||||
// Special loop variables
|
||||
foreach (var special in new[] { "$index", "$first", "$last" })
|
||||
{
|
||||
if (loopVars.Count > 0) // Only suggest if inside a foreach
|
||||
{
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = special,
|
||||
Kind = CompletionItemKind.Variable,
|
||||
Detail = special switch
|
||||
{
|
||||
"$index" => "Zero-based loop index",
|
||||
"$first" => "True on first iteration",
|
||||
"$last" => "True on last iteration",
|
||||
_ => ""
|
||||
},
|
||||
InsertText = special,
|
||||
SortText = sortIndex++.ToString("D3"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Loop variable property paths from JSON
|
||||
foreach (var loopVar in loopVars)
|
||||
{
|
||||
// Find array in JSON paths and suggest its item properties
|
||||
var arrayPaths = FindMatchingArrayPaths(loopVar.ItemsPath);
|
||||
|
||||
foreach (var prop in arrayPaths)
|
||||
{
|
||||
// prop.Path is like "Items[].Description" - we want "item.Description"
|
||||
var suffix = prop.Path;
|
||||
var bracketIdx = suffix.IndexOf("[].", StringComparison.Ordinal);
|
||||
if (bracketIdx >= 0)
|
||||
suffix = suffix[(bracketIdx + 3)..];
|
||||
else
|
||||
continue;
|
||||
|
||||
var label = $"{loopVar.VarName}.{suffix}";
|
||||
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = label,
|
||||
Kind = CompletionItemKind.Field,
|
||||
Detail = $"{prop.Type} (from {loopVar.ItemsPath})",
|
||||
InsertText = label,
|
||||
SortText = sortIndex++.ToString("D3"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level JSON paths
|
||||
foreach (var prop in _jsonPaths.Where(p => !p.Path.Contains("[].")))
|
||||
{
|
||||
var kind = prop.IsArray ? CompletionItemKind.Enum : CompletionItemKind.Field;
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = prop.Path,
|
||||
Kind = kind,
|
||||
Detail = prop.Type,
|
||||
InsertText = prop.Path,
|
||||
SortText = sortIndex++.ToString("D3"),
|
||||
});
|
||||
}
|
||||
|
||||
// Format specifiers
|
||||
if (prefix.Contains(':'))
|
||||
{
|
||||
items.Clear();
|
||||
foreach (var (fmt, desc) in FormatSpecifiers)
|
||||
{
|
||||
items.Add(new CompletionItem
|
||||
{
|
||||
LabelAsString = fmt,
|
||||
Kind = CompletionItemKind.Unit,
|
||||
Detail = desc,
|
||||
InsertText = fmt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static readonly (string Format, string Description)[] FormatSpecifiers =
|
||||
[
|
||||
("F2", "Fixed-point 2 decimals (9.50)"),
|
||||
("F0", "Fixed-point no decimals (10)"),
|
||||
("N0", "Number with separators (1,234)"),
|
||||
("N2", "Number with separators + decimals (1,234.56)"),
|
||||
("dd.MM.yyyy", "Date (15.03.2024)"),
|
||||
("HH:mm", "Time (14:30)"),
|
||||
("HH:mm:ss", "Time with seconds (14:30:00)"),
|
||||
("dd-MMM-yy HH:mm", "Date + time (15-Mar-24 14:30)"),
|
||||
("dd.MM.yyyy HH:mm", "Date + time (15.03.2024 14:30)"),
|
||||
];
|
||||
|
||||
#endregion
|
||||
|
||||
#region Loop Variable Detection
|
||||
|
||||
private record LoopVarInfo(string VarName, string ItemsPath);
|
||||
|
||||
private static List<LoopVarInfo> FindLoopVariables(string[] lines, int lineNumber)
|
||||
{
|
||||
var vars = new List<LoopVarInfo>();
|
||||
|
||||
// Walk backwards from current line to find enclosing foreach elements
|
||||
var depth = 0;
|
||||
for (var i = lineNumber - 1; i >= 0; i--)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
|
||||
// Count closing foreach tags (increase depth needed)
|
||||
if (line.Contains("</foreach>"))
|
||||
depth++;
|
||||
|
||||
// Check for opening foreach tags
|
||||
var foreachMatch = System.Text.RegularExpressions.Regex.Match(line,
|
||||
@"<foreach\s+items=""([^""]+)""\s+var=""([^""]+)""");
|
||||
if (!foreachMatch.Success)
|
||||
foreachMatch = System.Text.RegularExpressions.Regex.Match(line,
|
||||
@"<foreach\s+var=""([^""]+)""\s+items=""([^""]+)""");
|
||||
|
||||
if (foreachMatch.Success)
|
||||
{
|
||||
if (depth > 0)
|
||||
{
|
||||
depth--;
|
||||
continue;
|
||||
}
|
||||
|
||||
string itemsPath, varName;
|
||||
if (foreachMatch.Groups.Count >= 3)
|
||||
{
|
||||
// Determine which capture is items vs var based on pattern
|
||||
if (line.IndexOf("items=", StringComparison.Ordinal) < line.IndexOf("var=", StringComparison.Ordinal))
|
||||
{
|
||||
itemsPath = foreachMatch.Groups[1].Value;
|
||||
varName = foreachMatch.Groups[2].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
varName = foreachMatch.Groups[1].Value;
|
||||
itemsPath = foreachMatch.Groups[2].Value;
|
||||
}
|
||||
|
||||
vars.Add(new LoopVarInfo(varName, itemsPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
private List<JsonPropertyInfo> FindMatchingArrayPaths(string itemsPath)
|
||||
{
|
||||
// itemsPath could be "Items" or "gang.Dishes" etc.
|
||||
// We need to find JSON paths that start with the matching array path + "[]."
|
||||
return _jsonPaths
|
||||
.Where(p => p.Path.StartsWith(itemsPath + "[].", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
180
Inspectron.PrintServer.TemplateEditor/wwwroot/app.css
Normal file
180
Inspectron.PrintServer.TemplateEditor/wwwroot/app.css
Normal file
@@ -0,0 +1,180 @@
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #404040;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 4px 14px;
|
||||
background: #3c3c3c;
|
||||
color: #cccccc;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #505050;
|
||||
}
|
||||
|
||||
.toolbar-separator {
|
||||
width: 1px;
|
||||
background: #555;
|
||||
margin: 2px 6px;
|
||||
}
|
||||
|
||||
.json-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
background: #2b4a3e;
|
||||
color: #4EC9B0;
|
||||
border: 1px solid #3a6b57;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.split-pane {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pane-editor {
|
||||
flex: 1;
|
||||
border-right: 1px solid #404040;
|
||||
}
|
||||
|
||||
.pane-preview {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.pane-header {
|
||||
padding: 4px 10px;
|
||||
background: #252526;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid #404040;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#my-editor-id {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
color: #cccccc;
|
||||
}
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.status-bar-safe-area {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.status-bar-safe-area {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: env(safe-area-inset-top);
|
||||
background-color: #f7f7f7;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.flex-column, .navbar-brand {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
}
|
||||
33
Inspectron.PrintServer.TemplateEditor/wwwroot/index.html
Normal file
33
Inspectron.PrintServer.TemplateEditor/wwwroot/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Inspectron.PrintServer.TemplateEditor</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="Inspectron.PrintServer.TemplateEditor.styles.css" />
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="_content/BlazorMonaco/jsInterop.js"></script>
|
||||
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
|
||||
<script src="receiptLanguage.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="status-bar-safe-area"></div>
|
||||
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
158
Inspectron.PrintServer.TemplateEditor/wwwroot/receiptLanguage.js
Normal file
158
Inspectron.PrintServer.TemplateEditor/wwwroot/receiptLanguage.js
Normal file
@@ -0,0 +1,158 @@
|
||||
window.receiptLanguage = {
|
||||
_registered: false,
|
||||
|
||||
register: function () {
|
||||
if (this._registered) return;
|
||||
this._registered = true;
|
||||
|
||||
monaco.languages.register({ id: 'receipt-xml' });
|
||||
|
||||
monaco.languages.setLanguageConfiguration('receipt-xml', {
|
||||
comments: {
|
||||
blockComment: ['<!--', '-->']
|
||||
},
|
||||
brackets: [
|
||||
['<', '>'],
|
||||
['{{', '}}']
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: '<', close: '>' },
|
||||
{ open: '{{', close: '}}' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '<', close: '>' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
]
|
||||
});
|
||||
|
||||
var knownElements = [
|
||||
'receipt', 'line', 'columns', 'row', 'col', 'separator',
|
||||
'cut', 'feed', 'table', 'foreach', 'if', 'else'
|
||||
];
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('receipt-xml', {
|
||||
knownElements: knownElements,
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
// Expression open
|
||||
[/\{\{/, { token: 'delimiter.expression', next: '@expression' }],
|
||||
|
||||
// XML comment
|
||||
[/<!--/, { token: 'comment', next: '@comment' }],
|
||||
|
||||
// Closing tag
|
||||
[/<\//, { token: 'delimiter.tag', next: '@closingTag' }],
|
||||
|
||||
// Self-closing or opening tag
|
||||
[/</, { token: 'delimiter.tag', next: '@openingTag' }],
|
||||
|
||||
// Plain text
|
||||
[/[^<{]+/, 'text']
|
||||
],
|
||||
|
||||
expression: [
|
||||
[/\}\}/, { token: 'delimiter.expression', next: '@pop' }],
|
||||
[/\$index|\$first|\$last/, 'variable.special'],
|
||||
[/:[^}]+/, 'string.format'],
|
||||
[/[a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)*/, 'variable.property'],
|
||||
[/./, 'variable.property']
|
||||
],
|
||||
|
||||
openingTag: [
|
||||
// Match element name
|
||||
[/([a-zA-Z][\w-]*)/, {
|
||||
cases: {
|
||||
'@knownElements': { token: 'tag.element', next: '@tagAttributes' },
|
||||
'@default': { token: 'tag.unknown', next: '@tagAttributes' }
|
||||
}
|
||||
}],
|
||||
[/>/, { token: 'delimiter.tag', next: '@pop' }]
|
||||
],
|
||||
|
||||
closingTag: [
|
||||
[/([a-zA-Z][\w-]*)/, {
|
||||
cases: {
|
||||
'@knownElements': 'tag.element',
|
||||
'@default': 'tag.unknown'
|
||||
}
|
||||
}],
|
||||
[/>/, { token: 'delimiter.tag', next: '@pop' }],
|
||||
[/\s+/, '']
|
||||
],
|
||||
|
||||
tagAttributes: [
|
||||
// Self-closing
|
||||
[/\/>/, { token: 'delimiter.tag', next: '@popall' }],
|
||||
|
||||
// Close tag
|
||||
[/>/, { token: 'delimiter.tag', next: '@popall' }],
|
||||
|
||||
// Attribute name
|
||||
[/[a-zA-Z_][\w-]*/, 'attribute.name'],
|
||||
|
||||
// Equals sign
|
||||
[/=/, 'delimiter'],
|
||||
|
||||
// Quoted attribute values
|
||||
[/"/, { token: 'attribute.value.delimiter', next: '@attrValueDouble' }],
|
||||
[/'/, { token: 'attribute.value.delimiter', next: '@attrValueSingle' }],
|
||||
|
||||
// Whitespace
|
||||
[/\s+/, '']
|
||||
],
|
||||
|
||||
attrValueDouble: [
|
||||
[/\{\{/, { token: 'delimiter.expression', next: '@expression' }],
|
||||
[/"/, { token: 'attribute.value.delimiter', next: '@pop' }],
|
||||
[/[^"{]+/, 'attribute.value']
|
||||
],
|
||||
|
||||
attrValueSingle: [
|
||||
[/\{\{/, { token: 'delimiter.expression', next: '@expression' }],
|
||||
[/'/, { token: 'attribute.value.delimiter', next: '@pop' }],
|
||||
[/[^'{]+/, 'attribute.value']
|
||||
],
|
||||
|
||||
comment: [
|
||||
[/-->/, { token: 'comment', next: '@pop' }],
|
||||
[/./, 'comment']
|
||||
]
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
defineTheme: function () {
|
||||
monaco.editor.defineTheme('receipt-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'tag.element', foreground: '569CD6', fontStyle: 'bold' },
|
||||
{ token: 'tag.unknown', foreground: 'F44747' },
|
||||
{ token: 'attribute.name', foreground: '9CDCFE' },
|
||||
{ token: 'attribute.value', foreground: 'CE9178' },
|
||||
{ token: 'attribute.value.delimiter', foreground: 'CE9178' },
|
||||
{ token: 'delimiter.tag', foreground: '808080' },
|
||||
{ token: 'delimiter', foreground: '808080' },
|
||||
{ token: 'delimiter.expression', foreground: 'DCDCAA', fontStyle: 'bold' },
|
||||
{ token: 'variable.property', foreground: '4EC9B0' },
|
||||
{ token: 'variable.special', foreground: 'C586C0', fontStyle: 'italic' },
|
||||
{ token: 'string.format', foreground: 'D7BA7D' },
|
||||
{ token: 'comment', foreground: '6A9955', fontStyle: 'italic' },
|
||||
{ token: 'text', foreground: 'D4D4D4' }
|
||||
],
|
||||
colors: {}
|
||||
});
|
||||
},
|
||||
|
||||
getEditorContent: function (editorId) {
|
||||
var editor = blazorMonaco.editor.getEditorById(editorId);
|
||||
if (editor) {
|
||||
return editor.getValue();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user