diff --git a/README.md b/README.md index d07466d..fd2cb37 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@

text-table-fast

-

Generates borderless text table strings suitable for printing to stdout. Fast. 🏁

+

+ Generates borderless text table strings suitable for printing to stdout. + Fast. + 🏁 +

@@ -22,11 +26,132 @@ npm i text-table-fast ``` ```ts -import { greet } from "text-table-fast"; +import { textTable } from "text-table-fast"; + +console.log( + textTable([ + ["main", "0123456789abcdef"], + ["staging", "fedcba9876543210"], + ]), +); +``` + +```plaintext +main 0123456789abcdef +staging fedcba9876543210 +``` + +`textTable` takes in an array of arrays containing strings, numbers, or other printable values. + +## Options + +`text-table-fast`'s `textTable` can take in an optional second parameter as an object with options + +> 🔄 These options are equivalent to [`text-table`](https://www.npmjs.com/package/text-table)'s options, but with expanded names. + +### `align` + +- Default: `[]` +- Type: `("center" | "left" | "right")[]` + +The alignment for columns, in order. +These each default to `"left"`. + +```ts +import { textTable } from "text-table-fast"; + +console.log( + textTable( + [ + ["abc", "abcd", "ab"], + [1234, 12, 1234], + ], + { + alignment: ["left", "center", "right"], + }, + ), +); +``` -greet("Hello, world! 💖"); +```plaintext +abc abcd abc +1234 12 1234 ``` +### `horizontalSeparator` + +- Default: `" "` +- Type: `string` + +Characters to put between each column. + +```ts +import { textTable } from "text-table-fast"; + +console.log( + textTable( + [ + ["abc", "abcd", "ab"], + [1234, 12, 1234], + ], + { + horizontalSeparator: " | ", + }, + ), +); +``` + +```plaintext +abc | abcd | abc +1234 | 12 | 1234 +``` + +### `stringLength` + +- Default: `(value) => String(value).length` +- Type: `(value: string) => number` + +How to compute the length of strings, such as for stripping ANSI characters. + +```ts +import color from "cli-color"; +import { textTable } from "text-table-fast"; + +console.log( + textTable( + [ + [color.red("abc"), color.blue("def")], + [12, 34], + ], + { + stringLength: (value) => color.strip(value).length, + }, + ), +); +``` + +```plaintext +\x1B[31mabc\x1B[39m \x1B[34mdef\x1B[39m +12 34 +``` + +## Comparison to [`text-table`](https://www.npmjs.com/package/text-table) + +`text-table-fast` has three advantages over `text-table`: + +- It is fast in almost all scenarios, and significantly faster on larger tables. +- It is under active maintenance, whereas `text-table` hasn't been updated in over a decade. +- It's written in TypeScript and ships with its own `.d.ts` types, whereas `text-table` requires `@types/text-table` for typings. + +### Performance Comparison + +`text-table-fast` contains two meaningful optimizations over `text-table`: + +- `text-table` includes usage of of [an quadratically expensive `/\s+$/`](https://ota-meshi.github.io/eslint-plugin-regexp/playground/#eJyrVkrOT0lVslLSj4kp1lbRV6oFADQgBS4=); `text-table-fast` uses [`String.prototype.trimEnd`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd) instead. +- `text-table` executes a regular expression match on each row cell for its `'.'` (decimal) alignment option; `text-table-fast` will skip that match if and when decimal alignment support is added. + +> ESLint issue to be filed soon with a performance comparison. ⚡️ + ## Contributors @@ -47,6 +172,9 @@ greet("Hello, world! 💖"); - +## Acknowledgements + +This package is a near-drop-in replacement for venerable [`text-table`](https://www.npmjs.com/package/text-table), which has served a plethora of projects -including ESLint- well for over a decade. +Many thanks to substack for creating the original `text-table` package! 💖 > 💙 This package was templated with [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app). diff --git a/cspell.json b/cspell.json index 21e111e..b65fdfc 100644 --- a/cspell.json +++ b/cspell.json @@ -8,5 +8,16 @@ "node_modules", "pnpm-lock.yaml" ], - "words": ["borderless", "knip", "packagejson", "tseslint", "tsup"] + "words": [ + "borderless", + "hsep", + "knip", + "mabc", + "mdef", + "packagejson", + "substack", + "tseslint", + "tsup", + "vitest" + ] } diff --git a/eslint.config.js b/eslint.config.js index 22bfb19..2aeae40 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import jsonc from "eslint-plugin-jsonc"; import markdown from "eslint-plugin-markdown"; import n from "eslint-plugin-n"; import packageJson from "eslint-plugin-package-json/configs/recommended"; -import perfectionistNatural from "eslint-plugin-perfectionist/configs/recommended-natural"; +import perfectionist from "eslint-plugin-perfectionist"; import * as regexp from "eslint-plugin-regexp"; import vitest from "eslint-plugin-vitest"; import yml from "eslint-plugin-yml"; @@ -35,7 +35,9 @@ export default tseslint.config( jsdoc.configs["flat/recommended-typescript-error"], n.configs["flat/recommended"], packageJson, - perfectionistNatural, + // After updating for https://github.com/JoshuaKGoldberg/create-typescript-app/issues/1588 ... + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + perfectionist.configs["recommended-natural"], regexp.configs["flat/recommended"], ...tseslint.config({ extends: [ @@ -74,7 +76,7 @@ export default tseslint.config( "error", { order: "asc", - "partition-by-comment": true, + partitionByComment: true, type: "natural", }, ], @@ -97,6 +99,7 @@ export default tseslint.config( files: ["**/*.md/*.ts"], rules: { "n/no-missing-import": ["error", { allowModules: ["text-table-fast"] }], + "n/no-unpublished-import": ["error", { allowModules: ["cli-color"] }], }, }, { diff --git a/knip.json b/knip.json index d73a20f..2600f80 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/knip@latest/schema.json", - "entry": ["src/index.ts!"], + "entry": ["src/index.ts!", "src/**/*.test.ts!"], "ignoreExportsUsedInFile": { "interface": true, "type": true }, "project": ["src/**/*.ts!"] } diff --git a/package.json b/package.json index 77c6847..76f0f8e 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,11 @@ "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", "@eslint/js": "^9.7.0", "@release-it/conventional-changelog": "^8.0.1", + "@types/cli-color": "^2.0.6", "@types/eslint-plugin-markdown": "^2.0.2", + "@types/eslint__js": "^8.42.3", "@vitest/coverage-v8": "^2.0.4", + "cli-color": "^2.0.4", "console-fail-test": "^0.4.4", "cspell": "^8.11.0", "eslint": "^9.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9a554a..0343354 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,21 @@ importers: '@release-it/conventional-changelog': specifier: ^8.0.1 version: 8.0.1(release-it@17.6.0(typescript@5.5.3)) + '@types/cli-color': + specifier: ^2.0.6 + version: 2.0.6 '@types/eslint-plugin-markdown': specifier: ^2.0.2 version: 2.0.2 + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 '@vitest/coverage-v8': specifier: ^2.0.4 version: 2.0.4(vitest@2.0.4(@types/node@20.14.11)) + cli-color: + specifier: ^2.0.4 + version: 2.0.4 console-fail-test: specifier: ^0.4.4 version: 0.4.4 @@ -918,12 +927,18 @@ packages: '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + '@types/cli-color@2.0.6': + resolution: {integrity: sha512-uLK0/0dOYdkX8hNsezpYh1gc8eerbhf9bOKZ3e24sP67703mw9S14/yW6mSTatiaKO9v+mU/a1EVy4rOXXeZTA==} + '@types/eslint-plugin-markdown@2.0.2': resolution: {integrity: sha512-ImmEw5xBVb9vCaFfQ+5kUcVatUO4XPpTvryAmhpKzalUKhDb3EZmeuHvIUO6E1/WDOTw+/b9qlWsZhxULhZdfQ==} '@types/eslint@8.56.10': resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + '@types/eslint__js@8.42.3': + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1258,6 +1273,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-color@2.0.4: + resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} + engines: {node: '>=0.10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1465,6 +1484,10 @@ packages: engines: {node: '>=18'} hasBin: true + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + dargs@8.1.0: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} @@ -1595,6 +1618,20 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1726,6 +1763,10 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@10.1.0: resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1758,6 +1799,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1769,6 +1813,9 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2182,6 +2229,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-ssh@1.4.0: resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} @@ -2417,6 +2467,9 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru-queue@0.1.0: + resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + macos-release@3.2.0: resolution: {integrity: sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2468,6 +2521,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + memoizee@0.4.17: + resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} + engines: {node: '>=0.12'} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -2566,6 +2623,9 @@ packages: resolution: {integrity: sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -3229,6 +3289,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timers-ext@0.1.8: + resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} + engines: {node: '>=0.12'} + tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} @@ -3318,6 +3382,9 @@ packages: resolution: {integrity: sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==} engines: {node: '>=16'} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} @@ -4214,6 +4281,8 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} + '@types/cli-color@2.0.6': {} + '@types/eslint-plugin-markdown@2.0.2': dependencies: '@types/eslint': 8.56.10 @@ -4224,6 +4293,10 @@ snapshots: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 + '@types/eslint__js@8.42.3': + dependencies: + '@types/eslint': 8.56.10 + '@types/estree@1.0.5': {} '@types/glob@7.2.0': @@ -4601,6 +4674,14 @@ snapshots: cli-boxes@3.0.0: {} + cli-color@2.0.4: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.17 + timers-ext: 0.1.8 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -4869,6 +4950,11 @@ snapshots: strip-ansi: 7.1.0 vscode-uri: 3.0.8 + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + dargs@8.1.0: {} data-uri-to-buffer@4.0.1: {} @@ -4965,6 +5051,31 @@ snapshots: es-module-lexer@1.5.4: {} + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5193,6 +5304,13 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + espree@10.1.0: dependencies: acorn: 8.12.1 @@ -5223,6 +5341,11 @@ snapshots: esutils@2.0.3: {} + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + eventemitter3@5.0.1: {} execa@5.1.1: @@ -5249,6 +5372,10 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + ext@1.7.0: + dependencies: + type: 2.7.3 + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -5651,6 +5778,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@2.2.2: {} + is-ssh@1.4.0: dependencies: protocols: 2.0.1 @@ -5887,6 +6016,10 @@ snapshots: lru-cache@7.18.3: {} + lru-queue@0.1.0: + dependencies: + es5-ext: 0.10.64 + macos-release@3.2.0: {} magic-string@0.30.10: @@ -5961,6 +6094,17 @@ snapshots: mdurl@2.0.0: {} + memoizee@0.4.17: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-weak-map: 2.0.3 + event-emitter: 0.3.5 + is-promise: 2.2.2 + lru-queue: 0.1.0 + next-tick: 1.1.0 + timers-ext: 0.1.8 + meow@12.1.1: {} merge-stream@2.0.0: {} @@ -6037,6 +6181,8 @@ snapshots: dependencies: type-fest: 2.19.0 + next-tick@1.1.0: {} + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -6736,6 +6882,11 @@ snapshots: through@2.3.8: {} + timers-ext@0.1.8: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + tinybench@2.8.0: {} tinypool@1.0.0: {} @@ -6809,6 +6960,8 @@ snapshots: type-fest@4.23.0: {} + type@2.7.3: {} + typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 diff --git a/src/greet.test.ts b/src/greet.test.ts deleted file mode 100644 index f729115..0000000 --- a/src/greet.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { greet } from "./greet.js"; - -const message = "Yay, testing!"; - -describe("greet", () => { - it("logs to the console once when message is provided as a string", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet(message); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs to the console once when message is provided as an object", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet({ message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs once when times is not provided in an object", () => { - const logger = vi.fn(); - - greet({ logger, message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs a specified number of times when times is provided", () => { - const logger = vi.fn(); - const times = 7; - - greet({ logger, message, times }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(7); - }); -}); diff --git a/src/greet.ts b/src/greet.ts deleted file mode 100644 index a0d3b4c..0000000 --- a/src/greet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GreetOptions } from "./types.js"; - -export function greet(options: GreetOptions | string) { - const { - logger = console.log.bind(console), - message, - times = 1, - } = typeof options === "string" ? { message: options } : options; - - for (let i = 0; i < times; i += 1) { - logger(message); - } -} diff --git a/src/index.ts b/src/index.ts index a39b40f..e1b1c51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,68 @@ -export * from "./greet.js"; -export * from "./types.js"; +export type GetStringLength = (value: string) => number; + +export type TableAlignment = "center" | "left" | "right"; + +export interface TextTableOptions { + /** + * The alignment for columns, in order. + */ + align?: TableAlignment[]; + + /** + * Characters to put between each column. + */ + horizontalSeparator?: string; + + /** + * How to compute the length of strings, such as for stripping ANSI characters. + */ + stringLength?: GetStringLength; +} + +const defaultStringLength: GetStringLength = (value) => String(value).length; + +export function textTable(rows: string[][], opts: TextTableOptions = {}) { + const { + align = [], + horizontalSeparator = " ", + stringLength = defaultStringLength, + } = opts; + + const sizes = rows.reduce(function (acc, row) { + row.forEach(function (row, i) { + const rowLength = stringLength(row); + + if (!acc[i] || rowLength > acc[i]) { + acc[i] = rowLength; + } + }); + return acc; + }, []); + + return rows + .map((row) => + row + .map(function (row, i) { + const remainingWidth = sizes[i] - stringLength(row); + const spacing = Array(Math.max(remainingWidth + 1, 1)).join(" "); + + switch (align[i]) { + case "center": + return ( + Array(Math.ceil(remainingWidth / 2 + 1)).join(" ") + + row + + Array(Math.floor(remainingWidth / 2 + 1)).join(" ") + ); + + case "right": + return spacing + row; + + default: + return row + spacing; + } + }) + .join(horizontalSeparator) + .trimEnd(), + ) + .join("\n"); +} diff --git a/src/tests/__snapshots__/align.test.test.ts.snap b/src/tests/__snapshots__/align.test.test.ts.snap new file mode 100644 index 0000000..0ebaa1f --- /dev/null +++ b/src/tests/__snapshots__/align.test.test.ts.snap @@ -0,0 +1,93 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`align > [["abc","12"],["def","3456"]] ["left","right"] 1`] = ` +" +abc 12 +def 3456 +" +`; + +exports[`align > [["abc","12"]] ["right","right"] 1`] = ` +" +abc 12 +" +`; + +exports[`align > [["abc","1234"],["def","56"]] ["center","right"] 1`] = ` +" +abc 1234 +def 56 +" +`; + +exports[`align > [["abc","1234"],["def","56"]] ["left","right"] 1`] = ` +" +abc 1234 +def 56 +" +`; + +exports[`align > [["abc","1234"],["def","56"]] ["right","left"] 1`] = ` +" +abc 1234 +def 56 +" +`; + +exports[`align > [["abc","1234"],["def","56"]] ["right","right"] 1`] = ` +" +abc 1234 +def 56 +" +`; + +exports[`align > [["abc"]] ["right"] 1`] = ` +" +abc +" +`; + +exports[`align > [["beep","1024"],["blip","33450"],["abc","1006"],["def","45"]] ["left","right"] 1`] = ` +" +beep 1024 +blip 33450 +abc 1006 +def 45 +" +`; + +exports[`align > [["beep","1234","abc"],["blip","1234567","abc"],["abc","12345","abcdef"],["def","12","abcd"]] ["center","center","center"] 1`] = ` +" +beep 1234 abc +blip 1234567 abc + abc 12345 abcdef + def 12 abcd +" +`; + +exports[`align > [["beep","1234","abc"],["blip","1234567","abc"],["abc","12345","abcdef"],["def","12","abcd"]] ["center","right","center"] 1`] = ` +" +beep 1234 abc +blip 1234567 abc + abc 12345 abcdef + def 12 abcd +" +`; + +exports[`align > [["beep","1234","abc"],["blip","1234567","abc"],["abc","12345","abcdef"],["def","12","abcd"]] ["left","center","left"] 1`] = ` +" +beep 1234 abc +blip 1234567 abc +abc 12345 abcdef +def 12 abcd +" +`; + +exports[`align > [["beep","1234","abc"],["blip","1234567","abc"],["abc","12345","abcdef"],["def","12","abcd"]] ["right","right","right"] 1`] = ` +" +beep 1234 abc +blip 1234567 abc + abc 12345 abcdef + def 12 abcd +" +`; diff --git a/src/tests/__snapshots__/ansi-colors.test.ts.snap b/src/tests/__snapshots__/ansi-colors.test.ts.snap new file mode 100644 index 0000000..a5debbf --- /dev/null +++ b/src/tests/__snapshots__/ansi-colors.test.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ansi-colors > [["\\u001b[31mRed\\u001b[39m","\\u001b[32mGreen\\u001b[39m","\\u001b[34mBlue\\u001b[39m"],["\\u001b[1mBold\\u001b[22m","\\u001b[4mUnderline\\u001b[24m","\\u001b[3mItalic\\u001b[23m"],["\\u001b[7mInverse\\u001b[27m","\\u001b[9mStrike\\u001b[29m","\\u001b[5mBlink\\u001b[25m"],["bar","45","abcd"]] 1`] = ` +" +Red Green Blue +Bold Underline Italic +Inverse Strike Blink +bar 45 abcd +" +`; + +exports[`ansi-colors > [["\\u001b[31mRed\\u001b[39m","\\u001b[32mGreen\\u001b[39m","\\u001b[34mBlue\\u001b[39m"],["\\u001b[1mBold\\u001b[22m","\\u001b[4mUnderline\\u001b[24m","\\u001b[3mItalic\\u001b[23m"]] 1`] = ` +" +Red Green Blue +Bold Underline Italic +" +`; + +exports[`ansi-colors > [["\\u001b[31mRed\\u001b[39m","\\u001b[32mGreen\\u001b[39m","\\u001b[34mBlue\\u001b[39m"]] 1`] = ` +" +Red Green Blue +" +`; diff --git a/src/tests/__snapshots__/general.test.ts.snap b/src/tests/__snapshots__/general.test.ts.snap new file mode 100644 index 0000000..c5f0cd2 --- /dev/null +++ b/src/tests/__snapshots__/general.test.ts.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`general > [["main","0123456789abcdef"],["staging","fedcba9876543210"]] 1`] = ` +" +main 0123456789abcdef +staging fedcba9876543210 +" +`; + +exports[`general > [["main","0123456789abcdef"]] 1`] = ` +" +main 0123456789abcdef +" +`; + +exports[`general > [["master","0123456789abcdef"],["staging","fedcba9876543210"]] 1`] = ` +" +master 0123456789abcdef +staging fedcba9876543210 +" +`; diff --git a/src/tests/__snapshots__/horizontalSeparator.test.ts.snap b/src/tests/__snapshots__/horizontalSeparator.test.ts.snap new file mode 100644 index 0000000..fd8691e --- /dev/null +++ b/src/tests/__snapshots__/horizontalSeparator.test.ts.snap @@ -0,0 +1,51 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`align > [["abc","12"],["def","3456"]] " | " 1`] = ` +" +abc | 12 +def | 3456 +" +`; + +exports[`align > [["abc","12"],["def","3456"]] "|" 1`] = ` +" +abc|12 +def|3456 +" +`; + +exports[`align > [["abc","12"]] " " 1`] = ` +" +abc 12 +" +`; + +exports[`align > [["abc","12"]] " " 1`] = ` +" +abc 12 +" +`; + +exports[`align > [["abc","12"]] " " 1`] = ` +" +abc 12 +" +`; + +exports[`align > [["abc","12"]] "" 1`] = ` +" +abc12 +" +`; + +exports[`align > [["abc","12"]] "\\t" 1`] = ` +" +abc 12 +" +`; + +exports[`align > [["abc","12"]] "|" 1`] = ` +" +abc|12 +" +`; diff --git a/src/tests/align.test.test.ts b/src/tests/align.test.test.ts new file mode 100644 index 0000000..be1bf73 --- /dev/null +++ b/src/tests/align.test.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "vitest"; + +import { TableAlignment, textTable } from "../index.js"; + +describe("align", () => { + test.each<[string[][], TableAlignment[]]>([ + [[["abc"]], ["right"]], + [[["abc", "12"]], ["right", "right"]], + [ + [ + ["abc", "12"], + ["def", "3456"], + ], + ["left", "right"], + ], + [ + [ + ["abc", "1234"], + ["def", "56"], + ], + ["left", "right"], + ], + [ + [ + ["abc", "1234"], + ["def", "56"], + ], + ["right", "right"], + ], + [ + [ + ["beep", "1024"], + ["blip", "33450"], + ["abc", "1006"], + ["def", "45"], + ], + ["left", "right"], + ], + [ + [ + ["abc", "1234"], + ["def", "56"], + ], + ["center", "right"], + ], + [ + [ + ["abc", "1234"], + ["def", "56"], + ], + ["right", "left"], + ], + [ + [ + ["beep", "1234", "abc"], + ["blip", "1234567", "abc"], + ["abc", "12345", "abcdef"], + ["def", "12", "abcd"], + ], + ["left", "center", "left"], + ], + [ + [ + ["beep", "1234", "abc"], + ["blip", "1234567", "abc"], + ["abc", "12345", "abcdef"], + ["def", "12", "abcd"], + ], + ["center", "center", "center"], + ], + [ + [ + ["beep", "1234", "abc"], + ["blip", "1234567", "abc"], + ["abc", "12345", "abcdef"], + ["def", "12", "abcd"], + ], + ["center", "right", "center"], + ], + [ + [ + ["beep", "1234", "abc"], + ["blip", "1234567", "abc"], + ["abc", "12345", "abcdef"], + ["def", "12", "abcd"], + ], + ["right", "right", "right"], + ], + ])("%j %j", (rows, align) => { + expect("\n" + textTable(rows, { align }) + "\n").toMatchSnapshot(); + }); +}); diff --git a/src/tests/ansi-colors.test.ts b/src/tests/ansi-colors.test.ts new file mode 100644 index 0000000..b9154b6 --- /dev/null +++ b/src/tests/ansi-colors.test.ts @@ -0,0 +1,47 @@ +import color from "cli-color"; +import { describe, expect, test } from "vitest"; + +import { textTable } from "../index.js"; + +describe("ansi-colors", () => { + test.each([ + [[[color.red("Red"), color.green("Green"), color.blue("Blue")]]], + [ + [ + [color.red("Red"), color.green("Green"), color.blue("Blue")], + [ + color.bold("Bold"), + color.underline("Underline"), + color.italic("Italic"), + ], + ], + ], + [ + [ + [color.red("Red"), color.green("Green"), color.blue("Blue")], + [ + color.bold("Bold"), + color.underline("Underline"), + color.italic("Italic"), + ], + [ + color.inverse("Inverse"), + color.strike("Strike"), + color.blink("Blink"), + ], + ["bar", "45", "abcd"], + ], + ], + ])("%j", (rows) => { + expect( + "\n" + + color.strip( + textTable(rows, { + align: ["left", "center", "left"], + stringLength: (value) => color.strip(value).length, + }), + ) + + "\n", + ).toMatchSnapshot(); + }); +}); diff --git a/src/tests/general.test.ts b/src/tests/general.test.ts new file mode 100644 index 0000000..5dfd2ee --- /dev/null +++ b/src/tests/general.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; + +import { textTable } from "../index.js"; + +describe("general", () => { + test.each([ + [[["main", "0123456789abcdef"]]], + [ + [ + ["main", "0123456789abcdef"], + ["staging", "fedcba9876543210"], + ], + ], + [ + [ + ["master", "0123456789abcdef"], + ["staging", "fedcba9876543210"], + ], + ], + ])("%j", (rows) => { + expect("\n" + textTable(rows) + "\n").toMatchSnapshot(); + }); +}); diff --git a/src/tests/horizontalSeparator.test.ts b/src/tests/horizontalSeparator.test.ts new file mode 100644 index 0000000..1e10441 --- /dev/null +++ b/src/tests/horizontalSeparator.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; + +import { textTable } from "../index.js"; + +describe("align", () => { + test.each<[string[][], string]>([ + [[["abc", "12"]], ""], + [[["abc", "12"]], " "], + [[["abc", "12"]], " "], + [[["abc", "12"]], " "], + [[["abc", "12"]], "\t"], + [[["abc", "12"]], "|"], + [ + [ + ["abc", "12"], + ["def", "3456"], + ], + "|", + ], + [ + [ + ["abc", "12"], + ["def", "3456"], + ], + " | ", + ], + ])("%j %j", (rows, horizontalSeparator) => { + expect( + "\n" + textTable(rows, { horizontalSeparator }) + "\n", + ).toMatchSnapshot(); + }); +}); diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 4f16ae3..0000000 --- a/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GreetOptions { - logger?: (message: string) => void; - message: string; - times?: number; -}