├── .editorconfig ├── .gitattributes ├── .github ├── funding.yml ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: sindresorhus 2 | open_collective: sindresorhus 3 | tidelift: npm/decamelize 4 | custom: https://sindresorhus.com/donate 5 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | - 14 15 | - 12 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | The character or string used to separate words. 4 | 5 | @default '_' 6 | 7 | @example 8 | ``` 9 | import decamelize from 'decamelize'; 10 | 11 | decamelize('unicornRainbow'); 12 | //=> 'unicorn_rainbow' 13 | 14 | decamelize('unicornRainbow', {separator: '-'}); 15 | //=> 'unicorn-rainbow' 16 | ``` 17 | */ 18 | readonly separator?: string; 19 | 20 | /** 21 | Preserve sequences of uppercase characters. 22 | 23 | @default false 24 | 25 | @example 26 | ``` 27 | import decamelize from 'decamelize'; 28 | 29 | decamelize('testGUILabel'); 30 | //=> 'test_gui_label' 31 | 32 | decamelize('testGUILabel', {preserveConsecutiveUppercase: true}); 33 | //=> 'test_GUI_label' 34 | ``` 35 | */ 36 | readonly preserveConsecutiveUppercase?: boolean; 37 | }; 38 | 39 | /** 40 | Convert a camelized string into a lowercased one with a custom separator: `unicornRainbow` → `unicorn_rainbow`. 41 | 42 | @param string - The camelcase string to decamelize. 43 | 44 | @example 45 | ``` 46 | import decamelize from 'decamelize'; 47 | 48 | decamelize('unicornRainbow'); 49 | //=> 'unicorn_rainbow' 50 | 51 | decamelize('unicornRainbow', {separator: '-'}); 52 | //=> 'unicorn-rainbow' 53 | ``` 54 | */ 55 | export default function decamelize(string: string, options?: Options): string; 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const handlePreserveConsecutiveUppercase = (decamelized, separator) => { 2 | // Lowercase all single uppercase characters. As we 3 | // want to preserve uppercase sequences, we cannot 4 | // simply lowercase the separated string at the end. 5 | // `data_For_USACounties` → `data_for_USACounties` 6 | decamelized = decamelized.replace( 7 | /((? $0.toLowerCase(), 9 | ); 10 | 11 | // Remaining uppercase sequences will be separated from lowercase sequences. 12 | // `data_For_USACounties` → `data_for_USA_counties` 13 | return decamelized.replace( 14 | /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu, 15 | (_, $1, $2) => $1 + separator + $2.toLowerCase(), 16 | ); 17 | }; 18 | 19 | export default function decamelize( 20 | text, 21 | { 22 | separator = '_', 23 | preserveConsecutiveUppercase = false, 24 | } = {}, 25 | ) { 26 | if (!(typeof text === 'string' && typeof separator === 'string')) { 27 | throw new TypeError( 28 | 'The `text` and `separator` arguments should be of type `string`', 29 | ); 30 | } 31 | 32 | // Checking the second character is done later on. Therefore process shorter strings here. 33 | if (text.length < 2) { 34 | return preserveConsecutiveUppercase ? text : text.toLowerCase(); 35 | } 36 | 37 | const replacement = `$1${separator}$2`; 38 | 39 | // Split lowercase sequences followed by uppercase character. 40 | // `dataForUSACounties` → `data_For_USACounties` 41 | // `myURLstring → `my_URLstring` 42 | const decamelized = text.replace( 43 | /([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, 44 | replacement, 45 | ); 46 | 47 | if (preserveConsecutiveUppercase) { 48 | return handlePreserveConsecutiveUppercase(decamelized, separator); 49 | } 50 | 51 | // Split multiple uppercase characters followed by one or more lowercase characters. 52 | // `my_URLstring` → `my_ur_lstring` 53 | return decamelized 54 | .replace( 55 | /(\p{Uppercase_Letter})(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu, 56 | replacement, 57 | ) 58 | .toLowerCase(); 59 | } 60 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import decamelize from './index.js'; 3 | 4 | expectType(decamelize('unicornRainbow')); 5 | expectType(decamelize('unicornRainbow', {separator: '-'})); 6 | expectType(decamelize('unicornRainbow', { 7 | separator: '-', 8 | preserveConsecutiveUppercase: true, 9 | })); 10 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decamelize", 3 | "version": "6.0.0", 4 | "description": "Convert a camelized string into a lowercased one with a custom separator: unicornRainbow → unicorn_rainbow", 5 | "license": "MIT", 6 | "repository": "sindresorhus/decamelize", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "engines": { 16 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 17 | }, 18 | "scripts": { 19 | "test": "xo && ava && tsd" 20 | }, 21 | "files": [ 22 | "index.js", 23 | "index.d.ts" 24 | ], 25 | "keywords": [ 26 | "decamelize", 27 | "decamelcase", 28 | "camelcase", 29 | "lowercase", 30 | "case", 31 | "dash", 32 | "hyphen", 33 | "string", 34 | "text", 35 | "convert" 36 | ], 37 | "devDependencies": { 38 | "ava": "^5.0.1", 39 | "tsd": "^0.24.1", 40 | "xo": "^0.52.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # decamelize 2 | 3 | > Convert a camelized string into a lowercased one with a custom separator\ 4 | > Example: `unicornRainbow` → `unicorn_rainbow` 5 | 6 | If you use this on untrusted user input, don't forget to limit the length to something reasonable. 7 | 8 | ## Install 9 | 10 | ```sh 11 | npm install decamelize 12 | ``` 13 | 14 | *If you need Safari support, [stay on](https://github.com/sindresorhus/decamelize/issues/24) [version 3](https://github.com/sindresorhus/decamelize/issues/36) [until they implement](https://caniuse.com/js-regexp-lookbehind) regex lookbehinds.* 15 | 16 | ## Usage 17 | 18 | ```js 19 | import decamelize from 'decamelize'; 20 | 21 | decamelize('unicornRainbow'); 22 | //=> 'unicorn_rainbow' 23 | 24 | decamelize('unicornRainbow', {separator: '-'}); 25 | //=> 'unicorn-rainbow' 26 | 27 | decamelize('testGUILabel', {preserveConsecutiveUppercase: true}); 28 | //=> 'test_GUI_label' 29 | 30 | decamelize('testGUILabel', {preserveConsecutiveUppercase: false}); 31 | //=> 'test_gui_label' 32 | ``` 33 | 34 | ## API 35 | 36 | ### decamelize(input, options?) 37 | 38 | #### input 39 | 40 | Type: `string` 41 | 42 | #### options 43 | 44 | Type: `object` 45 | 46 | ##### separator 47 | 48 | Type: `string`\ 49 | Default: `'_'` 50 | 51 | The character or string used to separate words. 52 | 53 | ```js 54 | import decamelize from 'decamelize'; 55 | 56 | decamelize('unicornRainbow'); 57 | //=> 'unicorn_rainbow' 58 | 59 | decamelize('unicornRainbow', {separator: '-'}); 60 | //=> 'unicorn-rainbow' 61 | ``` 62 | 63 | ##### preserveConsecutiveUppercase 64 | 65 | Type: `boolean`\ 66 | Default: `false` 67 | 68 | Preserve sequences of uppercase characters. 69 | 70 | ```js 71 | import decamelize from 'decamelize'; 72 | 73 | decamelize('testGUILabel'); 74 | //=> 'test_gui_label' 75 | 76 | decamelize('testGUILabel', {preserveConsecutiveUppercase: true}); 77 | //=> 'test_GUI_label' 78 | ``` 79 | 80 | ## Related 81 | 82 | - [camelcase](https://github.com/sindresorhus/camelcase) - The inverse of this package 83 | - [decamelize-keys](https://github.com/sindresorhus/decamelize-keys) - Convert object keys from camel case 84 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import decamelize from './index.js'; 3 | 4 | test('decamelize', t => { 5 | t.is(decamelize(''), ''); 6 | t.is(decamelize('A'), 'a'); 7 | t.is(decamelize('A B'), 'a b'); 8 | t.is(decamelize('a2b'), 'a2b'); 9 | t.is(decamelize('A2B'), 'a2_b'); 10 | t.is(decamelize('_A2B'), '_a2_b'); 11 | t.is(decamelize('myURLstring'), 'my_ur_lstring'); 12 | t.is(decamelize('unicornsAndRainbows'), 'unicorns_and_rainbows'); 13 | t.is(decamelize('UNICORNS AND RAINBOWS'), 'unicorns and rainbows'); 14 | t.is(decamelize('unicorns-and-rainbows'), 'unicorns-and-rainbows'); 15 | t.is(decamelize('thisIsATest'), 'this_is_a_test'); 16 | t.is(decamelize('thisIsATest', {separator: ' '}), 'this is a test'); 17 | t.is(decamelize('thisIsATest', {separator: ''}), 'thisisatest'); 18 | t.is(decamelize('unicornRainbow', {separator: '|'}), 'unicorn|rainbow'); 19 | t.is(decamelize('unicornRainbow', {separator: '-'}), 'unicorn-rainbow'); 20 | t.is( 21 | decamelize('thisHasSpecialCharactersLikeČandŠ', {separator: ' '}), 22 | 'this has special characters like čand š', 23 | ); 24 | }); 25 | 26 | test('handles acronyms', t => { 27 | t.is(decamelize('myURLString'), 'my_url_string'); 28 | t.is(decamelize('URLString'), 'url_string'); 29 | t.is(decamelize('StringURL'), 'string_url'); 30 | t.is(decamelize('testGUILabel'), 'test_gui_label'); 31 | t.is(decamelize('CAPLOCKED1'), 'caplocked1'); 32 | }); 33 | 34 | test('separator in string', t => { 35 | t.is(decamelize('my_URL_string'), 'my_url_string'); 36 | }); 37 | 38 | test('separator and options passed', t => { 39 | t.is( 40 | decamelize('testGUILabel', { 41 | separator: '!', 42 | preserveConsecutiveUppercase: true, 43 | }), 44 | 'test!GUI!label', 45 | ); 46 | }); 47 | 48 | test('keeping blocks of consecutive uppercase characters but split the last if lowercase characters follow', t => { 49 | t.is( 50 | decamelize('A', { 51 | preserveConsecutiveUppercase: true, 52 | }), 53 | 'A', 54 | ); 55 | t.is( 56 | decamelize('myURLString', { 57 | preserveConsecutiveUppercase: true, 58 | }), 59 | 'my_URL_string', 60 | ); 61 | t.is( 62 | decamelize('URLString', { 63 | preserveConsecutiveUppercase: true, 64 | }), 65 | 'URL_string', 66 | ); 67 | t.is( 68 | decamelize('oxygenO2Level', { 69 | preserveConsecutiveUppercase: true, 70 | }), 71 | 'oxygen_O2_level', 72 | ); 73 | t.is( 74 | decamelize('StringURL', { 75 | preserveConsecutiveUppercase: true, 76 | }), 77 | 'string_URL', 78 | ); 79 | t.is( 80 | decamelize('STringURL', { 81 | preserveConsecutiveUppercase: true, 82 | }), 83 | 'S_tring_URL', 84 | ); 85 | t.is( 86 | decamelize('numberOfDataForUSA', { 87 | preserveConsecutiveUppercase: true, 88 | }), 89 | 'number_of_data_for_USA', 90 | ); 91 | t.is( 92 | decamelize('testGUILabel', { 93 | preserveConsecutiveUppercase: true, 94 | }), 95 | 'test_GUI_label', 96 | ); 97 | t.is( 98 | decamelize('CAPLOCKED1', { 99 | preserveConsecutiveUppercase: true, 100 | }), 101 | 'CAPLOCKED1', 102 | ); 103 | }); 104 | 105 | test('long strings', t => { 106 | // Factor to increase the test string 107 | const times = 100; 108 | const longString = 'Lb8SvAARMshcNvfxjgGCgfot3AZAzysuxRpG9XfpLCz89TeWqAd3TUo64K45VH2MfjLYhztt4LQYzrEbTpx7gGcG4T8ueKPm6VraXKtULJdncFQhEQfCRwWGNscdFe6UTEAvN7Nze4Qy4hvZuKLX5YiohGpvNZUtLGen3WP2jot8VeprzyXQmiKdxdxrEResSRgSWENCzXZPSerYuEfApVbjuDJZ9kGMRXFRZQVyBDDGfY9ERqtxHQxPw65TtEo3dgwhcuhvC3dMyRJ6jWaonKB3Pqtv27vRv5MgYb5mgvCE55oCTBG9yASPaw2KqYVz3amBge9HggEzXJGhwSXjkL7jUYk3WjQUbwVnZNHkH3P9MpvM98DtTnGAYfK5TjD8Y5oXPRJmdCHzhByboaW2oRJ2Ft7dxGKXLs2s7qsQs8FsJHVcYrmVHRa6th5CizHSXK7vr5D3KYsfsnr92AmtR4LERam7CV9emBBuykQJMejLGFsvgTrBKmmUqijxSgY'.repeat( 109 | 100, 110 | ); 111 | 112 | t.is( 113 | decamelize(longString), 114 | Array.from({length: times}) 115 | .fill( 116 | 'lb8_sv_aar_mshc_nvfxjg_g_cgfot3_az_azysux_rp_g9_xfp_l_cz89_te_wq_ad3_t_uo64_k45_vh2_mfj_l_yhztt4_lq_yzr_eb_tpx7g_gc_g4_t8ue_k_pm6_vra_x_kt_ul_jdnc_f_qh_e_qf_c_rw_wg_nscd_fe6_ute_av_n7_nze4_qy4hv_zu_klx5_yioh_gpv_nz_ut_l_gen3_wp2jot8_veprzy_x_qmi_kdxdxr_e_res_s_rg_swen_cz_xzp_ser_yu_ef_ap_vbju_djz9k_gmrxfrzq_vy_bdd_gf_y9_e_rqtx_h_qx_pw65_tt_eo3dgwhcuhv_c3d_my_rj6j_waon_kb3_pqtv27v_rv5_mg_yb5mgv_ce55o_ctbg9y_as_paw2_kq_y_vz3am_bge9_hgg_ez_xj_ghw_s_xjk_l7j_u_yk3_wj_q_ubw_vn_zn_hk_h3_p9_mpv_m98_dt_tn_ga_yf_k5_tj_d8_y5o_xpr_jmd_c_hzh_byboa_w2o_rj2_ft7dx_gkx_ls2s7qs_qs8_fs_jh_vc_yrm_vh_ra6th5_ciz_hsxk7vr5_d3_k_ysfsnr92_amt_r4_le_ram7_cv9em_b_buyk_qj_mej_lg_fsvg_tr_b_kmm_uqijx_sg_y', 117 | ) 118 | .join('_'), 119 | ); 120 | t.is( 121 | decamelize(longString, {separator: '!'}), 122 | Array.from({length: times}) 123 | .fill( 124 | 'lb8!sv!aar!mshc!nvfxjg!g!cgfot3!az!azysux!rp!g9!xfp!l!cz89!te!wq!ad3!t!uo64!k45!vh2!mfj!l!yhztt4!lq!yzr!eb!tpx7g!gc!g4!t8ue!k!pm6!vra!x!kt!ul!jdnc!f!qh!e!qf!c!rw!wg!nscd!fe6!ute!av!n7!nze4!qy4hv!zu!klx5!yioh!gpv!nz!ut!l!gen3!wp2jot8!veprzy!x!qmi!kdxdxr!e!res!s!rg!swen!cz!xzp!ser!yu!ef!ap!vbju!djz9k!gmrxfrzq!vy!bdd!gf!y9!e!rqtx!h!qx!pw65!tt!eo3dgwhcuhv!c3d!my!rj6j!waon!kb3!pqtv27v!rv5!mg!yb5mgv!ce55o!ctbg9y!as!paw2!kq!y!vz3am!bge9!hgg!ez!xj!ghw!s!xjk!l7j!u!yk3!wj!q!ubw!vn!zn!hk!h3!p9!mpv!m98!dt!tn!ga!yf!k5!tj!d8!y5o!xpr!jmd!c!hzh!byboa!w2o!rj2!ft7dx!gkx!ls2s7qs!qs8!fs!jh!vc!yrm!vh!ra6th5!ciz!hsxk7vr5!d3!k!ysfsnr92!amt!r4!le!ram7!cv9em!b!buyk!qj!mej!lg!fsvg!tr!b!kmm!uqijx!sg!y', 125 | ) 126 | .join('!'), 127 | ); 128 | t.is( 129 | decamelize(longString, { 130 | separator: '!', 131 | preserveConsecutiveUppercase: true, 132 | }), 133 | Array.from({length: times}) 134 | .fill( 135 | 'lb8!sv!AAR!mshc!nvfxjg!G!cgfot3!AZ!azysux!rp!G9!xfp!L!cz89!te!wq!ad3!T!uo64!K45!VH2!mfj!L!yhztt4!LQ!yzr!eb!tpx7g!gc!G4!T8ue!K!pm6!vra!X!kt!UL!jdnc!F!qh!E!qf!C!rw!WG!nscd!fe6!UTE!av!N7!nze4!qy4hv!zu!KLX5!yioh!gpv!NZ!ut!L!gen3!WP2jot8!veprzy!X!qmi!kdxdxr!E!res!S!rg!SWEN!cz!XZP!ser!yu!ef!ap!vbju!DJZ9k!GMRXFRZQ!vy!BDD!gf!Y9!E!rqtx!H!qx!pw65!tt!eo3dgwhcuhv!C3d!my!RJ6j!waon!KB3!pqtv27v!rv5!mg!yb5mgv!CE55o!CTBG9y!AS!paw2!kq!Y!vz3am!bge9!hgg!ez!XJ!ghw!S!xjk!L7j!U!yk3!wj!Q!ubw!vn!ZN!hk!H3!P9!mpv!M98!dt!tn!GA!yf!K5!tj!D8!Y5o!XPR!jmd!C!hzh!byboa!W2o!RJ2!ft7dx!GKX!ls2s7qs!qs8!fs!JH!vc!yrm!VH!ra6th5!ciz!HSXK7vr5!D3!K!ysfsnr92!amt!R4!LE!ram7!CV9em!B!buyk!QJ!mej!LG!fsvg!tr!B!kmm!uqijx!sg!', 136 | ) 137 | .join('Y!') + 'y', 138 | ); 139 | }); 140 | --------------------------------------------------------------------------------