fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 205 lines 6.2 kB view raw
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}