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.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +53 -4
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -1
  5. package/dist/index.umd.js +1 -1
  6. package/dist/types/core/di/Container.d.ts +84 -0
  7. package/dist/types/core/di/Tokens.d.ts +29 -0
  8. package/dist/types/core/di/index.d.ts +3 -0
  9. package/dist/types/core/event/EventBus.d.ts +66 -0
  10. package/dist/types/core/event/index.d.ts +2 -0
  11. package/dist/types/core/index.d.ts +5 -4
  12. package/dist/types/core/plugin/PluginManager.d.ts +64 -0
  13. package/dist/types/core/plugin/index.d.ts +2 -0
  14. package/dist/types/device/MultiPrinterManager.d.ts +2 -0
  15. package/dist/types/factory/di-factory.d.ts +52 -0
  16. package/dist/types/index.d.ts +5 -1
  17. package/dist/types/providers/ServiceProvider.d.ts +56 -0
  18. package/dist/types/providers/index.d.ts +2 -0
  19. package/dist/types/template/TemplateEngine.d.ts +24 -68
  20. package/dist/types/template/engines/TemplateRenderer.d.ts +71 -0
  21. package/dist/types/template/parsers/TemplateParser.d.ts +23 -0
  22. package/dist/types/utils/index.d.ts +8 -0
  23. package/dist/types/utils/logger.d.ts +4 -3
  24. package/dist/types/utils/outputLimiter.d.ts +87 -0
  25. package/dist/types/utils/validation.d.ts +11 -309
  26. package/dist/types/utils/validators/array.d.ts +19 -0
  27. package/dist/types/utils/validators/buffer.d.ts +18 -0
  28. package/dist/types/utils/validators/chain.d.ts +31 -0
  29. package/dist/types/utils/validators/common.d.ts +22 -0
  30. package/dist/types/utils/validators/number.d.ts +20 -0
  31. package/dist/types/utils/validators/object.d.ts +24 -0
  32. package/dist/types/utils/validators/printer.d.ts +40 -0
  33. package/dist/types/utils/validators/types.d.ts +125 -0
  34. package/dist/types/utils/validators/uuid.d.ts +23 -0
  35. package/package.json +1 -1
  36. package/src/core/BluetoothPrinter.ts +2 -1
  37. package/src/core/di/Container.ts +332 -0
  38. package/src/core/di/Tokens.ts +45 -0
  39. package/src/core/di/index.ts +3 -0
  40. package/src/core/event/EventBus.ts +251 -0
  41. package/src/core/event/index.ts +2 -0
  42. package/src/core/index.ts +10 -4
  43. package/src/core/plugin/PluginManager.ts +161 -0
  44. package/src/core/plugin/index.ts +2 -0
  45. package/src/device/MultiPrinterManager.ts +15 -6
  46. package/src/factory/di-factory.ts +61 -0
  47. package/src/index.ts +50 -1
  48. package/src/providers/ServiceProvider.ts +213 -0
  49. package/src/providers/index.ts +2 -0
  50. package/src/template/TemplateEngine.ts +27 -792
  51. package/src/template/engines/TemplateRenderer.ts +762 -0
  52. package/src/template/parsers/TemplateParser.ts +94 -0
  53. package/src/utils/index.ts +9 -0
  54. package/src/utils/logger.ts +17 -4
  55. package/src/utils/outputLimiter.ts +227 -0
  56. package/src/utils/validation.ts +21 -1138
  57. package/src/utils/validators/array.ts +95 -0
  58. package/src/utils/validators/buffer.ts +81 -0
  59. package/src/utils/validators/chain.ts +181 -0
  60. package/src/utils/validators/common.ts +216 -0
  61. package/src/utils/validators/number.ts +101 -0
  62. package/src/utils/validators/object.ts +63 -0
  63. package/src/utils/validators/printer.ts +294 -0
  64. package/src/utils/validators/types.ts +105 -0
  65. 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
+ }