@jsenv/humanize 1.7.5 → 1.7.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSupportsColor, isUnicodeSupported,
|
|
1
|
+
import { createSupportsColor, isUnicodeSupported, eastAsianWidth } from "./jsenv_humanize_node_modules.js";
|
|
2
2
|
import stripAnsi from "strip-ansi";
|
|
3
3
|
import { stripVTControlCharacters } from "node:util";
|
|
4
4
|
import ansiEscapes from "ansi-escapes";
|
|
@@ -1701,23 +1701,122 @@ const error = (...args) => console.error(...args);
|
|
|
1701
1701
|
|
|
1702
1702
|
const errorDisabled = () => {};
|
|
1703
1703
|
|
|
1704
|
+
// Whole-cluster zero-width: Default_Ignorable, Control, Format, Mark, Surrogate
|
|
1705
|
+
const zeroWidthClusterRegex =
|
|
1706
|
+
/^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+$/v;
|
|
1707
|
+
|
|
1708
|
+
// Strip leading non-printing chars to get the first visible scalar of a cluster
|
|
1709
|
+
const leadingNonPrintingRegex =
|
|
1710
|
+
/^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
|
|
1711
|
+
|
|
1712
|
+
// RGI emoji sequences (e.g. flag sequences, ZWJ families, keycap+VS16)
|
|
1713
|
+
const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
|
|
1714
|
+
|
|
1715
|
+
// Unqualified keycap: digit/# /* + combining enclosing keycap (no VS16)
|
|
1716
|
+
const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
|
|
1717
|
+
const extendedPictographicRegex = /\p{Extended_Pictographic}/gv;
|
|
1718
|
+
|
|
1719
|
+
const isDoubleWidthNonRgiEmojiSequence = (segment) => {
|
|
1720
|
+
if (segment.length > 50) {
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
if (unqualifiedKeycapRegex.test(segment)) {
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
// ZWJ sequences with 2+ Extended_Pictographic
|
|
1727
|
+
if (segment.includes("\u200D")) {
|
|
1728
|
+
const pictographics = segment.match(extendedPictographicRegex);
|
|
1729
|
+
return pictographics !== null && pictographics.length >= 2;
|
|
1730
|
+
}
|
|
1731
|
+
return false;
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
const baseVisible = (segment) => {
|
|
1735
|
+
return segment.replace(leadingNonPrintingRegex, "");
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
const isHangulLeadingJamo = (cp) => {
|
|
1739
|
+
return (cp >= 0x11_00 && cp <= 0x11_5f) || (cp >= 0xa9_60 && cp <= 0xa9_7c);
|
|
1740
|
+
};
|
|
1741
|
+
const isHangulVowelJamo = (cp) => {
|
|
1742
|
+
return (cp >= 0x11_60 && cp <= 0x11_a7) || (cp >= 0xd7_b0 && cp <= 0xd7_c6);
|
|
1743
|
+
};
|
|
1744
|
+
const isHangulTrailingJamo = (cp) => {
|
|
1745
|
+
return (cp >= 0x11_a8 && cp <= 0x11_ff) || (cp >= 0xd7_cb && cp <= 0xd7_fb);
|
|
1746
|
+
};
|
|
1747
|
+
const isHangulJamo = (cp) => {
|
|
1748
|
+
return (
|
|
1749
|
+
isHangulLeadingJamo(cp) || isHangulVowelJamo(cp) || isHangulTrailingJamo(cp)
|
|
1750
|
+
);
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
const hangulClusterWidth = (visibleSegment, eastAsianWidthOptions) => {
|
|
1754
|
+
const codePoints = [];
|
|
1755
|
+
for (const character of visibleSegment) {
|
|
1756
|
+
if (zeroWidthClusterRegex.test(character)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
codePoints.push(character.codePointAt(0));
|
|
1760
|
+
}
|
|
1761
|
+
if (codePoints.length === 0) {
|
|
1762
|
+
return undefined;
|
|
1763
|
+
}
|
|
1764
|
+
let width = 0;
|
|
1765
|
+
for (let index = 0; index < codePoints.length; index++) {
|
|
1766
|
+
const codePoint = codePoints[index];
|
|
1767
|
+
if (!isHangulJamo(codePoint)) {
|
|
1768
|
+
if (width === 0) {
|
|
1769
|
+
return undefined;
|
|
1770
|
+
}
|
|
1771
|
+
for (let remaining = index; remaining < codePoints.length; remaining++) {
|
|
1772
|
+
width += eastAsianWidth(codePoints[remaining], eastAsianWidthOptions);
|
|
1773
|
+
}
|
|
1774
|
+
return width;
|
|
1775
|
+
}
|
|
1776
|
+
if (
|
|
1777
|
+
isHangulLeadingJamo(codePoint) &&
|
|
1778
|
+
isHangulVowelJamo(codePoints[index + 1])
|
|
1779
|
+
) {
|
|
1780
|
+
width += 2;
|
|
1781
|
+
index += isHangulTrailingJamo(codePoints[index + 2]) ? 2 : 1;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
|
|
1785
|
+
}
|
|
1786
|
+
return width;
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const trailingHalfwidthWidth = (visibleSegment, eastAsianWidthOptions) => {
|
|
1790
|
+
let extra = 0;
|
|
1791
|
+
let first = true;
|
|
1792
|
+
for (const character of visibleSegment) {
|
|
1793
|
+
if (first) {
|
|
1794
|
+
first = false;
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (character >= "\uFF00" && character <= "\uFFEF") {
|
|
1798
|
+
extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return extra;
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1704
1804
|
const createMeasureTextWidth = ({ stripAnsi }) => {
|
|
1705
1805
|
const segmenter = new Intl.Segmenter();
|
|
1706
|
-
const defaultIgnorableCodePointRegex = /^\p{Default_Ignorable_Code_Point}$/u;
|
|
1707
1806
|
|
|
1708
1807
|
const measureTextWidth = (
|
|
1709
1808
|
string,
|
|
1710
|
-
{
|
|
1711
|
-
ambiguousIsNarrow = true,
|
|
1712
|
-
countAnsiEscapeCodes = false,
|
|
1713
|
-
skipEmojis = false,
|
|
1714
|
-
} = {},
|
|
1809
|
+
{ ambiguousIsNarrow = true, countAnsiEscapeCodes = false } = {},
|
|
1715
1810
|
) => {
|
|
1716
1811
|
if (typeof string !== "string" || string.length === 0) {
|
|
1717
1812
|
return 0;
|
|
1718
1813
|
}
|
|
1719
1814
|
|
|
1720
|
-
|
|
1815
|
+
// Only strip ANSI when escape codes are actually present
|
|
1816
|
+
if (
|
|
1817
|
+
!countAnsiEscapeCodes &&
|
|
1818
|
+
(string.includes("\u001B") || string.includes("\u009B"))
|
|
1819
|
+
) {
|
|
1721
1820
|
string = stripAnsi(string);
|
|
1722
1821
|
}
|
|
1723
1822
|
|
|
@@ -1725,70 +1824,54 @@ const createMeasureTextWidth = ({ stripAnsi }) => {
|
|
|
1725
1824
|
return 0;
|
|
1726
1825
|
}
|
|
1727
1826
|
|
|
1827
|
+
// Fast path: printable ASCII needs no segmenter or EAW lookup
|
|
1828
|
+
if (/^[\u0020-\u007E]*$/.test(string)) {
|
|
1829
|
+
return string.length;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1728
1832
|
let width = 0;
|
|
1729
1833
|
const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
|
|
1730
1834
|
|
|
1731
|
-
for (const { segment
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
// Ignore control characters
|
|
1735
|
-
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
1736
|
-
continue;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
// Ignore zero-width characters
|
|
1740
|
-
if (
|
|
1741
|
-
(codePoint >= 0x20_0b && codePoint <= 0x20_0f) || // Zero-width space, non-joiner, joiner, left-to-right mark, right-to-left mark
|
|
1742
|
-
codePoint === 0xfe_ff // Zero-width no-break space
|
|
1743
|
-
) {
|
|
1835
|
+
for (const { segment } of segmenter.segment(string)) {
|
|
1836
|
+
if (zeroWidthClusterRegex.test(segment)) {
|
|
1744
1837
|
continue;
|
|
1745
1838
|
}
|
|
1746
1839
|
|
|
1747
|
-
//
|
|
1840
|
+
// RGI emoji + unqualified emoji sequences are double-width
|
|
1748
1841
|
if (
|
|
1749
|
-
(
|
|
1750
|
-
(
|
|
1751
|
-
(codePoint >= 0x1d_c0 && codePoint <= 0x1d_ff) || // Combining diacritical marks supplement
|
|
1752
|
-
(codePoint >= 0x20_d0 && codePoint <= 0x20_ff) || // Combining diacritical marks for symbols
|
|
1753
|
-
(codePoint >= 0xfe_20 && codePoint <= 0xfe_2f) // Combining half marks
|
|
1842
|
+
rgiEmojiRegex.test(segment) ||
|
|
1843
|
+
isDoubleWidthNonRgiEmojiSequence(segment)
|
|
1754
1844
|
) {
|
|
1845
|
+
if (process.env.CAPTURING_SIDE_EFFECTS && segment === "✔️") {
|
|
1846
|
+
width += 2;
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
width += 2;
|
|
1755
1850
|
continue;
|
|
1756
1851
|
}
|
|
1757
1852
|
|
|
1758
|
-
|
|
1759
|
-
if (codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) {
|
|
1760
|
-
continue;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
// Ignore variation selectors
|
|
1764
|
-
if (codePoint >= 0xfe_00 && codePoint <= 0xfe_0f) {
|
|
1765
|
-
continue;
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// This covers some of the above cases, but we still keep them for performance reasons.
|
|
1769
|
-
if (defaultIgnorableCodePointRegex.test(character)) {
|
|
1770
|
-
continue;
|
|
1771
|
-
}
|
|
1853
|
+
const visibleSegment = baseVisible(segment);
|
|
1772
1854
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
}
|
|
1780
|
-
width += measureTextWidth(character, {
|
|
1781
|
-
skipEmojis: true,
|
|
1782
|
-
countAnsiEscapeCodes: true, // to skip call to stripAnsi
|
|
1783
|
-
});
|
|
1855
|
+
const hangulWidth = hangulClusterWidth(
|
|
1856
|
+
visibleSegment,
|
|
1857
|
+
eastAsianWidthOptions,
|
|
1858
|
+
);
|
|
1859
|
+
if (hangulWidth !== undefined) {
|
|
1860
|
+
width += hangulWidth;
|
|
1784
1861
|
continue;
|
|
1785
1862
|
}
|
|
1786
1863
|
|
|
1864
|
+
// EAW of the cluster's first visible scalar
|
|
1865
|
+
const codePoint = visibleSegment.codePointAt(0);
|
|
1787
1866
|
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
|
|
1867
|
+
|
|
1868
|
+
// Add width for trailing Halfwidth/Fullwidth Forms (e.g. ゙, ゚, ー)
|
|
1869
|
+
width += trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions);
|
|
1788
1870
|
}
|
|
1789
1871
|
|
|
1790
1872
|
return width;
|
|
1791
1873
|
};
|
|
1874
|
+
|
|
1792
1875
|
return measureTextWidth;
|
|
1793
1876
|
};
|
|
1794
1877
|
|