diff --git a/packages/block-editor/src/components/colors/utils.js b/packages/block-editor/src/components/colors/utils.js index e48b599faa6b96..1c1947bfc947cc 100644 --- a/packages/block-editor/src/components/colors/utils.js +++ b/packages/block-editor/src/components/colors/utils.js @@ -1,11 +1,15 @@ /** * External dependencies */ -import { kebabCase } from 'lodash'; import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; import a11yPlugin from 'colord/plugins/a11y'; +/** + * Internal dependencies + */ +import { kebabCase } from '../../utils/object'; + extend( [ namesPlugin, a11yPlugin ] ); /** diff --git a/packages/block-editor/src/components/colors/with-colors.js b/packages/block-editor/src/components/colors/with-colors.js index 0444aab3050516..c93ed8bf775a03 100644 --- a/packages/block-editor/src/components/colors/with-colors.js +++ b/packages/block-editor/src/components/colors/with-colors.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { kebabCase } from 'lodash'; - /** * WordPress dependencies */ @@ -19,6 +14,7 @@ import { getMostReadableColor, } from './utils'; import useSetting from '../use-setting'; +import { kebabCase } from '../../utils/object'; /** * Capitalizes the first letter in a string. diff --git a/packages/block-editor/src/components/font-sizes/utils.js b/packages/block-editor/src/components/font-sizes/utils.js index a6f1d80fda4e74..2f874f6665f8f3 100644 --- a/packages/block-editor/src/components/font-sizes/utils.js +++ b/packages/block-editor/src/components/font-sizes/utils.js @@ -1,7 +1,7 @@ /** - * External dependencies + * Internal dependencies */ -import { kebabCase } from 'lodash'; +import { kebabCase } from '../../utils/object'; /** * Returns the font size object based on an array of named font sizes and the namedFontSize and customFontSize values. diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index cea3fe9b9b73b8..76facd7024ee52 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, kebabCase, set } from 'lodash'; +import { get, set } from 'lodash'; /** * WordPress dependencies @@ -32,6 +32,7 @@ import { PresetDuotoneFilter } from '../duotone/components'; import { getGapCSSValue } from '../../hooks/gap'; import { store as blockEditorStore } from '../../store'; import { LAYOUT_DEFINITIONS } from '../../layouts/definitions'; +import { kebabCase } from '../../utils/object'; // List of block support features that can have their related styles // generated under their own feature level selector rather than the block's. diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index 754bcdd1b5bffc..0988b285564d3e 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { kebabCase } from 'lodash'; - /** * WordPress dependencies */ @@ -15,6 +10,7 @@ import TokenList from '@wordpress/token-list'; */ import { shouldSkipSerialization } from './utils'; import { TYPOGRAPHY_SUPPORT_KEY } from './typography'; +import { kebabCase } from '../utils/object'; export const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index c8f9648c2f6349..5362edea927e34 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { kebabCase } from 'lodash'; /** * WordPress dependencies @@ -31,6 +30,7 @@ import BlockList from '../components/block-list'; import { getLayoutType, getLayoutTypes } from '../layouts'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { LAYOUT_DEFINITIONS } from '../layouts/definitions'; +import { kebabCase } from '../utils/object'; const layoutBlockSupportKey = 'layout'; diff --git a/packages/block-editor/src/hooks/use-typography-props.js b/packages/block-editor/src/hooks/use-typography-props.js index f3f7d531974c52..1ed02d4a5835f2 100644 --- a/packages/block-editor/src/hooks/use-typography-props.js +++ b/packages/block-editor/src/hooks/use-typography-props.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { kebabCase } from 'lodash'; import classnames from 'classnames'; /** @@ -13,6 +12,7 @@ import { getTypographyFontSizeValue, getFluidTypographyOptionsFromSettings, } from '../components/global-styles/typography-utils'; +import { kebabCase } from '../utils/object'; /* * This utility is intended to assist where the serialization of the typography diff --git a/packages/block-editor/src/utils/object.js b/packages/block-editor/src/utils/object.js index a7496bd593923c..a543c1e4f1d301 100644 --- a/packages/block-editor/src/utils/object.js +++ b/packages/block-editor/src/utils/object.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { paramCase } from 'change-case'; + /** * Converts a path to an array of its fragments. * Supports strings, numbers and arrays: @@ -19,6 +24,36 @@ function normalizePath( path ) { return [ path ]; } +/** + * Converts any string to kebab case. + * Backwards compatible with Lodash's `_.kebabCase()`. + * Backwards compatible with `_wp_to_kebab_case()`. + * + * @see https://lodash.com/docs/4.17.15#kebabCase + * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ + * + * @param {string} str String to convert. + * @return {string} Kebab-cased string + */ +export function kebabCase( str ) { + let input = str; + if ( typeof str !== 'string' ) { + input = str?.toString?.() ?? ''; + } + + // See https://github.com/lodash/lodash/blob/b185fcee26b2133bd071f4aaca14b455c2ed1008/lodash.js#L4970 + input = input.replace( /['\u2019]/, '' ); + + return paramCase( input, { + splitRegexp: [ + /(?!(?:1ST|2ND|3RD|[4-9]TH)(?![a-z]))([a-z0-9])([A-Z])/g, // fooBar => foo-bar, 3Bar => 3-bar + /(?!(?:1st|2nd|3rd|[4-9]th)(?![a-z]))([0-9])([a-z])/g, // 3bar => 3-bar + /([A-Za-z])([0-9])/g, // Foo3 => foo-3, foo3 => foo-3 + /([A-Z])([A-Z][a-z])/g, // FOOBar => foo-bar + ], + } ); +} + /** * Clones an object. * Non-object values are returned unchanged. diff --git a/packages/block-editor/src/utils/test/object.js b/packages/block-editor/src/utils/test/object.js index 8e363e1511db2f..def7e5e9c8f057 100644 --- a/packages/block-editor/src/utils/test/object.js +++ b/packages/block-editor/src/utils/test/object.js @@ -1,7 +1,102 @@ /** * Internal dependencies */ -import { setImmutably } from '../object'; +import { kebabCase, setImmutably } from '../object'; + +describe( 'kebabCase', () => { + it( 'separates lowercase letters, followed by uppercase letters', () => { + expect( kebabCase( 'fooBar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'separates numbers, followed by uppercase letters', () => { + expect( kebabCase( '123FOO' ) ).toEqual( '123-foo' ); + } ); + + it( 'separates numbers, followed by lowercase characters', () => { + expect( kebabCase( '123bar' ) ).toEqual( '123-bar' ); + } ); + + it( 'separates uppercase letters, followed by numbers', () => { + expect( kebabCase( 'FOO123' ) ).toEqual( 'foo-123' ); + } ); + + it( 'separates lowercase letters, followed by numbers', () => { + expect( kebabCase( 'foo123' ) ).toEqual( 'foo-123' ); + } ); + + it( 'separates uppercase groups from capitalized groups', () => { + expect( kebabCase( 'FOOBar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'removes any non-dash special characters', () => { + expect( + kebabCase( 'foo±§!@#$%^&*()-_=+/?.>,<\\|{}[]`~\'";:bar' ) + ).toEqual( 'foo-bar' ); + } ); + + it( 'removes any spacing characters', () => { + expect( kebabCase( ' foo \t \n \r \f \v bar ' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'groups multiple dashes into a single one', () => { + expect( kebabCase( 'foo---bar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'returns an empty string unchanged', () => { + expect( kebabCase( '' ) ).toEqual( '' ); + } ); + + it( 'returns an existing kebab case string unchanged', () => { + expect( kebabCase( 'foo-123-bar' ) ).toEqual( 'foo-123-bar' ); + } ); + + it( 'returns an empty string if any nullish type is passed', () => { + expect( kebabCase( undefined ) ).toEqual( '' ); + expect( kebabCase( null ) ).toEqual( '' ); + } ); + + it( 'converts any unexpected non-nullish type to a string', () => { + expect( kebabCase( 12345 ) ).toEqual( '12345' ); + expect( kebabCase( [] ) ).toEqual( '' ); + expect( kebabCase( {} ) ).toEqual( 'object-object' ); + } ); + + /** + * Should cover all test cases of `_wp_to_kebab_case()`. + * + * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ + * @see https://github.com/WordPress/wordpress-develop/blob/76376fdbc3dc0b3261de377dffc350677345e7ba/tests/phpunit/tests/functions/wpToKebabCase.php#L35-L62 + */ + it.each( [ + [ 'white', 'white' ], + [ 'white+black', 'white-black' ], + [ 'white:black', 'white-black' ], + [ 'white*black', 'white-black' ], + [ 'white.black', 'white-black' ], + [ 'white black', 'white-black' ], + [ 'white black', 'white-black' ], + [ 'white-to-black', 'white-to-black' ], + [ 'white2white', 'white-2-white' ], + [ 'white2nd', 'white-2nd' ], + [ 'white2ndcolor', 'white-2-ndcolor' ], + [ 'white2ndColor', 'white-2nd-color' ], + [ 'white2nd_color', 'white-2nd-color' ], + [ 'white23color', 'white-23-color' ], + [ 'white23', 'white-23' ], + [ '23color', '23-color' ], + [ 'white4th', 'white-4th' ], + [ 'font2xl', 'font-2-xl' ], + [ 'whiteToWhite', 'white-to-white' ], + [ 'whiteTOwhite', 'white-t-owhite' ], + [ 'WHITEtoWHITE', 'whit-eto-white' ], + [ 42, '42' ], + [ "i've done", 'ive-done' ], + [ '#ffffff', 'ffffff' ], + [ '$ffffff', 'ffffff' ], + ] )( 'converts %s properly to %s', ( input, expected ) => { + expect( kebabCase( input ) ).toEqual( expected ); + } ); +} ); describe( 'setImmutably', () => { describe( 'handling falsy values properly', () => {