fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,350 @@
/**
* Internal dependencies
*/
import { createElement, cloneElement, Fragment, isValidElement } from './react';
/**
* Object containing a React element.
*
* @typedef {import('react').ReactElement} Element
*/
let indoc, offset, output, stack;
/**
* Matches tags in the localized string
*
* This is used for extracting the tag pattern groups for parsing the localized
* string and along with the map converting it to a react element.
*
* There are four references extracted using this tokenizer:
*
* match: Full match of the tag (i.e. <strong>, </strong>, <br/>)
* isClosing: The closing slash, if it exists.
* name: The name portion of the tag (strong, br) (if )
* isSelfClosed: The slash on a self closing tag, if it exists.
*
* @type {RegExp}
*/
const tokenizer = /<(\/)?(\w+)\s*(\/)?>/g;
/**
* The stack frame tracking parse progress.
*
* @typedef Frame
*
* @property {Element} element A parent element which may still have
* @property {number} tokenStart Offset at which parent element first
* appears.
* @property {number} tokenLength Length of string marking start of parent
* element.
* @property {number} [prevOffset] Running offset at which parsing should
* continue.
* @property {number} [leadingTextStart] Offset at which last closing element
* finished, used for finding text between
* elements.
* @property {Element[]} children Children.
*/
/**
* Tracks recursive-descent parse state.
*
* This is a Stack frame holding parent elements until all children have been
* parsed.
*
* @private
* @param {Element} element A parent element which may still have
* nested children not yet parsed.
* @param {number} tokenStart Offset at which parent element first
* appears.
* @param {number} tokenLength Length of string marking start of parent
* element.
* @param {number} [prevOffset] Running offset at which parsing should
* continue.
* @param {number} [leadingTextStart] Offset at which last closing element
* finished, used for finding text between
* elements.
*
* @return {Frame} The stack frame tracking parse progress.
*/
function createFrame(
element,
tokenStart,
tokenLength,
prevOffset,
leadingTextStart
) {
return {
element,
tokenStart,
tokenLength,
prevOffset,
leadingTextStart,
children: [],
};
}
/**
* This function creates an interpolated element from a passed in string with
* specific tags matching how the string should be converted to an element via
* the conversion map value.
*
* @example
* For example, for the given string:
*
* "This is a <span>string</span> with <a>a link</a> and a self-closing
* <CustomComponentB/> tag"
*
* You would have something like this as the conversionMap value:
*
* ```js
* {
* span: <span />,
* a: <a href={ 'https://github.com' } />,
* CustomComponentB: <CustomComponent />,
* }
* ```
*
* @param {string} interpolatedString The interpolation string to be parsed.
* @param {Record<string, Element>} conversionMap The map used to convert the string to
* a react element.
* @throws {TypeError}
* @return {Element} A wp element.
*/
const createInterpolateElement = ( interpolatedString, conversionMap ) => {
indoc = interpolatedString;
offset = 0;
output = [];
stack = [];
tokenizer.lastIndex = 0;
if ( ! isValidConversionMap( conversionMap ) ) {
throw new TypeError(
'The conversionMap provided is not valid. It must be an object with values that are React Elements'
);
}
do {
// twiddle our thumbs
} while ( proceed( conversionMap ) );
return createElement( Fragment, null, ...output );
};
/**
* Validate conversion map.
*
* A map is considered valid if it's an object and every value in the object
* is a React Element
*
* @private
*
* @param {Object} conversionMap The map being validated.
*
* @return {boolean} True means the map is valid.
*/
const isValidConversionMap = ( conversionMap ) => {
const isObject = typeof conversionMap === 'object';
const values = isObject && Object.values( conversionMap );
return (
isObject &&
values.length &&
values.every( ( element ) => isValidElement( element ) )
);
};
/**
* This is the iterator over the matches in the string.
*
* @private
*
* @param {Object} conversionMap The conversion map for the string.
*
* @return {boolean} true for continuing to iterate, false for finished.
*/
function proceed( conversionMap ) {
const next = nextToken();
const [ tokenType, name, startOffset, tokenLength ] = next;
const stackDepth = stack.length;
const leadingTextStart = startOffset > offset ? offset : null;
if ( ! conversionMap[ name ] ) {
addText();
return false;
}
switch ( tokenType ) {
case 'no-more-tokens':
if ( stackDepth !== 0 ) {
const { leadingTextStart: stackLeadingText, tokenStart } =
stack.pop();
output.push( indoc.substr( stackLeadingText, tokenStart ) );
}
addText();
return false;
case 'self-closed':
if ( 0 === stackDepth ) {
if ( null !== leadingTextStart ) {
output.push(
indoc.substr(
leadingTextStart,
startOffset - leadingTextStart
)
);
}
output.push( conversionMap[ name ] );
offset = startOffset + tokenLength;
return true;
}
// Otherwise we found an inner element.
addChild(
createFrame( conversionMap[ name ], startOffset, tokenLength )
);
offset = startOffset + tokenLength;
return true;
case 'opener':
stack.push(
createFrame(
conversionMap[ name ],
startOffset,
tokenLength,
startOffset + tokenLength,
leadingTextStart
)
);
offset = startOffset + tokenLength;
return true;
case 'closer':
// If we're not nesting then this is easy - close the block.
if ( 1 === stackDepth ) {
closeOuterElement( startOffset );
offset = startOffset + tokenLength;
return true;
}
// Otherwise we're nested and we have to close out the current
// block and add it as a innerBlock to the parent.
const stackTop = stack.pop();
const text = indoc.substr(
stackTop.prevOffset,
startOffset - stackTop.prevOffset
);
stackTop.children.push( text );
stackTop.prevOffset = startOffset + tokenLength;
const frame = createFrame(
stackTop.element,
stackTop.tokenStart,
stackTop.tokenLength,
startOffset + tokenLength
);
frame.children = stackTop.children;
addChild( frame );
offset = startOffset + tokenLength;
return true;
default:
addText();
return false;
}
}
/**
* Grabs the next token match in the string and returns it's details.
*
* @private
*
* @return {Array} An array of details for the token matched.
*/
function nextToken() {
const matches = tokenizer.exec( indoc );
// We have no more tokens.
if ( null === matches ) {
return [ 'no-more-tokens' ];
}
const startedAt = matches.index;
const [ match, isClosing, name, isSelfClosed ] = matches;
const length = match.length;
if ( isSelfClosed ) {
return [ 'self-closed', name, startedAt, length ];
}
if ( isClosing ) {
return [ 'closer', name, startedAt, length ];
}
return [ 'opener', name, startedAt, length ];
}
/**
* Pushes text extracted from the indoc string to the output stack given the
* current rawLength value and offset (if rawLength is provided ) or the
* indoc.length and offset.
*
* @private
*/
function addText() {
const length = indoc.length - offset;
if ( 0 === length ) {
return;
}
output.push( indoc.substr( offset, length ) );
}
/**
* Pushes a child element to the associated parent element's children for the
* parent currently active in the stack.
*
* @private
*
* @param {Frame} frame The Frame containing the child element and it's
* token information.
*/
function addChild( frame ) {
const { element, tokenStart, tokenLength, prevOffset, children } = frame;
const parent = stack[ stack.length - 1 ];
const text = indoc.substr(
parent.prevOffset,
tokenStart - parent.prevOffset
);
if ( text ) {
parent.children.push( text );
}
parent.children.push( cloneElement( element, null, ...children ) );
parent.prevOffset = prevOffset ? prevOffset : tokenStart + tokenLength;
}
/**
* This is called for closing tags. It creates the element currently active in
* the stack.
*
* @private
*
* @param {number} endOffset Offset at which the closing tag for the element
* begins in the string. If this is greater than the
* prevOffset attached to the element, then this
* helps capture any remaining nested text nodes in
* the element.
*/
function closeOuterElement( endOffset ) {
const { element, leadingTextStart, prevOffset, tokenStart, children } =
stack.pop();
const text = endOffset
? indoc.substr( prevOffset, endOffset - prevOffset )
: indoc.substr( prevOffset );
if ( text ) {
children.push( text );
}
if ( null !== leadingTextStart ) {
output.push(
indoc.substr( leadingTextStart, tokenStart - leadingTextStart )
);
}
output.push( cloneElement( element, null, ...children ) );
}
export default createInterpolateElement;

7
node_modules/@wordpress/element/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,7 @@
export { default as createInterpolateElement } from './create-interpolate-element';
export * from './react';
export * from './react-platform';
export * from './utils';
export { default as Platform } from './platform';
export { default as renderToString } from './serialize';
export { default as RawHTML } from './raw-html';

View File

@@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { Platform as OriginalPlatform } from 'react-native';
const Platform = {
...OriginalPlatform,
OS: 'native',
select: ( spec ) => {
if ( 'android' in spec ) {
return spec.android;
} else if ( 'native' in spec ) {
return spec.native;
}
return spec.default;
},
isNative: true,
isAndroid: true,
};
export default Platform;

21
node_modules/@wordpress/element/src/platform.ios.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* External dependencies
*/
import { Platform as OriginalPlatform } from 'react-native';
const Platform = {
...OriginalPlatform,
OS: 'native',
select: ( spec ) => {
if ( 'ios' in spec ) {
return spec.ios;
} else if ( 'native' in spec ) {
return spec.native;
}
return spec.default;
},
isNative: true,
isIOS: true,
};
export default Platform;

33
node_modules/@wordpress/element/src/platform.js generated vendored Normal file
View File

@@ -0,0 +1,33 @@
/**
* Parts of this source were derived and modified from react-native-web,
* released under the MIT license.
*
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) 2015-present, Facebook, Inc.
*
*/
const Platform = {
OS: 'web',
select: ( spec ) => ( 'web' in spec ? spec.web : spec.default ),
isWeb: true,
};
/**
* Component used to detect the current Platform being used.
* Use Platform.OS === 'web' to detect if running on web enviroment.
*
* This is the same concept as the React Native implementation.
*
* @see https://reactnative.dev/docs/platform-specific-code#platform-module
*
* Here is an example of how to use the select method:
* @example
* ```js
* import { Platform } from '@wordpress/element';
*
* const placeholderLabel = Platform.select( {
* native: __( 'Add media' ),
* web: __( 'Drag images, upload new ones or select files from your library.' ),
* } );
* ```
*/
export default Platform;

36
node_modules/@wordpress/element/src/raw-html.js generated vendored Normal file
View File

@@ -0,0 +1,36 @@
/**
* Internal dependencies
*/
import { Children, createElement } from './react';
/** @typedef {{children: string} & import('react').ComponentPropsWithoutRef<'div'>} RawHTMLProps */
/**
* Component used as equivalent of Fragment with unescaped HTML, in cases where
* it is desirable to render dangerous HTML without needing a wrapper element.
* To preserve additional props, a `div` wrapper _will_ be created if any props
* aside from `children` are passed.
*
* @param {RawHTMLProps} props Children should be a string of HTML or an array
* of strings. Other props will be passed through
* to the div wrapper.
*
* @return {JSX.Element} Dangerously-rendering component.
*/
export default function RawHTML( { children, ...props } ) {
let rawHtml = '';
// Cast children as an array, and concatenate each element if it is a string.
Children.toArray( children ).forEach( ( child ) => {
if ( typeof child === 'string' && child.trim() !== '' ) {
rawHtml += child;
}
} );
// The `div` wrapper will be stripped by the `renderElement` serializer in
// `./serialize.js` unless there are non-children props present.
return createElement( 'div', {
dangerouslySetInnerHTML: { __html: rawHtml },
...props,
} );
}

77
node_modules/@wordpress/element/src/react-platform.js generated vendored Normal file
View File

@@ -0,0 +1,77 @@
/**
* External dependencies
*/
import {
createPortal,
findDOMNode,
flushSync,
render,
hydrate,
unmountComponentAtNode,
} from 'react-dom';
import { createRoot, hydrateRoot } from 'react-dom/client';
/**
* Creates a portal into which a component can be rendered.
*
* @see https://github.com/facebook/react/issues/10309#issuecomment-318433235
*
* @param {import('react').ReactElement} child Any renderable child, such as an element,
* string, or fragment.
* @param {HTMLElement} container DOM node into which element should be rendered.
*/
export { createPortal };
/**
* Finds the dom node of a React component.
*
* @param {import('react').ComponentType} component Component's instance.
*/
export { findDOMNode };
/**
* Forces React to flush any updates inside the provided callback synchronously.
*
* @param {Function} callback Callback to run synchronously.
*/
export { flushSync };
/**
* Renders a given element into the target DOM node.
*
* @deprecated since WordPress 6.2.0. Use `createRoot` instead.
* @see https://react.dev/reference/react-dom/render
*/
export { render };
/**
* Hydrates a given element into the target DOM node.
*
* @deprecated since WordPress 6.2.0. Use `hydrateRoot` instead.
* @see https://react.dev/reference/react-dom/hydrate
*/
export { hydrate };
/**
* Creates a new React root for the target DOM node.
*
* @since 6.2.0 Introduced in WordPress core.
* @see https://react.dev/reference/react-dom/client/createRoot
*/
export { createRoot };
/**
* Creates a new React root for the target DOM node and hydrates it with a pre-generated markup.
*
* @since 6.2.0 Introduced in WordPress core.
* @see https://react.dev/reference/react-dom/client/hydrateRoot
*/
export { hydrateRoot };
/**
* Removes any mounted element from the target DOM node.
*
* @deprecated since WordPress 6.2.0. Use `root.unmount()` instead.
* @see https://react.dev/reference/react-dom/unmountComponentAtNode
*/
export { unmountComponentAtNode };

View File

@@ -0,0 +1,14 @@
/**
* External dependencies
*/
import { AppRegistry } from 'react-native';
/**
* Registers an app root component allowing the native system to run the app.
*
* @param {string} appKey Unique app name identifier.
* @param {Function} componentProvider Function returning the app root React component.
*/
export const registerComponent = ( appKey, componentProvider ) => {
AppRegistry.registerComponent( appKey, componentProvider );
};

293
node_modules/@wordpress/element/src/react.js generated vendored Normal file
View File

@@ -0,0 +1,293 @@
/**
* External dependencies
*/
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import {
Children,
cloneElement,
Component,
createContext,
createElement,
createRef,
forwardRef,
Fragment,
isValidElement,
memo,
PureComponent,
StrictMode,
useCallback,
useContext,
useDebugValue,
useDeferredValue,
useEffect,
useId,
useMemo,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
useReducer,
useRef,
useState,
useSyncExternalStore,
useTransition,
startTransition,
lazy,
Suspense,
} from 'react';
/**
* Object containing a React element.
*
* @typedef {import('react').ReactElement} Element
*/
/**
* Object containing a React component.
*
* @typedef {import('react').ComponentType} ComponentType
*/
/**
* Object containing a React synthetic event.
*
* @typedef {import('react').SyntheticEvent} SyntheticEvent
*/
/**
* Object containing a React synthetic event.
*
* @template T
* @typedef {import('react').RefObject<T>} RefObject<T>
*/
/**
* Object that provides utilities for dealing with React children.
*/
export { Children };
/**
* Creates a copy of an element with extended props.
*
* @param {Element} element Element
* @param {?Object} props Props to apply to cloned element
*
* @return {Element} Cloned element.
*/
export { cloneElement };
/**
* A base class to create WordPress Components (Refs, state and lifecycle hooks)
*/
export { Component };
/**
* Creates a context object containing two components: a provider and consumer.
*
* @param {Object} defaultValue A default data stored in the context.
*
* @return {Object} Context object.
*/
export { createContext };
/**
* Returns a new element of given type. Type can be either a string tag name or
* another function which itself returns an element.
*
* @param {?(string|Function)} type Tag name or element creator
* @param {Object} props Element properties, either attribute
* set to apply to DOM node or values to
* pass through to element creator
* @param {...Element} children Descendant elements
*
* @return {Element} Element.
*/
export { createElement };
/**
* Returns an object tracking a reference to a rendered element via its
* `current` property as either a DOMElement or Element, dependent upon the
* type of element rendered with the ref attribute.
*
* @return {Object} Ref object.
*/
export { createRef };
/**
* Component enhancer used to enable passing a ref to its wrapped component.
* Pass a function argument which receives `props` and `ref` as its arguments,
* returning an element using the forwarded ref. The return value is a new
* component which forwards its ref.
*
* @param {Function} forwarder Function passed `props` and `ref`, expected to
* return an element.
*
* @return {Component} Enhanced component.
*/
export { forwardRef };
/**
* A component which renders its children without any wrapping element.
*/
export { Fragment };
/**
* Checks if an object is a valid React Element.
*
* @param {Object} objectToCheck The object to be checked.
*
* @return {boolean} true if objectToTest is a valid React Element and false otherwise.
*/
export { isValidElement };
/**
* @see https://reactjs.org/docs/react-api.html#reactmemo
*/
export { memo };
/**
* Component that activates additional checks and warnings for its descendants.
*/
export { StrictMode };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usecallback
*/
export { useCallback };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usecontext
*/
export { useContext };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue
*/
export { useDebugValue };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usedeferredvalue
*/
export { useDeferredValue };
/**
* @see https://reactjs.org/docs/hooks-reference.html#useeffect
*/
export { useEffect };
/**
* @see https://reactjs.org/docs/hooks-reference.html#useid
*/
export { useId };
/**
* @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle
*/
export { useImperativeHandle };
/**
* @see https://reactjs.org/docs/hooks-reference.html#useinsertioneffect
*/
export { useInsertionEffect };
/**
* @see https://reactjs.org/docs/hooks-reference.html#uselayouteffect
*/
export { useLayoutEffect };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usememo
*/
export { useMemo };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usereducer
*/
export { useReducer };
/**
* @see https://reactjs.org/docs/hooks-reference.html#useref
*/
export { useRef };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usestate
*/
export { useState };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore
*/
export { useSyncExternalStore };
/**
* @see https://reactjs.org/docs/hooks-reference.html#usetransition
*/
export { useTransition };
/**
* @see https://reactjs.org/docs/react-api.html#starttransition
*/
export { startTransition };
/**
* @see https://reactjs.org/docs/react-api.html#reactlazy
*/
export { lazy };
/**
* @see https://reactjs.org/docs/react-api.html#reactsuspense
*/
export { Suspense };
/**
* @see https://reactjs.org/docs/react-api.html#reactpurecomponent
*/
export { PureComponent };
/**
* Concatenate two or more React children objects.
*
* @param {...?Object} childrenArguments Array of children arguments (array of arrays/strings/objects) to concatenate.
*
* @return {Array} The concatenated value.
*/
export function concatChildren( ...childrenArguments ) {
return childrenArguments.reduce( ( accumulator, children, i ) => {
Children.forEach( children, ( child, j ) => {
if ( child && 'string' !== typeof child ) {
child = cloneElement( child, {
key: [ i, j ].join(),
} );
}
accumulator.push( child );
} );
return accumulator;
}, [] );
}
/**
* Switches the nodeName of all the elements in the children object.
*
* @param {?Object} children Children object.
* @param {string} nodeName Node name.
*
* @return {?Object} The updated children object.
*/
export function switchChildrenNodeName( children, nodeName ) {
return (
children &&
Children.map( children, ( elt, index ) => {
if ( typeof elt?.valueOf() === 'string' ) {
return createElement( nodeName, { key: index }, elt );
}
const { children: childrenProp, ...props } = elt.props;
return createElement(
nodeName,
{ key: index, ...props },
childrenProp
);
} )
);
}

838
node_modules/@wordpress/element/src/serialize.js generated vendored Normal file
View File

@@ -0,0 +1,838 @@
/**
* Parts of this source were derived and modified from fast-react-render,
* released under the MIT license.
*
* https://github.com/alt-j/fast-react-render
*
* Copyright (c) 2016 Andrey Morozov
*
* 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:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 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.
*/
/**
* External dependencies
*/
import { isPlainObject } from 'is-plain-object';
import { paramCase as kebabCase } from 'change-case';
/**
* WordPress dependencies
*/
import {
escapeHTML,
escapeAttribute,
isValidAttributeName,
} from '@wordpress/escape-html';
/**
* Internal dependencies
*/
import { createContext, Fragment, StrictMode, forwardRef } from './react';
import RawHTML from './raw-html';
/** @typedef {import('react').ReactElement} ReactElement */
const { Provider, Consumer } = createContext( undefined );
const ForwardRef = forwardRef( () => {
return null;
} );
/**
* Valid attribute types.
*
* @type {Set<string>}
*/
const ATTRIBUTES_TYPES = new Set( [ 'string', 'boolean', 'number' ] );
/**
* Element tags which can be self-closing.
*
* @type {Set<string>}
*/
const SELF_CLOSING_TAGS = new Set( [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
] );
/**
* Boolean attributes are attributes whose presence as being assigned is
* meaningful, even if only empty.
*
* See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
* Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3
*
* Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ]
* .filter( ( tr ) => tr.lastChild.textContent.indexOf( 'Boolean attribute' ) !== -1 )
* .reduce( ( result, tr ) => Object.assign( result, {
* [ tr.firstChild.textContent.trim() ]: true
* } ), {} ) ).sort();
*
* @type {Set<string>}
*/
const BOOLEAN_ATTRIBUTES = new Set( [
'allowfullscreen',
'allowpaymentrequest',
'allowusermedia',
'async',
'autofocus',
'autoplay',
'checked',
'controls',
'default',
'defer',
'disabled',
'download',
'formnovalidate',
'hidden',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nomodule',
'novalidate',
'open',
'playsinline',
'readonly',
'required',
'reversed',
'selected',
'typemustmatch',
] );
/**
* Enumerated attributes are attributes which must be of a specific value form.
* Like boolean attributes, these are meaningful if specified, even if not of a
* valid enumerated value.
*
* See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#enumerated-attribute
* Extracted from: https://html.spec.whatwg.org/multipage/indices.html#attributes-3
*
* Object.keys( [ ...document.querySelectorAll( '#attributes-1 > tbody > tr' ) ]
* .filter( ( tr ) => /^("(.+?)";?\s*)+/.test( tr.lastChild.textContent.trim() ) )
* .reduce( ( result, tr ) => Object.assign( result, {
* [ tr.firstChild.textContent.trim() ]: true
* } ), {} ) ).sort();
*
* Some notable omissions:
*
* - `alt`: https://blog.whatwg.org/omit-alt
*
* @type {Set<string>}
*/
const ENUMERATED_ATTRIBUTES = new Set( [
'autocapitalize',
'autocomplete',
'charset',
'contenteditable',
'crossorigin',
'decoding',
'dir',
'draggable',
'enctype',
'formenctype',
'formmethod',
'http-equiv',
'inputmode',
'kind',
'method',
'preload',
'scope',
'shape',
'spellcheck',
'translate',
'type',
'wrap',
] );
/**
* Set of CSS style properties which support assignment of unitless numbers.
* Used in rendering of style properties, where `px` unit is assumed unless
* property is included in this set or value is zero.
*
* Generated via:
*
* Object.entries( document.createElement( 'div' ).style )
* .filter( ( [ key ] ) => (
* ! /^(webkit|ms|moz)/.test( key ) &&
* ( e.style[ key ] = 10 ) &&
* e.style[ key ] === '10'
* ) )
* .map( ( [ key ] ) => key )
* .sort();
*
* @type {Set<string>}
*/
const CSS_PROPERTIES_SUPPORTS_UNITLESS = new Set( [
'animation',
'animationIterationCount',
'baselineShift',
'borderImageOutset',
'borderImageSlice',
'borderImageWidth',
'columnCount',
'cx',
'cy',
'fillOpacity',
'flexGrow',
'flexShrink',
'floodOpacity',
'fontWeight',
'gridColumnEnd',
'gridColumnStart',
'gridRowEnd',
'gridRowStart',
'lineHeight',
'opacity',
'order',
'orphans',
'r',
'rx',
'ry',
'shapeImageThreshold',
'stopOpacity',
'strokeDasharray',
'strokeDashoffset',
'strokeMiterlimit',
'strokeOpacity',
'strokeWidth',
'tabSize',
'widows',
'x',
'y',
'zIndex',
'zoom',
] );
/**
* Returns true if the specified string is prefixed by one of an array of
* possible prefixes.
*
* @param {string} string String to check.
* @param {string[]} prefixes Possible prefixes.
*
* @return {boolean} Whether string has prefix.
*/
export function hasPrefix( string, prefixes ) {
return prefixes.some( ( prefix ) => string.indexOf( prefix ) === 0 );
}
/**
* Returns true if the given prop name should be ignored in attributes
* serialization, or false otherwise.
*
* @param {string} attribute Attribute to check.
*
* @return {boolean} Whether attribute should be ignored.
*/
function isInternalAttribute( attribute ) {
return 'key' === attribute || 'children' === attribute;
}
/**
* Returns the normal form of the element's attribute value for HTML.
*
* @param {string} attribute Attribute name.
* @param {*} value Non-normalized attribute value.
*
* @return {*} Normalized attribute value.
*/
function getNormalAttributeValue( attribute, value ) {
switch ( attribute ) {
case 'style':
return renderStyle( value );
}
return value;
}
/**
* This is a map of all SVG attributes that have dashes. Map(lower case prop => dashed lower case attribute).
* We need this to render e.g strokeWidth as stroke-width.
*
* List from: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute.
*/
const SVG_ATTRIBUTE_WITH_DASHES_LIST = [
'accentHeight',
'alignmentBaseline',
'arabicForm',
'baselineShift',
'capHeight',
'clipPath',
'clipRule',
'colorInterpolation',
'colorInterpolationFilters',
'colorProfile',
'colorRendering',
'dominantBaseline',
'enableBackground',
'fillOpacity',
'fillRule',
'floodColor',
'floodOpacity',
'fontFamily',
'fontSize',
'fontSizeAdjust',
'fontStretch',
'fontStyle',
'fontVariant',
'fontWeight',
'glyphName',
'glyphOrientationHorizontal',
'glyphOrientationVertical',
'horizAdvX',
'horizOriginX',
'imageRendering',
'letterSpacing',
'lightingColor',
'markerEnd',
'markerMid',
'markerStart',
'overlinePosition',
'overlineThickness',
'paintOrder',
'panose1',
'pointerEvents',
'renderingIntent',
'shapeRendering',
'stopColor',
'stopOpacity',
'strikethroughPosition',
'strikethroughThickness',
'strokeDasharray',
'strokeDashoffset',
'strokeLinecap',
'strokeLinejoin',
'strokeMiterlimit',
'strokeOpacity',
'strokeWidth',
'textAnchor',
'textDecoration',
'textRendering',
'underlinePosition',
'underlineThickness',
'unicodeBidi',
'unicodeRange',
'unitsPerEm',
'vAlphabetic',
'vHanging',
'vIdeographic',
'vMathematical',
'vectorEffect',
'vertAdvY',
'vertOriginX',
'vertOriginY',
'wordSpacing',
'writingMode',
'xmlnsXlink',
'xHeight',
].reduce( ( map, attribute ) => {
// The keys are lower-cased for more robust lookup.
map[ attribute.toLowerCase() ] = attribute;
return map;
}, {} );
/**
* This is a map of all case-sensitive SVG attributes. Map(lowercase key => proper case attribute).
* The keys are lower-cased for more robust lookup.
* Note that this list only contains attributes that contain at least one capital letter.
* Lowercase attributes don't need mapping, since we lowercase all attributes by default.
*/
const CASE_SENSITIVE_SVG_ATTRIBUTES = [
'allowReorder',
'attributeName',
'attributeType',
'autoReverse',
'baseFrequency',
'baseProfile',
'calcMode',
'clipPathUnits',
'contentScriptType',
'contentStyleType',
'diffuseConstant',
'edgeMode',
'externalResourcesRequired',
'filterRes',
'filterUnits',
'glyphRef',
'gradientTransform',
'gradientUnits',
'kernelMatrix',
'kernelUnitLength',
'keyPoints',
'keySplines',
'keyTimes',
'lengthAdjust',
'limitingConeAngle',
'markerHeight',
'markerUnits',
'markerWidth',
'maskContentUnits',
'maskUnits',
'numOctaves',
'pathLength',
'patternContentUnits',
'patternTransform',
'patternUnits',
'pointsAtX',
'pointsAtY',
'pointsAtZ',
'preserveAlpha',
'preserveAspectRatio',
'primitiveUnits',
'refX',
'refY',
'repeatCount',
'repeatDur',
'requiredExtensions',
'requiredFeatures',
'specularConstant',
'specularExponent',
'spreadMethod',
'startOffset',
'stdDeviation',
'stitchTiles',
'suppressContentEditableWarning',
'suppressHydrationWarning',
'surfaceScale',
'systemLanguage',
'tableValues',
'targetX',
'targetY',
'textLength',
'viewBox',
'viewTarget',
'xChannelSelector',
'yChannelSelector',
].reduce( ( map, attribute ) => {
// The keys are lower-cased for more robust lookup.
map[ attribute.toLowerCase() ] = attribute;
return map;
}, {} );
/**
* This is a map of all SVG attributes that have colons.
* Keys are lower-cased and stripped of their colons for more robust lookup.
*/
const SVG_ATTRIBUTES_WITH_COLONS = [
'xlink:actuate',
'xlink:arcrole',
'xlink:href',
'xlink:role',
'xlink:show',
'xlink:title',
'xlink:type',
'xml:base',
'xml:lang',
'xml:space',
'xmlns:xlink',
].reduce( ( map, attribute ) => {
map[ attribute.replace( ':', '' ).toLowerCase() ] = attribute;
return map;
}, {} );
/**
* Returns the normal form of the element's attribute name for HTML.
*
* @param {string} attribute Non-normalized attribute name.
*
* @return {string} Normalized attribute name.
*/
function getNormalAttributeName( attribute ) {
switch ( attribute ) {
case 'htmlFor':
return 'for';
case 'className':
return 'class';
}
const attributeLowerCase = attribute.toLowerCase();
if ( CASE_SENSITIVE_SVG_ATTRIBUTES[ attributeLowerCase ] ) {
return CASE_SENSITIVE_SVG_ATTRIBUTES[ attributeLowerCase ];
} else if ( SVG_ATTRIBUTE_WITH_DASHES_LIST[ attributeLowerCase ] ) {
return kebabCase(
SVG_ATTRIBUTE_WITH_DASHES_LIST[ attributeLowerCase ]
);
} else if ( SVG_ATTRIBUTES_WITH_COLONS[ attributeLowerCase ] ) {
return SVG_ATTRIBUTES_WITH_COLONS[ attributeLowerCase ];
}
return attributeLowerCase;
}
/**
* Returns the normal form of the style property name for HTML.
*
* - Converts property names to kebab-case, e.g. 'backgroundColor' → 'background-color'
* - Leaves custom attributes alone, e.g. '--myBackgroundColor' → '--myBackgroundColor'
* - Converts vendor-prefixed property names to -kebab-case, e.g. 'MozTransform' → '-moz-transform'
*
* @param {string} property Property name.
*
* @return {string} Normalized property name.
*/
function getNormalStylePropertyName( property ) {
if ( property.startsWith( '--' ) ) {
return property;
}
if ( hasPrefix( property, [ 'ms', 'O', 'Moz', 'Webkit' ] ) ) {
return '-' + kebabCase( property );
}
return kebabCase( property );
}
/**
* Returns the normal form of the style property value for HTML. Appends a
* default pixel unit if numeric, not a unitless property, and not zero.
*
* @param {string} property Property name.
* @param {*} value Non-normalized property value.
*
* @return {*} Normalized property value.
*/
function getNormalStylePropertyValue( property, value ) {
if (
typeof value === 'number' &&
0 !== value &&
! CSS_PROPERTIES_SUPPORTS_UNITLESS.has( property )
) {
return value + 'px';
}
return value;
}
/**
* Serializes a React element to string.
*
* @param {import('react').ReactNode} element Element to serialize.
* @param {Object} [context] Context object.
* @param {Object} [legacyContext] Legacy context object.
*
* @return {string} Serialized element.
*/
export function renderElement( element, context, legacyContext = {} ) {
if ( null === element || undefined === element || false === element ) {
return '';
}
if ( Array.isArray( element ) ) {
return renderChildren( element, context, legacyContext );
}
switch ( typeof element ) {
case 'string':
return escapeHTML( element );
case 'number':
return element.toString();
}
const { type, props } = /** @type {{type?: any, props?: any}} */ (
element
);
switch ( type ) {
case StrictMode:
case Fragment:
return renderChildren( props.children, context, legacyContext );
case RawHTML:
const { children, ...wrapperProps } = props;
return renderNativeComponent(
! Object.keys( wrapperProps ).length ? null : 'div',
{
...wrapperProps,
dangerouslySetInnerHTML: { __html: children },
},
context,
legacyContext
);
}
switch ( typeof type ) {
case 'string':
return renderNativeComponent( type, props, context, legacyContext );
case 'function':
if (
type.prototype &&
typeof type.prototype.render === 'function'
) {
return renderComponent( type, props, context, legacyContext );
}
return renderElement(
type( props, legacyContext ),
context,
legacyContext
);
}
switch ( type && type.$$typeof ) {
case Provider.$$typeof:
return renderChildren( props.children, props.value, legacyContext );
case Consumer.$$typeof:
return renderElement(
props.children( context || type._currentValue ),
context,
legacyContext
);
case ForwardRef.$$typeof:
return renderElement(
type.render( props ),
context,
legacyContext
);
}
return '';
}
/**
* Serializes a native component type to string.
*
* @param {?string} type Native component type to serialize, or null if
* rendering as fragment of children content.
* @param {Object} props Props object.
* @param {Object} [context] Context object.
* @param {Object} [legacyContext] Legacy context object.
*
* @return {string} Serialized element.
*/
export function renderNativeComponent(
type,
props,
context,
legacyContext = {}
) {
let content = '';
if ( type === 'textarea' && props.hasOwnProperty( 'value' ) ) {
// Textarea children can be assigned as value prop. If it is, render in
// place of children. Ensure to omit so it is not assigned as attribute
// as well.
content = renderChildren( props.value, context, legacyContext );
const { value, ...restProps } = props;
props = restProps;
} else if (
props.dangerouslySetInnerHTML &&
typeof props.dangerouslySetInnerHTML.__html === 'string'
) {
// Dangerous content is left unescaped.
content = props.dangerouslySetInnerHTML.__html;
} else if ( typeof props.children !== 'undefined' ) {
content = renderChildren( props.children, context, legacyContext );
}
if ( ! type ) {
return content;
}
const attributes = renderAttributes( props );
if ( SELF_CLOSING_TAGS.has( type ) ) {
return '<' + type + attributes + '/>';
}
return '<' + type + attributes + '>' + content + '</' + type + '>';
}
/** @typedef {import('react').ComponentType} ComponentType */
/**
* Serializes a non-native component type to string.
*
* @param {ComponentType} Component Component type to serialize.
* @param {Object} props Props object.
* @param {Object} [context] Context object.
* @param {Object} [legacyContext] Legacy context object.
*
* @return {string} Serialized element
*/
export function renderComponent(
Component,
props,
context,
legacyContext = {}
) {
const instance = new /** @type {import('react').ComponentClass} */ (
Component
)( props, legacyContext );
if (
typeof (
// Ignore reason: Current prettier reformats parens and mangles type assertion
// prettier-ignore
/** @type {{getChildContext?: () => unknown}} */ ( instance ).getChildContext
) === 'function'
) {
Object.assign(
legacyContext,
/** @type {{getChildContext?: () => unknown}} */ (
instance
).getChildContext()
);
}
const html = renderElement( instance.render(), context, legacyContext );
return html;
}
/**
* Serializes an array of children to string.
*
* @param {import('react').ReactNodeArray} children Children to serialize.
* @param {Object} [context] Context object.
* @param {Object} [legacyContext] Legacy context object.
*
* @return {string} Serialized children.
*/
function renderChildren( children, context, legacyContext = {} ) {
let result = '';
children = Array.isArray( children ) ? children : [ children ];
for ( let i = 0; i < children.length; i++ ) {
const child = children[ i ];
result += renderElement( child, context, legacyContext );
}
return result;
}
/**
* Renders a props object as a string of HTML attributes.
*
* @param {Object} props Props object.
*
* @return {string} Attributes string.
*/
export function renderAttributes( props ) {
let result = '';
for ( const key in props ) {
const attribute = getNormalAttributeName( key );
if ( ! isValidAttributeName( attribute ) ) {
continue;
}
let value = getNormalAttributeValue( key, props[ key ] );
// If value is not of serializable type, skip.
if ( ! ATTRIBUTES_TYPES.has( typeof value ) ) {
continue;
}
// Don't render internal attribute names.
if ( isInternalAttribute( key ) ) {
continue;
}
const isBooleanAttribute = BOOLEAN_ATTRIBUTES.has( attribute );
// Boolean attribute should be omitted outright if its value is false.
if ( isBooleanAttribute && value === false ) {
continue;
}
const isMeaningfulAttribute =
isBooleanAttribute ||
hasPrefix( key, [ 'data-', 'aria-' ] ) ||
ENUMERATED_ATTRIBUTES.has( attribute );
// Only write boolean value as attribute if meaningful.
if ( typeof value === 'boolean' && ! isMeaningfulAttribute ) {
continue;
}
result += ' ' + attribute;
// Boolean attributes should write attribute name, but without value.
// Mere presence of attribute name is effective truthiness.
if ( isBooleanAttribute ) {
continue;
}
if ( typeof value === 'string' ) {
value = escapeAttribute( value );
}
result += '="' + value + '"';
}
return result;
}
/**
* Renders a style object as a string attribute value.
*
* @param {Object} style Style object.
*
* @return {string} Style attribute value.
*/
export function renderStyle( style ) {
// Only generate from object, e.g. tolerate string value.
if ( ! isPlainObject( style ) ) {
return style;
}
let result;
for ( const property in style ) {
const value = style[ property ];
if ( null === value || undefined === value ) {
continue;
}
if ( result ) {
result += ';';
} else {
result = '';
}
const normalName = getNormalStylePropertyName( property );
const normalValue = getNormalStylePropertyValue( property, value );
result += normalName + ':' + normalValue;
}
return result;
}
export default renderElement;

View File

@@ -0,0 +1,237 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
/**
* Internal dependencies
*/
import { createElement, Fragment, Component } from '../react';
import createInterpolateElement from '../create-interpolate-element';
describe( 'createInterpolateElement', () => {
it( 'throws an error when there is no conversion map', () => {
const testString = 'This is a string';
expect( () => createInterpolateElement( testString, {} ) ).toThrow(
TypeError
);
} );
it( 'returns same string when there are no tokens in the string', () => {
const testString = 'This is a string';
const expectedElement = <>{ testString }</>;
expect(
createInterpolateElement( testString, { someValue: <em /> } )
).toEqual( expectedElement );
} );
it( 'throws an error when there is an invalid conversion map', () => {
const testString = 'This is a <someValue/> string';
expect( () =>
createInterpolateElement( testString, [
'someValue',
{ value: 10 },
] )
).toThrow( TypeError );
} );
it(
'throws an error when there is an invalid entry in the conversion ' +
'map',
() => {
const testString = 'This is a <item /> string and <somethingElse/>';
expect( () =>
createInterpolateElement( testString, {
someValue: <em />,
somethingElse: 10,
} )
).toThrow( TypeError );
}
);
it(
'returns same string when there is an non matching token in the ' +
'string',
() => {
const testString = 'This is a <non_parsed/> string';
const expectedElement = <>{ testString }</>;
expect(
createInterpolateElement( testString, {
someValue: <strong />,
} )
).toEqual( expectedElement );
}
);
it( 'returns same string when there is spaces in the token', () => {
const testString = 'This is a <spaced token/>string';
const expectedElement = <>{ testString }</>;
expect(
createInterpolateElement( testString, { 'spaced token': <em /> } )
).toEqual( expectedElement );
} );
it( 'returns expected react element for non nested components', () => {
const testString = 'This is a string with <a>a link</a>.';
const expectedElement = createElement(
Fragment,
null,
'This is a string with ',
createElement(
'a',
{ href: 'https://github.com', className: 'some_class' },
'a link'
),
'.'
);
const component = createInterpolateElement( testString, {
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href={ 'https://github.com' } className={ 'some_class' } />,
} );
expect( JSON.stringify( component ) ).toEqual(
JSON.stringify( expectedElement )
);
} );
it( 'returns expected react element for nested components', () => {
const testString = 'This is a <a>string that is <em>linked</em></a>.';
const expectedElement = createElement(
Fragment,
{},
'This is a ',
createElement(
'a',
null,
'string that is ',
createElement( 'em', null, 'linked' )
),
'.'
);
expect(
JSON.stringify(
createInterpolateElement( testString, {
a: createElement( 'a' ),
em: <em />,
} )
)
).toEqual( JSON.stringify( expectedElement ) );
} );
it(
'returns expected output for a custom component with children ' +
'replacement',
() => {
const TestComponent = ( props ) => {
return <div { ...props }>{ props.children }</div>;
};
const testString =
'This is a string with a <TestComponent>Custom Component</TestComponent>';
const expectedElement = createElement(
Fragment,
null,
'This is a string with a ',
createElement( TestComponent, null, 'Custom Component' )
);
expect(
JSON.stringify(
createInterpolateElement( testString, {
TestComponent: <TestComponent />,
} )
)
).toEqual( JSON.stringify( expectedElement ) );
}
);
it( 'returns expected output for self closing custom component', () => {
const TestComponent = ( props ) => {
return <div { ...props } />;
};
const testString =
'This is a string with a self closing custom component: <TestComponent/>';
const expectedElement = createElement(
Fragment,
null,
'This is a string with a self closing custom component: ',
createElement( TestComponent )
);
expect(
JSON.stringify(
createInterpolateElement( testString, {
TestComponent: <TestComponent />,
} )
)
).toEqual( JSON.stringify( expectedElement ) );
} );
it( 'throws an error with an invalid element in the conversion map', () => {
const test = () =>
createInterpolateElement( 'This is a <invalid /> string', {
invalid: 10,
} );
expect( test ).toThrow( TypeError );
} );
it( 'returns expected output for complex replacement', () => {
class TestComponent extends Component {
render( props ) {
return <div { ...props } />;
}
}
const testString =
'This is a complex string with ' +
'a <a1>nested <em1>emphasized string</em1> link</a1> and value: <TestComponent/>';
const expectedElement = createElement(
Fragment,
null,
'This is a complex string with a ',
createElement(
'a',
null,
'nested ',
createElement( 'em', null, 'emphasized string' ),
' link'
),
' and value: ',
createElement( TestComponent )
);
expect(
JSON.stringify(
createInterpolateElement( testString, {
TestComponent: <TestComponent />,
em1: <em />,
a1: createElement( 'a' ),
} )
)
).toEqual( JSON.stringify( expectedElement ) );
} );
it( 'renders expected components across renders for keys in use', () => {
const TestComponent = ( { switchKey } ) => {
const elementConfig = switchKey
? { item: <em /> }
: { item: <strong /> };
return (
<div>
{ createInterpolateElement(
'This is a <item>string!</item>',
elementConfig
) }
</div>
);
};
const { container, rerender } = render( <TestComponent switchKey /> );
expect( container ).toContainHTML( '<em>string!</em>' );
expect( container ).not.toContainHTML( '<strong>' );
rerender( <TestComponent switchKey={ false } /> );
expect( container ).toContainHTML( '<strong>string!</strong>' );
expect( container ).not.toContainHTML( '<em>' );
} );
it( 'handles parsing emojii correctly', () => {
const testString = '👳‍♀️<icon>🚨🤷fully</icon> here';
const expectedElement = createElement(
Fragment,
null,
'👳‍♀️',
createElement( 'strong', null, '🚨🤷fully' ),
' here'
);
expect(
JSON.stringify(
createInterpolateElement( testString, {
icon: <strong />,
} )
)
).toEqual( JSON.stringify( expectedElement ) );
} );
} );

133
node_modules/@wordpress/element/src/test/index.js generated vendored Normal file
View File

@@ -0,0 +1,133 @@
/* eslint-disable testing-library/render-result-naming-convention */
/**
* Internal dependencies
*/
import {
concatChildren,
createElement,
RawHTML,
renderToString,
switchChildrenNodeName,
} from '../';
describe( 'element', () => {
describe( 'renderToString', () => {
it( 'should return an empty string for booleans/null/undefined values', () => {
expect( renderToString() ).toBe( '' );
expect( renderToString( false ) ).toBe( '' );
expect( renderToString( true ) ).toBe( '' );
expect( renderToString( null ) ).toBe( '' );
} );
it( 'should return a string 0', () => {
expect( renderToString( 0 ) ).toBe( '0' );
} );
it( 'should return a string 12345', () => {
expect( renderToString( 12345 ) ).toBe( '12345' );
} );
it( 'should return a string verbatim', () => {
expect( renderToString( 'Zucchini' ) ).toBe( 'Zucchini' );
} );
it( 'should return a string from an array', () => {
expect(
renderToString( [
'Zucchini ',
createElement( 'em', null, 'is a' ),
' summer squash',
] )
).toBe( 'Zucchini <em>is a</em> summer squash' );
} );
it( 'should return a string from an element', () => {
expect(
renderToString( createElement( 'strong', null, 'Courgette' ) )
).toBe( '<strong>Courgette</strong>' );
} );
it( 'should escape attributes and html', () => {
const result = renderToString(
createElement(
'a',
{
href: '/index.php?foo=bar&qux=<"scary">',
style: {
backgroundColor: 'red',
},
},
'<"WordPress" & Friends>'
)
);
expect( result ).toBe(
'<a href="/index.php?foo=bar&amp;qux=<&quot;scary&quot;&gt;" style="background-color:red">' +
'&lt;"WordPress" &amp; Friends>' +
'</a>'
);
} );
it( 'strips raw html wrapper', () => {
const html = '<p>So scary!</p>';
expect( renderToString( <RawHTML>{ html }</RawHTML> ) ).toBe(
html
);
} );
} );
describe( 'concatChildren', () => {
it( 'should return an empty array for undefined children', () => {
expect( concatChildren() ).toEqual( [] );
} );
it( 'should concat the string arrays', () => {
expect( concatChildren( [ 'a' ], 'b' ) ).toEqual( [ 'a', 'b' ] );
} );
it( 'should concat the object arrays and rewrite keys', () => {
const concat = concatChildren(
[ createElement( 'strong', {}, 'Courgette' ) ],
createElement( 'strong', {}, 'Concombre' )
);
expect( concat ).toHaveLength( 2 );
expect( concat[ 0 ].key ).toBe( '0,0' );
expect( concat[ 1 ].key ).toBe( '1,0' );
} );
} );
describe( 'switchChildrenNodeName', () => {
it( 'should return undefined for undefined children', () => {
expect( switchChildrenNodeName() ).toBeUndefined();
} );
it( 'should switch strings', () => {
const children = switchChildrenNodeName( [ 'a', 'b' ], 'strong' );
expect( children ).toHaveLength( 2 );
expect( children[ 0 ].type ).toBe( 'strong' );
expect( children[ 0 ].props.children ).toBe( 'a' );
expect( children[ 1 ].type ).toBe( 'strong' );
expect( children[ 1 ].props.children ).toBe( 'b' );
} );
it( 'should switch elements', () => {
const children = switchChildrenNodeName(
[
createElement( 'strong', { align: 'left' }, 'Courgette' ),
createElement( 'strong', {}, 'Concombre' ),
],
'em'
);
expect( children ).toHaveLength( 2 );
expect( children[ 0 ].type ).toBe( 'em' );
expect( children[ 0 ].props.children ).toBe( 'Courgette' );
expect( children[ 0 ].props.align ).toBe( 'left' );
expect( children[ 1 ].type ).toBe( 'em' );
expect( children[ 1 ].props.children ).toBe( 'Concombre' );
} );
} );
} );
/* eslint-enable testing-library/render-result-naming-convention */

15
node_modules/@wordpress/element/src/test/platform.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import Platform from '../platform';
describe( 'Platform', () => {
it( 'is chooses the right thing', () => {
const element = Platform.select( {
web: <div />,
native: <button />,
} );
expect( element ).toEqual( <div /> );
} );
} );

View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import Platform from '../platform';
describe( 'Platform', () => {
it( 'is chooses the right thing', () => {
const selection = Platform.select( {
web: 'web',
native: 'native',
} );
expect( selection ).toBe( 'native' );
} );
} );

65
node_modules/@wordpress/element/src/test/raw-html.js generated vendored Normal file
View File

@@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
/**
* Internal dependencies
*/
import RawHTML from '../raw-html';
describe( 'RawHTML', () => {
it( 'is dangerous', () => {
const html = '<p>So scary!</p>';
const { container } = render( <RawHTML>{ html }</RawHTML> );
const expected = '<div><p>So scary!</p></div>';
expect( container.innerHTML ).toBe( expected );
} );
it( 'adds other props to container element', () => {
const html = '<p>So scary!</p>';
const { container } = render(
<RawHTML className="foo">{ html }</RawHTML>
);
expect( container.innerHTML ).toBe(
'<div class="foo"><p>So scary!</p></div>'
);
} );
it( 'concatenates children if multiple children present', () => {
const html = '<p>So scary!</p>';
const html2 = '<p>Extra paragraph</p>';
const { container } = render(
<RawHTML>
{ html }
{ html2 }
</RawHTML>
);
const expected = '<div><p>So scary!</p><p>Extra paragraph</p></div>';
expect( container.innerHTML ).toBe( expected );
} );
it( 'renders an empty container if there are no children', () => {
const { container } = render( <RawHTML></RawHTML> );
const expected = '<div></div>';
expect( container.innerHTML ).toBe( expected );
} );
it( 'ignores non-string based children', () => {
const html = '<p>So scary!</p>';
const { container } = render(
<RawHTML>
{ html }
<p>Ignore this!</p>
</RawHTML>
);
const expected = '<div><p>So scary!</p></div>';
expect( container.innerHTML ).toBe( expected );
} );
} );

723
node_modules/@wordpress/element/src/test/serialize.js generated vendored Normal file
View File

@@ -0,0 +1,723 @@
/* eslint-disable testing-library/render-result-naming-convention */
/**
* Internal dependencies
*/
import {
Component,
createContext,
createElement,
Fragment,
StrictMode,
forwardRef,
} from '../react';
import RawHTML from '../raw-html';
import serialize, {
hasPrefix,
renderElement,
renderNativeComponent,
renderComponent,
renderAttributes,
renderStyle,
} from '../serialize';
const noop = () => {};
describe( 'serialize()', () => {
it( 'should allow only valid attribute names', () => {
const element = createElement( 'div', {
'notok\u007F': 'bad',
'notok"': 'bad',
ok: 'good',
'notok\uFDD0': 'bad',
} );
const result = serialize( element );
expect( result ).toBe( '<div ok="good"></div>' );
} );
it( 'should render with context (legacy)', () => {
class Provider extends Component {
getChildContext() {
return {
greeting: 'Hello!',
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
greeting: noop,
};
// NOTE: Technically, a component should only receive context if it
// explicitly defines `contextTypes`. This requirement is ignored in
// our implementation.
function FunctionComponent( props, context ) {
return 'FunctionComponent: ' + context.greeting;
}
class ClassComponent extends Component {
render() {
return 'ClassComponent: ' + this.context.greeting;
}
}
const result = serialize(
<Provider>
<FunctionComponent />
<ClassComponent />
</Provider>
);
expect( result ).toBe(
'FunctionComponent: Hello!' + 'ClassComponent: Hello!'
);
} );
it( 'should render with forwardRef', () => {
const ForwardedComponent = forwardRef( () => {
return <div>test</div>;
} );
const result = serialize( <ForwardedComponent /> );
expect( result ).toBe( '<div>test</div>' );
} );
describe( 'empty attributes', () => {
it( 'should not render a null attribute value', () => {
const result = serialize( <video src={ undefined } /> );
expect( result ).toBe( '<video></video>' );
} );
it( 'should not render an undefined attribute value', () => {
const result = serialize( <video src={ null } /> );
expect( result ).toBe( '<video></video>' );
} );
it( 'should an explicitly empty string attribute', () => {
const result = serialize( <video className="" /> );
expect( result ).toBe( '<video class=""></video>' );
} );
it( 'should not render an empty object style', () => {
const result = serialize( <video style={ {} } /> );
expect( result ).toBe( '<video></video>' );
} );
} );
describe( 'boolean attributes', () => {
it( 'should render elements with false boolean attributes', () => {
[ false, null, undefined ].forEach( ( controls ) => {
const result = serialize(
<video src="/" controls={ controls } />
);
expect( result ).toBe( '<video src="/"></video>' );
} );
} );
it( 'should render elements with true boolean attributes', () => {
[ true, 'true', 'false', '' ].forEach( ( controls ) => {
const result = serialize(
<video src="/" controls={ controls } />
);
expect( result ).toBe( '<video src="/" controls></video>' );
} );
} );
it( 'should not render non-boolean-attribute with boolean value', () => {
const result = serialize( <video src controls /> );
expect( result ).toBe( '<video controls></video>' );
} );
} );
} );
describe( 'hasPrefix()', () => {
it( 'returns true if prefixed', () => {
const result = hasPrefix( 'Hello World', [ 'baz', 'Hello' ] );
expect( result ).toBe( true );
} );
it( 'returns false if not contains', () => {
const result = hasPrefix( 'World', [ 'Hello' ] );
expect( result ).toBe( false );
} );
it( 'returns false if contains but not prefix', () => {
const result = hasPrefix( 'World Hello', [ 'Hello' ] );
expect( result ).toBe( false );
} );
} );
describe( 'renderElement()', () => {
it( 'renders empty content as empty string', () => {
[ null, undefined, false ].forEach( ( element ) => {
const result = renderElement( element );
expect( result ).toBe( '' );
} );
} );
it( 'renders an array of mixed content', () => {
const result = renderElement( [ 'hello', <div key="div" /> ] );
expect( result ).toBe( 'hello<div></div>' );
} );
it( 'SVG attributes with dashes should be rendered as such - even with wrong casing', () => {
const result = renderElement(
<svg>
<rect x="0" y="0" strokeWidth="5" STROKELinejoin="miter"></rect>
</svg>
);
expect( result ).toBe(
'<svg><rect x="0" y="0" stroke-width="5" stroke-linejoin="miter"></rect></svg>'
);
} );
it( 'Case sensitive attributes should have the right casing - even with wrong casing', () => {
const result = renderElement(
<svg ViEWBOx="0 0 1 1" preserveAsPECTRatio="slice"></svg>
);
expect( result ).toBe(
'<svg viewBox="0 0 1 1" preserveAspectRatio="slice"></svg>'
);
} );
it( 'SVG attributes with colons should be rendered as such - even with wrong casing', () => {
const result = renderElement(
<svg
viewBox="0 0 1 1"
XLINKROLE="some-role"
xlinkShow="hello"
></svg>
);
expect( result ).toBe(
'<svg viewBox="0 0 1 1" xlink:role="some-role" xlink:show="hello"></svg>'
);
} );
it( 'renders escaped string element', () => {
const result = renderElement( 'hello & world &amp; friends <img/>' );
expect( result ).toBe( 'hello &amp; world &amp; friends &lt;img/>' );
} );
it( 'renders numeric element as string', () => {
const result = renderElement( 10 );
expect( result ).toBe( '10' );
} );
it( 'renders native component', () => {
const result = renderElement( <div className="greeting">Hello</div> );
expect( result ).toBe( '<div class="greeting">Hello</div>' );
} );
it( 'renders function component', () => {
function Greeting() {
return <div className="greeting">Hello</div>;
}
const result = renderElement( <Greeting /> );
expect( result ).toBe( '<div class="greeting">Hello</div>' );
} );
it( 'renders class component', () => {
class Greeting extends Component {
render() {
return <div className="greeting">Hello</div>;
}
}
const result = renderElement( <Greeting /> );
expect( result ).toBe( '<div class="greeting">Hello</div>' );
} );
it( 'renders empty string for indeterminite types', () => {
const result = renderElement( {} );
expect( result ).toBe( '' );
} );
it( 'renders Fragment as its inner children', () => {
const result = renderElement( <Fragment>Hello</Fragment> );
expect( result ).toBe( 'Hello' );
} );
it( 'renders StrictMode with undefined children', () => {
const result = renderElement( <StrictMode /> );
expect( result ).toBe( '' );
} );
it( 'renders StrictMode as its inner children', () => {
const result = renderElement( <StrictMode>Hello</StrictMode> );
expect( result ).toBe( 'Hello' );
} );
it( 'renders Fragment with undefined children', () => {
const result = renderElement( <Fragment /> );
expect( result ).toBe( '' );
} );
it( 'renders default value from Context API', () => {
const { Consumer } = createContext( {
value: 'default',
} );
const result = renderElement(
<Consumer>{ ( context ) => context.value }</Consumer>
);
expect( result ).toBe( 'default' );
} );
it( 'renders provided value through Context API', () => {
const { Consumer, Provider } = createContext( {
value: 'default',
} );
const result = renderElement(
<Provider value={ { value: 'provided' } }>
<Consumer>{ ( context ) => context.value }</Consumer>
</Provider>
);
expect( result ).toBe( 'provided' );
} );
it( 'renders proper value through Context API when multiple providers present', () => {
const { Consumer, Provider } = createContext( {
value: 'default',
} );
const result = renderElement(
<Fragment>
<Provider value={ { value: '1st provided' } }>
<Consumer>{ ( context ) => context.value }</Consumer>
</Provider>
{ '|' }
<Provider value={ { value: '2nd provided' } }>
<Consumer>{ ( context ) => context.value }</Consumer>
</Provider>
{ '|' }
<Consumer>{ ( context ) => context.value }</Consumer>
</Fragment>
);
expect( result ).toBe( '1st provided|2nd provided|default' );
} );
it( 'renders proper value through Context API when nested providers present', () => {
const { Consumer, Provider } = createContext( {
value: 'default',
} );
const result = renderElement(
<Provider value={ { value: 'outer provided' } }>
<Provider value={ { value: 'inner provided' } }>
<Consumer>{ ( context ) => context.value }</Consumer>
</Provider>
{ '|' }
<Consumer>{ ( context ) => context.value }</Consumer>
</Provider>
);
expect( result ).toBe( 'inner provided|outer provided' );
} );
it( 'renders RawHTML as its unescaped children', () => {
const result = renderElement( <RawHTML>{ '<img/>' }</RawHTML> );
expect( result ).toBe( '<img/>' );
} );
it( 'renders RawHTML with wrapper if props passed', () => {
const result = renderElement(
<RawHTML className="foo">{ '<img/>' }</RawHTML>
);
expect( result ).toBe( '<div class="foo"><img/></div>' );
} );
it( 'renders RawHTML with empty children as empty string', () => {
const result = renderElement( <RawHTML /> );
expect( result ).toBe( '' );
} );
it( 'renders RawHTML with wrapper and empty children', () => {
const result = renderElement( <RawHTML className="foo" /> );
expect( result ).toBe( '<div class="foo"></div>' );
} );
} );
describe( 'renderNativeComponent()', () => {
describe( 'textarea', () => {
it( 'should render textarea value as its content', () => {
const result = renderNativeComponent( 'textarea', {
value: 'Hello',
children: [],
} );
expect( result ).toBe( '<textarea>Hello</textarea>' );
} );
it( 'should render textarea children as its content', () => {
const result = renderNativeComponent( 'textarea', {
children: [ 'Hello' ],
} );
expect( result ).toBe( '<textarea>Hello</textarea>' );
} );
} );
describe( 'escaping', () => {
it( 'should escape children', () => {
const result = renderNativeComponent( 'div', {
children: [ '<img/>' ],
} );
expect( result ).toBe( '<div>&lt;img/></div>' );
} );
it( 'should not render invalid dangerouslySetInnerHTML', () => {
const result = renderNativeComponent( 'div', {
dangerouslySetInnerHTML: { __html: undefined },
} );
expect( result ).toBe( '<div></div>' );
} );
it( 'should not escape children with dangerouslySetInnerHTML', () => {
const result = renderNativeComponent( 'div', {
dangerouslySetInnerHTML: { __html: '<img/>' },
} );
expect( result ).toBe( '<div><img/></div>' );
} );
} );
describe( 'self-closing', () => {
it( 'should render self-closing elements', () => {
const result = renderNativeComponent( 'img', { src: 'foo.png' } );
expect( result ).toBe( '<img src="foo.png"/>' );
} );
it( 'should ignore self-closing elements children', () => {
const result = renderNativeComponent( 'img', {
src: 'foo.png',
children: [ 'Surprise!' ],
} );
expect( result ).toBe( '<img src="foo.png"/>' );
} );
} );
describe( 'with children', () => {
it( 'should render single literal child', () => {
const result = renderNativeComponent( 'div', {
children: 'Hello',
} );
expect( result ).toBe( '<div>Hello</div>' );
} );
it( 'should render array of children', () => {
const result = renderNativeComponent( 'div', {
children: [ 'Hello ', <Fragment key="toWhom">World</Fragment> ],
} );
expect( result ).toBe( '<div>Hello World</div>' );
} );
} );
} );
describe( 'renderComponent()', () => {
it( 'calls constructor', () => {
class Example extends Component {
constructor() {
super( ...arguments );
this.constructed = 'constructed';
}
render() {
return this.constructed;
}
}
const result = renderComponent( Example, {} );
expect( result ).toBe( 'constructed' );
} );
it( 'does not call componentDidMount', () => {
class Example extends Component {
constructor() {
super( ...arguments );
this.state = {};
}
componentDidMount() {
this.setState( { didMounted: 'didMounted' } );
}
render() {
return this.state.didMounted;
}
}
const result = renderComponent( Example, {} );
expect( result ).toBe( '' );
} );
} );
describe( 'renderAttributes()', () => {
describe( 'boolean attributes', () => {
it( 'should return boolean attributes false as omitted', () => {
const result = renderAttributes( { controls: false } );
expect( result ).toBe( '' );
} );
it( 'should return boolean attributes non-false as present', () => {
[ true, 'true', 'false', '' ].forEach( ( controls ) => {
const result = renderAttributes( { controls } );
expect( result ).toBe( ' controls' );
} );
} );
it( 'should consider normalized boolean attribute name', () => {
const result = renderAttributes( { allowFullscreen: true } );
expect( result ).toBe( ' allowfullscreen' );
} );
} );
describe( 'prefixed attributes', () => {
it( 'should not render if nullish', () => {
[ null, undefined ].forEach( ( value ) => {
const result = renderAttributes( { 'data-foo': value } );
expect( result ).toBe( '' );
} );
} );
it( 'should return in its string form unmodified', () => {
let result = renderAttributes( {
'aria-hidden': '',
} );
expect( result ).toBe( ' aria-hidden=""' );
result = renderAttributes( {
'aria-hidden': true,
} );
expect( result ).toBe( ' aria-hidden="true"' );
result = renderAttributes( {
'aria-hidden': false,
} );
expect( result ).toBe( ' aria-hidden="false"' );
} );
} );
describe( 'normalized attribute names', () => {
it( 'should return with normal attribute names', () => {
const result = renderAttributes( {
htmlFor: 'foo',
className: 'bar',
contentEditable: true,
} );
expect( result ).toBe(
' for="foo" class="bar" contenteditable="true"'
);
} );
} );
describe( 'string escaping', () => {
it( 'should escape string attributes', () => {
const result = renderAttributes( {
style: {
background: 'url("foo.png")',
},
href: '/index.php?foo=bar&qux=<"scary">',
} );
expect( result ).toBe(
' style="background:url(&quot;foo.png&quot;)" href="/index.php?foo=bar&amp;qux=<&quot;scary&quot;&gt;"'
);
} );
it( 'should render numeric attributes', () => {
const result = renderAttributes( {
size: 10,
} );
expect( result ).toBe( ' size="10"' );
} );
} );
describe( 'ignored attributes', () => {
it( 'does not render nullish attributes', () => {
const result = renderAttributes( {
className: null,
htmlFor: undefined,
} );
expect( result ).toBe( '' );
} );
it( 'does not render attributes of invalid types', () => {
const result = renderAttributes( {
onClick: () => {},
className: [],
} );
expect( result ).toBe( '' );
} );
it( 'does not render internal attributes', () => {
const result = renderAttributes( {
key: 'foo',
children: [ 'hello' ],
} );
expect( result ).toBe( '' );
} );
} );
} );
describe( 'renderStyle()', () => {
it( 'should return string verbatim', () => {
const result = renderStyle( 'color:red' );
expect( result ).toBe( 'color:red' );
} );
it( 'should return undefined if empty', () => {
const result = renderStyle( {} );
expect( result ).toBe( undefined );
} );
it( 'should render without trailing semi-colon', () => {
const result = renderStyle( {
color: 'red',
} );
expect( result ).toBe( 'color:red' );
} );
it( 'should not render nullish value', () => {
const result = renderStyle( {
border: null,
backgroundColor: undefined,
color: 'red',
} );
expect( result ).toBe( 'color:red' );
} );
it( 'should render a semi-colon delimited set', () => {
const result = renderStyle( {
color: 'red',
border: '1px dotted green',
} );
expect( result ).toBe( 'color:red;border:1px dotted green' );
} );
it( 'should kebab-case style properties', () => {
const result = renderStyle( {
color: 'red',
backgroundColor: 'green',
} );
expect( result ).toBe( 'color:red;background-color:green' );
} );
it( 'should not kebab-case custom properties', () => {
const result = renderStyle( {
'--myBackgroundColor': 'palegoldenrod',
} );
expect( result ).toBe( '--myBackgroundColor:palegoldenrod' );
} );
it( 'should -kebab-case style properties with a vendor prefix', () => {
const result = renderStyle( {
msTransform: 'none',
OTransform: 'none',
MozTransform: 'none',
WebkitTransform: 'none',
} );
expect( result ).toBe(
'-ms-transform:none;-o-transform:none;-moz-transform:none;-webkit-transform:none'
);
} );
describe( 'value unit', () => {
it( 'should not render zero unit', () => {
const result = renderStyle( {
borderWidth: 0,
} );
expect( result ).toBe( 'border-width:0' );
} );
it( 'should render numeric units', () => {
const result = renderStyle( {
borderWidth: 10,
} );
expect( result ).toBe( 'border-width:10px' );
} );
it( 'should not render numeric units for unitless properties', () => {
const result = renderStyle( {
order: 10,
} );
expect( result ).toBe( 'order:10' );
} );
} );
} );
/* eslint-enable testing-library/render-result-naming-convention */

23
node_modules/@wordpress/element/src/test/utils.js generated vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import { createElement } from '../react';
import { isEmptyElement } from '../utils';
describe( 'isEmptyElement', () => {
test( 'should be empty', () => {
expect( isEmptyElement( undefined ) ).toBe( true );
expect( isEmptyElement( false ) ).toBe( true );
expect( isEmptyElement( '' ) ).toBe( true );
expect( isEmptyElement( new String( '' ) ) ).toBe( true );
expect( isEmptyElement( [] ) ).toBe( true );
} );
test( 'should not be empty', () => {
expect( isEmptyElement( 0 ) ).toBe( false );
expect( isEmptyElement( 100 ) ).toBe( false );
expect( isEmptyElement( 'test' ) ).toBe( false );
expect( isEmptyElement( createElement( 'div' ) ) ).toBe( false );
expect( isEmptyElement( [ 'x' ] ) ).toBe( false );
} );
} );

17
node_modules/@wordpress/element/src/utils.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Checks if the provided WP element is empty.
*
* @param {*} element WP element to check.
* @return {boolean} True when an element is considered empty.
*/
export const isEmptyElement = ( element ) => {
if ( typeof element === 'number' ) {
return false;
}
if ( typeof element?.valueOf() === 'string' || Array.isArray( element ) ) {
return ! element.length;
}
return ! element;
};