From 45be034b18e9bdde79778d68dbe5a89d15d42b8e Mon Sep 17 00:00:00 2001 From: Kenneth Sills <132029135+Kenneth-Sills@users.noreply.github.com> Date: Tue, 6 Aug 2024 01:40:27 +0000 Subject: [PATCH] fix: correctly handle arbitrary unicode character widths This improves support for emojis and non-latin languages. Closes #249 --- package.json | 3 +- src/index.tsx | 3 +- tests/index.test.tsx | 79 ++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 31 +++++++++++++++++ 4 files changed, 114 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 83118065..f92bdac3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "component" ], "dependencies": { - "object-hash": "^2.0.3" + "object-hash": "^2.0.3", + "string-width": "^7.2.0" }, "devDependencies": { "@types/jest": "26.0.24", diff --git a/src/index.tsx b/src/index.tsx index 3284b2b8..20c22aef 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react' import { Box, Text } from 'ink' import { sha1 } from 'object-hash' +import stringWidth from 'string-width' /* Table */ @@ -92,7 +93,7 @@ export default class Table extends React.Component< const value = data[key] if (value == undefined || value == null) return 0 - return String(value).length + return stringWidth(String(value)) }) const width = Math.max(...data, header) + padding * 2 diff --git a/tests/index.test.tsx b/tests/index.test.tsx index a88a1c2f..c15470b0 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -485,4 +485,83 @@ test('Renders table with custom skeleton.', () => { expect(actual()).toBe(expected()) }) +test('Renders table with wide characters.', () => { + const data = [ + { name: '全角', width: 4 }, + { name: 'ハンカク', width: 4 }, + { name: '😀', width: 2 }, + ] + const { lastFrame: actual } = render() + + const { lastFrame: expected } = render( + <> + + {custom('┌')} + {custom('──────')} + {custom('┬')} + {custom('─────')} + {custom('┐')} + + + {custom('│')} + {header(' name ')} + {custom('│')} + {header(' width ')} + {custom('│')} + + + {custom('├')} + {custom('──────')} + {custom('┼')} + {custom('─────')} + {custom('┤')} + + + {custom('│')} + {cell(' 全角 ')} + {custom('│')} + {cell(' 4 ')} + {custom('│')} + + + {custom('├')} + {custom('──────')} + {custom('┼')} + {custom('─────')} + {custom('┤')} + + + {custom('│')} + {cell(' ハンカク ')} + {custom('│')} + {cell(' 4 ')} + {custom('│')} + + + {custom('├')} + {custom('──────')} + {custom('┼')} + {custom('─────')} + {custom('┤')} + + + {custom('│')} + {cell(' 😀 ')} + {custom('│')} + {cell(' 2 ')} + {custom('│')} + + + {custom('└')} + {custom('──────')} + {custom('┴')} + {custom('─────')} + {custom('┘')} + + , + ) + + expect(actual()).toBe(expected()) +}) + // --------------------------------------------------------------------------- diff --git a/yarn.lock b/yarn.lock index d4939a70..bad21545 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,6 +675,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1322,6 +1327,11 @@ emittery@^0.7.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== +emoji-regex@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1605,6 +1615,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e" + integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -3667,6 +3682,15 @@ string-width@^4.2.2: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -3674,6 +3698,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"