taro-bluetooth-print 2.5.0 → 2.6.0
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/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/logo.svg +17 -0
- package/dist/manifest.webmanifest +17 -0
- package/dist/types/adapters/WebBluetoothAdapter.d.ts +87 -1
- package/dist/types/adapters/index.d.ts +1 -1
- package/dist/types/barcode/BarcodeGenerator.d.ts +61 -4
- package/dist/types/barcode/index.d.ts +1 -1
- package/dist/types/template/TemplateEngine.d.ts +140 -1
- package/dist/types/utils/uuid.d.ts +191 -0
- package/package.json +1 -1
- package/src/adapters/ReactNativeAdapter.ts +19 -30
- package/src/adapters/WebBluetoothAdapter.ts +275 -14
- package/src/adapters/index.ts +6 -1
- package/src/barcode/BarcodeGenerator.ts +312 -6
- package/src/barcode/index.ts +1 -1
- package/src/drivers/StarPrinter.ts +14 -13
- package/src/encoding/EncodingService.ts +13 -6
- package/src/encoding/korean-japanese.ts +84 -48
- package/src/services/BatchPrintManager.ts +15 -16
- package/src/services/PrintStatistics.ts +8 -8
- package/src/services/ScheduledRetryManager.ts +15 -19
- package/src/template/TemplateEngine.ts +543 -4
- package/src/utils/image.ts +92 -43
- package/src/utils/platform.ts +28 -14
- package/src/utils/uuid.ts +522 -0
- package/src/utils/validation.ts +1155 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Template Engine
|
|
3
3
|
*
|
|
4
4
|
* Provides template parsing and rendering for receipts and labels.
|
|
5
|
-
* Supports variable substitution, conditional rendering, and
|
|
5
|
+
* Supports variable substitution, conditional rendering, loops, and border/table drawing.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```typescript
|
|
@@ -96,6 +96,134 @@ export interface LabelData {
|
|
|
96
96
|
expiryDate?: string;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Loop element for iterating over arrays
|
|
101
|
+
*/
|
|
102
|
+
export interface LoopElement {
|
|
103
|
+
/** Element type identifier */
|
|
104
|
+
type: 'loop';
|
|
105
|
+
/** Variable name to iterate over (array) */
|
|
106
|
+
items: string;
|
|
107
|
+
/** Item variable name for each iteration */
|
|
108
|
+
itemVar: string;
|
|
109
|
+
/** Index variable name (optional) */
|
|
110
|
+
indexVar?: string;
|
|
111
|
+
/** Template elements to render for each item */
|
|
112
|
+
elements: TemplateElement[];
|
|
113
|
+
/** Separator between iterations (optional) */
|
|
114
|
+
separator?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Condition element for conditional rendering
|
|
119
|
+
*/
|
|
120
|
+
export interface ConditionElement {
|
|
121
|
+
/** Element type identifier */
|
|
122
|
+
type: 'condition';
|
|
123
|
+
/** Variable name to evaluate */
|
|
124
|
+
variable: string;
|
|
125
|
+
/** Operator for comparison */
|
|
126
|
+
operator:
|
|
127
|
+
| 'exists'
|
|
128
|
+
| 'not_exists'
|
|
129
|
+
| 'equals'
|
|
130
|
+
| 'not_equals'
|
|
131
|
+
| 'gt'
|
|
132
|
+
| 'gte'
|
|
133
|
+
| 'lt'
|
|
134
|
+
| 'lte'
|
|
135
|
+
| 'truthy'
|
|
136
|
+
| 'falsy';
|
|
137
|
+
/** Value to compare against (for binary operators) */
|
|
138
|
+
value?: unknown;
|
|
139
|
+
/** Elements to render when condition is true */
|
|
140
|
+
then: TemplateElement[];
|
|
141
|
+
/** Elements to render when condition is false (optional) */
|
|
142
|
+
else?: TemplateElement[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Border style for box/table drawing
|
|
147
|
+
*/
|
|
148
|
+
export type BorderStyle = 'single' | 'double' | 'thick' | 'rounded' | 'dashed' | 'none';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Border element for drawing boxes/lines
|
|
152
|
+
*/
|
|
153
|
+
export interface BorderElement {
|
|
154
|
+
/** Element type identifier */
|
|
155
|
+
type: 'border';
|
|
156
|
+
/** Border style */
|
|
157
|
+
style?: BorderStyle;
|
|
158
|
+
/** Top-left corner character */
|
|
159
|
+
topLeft?: string;
|
|
160
|
+
/** Top-right corner character */
|
|
161
|
+
topRight?: string;
|
|
162
|
+
/** Bottom-left corner character */
|
|
163
|
+
bottomLeft?: string;
|
|
164
|
+
/** Bottom-right corner character */
|
|
165
|
+
bottomRight?: string;
|
|
166
|
+
/** Top border character */
|
|
167
|
+
top?: string;
|
|
168
|
+
/** Bottom border character */
|
|
169
|
+
bottom?: string;
|
|
170
|
+
/** Left border character */
|
|
171
|
+
left?: string;
|
|
172
|
+
/** Right border character */
|
|
173
|
+
right?: string;
|
|
174
|
+
/** Intersection character */
|
|
175
|
+
cross?: string;
|
|
176
|
+
/** Whether to draw top border */
|
|
177
|
+
drawTop?: boolean;
|
|
178
|
+
/** Whether to draw bottom border */
|
|
179
|
+
drawBottom?: boolean;
|
|
180
|
+
/** Whether to draw left border */
|
|
181
|
+
drawLeft?: boolean;
|
|
182
|
+
/** Whether to draw right border */
|
|
183
|
+
drawRight?: boolean;
|
|
184
|
+
/** Whether to fill inside with spaces */
|
|
185
|
+
filled?: boolean;
|
|
186
|
+
/** Inner padding (default: 0) */
|
|
187
|
+
padding?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Table column definition
|
|
192
|
+
*/
|
|
193
|
+
export interface TableColumn {
|
|
194
|
+
/** Column header text */
|
|
195
|
+
header: string;
|
|
196
|
+
/** Width of column in characters */
|
|
197
|
+
width: number;
|
|
198
|
+
/** Text alignment for header */
|
|
199
|
+
headerAlign?: TextAlign;
|
|
200
|
+
/** Text alignment for cells */
|
|
201
|
+
cellAlign?: TextAlign;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Table row data
|
|
206
|
+
*/
|
|
207
|
+
export type TableRowData = Record<string, string | number>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Table element for drawing table-like structures
|
|
211
|
+
*/
|
|
212
|
+
export interface TableElement {
|
|
213
|
+
/** Element type identifier */
|
|
214
|
+
type: 'table';
|
|
215
|
+
/** Table columns definition */
|
|
216
|
+
columns: TableColumn[];
|
|
217
|
+
/** Variable name of array to render as rows */
|
|
218
|
+
rowsVar: string;
|
|
219
|
+
/** Whether to draw header row */
|
|
220
|
+
showHeader?: boolean;
|
|
221
|
+
/** Border style for table */
|
|
222
|
+
borderStyle?: BorderStyle;
|
|
223
|
+
/** Whether to alternate row shading */
|
|
224
|
+
alternateRows?: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
99
227
|
/**
|
|
100
228
|
* Template element types
|
|
101
229
|
*/
|
|
@@ -106,7 +234,11 @@ export type TemplateElement =
|
|
|
106
234
|
| { type: 'qrcode'; content: string; size?: number }
|
|
107
235
|
| { type: 'barcode'; content: string; format: BarcodeFormat; height?: number }
|
|
108
236
|
| { type: 'feed'; lines: number }
|
|
109
|
-
| { type: 'variable'; name: string; format?: string }
|
|
237
|
+
| { type: 'variable'; name: string; format?: string }
|
|
238
|
+
| LoopElement
|
|
239
|
+
| ConditionElement
|
|
240
|
+
| BorderElement
|
|
241
|
+
| TableElement;
|
|
110
242
|
|
|
111
243
|
/**
|
|
112
244
|
* Template definition
|
|
@@ -141,6 +273,91 @@ export interface ITemplateEngine {
|
|
|
141
273
|
validate(template: TemplateDefinition, data: Record<string, unknown>): ValidationResult;
|
|
142
274
|
}
|
|
143
275
|
|
|
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
|
+
|
|
144
361
|
/**
|
|
145
362
|
* Template Engine class
|
|
146
363
|
* Renders print templates to ESC/POS commands
|
|
@@ -397,6 +614,17 @@ export class TemplateEngine implements ITemplateEngine {
|
|
|
397
614
|
code: 'MISSING_VARIABLE',
|
|
398
615
|
});
|
|
399
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
|
|
400
628
|
}
|
|
401
629
|
}
|
|
402
630
|
|
|
@@ -407,6 +635,316 @@ export class TemplateEngine implements ITemplateEngine {
|
|
|
407
635
|
* Render a single template element
|
|
408
636
|
*/
|
|
409
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
|
+
[loop.itemVar]: itemData,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
if (loop.indexVar) {
|
|
675
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
676
|
+
context[loop.indexVar] = i;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Render each element in the loop
|
|
680
|
+
for (const childElement of loop.elements) {
|
|
681
|
+
commands.push(...this.renderElement(childElement, context));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Add separator between items (but not after the last)
|
|
685
|
+
if (loop.separator && i < items.length - 1) {
|
|
686
|
+
commands.push(...this.driver.text(loop.separator));
|
|
687
|
+
commands.push(...this.driver.feed(1));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return commands;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Render a condition element
|
|
696
|
+
*/
|
|
697
|
+
private renderCondition(
|
|
698
|
+
condition: ConditionElement,
|
|
699
|
+
data: Record<string, unknown>
|
|
700
|
+
): Uint8Array[] {
|
|
701
|
+
const commands: Uint8Array[] = [];
|
|
702
|
+
const value = this.getNestedValue(data, condition.variable);
|
|
703
|
+
const result = this.evaluateCondition(value, condition.operator, condition.value);
|
|
704
|
+
|
|
705
|
+
const elementsToRender = result ? condition.then : (condition.else ?? []);
|
|
706
|
+
|
|
707
|
+
for (const childElement of elementsToRender) {
|
|
708
|
+
commands.push(...this.renderElement(childElement, data));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return commands;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Evaluate a condition
|
|
716
|
+
*/
|
|
717
|
+
private evaluateCondition(
|
|
718
|
+
value: unknown,
|
|
719
|
+
operator: ConditionElement['operator'],
|
|
720
|
+
compareValue?: unknown
|
|
721
|
+
): boolean {
|
|
722
|
+
switch (operator) {
|
|
723
|
+
case 'exists':
|
|
724
|
+
return value !== undefined && value !== null;
|
|
725
|
+
case 'not_exists':
|
|
726
|
+
return value === undefined || value === null;
|
|
727
|
+
case 'equals':
|
|
728
|
+
return value === compareValue;
|
|
729
|
+
case 'not_equals':
|
|
730
|
+
return value !== compareValue;
|
|
731
|
+
case 'gt':
|
|
732
|
+
return (
|
|
733
|
+
typeof value === 'number' && typeof compareValue === 'number' && value > compareValue
|
|
734
|
+
);
|
|
735
|
+
case 'gte':
|
|
736
|
+
return (
|
|
737
|
+
typeof value === 'number' && typeof compareValue === 'number' && value >= compareValue
|
|
738
|
+
);
|
|
739
|
+
case 'lt':
|
|
740
|
+
return (
|
|
741
|
+
typeof value === 'number' && typeof compareValue === 'number' && value < compareValue
|
|
742
|
+
);
|
|
743
|
+
case 'lte':
|
|
744
|
+
return (
|
|
745
|
+
typeof value === 'number' && typeof compareValue === 'number' && value <= compareValue
|
|
746
|
+
);
|
|
747
|
+
case 'truthy':
|
|
748
|
+
return Boolean(value);
|
|
749
|
+
case 'falsy':
|
|
750
|
+
return !value;
|
|
751
|
+
default:
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Render a border element
|
|
758
|
+
*/
|
|
759
|
+
private renderBorder(border: BorderElement): Uint8Array[] {
|
|
760
|
+
const commands: Uint8Array[] = [];
|
|
761
|
+
const style = BORDER_CHARS[border.style ?? 'single'];
|
|
762
|
+
const width = this.paperWidth;
|
|
763
|
+
const padding = border.padding ?? 0;
|
|
764
|
+
|
|
765
|
+
// Build custom or default characters
|
|
766
|
+
const tl = border.topLeft ?? style.topLeft;
|
|
767
|
+
const tr = border.topRight ?? style.topRight;
|
|
768
|
+
const bl = border.bottomLeft ?? style.bottomLeft;
|
|
769
|
+
const br = border.bottomRight ?? style.bottomRight;
|
|
770
|
+
const t = border.top ?? style.top;
|
|
771
|
+
const b = border.bottom ?? style.bottom;
|
|
772
|
+
const l = border.left ?? style.left;
|
|
773
|
+
const r = border.right ?? style.right;
|
|
774
|
+
|
|
775
|
+
// Top border
|
|
776
|
+
if (border.drawTop !== false) {
|
|
777
|
+
const topLine = tl + t.repeat(width - 2) + tr;
|
|
778
|
+
commands.push(...this.driver.text(topLine));
|
|
779
|
+
commands.push(...this.driver.feed(1));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Middle lines with optional fill
|
|
783
|
+
if (border.filled) {
|
|
784
|
+
const innerWidth = width - 2 - padding * 2;
|
|
785
|
+
const fillChar = ' ';
|
|
786
|
+
for (let i = 0; i < padding; i++) {
|
|
787
|
+
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
788
|
+
commands.push(...this.driver.text(fillLine));
|
|
789
|
+
commands.push(...this.driver.feed(1));
|
|
790
|
+
}
|
|
791
|
+
const contentLine =
|
|
792
|
+
l + fillChar.repeat(padding) + fillChar.repeat(innerWidth) + fillChar.repeat(padding) + r;
|
|
793
|
+
commands.push(...this.driver.text(contentLine));
|
|
794
|
+
commands.push(...this.driver.feed(1));
|
|
795
|
+
for (let i = 0; i < padding; i++) {
|
|
796
|
+
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
797
|
+
commands.push(...this.driver.text(fillLine));
|
|
798
|
+
commands.push(...this.driver.feed(1));
|
|
799
|
+
}
|
|
800
|
+
} else if (border.drawLeft || border.drawRight) {
|
|
801
|
+
const middleLine =
|
|
802
|
+
(border.drawLeft !== false ? l : ' ') +
|
|
803
|
+
' '.repeat(width - 2) +
|
|
804
|
+
(border.drawRight !== false ? r : ' ');
|
|
805
|
+
commands.push(...this.driver.text(middleLine));
|
|
806
|
+
commands.push(...this.driver.feed(1));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Bottom border
|
|
810
|
+
if (border.drawBottom !== false) {
|
|
811
|
+
const bottomLine = bl + b.repeat(width - 2) + br;
|
|
812
|
+
commands.push(...this.driver.text(bottomLine));
|
|
813
|
+
commands.push(...this.driver.feed(1));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return commands;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Render a table element
|
|
821
|
+
*/
|
|
822
|
+
private renderTable(table: TableElement, data: Record<string, unknown>): Uint8Array[] {
|
|
823
|
+
const commands: Uint8Array[] = [];
|
|
824
|
+
const rows = this.getNestedValue(data, table.rowsVar);
|
|
825
|
+
|
|
826
|
+
if (!Array.isArray(rows)) {
|
|
827
|
+
this.logger.warn(`Table rows variable '${table.rowsVar}' is not an array`);
|
|
828
|
+
return commands;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const style = BORDER_CHARS[table.borderStyle ?? 'single'];
|
|
832
|
+
|
|
833
|
+
// Draw top border
|
|
834
|
+
if (table.showHeader) {
|
|
835
|
+
const topLine =
|
|
836
|
+
style.topLeft +
|
|
837
|
+
table.columns
|
|
838
|
+
.map(col => {
|
|
839
|
+
const width = col.width + 1;
|
|
840
|
+
return style.top.repeat(width);
|
|
841
|
+
})
|
|
842
|
+
.join('') +
|
|
843
|
+
style.topRight;
|
|
844
|
+
commands.push(...this.driver.text(topLine));
|
|
845
|
+
commands.push(...this.driver.feed(1));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Draw header row
|
|
849
|
+
if (table.showHeader) {
|
|
850
|
+
let headerContent = style.left + ' ';
|
|
851
|
+
table.columns.forEach((col, i) => {
|
|
852
|
+
const cellText = col.header.substring(0, col.width);
|
|
853
|
+
const aligned = this.alignText(cellText, col.width, col.headerAlign ?? TextAlign.LEFT);
|
|
854
|
+
headerContent +=
|
|
855
|
+
aligned + (i < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
856
|
+
});
|
|
857
|
+
commands.push(...this.driver.text(headerContent));
|
|
858
|
+
commands.push(...this.driver.feed(1));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Draw separator after header
|
|
862
|
+
if (table.showHeader) {
|
|
863
|
+
const sepLine =
|
|
864
|
+
style.cross +
|
|
865
|
+
table.columns
|
|
866
|
+
.map(col => {
|
|
867
|
+
return style.bottom.repeat(col.width + 1);
|
|
868
|
+
})
|
|
869
|
+
.join('') +
|
|
870
|
+
style.cross;
|
|
871
|
+
commands.push(...this.driver.text(sepLine));
|
|
872
|
+
commands.push(...this.driver.feed(1));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Draw data rows
|
|
876
|
+
rows.forEach((rowData, rowIndex) => {
|
|
877
|
+
const row = rowData as TableRowData;
|
|
878
|
+
let rowContent = style.left + ' ';
|
|
879
|
+
|
|
880
|
+
table.columns.forEach((col, colIndex) => {
|
|
881
|
+
const cellValue = row[col.header] ?? '';
|
|
882
|
+
const cellText = String(cellValue).substring(0, col.width);
|
|
883
|
+
const aligned = this.alignText(cellText, col.width, col.cellAlign ?? TextAlign.LEFT);
|
|
884
|
+
rowContent +=
|
|
885
|
+
aligned + (colIndex < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
commands.push(...this.driver.text(rowContent));
|
|
889
|
+
commands.push(...this.driver.feed(1));
|
|
890
|
+
|
|
891
|
+
// Draw row separator
|
|
892
|
+
if (rowIndex < rows.length - 1) {
|
|
893
|
+
const sepLine =
|
|
894
|
+
style.cross +
|
|
895
|
+
table.columns
|
|
896
|
+
.map(col => {
|
|
897
|
+
return style.bottom.repeat(col.width + 1);
|
|
898
|
+
})
|
|
899
|
+
.join('') +
|
|
900
|
+
style.cross;
|
|
901
|
+
commands.push(...this.driver.text(sepLine));
|
|
902
|
+
commands.push(...this.driver.feed(1));
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Draw bottom border
|
|
907
|
+
const bottomLine =
|
|
908
|
+
style.bottomLeft +
|
|
909
|
+
table.columns
|
|
910
|
+
.map(col => {
|
|
911
|
+
return style.bottom.repeat(col.width + 1);
|
|
912
|
+
})
|
|
913
|
+
.join('') +
|
|
914
|
+
style.bottomRight;
|
|
915
|
+
commands.push(...this.driver.text(bottomLine));
|
|
916
|
+
commands.push(...this.driver.feed(1));
|
|
917
|
+
|
|
918
|
+
return commands;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Align text within a specified width
|
|
923
|
+
*/
|
|
924
|
+
private alignText(text: string, width: number, align: TextAlign): string {
|
|
925
|
+
const padded = text.padEnd(width).substring(0, width);
|
|
926
|
+
switch (align) {
|
|
927
|
+
case TextAlign.CENTER: {
|
|
928
|
+
const leftPad = Math.floor((width - text.length) / 2);
|
|
929
|
+
return text.padStart(leftPad + text.length).padEnd(width);
|
|
930
|
+
}
|
|
931
|
+
case TextAlign.RIGHT:
|
|
932
|
+
return text.padStart(width);
|
|
933
|
+
default:
|
|
934
|
+
return padded;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Render standard elements (text, line, image, qrcode, barcode, feed, variable)
|
|
940
|
+
*/
|
|
941
|
+
private renderStandardElement(
|
|
942
|
+
element: Exclude<
|
|
943
|
+
TemplateElement,
|
|
944
|
+
LoopElement | ConditionElement | BorderElement | TableElement
|
|
945
|
+
>,
|
|
946
|
+
data: Record<string, unknown>
|
|
947
|
+
): Uint8Array[] {
|
|
410
948
|
const commands: Uint8Array[] = [];
|
|
411
949
|
|
|
412
950
|
switch (element.type) {
|
|
@@ -483,9 +1021,10 @@ export class TemplateEngine implements ITemplateEngine {
|
|
|
483
1021
|
/**
|
|
484
1022
|
* Render a separator line
|
|
485
1023
|
*/
|
|
486
|
-
private renderLine(char
|
|
1024
|
+
private renderLine(char?: string, length?: number): Uint8Array[] {
|
|
1025
|
+
const lineChar = char ?? '-';
|
|
487
1026
|
const lineLength = length ?? this.paperWidth;
|
|
488
|
-
const line =
|
|
1027
|
+
const line = lineChar.repeat(lineLength);
|
|
489
1028
|
return [...this.driver.text(line), ...this.driver.feed(1)];
|
|
490
1029
|
}
|
|
491
1030
|
|