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
@@ -16,9 +16,8 @@
16
16
  */
17
17
 
18
18
  import { Logger } from '@/utils/logger';
19
- import { TextFormatter, TextAlign } from '@/formatter';
20
- import { BarcodeGenerator, BarcodeFormat } from '@/barcode';
21
- import { EscPos } from '@/drivers/EscPos';
19
+ import { TemplateParser } from './parsers/TemplateParser';
20
+ import { TemplateRenderer } from './engines/TemplateRenderer';
22
21
 
23
22
  /**
24
23
  * Template type
@@ -90,7 +89,7 @@ export interface LabelData {
90
89
  name: string;
91
90
  price: number;
92
91
  barcode?: string;
93
- barcodeFormat?: BarcodeFormat;
92
+ barcodeFormat?: import('@/barcode').BarcodeFormat;
94
93
  spec?: string;
95
94
  productionDate?: string;
96
95
  expiryDate?: string;
@@ -196,9 +195,9 @@ export interface TableColumn {
196
195
  /** Width of column in characters */
197
196
  width: number;
198
197
  /** Text alignment for header */
199
- headerAlign?: TextAlign;
198
+ headerAlign?: import('@/formatter').TextAlign;
200
199
  /** Text alignment for cells */
201
- cellAlign?: TextAlign;
200
+ cellAlign?: import('@/formatter').TextAlign;
202
201
  }
203
202
 
204
203
  /**
@@ -228,11 +227,22 @@ export interface TableElement {
228
227
  * Template element types
229
228
  */
230
229
  export type TemplateElement =
231
- | { type: 'text'; content: string; align?: TextAlign; size?: number; bold?: boolean }
230
+ | {
231
+ type: 'text';
232
+ content: string;
233
+ align?: import('@/formatter').TextAlign;
234
+ size?: number;
235
+ bold?: boolean;
236
+ }
232
237
  | { type: 'line'; char?: string; length?: number }
233
238
  | { type: 'image'; data: Uint8Array; width: number; height: number }
234
239
  | { type: 'qrcode'; content: string; size?: number }
235
- | { type: 'barcode'; content: string; format: BarcodeFormat; height?: number }
240
+ | {
241
+ type: 'barcode';
242
+ content: string;
243
+ format: import('@/barcode').BarcodeFormat;
244
+ height?: number;
245
+ }
236
246
  | { type: 'feed'; lines: number }
237
247
  | { type: 'variable'; name: string; format?: string }
238
248
  | LoopElement
@@ -273,314 +283,43 @@ export interface ITemplateEngine {
273
283
  validate(template: TemplateDefinition, data: Record<string, unknown>): ValidationResult;
274
284
  }
275
285
 
276
- /**
277
- * Border style character sets
278
- */
279
- const BORDER_CHARS: Record<
280
- BorderStyle,
281
- {
282
- topLeft: string;
283
- topRight: string;
284
- bottomLeft: string;
285
- bottomRight: string;
286
- top: string;
287
- bottom: string;
288
- left: string;
289
- right: string;
290
- cross: string;
291
- }
292
- > = {
293
- single: {
294
- topLeft: '+',
295
- topRight: '+',
296
- bottomLeft: '+',
297
- bottomRight: '+',
298
- top: '-',
299
- bottom: '-',
300
- left: '|',
301
- right: '|',
302
- cross: '+',
303
- },
304
- double: {
305
- topLeft: '╔',
306
- topRight: '╗',
307
- bottomLeft: '╚',
308
- bottomRight: '╝',
309
- top: '═',
310
- bottom: '═',
311
- left: '║',
312
- right: '║',
313
- cross: '╬',
314
- },
315
- thick: {
316
- topLeft: '┏',
317
- topRight: '┓',
318
- bottomLeft: '┗',
319
- bottomRight: '┛',
320
- top: '━',
321
- bottom: '━',
322
- left: '┃',
323
- right: '┃',
324
- cross: '╋',
325
- },
326
- rounded: {
327
- topLeft: '╭',
328
- topRight: '╮',
329
- bottomLeft: '╰',
330
- bottomRight: '╯',
331
- top: '─',
332
- bottom: '─',
333
- left: '│',
334
- right: '│',
335
- cross: '┼',
336
- },
337
- dashed: {
338
- topLeft: '+',
339
- topRight: '+',
340
- bottomLeft: '+',
341
- bottomRight: '+',
342
- top: '-',
343
- bottom: '-',
344
- left: ':',
345
- right: ':',
346
- cross: '+',
347
- },
348
- none: {
349
- topLeft: ' ',
350
- topRight: ' ',
351
- bottomLeft: ' ',
352
- bottomRight: ' ',
353
- top: ' ',
354
- bottom: ' ',
355
- left: ' ',
356
- right: ' ',
357
- cross: ' ',
358
- },
359
- };
360
-
361
286
  /**
362
287
  * Template Engine class
363
- * Renders print templates to ESC/POS commands
288
+ * Facade for template parsing and rendering
364
289
  */
365
290
  export class TemplateEngine implements ITemplateEngine {
366
291
  private readonly logger = Logger.scope('TemplateEngine');
367
- private readonly formatter: TextFormatter;
368
- private readonly barcodeGenerator: BarcodeGenerator;
369
- private readonly driver: EscPos;
292
+ private readonly parser: TemplateParser;
293
+ private readonly renderer: TemplateRenderer;
370
294
  private readonly templates: Map<string, TemplateDefinition> = new Map();
371
- private readonly paperWidth: number;
372
295
 
373
296
  /**
374
297
  * Creates a new TemplateEngine instance
375
298
  */
376
299
  constructor(paperWidth = 48) {
377
- this.paperWidth = paperWidth;
378
- this.formatter = new TextFormatter();
379
- this.barcodeGenerator = new BarcodeGenerator();
380
- this.driver = new EscPos();
300
+ this.parser = new TemplateParser();
301
+ this.renderer = new TemplateRenderer(paperWidth);
381
302
  }
382
303
 
383
304
  /**
384
305
  * Render a receipt template
385
306
  */
386
307
  renderReceipt(data: ReceiptData): Uint8Array {
387
- const commands: Uint8Array[] = [];
388
-
389
- // Initialize printer
390
- commands.push(...this.driver.init());
391
-
392
- // Store header
393
- commands.push(...this.formatter.align(TextAlign.CENTER));
394
- commands.push(...this.formatter.setSize(2, 2));
395
- commands.push(...this.formatter.setBold(true));
396
- commands.push(...this.driver.text(data.store.name));
397
- commands.push(...this.driver.feed(1));
398
- commands.push(...this.formatter.resetStyle());
399
-
400
- // Store address and phone
401
- if (data.store.address) {
402
- commands.push(...this.formatter.align(TextAlign.CENTER));
403
- commands.push(...this.driver.text(data.store.address));
404
- commands.push(...this.driver.feed(1));
405
- }
406
- if (data.store.phone) {
407
- commands.push(...this.formatter.align(TextAlign.CENTER));
408
- commands.push(...this.driver.text(`电话: ${data.store.phone}`));
409
- commands.push(...this.driver.feed(1));
410
- }
411
-
412
- // Separator line
413
- commands.push(...this.renderLine());
414
-
415
- // Order info
416
- if (data.order) {
417
- commands.push(...this.formatter.align(TextAlign.LEFT));
418
- commands.push(...this.driver.text(`订单号: ${data.order.id}`));
419
- commands.push(...this.driver.feed(1));
420
- commands.push(...this.driver.text(`日期: ${data.order.date}`));
421
- commands.push(...this.driver.feed(1));
422
- if (data.order.cashier) {
423
- commands.push(...this.driver.text(`收银员: ${data.order.cashier}`));
424
- commands.push(...this.driver.feed(1));
425
- }
426
- commands.push(...this.renderLine());
427
- }
428
-
429
- // Items header
430
- commands.push(...this.formatter.align(TextAlign.LEFT));
431
- commands.push(...this.formatter.setBold(true));
432
- commands.push(...this.driver.text(this.formatItemLine('商品', '数量', '金额')));
433
- commands.push(...this.driver.feed(1));
434
- commands.push(...this.formatter.setBold(false));
435
- commands.push(...this.renderLine('-'));
436
-
437
- // Items
438
- for (const item of data.items) {
439
- const amount = item.quantity * item.price - (item.discount ?? 0);
440
- commands.push(...this.driver.text(item.name));
441
- commands.push(...this.driver.feed(1));
442
- commands.push(
443
- ...this.driver.text(this.formatItemLine('', `x${item.quantity}`, `¥${amount.toFixed(2)}`))
444
- );
445
- commands.push(...this.driver.feed(1));
446
- }
447
-
448
- commands.push(...this.renderLine());
449
-
450
- // Payment summary
451
- commands.push(...this.formatter.align(TextAlign.RIGHT));
452
- if (data.payment.subtotal !== data.payment.total) {
453
- commands.push(...this.driver.text(`小计: ¥${data.payment.subtotal.toFixed(2)}`));
454
- commands.push(...this.driver.feed(1));
455
- }
456
- if (data.payment.tax) {
457
- commands.push(...this.driver.text(`税额: ¥${data.payment.tax.toFixed(2)}`));
458
- commands.push(...this.driver.feed(1));
459
- }
460
- if (data.payment.discount) {
461
- commands.push(...this.driver.text(`优惠: -¥${data.payment.discount.toFixed(2)}`));
462
- commands.push(...this.driver.feed(1));
463
- }
464
-
465
- commands.push(...this.formatter.setBold(true));
466
- commands.push(...this.formatter.setSize(1, 2));
467
- commands.push(...this.driver.text(`合计: ¥${data.payment.total.toFixed(2)}`));
468
- commands.push(...this.driver.feed(1));
469
- commands.push(...this.formatter.resetStyle());
470
-
471
- commands.push(...this.formatter.align(TextAlign.RIGHT));
472
- commands.push(...this.driver.text(`支付方式: ${data.payment.method}`));
473
- commands.push(...this.driver.feed(1));
474
-
475
- if (data.payment.received !== undefined) {
476
- commands.push(...this.driver.text(`实收: ¥${data.payment.received.toFixed(2)}`));
477
- commands.push(...this.driver.feed(1));
478
- }
479
- if (data.payment.change !== undefined) {
480
- commands.push(...this.driver.text(`找零: ¥${data.payment.change.toFixed(2)}`));
481
- commands.push(...this.driver.feed(1));
482
- }
483
-
484
- commands.push(...this.renderLine());
485
-
486
- // QR Code
487
- if (data.qrCode) {
488
- commands.push(...this.formatter.align(TextAlign.CENTER));
489
- commands.push(...this.driver.qr(data.qrCode, { size: 6 }));
490
- commands.push(...this.driver.feed(1));
491
- }
492
-
493
- // Footer
494
- if (data.footer) {
495
- commands.push(...this.formatter.align(TextAlign.CENTER));
496
- commands.push(...this.driver.text(data.footer));
497
- commands.push(...this.driver.feed(1));
498
- } else {
499
- commands.push(...this.formatter.align(TextAlign.CENTER));
500
- commands.push(...this.driver.text('谢谢惠顾,欢迎再次光临!'));
501
- commands.push(...this.driver.feed(1));
502
- }
503
-
504
- commands.push(...this.driver.feed(3));
505
- commands.push(...this.driver.cut());
506
-
507
- return this.combineCommands(commands);
308
+ return this.renderer.renderReceipt(data);
508
309
  }
509
310
 
510
311
  /**
511
312
  * Render a label template
512
313
  */
513
314
  renderLabel(data: LabelData): Uint8Array {
514
- const commands: Uint8Array[] = [];
515
-
516
- // Initialize printer
517
- commands.push(...this.driver.init());
518
-
519
- // Product name
520
- commands.push(...this.formatter.align(TextAlign.CENTER));
521
- commands.push(...this.formatter.setBold(true));
522
- commands.push(...this.driver.text(data.name));
523
- commands.push(...this.driver.feed(1));
524
- commands.push(...this.formatter.setBold(false));
525
-
526
- // Spec
527
- if (data.spec) {
528
- commands.push(...this.driver.text(data.spec));
529
- commands.push(...this.driver.feed(1));
530
- }
531
-
532
- // Price
533
- commands.push(...this.formatter.setSize(2, 2));
534
- commands.push(...this.driver.text(`¥${data.price.toFixed(2)}`));
535
- commands.push(...this.driver.feed(1));
536
- commands.push(...this.formatter.resetStyle());
537
-
538
- // Dates
539
- commands.push(...this.formatter.align(TextAlign.LEFT));
540
- if (data.productionDate) {
541
- commands.push(...this.driver.text(`生产日期: ${data.productionDate}`));
542
- commands.push(...this.driver.feed(1));
543
- }
544
- if (data.expiryDate) {
545
- commands.push(...this.driver.text(`保质期至: ${data.expiryDate}`));
546
- commands.push(...this.driver.feed(1));
547
- }
548
-
549
- // Barcode
550
- if (data.barcode) {
551
- commands.push(...this.formatter.align(TextAlign.CENTER));
552
- const barcodeCommands = this.barcodeGenerator.generate(data.barcode, {
553
- format: data.barcodeFormat ?? BarcodeFormat.CODE128,
554
- height: 60,
555
- showText: true,
556
- });
557
- commands.push(...barcodeCommands);
558
- commands.push(...this.driver.feed(1));
559
- }
560
-
561
- commands.push(...this.driver.feed(2));
562
- commands.push(...this.driver.cut());
563
-
564
- return this.combineCommands(commands);
315
+ return this.renderer.renderLabel(data);
565
316
  }
566
317
 
567
318
  /**
568
319
  * Render a custom template
569
320
  */
570
321
  render(template: TemplateDefinition, data: Record<string, unknown>): Uint8Array {
571
- const commands: Uint8Array[] = [];
572
-
573
- // Initialize printer
574
- commands.push(...this.driver.init());
575
-
576
- for (const element of template.elements) {
577
- commands.push(...this.renderElement(element, data));
578
- }
579
-
580
- commands.push(...this.driver.feed(2));
581
- commands.push(...this.driver.cut());
582
-
583
- return this.combineCommands(commands);
322
+ return this.renderer.render(template, data);
584
323
  }
585
324
 
586
325
  /**
@@ -602,511 +341,7 @@ export class TemplateEngine implements ITemplateEngine {
602
341
  * Validate template data
603
342
  */
604
343
  validate(template: TemplateDefinition, data: Record<string, unknown>): ValidationResult {
605
- const errors: ValidationResult['errors'] = [];
606
-
607
- for (const element of template.elements) {
608
- if (element.type === 'variable') {
609
- const value = this.getNestedValue(data, element.name);
610
- if (value === undefined) {
611
- errors.push({
612
- field: element.name,
613
- message: `Missing required variable: ${element.name}`,
614
- code: 'MISSING_VARIABLE',
615
- });
616
- }
617
- } else if (element.type === 'loop') {
618
- const items = this.getNestedValue(data, element.items);
619
- if (!Array.isArray(items)) {
620
- errors.push({
621
- field: element.items,
622
- message: `Loop variable '${element.items}' must be an array`,
623
- code: 'INVALID_LOOP_VARIABLE',
624
- });
625
- }
626
- } else if (element.type === 'condition') {
627
- // Condition validation is optional - conditions can reference missing data
628
- }
629
- }
630
-
631
- return { valid: errors.length === 0, errors };
632
- }
633
-
634
- /**
635
- * Render a single template element
636
- */
637
- private renderElement(element: TemplateElement, data: Record<string, unknown>): Uint8Array[] {
638
- switch (element.type) {
639
- case 'loop':
640
- return this.renderLoop(element, data);
641
- case 'condition':
642
- return this.renderCondition(element, data);
643
- case 'border':
644
- return this.renderBorder(element);
645
- case 'table':
646
- return this.renderTable(element, data);
647
- default:
648
- return this.renderStandardElement(element, data);
649
- }
650
- }
651
-
652
- /**
653
- * Render a loop element
654
- */
655
- private renderLoop(loop: LoopElement, data: Record<string, unknown>): Uint8Array[] {
656
- const commands: Uint8Array[] = [];
657
- const items = this.getNestedValue(data, loop.items);
658
-
659
- if (!Array || !Array.isArray(items)) {
660
- this.logger.warn(`Loop variable '${loop.items}' is not an array`);
661
- return commands;
662
- }
663
-
664
- for (let i = 0; i < items.length; i++) {
665
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
666
- const itemData: any = items[i];
667
-
668
- // Create iteration context with item and optionally index
669
- const context: Record<string, unknown> = {
670
- ...data,
671
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
672
- [loop.itemVar]: itemData,
673
- /* eslint-enable @typescript-eslint/no-unsafe-assignment */
674
- };
675
-
676
- if (loop.indexVar) {
677
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
678
- context[loop.indexVar] = i;
679
- /* eslint-enable @typescript-eslint/no-unsafe-assignment */
680
- }
681
-
682
- // Render each element in the loop
683
- for (const childElement of loop.elements) {
684
- commands.push(...this.renderElement(childElement, context));
685
- }
686
-
687
- // Add separator between items (but not after the last)
688
- if (loop.separator && i < items.length - 1) {
689
- commands.push(...this.driver.text(loop.separator));
690
- commands.push(...this.driver.feed(1));
691
- }
692
- }
693
-
694
- return commands;
695
- }
696
-
697
- /**
698
- * Render a condition element
699
- */
700
- private renderCondition(
701
- condition: ConditionElement,
702
- data: Record<string, unknown>
703
- ): Uint8Array[] {
704
- const commands: Uint8Array[] = [];
705
- const value = this.getNestedValue(data, condition.variable);
706
- const result = this.evaluateCondition(value, condition.operator, condition.value);
707
-
708
- const elementsToRender = result ? condition.then : (condition.else ?? []);
709
-
710
- for (const childElement of elementsToRender) {
711
- commands.push(...this.renderElement(childElement, data));
712
- }
713
-
714
- return commands;
715
- }
716
-
717
- /**
718
- * Evaluate a condition
719
- */
720
- private evaluateCondition(
721
- value: unknown,
722
- operator: ConditionElement['operator'],
723
- compareValue?: unknown
724
- ): boolean {
725
- switch (operator) {
726
- case 'exists':
727
- return value !== undefined && value !== null;
728
- case 'not_exists':
729
- return value === undefined || value === null;
730
- case 'equals':
731
- return value === compareValue;
732
- case 'not_equals':
733
- return value !== compareValue;
734
- case 'gt':
735
- return (
736
- typeof value === 'number' && typeof compareValue === 'number' && value > compareValue
737
- );
738
- case 'gte':
739
- return (
740
- typeof value === 'number' && typeof compareValue === 'number' && value >= compareValue
741
- );
742
- case 'lt':
743
- return (
744
- typeof value === 'number' && typeof compareValue === 'number' && value < compareValue
745
- );
746
- case 'lte':
747
- return (
748
- typeof value === 'number' && typeof compareValue === 'number' && value <= compareValue
749
- );
750
- case 'truthy':
751
- return Boolean(value);
752
- case 'falsy':
753
- return !value;
754
- default:
755
- return false;
756
- }
757
- }
758
-
759
- /**
760
- * Render a border element
761
- */
762
- private renderBorder(border: BorderElement): Uint8Array[] {
763
- const commands: Uint8Array[] = [];
764
- const style = BORDER_CHARS[border.style ?? 'single'];
765
- const width = this.paperWidth;
766
- const padding = border.padding ?? 0;
767
-
768
- // Build custom or default characters
769
- const tl = border.topLeft ?? style.topLeft;
770
- const tr = border.topRight ?? style.topRight;
771
- const bl = border.bottomLeft ?? style.bottomLeft;
772
- const br = border.bottomRight ?? style.bottomRight;
773
- const t = border.top ?? style.top;
774
- const b = border.bottom ?? style.bottom;
775
- const l = border.left ?? style.left;
776
- const r = border.right ?? style.right;
777
-
778
- // Top border
779
- if (border.drawTop !== false) {
780
- const topLine = tl + t.repeat(width - 2) + tr;
781
- commands.push(...this.driver.text(topLine));
782
- commands.push(...this.driver.feed(1));
783
- }
784
-
785
- // Middle lines with optional fill
786
- if (border.filled) {
787
- const innerWidth = width - 2 - padding * 2;
788
- const fillChar = ' ';
789
- for (let i = 0; i < padding; i++) {
790
- const fillLine = l + fillChar.repeat(width - 2) + r;
791
- commands.push(...this.driver.text(fillLine));
792
- commands.push(...this.driver.feed(1));
793
- }
794
- const contentLine =
795
- l + fillChar.repeat(padding) + fillChar.repeat(innerWidth) + fillChar.repeat(padding) + r;
796
- commands.push(...this.driver.text(contentLine));
797
- commands.push(...this.driver.feed(1));
798
- for (let i = 0; i < padding; i++) {
799
- const fillLine = l + fillChar.repeat(width - 2) + r;
800
- commands.push(...this.driver.text(fillLine));
801
- commands.push(...this.driver.feed(1));
802
- }
803
- } else if (border.drawLeft || border.drawRight) {
804
- const middleLine =
805
- (border.drawLeft !== false ? l : ' ') +
806
- ' '.repeat(width - 2) +
807
- (border.drawRight !== false ? r : ' ');
808
- commands.push(...this.driver.text(middleLine));
809
- commands.push(...this.driver.feed(1));
810
- }
811
-
812
- // Bottom border
813
- if (border.drawBottom !== false) {
814
- const bottomLine = bl + b.repeat(width - 2) + br;
815
- commands.push(...this.driver.text(bottomLine));
816
- commands.push(...this.driver.feed(1));
817
- }
818
-
819
- return commands;
820
- }
821
-
822
- /**
823
- * Render a table element
824
- */
825
- private renderTable(table: TableElement, data: Record<string, unknown>): Uint8Array[] {
826
- const commands: Uint8Array[] = [];
827
- const rows = this.getNestedValue(data, table.rowsVar);
828
-
829
- if (!Array.isArray(rows)) {
830
- this.logger.warn(`Table rows variable '${table.rowsVar}' is not an array`);
831
- return commands;
832
- }
833
-
834
- const style = BORDER_CHARS[table.borderStyle ?? 'single'];
835
-
836
- // Draw top border
837
- if (table.showHeader) {
838
- const topLine =
839
- style.topLeft +
840
- table.columns
841
- .map(col => {
842
- const width = col.width + 1;
843
- return style.top.repeat(width);
844
- })
845
- .join('') +
846
- style.topRight;
847
- commands.push(...this.driver.text(topLine));
848
- commands.push(...this.driver.feed(1));
849
- }
850
-
851
- // Draw header row
852
- if (table.showHeader) {
853
- let headerContent = style.left + ' ';
854
- table.columns.forEach((col, i) => {
855
- const cellText = col.header.substring(0, col.width);
856
- const aligned = this.alignText(cellText, col.width, col.headerAlign ?? TextAlign.LEFT);
857
- headerContent +=
858
- aligned + (i < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
859
- });
860
- commands.push(...this.driver.text(headerContent));
861
- commands.push(...this.driver.feed(1));
862
- }
863
-
864
- // Draw separator after header
865
- if (table.showHeader) {
866
- const sepLine =
867
- style.cross +
868
- table.columns
869
- .map(col => {
870
- return style.bottom.repeat(col.width + 1);
871
- })
872
- .join('') +
873
- style.cross;
874
- commands.push(...this.driver.text(sepLine));
875
- commands.push(...this.driver.feed(1));
876
- }
877
-
878
- // Draw data rows
879
- rows.forEach((rowData, rowIndex) => {
880
- const row = rowData as TableRowData;
881
- let rowContent = style.left + ' ';
882
-
883
- table.columns.forEach((col, colIndex) => {
884
- const cellValue = row[col.header] ?? '';
885
- const cellText = String(cellValue).substring(0, col.width);
886
- const aligned = this.alignText(cellText, col.width, col.cellAlign ?? TextAlign.LEFT);
887
- rowContent +=
888
- aligned + (colIndex < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
889
- });
890
-
891
- commands.push(...this.driver.text(rowContent));
892
- commands.push(...this.driver.feed(1));
893
-
894
- // Draw row separator
895
- if (rowIndex < rows.length - 1) {
896
- const sepLine =
897
- style.cross +
898
- table.columns
899
- .map(col => {
900
- return style.bottom.repeat(col.width + 1);
901
- })
902
- .join('') +
903
- style.cross;
904
- commands.push(...this.driver.text(sepLine));
905
- commands.push(...this.driver.feed(1));
906
- }
907
- });
908
-
909
- // Draw bottom border
910
- const bottomLine =
911
- style.bottomLeft +
912
- table.columns
913
- .map(col => {
914
- return style.bottom.repeat(col.width + 1);
915
- })
916
- .join('') +
917
- style.bottomRight;
918
- commands.push(...this.driver.text(bottomLine));
919
- commands.push(...this.driver.feed(1));
920
-
921
- return commands;
922
- }
923
-
924
- /**
925
- * Align text within a specified width
926
- */
927
- private alignText(text: string, width: number, align: TextAlign): string {
928
- const padded = text.padEnd(width).substring(0, width);
929
- switch (align) {
930
- case TextAlign.CENTER: {
931
- const leftPad = Math.floor((width - padded.length) / 2);
932
- return padded.padStart(leftPad + padded.length).padEnd(width);
933
- }
934
- case TextAlign.RIGHT:
935
- return text.padStart(width);
936
- default:
937
- return padded;
938
- }
939
- }
940
-
941
- /**
942
- * Render standard elements (text, line, image, qrcode, barcode, feed, variable)
943
- */
944
- private renderStandardElement(
945
- element: Exclude<
946
- TemplateElement,
947
- LoopElement | ConditionElement | BorderElement | TableElement
948
- >,
949
- data: Record<string, unknown>
950
- ): Uint8Array[] {
951
- const commands: Uint8Array[] = [];
952
-
953
- switch (element.type) {
954
- case 'text': {
955
- const content = this.substituteVariables(element.content, data);
956
- if (element.align) {
957
- commands.push(...this.formatter.align(element.align));
958
- }
959
- if (element.size) {
960
- commands.push(...this.formatter.setSize(element.size, element.size));
961
- }
962
- if (element.bold) {
963
- commands.push(...this.formatter.setBold(true));
964
- }
965
- commands.push(...this.driver.text(content));
966
- commands.push(...this.driver.feed(1));
967
- if (element.bold) {
968
- commands.push(...this.formatter.setBold(false));
969
- }
970
- if (element.size) {
971
- commands.push(...this.formatter.setSize(1, 1));
972
- }
973
- break;
974
- }
975
-
976
- case 'line':
977
- commands.push(...this.renderLine(element.char, element.length));
978
- break;
979
-
980
- case 'image':
981
- commands.push(...this.driver.image(element.data, element.width, element.height));
982
- commands.push(...this.driver.feed(1));
983
- break;
984
-
985
- case 'qrcode': {
986
- const qrContent = this.substituteVariables(element.content, data);
987
- commands.push(...this.formatter.align(TextAlign.CENTER));
988
- commands.push(...this.driver.qr(qrContent, { size: element.size ?? 6 }));
989
- commands.push(...this.driver.feed(1));
990
- break;
991
- }
992
-
993
- case 'barcode': {
994
- const barcodeContent = this.substituteVariables(element.content, data);
995
- commands.push(...this.formatter.align(TextAlign.CENTER));
996
- const barcodeCommands = this.barcodeGenerator.generate(barcodeContent, {
997
- format: element.format,
998
- height: element.height ?? 60,
999
- showText: true,
1000
- });
1001
- commands.push(...barcodeCommands);
1002
- commands.push(...this.driver.feed(1));
1003
- break;
1004
- }
1005
-
1006
- case 'feed':
1007
- commands.push(...this.driver.feed(element.lines));
1008
- break;
1009
-
1010
- case 'variable': {
1011
- const value = this.getNestedValue(data, element.name);
1012
- if (value !== undefined) {
1013
- const formatted = this.formatValue(value, element.format);
1014
- commands.push(...this.driver.text(formatted));
1015
- commands.push(...this.driver.feed(1));
1016
- }
1017
- break;
1018
- }
1019
- }
1020
-
1021
- return commands;
1022
- }
1023
-
1024
- /**
1025
- * Render a separator line
1026
- */
1027
- private renderLine(char?: string, length?: number): Uint8Array[] {
1028
- const lineChar = char ?? '-';
1029
- const lineLength = length ?? this.paperWidth;
1030
- const line = lineChar.repeat(lineLength);
1031
- return [...this.driver.text(line), ...this.driver.feed(1)];
1032
- }
1033
-
1034
- /**
1035
- * Format an item line with columns
1036
- */
1037
- private formatItemLine(name: string, qty: string, amount: string): string {
1038
- const nameWidth = this.paperWidth - 16;
1039
- const qtyWidth = 8;
1040
- const amountWidth = 8;
1041
-
1042
- const paddedName = name.padEnd(nameWidth).substring(0, nameWidth);
1043
- const paddedQty = qty.padStart(qtyWidth);
1044
- const paddedAmount = amount.padStart(amountWidth);
1045
-
1046
- return `${paddedName}${paddedQty}${paddedAmount}`;
1047
- }
1048
-
1049
- /**
1050
- * Substitute variables in a string
1051
- */
1052
- private substituteVariables(template: string, data: Record<string, unknown>): string {
1053
- return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, key: string) => {
1054
- const value = this.getNestedValue(data, key);
1055
- if (value === undefined) return '';
1056
- if (typeof value === 'object') return JSON.stringify(value);
1057
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
1058
- return String(value);
1059
- });
1060
- }
1061
-
1062
- /**
1063
- * Get nested value from object
1064
- */
1065
- private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
1066
- const keys = path.split('.');
1067
- let current: unknown = obj;
1068
-
1069
- for (const key of keys) {
1070
- if (current === null || current === undefined) {
1071
- return undefined;
1072
- }
1073
- if (typeof current === 'object') {
1074
- current = (current as Record<string, unknown>)[key];
1075
- } else {
1076
- return undefined;
1077
- }
1078
- }
1079
-
1080
- return current;
1081
- }
1082
-
1083
- /**
1084
- * Format a value with optional format string
1085
- */
1086
- private formatValue(value: unknown, format?: string): string {
1087
- if (format === 'currency' && typeof value === 'number') {
1088
- return `¥${value.toFixed(2)}`;
1089
- }
1090
- if (format === 'date' && value instanceof Date) {
1091
- return value.toLocaleDateString('zh-CN');
1092
- }
1093
- return String(value);
1094
- }
1095
-
1096
- /**
1097
- * Combine multiple command arrays into one
1098
- */
1099
- private combineCommands(commands: Uint8Array[]): Uint8Array {
1100
- const totalLength = commands.reduce((acc, cmd) => acc + cmd.length, 0);
1101
- const result = new Uint8Array(totalLength);
1102
- let offset = 0;
1103
-
1104
- for (const cmd of commands) {
1105
- result.set(cmd, offset);
1106
- offset += cmd.length;
1107
- }
1108
-
1109
- return result;
344
+ return this.parser.validate(template, data);
1110
345
  }
1111
346
  }
1112
347