diff --git a/docs/manifest.json b/docs/manifest.json index ce41daad14bf8d..8d8c3deaae00f5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2621,6 +2621,12 @@ "markdown_source": "../packages/jest-preset-default/README.md", "parent": "packages" }, + { + "title": "@wordpress/kebab-case", + "slug": "packages-kebab-case", + "markdown_source": "../packages/kebab-case/README.md", + "parent": "packages" + }, { "title": "@wordpress/keyboard-shortcuts", "slug": "packages-keyboard-shortcuts", diff --git a/package-lock.json b/package-lock.json index 830db426cd49a1..ebc96c94b05972 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17504,6 +17504,10 @@ "resolved": "packages/jest-preset-default", "link": true }, + "node_modules/@wordpress/kebab-case": { + "resolved": "packages/kebab-case", + "link": true + }, "node_modules/@wordpress/keyboard-shortcuts": { "resolved": "packages/keyboard-shortcuts", "link": true @@ -50603,6 +50607,7 @@ "@wordpress/image-cropper": "file:../image-cropper", "@wordpress/interactivity": "file:../interactivity", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", @@ -50697,6 +50702,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/interactivity": "file:../interactivity", "@wordpress/interactivity-router": "file:../interactivity-router", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/latex-to-mathml": "file:../latex-to-mathml", @@ -50942,6 +50948,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", @@ -51463,13 +51470,13 @@ "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/ui": "file:../ui", "@wordpress/warning": "file:../warning", - "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.9.3", "date-fns": "^4.4.0", @@ -51898,6 +51905,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interface": "file:../interface", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-editor": "file:../media-editor", @@ -52345,6 +52353,7 @@ "@wordpress/global-styles-engine": "file:../global-styles-engine", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/private-apis": "file:../private-apis", "@wordpress/ui": "file:../ui", @@ -52705,6 +52714,18 @@ "jest": ">=30" } }, + "packages/kebab-case": { + "name": "@wordpress/kebab-case", + "version": "1.0.0-prerelease", + "license": "GPL-2.0-or-later", + "dependencies": { + "change-case": "^4.1.2" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", "version": "5.52.0", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index f040f89af84a3a..becb51cffe9c25 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- Use the new `@wordpress/kebab-case` package instead of unlocking the `kebabCase` utility from the `@wordpress/components` private APIs ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). + ### Enhancements - Creating a new block next to a sibling of the same type now inherits the sibling's attributes consistently, whether it is created by the appender, the inserter, or Enter at the edge of the text. Everything except the sibling's content (attributes with the `content` role) and its `metadata` is copied. The `attributesToCopy` list of a default block is removed: the copied attributes derive from the block's attribute roles. diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index be74a43fd92fd4..4a0427d52aff76 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -83,6 +83,7 @@ "@wordpress/image-cropper": "file:../image-cropper", "@wordpress/interactivity": "file:../interactivity", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", diff --git a/packages/block-editor/src/components/colors/utils.js b/packages/block-editor/src/components/colors/utils.js index f7ade1de91e14e..5bcd1c01a9535f 100644 --- a/packages/block-editor/src/components/colors/utils.js +++ b/packages/block-editor/src/components/colors/utils.js @@ -1,13 +1,10 @@ import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; import a11yPlugin from 'colord/plugins/a11y'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../../lock-unlock'; +import { kebabCase } from '@wordpress/kebab-case'; extend( [ namesPlugin, a11yPlugin ] ); -const { kebabCase } = unlock( componentsPrivateApis ); - /** * Provided an array of color objects as set by the theme or by the editor defaults, * and the values of the defined color or custom color returns a color object describing the color. diff --git a/packages/block-editor/src/components/colors/with-colors.js b/packages/block-editor/src/components/colors/with-colors.js index 6e7f12cb6f7855..2b227e14dfbc89 100644 --- a/packages/block-editor/src/components/colors/with-colors.js +++ b/packages/block-editor/src/components/colors/with-colors.js @@ -1,6 +1,6 @@ import { useMemo, Component } from '@wordpress/element'; import { compose, createHigherOrderComponent } from '@wordpress/compose'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { kebabCase } from '@wordpress/kebab-case'; import { getColorClassName, getColorObjectByColorValue, @@ -8,9 +8,6 @@ import { getMostReadableColor, } from './utils'; import { useSettings } from '../use-settings'; -import { unlock } from '../../lock-unlock'; - -const { kebabCase } = unlock( componentsPrivateApis ); /** * 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 b770f4d73d26a3..f9fd51ac5ec0bc 100644 --- a/packages/block-editor/src/components/font-sizes/utils.js +++ b/packages/block-editor/src/components/font-sizes/utils.js @@ -1,7 +1,4 @@ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../../lock-unlock'; - -const { kebabCase } = unlock( componentsPrivateApis ); +import { kebabCase } from '@wordpress/kebab-case'; /** * 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/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index 5a42c8baf716ed..a52f78c85f7001 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -1,13 +1,11 @@ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; import TokenList from '@wordpress/token-list'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { kebabCase } from '@wordpress/kebab-case'; import { shouldSkipSerialization } from './utils'; import { TYPOGRAPHY_SUPPORT_KEY } from './typography'; -import { unlock } from '../lock-unlock'; export const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; -const { kebabCase } = unlock( componentsPrivateApis ); /** * Filters registered block settings, extending attributes to include diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 08cc7ff6a388c4..b1a50d60894fe8 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -14,9 +14,9 @@ import { __experimentalToggleGroupControlOption as ToggleGroupControlOption, ToggleControl, __experimentalToolsPanelItem as ToolsPanelItem, - privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { kebabCase } from '@wordpress/kebab-case'; import { store as blockEditorStore } from '../store'; import { InspectorControls } from '../components'; import { useSettings } from '../components/use-settings'; @@ -47,7 +47,6 @@ const CHILD_LAYOUT_KEYS = [ 'rowStart', 'rowSpan', ]; -const { kebabCase } = unlock( componentsPrivateApis ); const { getResponsiveMediaQueries } = unlock( globalStylesEnginePrivateApis ); function getDefaultLayout( layoutBlockSupport = {}, blockVariation ) { diff --git a/packages/block-editor/src/hooks/use-typography-props.js b/packages/block-editor/src/hooks/use-typography-props.js index 3d8a0e801f1c26..30a2a03023c2a6 100644 --- a/packages/block-editor/src/hooks/use-typography-props.js +++ b/packages/block-editor/src/hooks/use-typography-props.js @@ -1,11 +1,8 @@ import clsx from 'clsx'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { getTypographyFontSizeValue } from '@wordpress/global-styles-engine'; +import { kebabCase } from '@wordpress/kebab-case'; import { getInlineStyles } from './style'; import { getFontSizeClass } from '../components/font-sizes'; -import { unlock } from '../lock-unlock'; - -const { kebabCase } = unlock( componentsPrivateApis ); /* * This utility is intended to assist where the serialization of the typography diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json index 213ddd9ddb83f6..4e4790f8186f41 100644 --- a/packages/block-editor/tsconfig.json +++ b/packages/block-editor/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../image-cropper" }, { "path": "../interactivity" }, { "path": "../is-shallow-equal" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../keyboard-shortcuts" }, { "path": "../notices" }, diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 3ad8f4f951ae22..0a90c641226da0 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- Embed: Use the new `@wordpress/kebab-case` package instead of unlocking the `kebabCase` utility from the `@wordpress/components` private APIs ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). + ### Enhancements - Playlist Track: Use a dedicated icon for the block toolbar. ([#80959](https://github.com/WordPress/gutenberg/pull/80959)) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 03d5121271dd27..2112e938fc433d 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -116,6 +116,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/interactivity": "file:../interactivity", "@wordpress/interactivity-router": "file:../interactivity-router", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/latex-to-mathml": "file:../latex-to-mathml", diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index fcdec6cd07baeb..9f6e04c3d4ddc5 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -1,6 +1,5 @@ import clsx from 'clsx'; import memoize from 'memize'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { renderToString } from '@wordpress/element'; import { createBlock, @@ -8,12 +7,11 @@ import { getBlockVariations, } from '@wordpress/blocks'; import { getAuthority } from '@wordpress/url'; +import { kebabCase } from '@wordpress/kebab-case'; import metadata from './block.json'; import { ASPECT_RATIOS, WP_EMBED_TYPE } from './constants'; -import { unlock } from '../lock-unlock'; const { name: DEFAULT_EMBED_BLOCK } = metadata; -const { kebabCase } = unlock( componentsPrivateApis ); /** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index bb1821a17c5024..2d7ab883e9b35c 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -31,6 +31,7 @@ { "path": "../interactivity-router" }, { "path": "../latex-to-mathml" }, { "path": "../notices" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../keyboard-shortcuts" }, { "path": "../primitives" }, diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index dc76b94f00dca0..bdd73f22921ae0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -35,6 +35,8 @@ ### Internal - Move `withIgnoreIMEEvents` to `@wordpress/keycodes`, where it is now a public API, and remove it from this package's private APIs ([#81343](https://github.com/WordPress/gutenberg/pull/81343)). +- Extract the private `kebabCase` utility to the new `@wordpress/kebab-case` package and remove it from the private APIs; all consumers now use the package directly ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). +- Remove `normalizeTextString` from the private APIs; the utility remains internal to this package ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). - Remove obsolete dependency grouping comments as part of the repository-wide separator-free import migration. ([#81248](https://github.com/WordPress/gutenberg/pull/81248)) - `Button`: Expand the Storybook e2e `VariantStates` matrix with compact, small, and with-icon rows ([#80793](https://github.com/WordPress/gutenberg/pull/80793)). - Update `react-day-picker` to 9.14.0 ([#80792](https://github.com/WordPress/gutenberg/pull/80792)). diff --git a/packages/components/package.json b/packages/components/package.json index 1472cad58816cb..3f11e59e10687c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -75,6 +75,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index f8254566b3d858..f68877e9b68906 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -9,6 +9,7 @@ import { import { __, sprintf } from '@wordpress/i18n'; import { lineSolid, moreVertical, plus } from '@wordpress/icons'; import { useDebounce } from '@wordpress/compose'; +import { kebabCase } from '@wordpress/kebab-case'; import Button from '../button'; import { ColorPicker } from '../color-picker'; import { FlexBlock, FlexItem } from '../flex'; @@ -33,7 +34,6 @@ import { import { NavigableMenu } from '../navigable-container'; import { DEFAULT_GRADIENT } from '../custom-gradient-picker/constants'; import CustomGradientPicker from '../custom-gradient-picker'; -import { kebabCase } from '../utils/strings'; import type { Color, ColorPickerPopoverProps, diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index e498a49530867b..a0777cfeb5335c 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -3,7 +3,6 @@ import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } import { Menu } from './menu'; import { ComponentsContext } from './context/context-system-provider'; import { Tabs } from './tabs'; -import { kebabCase, normalizeTextString } from './utils/strings'; import { lock } from './lock-unlock'; import Badge from './badge'; import { DateCalendar, DateRangeCalendar, TZDate } from './calendar'; @@ -30,9 +29,7 @@ lock( privateApis, { ComponentsContext, Tabs, Menu, - kebabCase, Badge, - normalizeTextString, DateCalendar, DateRangeCalendar, TZDate, diff --git a/packages/components/src/utils/strings.ts b/packages/components/src/utils/strings.ts index f38cc2cc03acb4..4c956191606fa4 100644 --- a/packages/components/src/utils/strings.ts +++ b/packages/components/src/utils/strings.ts @@ -1,5 +1,4 @@ import removeAccents from 'remove-accents'; -import { paramCase } from 'change-case'; /** * All unicode characters that we consider "dash-like": @@ -22,33 +21,6 @@ export const normalizeTextString = ( value: string ): string => { .replace( ALL_UNICODE_DASH_CHARACTERS, '-' ); }; -/** - * 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 str String to convert. - * @return Kebab-cased string - */ -export function kebabCase( str: unknown ) { - let 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 - ], - } ); -} - /** * Escapes the RegExp special characters. * diff --git a/packages/components/src/utils/test/strings.js b/packages/components/src/utils/test/strings.js index 52734d37b610d9..c3d6b346d8613e 100644 --- a/packages/components/src/utils/test/strings.js +++ b/packages/components/src/utils/test/strings.js @@ -1,99 +1,4 @@ -import { kebabCase, normalizeTextString } from '../strings'; - -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 ); - } ); -} ); +import { normalizeTextString } from '../strings'; describe( 'normalizeTextString', () => { it( 'should normalize hyphen-like characters to hyphens', () => { diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index eb0d9fe675936c..cd4938011fb98b 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -23,6 +23,7 @@ { "path": "../i18n" }, { "path": "../icons" }, { "path": "../is-shallow-equal" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../primitives" }, { "path": "../private-apis" }, diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 2fcaafeb84dee3..61cd2670576f5c 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- DataViews: Replace the inlined `kebabCase` utility with the new `@wordpress/kebab-case` package. [#81294](https://github.com/WordPress/gutenberg/pull/81294) + ### New Features - DataViews: Add a `time` field type and matching `time` control for times of day with no date attached. Values are wall-clock `HH:mm`/`HH:mm:ss` strings that render identically regardless of the visitor's timezone, and `format.time` controls both the rendered format and whether the control offers a seconds field. [#80830](https://github.com/WordPress/gutenberg/pull/80830) diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 03744086bf5a4b..53d8d90a67e03f 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -62,13 +62,13 @@ "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/ui": "file:../ui", "@wordpress/warning": "file:../warning", - "change-case": "^4.1.2", "clsx": "^2.1.1", "colord": "^2.9.3", "date-fns": "^4.4.0", diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index 5ffd1ebbe9cfee..13e1536895e759 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -10,8 +10,8 @@ import { moreVertical } from '@wordpress/icons'; import { useRegistry } from '@wordpress/data'; import { useViewportMatch } from '@wordpress/compose'; import { Stack } from '@wordpress/ui'; +import { kebabCase } from '@wordpress/kebab-case'; import { unlock } from '../../lock-unlock'; -import { kebabCase } from '../../utils/kebab-case'; import type { Action, ActionModal as ActionModalType } from '../../types'; const { Menu } = unlock( componentsPrivateApis ); diff --git a/packages/dataviews/tsconfig.json b/packages/dataviews/tsconfig.json index abf2f1b1c1585e..eb2213b34ae78a 100644 --- a/packages/dataviews/tsconfig.json +++ b/packages/dataviews/tsconfig.json @@ -21,6 +21,7 @@ { "path": "../element" }, { "path": "../i18n" }, { "path": "../icons" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../primitives" }, { "path": "../private-apis" }, diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index 650769b48ebfce..36af841b766d80 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Enhancements + +- Add `@wordpress/kebab-case` as a bundled package, so consumer builds bundle it instead of externalizing it to a nonexistent `wp-kebab-case` script ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). ## 6.52.0 (2026-07-29) diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index 71a1002af83a9e..3bdf02b2dd3523 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -7,6 +7,7 @@ const BUNDLED_PACKAGES = [ '@wordpress/grid', '@wordpress/icons', '@wordpress/interface', + '@wordpress/kebab-case', '@wordpress/style-runtime', '@wordpress/ui', '@wordpress/undo-manager', diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index a18d6ada046cfe..4771711cef7051 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Internal + +- Use the new `@wordpress/kebab-case` package instead of unlocking the `kebabCase` utility from the `@wordpress/components` private APIs ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). +- Vendor a local copy of the `normalizeTextString` utility instead of unlocking it from the `@wordpress/components` private APIs ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). + ### Enhancements - Add a read-only code diff to the revisions screen ([#80314](https://github.com/WordPress/gutenberg/pull/80314)). diff --git a/packages/editor/package.json b/packages/editor/package.json index b8b63df9980d3b..242ee8598a42dc 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -85,6 +85,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/interface": "file:../interface", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-editor": "file:../media-editor", diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index c6613f71a41c95..5fb8c808ca4573 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -8,10 +8,11 @@ import { } from '@wordpress/components'; import { moreVertical } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; +import { kebabCase } from '@wordpress/kebab-case'; import { unlock } from '../../lock-unlock'; import { usePostActions } from './actions'; -const { Menu, kebabCase } = unlock( componentsPrivateApis ); +const { Menu } = unlock( componentsPrivateApis ); export default function PostActions( { postType, postId, onActionPerformed } ) { const [ activeModalAction, setActiveModalAction ] = useState( null ); diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index 322dd58a65dbb0..11d3a232604a13 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -11,7 +11,6 @@ import { FlexItem, SearchControl, Spinner, - privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { useDebounce } from '@wordpress/compose'; @@ -22,10 +21,10 @@ import { import { speak } from '@wordpress/a11y'; import { decodeEntities } from '@wordpress/html-entities'; import { buildTermsTree } from '../../utils/terms'; +import { normalizeTextString } from '../../utils/normalize-text-string'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { normalizeTextString } = unlock( componentsPrivateApis ); const { RECEIVE_INTERMEDIATE_RESULTS } = unlock( coreDataPrivateApis ); /** diff --git a/packages/editor/src/utils/normalize-text-string.ts b/packages/editor/src/utils/normalize-text-string.ts new file mode 100644 index 00000000000000..8de4c28786f288 --- /dev/null +++ b/packages/editor/src/utils/normalize-text-string.ts @@ -0,0 +1,22 @@ +import removeAccents from 'remove-accents'; + +/** + * All unicode characters that we consider "dash-like": + * - `\u007e`: ~ (tilde) + * - `\u00ad`: ­ (soft hyphen) + * - `\u2053`: ⁓ (swung dash) + * - `\u207b`: ⁻ (superscript minus) + * - `\u208b`: ₋ (subscript minus) + * - `\u2212`: − (minus sign) + * - `\\p{Pd}`: any other Unicode dash character + */ +const ALL_UNICODE_DASH_CHARACTERS = new RegExp( + /[\u007e\u00ad\u2053\u207b\u208b\u2212\p{Pd}]/gu +); + +export const normalizeTextString = ( value: string ): string => { + return removeAccents( value ) + .normalize( 'NFKC' ) + .toLocaleLowerCase() + .replace( ALL_UNICODE_DASH_CHARACTERS, '-' ); +}; diff --git a/packages/editor/src/utils/test/normalize-text-string.js b/packages/editor/src/utils/test/normalize-text-string.js new file mode 100644 index 00000000000000..857cafabbed0cb --- /dev/null +++ b/packages/editor/src/utils/test/normalize-text-string.js @@ -0,0 +1,85 @@ +import { normalizeTextString } from '../normalize-text-string'; + +describe( 'normalizeTextString', () => { + it( 'should normalize hyphen-like characters to hyphens', () => { + expect( normalizeTextString( 'foo~bar' ) ).toBe( 'foo-bar' ); + expect( normalizeTextString( 'foo־bar' ) ).toBe( 'foo-bar' ); + expect( normalizeTextString( 'foo‐bar' ) ).toBe( 'foo-bar' ); + expect( normalizeTextString( 'foo⸻bar' ) ).toBe( 'foo-bar' ); + expect( normalizeTextString( 'foo゠bar' ) ).toBe( 'foo-bar' ); + expect( normalizeTextString( 'foo-bar' ) ).toBe( 'foo-bar' ); + + const dashCharacters = [ + // - (hyphen-minus) + '\u002d', + // ~ (tilde) + '\u007e', + // ­ (soft hyphen) + '\u00ad', + // ֊ (armenian hyphen) + '\u058a', + // ־ (hebrew punctuation maqaf) + '\u05be', + // ᐀ (canadian syllabics hyphen) + '\u1400', + // ᠆ (mongolian todo soft hyphen) + '\u1806', + // ‐ (hyphen) + '\u2010', + // non-breaking hyphen) + '\u2011', + // ‒ (figure dash) + '\u2012', + // – (en dash) + '\u2013', + // — (em dash) + '\u2014', + // ― (horizontal bar) + '\u2015', + // ⁓ (swung dash) + '\u2053', + // ⁻ (superscript minus) + '\u207b', + // ₋ (subscript minus) + '\u208b', + // − (minus sign) + '\u2212', + // ⸗ (double oblique hyphen) + '\u2e17', + // ⸺ (two-em dash) + '\u2e3a', + // ⸻ (three-em dash) + '\u2e3b', + // 〜 (wave dash) + '\u301c', + // 〰 (wavy dash) + '\u3030', + // ゠ (katakana-hiragana double hyphen) + '\u30a0', + // ︱ (presentation form for vertical em dash) + '\ufe31', + // ︲ (presentation form for vertical en dash) + '\ufe32', + // ﹘ (small em dash) + '\ufe58', + // ﹣ (small hyphen-minus) + '\ufe63', + // - (fullwidth hyphen-minus) + '\uff0d', + ]; + expect( normalizeTextString( dashCharacters.join( '' ) ) ).toBe( + '-'.repeat( dashCharacters.length ) + ); + } ); + + it( 'should normalize unicode to standard characters', () => { + expect( normalizeTextString( '①' ) ).toBe( '1' ); + expect( normalizeTextString( 'Ⅸ' ) ).toBe( 'ix' ); + expect( normalizeTextString( 'MC²' ) ).toBe( 'mc2' ); + expect( normalizeTextString( 'PlayStation 2' ) ).toBe( + 'playstation 2' + ); + expect( normalizeTextString( 'ABC' ) ).toBe( 'abc' ); + expect( normalizeTextString( 'Amélie' ) ).toBe( 'amelie' ); + } ); +} ); diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json index b495a3ebe97e47..48a4493a5b8248 100644 --- a/packages/editor/tsconfig.json +++ b/packages/editor/tsconfig.json @@ -29,6 +29,7 @@ { "path": "../html-entities" }, { "path": "../i18n" }, { "path": "../icons" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../keyboard-shortcuts" }, { "path": "../media-editor" }, diff --git a/packages/global-styles-ui/CHANGELOG.md b/packages/global-styles-ui/CHANGELOG.md index 8f85b917d73ba8..cf1758b124fa9d 100644 --- a/packages/global-styles-ui/CHANGELOG.md +++ b/packages/global-styles-ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internal + +- Font Library: Use the new `@wordpress/kebab-case` package instead of unlocking the `kebabCase` utility from the `@wordpress/components` private APIs ([#81294](https://github.com/WordPress/gutenberg/pull/81294)). + ### Enhancements - Add a `showBlockStateControls` prop to `GlobalStylesUI`, defaulting to `true`, which hides the state controls for blocks when set to `false` ([#80956](https://github.com/WordPress/gutenberg/pull/80956)). diff --git a/packages/global-styles-ui/package.json b/packages/global-styles-ui/package.json index f747679f9d3847..c8401d1e13d166 100644 --- a/packages/global-styles-ui/package.json +++ b/packages/global-styles-ui/package.json @@ -57,6 +57,7 @@ "@wordpress/global-styles-engine": "file:../global-styles-engine", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/kebab-case": "file:../kebab-case", "@wordpress/keycodes": "file:../keycodes", "@wordpress/private-apis": "file:../private-apis", "@wordpress/ui": "file:../ui", diff --git a/packages/global-styles-ui/src/font-library/utils/index.ts b/packages/global-styles-ui/src/font-library/utils/index.ts index 05feb10dc3b05d..b3129216a1f203 100644 --- a/packages/global-styles-ui/src/font-library/utils/index.ts +++ b/packages/global-styles-ui/src/font-library/utils/index.ts @@ -1,17 +1,15 @@ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; import type { FontFamily, FontFace } from '@wordpress/core-data'; import type { DataRegistry } from '@wordpress/data'; +import { kebabCase } from '@wordpress/kebab-case'; import { FONT_WEIGHTS, FONT_STYLES } from './constants'; import { fetchInstallFontFace } from '../api'; import { formatFontFaceName } from './preview-styles'; import type { FontFamilyToUpload, FontUploadResult } from '../types'; -import { unlock } from '../../lock-unlock'; /** * Browser dependencies */ const { File } = window; -const { kebabCase } = unlock( componentsPrivateApis ); export function setUIValuesNeeded( font: FontFamily, diff --git a/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts b/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts index 5f00e075b6bde0..87d29648c4b60c 100644 --- a/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts +++ b/packages/global-styles-ui/src/font-library/utils/make-families-from-faces.ts @@ -1,8 +1,5 @@ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; import type { FontFamily, FontFace } from '@wordpress/core-data'; -import { unlock } from '../../lock-unlock'; - -const { kebabCase } = unlock( componentsPrivateApis ); +import { kebabCase } from '@wordpress/kebab-case'; export default function makeFamiliesFromFaces( fontFaces: FontFace[] diff --git a/packages/global-styles-ui/tsconfig.json b/packages/global-styles-ui/tsconfig.json index 71ce10c14a3711..552ba2d56e3b45 100644 --- a/packages/global-styles-ui/tsconfig.json +++ b/packages/global-styles-ui/tsconfig.json @@ -19,6 +19,7 @@ { "path": "../global-styles-engine" }, { "path": "../i18n" }, { "path": "../icons" }, + { "path": "../kebab-case" }, { "path": "../keycodes" }, { "path": "../private-apis" }, { "path": "../ui" } diff --git a/packages/kebab-case/CHANGELOG.md b/packages/kebab-case/CHANGELOG.md new file mode 100644 index 00000000000000..858a49f8e24ed5 --- /dev/null +++ b/packages/kebab-case/CHANGELOG.md @@ -0,0 +1,5 @@ + + +## Unreleased + +- Initial release with the `kebabCase` utility. [#81294](https://github.com/WordPress/gutenberg/pull/81294) diff --git a/packages/kebab-case/README.md b/packages/kebab-case/README.md new file mode 100644 index 00000000000000..9170e83f6eb7e7 --- /dev/null +++ b/packages/kebab-case/README.md @@ -0,0 +1,62 @@ +# Kebab case + +Convert strings to kebab-case, matching WordPress Core's [`_wp_to_kebab_case()`](https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/). + +This package exists because no off-the-shelf library matches the semantics of WordPress Core's `_wp_to_kebab_case()` function, which WordPress needs to preserve for backwards compatibility. + +## Installation + +Install the module + +```bash +npm install @wordpress/kebab-case --save +``` + +## Usage + +### `kebabCase` + +Converts any string to kebab-case. + +The conversion is designed to match WordPress Core's [`_wp_to_kebab_case()`](https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/) function (and, transitively, Lodash's [`_.kebabCase()`](https://lodash.com/docs/4.17.15#kebabCase)): the package's test suite covers every test case of the Core function. These semantics differ from other kebab-case implementations in how numbers are handled (`'white23'` becomes `'white-23'`, `'font2xl'` becomes `'font-2-xl'`, yet `'white4th'` stays `'white-4th'`). + +```js +import { kebabCase } from '@wordpress/kebab-case'; + +kebabCase( 'whiteToWhite' ); // 'white-to-white' +kebabCase( 'white2white' ); // 'white-2-white' +kebabCase( 'font2xl' ); // 'font-2-xl' +kebabCase( '#ffffff' ); // 'ffffff' +kebabCase( "i've done" ); // 'ive-done' +``` + +## API + + + +### kebabCase + +Converts any string to kebab case. Backwards compatible with Lodash's `_.kebabCase()`. Backwards compatible with `_wp_to_kebab_case()`. + +_Related_ + +- +- + +_Parameters_ + +- _str_ `unknown`: String to convert. + +_Returns_ + +- Kebab-cased string + + + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

Code is Poetry.

diff --git a/packages/kebab-case/package.json b/packages/kebab-case/package.json new file mode 100644 index 00000000000000..e848570ba2a3c6 --- /dev/null +++ b/packages/kebab-case/package.json @@ -0,0 +1,51 @@ +{ + "name": "@wordpress/kebab-case", + "version": "1.0.0-prerelease", + "description": "Convert strings to kebab-case, matching WordPress Core's `_wp_to_kebab_case()`.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "kebab-case" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/kebab-case/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/kebab-case" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + }, + "files": [ + "src", + "build", + "build-module", + "build-types", + "*.md" + ], + "main": "build/index.cjs", + "module": "build-module/index.mjs", + "exports": { + ".": { + "types": "./build-types/index.d.ts", + "import": "./build-module/index.mjs", + "default": "./build/index.cjs" + }, + "./package.json": "./package.json" + }, + "wpScript": false, + "types": "build-types", + "sideEffects": false, + "dependencies": { + "change-case": "^4.1.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/dataviews/src/utils/kebab-case.ts b/packages/kebab-case/src/index.ts similarity index 81% rename from packages/dataviews/src/utils/kebab-case.ts rename to packages/kebab-case/src/index.ts index dbecba48ce47b9..84a7a2a60b80f9 100644 --- a/packages/dataviews/src/utils/kebab-case.ts +++ b/packages/kebab-case/src/index.ts @@ -5,10 +5,6 @@ import { paramCase } from 'change-case'; * Backwards compatible with Lodash's `_.kebabCase()`. * Backwards compatible with `_wp_to_kebab_case()`. * - * Verbatim copy of the private `kebabCase` utility of `@wordpress/components`, - * inlined so DataViews does not depend on private cross-package APIs. - * Keep in sync with `packages/components/src/utils/strings.ts`. - * * @see https://lodash.com/docs/4.17.15#kebabCase * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ * diff --git a/packages/dataviews/src/utils/test/kebab-case.js b/packages/kebab-case/src/test/index.js similarity index 93% rename from packages/dataviews/src/utils/test/kebab-case.js rename to packages/kebab-case/src/test/index.js index 05fbc6fa954745..2bcdb9ea812134 100644 --- a/packages/dataviews/src/utils/test/kebab-case.js +++ b/packages/kebab-case/src/test/index.js @@ -1,8 +1,5 @@ -import { kebabCase } from '../kebab-case'; +import { kebabCase } from '..'; -// Mirrors the test suite of the private `kebabCase` utility of -// `@wordpress/components` (packages/components/src/utils/test/strings.js), -// which this function is a verbatim copy of. describe( 'kebabCase', () => { it( 'separates lowercase letters, followed by uppercase letters', () => { expect( kebabCase( 'fooBar' ) ).toEqual( 'foo-bar' ); diff --git a/packages/kebab-case/tsconfig.json b/packages/kebab-case/tsconfig.json new file mode 100644 index 00000000000000..7ff060ab6ce105 --- /dev/null +++ b/packages/kebab-case/tsconfig.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json" +} diff --git a/tsconfig.json b/tsconfig.json index b0d47c9fa3b544..bc1c6e5c96243c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,6 +49,7 @@ { "path": "packages/interactivity/tsconfig.test.json" }, { "path": "packages/is-shallow-equal" }, { "path": "packages/jest-console" }, + { "path": "packages/kebab-case" }, { "path": "packages/keycodes" }, { "path": "packages/keyboard-shortcuts" }, { "path": "packages/latex-to-mathml" },