fork of hey-api/openapi-ts because I need some additional things
1import type { Casing, NamingConfig, NamingRule } from './types';
2
3const uppercaseRegExp = /[\p{Lu}]/u;
4const lowercaseRegExp = /[\p{Ll}]/u;
5const identifierRegExp = /([\p{Alpha}\p{N}_]|$)/u;
6const separatorsRegExp = /[_.$+:\- `\\[\](){}\\/]+/;
7
8const leadingSeparatorsRegExp = new RegExp(`^${separatorsRegExp.source}`);
9const separatorsAndIdentifierRegExp = new RegExp(
10 `${separatorsRegExp.source}${identifierRegExp.source}`,
11 'gu',
12);
13const numbersAndIdentifierRegExp = new RegExp(`\\d+${identifierRegExp.source}`, 'gu');
14
15const preserveCase = (value: string, casing: Casing) => {
16 let isLastCharLower = false;
17 let isLastCharUpper = false;
18 let isLastLastCharUpper = false;
19 let isLastLastCharPreserved = false;
20
21 const separator = casing === 'snake_case' || casing === 'SCREAMING_SNAKE_CASE' ? '_' : '-';
22
23 for (let index = 0; index < value.length; index++) {
24 const character = value[index]!;
25 isLastLastCharPreserved = index > 2 ? value[index - 3] === separator : true;
26
27 let nextIndex = index + 1;
28 let nextCharacter = value[nextIndex];
29 separatorsRegExp.lastIndex = 0;
30 while (nextCharacter && separatorsRegExp.test(nextCharacter)) {
31 nextIndex += 1;
32 nextCharacter = value[nextIndex];
33 }
34 const isSeparatorBeforeNextCharacter = nextIndex !== index + 1;
35
36 lowercaseRegExp.lastIndex = 0;
37 uppercaseRegExp.lastIndex = 0;
38 if (
39 uppercaseRegExp.test(character) &&
40 (isLastCharLower ||
41 (nextCharacter &&
42 !isSeparatorBeforeNextCharacter &&
43 nextCharacter !== 's' &&
44 lowercaseRegExp.test(nextCharacter)))
45 ) {
46 // insert separator behind character
47 value = `${value.slice(0, index)}${separator}${value.slice(index)}`;
48 index++;
49 isLastLastCharUpper = isLastCharUpper;
50 isLastCharLower = false;
51 isLastCharUpper = true;
52 } else if (
53 isLastCharUpper &&
54 isLastLastCharUpper &&
55 lowercaseRegExp.test(character) &&
56 !isLastLastCharPreserved &&
57 // naive detection of plurals
58 !(
59 character === 's' &&
60 (!nextCharacter || nextCharacter.toLocaleLowerCase() !== nextCharacter)
61 )
62 ) {
63 // insert separator 2 characters behind
64 value = `${value.slice(0, index - 1)}${separator}${value.slice(index - 1)}`;
65 isLastLastCharUpper = isLastCharUpper;
66 isLastCharLower = true;
67 isLastCharUpper = false;
68 } else {
69 const characterLower = character.toLocaleLowerCase();
70 const characterUpper = character.toLocaleUpperCase();
71 isLastLastCharUpper = isLastCharUpper;
72 isLastCharLower = characterLower === character && characterUpper !== character;
73 isLastCharUpper = characterUpper === character && characterLower !== character;
74 }
75 }
76
77 return value;
78};
79
80/**
81 * Convert a string to the specified casing.
82 *
83 * @param value - The string to convert
84 * @param casing - The target casing
85 * @param options - Additional options
86 * @returns The converted string
87 */
88export function toCase(
89 value: string,
90 casing: Casing | undefined,
91 options: {
92 /**
93 * If leading separators have a semantic meaning, we might not want to
94 * remove them.
95 */
96 stripLeadingSeparators?: boolean;
97 } = {},
98): string {
99 const stripLeadingSeparators = options.stripLeadingSeparators ?? true;
100
101 let result = value.trim();
102
103 if (!result.length || !casing || casing === 'preserve') {
104 return result;
105 }
106
107 if (result.length === 1) {
108 separatorsRegExp.lastIndex = 0;
109 if (separatorsRegExp.test(result)) {
110 return '';
111 }
112
113 return casing === 'PascalCase' || casing === 'SCREAMING_SNAKE_CASE'
114 ? result.toLocaleUpperCase()
115 : result.toLocaleLowerCase();
116 }
117
118 const hasUpperCase = result !== result.toLocaleLowerCase();
119
120 if (hasUpperCase) {
121 result = preserveCase(result, casing);
122 }
123
124 if (stripLeadingSeparators || result[0] !== value[0]) {
125 result = result.replace(leadingSeparatorsRegExp, '');
126 }
127
128 result =
129 casing === 'SCREAMING_SNAKE_CASE' ? result.toLocaleUpperCase() : result.toLocaleLowerCase();
130
131 if (casing === 'PascalCase') {
132 result = `${result.charAt(0).toLocaleUpperCase()}${result.slice(1)}`;
133 }
134
135 if (casing === 'snake_case' || casing === 'SCREAMING_SNAKE_CASE') {
136 result = result.replaceAll(separatorsAndIdentifierRegExp, (match, identifier, offset) => {
137 if (offset === 0 && !stripLeadingSeparators) {
138 return match;
139 }
140 return `_${identifier}`;
141 });
142
143 if (result[result.length - 1] === '_') {
144 // strip trailing underscore
145 result = result.slice(0, result.length - 1);
146 }
147 } else {
148 separatorsAndIdentifierRegExp.lastIndex = 0;
149 numbersAndIdentifierRegExp.lastIndex = 0;
150
151 result = result.replaceAll(numbersAndIdentifierRegExp, (match, _, offset) => {
152 if (['_', '-', '.'].includes(result.charAt(offset + match.length))) {
153 return match;
154 }
155
156 return match.toLocaleUpperCase();
157 });
158
159 result = result.replaceAll(separatorsAndIdentifierRegExp, (match, identifier, offset) => {
160 if (offset === 0 && !stripLeadingSeparators && match[0] && value.startsWith(match[0])) {
161 return match;
162 }
163 return identifier.toLocaleUpperCase();
164 });
165 }
166
167 return result;
168}
169
170/**
171 * Normalize a NamingRule to NamingConfig.
172 */
173export function resolveNaming(rule: NamingRule | undefined): NamingConfig {
174 if (!rule) {
175 return {};
176 }
177 if (typeof rule === 'string' || typeof rule === 'function') {
178 return { name: rule };
179 }
180 return rule;
181}
182
183/**
184 * Apply naming configuration to a value.
185 *
186 * Casing is applied first, then transformation.
187 */
188export function applyNaming(value: string, config: NamingConfig): string {
189 let result = value;
190
191 const casing = config.casing ?? config.case;
192
193 if (config.name) {
194 if (typeof config.name === 'function') {
195 result = config.name(result);
196 } else {
197 // TODO: refactor so there's no need for separators?
198 const separator = !casing || casing === 'preserve' ? '' : '-';
199 result = config.name.replace('{{name}}', `${separator}${result}${separator}`);
200 }
201 }
202
203 // TODO: apply case before name?
204 return toCase(result, casing);
205}