@thermal-print/escpos 0.1.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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/command-adapters/escbematech-adapter.d.ts +30 -0
- package/dist/command-adapters/escbematech-adapter.d.ts.map +1 -0
- package/dist/command-adapters/escbematech-adapter.js +175 -0
- package/dist/command-adapters/escpos-adapter.d.ts +25 -0
- package/dist/command-adapters/escpos-adapter.d.ts.map +1 -0
- package/dist/command-adapters/escpos-adapter.js +144 -0
- package/dist/command-adapters/index.d.ts +11 -0
- package/dist/command-adapters/index.d.ts.map +1 -0
- package/dist/command-adapters/index.js +14 -0
- package/dist/command-adapters/types.d.ts +78 -0
- package/dist/command-adapters/types.d.ts.map +1 -0
- package/dist/command-adapters/types.js +8 -0
- package/dist/commands/escbematech.d.ts +867 -0
- package/dist/commands/escbematech.d.ts.map +1 -0
- package/dist/commands/escbematech.js +1387 -0
- package/dist/commands/escpos.d.ts +582 -0
- package/dist/commands/escpos.d.ts.map +1 -0
- package/dist/commands/escpos.js +1048 -0
- package/dist/converter.d.ts +47 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +92 -0
- package/dist/encodings/cp860.d.ts +25 -0
- package/dist/encodings/cp860.d.ts.map +1 -0
- package/dist/encodings/cp860.js +187 -0
- package/dist/generator.d.ts +118 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +383 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/styles.d.ts +72 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +210 -0
- package/dist/traverser.d.ts +60 -0
- package/dist/traverser.d.ts.map +1 -0
- package/dist/traverser.js +285 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/package.json +31 -0
package/dist/styles.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractTextStyle = extractTextStyle;
|
|
4
|
+
exports.extractViewStyle = extractViewStyle;
|
|
5
|
+
exports.isBold = isBold;
|
|
6
|
+
exports.mapFontSizeToESCPOS = mapFontSizeToESCPOS;
|
|
7
|
+
exports.mapTextAlign = mapTextAlign;
|
|
8
|
+
exports.calculateSpacing = calculateSpacing;
|
|
9
|
+
exports.isDashedBorder = isDashedBorder;
|
|
10
|
+
exports.generateDividerLine = generateDividerLine;
|
|
11
|
+
exports.mergeStyles = mergeStyles;
|
|
12
|
+
exports.parseWidth = parseWidth;
|
|
13
|
+
exports.alignTextInColumn = alignTextInColumn;
|
|
14
|
+
exports.wrapText = wrapText;
|
|
15
|
+
const cp860_1 = require("./encodings/cp860");
|
|
16
|
+
/**
|
|
17
|
+
* Extracts text style from a style object
|
|
18
|
+
*/
|
|
19
|
+
function extractTextStyle(style) {
|
|
20
|
+
return {
|
|
21
|
+
fontSize: style?.fontSize,
|
|
22
|
+
fontWeight: style?.fontWeight,
|
|
23
|
+
fontFamily: style?.fontFamily,
|
|
24
|
+
textAlign: style?.textAlign || "left",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extracts view/layout style from a style object
|
|
29
|
+
*/
|
|
30
|
+
function extractViewStyle(style) {
|
|
31
|
+
return {
|
|
32
|
+
display: style?.display,
|
|
33
|
+
flexDirection: style?.flexDirection || "column",
|
|
34
|
+
justifyContent: style?.justifyContent,
|
|
35
|
+
alignItems: style?.alignItems,
|
|
36
|
+
padding: style?.padding,
|
|
37
|
+
paddingTop: style?.paddingTop,
|
|
38
|
+
paddingBottom: style?.paddingBottom,
|
|
39
|
+
paddingLeft: style?.paddingLeft,
|
|
40
|
+
paddingRight: style?.paddingRight,
|
|
41
|
+
margin: style?.margin,
|
|
42
|
+
marginTop: style?.marginTop,
|
|
43
|
+
marginBottom: style?.marginBottom,
|
|
44
|
+
marginLeft: style?.marginLeft,
|
|
45
|
+
marginRight: style?.marginRight,
|
|
46
|
+
borderBottom: style?.borderBottom,
|
|
47
|
+
borderTop: style?.borderTop,
|
|
48
|
+
width: style?.width,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Determines if text should be bold
|
|
53
|
+
*/
|
|
54
|
+
function isBold(style) {
|
|
55
|
+
return (style.fontWeight === "bold" ||
|
|
56
|
+
style.fontWeight === 700 ||
|
|
57
|
+
style.fontWeight === "Helvetica-Bold" ||
|
|
58
|
+
style.fontFamily === "Helvetica-Bold");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Maps fontSize to ESC/POS character size (width x height multipliers)
|
|
62
|
+
*
|
|
63
|
+
* Font size mapping using ESC ! command (limited to 2x2 maximum):
|
|
64
|
+
* - 8-12px → 1x1 (normal)
|
|
65
|
+
* - 13-18px → 1x2 (normal width, double height)
|
|
66
|
+
* - 19-24px → 2x1 (double width, normal height)
|
|
67
|
+
* - 25+px → 2x2 (double width, double height)
|
|
68
|
+
*
|
|
69
|
+
* Note: ESC ! command only supports up to 2x2 character size.
|
|
70
|
+
* Larger sizes (3x, 4x, etc.) would require GS ! command which may not
|
|
71
|
+
* be supported on all thermal printers (e.g., Bematech MP-4200 TH).
|
|
72
|
+
*
|
|
73
|
+
* PDF uses zoom factor (default 0.46), so actual sizes are smaller:
|
|
74
|
+
* - 16 * 0.46 = 7.36 (normal text)
|
|
75
|
+
* - 18 * 0.46 = 8.28 (medium text)
|
|
76
|
+
* - 20 * 0.46 = 9.2 (title text)
|
|
77
|
+
*
|
|
78
|
+
* We use raw fontSize values (ignoring zoom) for better differentiation
|
|
79
|
+
*/
|
|
80
|
+
function mapFontSizeToESCPOS(fontSize) {
|
|
81
|
+
// Default to 1x1 (normal size)
|
|
82
|
+
if (!fontSize)
|
|
83
|
+
return { width: 1, height: 1 };
|
|
84
|
+
// Parse fontSize if it's a string (e.g., "8.28px")
|
|
85
|
+
const size = typeof fontSize === "string" ? parseFloat(fontSize) : fontSize;
|
|
86
|
+
if (isNaN(size))
|
|
87
|
+
return { width: 1, height: 1 };
|
|
88
|
+
// Map font size to character multipliers (max 2x2 for ESC ! compatibility)
|
|
89
|
+
if (size >= 25)
|
|
90
|
+
return { width: 2, height: 2 }; // 25+px → 2x2 (maximum)
|
|
91
|
+
if (size >= 19)
|
|
92
|
+
return { width: 2, height: 1 }; // 19-24px → 2x1
|
|
93
|
+
if (size >= 13)
|
|
94
|
+
return { width: 1, height: 2 }; // 13-18px → 1x2
|
|
95
|
+
return { width: 1, height: 1 }; // 8-12px → 1x1 (normal)
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Maps textAlign to ESC/POS alignment
|
|
99
|
+
*/
|
|
100
|
+
function mapTextAlign(textAlign) {
|
|
101
|
+
if (textAlign === "center")
|
|
102
|
+
return "center";
|
|
103
|
+
if (textAlign === "right")
|
|
104
|
+
return "right";
|
|
105
|
+
return "left";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Calculates spacing (margin/padding) in lines
|
|
109
|
+
* Approximates pixels to line feeds
|
|
110
|
+
*/
|
|
111
|
+
function calculateSpacing(value) {
|
|
112
|
+
if (!value)
|
|
113
|
+
return 0;
|
|
114
|
+
// Rough approximation: ~20 pixels = 1 line feed
|
|
115
|
+
return Math.round(value / 20);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Determines if a border is dashed
|
|
119
|
+
*/
|
|
120
|
+
function isDashedBorder(border) {
|
|
121
|
+
return border?.includes("dashed") ?? false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generates a divider line based on border style
|
|
125
|
+
*/
|
|
126
|
+
function generateDividerLine(width, dashed = false) {
|
|
127
|
+
const char = dashed ? "-" : "─";
|
|
128
|
+
return char.repeat(width);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Merges multiple style objects (handles spread syntax)
|
|
132
|
+
*/
|
|
133
|
+
function mergeStyles(...styles) {
|
|
134
|
+
return Object.assign({}, ...styles.filter((s) => s));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parses width percentage to column width in characters
|
|
138
|
+
* Uses Math.round() to minimize rounding errors
|
|
139
|
+
*/
|
|
140
|
+
function parseWidth(width, totalWidth) {
|
|
141
|
+
if (!width)
|
|
142
|
+
return totalWidth;
|
|
143
|
+
if (typeof width === "number")
|
|
144
|
+
return width;
|
|
145
|
+
if (typeof width === "string") {
|
|
146
|
+
if (width.includes("%")) {
|
|
147
|
+
const percentage = parseInt(width.replace("%", ""));
|
|
148
|
+
return Math.round((percentage / 100) * totalWidth);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return totalWidth;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Aligns text within a column width (using CP860 byte length for accurate padding)
|
|
155
|
+
*/
|
|
156
|
+
function alignTextInColumn(text, width, align) {
|
|
157
|
+
// Get actual byte length when encoded to CP860
|
|
158
|
+
let encodedLength = (0, cp860_1.encodeCP860)(text).length;
|
|
159
|
+
let truncatedText = text;
|
|
160
|
+
// Truncate if too long (character by character until it fits)
|
|
161
|
+
while (encodedLength > width && truncatedText.length > 0) {
|
|
162
|
+
truncatedText = truncatedText.substring(0, truncatedText.length - 1);
|
|
163
|
+
encodedLength = (0, cp860_1.encodeCP860)(truncatedText).length;
|
|
164
|
+
}
|
|
165
|
+
// Calculate padding based on actual encoded byte length
|
|
166
|
+
const padding = width - encodedLength;
|
|
167
|
+
// Already fits perfectly
|
|
168
|
+
if (padding === 0) {
|
|
169
|
+
return truncatedText;
|
|
170
|
+
}
|
|
171
|
+
// Pad based on alignment
|
|
172
|
+
if (align === "right") {
|
|
173
|
+
return " ".repeat(padding) + truncatedText;
|
|
174
|
+
}
|
|
175
|
+
else if (align === "center") {
|
|
176
|
+
const leftPad = Math.floor(padding / 2);
|
|
177
|
+
const rightPad = padding - leftPad;
|
|
178
|
+
return " ".repeat(leftPad) + truncatedText + " ".repeat(rightPad);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// left align
|
|
182
|
+
return truncatedText + " ".repeat(padding);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Splits text into multiple lines if it exceeds width
|
|
187
|
+
*/
|
|
188
|
+
function wrapText(text, width) {
|
|
189
|
+
if (text.length <= width) {
|
|
190
|
+
return [text];
|
|
191
|
+
}
|
|
192
|
+
const lines = [];
|
|
193
|
+
let currentLine = "";
|
|
194
|
+
const words = text.split(" ");
|
|
195
|
+
for (const word of words) {
|
|
196
|
+
if ((currentLine + " " + word).trim().length <= width) {
|
|
197
|
+
currentLine = (currentLine + " " + word).trim();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if (currentLine) {
|
|
201
|
+
lines.push(currentLine);
|
|
202
|
+
}
|
|
203
|
+
currentLine = word;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (currentLine) {
|
|
207
|
+
lines.push(currentLine);
|
|
208
|
+
}
|
|
209
|
+
return lines.length > 0 ? lines : [text.substring(0, width)];
|
|
210
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ElementNode } from "@thermal-print/core";
|
|
2
|
+
import { ESCPOSGenerator } from "./generator";
|
|
3
|
+
/**
|
|
4
|
+
* Tree Traverser
|
|
5
|
+
* Walks through the element tree and generates ESC/POS commands
|
|
6
|
+
*/
|
|
7
|
+
export declare class TreeTraverser {
|
|
8
|
+
private generator;
|
|
9
|
+
constructor(generator: ESCPOSGenerator);
|
|
10
|
+
/**
|
|
11
|
+
* Traverse the entire tree starting from root
|
|
12
|
+
*/
|
|
13
|
+
traverse(node: ElementNode | null): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Handle Document element
|
|
16
|
+
*/
|
|
17
|
+
private handleDocument;
|
|
18
|
+
/**
|
|
19
|
+
* Handle Page element
|
|
20
|
+
*/
|
|
21
|
+
private handlePage;
|
|
22
|
+
/**
|
|
23
|
+
* Handle View element (container with layout)
|
|
24
|
+
*/
|
|
25
|
+
private handleView;
|
|
26
|
+
/**
|
|
27
|
+
* Handle column layout (stacked vertically)
|
|
28
|
+
* Adds newlines between sibling elements for proper spacing
|
|
29
|
+
*/
|
|
30
|
+
private handleColumnLayout;
|
|
31
|
+
/**
|
|
32
|
+
* Handle row layout (side-by-side columns)
|
|
33
|
+
*/
|
|
34
|
+
private handleRowLayout;
|
|
35
|
+
/**
|
|
36
|
+
* Find the first Text node in a tree
|
|
37
|
+
*/
|
|
38
|
+
private findFirstTextNode;
|
|
39
|
+
/**
|
|
40
|
+
* Collect text content from a node and its children
|
|
41
|
+
*/
|
|
42
|
+
private collectTextContent;
|
|
43
|
+
/**
|
|
44
|
+
* Handle Text element
|
|
45
|
+
*/
|
|
46
|
+
private handleText;
|
|
47
|
+
/**
|
|
48
|
+
* Handle TextNode (raw text)
|
|
49
|
+
*/
|
|
50
|
+
private handleTextNode;
|
|
51
|
+
/**
|
|
52
|
+
* Handle Image element
|
|
53
|
+
*/
|
|
54
|
+
private handleImage;
|
|
55
|
+
/**
|
|
56
|
+
* Traverse children nodes
|
|
57
|
+
*/
|
|
58
|
+
private traverseChildren;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=traverser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traverser.d.ts","sourceRoot":"","sources":["../src/traverser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAkB;gBAEvB,SAAS,EAAE,eAAe;IAItC;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BvD;;OAEG;YACW,cAAc;IAO5B;;OAEG;YACW,UAAU;IAIxB;;OAEG;YACW,UAAU;IAkBxB;;;OAGG;YACW,kBAAkB;IAchC;;OAEG;YACW,eAAe;IA+F7B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB;;OAEG;YACW,kBAAkB;IAmBhC;;OAEG;YACW,UAAU;IAgCxB;;OAEG;YACW,cAAc;IAM5B;;OAEG;YACW,WAAW;IA+BzB;;OAEG;YACW,gBAAgB;CAK/B"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TreeTraverser = void 0;
|
|
4
|
+
const styles_1 = require("./styles");
|
|
5
|
+
/**
|
|
6
|
+
* Tree Traverser
|
|
7
|
+
* Walks through the element tree and generates ESC/POS commands
|
|
8
|
+
*/
|
|
9
|
+
class TreeTraverser {
|
|
10
|
+
constructor(generator) {
|
|
11
|
+
this.generator = generator;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Traverse the entire tree starting from root
|
|
15
|
+
*/
|
|
16
|
+
async traverse(node) {
|
|
17
|
+
if (!node)
|
|
18
|
+
return;
|
|
19
|
+
// Work with lowercase normalized types
|
|
20
|
+
const nodeType = node.type.toLowerCase();
|
|
21
|
+
switch (nodeType) {
|
|
22
|
+
case "document":
|
|
23
|
+
await this.handleDocument(node);
|
|
24
|
+
break;
|
|
25
|
+
case "page":
|
|
26
|
+
await this.handlePage(node);
|
|
27
|
+
break;
|
|
28
|
+
case "view":
|
|
29
|
+
await this.handleView(node);
|
|
30
|
+
break;
|
|
31
|
+
case "text":
|
|
32
|
+
await this.handleText(node);
|
|
33
|
+
break;
|
|
34
|
+
case "textnode":
|
|
35
|
+
await this.handleTextNode(node);
|
|
36
|
+
break;
|
|
37
|
+
case "image":
|
|
38
|
+
await this.handleImage(node);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
// Unknown element, traverse children
|
|
42
|
+
await this.traverseChildren(node);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Handle Document element
|
|
47
|
+
*/
|
|
48
|
+
async handleDocument(node) {
|
|
49
|
+
this.generator.initialize();
|
|
50
|
+
await this.traverseChildren(node);
|
|
51
|
+
// Add 6 blank lines at end for paper feed spacing
|
|
52
|
+
this.generator.addNewline(2);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Handle Page element
|
|
56
|
+
*/
|
|
57
|
+
async handlePage(node) {
|
|
58
|
+
await this.traverseChildren(node);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Handle View element (container with layout)
|
|
62
|
+
*/
|
|
63
|
+
async handleView(node) {
|
|
64
|
+
const viewStyle = (0, styles_1.extractViewStyle)(node.style);
|
|
65
|
+
// Apply spacing before
|
|
66
|
+
this.generator.applyViewSpacing(node.style, "before");
|
|
67
|
+
// Handle different layout modes
|
|
68
|
+
if (viewStyle.flexDirection === "row") {
|
|
69
|
+
await this.handleRowLayout(node);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Column layout (default) - add spacing between children
|
|
73
|
+
await this.handleColumnLayout(node);
|
|
74
|
+
}
|
|
75
|
+
// Apply spacing after
|
|
76
|
+
this.generator.applyViewSpacing(node.style, "after");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Handle column layout (stacked vertically)
|
|
80
|
+
* Adds newlines between sibling elements for proper spacing
|
|
81
|
+
*/
|
|
82
|
+
async handleColumnLayout(node) {
|
|
83
|
+
const children = node.children;
|
|
84
|
+
for (let i = 0; i < children.length; i++) {
|
|
85
|
+
await this.traverse(children[i]);
|
|
86
|
+
// Add newline after each child except the last one
|
|
87
|
+
// This ensures proper spacing between elements in column layout
|
|
88
|
+
// if (i < children.length - 1 && children[i].type.toLowerCase() === "view") {
|
|
89
|
+
// this.generator.addNewline();
|
|
90
|
+
// }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handle row layout (side-by-side columns)
|
|
95
|
+
*/
|
|
96
|
+
async handleRowLayout(node) {
|
|
97
|
+
const children = node.children;
|
|
98
|
+
if (children.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
// If there's only 1 child, just render it normally (not as a row)
|
|
101
|
+
// This prevents nested column layouts from being flattened
|
|
102
|
+
if (children.length === 1) {
|
|
103
|
+
await this.traverse(children[0]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const viewStyle = (0, styles_1.extractViewStyle)(node.style);
|
|
107
|
+
const paperWidth = this.generator.getPaperWidth();
|
|
108
|
+
// Check layout justification mode
|
|
109
|
+
const isSpaceBetween = viewStyle.justifyContent === "space-between";
|
|
110
|
+
const isCentered = viewStyle.justifyContent === "center";
|
|
111
|
+
// Calculate column widths
|
|
112
|
+
const columns = [];
|
|
113
|
+
for (const child of children) {
|
|
114
|
+
const childStyle = (0, styles_1.extractViewStyle)(child.style);
|
|
115
|
+
const width = (0, styles_1.parseWidth)(childStyle.width, paperWidth);
|
|
116
|
+
// Collect text content from child
|
|
117
|
+
const content = await this.collectTextContent(child);
|
|
118
|
+
// Determine alignment - check for Text node textAlign first
|
|
119
|
+
let align = "left";
|
|
120
|
+
// Look for Text element with textAlign
|
|
121
|
+
const textNode = this.findFirstTextNode(child);
|
|
122
|
+
if (textNode && textNode.style) {
|
|
123
|
+
const textStyle = (0, styles_1.extractTextStyle)(textNode.style);
|
|
124
|
+
align = (0, styles_1.mapTextAlign)(textStyle.textAlign);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Fall back to View alignment
|
|
128
|
+
if (childStyle.alignItems === "center" || childStyle.justifyContent === "center") {
|
|
129
|
+
align = "center";
|
|
130
|
+
}
|
|
131
|
+
else if (childStyle.alignItems === "flex-end" || childStyle.justifyContent === "flex-end") {
|
|
132
|
+
align = "right";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
columns.push({ node: child, width, content, align });
|
|
136
|
+
}
|
|
137
|
+
// Build row text
|
|
138
|
+
let rowText = "";
|
|
139
|
+
// Check if columns have explicit widths (table layout) or should use space-between
|
|
140
|
+
const hasExplicitWidths = columns.some((col) => {
|
|
141
|
+
const childStyle = (0, styles_1.extractViewStyle)(col.node.style);
|
|
142
|
+
return childStyle.width !== undefined;
|
|
143
|
+
});
|
|
144
|
+
if (isSpaceBetween && columns.length === 2 && !hasExplicitWidths) {
|
|
145
|
+
// Special handling for space-between layout (payment summary style) WITHOUT explicit widths
|
|
146
|
+
// Calculate space between
|
|
147
|
+
const usedSpace = columns[0].content.length + columns[1].content.length;
|
|
148
|
+
const gap = Math.max(1, paperWidth - usedSpace);
|
|
149
|
+
rowText = columns[0].content + " ".repeat(gap) + columns[1].content;
|
|
150
|
+
}
|
|
151
|
+
else if (isCentered && !hasExplicitWidths) {
|
|
152
|
+
// Center the entire row on the paper by calculating total content width
|
|
153
|
+
// and adding leading spaces
|
|
154
|
+
// Add spacing between columns (1 space per gap)
|
|
155
|
+
const spacingBetweenColumns = Math.max(0, columns.length - 1);
|
|
156
|
+
const totalContentWidth = columns.reduce((sum, col) => sum + col.content.length, 0) + spacingBetweenColumns;
|
|
157
|
+
// Calculate leading spaces to center the entire row
|
|
158
|
+
const leadingSpaces = Math.max(0, Math.floor((paperWidth - totalContentWidth) / 2));
|
|
159
|
+
// Build row with leading spaces (center entire row)
|
|
160
|
+
rowText = " ".repeat(leadingSpaces);
|
|
161
|
+
for (let i = 0; i < columns.length; i++) {
|
|
162
|
+
if (i > 0) {
|
|
163
|
+
rowText += " "; // Add space between columns
|
|
164
|
+
}
|
|
165
|
+
rowText += columns[i].content;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Normal column layout OR space-between with explicit widths (use column padding)
|
|
170
|
+
for (let i = 0; i < columns.length; i++) {
|
|
171
|
+
const col = columns[i];
|
|
172
|
+
const cellText = (0, styles_1.alignTextInColumn)(col.content, col.width, col.align);
|
|
173
|
+
rowText += cellText;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.generator.addText(rowText);
|
|
177
|
+
this.generator.addNewline();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Find the first Text node in a tree
|
|
181
|
+
*/
|
|
182
|
+
findFirstTextNode(node) {
|
|
183
|
+
const normalizedType = node.type.toLowerCase();
|
|
184
|
+
if (normalizedType === "text") {
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
for (const child of node.children) {
|
|
188
|
+
const found = this.findFirstTextNode(child);
|
|
189
|
+
if (found)
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Collect text content from a node and its children
|
|
196
|
+
*/
|
|
197
|
+
async collectTextContent(node) {
|
|
198
|
+
let text = "";
|
|
199
|
+
const normalizedType = node.type.toLowerCase();
|
|
200
|
+
if (normalizedType === "text" || normalizedType === "textnode") {
|
|
201
|
+
// Get text from props.children
|
|
202
|
+
if (node.props.children !== undefined) {
|
|
203
|
+
text += String(node.props.children);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Recursively collect from children
|
|
207
|
+
for (const child of node.children) {
|
|
208
|
+
text += await this.collectTextContent(child);
|
|
209
|
+
}
|
|
210
|
+
return text;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Handle Text element
|
|
214
|
+
*/
|
|
215
|
+
async handleText(node) {
|
|
216
|
+
// Merge styles from parent if needed
|
|
217
|
+
const style = (0, styles_1.mergeStyles)(node.style);
|
|
218
|
+
// Apply text styling (sets alignment, bold, size)
|
|
219
|
+
this.generator.applyTextStyle(style);
|
|
220
|
+
// Get text content
|
|
221
|
+
let textContent = "";
|
|
222
|
+
// Check props.children first
|
|
223
|
+
if (node.props.children !== undefined) {
|
|
224
|
+
if (typeof node.props.children === "string" || typeof node.props.children === "number") {
|
|
225
|
+
textContent = String(node.props.children);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// If we have direct text, print it
|
|
229
|
+
if (textContent) {
|
|
230
|
+
this.generator.addText(textContent);
|
|
231
|
+
}
|
|
232
|
+
// Traverse children (nested Text elements)
|
|
233
|
+
await this.traverseChildren(node);
|
|
234
|
+
// Reset formatting after text (especially bold)
|
|
235
|
+
this.generator.resetFormatting();
|
|
236
|
+
// Add newline after text element
|
|
237
|
+
this.generator.addNewline();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Handle TextNode (raw text)
|
|
241
|
+
*/
|
|
242
|
+
async handleTextNode(node) {
|
|
243
|
+
if (node.props.children) {
|
|
244
|
+
this.generator.addText(String(node.props.children));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Handle Image element
|
|
249
|
+
*/
|
|
250
|
+
async handleImage(node) {
|
|
251
|
+
const source = node.props.source || node.props.src;
|
|
252
|
+
if (source) {
|
|
253
|
+
// Apply alignment from style (check both node style and parent style)
|
|
254
|
+
const style = (0, styles_1.mergeStyles)(node.style);
|
|
255
|
+
const viewStyle = (0, styles_1.extractViewStyle)(style);
|
|
256
|
+
const textStyle = (0, styles_1.extractTextStyle)(style);
|
|
257
|
+
// Determine alignment from textAlign or justifyContent
|
|
258
|
+
let align = "left";
|
|
259
|
+
if (textStyle.textAlign) {
|
|
260
|
+
align = (0, styles_1.mapTextAlign)(textStyle.textAlign);
|
|
261
|
+
}
|
|
262
|
+
else if (viewStyle.justifyContent === "center" || viewStyle.alignItems === "center") {
|
|
263
|
+
align = "center";
|
|
264
|
+
}
|
|
265
|
+
else if (viewStyle.justifyContent === "flex-end" || viewStyle.alignItems === "flex-end") {
|
|
266
|
+
align = "right";
|
|
267
|
+
}
|
|
268
|
+
// Set alignment before adding image
|
|
269
|
+
this.generator.setAlign(align);
|
|
270
|
+
await this.generator.addImage(source);
|
|
271
|
+
// Reset alignment
|
|
272
|
+
this.generator.setAlign("left");
|
|
273
|
+
}
|
|
274
|
+
this.generator.addNewline();
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Traverse children nodes
|
|
278
|
+
*/
|
|
279
|
+
async traverseChildren(node) {
|
|
280
|
+
for (const child of node.children) {
|
|
281
|
+
await this.traverse(child);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exports.TreeTraverser = TreeTraverser;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESC/POS-specific type definitions
|
|
3
|
+
*/
|
|
4
|
+
export interface ESCPOSCommand {
|
|
5
|
+
type: 'raw' | 'text' | 'feed' | 'cut' | 'image' | 'qr';
|
|
6
|
+
data?: any;
|
|
7
|
+
buffer?: Buffer;
|
|
8
|
+
}
|
|
9
|
+
export interface ConversionContext {
|
|
10
|
+
paperWidth: number;
|
|
11
|
+
currentAlign: 'left' | 'center' | 'right';
|
|
12
|
+
currentSize: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
currentBold: boolean;
|
|
17
|
+
encoding: string;
|
|
18
|
+
debug: boolean;
|
|
19
|
+
buffer: Buffer[];
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;IACvD,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC1C,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thermal-print/escpos",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESC/POS command generation and thermal printer control",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"thermal-printer",
|
|
9
|
+
"escpos",
|
|
10
|
+
"receipt-printer",
|
|
11
|
+
"pos"
|
|
12
|
+
],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@thermal-print/core": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc --build",
|
|
29
|
+
"build:watch": "tsc --build --watch"
|
|
30
|
+
}
|
|
31
|
+
}
|