@jsenv/core 41.2.6 → 41.2.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.
@@ -252,19 +252,18 @@ function stripAnsi(string) {
252
252
  return string.replace(regex, '');
253
253
  }
254
254
 
255
- const r = String.raw,
256
- e = r`\p{Emoji}(?:\p{EMod}|[\u{E0020}-\u{E007E}]+\u{E007F}|\uFE0F?\u20E3?)`;
257
- const emojiRegex = () => new RegExp(r`\p{RI}{2}|(?![#*\d](?!\uFE0F?\u20E3))${e}(?:\u200D${e})*`, 'gu');
258
-
259
255
  // Generated by scripts/build.js
260
256
 
261
- // prettier-ignore
257
+ const ambiguousMinimalCodePoint = 161;
258
+ const ambiguousMaximumCodePoint = 1114109;
262
259
  const ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
263
260
 
264
- // prettier-ignore
261
+ const fullwidthMinimalCodePoint = 12288;
262
+ const fullwidthMaximumCodePoint = 65510;
265
263
  const fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
266
264
 
267
- // prettier-ignore
265
+ const wideMinimalCodePoint = 4352;
266
+ const wideMaximumCodePoint = 262141;
268
267
  const wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678, 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141];
269
268
 
270
269
  /**
@@ -292,15 +291,8 @@ const isInRange = (ranges, codePoint) => {
292
291
  return false;
293
292
  };
294
293
 
295
- const minimumAmbiguousCodePoint = ambiguousRanges[0];
296
- const maximumAmbiguousCodePoint = ambiguousRanges.at(-1);
297
- const minimumFullWidthCodePoint = fullwidthRanges[0];
298
- const maximumFullWidthCodePoint = fullwidthRanges.at(-1);
299
- const minimumWideCodePoint = wideRanges[0];
300
- const maximumWideCodePoint = wideRanges.at(-1);
301
-
302
294
  const commonCjkCodePoint = 0x4E_00;
303
- const [wideFastPathStart, wideFastPathEnd] = findWideFastPathRange(wideRanges);
295
+ const [wideFastPathStart, wideFastPathEnd] = /* #__PURE__ */ findWideFastPathRange(wideRanges);
304
296
 
305
297
  // Use a hot-path range so common `isWide` calls can skip binary search.
306
298
  // The range containing U+4E00 covers common CJK ideographs;
@@ -331,8 +323,8 @@ function findWideFastPathRange(ranges) {
331
323
 
332
324
  const isAmbiguous = codePoint => {
333
325
  if (
334
- codePoint < minimumAmbiguousCodePoint
335
- || codePoint > maximumAmbiguousCodePoint
326
+ codePoint < ambiguousMinimalCodePoint
327
+ || codePoint > ambiguousMaximumCodePoint
336
328
  ) {
337
329
  return false;
338
330
  }
@@ -342,8 +334,8 @@ const isAmbiguous = codePoint => {
342
334
 
343
335
  const isFullWidth = codePoint => {
344
336
  if (
345
- codePoint < minimumFullWidthCodePoint
346
- || codePoint > maximumFullWidthCodePoint
337
+ codePoint < fullwidthMinimalCodePoint
338
+ || codePoint > fullwidthMaximumCodePoint
347
339
  ) {
348
340
  return false;
349
341
  }
@@ -360,8 +352,8 @@ const isWide = codePoint => {
360
352
  }
361
353
 
362
354
  if (
363
- codePoint < minimumWideCodePoint
364
- || codePoint > maximumWideCodePoint
355
+ codePoint < wideMinimalCodePoint
356
+ || codePoint > wideMaximumCodePoint
365
357
  ) {
366
358
  return false;
367
359
  }
@@ -491,4 +483,4 @@ const clearTerminal = isOldWindows()
491
483
  // More info: https://www.real-world-systems.com/docs/ANSIcode.html
492
484
  : `${eraseScreen}${ESC}3J${ESC}H`;
493
485
 
494
- export { clearTerminal, createSupportsColor, eastAsianWidth, emojiRegex, eraseLines, isUnicodeSupported, stripAnsi };
486
+ export { clearTerminal, createSupportsColor, eastAsianWidth, eraseLines, isUnicodeSupported, stripAnsi };
@@ -1,4 +1,4 @@
1
- import { createSupportsColor, isUnicodeSupported, stripAnsi, emojiRegex, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
1
+ import { createSupportsColor, isUnicodeSupported, stripAnsi, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
2
2
  import { extname } from "node:path";
3
3
  import { readFileSync as readFileSync$1, existsSync, readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, unlink, rmdir, watch, realpathSync } from "node:fs";
4
4
  import crypto, { createHash } from "node:crypto";
@@ -1437,23 +1437,122 @@ const error = (...args) => console.error(...args);
1437
1437
 
1438
1438
  const errorDisabled = () => {};
1439
1439
 
1440
+ // Whole-cluster zero-width: Default_Ignorable, Control, Format, Mark, Surrogate
1441
+ const zeroWidthClusterRegex =
1442
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+$/v;
1443
+
1444
+ // Strip leading non-printing chars to get the first visible scalar of a cluster
1445
+ const leadingNonPrintingRegex =
1446
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
1447
+
1448
+ // RGI emoji sequences (e.g. flag sequences, ZWJ families, keycap+VS16)
1449
+ const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
1450
+
1451
+ // Unqualified keycap: digit/# /* + combining enclosing keycap (no VS16)
1452
+ const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
1453
+ const extendedPictographicRegex = /\p{Extended_Pictographic}/gv;
1454
+
1455
+ const isDoubleWidthNonRgiEmojiSequence = (segment) => {
1456
+ if (segment.length > 50) {
1457
+ return false;
1458
+ }
1459
+ if (unqualifiedKeycapRegex.test(segment)) {
1460
+ return true;
1461
+ }
1462
+ // ZWJ sequences with 2+ Extended_Pictographic
1463
+ if (segment.includes("\u200D")) {
1464
+ const pictographics = segment.match(extendedPictographicRegex);
1465
+ return pictographics !== null && pictographics.length >= 2;
1466
+ }
1467
+ return false;
1468
+ };
1469
+
1470
+ const baseVisible = (segment) => {
1471
+ return segment.replace(leadingNonPrintingRegex, "");
1472
+ };
1473
+
1474
+ const isHangulLeadingJamo = (cp) => {
1475
+ return (cp >= 0x11_00 && cp <= 0x11_5f) || (cp >= 0xa9_60 && cp <= 0xa9_7c);
1476
+ };
1477
+ const isHangulVowelJamo = (cp) => {
1478
+ return (cp >= 0x11_60 && cp <= 0x11_a7) || (cp >= 0xd7_b0 && cp <= 0xd7_c6);
1479
+ };
1480
+ const isHangulTrailingJamo = (cp) => {
1481
+ return (cp >= 0x11_a8 && cp <= 0x11_ff) || (cp >= 0xd7_cb && cp <= 0xd7_fb);
1482
+ };
1483
+ const isHangulJamo = (cp) => {
1484
+ return (
1485
+ isHangulLeadingJamo(cp) || isHangulVowelJamo(cp) || isHangulTrailingJamo(cp)
1486
+ );
1487
+ };
1488
+
1489
+ const hangulClusterWidth = (visibleSegment, eastAsianWidthOptions) => {
1490
+ const codePoints = [];
1491
+ for (const character of visibleSegment) {
1492
+ if (zeroWidthClusterRegex.test(character)) {
1493
+ continue;
1494
+ }
1495
+ codePoints.push(character.codePointAt(0));
1496
+ }
1497
+ if (codePoints.length === 0) {
1498
+ return undefined;
1499
+ }
1500
+ let width = 0;
1501
+ for (let index = 0; index < codePoints.length; index++) {
1502
+ const codePoint = codePoints[index];
1503
+ if (!isHangulJamo(codePoint)) {
1504
+ if (width === 0) {
1505
+ return undefined;
1506
+ }
1507
+ for (let remaining = index; remaining < codePoints.length; remaining++) {
1508
+ width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions);
1509
+ }
1510
+ return width;
1511
+ }
1512
+ if (
1513
+ isHangulLeadingJamo(codePoint) &&
1514
+ isHangulVowelJamo(codePoints[index + 1])
1515
+ ) {
1516
+ width += 2;
1517
+ index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
1518
+ continue;
1519
+ }
1520
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
1521
+ }
1522
+ return width;
1523
+ };
1524
+
1525
+ const trailingHalfwidthWidth = (visibleSegment, eastAsianWidthOptions) => {
1526
+ let extra = 0;
1527
+ let first = true;
1528
+ for (const character of visibleSegment) {
1529
+ if (first) {
1530
+ first = false;
1531
+ continue;
1532
+ }
1533
+ if (character >= "\uFF00" && character <= "\uFFEF") {
1534
+ extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
1535
+ }
1536
+ }
1537
+ return extra;
1538
+ };
1539
+
1440
1540
  const createMeasureTextWidth = ({ stripAnsi }) => {
1441
1541
  const segmenter = new Intl.Segmenter();
1442
- const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
1443
1542
 
1444
1543
  const measureTextWidth = (
1445
1544
  string,
1446
- {
1447
- ambiguousIsNarrow = true,
1448
- countAnsiEscapeCodes = false,
1449
- skipEmojis = false,
1450
- } = {},
1545
+ { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = {},
1451
1546
  ) => {
1452
1547
  if (typeof string !== "string" || string.length === 0) {
1453
1548
  return 0;
1454
1549
  }
1455
1550
 
1456
- if (!countAnsiEscapeCodes) {
1551
+ // Only strip ANSI when escape codes are actually present
1552
+ if (
1553
+ !countAnsiEscapeCodes &&
1554
+ (string.includes("\u001B") || string.includes("\u009B"))
1555
+ ) {
1457
1556
  string = stripAnsi(string);
1458
1557
  }
1459
1558
 
@@ -1461,70 +1560,54 @@ const createMeasureTextWidth = ({ stripAnsi }) => {
1461
1560
  return 0;
1462
1561
  }
1463
1562
 
1563
+ // Fast path: printable ASCII needs no segmenter or EAW lookup
1564
+ if (/^[\u0020-\u007E]*$/.test(string)) {
1565
+ return string.length;
1566
+ }
1567
+
1464
1568
  let width = 0;
1465
1569
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
1466
1570
 
1467
- for (const { segment: character } of segmenter.segment(string)) {
1468
- const codePoint = character.codePointAt(0);
1469
-
1470
- // Ignore control characters
1471
- if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
1472
- continue;
1473
- }
1474
-
1475
- // Ignore zero-width characters
1476
- if (
1477
- (codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
1478
- codePoint === 0xfe_ff // Zero-width no-break space
1479
- ) {
1571
+ for (const { segment } of segmenter.segment(string)) {
1572
+ if (zeroWidthClusterRegex.test(segment)) {
1480
1573
  continue;
1481
1574
  }
1482
1575
 
1483
- // Ignore combining characters
1576
+ // RGI emoji + unqualified emoji sequences are double-width
1484
1577
  if (
1485
- (codePoint >= 0x3_00 && codePoint <= 0x3_6f) || // Combining diacritical marks
1486
- (codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || // Combining diacritical marks extended
1487
- (codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
1488
- (codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
1489
- (codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
1578
+ rgiEmojiRegex.test(segment) ||
1579
+ isDoubleWidthNonRgiEmojiSequence(segment)
1490
1580
  ) {
1581
+ if (process.env.CAPTURING_SIDE_EFFECTS && segment === "✔️") {
1582
+ width += 2;
1583
+ continue;
1584
+ }
1585
+ width += 2;
1491
1586
  continue;
1492
1587
  }
1493
1588
 
1494
- // Ignore surrogate pairs
1495
- if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
1496
- continue;
1497
- }
1589
+ const visibleSegment = baseVisible(segment);
1498
1590
 
1499
- // Ignore variation selectors
1500
- if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
1501
- continue;
1502
- }
1503
-
1504
- // This covers some of the above cases, but we still keep them for performance reasons.
1505
- if (defaultIgnorableCodePointRegex.test(character)) {
1506
- continue;
1507
- }
1508
-
1509
- if (!skipEmojis && emojiRegex().test(character)) {
1510
- if (process.env.CAPTURING_SIDE_EFFECTS) {
1511
- if (character === "✔️") {
1512
- width += 2;
1513
- continue;
1514
- }
1515
- }
1516
- width += measureTextWidth(character, {
1517
- skipEmojis: true,
1518
- countAnsiEscapeCodes: true, // to skip call to stripAnsi
1519
- });
1591
+ const hangulWidth = hangulClusterWidth(
1592
+ visibleSegment,
1593
+ eastAsianWidthOptions,
1594
+ );
1595
+ if (hangulWidth !== undefined) {
1596
+ width += hangulWidth;
1520
1597
  continue;
1521
1598
  }
1522
1599
 
1600
+ // EAW of the cluster's first visible scalar
1601
+ const codePoint = visibleSegment.codePointAt(0);
1523
1602
  width += eastAsianWidth(codePoint, eastAsianWidthOptions);
1603
+
1604
+ // Add width for trailing Halfwidth/Fullwidth Forms (e.g. ゙, ゚, ー)
1605
+ width += trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions);
1524
1606
  }
1525
1607
 
1526
1608
  return width;
1527
1609
  };
1610
+
1528
1611
  return measureTextWidth;
1529
1612
  };
1530
1613
 
@@ -219,19 +219,18 @@ function isUnicodeSupported() {
219
219
  || env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
220
220
  }
221
221
 
222
- const r = String.raw,
223
- e = r`\p{Emoji}(?:\p{EMod}|[\u{E0020}-\u{E007E}]+\u{E007F}|\uFE0F?\u20E3?)`;
224
- const emojiRegex = () => new RegExp(r`\p{RI}{2}|(?![#*\d](?!\uFE0F?\u20E3))${e}(?:\u200D${e})*`, 'gu');
225
-
226
222
  // Generated by scripts/build.js
227
223
 
228
- // prettier-ignore
224
+ const ambiguousMinimalCodePoint = 161;
225
+ const ambiguousMaximumCodePoint = 1114109;
229
226
  const ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
230
227
 
231
- // prettier-ignore
228
+ const fullwidthMinimalCodePoint = 12288;
229
+ const fullwidthMaximumCodePoint = 65510;
232
230
  const fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
233
231
 
234
- // prettier-ignore
232
+ const wideMinimalCodePoint = 4352;
233
+ const wideMaximumCodePoint = 262141;
235
234
  const wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678, 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141];
236
235
 
237
236
  /**
@@ -259,15 +258,8 @@ const isInRange = (ranges, codePoint) => {
259
258
  return false;
260
259
  };
261
260
 
262
- const minimumAmbiguousCodePoint = ambiguousRanges[0];
263
- const maximumAmbiguousCodePoint = ambiguousRanges.at(-1);
264
- const minimumFullWidthCodePoint = fullwidthRanges[0];
265
- const maximumFullWidthCodePoint = fullwidthRanges.at(-1);
266
- const minimumWideCodePoint = wideRanges[0];
267
- const maximumWideCodePoint = wideRanges.at(-1);
268
-
269
261
  const commonCjkCodePoint = 0x4E_00;
270
- const [wideFastPathStart, wideFastPathEnd] = findWideFastPathRange(wideRanges);
262
+ const [wideFastPathStart, wideFastPathEnd] = /* #__PURE__ */ findWideFastPathRange(wideRanges);
271
263
 
272
264
  // Use a hot-path range so common `isWide` calls can skip binary search.
273
265
  // The range containing U+4E00 covers common CJK ideographs;
@@ -298,8 +290,8 @@ function findWideFastPathRange(ranges) {
298
290
 
299
291
  const isAmbiguous = codePoint => {
300
292
  if (
301
- codePoint < minimumAmbiguousCodePoint
302
- || codePoint > maximumAmbiguousCodePoint
293
+ codePoint < ambiguousMinimalCodePoint
294
+ || codePoint > ambiguousMaximumCodePoint
303
295
  ) {
304
296
  return false;
305
297
  }
@@ -309,8 +301,8 @@ const isAmbiguous = codePoint => {
309
301
 
310
302
  const isFullWidth = codePoint => {
311
303
  if (
312
- codePoint < minimumFullWidthCodePoint
313
- || codePoint > maximumFullWidthCodePoint
304
+ codePoint < fullwidthMinimalCodePoint
305
+ || codePoint > fullwidthMaximumCodePoint
314
306
  ) {
315
307
  return false;
316
308
  }
@@ -327,8 +319,8 @@ const isWide = codePoint => {
327
319
  }
328
320
 
329
321
  if (
330
- codePoint < minimumWideCodePoint
331
- || codePoint > maximumWideCodePoint
322
+ codePoint < wideMinimalCodePoint
323
+ || codePoint > wideMaximumCodePoint
332
324
  ) {
333
325
  return false;
334
326
  }
@@ -458,4 +450,4 @@ const clearTerminal = isOldWindows()
458
450
  // More info: https://www.real-world-systems.com/docs/ANSIcode.html
459
451
  : `${eraseScreen}${ESC}3J${ESC}H`;
460
452
 
461
- export { clearTerminal, createSupportsColor, eastAsianWidth, emojiRegex, eraseLines, isUnicodeSupported };
453
+ export { clearTerminal, createSupportsColor, eastAsianWidth, eraseLines, isUnicodeSupported };
@@ -1,4 +1,4 @@
1
- import { createSupportsColor, isUnicodeSupported, emojiRegex, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
1
+ import { createSupportsColor, isUnicodeSupported, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
2
2
  import { stripVTControlCharacters } from "node:util";
3
3
 
4
4
  const createCallbackListNotifiedOnce = () => {
@@ -997,23 +997,122 @@ const error = (...args) => console.error(...args);
997
997
 
998
998
  const errorDisabled = () => {};
999
999
 
1000
+ // Whole-cluster zero-width: Default_Ignorable, Control, Format, Mark, Surrogate
1001
+ const zeroWidthClusterRegex =
1002
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+$/v;
1003
+
1004
+ // Strip leading non-printing chars to get the first visible scalar of a cluster
1005
+ const leadingNonPrintingRegex =
1006
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
1007
+
1008
+ // RGI emoji sequences (e.g. flag sequences, ZWJ families, keycap+VS16)
1009
+ const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
1010
+
1011
+ // Unqualified keycap: digit/# /* + combining enclosing keycap (no VS16)
1012
+ const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
1013
+ const extendedPictographicRegex = /\p{Extended_Pictographic}/gv;
1014
+
1015
+ const isDoubleWidthNonRgiEmojiSequence = (segment) => {
1016
+ if (segment.length > 50) {
1017
+ return false;
1018
+ }
1019
+ if (unqualifiedKeycapRegex.test(segment)) {
1020
+ return true;
1021
+ }
1022
+ // ZWJ sequences with 2+ Extended_Pictographic
1023
+ if (segment.includes("\u200D")) {
1024
+ const pictographics = segment.match(extendedPictographicRegex);
1025
+ return pictographics !== null && pictographics.length >= 2;
1026
+ }
1027
+ return false;
1028
+ };
1029
+
1030
+ const baseVisible = (segment) => {
1031
+ return segment.replace(leadingNonPrintingRegex, "");
1032
+ };
1033
+
1034
+ const isHangulLeadingJamo = (cp) => {
1035
+ return (cp >= 0x11_00 && cp <= 0x11_5f) || (cp >= 0xa9_60 && cp <= 0xa9_7c);
1036
+ };
1037
+ const isHangulVowelJamo = (cp) => {
1038
+ return (cp >= 0x11_60 && cp <= 0x11_a7) || (cp >= 0xd7_b0 && cp <= 0xd7_c6);
1039
+ };
1040
+ const isHangulTrailingJamo = (cp) => {
1041
+ return (cp >= 0x11_a8 && cp <= 0x11_ff) || (cp >= 0xd7_cb && cp <= 0xd7_fb);
1042
+ };
1043
+ const isHangulJamo = (cp) => {
1044
+ return (
1045
+ isHangulLeadingJamo(cp) || isHangulVowelJamo(cp) || isHangulTrailingJamo(cp)
1046
+ );
1047
+ };
1048
+
1049
+ const hangulClusterWidth = (visibleSegment, eastAsianWidthOptions) => {
1050
+ const codePoints = [];
1051
+ for (const character of visibleSegment) {
1052
+ if (zeroWidthClusterRegex.test(character)) {
1053
+ continue;
1054
+ }
1055
+ codePoints.push(character.codePointAt(0));
1056
+ }
1057
+ if (codePoints.length === 0) {
1058
+ return undefined;
1059
+ }
1060
+ let width = 0;
1061
+ for (let index = 0; index < codePoints.length; index++) {
1062
+ const codePoint = codePoints[index];
1063
+ if (!isHangulJamo(codePoint)) {
1064
+ if (width === 0) {
1065
+ return undefined;
1066
+ }
1067
+ for (let remaining = index; remaining < codePoints.length; remaining++) {
1068
+ width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions);
1069
+ }
1070
+ return width;
1071
+ }
1072
+ if (
1073
+ isHangulLeadingJamo(codePoint) &&
1074
+ isHangulVowelJamo(codePoints[index + 1])
1075
+ ) {
1076
+ width += 2;
1077
+ index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
1078
+ continue;
1079
+ }
1080
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
1081
+ }
1082
+ return width;
1083
+ };
1084
+
1085
+ const trailingHalfwidthWidth = (visibleSegment, eastAsianWidthOptions) => {
1086
+ let extra = 0;
1087
+ let first = true;
1088
+ for (const character of visibleSegment) {
1089
+ if (first) {
1090
+ first = false;
1091
+ continue;
1092
+ }
1093
+ if (character >= "\uFF00" && character <= "\uFFEF") {
1094
+ extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
1095
+ }
1096
+ }
1097
+ return extra;
1098
+ };
1099
+
1000
1100
  const createMeasureTextWidth = ({ stripAnsi }) => {
1001
1101
  const segmenter = new Intl.Segmenter();
1002
- const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
1003
1102
 
1004
1103
  const measureTextWidth = (
1005
1104
  string,
1006
- {
1007
- ambiguousIsNarrow = true,
1008
- countAnsiEscapeCodes = false,
1009
- skipEmojis = false,
1010
- } = {},
1105
+ { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = {},
1011
1106
  ) => {
1012
1107
  if (typeof string !== "string" || string.length === 0) {
1013
1108
  return 0;
1014
1109
  }
1015
1110
 
1016
- if (!countAnsiEscapeCodes) {
1111
+ // Only strip ANSI when escape codes are actually present
1112
+ if (
1113
+ !countAnsiEscapeCodes &&
1114
+ (string.includes("\u001B") || string.includes("\u009B"))
1115
+ ) {
1017
1116
  string = stripAnsi(string);
1018
1117
  }
1019
1118
 
@@ -1021,70 +1120,54 @@ const createMeasureTextWidth = ({ stripAnsi }) => {
1021
1120
  return 0;
1022
1121
  }
1023
1122
 
1123
+ // Fast path: printable ASCII needs no segmenter or EAW lookup
1124
+ if (/^[\u0020-\u007E]*$/.test(string)) {
1125
+ return string.length;
1126
+ }
1127
+
1024
1128
  let width = 0;
1025
1129
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
1026
1130
 
1027
- for (const { segment: character } of segmenter.segment(string)) {
1028
- const codePoint = character.codePointAt(0);
1029
-
1030
- // Ignore control characters
1031
- if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
1032
- continue;
1033
- }
1034
-
1035
- // Ignore zero-width characters
1036
- if (
1037
- (codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
1038
- codePoint === 0xfe_ff // Zero-width no-break space
1039
- ) {
1131
+ for (const { segment } of segmenter.segment(string)) {
1132
+ if (zeroWidthClusterRegex.test(segment)) {
1040
1133
  continue;
1041
1134
  }
1042
1135
 
1043
- // Ignore combining characters
1136
+ // RGI emoji + unqualified emoji sequences are double-width
1044
1137
  if (
1045
- (codePoint >= 0x3_00 && codePoint <= 0x3_6f) || // Combining diacritical marks
1046
- (codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || // Combining diacritical marks extended
1047
- (codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
1048
- (codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
1049
- (codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
1138
+ rgiEmojiRegex.test(segment) ||
1139
+ isDoubleWidthNonRgiEmojiSequence(segment)
1050
1140
  ) {
1141
+ if (process.env.CAPTURING_SIDE_EFFECTS && segment === "✔️") {
1142
+ width += 2;
1143
+ continue;
1144
+ }
1145
+ width += 2;
1051
1146
  continue;
1052
1147
  }
1053
1148
 
1054
- // Ignore surrogate pairs
1055
- if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
1056
- continue;
1057
- }
1058
-
1059
- // Ignore variation selectors
1060
- if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
1061
- continue;
1062
- }
1063
-
1064
- // This covers some of the above cases, but we still keep them for performance reasons.
1065
- if (defaultIgnorableCodePointRegex.test(character)) {
1066
- continue;
1067
- }
1149
+ const visibleSegment = baseVisible(segment);
1068
1150
 
1069
- if (!skipEmojis && emojiRegex().test(character)) {
1070
- if (process.env.CAPTURING_SIDE_EFFECTS) {
1071
- if (character === "✔️") {
1072
- width += 2;
1073
- continue;
1074
- }
1075
- }
1076
- width += measureTextWidth(character, {
1077
- skipEmojis: true,
1078
- countAnsiEscapeCodes: true, // to skip call to stripAnsi
1079
- });
1151
+ const hangulWidth = hangulClusterWidth(
1152
+ visibleSegment,
1153
+ eastAsianWidthOptions,
1154
+ );
1155
+ if (hangulWidth !== undefined) {
1156
+ width += hangulWidth;
1080
1157
  continue;
1081
1158
  }
1082
1159
 
1160
+ // EAW of the cluster's first visible scalar
1161
+ const codePoint = visibleSegment.codePointAt(0);
1083
1162
  width += eastAsianWidth(codePoint, eastAsianWidthOptions);
1163
+
1164
+ // Add width for trailing Halfwidth/Fullwidth Forms (e.g. ゙, ゚, ー)
1165
+ width += trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions);
1084
1166
  }
1085
1167
 
1086
1168
  return width;
1087
1169
  };
1170
+
1088
1171
  return measureTextWidth;
1089
1172
  };
1090
1173
 
@@ -219,19 +219,18 @@ function isUnicodeSupported() {
219
219
  || env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
220
220
  }
221
221
 
222
- const r = String.raw,
223
- e = r`\p{Emoji}(?:\p{EMod}|[\u{E0020}-\u{E007E}]+\u{E007F}|\uFE0F?\u20E3?)`;
224
- const emojiRegex = () => new RegExp(r`\p{RI}{2}|(?![#*\d](?!\uFE0F?\u20E3))${e}(?:\u200D${e})*`, 'gu');
225
-
226
222
  // Generated by scripts/build.js
227
223
 
228
- // prettier-ignore
224
+ const ambiguousMinimalCodePoint = 161;
225
+ const ambiguousMaximumCodePoint = 1114109;
229
226
  const ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
230
227
 
231
- // prettier-ignore
228
+ const fullwidthMinimalCodePoint = 12288;
229
+ const fullwidthMaximumCodePoint = 65510;
232
230
  const fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
233
231
 
234
- // prettier-ignore
232
+ const wideMinimalCodePoint = 4352;
233
+ const wideMaximumCodePoint = 262141;
235
234
  const wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678, 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141];
236
235
 
237
236
  /**
@@ -259,15 +258,8 @@ const isInRange = (ranges, codePoint) => {
259
258
  return false;
260
259
  };
261
260
 
262
- const minimumAmbiguousCodePoint = ambiguousRanges[0];
263
- const maximumAmbiguousCodePoint = ambiguousRanges.at(-1);
264
- const minimumFullWidthCodePoint = fullwidthRanges[0];
265
- const maximumFullWidthCodePoint = fullwidthRanges.at(-1);
266
- const minimumWideCodePoint = wideRanges[0];
267
- const maximumWideCodePoint = wideRanges.at(-1);
268
-
269
261
  const commonCjkCodePoint = 0x4E_00;
270
- const [wideFastPathStart, wideFastPathEnd] = findWideFastPathRange(wideRanges);
262
+ const [wideFastPathStart, wideFastPathEnd] = /* #__PURE__ */ findWideFastPathRange(wideRanges);
271
263
 
272
264
  // Use a hot-path range so common `isWide` calls can skip binary search.
273
265
  // The range containing U+4E00 covers common CJK ideographs;
@@ -298,8 +290,8 @@ function findWideFastPathRange(ranges) {
298
290
 
299
291
  const isAmbiguous = codePoint => {
300
292
  if (
301
- codePoint < minimumAmbiguousCodePoint
302
- || codePoint > maximumAmbiguousCodePoint
293
+ codePoint < ambiguousMinimalCodePoint
294
+ || codePoint > ambiguousMaximumCodePoint
303
295
  ) {
304
296
  return false;
305
297
  }
@@ -309,8 +301,8 @@ const isAmbiguous = codePoint => {
309
301
 
310
302
  const isFullWidth = codePoint => {
311
303
  if (
312
- codePoint < minimumFullWidthCodePoint
313
- || codePoint > maximumFullWidthCodePoint
304
+ codePoint < fullwidthMinimalCodePoint
305
+ || codePoint > fullwidthMaximumCodePoint
314
306
  ) {
315
307
  return false;
316
308
  }
@@ -327,8 +319,8 @@ const isWide = codePoint => {
327
319
  }
328
320
 
329
321
  if (
330
- codePoint < minimumWideCodePoint
331
- || codePoint > maximumWideCodePoint
322
+ codePoint < wideMinimalCodePoint
323
+ || codePoint > wideMaximumCodePoint
332
324
  ) {
333
325
  return false;
334
326
  }
@@ -458,4 +450,4 @@ const clearTerminal = isOldWindows()
458
450
  // More info: https://www.real-world-systems.com/docs/ANSIcode.html
459
451
  : `${eraseScreen}${ESC}3J${ESC}H`;
460
452
 
461
- export { clearTerminal, createSupportsColor, eastAsianWidth, emojiRegex, eraseLines, isUnicodeSupported };
453
+ export { clearTerminal, createSupportsColor, eastAsianWidth, eraseLines, isUnicodeSupported };
@@ -1,4 +1,4 @@
1
- import { createSupportsColor, isUnicodeSupported, emojiRegex, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
1
+ import { createSupportsColor, isUnicodeSupported, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_core_node_modules.js";
2
2
  import { stripVTControlCharacters } from "node:util";
3
3
  import { readFileSync, existsSync, chmodSync, statSync, lstatSync, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, writeFileSync as writeFileSync$1, watch, realpathSync } from "node:fs";
4
4
  import { extname } from "node:path";
@@ -800,23 +800,122 @@ const error = (...args) => console.error(...args);
800
800
 
801
801
  const errorDisabled = () => {};
802
802
 
803
+ // Whole-cluster zero-width: Default_Ignorable, Control, Format, Mark, Surrogate
804
+ const zeroWidthClusterRegex =
805
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+$/v;
806
+
807
+ // Strip leading non-printing chars to get the first visible scalar of a cluster
808
+ const leadingNonPrintingRegex =
809
+ /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
810
+
811
+ // RGI emoji sequences (e.g. flag sequences, ZWJ families, keycap+VS16)
812
+ const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
813
+
814
+ // Unqualified keycap: digit/# /* + combining enclosing keycap (no VS16)
815
+ const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
816
+ const extendedPictographicRegex = /\p{Extended_Pictographic}/gv;
817
+
818
+ const isDoubleWidthNonRgiEmojiSequence = (segment) => {
819
+ if (segment.length > 50) {
820
+ return false;
821
+ }
822
+ if (unqualifiedKeycapRegex.test(segment)) {
823
+ return true;
824
+ }
825
+ // ZWJ sequences with 2+ Extended_Pictographic
826
+ if (segment.includes("\u200D")) {
827
+ const pictographics = segment.match(extendedPictographicRegex);
828
+ return pictographics !== null && pictographics.length >= 2;
829
+ }
830
+ return false;
831
+ };
832
+
833
+ const baseVisible = (segment) => {
834
+ return segment.replace(leadingNonPrintingRegex, "");
835
+ };
836
+
837
+ const isHangulLeadingJamo = (cp) => {
838
+ return (cp >= 0x11_00 && cp <= 0x11_5f) || (cp >= 0xa9_60 && cp <= 0xa9_7c);
839
+ };
840
+ const isHangulVowelJamo = (cp) => {
841
+ return (cp >= 0x11_60 && cp <= 0x11_a7) || (cp >= 0xd7_b0 && cp <= 0xd7_c6);
842
+ };
843
+ const isHangulTrailingJamo = (cp) => {
844
+ return (cp >= 0x11_a8 && cp <= 0x11_ff) || (cp >= 0xd7_cb && cp <= 0xd7_fb);
845
+ };
846
+ const isHangulJamo = (cp) => {
847
+ return (
848
+ isHangulLeadingJamo(cp) || isHangulVowelJamo(cp) || isHangulTrailingJamo(cp)
849
+ );
850
+ };
851
+
852
+ const hangulClusterWidth = (visibleSegment, eastAsianWidthOptions) => {
853
+ const codePoints = [];
854
+ for (const character of visibleSegment) {
855
+ if (zeroWidthClusterRegex.test(character)) {
856
+ continue;
857
+ }
858
+ codePoints.push(character.codePointAt(0));
859
+ }
860
+ if (codePoints.length === 0) {
861
+ return undefined;
862
+ }
863
+ let width = 0;
864
+ for (let index = 0; index < codePoints.length; index++) {
865
+ const codePoint = codePoints[index];
866
+ if (!isHangulJamo(codePoint)) {
867
+ if (width === 0) {
868
+ return undefined;
869
+ }
870
+ for (let remaining = index; remaining < codePoints.length; remaining++) {
871
+ width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions);
872
+ }
873
+ return width;
874
+ }
875
+ if (
876
+ isHangulLeadingJamo(codePoint) &&
877
+ isHangulVowelJamo(codePoints[index + 1])
878
+ ) {
879
+ width += 2;
880
+ index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
881
+ continue;
882
+ }
883
+ width += eastAsianWidth(codePoint, eastAsianWidthOptions);
884
+ }
885
+ return width;
886
+ };
887
+
888
+ const trailingHalfwidthWidth = (visibleSegment, eastAsianWidthOptions) => {
889
+ let extra = 0;
890
+ let first = true;
891
+ for (const character of visibleSegment) {
892
+ if (first) {
893
+ first = false;
894
+ continue;
895
+ }
896
+ if (character >= "\uFF00" && character <= "\uFFEF") {
897
+ extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
898
+ }
899
+ }
900
+ return extra;
901
+ };
902
+
803
903
  const createMeasureTextWidth = ({ stripAnsi }) => {
804
904
  const segmenter = new Intl.Segmenter();
805
- const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
806
905
 
807
906
  const measureTextWidth = (
808
907
  string,
809
- {
810
- ambiguousIsNarrow = true,
811
- countAnsiEscapeCodes = false,
812
- skipEmojis = false,
813
- } = {},
908
+ { ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = {},
814
909
  ) => {
815
910
  if (typeof string !== "string" || string.length === 0) {
816
911
  return 0;
817
912
  }
818
913
 
819
- if (!countAnsiEscapeCodes) {
914
+ // Only strip ANSI when escape codes are actually present
915
+ if (
916
+ !countAnsiEscapeCodes &&
917
+ (string.includes("\u001B") || string.includes("\u009B"))
918
+ ) {
820
919
  string = stripAnsi(string);
821
920
  }
822
921
 
@@ -824,70 +923,54 @@ const createMeasureTextWidth = ({ stripAnsi }) => {
824
923
  return 0;
825
924
  }
826
925
 
926
+ // Fast path: printable ASCII needs no segmenter or EAW lookup
927
+ if (/^[\u0020-\u007E]*$/.test(string)) {
928
+ return string.length;
929
+ }
930
+
827
931
  let width = 0;
828
932
  const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
829
933
 
830
- for (const { segment: character } of segmenter.segment(string)) {
831
- const codePoint = character.codePointAt(0);
832
-
833
- // Ignore control characters
834
- if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
835
- continue;
836
- }
837
-
838
- // Ignore zero-width characters
839
- if (
840
- (codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
841
- codePoint === 0xfe_ff // Zero-width no-break space
842
- ) {
934
+ for (const { segment } of segmenter.segment(string)) {
935
+ if (zeroWidthClusterRegex.test(segment)) {
843
936
  continue;
844
937
  }
845
938
 
846
- // Ignore combining characters
939
+ // RGI emoji + unqualified emoji sequences are double-width
847
940
  if (
848
- (codePoint >= 0x3_00 && codePoint <= 0x3_6f) || // Combining diacritical marks
849
- (codePoint >= 0x1a_b0 && codePoint <= 0x1a_ff) || // Combining diacritical marks extended
850
- (codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
851
- (codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
852
- (codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
941
+ rgiEmojiRegex.test(segment) ||
942
+ isDoubleWidthNonRgiEmojiSequence(segment)
853
943
  ) {
944
+ if (process.env.CAPTURING_SIDE_EFFECTS && segment === "✔️") {
945
+ width += 2;
946
+ continue;
947
+ }
948
+ width += 2;
854
949
  continue;
855
950
  }
856
951
 
857
- // Ignore surrogate pairs
858
- if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
859
- continue;
860
- }
952
+ const visibleSegment = baseVisible(segment);
861
953
 
862
- // Ignore variation selectors
863
- if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
864
- continue;
865
- }
866
-
867
- // This covers some of the above cases, but we still keep them for performance reasons.
868
- if (defaultIgnorableCodePointRegex.test(character)) {
869
- continue;
870
- }
871
-
872
- if (!skipEmojis && emojiRegex().test(character)) {
873
- if (process.env.CAPTURING_SIDE_EFFECTS) {
874
- if (character === "✔️") {
875
- width += 2;
876
- continue;
877
- }
878
- }
879
- width += measureTextWidth(character, {
880
- skipEmojis: true,
881
- countAnsiEscapeCodes: true, // to skip call to stripAnsi
882
- });
954
+ const hangulWidth = hangulClusterWidth(
955
+ visibleSegment,
956
+ eastAsianWidthOptions,
957
+ );
958
+ if (hangulWidth !== undefined) {
959
+ width += hangulWidth;
883
960
  continue;
884
961
  }
885
962
 
963
+ // EAW of the cluster's first visible scalar
964
+ const codePoint = visibleSegment.codePointAt(0);
886
965
  width += eastAsianWidth(codePoint, eastAsianWidthOptions);
966
+
967
+ // Add width for trailing Halfwidth/Fullwidth Forms (e.g. ゙, ゚, ー)
968
+ width += trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions);
887
969
  }
888
970
 
889
971
  return width;
890
972
  };
973
+
891
974
  return measureTextWidth;
892
975
  };
893
976
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.2.6",
3
+ "version": "41.2.7",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -72,12 +72,12 @@
72
72
  "test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
73
73
  },
74
74
  "dependencies": {
75
- "@jsenv/ast": "6.8.0",
76
- "@jsenv/js-module-fallback": "1.4.30",
75
+ "@jsenv/ast": "6.8.1",
76
+ "@jsenv/js-module-fallback": "1.4.31",
77
77
  "@jsenv/plugin-bundling": "2.10.11",
78
78
  "@jsenv/plugin-minification": "1.7.3",
79
- "@jsenv/plugin-supervisor": "1.8.1",
80
- "@jsenv/plugin-transpilation": "1.5.72",
79
+ "@jsenv/plugin-supervisor": "1.8.2",
80
+ "@jsenv/plugin-transpilation": "1.5.73",
81
81
  "@jsenv/server": "17.3.0",
82
82
  "@jsenv/sourcemap": "1.3.17",
83
83
  "react-table": "7.8.0"