taro-bluetooth-print 2.9.0 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -1
- package/README.md +53 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/core/di/Container.d.ts +84 -0
- package/dist/types/core/di/Tokens.d.ts +29 -0
- package/dist/types/core/di/index.d.ts +3 -0
- package/dist/types/core/event/EventBus.d.ts +66 -0
- package/dist/types/core/event/index.d.ts +2 -0
- package/dist/types/core/index.d.ts +5 -4
- package/dist/types/core/plugin/PluginManager.d.ts +64 -0
- package/dist/types/core/plugin/index.d.ts +2 -0
- package/dist/types/device/MultiPrinterManager.d.ts +2 -0
- package/dist/types/factory/di-factory.d.ts +52 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/providers/ServiceProvider.d.ts +56 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/template/TemplateEngine.d.ts +24 -68
- package/dist/types/template/engines/TemplateRenderer.d.ts +71 -0
- package/dist/types/template/parsers/TemplateParser.d.ts +23 -0
- package/dist/types/utils/index.d.ts +8 -0
- package/dist/types/utils/logger.d.ts +4 -3
- package/dist/types/utils/outputLimiter.d.ts +87 -0
- package/dist/types/utils/validation.d.ts +11 -309
- package/dist/types/utils/validators/array.d.ts +19 -0
- package/dist/types/utils/validators/buffer.d.ts +18 -0
- package/dist/types/utils/validators/chain.d.ts +31 -0
- package/dist/types/utils/validators/common.d.ts +22 -0
- package/dist/types/utils/validators/number.d.ts +20 -0
- package/dist/types/utils/validators/object.d.ts +24 -0
- package/dist/types/utils/validators/printer.d.ts +40 -0
- package/dist/types/utils/validators/types.d.ts +125 -0
- package/dist/types/utils/validators/uuid.d.ts +23 -0
- package/package.json +1 -1
- package/src/core/BluetoothPrinter.ts +2 -1
- package/src/core/di/Container.ts +332 -0
- package/src/core/di/Tokens.ts +45 -0
- package/src/core/di/index.ts +3 -0
- package/src/core/event/EventBus.ts +251 -0
- package/src/core/event/index.ts +2 -0
- package/src/core/index.ts +10 -4
- package/src/core/plugin/PluginManager.ts +161 -0
- package/src/core/plugin/index.ts +2 -0
- package/src/device/MultiPrinterManager.ts +15 -6
- package/src/factory/di-factory.ts +61 -0
- package/src/index.ts +50 -1
- package/src/providers/ServiceProvider.ts +213 -0
- package/src/providers/index.ts +2 -0
- package/src/template/TemplateEngine.ts +27 -792
- package/src/template/engines/TemplateRenderer.ts +762 -0
- package/src/template/parsers/TemplateParser.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +17 -4
- package/src/utils/outputLimiter.ts +227 -0
- package/src/utils/validation.ts +21 -1138
- package/src/utils/validators/array.ts +95 -0
- package/src/utils/validators/buffer.ts +81 -0
- package/src/utils/validators/chain.ts +181 -0
- package/src/utils/validators/common.ts +216 -0
- package/src/utils/validators/number.ts +101 -0
- package/src/utils/validators/object.ts +63 -0
- package/src/utils/validators/printer.ts +294 -0
- package/src/utils/validators/types.ts +105 -0
- package/src/utils/validators/uuid.ts +49 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Renderer
|
|
3
|
+
*
|
|
4
|
+
* Handles rendering of template elements to ESC/POS commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger } from '@/utils/logger';
|
|
8
|
+
import { TextFormatter, TextAlign } from '@/formatter';
|
|
9
|
+
import { BarcodeGenerator, BarcodeFormat } from '@/barcode';
|
|
10
|
+
import { EscPos } from '@/drivers/EscPos';
|
|
11
|
+
import type {
|
|
12
|
+
TemplateElement,
|
|
13
|
+
LoopElement,
|
|
14
|
+
ConditionElement,
|
|
15
|
+
BorderElement,
|
|
16
|
+
TableElement,
|
|
17
|
+
BorderStyle,
|
|
18
|
+
TableRowData,
|
|
19
|
+
ReceiptData,
|
|
20
|
+
LabelData,
|
|
21
|
+
} from '../TemplateEngine';
|
|
22
|
+
import { TemplateParser } from '../parsers/TemplateParser';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Border style character sets
|
|
26
|
+
*/
|
|
27
|
+
const BORDER_CHARS: Record<
|
|
28
|
+
BorderStyle,
|
|
29
|
+
{
|
|
30
|
+
topLeft: string;
|
|
31
|
+
topRight: string;
|
|
32
|
+
bottomLeft: string;
|
|
33
|
+
bottomRight: string;
|
|
34
|
+
top: string;
|
|
35
|
+
bottom: string;
|
|
36
|
+
left: string;
|
|
37
|
+
right: string;
|
|
38
|
+
cross: string;
|
|
39
|
+
}
|
|
40
|
+
> = {
|
|
41
|
+
single: {
|
|
42
|
+
topLeft: '+',
|
|
43
|
+
topRight: '+',
|
|
44
|
+
bottomLeft: '+',
|
|
45
|
+
bottomRight: '+',
|
|
46
|
+
top: '-',
|
|
47
|
+
bottom: '-',
|
|
48
|
+
left: '|',
|
|
49
|
+
right: '|',
|
|
50
|
+
cross: '+',
|
|
51
|
+
},
|
|
52
|
+
double: {
|
|
53
|
+
topLeft: '╔',
|
|
54
|
+
topRight: '╗',
|
|
55
|
+
bottomLeft: '╚',
|
|
56
|
+
bottomRight: '╝',
|
|
57
|
+
top: '═',
|
|
58
|
+
bottom: '═',
|
|
59
|
+
left: '║',
|
|
60
|
+
right: '║',
|
|
61
|
+
cross: '╬',
|
|
62
|
+
},
|
|
63
|
+
thick: {
|
|
64
|
+
topLeft: '┏',
|
|
65
|
+
topRight: '┓',
|
|
66
|
+
bottomLeft: '┗',
|
|
67
|
+
bottomRight: '┛',
|
|
68
|
+
top: '━',
|
|
69
|
+
bottom: '━',
|
|
70
|
+
left: '┃',
|
|
71
|
+
right: '┃',
|
|
72
|
+
cross: '╋',
|
|
73
|
+
},
|
|
74
|
+
rounded: {
|
|
75
|
+
topLeft: '╭',
|
|
76
|
+
topRight: '╮',
|
|
77
|
+
bottomLeft: '╰',
|
|
78
|
+
bottomRight: '╯',
|
|
79
|
+
top: '─',
|
|
80
|
+
bottom: '─',
|
|
81
|
+
left: '│',
|
|
82
|
+
right: '│',
|
|
83
|
+
cross: '┼',
|
|
84
|
+
},
|
|
85
|
+
dashed: {
|
|
86
|
+
topLeft: '+',
|
|
87
|
+
topRight: '+',
|
|
88
|
+
bottomLeft: '+',
|
|
89
|
+
bottomRight: '+',
|
|
90
|
+
top: '-',
|
|
91
|
+
bottom: '-',
|
|
92
|
+
left: ':',
|
|
93
|
+
right: ':',
|
|
94
|
+
cross: '+',
|
|
95
|
+
},
|
|
96
|
+
none: {
|
|
97
|
+
topLeft: ' ',
|
|
98
|
+
topRight: ' ',
|
|
99
|
+
bottomLeft: ' ',
|
|
100
|
+
bottomRight: ' ',
|
|
101
|
+
top: ' ',
|
|
102
|
+
bottom: ' ',
|
|
103
|
+
left: ' ',
|
|
104
|
+
right: ' ',
|
|
105
|
+
cross: ' ',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Template Renderer class
|
|
111
|
+
* Renders template elements to ESC/POS commands
|
|
112
|
+
*/
|
|
113
|
+
export class TemplateRenderer {
|
|
114
|
+
private readonly logger = Logger.scope('TemplateRenderer');
|
|
115
|
+
private readonly formatter: TextFormatter;
|
|
116
|
+
private readonly barcodeGenerator: BarcodeGenerator;
|
|
117
|
+
private readonly driver: EscPos;
|
|
118
|
+
private readonly parser: TemplateParser;
|
|
119
|
+
private readonly paperWidth: number;
|
|
120
|
+
|
|
121
|
+
constructor(paperWidth = 48) {
|
|
122
|
+
this.paperWidth = paperWidth;
|
|
123
|
+
this.formatter = new TextFormatter();
|
|
124
|
+
this.barcodeGenerator = new BarcodeGenerator();
|
|
125
|
+
this.driver = new EscPos();
|
|
126
|
+
this.parser = new TemplateParser();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render a receipt template
|
|
131
|
+
*/
|
|
132
|
+
renderReceipt(data: ReceiptData): Uint8Array {
|
|
133
|
+
const commands: Uint8Array[] = [];
|
|
134
|
+
|
|
135
|
+
// Initialize printer
|
|
136
|
+
commands.push(...this.driver.init());
|
|
137
|
+
|
|
138
|
+
// Store header
|
|
139
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
140
|
+
commands.push(...this.formatter.setSize(2, 2));
|
|
141
|
+
commands.push(...this.formatter.setBold(true));
|
|
142
|
+
commands.push(...this.driver.text(data.store.name));
|
|
143
|
+
commands.push(...this.driver.feed(1));
|
|
144
|
+
commands.push(...this.formatter.resetStyle());
|
|
145
|
+
|
|
146
|
+
// Store address and phone
|
|
147
|
+
if (data.store.address) {
|
|
148
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
149
|
+
commands.push(...this.driver.text(data.store.address));
|
|
150
|
+
commands.push(...this.driver.feed(1));
|
|
151
|
+
}
|
|
152
|
+
if (data.store.phone) {
|
|
153
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
154
|
+
commands.push(...this.driver.text(`电话: ${data.store.phone}`));
|
|
155
|
+
commands.push(...this.driver.feed(1));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Separator line
|
|
159
|
+
commands.push(...this.renderLine());
|
|
160
|
+
|
|
161
|
+
// Order info
|
|
162
|
+
if (data.order) {
|
|
163
|
+
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
164
|
+
commands.push(...this.driver.text(`订单号: ${data.order.id}`));
|
|
165
|
+
commands.push(...this.driver.feed(1));
|
|
166
|
+
commands.push(...this.driver.text(`日期: ${data.order.date}`));
|
|
167
|
+
commands.push(...this.driver.feed(1));
|
|
168
|
+
if (data.order.cashier) {
|
|
169
|
+
commands.push(...this.driver.text(`收银员: ${data.order.cashier}`));
|
|
170
|
+
commands.push(...this.driver.feed(1));
|
|
171
|
+
}
|
|
172
|
+
commands.push(...this.renderLine());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Items header
|
|
176
|
+
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
177
|
+
commands.push(...this.formatter.setBold(true));
|
|
178
|
+
commands.push(...this.driver.text(this.formatItemLine('商品', '数量', '金额')));
|
|
179
|
+
commands.push(...this.driver.feed(1));
|
|
180
|
+
commands.push(...this.formatter.setBold(false));
|
|
181
|
+
commands.push(...this.renderLine('-'));
|
|
182
|
+
|
|
183
|
+
// Items
|
|
184
|
+
for (const item of data.items) {
|
|
185
|
+
const amount = item.quantity * item.price - (item.discount ?? 0);
|
|
186
|
+
commands.push(...this.driver.text(item.name));
|
|
187
|
+
commands.push(...this.driver.feed(1));
|
|
188
|
+
commands.push(
|
|
189
|
+
...this.driver.text(this.formatItemLine('', `x${item.quantity}`, `¥${amount.toFixed(2)}`))
|
|
190
|
+
);
|
|
191
|
+
commands.push(...this.driver.feed(1));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
commands.push(...this.renderLine());
|
|
195
|
+
|
|
196
|
+
// Payment summary
|
|
197
|
+
commands.push(...this.formatter.align(TextAlign.RIGHT));
|
|
198
|
+
if (data.payment.subtotal !== data.payment.total) {
|
|
199
|
+
commands.push(...this.driver.text(`小计: ¥${data.payment.subtotal.toFixed(2)}`));
|
|
200
|
+
commands.push(...this.driver.feed(1));
|
|
201
|
+
}
|
|
202
|
+
if (data.payment.tax) {
|
|
203
|
+
commands.push(...this.driver.text(`税额: ¥${data.payment.tax.toFixed(2)}`));
|
|
204
|
+
commands.push(...this.driver.feed(1));
|
|
205
|
+
}
|
|
206
|
+
if (data.payment.discount) {
|
|
207
|
+
commands.push(...this.driver.text(`优惠: -¥${data.payment.discount.toFixed(2)}`));
|
|
208
|
+
commands.push(...this.driver.feed(1));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
commands.push(...this.formatter.setBold(true));
|
|
212
|
+
commands.push(...this.formatter.setSize(1, 2));
|
|
213
|
+
commands.push(...this.driver.text(`合计: ¥${data.payment.total.toFixed(2)}`));
|
|
214
|
+
commands.push(...this.driver.feed(1));
|
|
215
|
+
commands.push(...this.formatter.resetStyle());
|
|
216
|
+
|
|
217
|
+
commands.push(...this.formatter.align(TextAlign.RIGHT));
|
|
218
|
+
commands.push(...this.driver.text(`支付方式: ${data.payment.method}`));
|
|
219
|
+
commands.push(...this.driver.feed(1));
|
|
220
|
+
|
|
221
|
+
if (data.payment.received !== undefined) {
|
|
222
|
+
commands.push(...this.driver.text(`实收: ¥${data.payment.received.toFixed(2)}`));
|
|
223
|
+
commands.push(...this.driver.feed(1));
|
|
224
|
+
}
|
|
225
|
+
if (data.payment.change !== undefined) {
|
|
226
|
+
commands.push(...this.driver.text(`找零: ¥${data.payment.change.toFixed(2)}`));
|
|
227
|
+
commands.push(...this.driver.feed(1));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
commands.push(...this.renderLine());
|
|
231
|
+
|
|
232
|
+
// QR Code
|
|
233
|
+
if (data.qrCode) {
|
|
234
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
235
|
+
commands.push(...this.driver.qr(data.qrCode, { size: 6 }));
|
|
236
|
+
commands.push(...this.driver.feed(1));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Footer
|
|
240
|
+
if (data.footer) {
|
|
241
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
242
|
+
commands.push(...this.driver.text(data.footer));
|
|
243
|
+
commands.push(...this.driver.feed(1));
|
|
244
|
+
} else {
|
|
245
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
246
|
+
commands.push(...this.driver.text('谢谢惠顾,欢迎再次光临!'));
|
|
247
|
+
commands.push(...this.driver.feed(1));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
commands.push(...this.driver.feed(3));
|
|
251
|
+
commands.push(...this.driver.cut());
|
|
252
|
+
|
|
253
|
+
return this.combineCommands(commands);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render a label template
|
|
258
|
+
*/
|
|
259
|
+
renderLabel(data: LabelData): Uint8Array {
|
|
260
|
+
const commands: Uint8Array[] = [];
|
|
261
|
+
|
|
262
|
+
// Initialize printer
|
|
263
|
+
commands.push(...this.driver.init());
|
|
264
|
+
|
|
265
|
+
// Product name
|
|
266
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
267
|
+
commands.push(...this.formatter.setBold(true));
|
|
268
|
+
commands.push(...this.driver.text(data.name));
|
|
269
|
+
commands.push(...this.driver.feed(1));
|
|
270
|
+
commands.push(...this.formatter.setBold(false));
|
|
271
|
+
|
|
272
|
+
// Spec
|
|
273
|
+
if (data.spec) {
|
|
274
|
+
commands.push(...this.driver.text(data.spec));
|
|
275
|
+
commands.push(...this.driver.feed(1));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Price
|
|
279
|
+
commands.push(...this.formatter.setSize(2, 2));
|
|
280
|
+
commands.push(...this.driver.text(`¥${data.price.toFixed(2)}`));
|
|
281
|
+
commands.push(...this.driver.feed(1));
|
|
282
|
+
commands.push(...this.formatter.resetStyle());
|
|
283
|
+
|
|
284
|
+
// Dates
|
|
285
|
+
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
286
|
+
if (data.productionDate) {
|
|
287
|
+
commands.push(...this.driver.text(`生产日期: ${data.productionDate}`));
|
|
288
|
+
commands.push(...this.driver.feed(1));
|
|
289
|
+
}
|
|
290
|
+
if (data.expiryDate) {
|
|
291
|
+
commands.push(...this.driver.text(`保质期至: ${data.expiryDate}`));
|
|
292
|
+
commands.push(...this.driver.feed(1));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Barcode
|
|
296
|
+
if (data.barcode) {
|
|
297
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
298
|
+
const barcodeCommands = this.barcodeGenerator.generate(data.barcode, {
|
|
299
|
+
format: data.barcodeFormat ?? BarcodeFormat.CODE128,
|
|
300
|
+
height: 60,
|
|
301
|
+
showText: true,
|
|
302
|
+
});
|
|
303
|
+
commands.push(...barcodeCommands);
|
|
304
|
+
commands.push(...this.driver.feed(1));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
commands.push(...this.driver.feed(2));
|
|
308
|
+
commands.push(...this.driver.cut());
|
|
309
|
+
|
|
310
|
+
return this.combineCommands(commands);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Render a custom template
|
|
315
|
+
*/
|
|
316
|
+
render(
|
|
317
|
+
template: import('../TemplateEngine').TemplateDefinition,
|
|
318
|
+
data: Record<string, unknown>
|
|
319
|
+
): Uint8Array {
|
|
320
|
+
const commands: Uint8Array[] = [];
|
|
321
|
+
|
|
322
|
+
// Initialize printer
|
|
323
|
+
commands.push(...this.driver.init());
|
|
324
|
+
|
|
325
|
+
for (const element of template.elements) {
|
|
326
|
+
commands.push(...this.renderElement(element, data));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
commands.push(...this.driver.feed(2));
|
|
330
|
+
commands.push(...this.driver.cut());
|
|
331
|
+
|
|
332
|
+
return this.combineCommands(commands);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Render a single template element
|
|
337
|
+
*/
|
|
338
|
+
renderElement(element: TemplateElement, data: Record<string, unknown>): Uint8Array[] {
|
|
339
|
+
switch (element.type) {
|
|
340
|
+
case 'loop':
|
|
341
|
+
return this.renderLoop(element, data);
|
|
342
|
+
case 'condition':
|
|
343
|
+
return this.renderCondition(element, data);
|
|
344
|
+
case 'border':
|
|
345
|
+
return this.renderBorder(element);
|
|
346
|
+
case 'table':
|
|
347
|
+
return this.renderTable(element, data);
|
|
348
|
+
default:
|
|
349
|
+
return this.renderStandardElement(element, data);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Render a loop element
|
|
355
|
+
*/
|
|
356
|
+
renderLoop(loop: LoopElement, data: Record<string, unknown>): Uint8Array[] {
|
|
357
|
+
const commands: Uint8Array[] = [];
|
|
358
|
+
const items = this.parser.getNestedValue(data, loop.items);
|
|
359
|
+
|
|
360
|
+
if (!items || !Array.isArray(items)) {
|
|
361
|
+
this.logger.warn(`Loop variable '${loop.items}' is not an array`);
|
|
362
|
+
return commands;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < items.length; i++) {
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
|
367
|
+
const itemData: any = items[i];
|
|
368
|
+
|
|
369
|
+
// Create iteration context with item and optionally index
|
|
370
|
+
const context: Record<string, unknown> = {
|
|
371
|
+
...data,
|
|
372
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
373
|
+
[loop.itemVar]: itemData,
|
|
374
|
+
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (loop.indexVar) {
|
|
378
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
379
|
+
context[loop.indexVar] = i;
|
|
380
|
+
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Render each element in the loop
|
|
384
|
+
for (const childElement of loop.elements) {
|
|
385
|
+
commands.push(...this.renderElement(childElement, context));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Add separator between items (but not after the last)
|
|
389
|
+
if (loop.separator && i < items.length - 1) {
|
|
390
|
+
commands.push(...this.driver.text(loop.separator));
|
|
391
|
+
commands.push(...this.driver.feed(1));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return commands;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Render a condition element
|
|
400
|
+
*/
|
|
401
|
+
renderCondition(condition: ConditionElement, data: Record<string, unknown>): Uint8Array[] {
|
|
402
|
+
const commands: Uint8Array[] = [];
|
|
403
|
+
const value = this.parser.getNestedValue(data, condition.variable);
|
|
404
|
+
const result = this.evaluateCondition(value, condition.operator, condition.value);
|
|
405
|
+
|
|
406
|
+
const elementsToRender = result ? condition.then : (condition.else ?? []);
|
|
407
|
+
|
|
408
|
+
for (const childElement of elementsToRender) {
|
|
409
|
+
commands.push(...this.renderElement(childElement, data));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return commands;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Evaluate a condition
|
|
417
|
+
*/
|
|
418
|
+
evaluateCondition(
|
|
419
|
+
value: unknown,
|
|
420
|
+
operator: ConditionElement['operator'],
|
|
421
|
+
compareValue?: unknown
|
|
422
|
+
): boolean {
|
|
423
|
+
switch (operator) {
|
|
424
|
+
case 'exists':
|
|
425
|
+
return value !== undefined && value !== null;
|
|
426
|
+
case 'not_exists':
|
|
427
|
+
return value === undefined || value === null;
|
|
428
|
+
case 'equals':
|
|
429
|
+
return value === compareValue;
|
|
430
|
+
case 'not_equals':
|
|
431
|
+
return value !== compareValue;
|
|
432
|
+
case 'gt':
|
|
433
|
+
return (
|
|
434
|
+
typeof value === 'number' && typeof compareValue === 'number' && value > compareValue
|
|
435
|
+
);
|
|
436
|
+
case 'gte':
|
|
437
|
+
return (
|
|
438
|
+
typeof value === 'number' && typeof compareValue === 'number' && value >= compareValue
|
|
439
|
+
);
|
|
440
|
+
case 'lt':
|
|
441
|
+
return (
|
|
442
|
+
typeof value === 'number' && typeof compareValue === 'number' && value < compareValue
|
|
443
|
+
);
|
|
444
|
+
case 'lte':
|
|
445
|
+
return (
|
|
446
|
+
typeof value === 'number' && typeof compareValue === 'number' && value <= compareValue
|
|
447
|
+
);
|
|
448
|
+
case 'truthy':
|
|
449
|
+
return Boolean(value);
|
|
450
|
+
case 'falsy':
|
|
451
|
+
return !value;
|
|
452
|
+
default:
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Render a border element
|
|
459
|
+
*/
|
|
460
|
+
renderBorder(border: BorderElement): Uint8Array[] {
|
|
461
|
+
const commands: Uint8Array[] = [];
|
|
462
|
+
const style = BORDER_CHARS[border.style ?? 'single'];
|
|
463
|
+
const width = this.paperWidth;
|
|
464
|
+
const padding = border.padding ?? 0;
|
|
465
|
+
|
|
466
|
+
// Build custom or default characters
|
|
467
|
+
const tl = border.topLeft ?? style.topLeft;
|
|
468
|
+
const tr = border.topRight ?? style.topRight;
|
|
469
|
+
const bl = border.bottomLeft ?? style.bottomLeft;
|
|
470
|
+
const br = border.bottomRight ?? style.bottomRight;
|
|
471
|
+
const t = border.top ?? style.top;
|
|
472
|
+
const b = border.bottom ?? style.bottom;
|
|
473
|
+
const l = border.left ?? style.left;
|
|
474
|
+
const r = border.right ?? style.right;
|
|
475
|
+
|
|
476
|
+
// Top border
|
|
477
|
+
if (border.drawTop !== false) {
|
|
478
|
+
const topLine = tl + t.repeat(width - 2) + tr;
|
|
479
|
+
commands.push(...this.driver.text(topLine));
|
|
480
|
+
commands.push(...this.driver.feed(1));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Middle lines with optional fill
|
|
484
|
+
if (border.filled) {
|
|
485
|
+
const innerWidth = width - 2 - padding * 2;
|
|
486
|
+
const fillChar = ' ';
|
|
487
|
+
for (let i = 0; i < padding; i++) {
|
|
488
|
+
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
489
|
+
commands.push(...this.driver.text(fillLine));
|
|
490
|
+
commands.push(...this.driver.feed(1));
|
|
491
|
+
}
|
|
492
|
+
const contentLine =
|
|
493
|
+
l + fillChar.repeat(padding) + fillChar.repeat(innerWidth) + fillChar.repeat(padding) + r;
|
|
494
|
+
commands.push(...this.driver.text(contentLine));
|
|
495
|
+
commands.push(...this.driver.feed(1));
|
|
496
|
+
for (let i = 0; i < padding; i++) {
|
|
497
|
+
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
498
|
+
commands.push(...this.driver.text(fillLine));
|
|
499
|
+
commands.push(...this.driver.feed(1));
|
|
500
|
+
}
|
|
501
|
+
} else if (border.drawLeft || border.drawRight) {
|
|
502
|
+
const middleLine =
|
|
503
|
+
(border.drawLeft !== false ? l : ' ') +
|
|
504
|
+
' '.repeat(width - 2) +
|
|
505
|
+
(border.drawRight !== false ? r : ' ');
|
|
506
|
+
commands.push(...this.driver.text(middleLine));
|
|
507
|
+
commands.push(...this.driver.feed(1));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Bottom border
|
|
511
|
+
if (border.drawBottom !== false) {
|
|
512
|
+
const bottomLine = bl + b.repeat(width - 2) + br;
|
|
513
|
+
commands.push(...this.driver.text(bottomLine));
|
|
514
|
+
commands.push(...this.driver.feed(1));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return commands;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Render a table element
|
|
522
|
+
*/
|
|
523
|
+
renderTable(table: TableElement, data: Record<string, unknown>): Uint8Array[] {
|
|
524
|
+
const commands: Uint8Array[] = [];
|
|
525
|
+
const rows = this.parser.getNestedValue(data, table.rowsVar);
|
|
526
|
+
|
|
527
|
+
if (!Array.isArray(rows)) {
|
|
528
|
+
this.logger.warn(`Table rows variable '${table.rowsVar}' is not an array`);
|
|
529
|
+
return commands;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const style = BORDER_CHARS[table.borderStyle ?? 'single'];
|
|
533
|
+
|
|
534
|
+
// Draw top border
|
|
535
|
+
if (table.showHeader) {
|
|
536
|
+
const topLine =
|
|
537
|
+
style.topLeft +
|
|
538
|
+
table.columns
|
|
539
|
+
.map(col => {
|
|
540
|
+
const width = col.width + 1;
|
|
541
|
+
return style.top.repeat(width);
|
|
542
|
+
})
|
|
543
|
+
.join('') +
|
|
544
|
+
style.topRight;
|
|
545
|
+
commands.push(...this.driver.text(topLine));
|
|
546
|
+
commands.push(...this.driver.feed(1));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Draw header row
|
|
550
|
+
if (table.showHeader) {
|
|
551
|
+
let headerContent = style.left + ' ';
|
|
552
|
+
table.columns.forEach((col, i) => {
|
|
553
|
+
const cellText = col.header.substring(0, col.width);
|
|
554
|
+
const aligned = this.alignText(cellText, col.width, col.headerAlign ?? TextAlign.LEFT);
|
|
555
|
+
headerContent +=
|
|
556
|
+
aligned + (i < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
557
|
+
});
|
|
558
|
+
commands.push(...this.driver.text(headerContent));
|
|
559
|
+
commands.push(...this.driver.feed(1));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Draw separator after header
|
|
563
|
+
if (table.showHeader) {
|
|
564
|
+
const sepLine =
|
|
565
|
+
style.cross +
|
|
566
|
+
table.columns
|
|
567
|
+
.map(col => {
|
|
568
|
+
return style.bottom.repeat(col.width + 1);
|
|
569
|
+
})
|
|
570
|
+
.join('') +
|
|
571
|
+
style.cross;
|
|
572
|
+
commands.push(...this.driver.text(sepLine));
|
|
573
|
+
commands.push(...this.driver.feed(1));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Draw data rows
|
|
577
|
+
rows.forEach((rowData, rowIndex) => {
|
|
578
|
+
const row = rowData as TableRowData;
|
|
579
|
+
let rowContent = style.left + ' ';
|
|
580
|
+
|
|
581
|
+
table.columns.forEach((col, colIndex) => {
|
|
582
|
+
const cellValue = row[col.header] ?? '';
|
|
583
|
+
const cellText = String(cellValue).substring(0, col.width);
|
|
584
|
+
const aligned = this.alignText(cellText, col.width, col.cellAlign ?? TextAlign.LEFT);
|
|
585
|
+
rowContent +=
|
|
586
|
+
aligned + (colIndex < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
commands.push(...this.driver.text(rowContent));
|
|
590
|
+
commands.push(...this.driver.feed(1));
|
|
591
|
+
|
|
592
|
+
// Draw row separator
|
|
593
|
+
if (rowIndex < rows.length - 1) {
|
|
594
|
+
const sepLine =
|
|
595
|
+
style.cross +
|
|
596
|
+
table.columns
|
|
597
|
+
.map(col => {
|
|
598
|
+
return style.bottom.repeat(col.width + 1);
|
|
599
|
+
})
|
|
600
|
+
.join('') +
|
|
601
|
+
style.cross;
|
|
602
|
+
commands.push(...this.driver.text(sepLine));
|
|
603
|
+
commands.push(...this.driver.feed(1));
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Draw bottom border
|
|
608
|
+
const bottomLine =
|
|
609
|
+
style.bottomLeft +
|
|
610
|
+
table.columns
|
|
611
|
+
.map(col => {
|
|
612
|
+
return style.bottom.repeat(col.width + 1);
|
|
613
|
+
})
|
|
614
|
+
.join('') +
|
|
615
|
+
style.bottomRight;
|
|
616
|
+
commands.push(...this.driver.text(bottomLine));
|
|
617
|
+
commands.push(...this.driver.feed(1));
|
|
618
|
+
|
|
619
|
+
return commands;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Align text within a specified width
|
|
624
|
+
*/
|
|
625
|
+
alignText(text: string, width: number, align: TextAlign): string {
|
|
626
|
+
const padded = text.padEnd(width).substring(0, width);
|
|
627
|
+
switch (align) {
|
|
628
|
+
case TextAlign.CENTER: {
|
|
629
|
+
const leftPad = Math.floor((width - padded.length) / 2);
|
|
630
|
+
return padded.padStart(leftPad + padded.length).padEnd(width);
|
|
631
|
+
}
|
|
632
|
+
case TextAlign.RIGHT:
|
|
633
|
+
return text.padStart(width);
|
|
634
|
+
default:
|
|
635
|
+
return padded;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Render standard elements (text, line, image, qrcode, barcode, feed, variable)
|
|
641
|
+
*/
|
|
642
|
+
renderStandardElement(
|
|
643
|
+
element: Exclude<
|
|
644
|
+
TemplateElement,
|
|
645
|
+
LoopElement | ConditionElement | BorderElement | TableElement
|
|
646
|
+
>,
|
|
647
|
+
data: Record<string, unknown>
|
|
648
|
+
): Uint8Array[] {
|
|
649
|
+
const commands: Uint8Array[] = [];
|
|
650
|
+
|
|
651
|
+
switch (element.type) {
|
|
652
|
+
case 'text': {
|
|
653
|
+
const content = this.parser.substituteVariables(element.content, data);
|
|
654
|
+
if (element.align) {
|
|
655
|
+
commands.push(...this.formatter.align(element.align));
|
|
656
|
+
}
|
|
657
|
+
if (element.size) {
|
|
658
|
+
commands.push(...this.formatter.setSize(element.size, element.size));
|
|
659
|
+
}
|
|
660
|
+
if (element.bold) {
|
|
661
|
+
commands.push(...this.formatter.setBold(true));
|
|
662
|
+
}
|
|
663
|
+
commands.push(...this.driver.text(content));
|
|
664
|
+
commands.push(...this.driver.feed(1));
|
|
665
|
+
if (element.bold) {
|
|
666
|
+
commands.push(...this.formatter.setBold(false));
|
|
667
|
+
}
|
|
668
|
+
if (element.size) {
|
|
669
|
+
commands.push(...this.formatter.setSize(1, 1));
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
case 'line':
|
|
675
|
+
commands.push(...this.renderLine(element.char, element.length));
|
|
676
|
+
break;
|
|
677
|
+
|
|
678
|
+
case 'image':
|
|
679
|
+
commands.push(...this.driver.image(element.data, element.width, element.height));
|
|
680
|
+
commands.push(...this.driver.feed(1));
|
|
681
|
+
break;
|
|
682
|
+
|
|
683
|
+
case 'qrcode': {
|
|
684
|
+
const qrContent = this.parser.substituteVariables(element.content, data);
|
|
685
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
686
|
+
commands.push(...this.driver.qr(qrContent, { size: element.size ?? 6 }));
|
|
687
|
+
commands.push(...this.driver.feed(1));
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
case 'barcode': {
|
|
692
|
+
const barcodeContent = this.parser.substituteVariables(element.content, data);
|
|
693
|
+
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
694
|
+
const barcodeCommands = this.barcodeGenerator.generate(barcodeContent, {
|
|
695
|
+
format: element.format,
|
|
696
|
+
height: element.height ?? 60,
|
|
697
|
+
showText: true,
|
|
698
|
+
});
|
|
699
|
+
commands.push(...barcodeCommands);
|
|
700
|
+
commands.push(...this.driver.feed(1));
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
case 'feed':
|
|
705
|
+
commands.push(...this.driver.feed(element.lines));
|
|
706
|
+
break;
|
|
707
|
+
|
|
708
|
+
case 'variable': {
|
|
709
|
+
const value = this.parser.getNestedValue(data, element.name);
|
|
710
|
+
if (value !== undefined) {
|
|
711
|
+
const formatted = this.parser.formatValue(value, element.format);
|
|
712
|
+
commands.push(...this.driver.text(formatted));
|
|
713
|
+
commands.push(...this.driver.feed(1));
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return commands;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Render a separator line
|
|
724
|
+
*/
|
|
725
|
+
renderLine(char?: string, length?: number): Uint8Array[] {
|
|
726
|
+
const lineChar = char ?? '-';
|
|
727
|
+
const lineLength = length ?? this.paperWidth;
|
|
728
|
+
const line = lineChar.repeat(lineLength);
|
|
729
|
+
return [...this.driver.text(line), ...this.driver.feed(1)];
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Format an item line with columns
|
|
734
|
+
*/
|
|
735
|
+
formatItemLine(name: string, qty: string, amount: string): string {
|
|
736
|
+
const nameWidth = this.paperWidth - 16;
|
|
737
|
+
const qtyWidth = 8;
|
|
738
|
+
const amountWidth = 8;
|
|
739
|
+
|
|
740
|
+
const paddedName = name.padEnd(nameWidth).substring(0, nameWidth);
|
|
741
|
+
const paddedQty = qty.padStart(qtyWidth);
|
|
742
|
+
const paddedAmount = amount.padStart(amountWidth);
|
|
743
|
+
|
|
744
|
+
return `${paddedName}${paddedQty}${paddedAmount}`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Combine multiple command arrays into one
|
|
749
|
+
*/
|
|
750
|
+
combineCommands(commands: Uint8Array[]): Uint8Array {
|
|
751
|
+
const totalLength = commands.reduce((acc, cmd) => acc + cmd.length, 0);
|
|
752
|
+
const result = new Uint8Array(totalLength);
|
|
753
|
+
let offset = 0;
|
|
754
|
+
|
|
755
|
+
for (const cmd of commands) {
|
|
756
|
+
result.set(cmd, offset);
|
|
757
|
+
offset += cmd.length;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
}
|