Skip to content

Commit 5fb74f0

Browse files
committed
feat: add justify util
1 parent fc58231 commit 5fb74f0

File tree

5 files changed

+248
-28
lines changed

5 files changed

+248
-28
lines changed

README.md

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,37 @@ import string from '@poppinss/string'
2929

3030
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
3131
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
32+
3233
**Table of Contents**
3334

34-
- [excerpt](#excerpt)
35-
- [truncate](#truncate)
36-
- [slug](#slug)
37-
- [interpolate](#interpolate)
38-
- [plural](#plural)
39-
- [singular](#singular)
40-
- [pluralize](#pluralize)
41-
- [isPlural](#isplural)
42-
- [isSingular](#issingular)
43-
- [camelCase](#camelcase)
44-
- [capitalCase](#capitalcase)
45-
- [dashCase](#dashcase)
46-
- [dotCase](#dotcase)
47-
- [noCase](#nocase)
48-
- [pascalCase](#pascalcase)
49-
- [sentenceCase](#sentencecase)
50-
- [snakeCase](#snakecase)
51-
- [titleCase](#titlecase)
52-
- [wordWrap](#wordwrap)
53-
- [htmlEscape](#htmlescape)
54-
- [random](#random)
55-
- [toSentence](#tosentence)
56-
- [condenseWhitespace](#condensewhitespace)
57-
- [ordinal](#ordinal)
58-
- [seconds.(parse/format)](#secondsparseformat)
59-
- [milliseconds.(parse/format)](#millisecondsparseformat)
60-
- [bytes.(parse/format)](#bytesparseformat)
61-
- [String builder](#string-builder)
35+
- [excerpt](#excerpt)
36+
- [truncate](#truncate)
37+
- [slug](#slug)
38+
- [interpolate](#interpolate)
39+
- [plural](#plural)
40+
- [singular](#singular)
41+
- [pluralize](#pluralize)
42+
- [isPlural](#isplural)
43+
- [isSingular](#issingular)
44+
- [camelCase](#camelcase)
45+
- [capitalCase](#capitalcase)
46+
- [dashCase](#dashcase)
47+
- [dotCase](#dotcase)
48+
- [noCase](#nocase)
49+
- [pascalCase](#pascalcase)
50+
- [sentenceCase](#sentencecase)
51+
- [snakeCase](#snakecase)
52+
- [titleCase](#titlecase)
53+
- [wordWrap](#wordwrap)
54+
- [htmlEscape](#htmlescape)
55+
- [random](#random)
56+
- [toSentence](#tosentence)
57+
- [condenseWhitespace](#condensewhitespace)
58+
- [ordinal](#ordinal)
59+
- [seconds.(parse/format)](#secondsparseformat)
60+
- [milliseconds.(parse/format)](#millisecondsparseformat)
61+
- [bytes.(parse/format)](#bytesparseformat)
62+
- [String builder](#string-builder)
6263
- [Contributing](#contributing)
6364
- [Code of Conduct](#code-of-conduct)
6465
- [License](#license)
@@ -564,6 +565,59 @@ Following are some examples.
564565
| `htmlEscape('<ta\'&g">')` | `'&lt;ta&#39;&amp;g&quot;&gt;'` |
565566
| `htmlEscape('foo<<bar')` | `'foo&lt;&lt;bar'` |
566567

568+
### justify
569+
570+
Justify text of multiple columns as per the define max width. Columns smaller than the provided max width will be padded with empty spaces or the provided `indent` char.
571+
572+
```ts
573+
import string from '@poppinss/string'
574+
575+
const output = string.justify(['help', 'serve', 'make:controller'], {
576+
width: 20,
577+
})
578+
579+
/**
580+
[
581+
'help ',
582+
'serve ',
583+
'make:controller ',
584+
]
585+
*/
586+
```
587+
588+
By default the columns are left aligned. However, they can also be right aligned using the `align` option.
589+
590+
```ts
591+
const output = string.justify(['help', 'serve', 'make:controller'], {
592+
width: 20,
593+
align: 'right',
594+
})
595+
596+
/**
597+
[
598+
' help',
599+
' serve',
600+
' make:controller',
601+
]
602+
*/
603+
```
604+
605+
If the columns contains ANSI escape sequences, then you must specify a custom `getLength` method to compute the column length without counting ANSI escape sequences.
606+
607+
```ts
608+
import stringWidth from 'string-width'
609+
610+
const output = string.justify(['help', 'serve', 'make:controller'], {
611+
width: 20,
612+
align: 'right',
613+
/**
614+
* The `string-width` package returns the length of the string
615+
* without accounting for ANSI escape sequences
616+
*/
617+
getLength: (chunk) => stringWidth(chunk),
618+
})
619+
```
620+
567621
### random
568622

569623
Generate a cryptographically secure random string of a given length. The output value is URL safe base64 encoded string.

index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import seconds from './src/seconds.js'
1212
import { slug } from './src/slugify.js'
1313
import { random } from './src/random.js'
1414
import { excerpt } from './src/excerpt.js'
15+
import { justify } from './src/justify.js'
1516
import { ordinal } from './src/ordinal.js'
1617
import { truncate } from './src/truncate.js'
1718
import { sentence } from './src/sentence.js'
@@ -67,6 +68,7 @@ const string = {
6768
bytes,
6869
ordinal,
6970
htmlEscape,
71+
justify,
7072
}
7173

7274
export default string

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
"@japa/file-system": "^2.3.1",
4040
"@japa/runner": "^3.1.4",
4141
"@japa/snapshot": "^2.0.7",
42+
"@poppinss/colors": "^4.1.4",
4243
"@release-it/conventional-changelog": "^9.0.4",
4344
"@swc/core": "^1.10.4",
4445
"@types/node": "^22.10.4",
4546
"c8": "^10.1.3",
4647
"eslint": "^9.17.0",
4748
"prettier": "^3.4.2",
4849
"release-it": "^17.11.0",
50+
"string-width": "^7.2.0",
4951
"ts-node-maintained": "^10.9.4",
5052
"tsup": "^8.3.5",
5153
"typescript": "^5.7.2"

src/justify.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* @poppinss/string
3+
*
4+
* (c) Poppinss
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
/**
11+
* Applies padding to the left or the right of the string by repeating
12+
* a given char.
13+
*
14+
* The method is not same as `padLeft` or `padRight` from JavaScript STD lib,
15+
* since it repeats a char regardless of the max width.
16+
*/
17+
function applyPadding(
18+
value: string,
19+
options: { paddingLeft?: number; paddingRight?: number; paddingChar: string }
20+
) {
21+
if (options.paddingLeft) {
22+
value = `${options.paddingChar.repeat(options.paddingLeft)}${value}`
23+
}
24+
25+
if (options.paddingRight) {
26+
value = `${value}${options.paddingChar.repeat(options.paddingRight)}`
27+
}
28+
29+
return value
30+
}
31+
32+
/**
33+
* Justify the columns to have the same width by filling
34+
* the empty slots with a padding char.
35+
*
36+
* Optionally, the column can be aligned left or right.
37+
*/
38+
export function justify(
39+
columns: string[],
40+
options: {
41+
width: number
42+
align?: 'left' | 'right'
43+
indent?: string
44+
getLength?: (value: string) => number
45+
}
46+
) {
47+
const normalizedOptions = {
48+
align: 'left' as const,
49+
indent: ' ',
50+
...options,
51+
}
52+
53+
return columns.map((column) => {
54+
const columnWidth = options.getLength?.(column) ?? column.length
55+
56+
/**
57+
* Column is already same or greater than the width
58+
*/
59+
if (columnWidth >= normalizedOptions.width) {
60+
return column
61+
}
62+
63+
/**
64+
* Fill empty space on the right
65+
*/
66+
if (normalizedOptions.align === 'left') {
67+
return applyPadding(column, {
68+
paddingChar: normalizedOptions.indent,
69+
paddingRight: normalizedOptions.width - columnWidth,
70+
})
71+
}
72+
73+
/**
74+
* Fill empty space on the left
75+
*/
76+
return applyPadding(column, {
77+
paddingChar: normalizedOptions.indent,
78+
paddingLeft: normalizedOptions.width - columnWidth,
79+
})
80+
})
81+
}

tests/justify.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* @poppinss/string
3+
*
4+
* (c) Poppinss
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { test } from '@japa/runner'
11+
import stringWidth from 'string-width'
12+
import useColors from '@poppinss/colors'
13+
import { justify } from '../src/justify.js'
14+
15+
const colors = useColors.ansi()
16+
17+
test.group('justify', () => {
18+
test('justify a string by adding space to the end of it', ({ assert }) => {
19+
const width = 20
20+
const justifiedColumns = justify(['help', 'serve', 'make:controller'], { width })
21+
22+
assert.deepEqual(justifiedColumns, [
23+
'help ',
24+
'serve ',
25+
'make:controller ',
26+
])
27+
})
28+
29+
test('justify and right align', ({ assert }) => {
30+
const width = 20
31+
const justifiedColumns = justify(['help', 'serve', 'make:controller'], {
32+
width,
33+
align: 'right',
34+
})
35+
36+
assert.deepEqual(justifiedColumns, [
37+
' help',
38+
' serve',
39+
' make:controller',
40+
])
41+
})
42+
43+
test('use custom padding char', ({ assert }) => {
44+
const width = 20
45+
const justifiedColumns = justify(['help', 'serve', 'make:controller'], {
46+
width,
47+
align: 'right',
48+
indent: '.',
49+
})
50+
51+
assert.deepEqual(justifiedColumns, [
52+
'................help',
53+
'...............serve',
54+
'.....make:controller',
55+
])
56+
})
57+
58+
test('do not add padding when column size is already same as the width', ({ assert }) => {
59+
const width = 15
60+
const justifiedColumns = justify(['help', 'serve', 'make:controller'], {
61+
width,
62+
align: 'right',
63+
})
64+
65+
assert.deepEqual(justifiedColumns, [' help', ' serve', 'make:controller'])
66+
})
67+
68+
test('justify string with colors', ({ assert }) => {
69+
const width = 20
70+
const justifiedColumns = justify(
71+
[colors.cyan('help'), colors.cyan('serve'), colors.cyan('make:controller')],
72+
{ width, getLength: (chunk) => stringWidth(chunk) }
73+
)
74+
75+
assert.deepEqual(justifiedColumns, [
76+
`${colors.cyan('help')} `,
77+
`${colors.cyan('serve')} `,
78+
`${colors.cyan('make:controller')} `,
79+
])
80+
})
81+
})

0 commit comments

Comments
 (0)