@nshiab/simple-data-analysis-core 2.1.10 → 2.1.11

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.
@@ -3,6 +3,8 @@
3
3
  * within headers and cells. Unlike `console.table()`, this function properly
4
4
  * handles multi-line content, making it ideal for displaying data with long
5
5
  * column names and text values.
6
+ * Widths count graphemes, keeping combining accents with their letters. Wide
7
+ * characters and emoji are not measured by their terminal display width.
6
8
  *
7
9
  * @example
8
10
  * ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"printTable.d.ts","sourceRoot":"","sources":["../../src/helpers/printTable.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CAgNN"}
1
+ {"version":3,"file":"printTable.d.ts","sourceRoot":"","sources":["../../src/helpers/printTable.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CAqNN"}
@@ -1,10 +1,13 @@
1
1
  import LogValue from "./LogValue.js";
2
+ import splitGraphemes from "./splitGraphemes.js";
2
3
  import wrapString from "./wrapString.js";
3
4
  /**
4
5
  * Prints a formatted table to the console with support for word wrapping
5
6
  * within headers and cells. Unlike `console.table()`, this function properly
6
7
  * handles multi-line content, making it ideal for displaying data with long
7
8
  * column names and text values.
9
+ * Widths count graphemes, keeping combining accents with their letters. Wide
10
+ * characters and emoji are not measured by their terminal display width.
8
11
  *
9
12
  * @example
10
13
  * ```typescript
@@ -73,7 +76,7 @@ export default function printTable(data, options) {
73
76
  // First, seed widths from header names.
74
77
  const columnWidths = {};
75
78
  for (const col of columns) {
76
- columnWidths[col] = Math.max(...col.split("\n").map((line) => line.length));
79
+ columnWidths[col] = Math.max(...col.split("\n").map((line) => splitGraphemes(line).length));
77
80
  }
78
81
  // Format every cell, track the longest raw line per column, and
79
82
  // store the formatted strings for the wrapping step that follows.
@@ -88,8 +91,9 @@ export default function printTable(data, options) {
88
91
  const lines = value.split("\n");
89
92
  let longestLine = 0;
90
93
  for (const line of lines) {
91
- if (line.length > longestLine)
92
- longestLine = line.length;
94
+ const length = splitGraphemes(line).length;
95
+ if (length > longestLine)
96
+ longestLine = length;
93
97
  }
94
98
  if (longestLine > columnWidths[col]) {
95
99
  columnWidths[col] = longestLine;
@@ -103,7 +107,9 @@ export default function printTable(data, options) {
103
107
  }
104
108
  // Use the same newline and word wrapping rules for headers and cells.
105
109
  const wrapCell = (value, width) => {
106
- const lines = value.split("\n").flatMap((line) => line.length <= width ? [line] : wrapString(line, width).split("\n"));
110
+ const lines = value.split("\n").flatMap((line) => splitGraphemes(line).length <= width
111
+ ? [line]
112
+ : wrapString(line, width).split("\n"));
107
113
  return lines.length > 0 ? lines : [""];
108
114
  };
109
115
  const wrappedHeaders = columns.map((col) => wrapCell(col, columnWidths[col]));
@@ -151,7 +157,7 @@ export default function printTable(data, options) {
151
157
  const createBottomBorder = () => createBorderLine("└", "┴", "┘");
152
158
  // Helper function to pad a string to a specific width
153
159
  const pad = (str, width) => {
154
- return str + " ".repeat(Math.max(0, width - str.length));
160
+ return str + " ".repeat(Math.max(0, width - splitGraphemes(str).length));
155
161
  };
156
162
  // Print top border
157
163
  console.log(createTopBorder());
@@ -0,0 +1,2 @@
1
+ export default function splitGraphemes(text: string): string[];
2
+ //# sourceMappingURL=splitGraphemes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitGraphemes.d.ts","sourceRoot":"","sources":["../../src/helpers/splitGraphemes.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAE7D"}
@@ -0,0 +1,6 @@
1
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
2
+ // Keep combining characters attached to their base without normalizing text.
3
+ // Grapheme counts are not terminal display widths for wide characters or emoji.
4
+ export default function splitGraphemes(text) {
5
+ return Array.from(segmenter.segment(text), ({ segment }) => segment);
6
+ }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Wraps a string to a specified maximum width, attempting to break at word
3
3
  * boundaries when possible. If a single word exceeds the maximum width, it
4
- * will be broken at the character boundary. This function is primarily used
5
- * for preparing text to be displayed in console tables.
4
+ * will be broken at a grapheme boundary, keeping combining accents with their
5
+ * letters. This function is primarily used for preparing text to be displayed
6
+ * in console tables. Widths count graphemes, not terminal columns for wide
7
+ * characters or emoji.
6
8
  *
7
9
  * @example
8
10
  * ```typescript
@@ -26,10 +28,10 @@
26
28
  *
27
29
  * @param str - The string to wrap.
28
30
  *
29
- * @param maxWidth - The maximum width of each line.
31
+ * @param maxWidth - The maximum number of graphemes in each line.
30
32
  *
31
33
  * @param wordWrap - If true, attempts to break at word boundaries. If false,
32
- * breaks at character boundaries. Defaults to `true`.
34
+ * breaks at grapheme boundaries. Defaults to `true`.
33
35
  *
34
36
  * @returns The wrapped string with newline characters inserted at appropriate
35
37
  * positions.
@@ -1 +1 @@
1
- {"version":3,"file":"wrapString.d.ts","sourceRoot":"","sources":["../../src/helpers/wrapString.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,UAAO,GACd,MAAM,CAqDR"}
1
+ {"version":3,"file":"wrapString.d.ts","sourceRoot":"","sources":["../../src/helpers/wrapString.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,UAAO,GACd,MAAM,CA6DR"}
@@ -1,8 +1,11 @@
1
+ import splitGraphemes from "./splitGraphemes.js";
1
2
  /**
2
3
  * Wraps a string to a specified maximum width, attempting to break at word
3
4
  * boundaries when possible. If a single word exceeds the maximum width, it
4
- * will be broken at the character boundary. This function is primarily used
5
- * for preparing text to be displayed in console tables.
5
+ * will be broken at a grapheme boundary, keeping combining accents with their
6
+ * letters. This function is primarily used for preparing text to be displayed
7
+ * in console tables. Widths count graphemes, not terminal columns for wide
8
+ * characters or emoji.
6
9
  *
7
10
  * @example
8
11
  * ```typescript
@@ -26,56 +29,64 @@
26
29
  *
27
30
  * @param str - The string to wrap.
28
31
  *
29
- * @param maxWidth - The maximum width of each line.
32
+ * @param maxWidth - The maximum number of graphemes in each line.
30
33
  *
31
34
  * @param wordWrap - If true, attempts to break at word boundaries. If false,
32
- * breaks at character boundaries. Defaults to `true`.
35
+ * breaks at grapheme boundaries. Defaults to `true`.
33
36
  *
34
37
  * @returns The wrapped string with newline characters inserted at appropriate
35
38
  * positions.
36
39
  */
37
40
  export default function wrapString(str, maxWidth, wordWrap = true) {
38
- if (str.length <= maxWidth) {
41
+ const graphemes = splitGraphemes(str);
42
+ if (graphemes.length <= maxWidth) {
39
43
  return str;
40
44
  }
41
45
  if (!wordWrap) {
42
- // Simple character-based wrapping
46
+ // Simple grapheme-based wrapping
43
47
  const lines = [];
44
- for (let i = 0; i < str.length; i += maxWidth) {
45
- lines.push(str.slice(i, i + maxWidth));
48
+ for (let i = 0; i < graphemes.length; i += maxWidth) {
49
+ lines.push(graphemes.slice(i, i + maxWidth).join(""));
46
50
  }
47
51
  return lines.join("\n");
48
52
  }
49
53
  // Word-aware wrapping
50
54
  const lines = [];
51
55
  let currentLine = "";
56
+ let currentWidth = 0;
52
57
  const words = str.split(/(\s+)/); // Split but keep whitespace
53
58
  for (const word of words) {
59
+ const wordGraphemes = splitGraphemes(word);
60
+ const wordWidth = wordGraphemes.length;
54
61
  // If adding this word would exceed maxWidth
55
- if (currentLine.length + word.length > maxWidth) {
62
+ if (currentWidth + wordWidth > maxWidth) {
56
63
  // If current line is not empty, save it
57
64
  if (currentLine.length > 0) {
58
65
  lines.push(currentLine.trimEnd());
59
66
  currentLine = "";
67
+ currentWidth = 0;
60
68
  }
61
69
  // If the word itself is longer than maxWidth, break it
62
- if (word.length > maxWidth) {
63
- for (let i = 0; i < word.length; i += maxWidth) {
64
- const chunk = word.slice(i, i + maxWidth);
65
- if (i + maxWidth < word.length) {
70
+ if (wordWidth > maxWidth) {
71
+ for (let i = 0; i < wordWidth; i += maxWidth) {
72
+ const chunk = wordGraphemes.slice(i, i + maxWidth).join("");
73
+ if (i + maxWidth < wordWidth) {
66
74
  lines.push(chunk);
67
75
  }
68
76
  else {
69
77
  currentLine = chunk;
78
+ currentWidth = wordWidth - i;
70
79
  }
71
80
  }
72
81
  }
73
82
  else {
74
83
  currentLine = word;
84
+ currentWidth = wordWidth;
75
85
  }
76
86
  }
77
87
  else {
78
88
  currentLine += word;
89
+ currentWidth += wordWidth;
79
90
  }
80
91
  }
81
92
  // Add any remaining text
package/llm.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # The Simple Data Analysis (Core) Library
2
2
 
3
3
  - Package: `@nshiab/simple-data-analysis-core`
4
- - Version: `2.1.10`
4
+ - Version: `2.1.11`
5
5
 
6
6
  To install the library with Deno, use:
7
7
 
package/llms.txt CHANGED
@@ -3,7 +3,7 @@
3
3
  > Lightweight DuckDB-powered TypeScript library for tabular, SQL, CSV, Parquet, and geospatial data analysis on Deno, Node.js, and Bun.
4
4
 
5
5
  - Package: `@nshiab/simple-data-analysis-core`
6
- - Version: `2.1.10`
6
+ - Version: `2.1.11`
7
7
 
8
8
  Install with Deno: `deno add jsr:@nshiab/simple-data-analysis-core`
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nshiab/simple-data-analysis-core",
3
- "version": "2.1.10",
3
+ "version": "2.1.11",
4
4
  "description": "Lightweight DuckDB-powered TypeScript library for tabular, SQL, CSV, Parquet, and geospatial data analysis on Deno, Node.js, and Bun.",
5
5
  "keywords": [
6
6
  "data-analysis",
@@ -3,6 +3,8 @@
3
3
  * within headers and cells. Unlike `console.table()`, this function properly
4
4
  * handles multi-line content, making it ideal for displaying data with long
5
5
  * column names and text values.
6
+ * Widths count graphemes, keeping combining accents with their letters. Wide
7
+ * characters and emoji are not measured by their terminal display width.
6
8
  *
7
9
  * @example
8
10
  * ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"printTable.d.ts","sourceRoot":"","sources":["../../src/helpers/printTable.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CAgNN"}
1
+ {"version":3,"file":"printTable.d.ts","sourceRoot":"","sources":["../../src/helpers/printTable.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACA,IAAI,CAqNN"}
@@ -5,12 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = printTable;
7
7
  const LogValue_js_1 = __importDefault(require("./LogValue.js"));
8
+ const splitGraphemes_js_1 = __importDefault(require("./splitGraphemes.js"));
8
9
  const wrapString_js_1 = __importDefault(require("./wrapString.js"));
9
10
  /**
10
11
  * Prints a formatted table to the console with support for word wrapping
11
12
  * within headers and cells. Unlike `console.table()`, this function properly
12
13
  * handles multi-line content, making it ideal for displaying data with long
13
14
  * column names and text values.
15
+ * Widths count graphemes, keeping combining accents with their letters. Wide
16
+ * characters and emoji are not measured by their terminal display width.
14
17
  *
15
18
  * @example
16
19
  * ```typescript
@@ -79,7 +82,7 @@ function printTable(data, options) {
79
82
  // First, seed widths from header names.
80
83
  const columnWidths = {};
81
84
  for (const col of columns) {
82
- columnWidths[col] = Math.max(...col.split("\n").map((line) => line.length));
85
+ columnWidths[col] = Math.max(...col.split("\n").map((line) => (0, splitGraphemes_js_1.default)(line).length));
83
86
  }
84
87
  // Format every cell, track the longest raw line per column, and
85
88
  // store the formatted strings for the wrapping step that follows.
@@ -94,8 +97,9 @@ function printTable(data, options) {
94
97
  const lines = value.split("\n");
95
98
  let longestLine = 0;
96
99
  for (const line of lines) {
97
- if (line.length > longestLine)
98
- longestLine = line.length;
100
+ const length = (0, splitGraphemes_js_1.default)(line).length;
101
+ if (length > longestLine)
102
+ longestLine = length;
99
103
  }
100
104
  if (longestLine > columnWidths[col]) {
101
105
  columnWidths[col] = longestLine;
@@ -109,7 +113,9 @@ function printTable(data, options) {
109
113
  }
110
114
  // Use the same newline and word wrapping rules for headers and cells.
111
115
  const wrapCell = (value, width) => {
112
- const lines = value.split("\n").flatMap((line) => line.length <= width ? [line] : (0, wrapString_js_1.default)(line, width).split("\n"));
116
+ const lines = value.split("\n").flatMap((line) => (0, splitGraphemes_js_1.default)(line).length <= width
117
+ ? [line]
118
+ : (0, wrapString_js_1.default)(line, width).split("\n"));
113
119
  return lines.length > 0 ? lines : [""];
114
120
  };
115
121
  const wrappedHeaders = columns.map((col) => wrapCell(col, columnWidths[col]));
@@ -157,7 +163,7 @@ function printTable(data, options) {
157
163
  const createBottomBorder = () => createBorderLine("└", "┴", "┘");
158
164
  // Helper function to pad a string to a specific width
159
165
  const pad = (str, width) => {
160
- return str + " ".repeat(Math.max(0, width - str.length));
166
+ return str + " ".repeat(Math.max(0, width - (0, splitGraphemes_js_1.default)(str).length));
161
167
  };
162
168
  // Print top border
163
169
  console.log(createTopBorder());
@@ -0,0 +1,2 @@
1
+ export default function splitGraphemes(text: string): string[];
2
+ //# sourceMappingURL=splitGraphemes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitGraphemes.d.ts","sourceRoot":"","sources":["../../src/helpers/splitGraphemes.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAE7D"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = splitGraphemes;
4
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
5
+ // Keep combining characters attached to their base without normalizing text.
6
+ // Grapheme counts are not terminal display widths for wide characters or emoji.
7
+ function splitGraphemes(text) {
8
+ return Array.from(segmenter.segment(text), ({ segment }) => segment);
9
+ }
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Wraps a string to a specified maximum width, attempting to break at word
3
3
  * boundaries when possible. If a single word exceeds the maximum width, it
4
- * will be broken at the character boundary. This function is primarily used
5
- * for preparing text to be displayed in console tables.
4
+ * will be broken at a grapheme boundary, keeping combining accents with their
5
+ * letters. This function is primarily used for preparing text to be displayed
6
+ * in console tables. Widths count graphemes, not terminal columns for wide
7
+ * characters or emoji.
6
8
  *
7
9
  * @example
8
10
  * ```typescript
@@ -26,10 +28,10 @@
26
28
  *
27
29
  * @param str - The string to wrap.
28
30
  *
29
- * @param maxWidth - The maximum width of each line.
31
+ * @param maxWidth - The maximum number of graphemes in each line.
30
32
  *
31
33
  * @param wordWrap - If true, attempts to break at word boundaries. If false,
32
- * breaks at character boundaries. Defaults to `true`.
34
+ * breaks at grapheme boundaries. Defaults to `true`.
33
35
  *
34
36
  * @returns The wrapped string with newline characters inserted at appropriate
35
37
  * positions.
@@ -1 +1 @@
1
- {"version":3,"file":"wrapString.d.ts","sourceRoot":"","sources":["../../src/helpers/wrapString.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,UAAO,GACd,MAAM,CAqDR"}
1
+ {"version":3,"file":"wrapString.d.ts","sourceRoot":"","sources":["../../src/helpers/wrapString.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,QAAQ,UAAO,GACd,MAAM,CA6DR"}
@@ -1,11 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.default = wrapString;
7
+ const splitGraphemes_js_1 = __importDefault(require("./splitGraphemes.js"));
4
8
  /**
5
9
  * Wraps a string to a specified maximum width, attempting to break at word
6
10
  * boundaries when possible. If a single word exceeds the maximum width, it
7
- * will be broken at the character boundary. This function is primarily used
8
- * for preparing text to be displayed in console tables.
11
+ * will be broken at a grapheme boundary, keeping combining accents with their
12
+ * letters. This function is primarily used for preparing text to be displayed
13
+ * in console tables. Widths count graphemes, not terminal columns for wide
14
+ * characters or emoji.
9
15
  *
10
16
  * @example
11
17
  * ```typescript
@@ -29,56 +35,64 @@ exports.default = wrapString;
29
35
  *
30
36
  * @param str - The string to wrap.
31
37
  *
32
- * @param maxWidth - The maximum width of each line.
38
+ * @param maxWidth - The maximum number of graphemes in each line.
33
39
  *
34
40
  * @param wordWrap - If true, attempts to break at word boundaries. If false,
35
- * breaks at character boundaries. Defaults to `true`.
41
+ * breaks at grapheme boundaries. Defaults to `true`.
36
42
  *
37
43
  * @returns The wrapped string with newline characters inserted at appropriate
38
44
  * positions.
39
45
  */
40
46
  function wrapString(str, maxWidth, wordWrap = true) {
41
- if (str.length <= maxWidth) {
47
+ const graphemes = (0, splitGraphemes_js_1.default)(str);
48
+ if (graphemes.length <= maxWidth) {
42
49
  return str;
43
50
  }
44
51
  if (!wordWrap) {
45
- // Simple character-based wrapping
52
+ // Simple grapheme-based wrapping
46
53
  const lines = [];
47
- for (let i = 0; i < str.length; i += maxWidth) {
48
- lines.push(str.slice(i, i + maxWidth));
54
+ for (let i = 0; i < graphemes.length; i += maxWidth) {
55
+ lines.push(graphemes.slice(i, i + maxWidth).join(""));
49
56
  }
50
57
  return lines.join("\n");
51
58
  }
52
59
  // Word-aware wrapping
53
60
  const lines = [];
54
61
  let currentLine = "";
62
+ let currentWidth = 0;
55
63
  const words = str.split(/(\s+)/); // Split but keep whitespace
56
64
  for (const word of words) {
65
+ const wordGraphemes = (0, splitGraphemes_js_1.default)(word);
66
+ const wordWidth = wordGraphemes.length;
57
67
  // If adding this word would exceed maxWidth
58
- if (currentLine.length + word.length > maxWidth) {
68
+ if (currentWidth + wordWidth > maxWidth) {
59
69
  // If current line is not empty, save it
60
70
  if (currentLine.length > 0) {
61
71
  lines.push(currentLine.trimEnd());
62
72
  currentLine = "";
73
+ currentWidth = 0;
63
74
  }
64
75
  // If the word itself is longer than maxWidth, break it
65
- if (word.length > maxWidth) {
66
- for (let i = 0; i < word.length; i += maxWidth) {
67
- const chunk = word.slice(i, i + maxWidth);
68
- if (i + maxWidth < word.length) {
76
+ if (wordWidth > maxWidth) {
77
+ for (let i = 0; i < wordWidth; i += maxWidth) {
78
+ const chunk = wordGraphemes.slice(i, i + maxWidth).join("");
79
+ if (i + maxWidth < wordWidth) {
69
80
  lines.push(chunk);
70
81
  }
71
82
  else {
72
83
  currentLine = chunk;
84
+ currentWidth = wordWidth - i;
73
85
  }
74
86
  }
75
87
  }
76
88
  else {
77
89
  currentLine = word;
90
+ currentWidth = wordWidth;
78
91
  }
79
92
  }
80
93
  else {
81
94
  currentLine += word;
95
+ currentWidth += wordWidth;
82
96
  }
83
97
  }
84
98
  // Add any remaining text