Skip to content

Commit 2bde01b

Browse files
committed
chore: wip
chore: wip
1 parent fb2b8f3 commit 2bde01b

File tree

14 files changed

+1084
-74
lines changed

14 files changed

+1084
-74
lines changed

storage/framework/core/strings/build.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const result = await Bun.build({
99
entrypoints: ['./src/index.ts'],
1010
outdir: './dist',
1111
format: 'esm',
12-
sourcemap: 'linked',
13-
minify: true,
1412
target: 'bun',
1513
plugins: [
1614
dts({

storage/framework/core/strings/package.json

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,9 @@
3636
],
3737
"exports": {
3838
".": {
39-
"bun": "./src/index.ts",
4039
"import": "./dist/index.js"
4140
},
4241
"./*": {
43-
"bun": "./src/*",
4442
"import": "./dist/*"
4543
}
4644
},
@@ -54,21 +52,13 @@
5452
"prepublishOnly": "bun run build"
5553
},
5654
"dependencies": {
57-
"@stacksjs/alias": "workspace:*",
58-
"@stacksjs/types": "workspace:*",
59-
"change-case": "^5.4.4",
60-
"detect-indent": "^7.0.1",
61-
"detect-newline": "^4.0.1",
6255
"macroable": "^7.0.2",
63-
"pluralize": "^8.0.0",
64-
"slugify": "^1.6.6",
65-
"string-ts": "^2.2.0",
66-
"title-case": "^4.3.2",
6756
"validator": "^13.12.0"
6857
},
6958
"devDependencies": {
59+
"@stacksjs/alias": "workspace:*",
7060
"@stacksjs/development": "workspace:*",
71-
"@types/pluralize": "^0.0.33",
61+
"@stacksjs/types": "workspace:*",
7262
"@types/validator": "^13.12.2"
7363
}
7464
}
Lines changed: 293 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// thank you https://github.com/blakeembrey/change-case
2+
// for much of this code
3+
4+
// TODO: implement `string-ts` logic
5+
16
/**
27
* First letter uppercase, other lowercase
38
* @category string
@@ -14,18 +19,291 @@ export function lowercase(str: string): string {
1419
return str.toLowerCase()
1520
}
1621

17-
export {
18-
camelCase,
19-
capitalCase,
20-
constantCase,
21-
dotCase,
22-
kebabCase,
23-
kebabCase as paramCase,
24-
noCase,
25-
pascalCase,
26-
pathCase,
27-
sentenceCase,
28-
snakeCase,
29-
split,
30-
} from 'change-case'
31-
export { titleCase } from 'title-case'
22+
// Regexps involved with splitting words in various case formats.
23+
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu
24+
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})(\p{Lu}\p{Ll})/gu
25+
26+
// Used to iterate over the initial split result and separate numbers.
27+
const SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u
28+
29+
// Regexp involved with stripping non-word characters from the result.
30+
const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu
31+
32+
// The replacement value for splits.
33+
const SPLIT_REPLACE_VALUE = '$1\0$2'
34+
35+
// The default characters to keep after transforming case.
36+
const DEFAULT_PREFIX_SUFFIX_CHARACTERS = ''
37+
38+
/**
39+
* Supported locale values. Use `false` to ignore locale.
40+
* Defaults to `undefined`, which uses the host environment.
41+
*/
42+
export type Locale = string[] | string | false | undefined
43+
44+
/**
45+
* Options used for converting strings to pascal/camel case.
46+
*/
47+
export interface PascalCaseOptions extends CaseOptions {
48+
mergeAmbiguousCharacters?: boolean
49+
}
50+
51+
/**
52+
* Options used for converting strings to any case.
53+
*/
54+
export interface CaseOptions {
55+
locale?: Locale
56+
split?: (value: string) => string[]
57+
delimiter?: string
58+
prefixCharacters?: string
59+
suffixCharacters?: string
60+
}
61+
62+
/**
63+
* Split any cased input strings into an array of words.
64+
*/
65+
export function split(value: string): string[] {
66+
let result = value.trim()
67+
68+
result = result
69+
.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE)
70+
.replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE)
71+
72+
result = result.replace(DEFAULT_STRIP_REGEXP, '\0')
73+
74+
let start = 0
75+
let end = result.length
76+
77+
// Trim the delimiter from around the output string.
78+
while (result.charAt(start) === '\0') start++
79+
if (start === end)
80+
return []
81+
while (result.charAt(end - 1) === '\0') end--
82+
83+
return result.slice(start, end).split(/\0/g)
84+
}
85+
86+
/**
87+
* Split the input string into an array of words, separating numbers.
88+
*/
89+
export function splitSeparateNumbers(value: string): string[] {
90+
const words = split(value)
91+
for (let i = 0; i < words.length; i++) {
92+
const word = words[i]
93+
const match = SPLIT_SEPARATE_NUMBER_RE.exec(word)
94+
if (match) {
95+
const offset = match.index + (match[1] ?? match[2]).length
96+
words.splice(i, 1, word.slice(0, offset), word.slice(offset))
97+
}
98+
}
99+
return words
100+
}
101+
102+
/**
103+
* Convert a string to space separated lower case (`foo bar`).
104+
*/
105+
export function noCase(input: string, options?: CaseOptions): string {
106+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
107+
return (
108+
prefix
109+
+ words.map(lowerFactory(options?.locale)).join(options?.delimiter ?? ' ')
110+
+ suffix
111+
)
112+
}
113+
114+
/**
115+
* Convert a string to camel case (`fooBar`).
116+
*/
117+
export function camelCase(input: string, options?: PascalCaseOptions): string {
118+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
119+
const lower = lowerFactory(options?.locale)
120+
const upper = upperFactory(options?.locale)
121+
const transform = options?.mergeAmbiguousCharacters
122+
? capitalCaseTransformFactory(lower, upper)
123+
: pascalCaseTransformFactory(lower, upper)
124+
return (
125+
prefix
126+
+ words
127+
.map((word, index) => {
128+
if (index === 0)
129+
return lower(word)
130+
return transform(word, index)
131+
})
132+
.join(options?.delimiter ?? '')
133+
+ suffix
134+
)
135+
}
136+
137+
/**
138+
* Convert a string to pascal case (`FooBar`).
139+
*/
140+
export function pascalCase(input: string, options?: PascalCaseOptions): string {
141+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
142+
const lower = lowerFactory(options?.locale)
143+
const upper = upperFactory(options?.locale)
144+
const transform = options?.mergeAmbiguousCharacters
145+
? capitalCaseTransformFactory(lower, upper)
146+
: pascalCaseTransformFactory(lower, upper)
147+
return prefix + words.map(transform).join(options?.delimiter ?? '') + suffix
148+
}
149+
150+
/**
151+
* Convert a string to pascal snake case (`Foo_Bar`).
152+
*/
153+
export function pascalSnakeCase(input: string, options?: CaseOptions): string {
154+
return capitalCase(input, { delimiter: '_', ...options })
155+
}
156+
157+
/**
158+
* Convert a string to capital case (`Foo Bar`).
159+
*/
160+
export function capitalCase(input: string, options?: CaseOptions): string {
161+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
162+
const lower = lowerFactory(options?.locale)
163+
const upper = upperFactory(options?.locale)
164+
return (
165+
prefix
166+
+ words
167+
.map(capitalCaseTransformFactory(lower, upper))
168+
.join(options?.delimiter ?? ' ')
169+
+ suffix
170+
)
171+
}
172+
173+
/**
174+
* Convert a string to constant case (`FOO_BAR`).
175+
*/
176+
export function constantCase(input: string, options?: CaseOptions): string {
177+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
178+
return (
179+
prefix
180+
+ words.map(upperFactory(options?.locale)).join(options?.delimiter ?? '_')
181+
+ suffix
182+
)
183+
}
184+
185+
/**
186+
* Convert a string to dot case (`foo.bar`).
187+
*/
188+
export function dotCase(input: string, options?: CaseOptions): string {
189+
return noCase(input, { delimiter: '.', ...options })
190+
}
191+
192+
/**
193+
* Convert a string to kebab case (`foo-bar`).
194+
*/
195+
export function kebabCase(input: string, options?: CaseOptions): string {
196+
return noCase(input, { delimiter: '-', ...options })
197+
}
198+
199+
/**
200+
* Convert a string to path case (`foo/bar`).
201+
*/
202+
export function pathCase(input: string, options?: CaseOptions): string {
203+
return noCase(input, { delimiter: '/', ...options })
204+
}
205+
206+
/**
207+
* Convert a string to path case (`Foo bar`).
208+
*/
209+
export function sentenceCase(input: string, options?: CaseOptions): string {
210+
const [prefix, words, suffix] = splitPrefixSuffix(input, options)
211+
const lower = lowerFactory(options?.locale)
212+
const upper = upperFactory(options?.locale)
213+
const transform = capitalCaseTransformFactory(lower, upper)
214+
return (
215+
prefix
216+
+ words
217+
.map((word, index) => {
218+
if (index === 0)
219+
return transform(word)
220+
return lower(word)
221+
})
222+
.join(options?.delimiter ?? ' ')
223+
+ suffix
224+
)
225+
}
226+
227+
/**
228+
* Convert a string to snake case (`foo_bar`).
229+
*/
230+
export function snakeCase(input: string, options?: CaseOptions): string {
231+
return noCase(input, { delimiter: '_', ...options })
232+
}
233+
234+
/**
235+
* Convert a string to header case (`Foo-Bar`).
236+
*/
237+
export function trainCase(input: string, options?: CaseOptions): string {
238+
return capitalCase(input, { delimiter: '-', ...options })
239+
}
240+
241+
function lowerFactory(locale: Locale): (input: string) => string {
242+
return locale === false
243+
? (input: string) => input.toLowerCase()
244+
: (input: string) => input.toLocaleLowerCase(locale)
245+
}
246+
247+
function upperFactory(locale: Locale): (input: string) => string {
248+
return locale === false
249+
? (input: string) => input.toUpperCase()
250+
: (input: string) => input.toLocaleUpperCase(locale)
251+
}
252+
253+
function capitalCaseTransformFactory(
254+
lower: (input: string) => string,
255+
upper: (input: string) => string,
256+
) {
257+
return (word: string) => `${upper(word[0])}${lower(word.slice(1))}`
258+
}
259+
260+
function pascalCaseTransformFactory(
261+
lower: (input: string) => string,
262+
upper: (input: string) => string,
263+
) {
264+
return (word: string, index: number) => {
265+
const char0 = word[0]
266+
const initial
267+
= index > 0 && char0 >= '0' && char0 <= '9' ? `_${char0}` : upper(char0)
268+
return initial + lower(word.slice(1))
269+
}
270+
}
271+
272+
function splitPrefixSuffix(
273+
input: string,
274+
options: CaseOptions = {},
275+
): [string, string[], string] {
276+
const splitFn
277+
= options.split ?? split
278+
const prefixCharacters
279+
= options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS
280+
const suffixCharacters
281+
= options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS
282+
let prefixIndex = 0
283+
let suffixIndex = input.length
284+
285+
while (prefixIndex < input.length) {
286+
const char = input.charAt(prefixIndex)
287+
if (!prefixCharacters.includes(char))
288+
break
289+
prefixIndex++
290+
}
291+
292+
while (suffixIndex > prefixIndex) {
293+
const index = suffixIndex - 1
294+
const char = input.charAt(index)
295+
if (!suffixCharacters.includes(char))
296+
break
297+
suffixIndex = index
298+
}
299+
300+
return [
301+
input.slice(0, prefixIndex),
302+
splitFn(input.slice(prefixIndex, suffixIndex)),
303+
input.slice(suffixIndex),
304+
]
305+
}
306+
307+
export * from './title-case'
308+
export * from './swap-case'
309+
export * from './sponge-case'

0 commit comments

Comments
 (0)