/
githubmirror
/
gutenberg
Обзор
Документация
Войти
/
githubmirror
/
gutenberg
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
packages/global-styles-engine/src/utils/common.ts
739 строк
20 KB
Marco Ciampini
ESLint: Replace strict config with bulk suppressions (#81248)
07 авг 2026, 14:34
Не верифицирован
07 авг 2026, 14:34
7f43eaf
Код
Авторство
О чём код?
import { getCSSValueFromRawStyle } from '@wordpress/style-engine'; import type { GlobalStylesSettings, ThemeFileLink, TypographyPreset, UnresolvedValue, GlobalStylesConfig, } from '../types'; import { getTypographyFontSizeValue } from './typography'; import { getValueFromObjectPath } from './object'; export const ROOT_BLOCK_SELECTOR = 'body'; export const ROOT_CSS_PROPERTIES_SELECTOR = ':root'; export function splitSelectorList( selector: string ) { if ( ! selector.includes( ',' ) ) { return [ selector ]; } const selectors: string[] = []; let currentSelector = ''; let parenthesesDepth = 0; for ( const char of selector ) { if ( char === '(' ) { parenthesesDepth++; } else if ( char === ')' && parenthesesDepth > 0 ) { parenthesesDepth--; } else if ( char === ',' && parenthesesDepth === 0 ) { selectors.push( currentSelector ); currentSelector = ''; continue; } currentSelector += char; } selectors.push( currentSelector ); return selectors; } export const PRESET_METADATA = [ { path: [ 'color', 'palette' ], valueKey: 'color', cssVarInfix: 'color', classes: [ { classSuffix: 'color', propertyName: 'color' }, { classSuffix: 'background-color', propertyName: 'background-color', }, { classSuffix: 'border-color', propertyName: 'border-color', }, ], }, { path: [ 'color', 'gradients' ], valueKey: 'gradient', cssVarInfix: 'gradient', classes: [ { classSuffix: 'gradient-background', propertyName: 'background', }, ], }, { path: [ 'color', 'duotone' ], valueKey: 'colors', cssVarInfix: 'duotone', valueFunc: ( { slug }: { slug: string } ) => `url( '#wp-duotone-${ slug }' )`, classes: [], }, { path: [ 'shadow', 'presets' ], valueKey: 'shadow', cssVarInfix: 'shadow', classes: [], }, { path: [ 'typography', 'fontSizes' ], valueFunc: ( preset: TypographyPreset, settings: GlobalStylesSettings ) => getTypographyFontSizeValue( preset, settings ), valueKey: 'size', cssVarInfix: 'font-size', classes: [ { classSuffix: 'font-size', propertyName: 'font-size' } ], }, { path: [ 'typography', 'fontFamilies' ], valueKey: 'fontFamily', cssVarInfix: 'font-family', classes: [ { classSuffix: 'font-family', propertyName: 'font-family' }, ], }, { path: [ 'spacing', 'spacingSizes' ], valueKey: 'size', cssVarInfix: 'spacing', valueFunc: ( { size }: { size: string } ) => size, classes: [], }, { path: [ 'border', 'radiusSizes' ], valueKey: 'size', cssVarInfix: 'border-radius', classes: [], }, { path: [ 'dimensions', 'dimensionSizes' ], valueKey: 'size', cssVarInfix: 'dimension', classes: [], }, ]; export const STYLE_PATH_TO_CSS_VAR_INFIX: Record< string, string > = { 'color.background': 'color', 'color.text': 'color', 'filter.duotone': 'duotone', 'elements.link.color.text': 'color', 'elements.link.:hover.color.text': 'color', 'elements.link.typography.fontFamily': 'font-family', 'elements.link.typography.fontSize': 'font-size', 'elements.button.color.text': 'color', 'elements.button.color.background': 'color', 'elements.caption.color.text': 'color', 'elements.button.typography.fontFamily': 'font-family', 'elements.button.typography.fontSize': 'font-size', 'elements.heading.color': 'color', 'elements.heading.color.background': 'color', 'elements.heading.typography.fontFamily': 'font-family', 'elements.heading.gradient': 'gradient', 'elements.heading.color.gradient': 'gradient', 'elements.h1.color': 'color', 'elements.h1.color.background': 'color', 'elements.h1.typography.fontFamily': 'font-family', 'elements.h1.color.gradient': 'gradient', 'elements.h2.color': 'color', 'elements.h2.color.background': 'color', 'elements.h2.typography.fontFamily': 'font-family', 'elements.h2.color.gradient': 'gradient', 'elements.h3.color': 'color', 'elements.h3.color.background': 'color', 'elements.h3.typography.fontFamily': 'font-family', 'elements.h3.color.gradient': 'gradient', 'elements.h4.color': 'color', 'elements.h4.color.background': 'color', 'elements.h4.typography.fontFamily': 'font-family', 'elements.h4.color.gradient': 'gradient', 'elements.h5.color': 'color', 'elements.h5.color.background': 'color', 'elements.h5.typography.fontFamily': 'font-family', 'elements.h5.color.gradient': 'gradient', 'elements.h6.color': 'color', 'elements.h6.color.background': 'color', 'elements.h6.typography.fontFamily': 'font-family', 'elements.h6.color.gradient': 'gradient', 'color.gradient': 'gradient', shadow: 'shadow', 'typography.fontSize': 'font-size', 'typography.fontFamily': 'font-family', }; /** * Function that scopes a selector with another one. This works a bit like * SCSS nesting except the `&` operator isn't supported. * * @example * ```js * const scope = '.a, .b .c'; * const selector = '> .x, .y'; * const merged = scopeSelector( scope, selector ); * // merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' * ``` * * @param scope Selector to scope to. * @param selector Original selector. * * @return Scoped selector. */ export function scopeSelector( scope: string | undefined, selector: string ) { if ( ! scope || ! selector ) { return selector; } const scopes = splitSelectorList( scope ); const selectors = splitSelectorList( selector ); const selectorsScoped: string[] = []; scopes.forEach( ( outer ) => { selectors.forEach( ( inner ) => { selectorsScoped.push( `${ outer.trim() } ${ inner.trim() }` ); } ); } ); return selectorsScoped.join( ', ' ); } /** * Scopes a collection of selectors for features and subfeatures. * * @example * ```js * const scope = '.custom-scope'; * const selectors = { * color: '.wp-my-block p', * typography: { fontSize: '.wp-my-block caption' }, * }; * const result = scopeFeatureSelector( scope, selectors ); * // result is { * // color: '.custom-scope .wp-my-block p', * // typography: { fonSize: '.custom-scope .wp-my-block caption' }, * // } * ``` * * @param scope Selector to scope collection of selectors with. * @param selectors Collection of feature selectors e.g. * * @return Scoped collection of feature selectors. */ export function scopeFeatureSelectors( scope: string | undefined, selectors: string | Record< string, string | Record< string, string > > ) { if ( ! scope || ! selectors ) { return; } const featureSelectors: Record< string, string | Record< string, string > > = {}; Object.entries( selectors ).forEach( ( [ feature, selector ] ) => { if ( typeof selector === 'string' ) { featureSelectors[ feature ] = scopeSelector( scope, selector ); } if ( typeof selector === 'object' ) { featureSelectors[ feature ] = {}; Object.entries( selector ).forEach( ( [ subfeature, subfeatureSelector ] ) => { // @ts-expect-error A string key cannot index `string | Record<string, string>`. featureSelectors[ feature ][ subfeature ] = scopeSelector( scope, subfeatureSelector as string ); } ); } } ); return featureSelectors; } /** * Appends a sub-selector to an existing one. * * Given the compounded `selector` "h1, h2, h3" * and the `toAppend` selector ".some-class" the result will be * "h1.some-class, h2.some-class, h3.some-class". * * @param selector Original selector. * @param toAppend Selector to append. * * @return The new selector. */ export function appendToSelector( selector: string, toAppend: string ) { if ( ! selector.includes( ',' ) ) { return selector + toAppend; } const selectors = splitSelectorList( selector ); const newSelectors = selectors.map( ( sel ) => sel + toAppend ); return newSelectors.join( ',' ); } /** * Generates the selector for a block style variation by creating the * appropriate CSS class and adding it to the ancestor portion of the block's * selector. * * For example, take the Button block which has a compound selector: * `.wp-block-button .wp-block-button__link`. With a variation named 'custom', * the class `.is-style-custom` should be added to the `.wp-block-button` * ancestor only. * * This function will take into account comma separated and complex selectors. * * @param variation Name for the variation. * @param blockSelector CSS selector for the block. * * @return CSS selector for the block style variation. */ export function getBlockStyleVariationSelector( variation: string, blockSelector: string ) { const variationClass = `.is-style-${ variation }`; if ( ! blockSelector ) { return variationClass; } /* * Append the variation class to each selector's ancestor: the first run * of characters before any combinator (whitespace) or pseudo-class (`:`). * `String.prototype.replace` only replaces the first match. * * Examples ("custom" variation): * - `.wp-block` => `.wp-block.is-style-custom` * - `.wp-block .inner` => `.wp-block.is-style-custom .inner` * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)` * - `:where(.outer .inner)` => `:where(.outer.is-style-custom .inner)` */ const ancestorRegex = /[^\s:]+/; const addVariationClass = ( match: string ) => match + variationClass; const result = splitSelectorList( blockSelector ).map( ( part ) => part.replace( ancestorRegex, addVariationClass ) ); return result.join( ',' ); } /** * Generates the selector for a block style variation feature selector. * * Feature selectors can target a different element than the block root * selector. Apply the variation class directly to the selector that receives * the declarations instead of deriving it from the block root selector. * * @param variation Name for the variation. * @param featureSelector CSS selector for the feature. * * @return CSS selector for the block style variation feature. */ export function getBlockStyleVariationFeatureSelector( variation: string, featureSelector: string ) { const variationClass = `.is-style-${ variation }`; const selectorParts = splitSelectorList( featureSelector ).map( ( selector ) => { const trimmedSelector = selector.trim(); const prefix = `${ variationClass } `; if ( trimmedSelector.startsWith( prefix ) ) { return trimmedSelector.slice( prefix.length ); } return trimmedSelector; } ); return getBlockStyleVariationSelector( variation, selectorParts.join( ',' ) ); } /** * Resolves ref values in theme JSON. * * @param ruleValue A block style value that may contain a reference to a theme.json value. * @param tree A theme.json object. * @return The resolved value or incoming ruleValue. */ export function getResolvedRefValue( ruleValue: UnresolvedValue, tree?: GlobalStylesConfig ): UnresolvedValue { if ( ! ruleValue || ! tree ) { return ruleValue; } /* * Where the rule value is an object with a 'ref' property pointing * to a path, this converts that path into the value at that path. * For example: { "ref": "style.color.background" } => "#fff". */ if ( typeof ruleValue === 'object' && 'ref' in ruleValue && ruleValue?.ref ) { const resolvedRuleValue = getCSSValueFromRawStyle( getValueFromObjectPath( tree, ruleValue.ref ) ) as UnresolvedValue; /* * Presence of another ref indicates a reference to another dynamic value. * Pointing to another dynamic value is not supported. */ if ( typeof resolvedRuleValue === 'object' && resolvedRuleValue !== null && 'ref' in resolvedRuleValue && resolvedRuleValue?.ref ) { return undefined; } if ( resolvedRuleValue === undefined ) { return ruleValue; } return resolvedRuleValue; } return ruleValue; } /** * Looks up a theme file URI based on a relative path. * * @param file A relative path. * @param themeFileURIs A collection of absolute theme file URIs and their corresponding file paths. * @return A resolved theme file URI, if one is found in the themeFileURIs collection. */ export function getResolvedThemeFilePath( file: string, themeFileURIs?: ThemeFileLink[] ) { if ( ! file || ! themeFileURIs || ! Array.isArray( themeFileURIs ) ) { return file; } const uri = themeFileURIs.find( ( themeFileUri ) => themeFileUri?.name === file ); if ( ! uri?.href ) { return file; } return uri?.href; } /** * Resolves ref and relative path values in theme JSON. * * @param ruleValue A block style value that may contain a reference to a theme.json value. * @param tree A theme.json object. * @return The resolved value or incoming ruleValue. */ export function getResolvedValue( ruleValue: UnresolvedValue, tree: GlobalStylesConfig | undefined ) { if ( ! ruleValue || ! tree ) { return ruleValue; } // Resolve ref values. const resolvedValue = getResolvedRefValue( ruleValue, tree ); // Resolve relative paths. if ( typeof resolvedValue === 'object' && resolvedValue !== null && 'url' in resolvedValue && resolvedValue?.url ) { resolvedValue.url = getResolvedThemeFilePath( resolvedValue.url, tree?._links?.[ 'wp:theme-file' ] ); } return resolvedValue; } function findInPresetsBy( settings: GlobalStylesSettings, blockName?: string, presetPath: string[] = [], presetProperty: string = 'slug', presetValueValue?: string ) { // Block presets take priority above root level presets. const orderedPresetsByOrigin = [ blockName ? getValueFromObjectPath( settings, [ 'blocks', blockName, ...presetPath, ] ) : undefined, getValueFromObjectPath( settings, presetPath ), ].filter( Boolean ); for ( const presetByOrigin of orderedPresetsByOrigin ) { if ( presetByOrigin ) { // Preset origins ordered by priority. const origins = [ 'custom', 'theme', 'default' ]; for ( const origin of origins ) { // @ts-expect-error `presetByOrigin` is typed as `Object`, which has no index signature. const presets = presetByOrigin[ origin ]; if ( presets ) { const presetObject = presets.find( ( preset: any ) => preset[ presetProperty ] === presetValueValue ); if ( presetObject ) { if ( presetProperty === 'slug' ) { return presetObject; } // If there is a highest priority preset with the same slug but different value the preset we found was overwritten and should be ignored. const highestPresetObjectWithSameSlug = findInPresetsBy( settings, blockName, presetPath, 'slug', presetObject.slug ); if ( highestPresetObjectWithSameSlug[ presetProperty ] === presetObject[ presetProperty ] ) { return presetObject; } return undefined; } } } } } } function getValueFromPresetVariable( features: GlobalStylesConfig, blockName?: string, variable?: string, [ presetType, slug ]: string[] = [] ) { const metadata = PRESET_METADATA.find( ( data ) => data.cssVarInfix === presetType ); if ( ! metadata || ! features.settings ) { return variable; } const presetObject = findInPresetsBy( features.settings, blockName, metadata.path, 'slug', slug ); if ( presetObject ) { const { valueKey } = metadata; const result = presetObject[ valueKey ]; return getValueFromVariable( features, blockName, result ); } return variable; } function getValueFromCustomVariable( features: GlobalStylesConfig, blockName?: string, variable?: string, path: string[] = [] ): string | undefined { const result = ( blockName ? getValueFromObjectPath( features?.settings ?? {}, [ 'blocks', blockName, 'custom', ...path, ] ) : undefined ) ?? getValueFromObjectPath( features?.settings ?? {}, [ 'custom', ...path, ] ); if ( ! result ) { return variable; } // A variable may reference another variable so we need recursion until we find the value. return getValueFromVariable( features, blockName, result as string ); } /** * Attempts to fetch the value of a theme.json CSS variable. * * This function resolves CSS variable references in two formats: * - User format: `var:preset|color|red` or `var:custom|spacing|small` * - Theme format: `var(--wp--preset--color--red)` or `var(--wp--custom--spacing--small)` * * It also handles ref-style variables in the format `{ ref: "path.to.value" }`. * * @param features GlobalStylesContext config (user, base, or merged). Represents the theme.json tree. * @param blockName The name of a block as represented in the styles property. E.g., 'root' for root-level, and 'core/block-name' for blocks. * @param variable An incoming style value. A CSS var value is expected, but it could be any value. * @return The value of the CSS var, if found. If not found, returns the original variable argument. */ export function getValueFromVariable( features: GlobalStylesConfig, blockName?: string, variable?: string | UnresolvedValue ): any { if ( ! variable || typeof variable !== 'string' ) { if ( typeof variable === 'object' && variable !== null && 'ref' in variable && typeof variable.ref === 'string' ) { const resolvedVariable = getValueFromObjectPath( features, variable.ref ); // Presence of another ref indicates a reference to another dynamic value. // Pointing to another dynamic value is not supported. if ( ! resolvedVariable || ( typeof resolvedVariable === 'object' && 'ref' in resolvedVariable ) ) { return resolvedVariable; } variable = resolvedVariable as string; } else { return variable; } } const USER_VALUE_PREFIX = 'var:'; const THEME_VALUE_PREFIX = 'var(--wp--'; const THEME_VALUE_SUFFIX = ')'; let parsedVar; if ( variable.startsWith( USER_VALUE_PREFIX ) ) { parsedVar = variable.slice( USER_VALUE_PREFIX.length ).split( '|' ); } else if ( variable.startsWith( THEME_VALUE_PREFIX ) && variable.endsWith( THEME_VALUE_SUFFIX ) ) { parsedVar = variable .slice( THEME_VALUE_PREFIX.length, -THEME_VALUE_SUFFIX.length ) .split( '--' ); } else { // We don't know how to parse the value: either is raw of uses complex CSS such as `calc(1px * var(--wp--variable) )` return variable; } const [ type, ...path ] = parsedVar; if ( type === 'preset' ) { return getValueFromPresetVariable( features, blockName, variable, path ); } if ( type === 'custom' ) { return getValueFromCustomVariable( features, blockName, variable, path ); } return variable; } /** * Encodes a value to a preset variable format if it matches a preset. * This is the inverse operation of getValueFromVariable(). * * @example * ```js * const presetVar = getPresetVariableFromValue( * globalStyles.settings, * 'core/paragraph', * 'color.text', * '#ff0000' * ); * // If #ff0000 is the 'red' preset color, returns 'var:preset|color|red' * // Otherwise returns '#ff0000' * ``` * * @param features GlobalStylesContext settings object. * @param blockName The name of a block (e.g., 'core/paragraph'). * @param variableStylePath The style path (e.g., 'color.text', 'typography.fontSize'). * @param presetPropertyValue The value to encode (e.g., '#ff0000'). * @return The preset variable if found, otherwise the original value. */ export function getPresetVariableFromValue( features: GlobalStylesSettings, blockName: string | undefined, variableStylePath: string, presetPropertyValue: any ): any { if ( ! presetPropertyValue ) { return presetPropertyValue; } const cssVarInfix = STYLE_PATH_TO_CSS_VAR_INFIX[ variableStylePath ]; const metadata = PRESET_METADATA.find( ( data ) => data.cssVarInfix === cssVarInfix ); if ( ! metadata ) { // The property doesn't have preset data // so the value should be returned as it is. return presetPropertyValue; } const { valueKey, path } = metadata; const presetObject = findInPresetsBy( features, blockName, path, valueKey, presetPropertyValue ); if ( ! presetObject ) { // Value wasn't found in the presets, // so it must be a custom value. return presetPropertyValue; } return `var:preset|${ cssVarInfix }|${ presetObject.slug }`; }