Skip to content

Commit 6052fdb

Browse files
authored
Add normalizeName string utility function (#2549)
1 parent b886e4b commit 6052fdb

File tree

3 files changed

+60
-7
lines changed

3 files changed

+60
-7
lines changed

app/components/form/fields/NameField.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import type { FieldPath, FieldValues } from 'react-hook-form'
99

10-
import { capitalize } from '~/util/str'
10+
import { capitalize, normalizeName } from '~/util/str'
1111

1212
import { TextField, type TextFieldProps } from './TextField'
1313

@@ -30,12 +30,7 @@ export function NameField<
3030
required={required}
3131
label={label}
3232
name={name}
33-
transform={(value) =>
34-
value
35-
.toLowerCase()
36-
.replace(/[\s_]+/g, '-')
37-
.replace(/[^a-z0-9-]/g, '')
38-
}
33+
transform={(value) => normalizeName(value)}
3934
// https://www.stefanjudis.com/snippets/turn-off-password-managers/
4035
data-1p-ignore
4136
data-bwignore

app/util/str.spec.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
commaSeries,
1414
extractText,
1515
kebabCase,
16+
normalizeName,
1617
titleCase,
1718
} from './str'
1819

@@ -110,3 +111,40 @@ describe('extractText', () => {
110111
expect(extractText('Some more text')).toBe('Some more text')
111112
})
112113
})
114+
115+
describe('normalizeName', () => {
116+
it('converts to lowercase', () => {
117+
expect(normalizeName('Hello')).toBe('hello')
118+
})
119+
120+
it('replaces spaces with dashes', () => {
121+
expect(normalizeName('Hello World')).toBe('hello-world')
122+
})
123+
124+
it('removes non-alphanumeric characters', () => {
125+
expect(normalizeName('Hello, World!')).toBe('hello-world')
126+
})
127+
128+
it('caps at 63 characters', () => {
129+
expect(normalizeName('aaa')).toBe('aaa')
130+
expect(normalizeName('aaaaaaaaa')).toBe('aaaaaaaaa')
131+
expect(normalizeName('a'.repeat(63))).toBe('a'.repeat(63))
132+
expect(normalizeName('a'.repeat(64))).toBe('a'.repeat(63))
133+
})
134+
135+
it('can optionally start with numbers', () => {
136+
expect(normalizeName('123abc')).toBe('abc')
137+
expect(normalizeName('123abc', false)).toBe('abc')
138+
expect(normalizeName('123abc', true)).toBe('123abc')
139+
})
140+
141+
it('can optionally start with a dash', () => {
142+
expect(normalizeName('-abc')).toBe('abc')
143+
expect(normalizeName('-abc', false)).toBe('abc')
144+
expect(normalizeName('-abc', true)).toBe('-abc')
145+
})
146+
147+
it('does not complain when multiple dashes are present', () => {
148+
expect(normalizeName('a--b')).toBe('a--b')
149+
})
150+
})

app/util/str.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ export const titleCase = (text: string): string => {
5858
*/
5959
export const isAllZeros = (base64Data: string) => /^A*=*$/.test(base64Data)
6060

61+
/** Clean up text so that it conforms to Name field syntax rules:
62+
* - lowercase only
63+
* - no spaces
64+
* - only letters/numbers/dashes allowed
65+
* - capped at 63 characters
66+
* By default, it must start with a letter; this can be overriden with the second argument,
67+
* for contexts where we want to allow numbers at the start, like searching in comboboxes.
68+
*/
69+
export const normalizeName = (text: string, allowNonLetterStart = false): string => {
70+
const normalizedName = text
71+
.toLowerCase()
72+
.replace(/[\s_]+/g, '-') // Replace spaces and underscores with dashes
73+
.replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric (or dash) characters
74+
.slice(0, 63) // Limit string to 63 characters
75+
if (allowNonLetterStart) {
76+
return normalizedName
77+
}
78+
return normalizedName.replace(/^[^a-z]+/, '') // Remove any non-letter characters from the start
79+
}
80+
6181
/**
6282
* Extract the string contents of a ReactNode, so <>This <HL>highlighted</HL> text</> becomes "This highlighted text"
6383
*/

0 commit comments

Comments
 (0)