@thermal-print/escpos 0.3.1-beta.6 → 0.3.1-beta.7

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/traverser.js CHANGED
@@ -1,10 +1,16 @@
1
- import { alignTextInColumn, extractTextStyle, extractViewStyle, mapTextAlign, mergeStyles, parseWidth, wrapText } from "./styles";
1
+ import { alignTextInColumn, calculateHorizontalSpacing, distributeColumnWidths, distributeGaps, extractTextStyle, extractViewStyle, FONT_LEVEL_COLUMN_UNITS, isBold, mapTextAlign, mergeStyles, parsePercentageWidth, parseSize, wrapText, } from "./styles";
2
2
  /**
3
3
  * Tree Traverser
4
4
  * Walks through the element tree and generates ESC/POS commands
5
5
  */
6
6
  export class TreeTraverser {
7
7
  constructor(generator) {
8
+ /**
9
+ * Alignment inherited from an ancestor View's `alignItems`, mirroring
10
+ * PDFTraverser.alignmentContext. A Text or Image without its own textAlign
11
+ * follows it.
12
+ */
13
+ this.alignmentContext = "left";
8
14
  this.generator = generator;
9
15
  }
10
16
  /**
@@ -45,46 +51,97 @@ export class TreeTraverser {
45
51
  async handleDocument(node) {
46
52
  this.generator.initialize();
47
53
  await this.traverseChildren(node);
48
- // Add 6 blank lines at end for paper feed spacing
54
+ // Add blank lines at end for paper feed spacing
49
55
  this.generator.addNewline(2);
50
56
  }
51
57
  /**
52
- * Handle Page element
58
+ * Handle Page element.
59
+ *
60
+ * In "rico" mode the Page padding becomes the receipt margin, the same way
61
+ * PDFTraverser.handlePage turns it into page margins. Margins arrive in
62
+ * points; the generator clamps them so they can never eat the whole line.
53
63
  */
54
64
  async handlePage(node) {
65
+ if (!this.generator.isRichStyleMode()) {
66
+ await this.traverseChildren(node);
67
+ return;
68
+ }
69
+ const style = node.style || {};
70
+ const padding = style.padding;
71
+ const left = calculateHorizontalSpacing(style.paddingLeft ?? padding);
72
+ const right = calculateHorizontalSpacing(style.paddingRight ?? padding);
73
+ const hasHorizontalPadding = left > 0 || right > 0;
74
+ if (hasHorizontalPadding) {
75
+ this.generator.pushHorizontalPadding(left, right);
76
+ }
77
+ this.generator.addSpacing(style.paddingTop ?? padding);
55
78
  await this.traverseChildren(node);
79
+ this.generator.addSpacing(style.paddingBottom ?? padding);
80
+ if (hasHorizontalPadding) {
81
+ this.generator.popHorizontalPadding();
82
+ }
56
83
  }
57
84
  /**
58
85
  * Handle View element (container with layout)
59
86
  */
60
87
  async handleView(node) {
61
88
  const viewStyle = extractViewStyle(node.style);
62
- // Apply spacing before
89
+ const rich = this.generator.isRichStyleMode();
90
+ // margin-top -> border-top -> padding-top
63
91
  this.generator.applyViewSpacing(node.style, "before");
64
- // Handle different layout modes
65
- if (viewStyle.flexDirection === "row") {
66
- await this.handleRowLayout(node);
92
+ // Horizontal padding shrinks the line for everything inside this View
93
+ let hasHorizontalPadding = false;
94
+ if (rich) {
95
+ const padding = viewStyle.padding;
96
+ const left = calculateHorizontalSpacing(viewStyle.paddingLeft ?? padding);
97
+ const right = calculateHorizontalSpacing(viewStyle.paddingRight ?? padding);
98
+ hasHorizontalPadding = left > 0 || right > 0;
99
+ if (hasHorizontalPadding) {
100
+ this.generator.pushHorizontalPadding(left, right);
101
+ }
102
+ }
103
+ // A percentage width constrains images drawn inside this View
104
+ const previousWidthFraction = this.widthFraction;
105
+ const widthFraction = parsePercentageWidth(viewStyle.width);
106
+ if (widthFraction !== undefined) {
107
+ this.widthFraction = widthFraction;
108
+ }
109
+ // alignItems on a column container aligns its children horizontally
110
+ const previousAlignment = this.alignmentContext;
111
+ if (viewStyle.flexDirection !== "row") {
112
+ if (viewStyle.alignItems === "center") {
113
+ this.alignmentContext = "center";
114
+ }
115
+ else if (viewStyle.alignItems === "flex-end") {
116
+ this.alignmentContext = "right";
117
+ }
118
+ }
119
+ if (node.children.length > 0) {
120
+ if (viewStyle.flexDirection === "row") {
121
+ await this.handleRowLayout(node);
122
+ }
123
+ else {
124
+ await this.handleColumnLayout(node);
125
+ }
67
126
  }
68
127
  else {
69
- // Column layout (default) - add spacing between children
70
- await this.handleColumnLayout(node);
128
+ // An empty View with an explicit height is a spacer, not a no-op
129
+ this.generator.addSpacing(parseSize(viewStyle.height));
130
+ }
131
+ this.alignmentContext = previousAlignment;
132
+ this.widthFraction = previousWidthFraction;
133
+ if (hasHorizontalPadding) {
134
+ this.generator.popHorizontalPadding();
71
135
  }
72
- // Apply spacing after
136
+ // padding-bottom -> border-bottom -> margin-bottom
73
137
  this.generator.applyViewSpacing(node.style, "after");
74
138
  }
75
139
  /**
76
140
  * Handle column layout (stacked vertically)
77
- * Adds newlines between sibling elements for proper spacing
78
141
  */
79
142
  async handleColumnLayout(node) {
80
- const children = node.children;
81
- for (let i = 0; i < children.length; i++) {
82
- await this.traverse(children[i]);
83
- // Add newline after each child except the last one
84
- // This ensures proper spacing between elements in column layout
85
- // if (i < children.length - 1 && children[i].type.toLowerCase() === "view") {
86
- // this.generator.addNewline();
87
- // }
143
+ for (const child of node.children) {
144
+ await this.traverse(child);
88
145
  }
89
146
  }
90
147
  /**
@@ -101,90 +158,116 @@ export class TreeTraverser {
101
158
  return;
102
159
  }
103
160
  const viewStyle = extractViewStyle(node.style);
104
- const paperWidth = this.generator.getPaperWidth();
105
- // Check layout justification mode
161
+ const rich = this.generator.isRichStyleMode();
106
162
  const isSpaceBetween = viewStyle.justifyContent === "space-between";
107
163
  const isCentered = viewStyle.justifyContent === "center";
108
- // Calculate column widths
109
- const columns = [];
110
- // Extract text style from the first Text node for the entire row
111
- // This preserves formatting like bold, fontSize across all columns
112
- let rowTextStyle = null;
113
- const firstTextNode = this.findFirstTextNode(children[0]);
114
- if (firstTextNode && firstTextNode.style) {
115
- rowTextStyle = mergeStyles(firstTextNode.style);
116
- }
164
+ const cells = [];
117
165
  for (const child of children) {
118
166
  const childStyle = extractViewStyle(child.style);
119
- const width = parseWidth(childStyle.width, paperWidth);
120
- // Collect text content from child
121
167
  const content = await this.collectTextContent(child);
122
- // Determine alignment - check for Text node textAlign first
123
- let align = "left";
124
- // Look for Text element with textAlign
125
168
  const textNode = this.findFirstTextNode(child);
169
+ // Alignment: the cell's own Text wins, then the cell View's own flex props
170
+ let align = "left";
126
171
  if (textNode && textNode.style) {
127
- const textStyle = extractTextStyle(textNode.style);
128
- align = mapTextAlign(textStyle.textAlign);
172
+ align = mapTextAlign(extractTextStyle(textNode.style).textAlign);
129
173
  }
130
- else {
131
- // Fall back to View alignment
132
- if (childStyle.alignItems === "center" || childStyle.justifyContent === "center") {
133
- align = "center";
134
- }
135
- else if (childStyle.alignItems === "flex-end" || childStyle.justifyContent === "flex-end") {
136
- align = "right";
137
- }
174
+ else if (childStyle.alignItems === "center" || childStyle.justifyContent === "center") {
175
+ align = "center";
138
176
  }
139
- columns.push({ node: child, width, content, align });
177
+ else if (childStyle.alignItems === "flex-end" || childStyle.justifyContent === "flex-end") {
178
+ align = "right";
179
+ }
180
+ cells.push({
181
+ rawWidth: childStyle.width,
182
+ content,
183
+ align,
184
+ textStyle: textNode?.style ? mergeStyles(textNode.style) : null,
185
+ });
140
186
  }
141
- // Check if columns have explicit widths (table layout) or should use space-between
142
- const hasExplicitWidths = columns.some((col) => {
143
- const childStyle = extractViewStyle(col.node.style);
144
- return childStyle.width !== undefined;
145
- });
146
- // Determine max number of sub-lines across all columns
147
- const columnLines = columns.map(col => col.content.split("\n"));
148
- const maxLines = Math.max(...columnLines.map(lines => lines.length));
149
- // Apply text style (bold, fontSize) before adding the row text
150
- if (rowTextStyle) {
151
- this.generator.applyTextStyle(rowTextStyle);
187
+ const hasExplicitWidths = cells.some((cell) => cell.rawWidth !== undefined);
188
+ // space-between / centered place the cells by their content, not on a grid
189
+ const usesColumnGrid = hasExplicitWidths || (!isSpaceBetween && !isCentered);
190
+ // The row prints in the font of its first Text node; column widths are
191
+ // measured in characters of THAT font, so the print mode goes first.
192
+ const rowTextStyle = cells[0]?.textStyle ?? null;
193
+ if (rowTextStyle || rich) {
194
+ this.generator.applyTextStyle(rowTextStyle ?? {}, {
195
+ inheritedAlign: this.alignmentContext,
196
+ });
152
197
  }
153
- // Emit one output line per sub-line
198
+ const rowLevel = this.generator.getFontLevel();
199
+ const rowBold = isBold(extractTextStyle(rowTextStyle));
200
+ const paperWidth = this.generator.getPaperWidth();
201
+ const widths = distributeColumnWidths(cells.map((cell) => cell.rawWidth), paperWidth);
202
+ // In "rico" a cell may print one level up or down from the row; a bigger
203
+ // character eats more of the column, so the capacity is not the width.
204
+ const cellLevels = cells.map((cell) => rich && cell.textStyle ? this.generator.resolveTextLevel(cell.textStyle.fontSize) : rowLevel);
205
+ const cellBolds = cells.map((cell) => rich && cell.textStyle ? isBold(extractTextStyle(cell.textStyle)) : rowBold);
206
+ const capacities = widths.map((width, i) => Math.max(1, Math.floor((width * FONT_LEVEL_COLUMN_UNITS[rowLevel]) / FONT_LEVEL_COLUMN_UNITS[cellLevels[i]])));
207
+ const perCellStyles = rich && cellLevels.some((level, i) => level !== rowLevel || cellBolds[i] !== rowBold);
208
+ // A cell wider than its column wraps onto the next row line — it used to be
209
+ // silently truncated, which lost the end of long product names.
210
+ const columnLines = cells.map((cell, i) => cell.content
211
+ .split("\n")
212
+ .flatMap((line) => (usesColumnGrid ? wrapText(line, capacities[i]) : [line])));
213
+ const maxLines = Math.max(...columnLines.map((lines) => lines.length));
154
214
  for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
155
- let rowText = "";
156
- if (isSpaceBetween && columns.length === 2 && !hasExplicitWidths) {
157
- const left = (columnLines[0][lineIdx] || "");
158
- const right = (columnLines[1][lineIdx] || "");
159
- const usedSpace = left.length + right.length;
160
- const gap = Math.max(1, paperWidth - usedSpace);
161
- rowText = left + " ".repeat(gap) + right;
215
+ if (usesColumnGrid) {
216
+ this.emitGridLine(columnLines, capacities, cells, lineIdx, {
217
+ perCellStyles,
218
+ cellLevels,
219
+ cellBolds,
220
+ rowLevel,
221
+ rowBold,
222
+ });
162
223
  }
163
- else if (isCentered && !hasExplicitWidths) {
164
- const parts = [];
165
- for (let i = 0; i < columns.length; i++) {
166
- parts.push(columnLines[i][lineIdx] || "");
224
+ else if (isSpaceBetween) {
225
+ const parts = columnLines.map((lines) => lines[lineIdx] || "");
226
+ const used = parts.reduce((sum, part) => sum + part.length, 0);
227
+ const gaps = distributeGaps(used, paperWidth, parts.length - 1);
228
+ let rowText = parts[0];
229
+ for (let i = 1; i < parts.length; i++) {
230
+ rowText += " ".repeat(gaps[i - 1]) + parts[i];
167
231
  }
168
- const spacingBetweenColumns = Math.max(0, columns.length - 1);
169
- const totalContentWidth = parts.reduce((sum, p) => sum + p.length, 0) + spacingBetweenColumns;
170
- const leadingSpaces = Math.max(0, Math.floor((paperWidth - totalContentWidth) / 2));
171
- rowText = " ".repeat(leadingSpaces) + parts.join(" ");
232
+ this.generator.addTextLine(rowText);
172
233
  }
173
234
  else {
174
- for (let i = 0; i < columns.length; i++) {
175
- const cellContent = columnLines[i][lineIdx] || "";
176
- const cellText = alignTextInColumn(cellContent, columns[i].width, columns[i].align);
177
- rowText += cellText;
178
- }
235
+ const parts = columnLines.map((lines) => lines[lineIdx] || "");
236
+ const spacingBetweenColumns = Math.max(0, parts.length - 1);
237
+ const totalContentWidth = parts.reduce((sum, part) => sum + part.length, 0) + spacingBetweenColumns;
238
+ const leadingSpaces = Math.max(0, Math.floor((paperWidth - totalContentWidth) / 2));
239
+ this.generator.addTextLine(" ".repeat(leadingSpaces) + parts.join(" "));
179
240
  }
180
- this.generator.addText(rowText);
181
241
  this.generator.addNewline();
182
242
  }
183
243
  // Reset formatting after the row
184
- if (rowTextStyle) {
244
+ if (rowTextStyle || rich) {
185
245
  this.generator.resetFormatting();
186
246
  }
187
247
  }
248
+ /**
249
+ * Emits one line of a column-grid row, padding every cell to its capacity.
250
+ * With per-cell styling the cells are printed one by one so each can carry
251
+ * its own print mode; otherwise the whole line goes out as a single string.
252
+ */
253
+ emitGridLine(columnLines, capacities, cells, lineIdx, style) {
254
+ if (!style.perCellStyles) {
255
+ let rowText = "";
256
+ for (let i = 0; i < cells.length; i++) {
257
+ rowText += alignTextInColumn(columnLines[i][lineIdx] || "", capacities[i], cells[i].align);
258
+ }
259
+ this.generator.addTextLine(rowText);
260
+ return;
261
+ }
262
+ for (let i = 0; i < cells.length; i++) {
263
+ this.generator.setPrintState(style.cellLevels[i], style.cellBolds[i]);
264
+ this.generator.addText(alignTextInColumn(columnLines[i][lineIdx] || "", capacities[i], cells[i].align));
265
+ }
266
+ // Back to the row font BEFORE closing the line, so the right inset is
267
+ // measured in the same characters the row was laid out in.
268
+ this.generator.setPrintState(style.rowLevel, style.rowBold);
269
+ this.generator.endTextLine();
270
+ }
188
271
  /**
189
272
  * Find the first Text node in a tree
190
273
  */
@@ -244,18 +327,20 @@ export class TreeTraverser {
244
327
  * Collects ALL text (props + nested children) then applies word wrap
245
328
  */
246
329
  async handleText(node) {
247
- // Merge styles from parent if needed
248
330
  const style = mergeStyles(node.style);
249
- // Apply text styling (sets alignment, bold, size)
250
- this.generator.applyTextStyle(style);
251
- // Collect ALL text content from this node and all nested children
331
+ // Collect ALL text content before styling: in "rico" the text decides
332
+ // whether a 2x2 size actually fits on the line.
252
333
  const fullText = await this.collectTextContent(node);
253
- // Word wrap the full text to fit paper width
334
+ this.generator.applyTextStyle(style, {
335
+ inheritedAlign: this.alignmentContext,
336
+ text: fullText,
337
+ });
338
+ // Word wrap the full text to fit the line at the size just selected
254
339
  if (fullText) {
255
340
  const paperWidth = this.generator.getPaperWidth();
256
341
  const lines = wrapText(fullText, paperWidth);
257
342
  for (let i = 0; i < lines.length; i++) {
258
- this.generator.addText(lines[i]);
343
+ this.generator.addTextLine(lines[i]);
259
344
  if (i < lines.length - 1) {
260
345
  this.generator.addNewline();
261
346
  }
@@ -280,24 +365,28 @@ export class TreeTraverser {
280
365
  async handleImage(node) {
281
366
  const source = node.props.source || node.props.src;
282
367
  if (source) {
283
- // Apply alignment from style (check both node style and parent style)
284
368
  const style = mergeStyles(node.style);
285
369
  const viewStyle = extractViewStyle(style);
286
- const textStyle = extractTextStyle(style);
287
- // Determine alignment from textAlign or justifyContent
288
- let align = "left";
289
- if (textStyle.textAlign) {
290
- align = mapTextAlign(textStyle.textAlign);
370
+ // Own textAlign wins, then the image's own flex props, then the parent's
371
+ // alignItems. `extractTextStyle` defaults textAlign to "left", so reading
372
+ // it here used to make the justifyContent branches dead code.
373
+ let align = this.alignmentContext;
374
+ if (style?.textAlign) {
375
+ align = mapTextAlign(style.textAlign);
291
376
  }
292
377
  else if (viewStyle.justifyContent === "center" || viewStyle.alignItems === "center") {
293
378
  align = "center";
294
379
  }
295
- else if (viewStyle.justifyContent === "flex-end" || viewStyle.alignItems === "flex-end") {
380
+ else if (viewStyle.justifyContent === "flex-end" ||
381
+ viewStyle.alignItems === "flex-end") {
296
382
  align = "right";
297
383
  }
298
- // Set alignment before adding image
299
384
  this.generator.setAlign(align);
300
- await this.generator.addImage(source);
385
+ // A percentage width on the parent View caps the image, like in the PDF
386
+ const maxWidthColumns = this.widthFraction !== undefined
387
+ ? Math.max(1, Math.floor(this.generator.getPaperWidth() * this.widthFraction))
388
+ : undefined;
389
+ await this.generator.addImage(source, maxWidthColumns);
301
390
  // Reset alignment
302
391
  this.generator.setAlign("left");
303
392
  }
package/dist/types.d.ts CHANGED
@@ -7,13 +7,16 @@ export interface ESCPOSCommand {
7
7
  buffer?: Buffer;
8
8
  }
9
9
  export interface ConversionContext {
10
+ /** Characters that fit on a line at the CURRENT font level, minus horizontal padding. */
10
11
  paperWidth: number;
12
+ /** Characters that fit on a line at the document's base font level, with no padding. */
11
13
  basePaperWidth: number;
12
14
  currentAlign: 'left' | 'center' | 'right';
13
15
  currentSize: {
14
16
  width: number;
15
17
  height: number;
16
18
  };
19
+ /** ESC ! bit 0: 0 = Font A (12x24), 1 = Font B (9x17). */
17
20
  currentFont: 0 | 1;
18
21
  currentBold: boolean;
19
22
  encoding: string;
@@ -1 +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,cAAc,EAAE,MAAM,CAAC;IACvB,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,CAAC,GAAG,CAAC,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
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,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,cAAc,EAAE,MAAM,CAAC;IACvB,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,0DAA0D;IAC1D,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thermal-print/escpos",
3
- "version": "0.3.1-beta.6",
3
+ "version": "0.3.1-beta.7",
4
4
  "type": "module",
5
5
  "description": "ESC/POS command generation and thermal printer control",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,12 @@
13
13
  "require": "./dist/index.js"
14
14
  }
15
15
  },
16
+ "scripts": {
17
+ "build": "tsc --build",
18
+ "build:watch": "tsc --build --watch",
19
+ "test": "tsx --test test/*.test.ts",
20
+ "typecheck": "tsc -p tsconfig.test.json"
21
+ },
16
22
  "keywords": [
17
23
  "thermal-printer",
18
24
  "escpos",
@@ -22,8 +28,8 @@
22
28
  "author": "",
23
29
  "license": "MIT",
24
30
  "dependencies": {
25
- "jimp": "1.6.0",
26
- "@thermal-print/core": "0.3.1-beta.6"
31
+ "@thermal-print/core": "workspace:*",
32
+ "jimp": "1.6.0"
27
33
  },
28
34
  "devDependencies": {
29
35
  "typescript": "5.0.0"
@@ -33,9 +39,5 @@
33
39
  },
34
40
  "files": [
35
41
  "dist"
36
- ],
37
- "scripts": {
38
- "build": "tsc --build",
39
- "build:watch": "tsc --build --watch"
40
- }
41
- }
42
+ ]
43
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 NUVEL
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.