Refactor DOM Bindings Completely Off of DOMProperty Meta Programming (#26546)

There are four places we have special cases based off the DOMProperty
config:

1) DEV-only: ReactDOMUnknownPropertyHook warns for passing booleans to
non-boolean attributes. We just need a simple list of all properties
that are affected by that. We could probably move this in under setProp
instead and have it covered by that list.
2) DEV-only: Hydration. This just needs to read the value from an
attribute and compare it to what we'd expect to see if it was rendered
on the client. This could use some simplification/unification of the
code but I decided to just keep it simple and duplicated since code size
isn't an issue.
3) DOMServerFormatConfig pushAttribute: This just maps the special case
to how to emit it as a HTML attribute.
4) ReactDOMComponent setProp: This just maps the special case to how to
emit it as setAttribute or removeAttribute.

Basically we just have to remember to keep pushAttribute and setProp
aligned. There's only one long switch in prod per environment.

This just turns it all to a giant simple switch statement with string
cases. This is in theory the most optimizable since syntactically all
the information for a hash table is there. However, unfortunately we
know that most VMs don't optimize this very well and instead just turn
them into a bunch of ifs. JSC is best. We can minimize the cost by just
moving common attribute to the beginning of the list.

If we shipped this, maybe VMs will get it together to start optimizing
this case but there's a chicken and egg problem here and the game theory
reality is that we probably don't want to regress. Therefore, I intend
to do a follow up after landing this which reintroduces an object
indirection for simple property aliases. That should be enough to make
the remaining cases palatable. I'll also extract the most common
attributes to the beginning or separate ifs.

Ran attribute-behavior fixture and the table is the same.
This commit is contained in:
Sebastian Markbåge
2023-04-04 11:05:56 -04:00
committed by GitHub
parent 0ba4d7b0d8
commit eeabb7312f
7 changed files with 2611 additions and 1014 deletions
@@ -72,6 +72,21 @@ export function createDangerousStringForStyles(styles) {
* @param {object} styles
*/
export function setValueForStyles(node, styles) {
if (styles != null && typeof styles !== 'object') {
throw new Error(
'The `style` prop expects a mapping from style properties to values, ' +
"not a string. For example, style={{marginRight: spacing + 'em'}} when " +
'using JSX.',
);
}
if (__DEV__) {
if (styles) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(styles);
}
}
const style = node.style;
for (const styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
@@ -7,181 +7,14 @@
* @flow
*/
import {
BOOLEAN,
OVERLOADED_BOOLEAN,
NUMERIC,
POSITIVE_NUMERIC,
} from '../shared/DOMProperty';
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import sanitizeURL from '../shared/sanitizeURL';
import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableFilterEmptyStringAttributesDOM,
} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import type {PropertyInfo} from '../shared/DOMProperty';
/**
* Get the value for a property on a node. Only used in DEV for SSR validation.
* The "expected" argument is used as a hint of what the expected value is.
* Some properties have multiple equivalent values.
*/
export function getValueForProperty(
node: Element,
name: string,
expected: mixed,
propertyInfo: PropertyInfo,
): mixed {
if (__DEV__) {
const attributeName = propertyInfo.attributeName;
if (!node.hasAttribute(attributeName)) {
// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return expected;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
return expected;
}
}
}
switch (propertyInfo.type) {
case BOOLEAN: {
if (!expected) {
return expected;
}
break;
}
case OVERLOADED_BOOLEAN: {
if (expected === false) {
return expected;
}
break;
}
case NUMERIC: {
if (isNaN(expected)) {
return expected;
}
break;
}
case POSITIVE_NUMERIC: {
if (isNaN(expected) || (expected: any) < 1) {
return expected;
}
break;
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && expected === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
return expected;
}
}
return expected === undefined ? undefined : null;
}
// Even if this property uses a namespace we use getAttribute
// because we assume its namespaced name is the same as our config.
// To use getAttributeNS we need the local name which we don't have
// in our config atm.
const value = node.getAttribute(attributeName);
if (expected == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return value;
}
switch (propertyInfo.type) {
case BOOLEAN: {
if (expected) {
// If this was a boolean, it doesn't matter what the value is
// the fact that we have it is the same as the expected.
// As long as it's positive.
return expected;
}
return value;
}
case OVERLOADED_BOOLEAN: {
if (value === '') {
return true;
}
if (expected === false) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
case NUMERIC: {
if (isNaN(expected)) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
case POSITIVE_NUMERIC: {
if (isNaN(expected) || (expected: any) < 1) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return value;
}
break;
}
}
if (__DEV__) {
checkAttributeStringCoercion(expected, name);
}
if (propertyInfo.sanitizeURL) {
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (sanitizeURL(expected): any)) {
return expected;
}
return value;
}
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (expected: any)) {
return expected;
}
return value;
}
}
/**
* Get the value for a attribute on a node. Only used in DEV for SSR validation.
* The third argument is used as a hint of what the expected value is. Some
@@ -271,138 +104,6 @@ export function getValueForAttributeOnCustomComponent(
}
}
/**
* Sets the value for a property on a node.
*
* @param {DOMElement} node
* @param {string} name
* @param {*} value
*/
export function setValueForProperty(
node: Element,
propertyInfo: PropertyInfo,
value: mixed,
) {
const attributeName = propertyInfo.attributeName;
if (value === null) {
node.removeAttribute(attributeName);
return;
}
// shouldRemoveAttribute
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol': // eslint-disable-line
node.removeAttribute(attributeName);
return;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
node.removeAttribute(attributeName);
return;
}
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === '') {
if (__DEV__) {
if (attributeName === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
attributeName,
attributeName,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
attributeName,
attributeName,
);
}
}
node.removeAttribute(attributeName);
return;
}
}
switch (propertyInfo.type) {
case BOOLEAN:
if (value) {
node.setAttribute(attributeName, '');
} else {
node.removeAttribute(attributeName);
return;
}
break;
case OVERLOADED_BOOLEAN:
if (value === true) {
node.setAttribute(attributeName, '');
} else if (value === false) {
node.removeAttribute(attributeName);
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
}
return;
case NUMERIC:
if (!isNaN(value)) {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
} else {
node.removeAttribute(attributeName);
}
break;
case POSITIVE_NUMERIC:
if (!isNaN(value) && (value: any) >= 1) {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
node.setAttribute(attributeName, (value: any));
} else {
node.removeAttribute(attributeName);
}
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
let attributeValue;
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
if (enableTrustedTypesIntegration) {
if (propertyInfo.sanitizeURL) {
attributeValue = (sanitizeURL(value): any);
} else {
attributeValue = (value: any);
}
} else {
// We have already verified this above.
// eslint-disable-next-line react-internal/safe-string-coercion
attributeValue = '' + (value: any);
if (propertyInfo.sanitizeURL) {
attributeValue = sanitizeURL(attributeValue);
}
}
const attributeNamespace = propertyInfo.attributeNamespace;
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}
}
export function setValueForAttribute(
node: Element,
name: string,
@@ -439,6 +140,35 @@ export function setValueForAttribute(
}
}
export function setValueForNamespacedAttribute(
node: Element,
namespace: string,
name: string,
value: mixed,
) {
if (value === null) {
node.removeAttribute(name);
return;
}
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean': {
node.removeAttribute(name);
return;
}
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
node.setAttributeNS(
namespace,
name,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}
export function setValueForPropertyOnCustomComponent(
node: Element,
name: string,
File diff suppressed because it is too large Load Diff
@@ -39,13 +39,6 @@ import {
} from 'react-server/src/ReactServerStreamConfig';
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import {
getPropertyInfo,
BOOLEAN,
OVERLOADED_BOOLEAN,
NUMERIC,
POSITIVE_NUMERIC,
} from '../shared/DOMProperty';
import isUnitlessNumber from '../shared/isUnitlessNumber';
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
@@ -621,6 +614,26 @@ function pushBooleanAttribute(
}
}
function pushStringAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
value: string | boolean | number | Function | Object, // not null or undefined
): void {
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
typeof value !== 'boolean'
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
}
function pushAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
@@ -638,151 +651,505 @@ function pushAttribute(
case 'suppressHydrationWarning':
// Ignored. These are built-in to React on the client.
return;
case 'autoFocus':
case 'multiple':
case 'muted':
pushBooleanAttribute(target, name, value);
case 'muted': {
pushBooleanAttribute(target, name.toLowerCase(), value);
return;
}
if (
// shouldIgnoreAttribute
// We have already filtered out null/undefined and reserved words.
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}
const propertyInfo = getPropertyInfo(name);
if (propertyInfo !== null) {
// shouldRemoveAttribute
switch (typeof value) {
case 'function':
case 'symbol': // eslint-disable-line
return;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
}
case 'src':
case 'href':
case 'action':
if (enableFilterEmptyStringAttributesDOM) {
if (value === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
return;
}
}
}
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
// Fall through to the last case which shouldn't remove empty strings.
// eslint-disable-next-line no-fallthrough
case 'formAction': {
if (
value == null ||
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
const sanitizedValue = sanitizeURL('' + value);
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(sanitizedValue)),
attributeEnd,
);
return;
}
const attributeName = propertyInfo.attributeName;
const attributeNameChunk = stringToChunk(attributeName); // TODO: If it's known we can cache the chunk.
switch (propertyInfo.type) {
case BOOLEAN:
if (value) {
target.push(
attributeSeparator,
attributeNameChunk,
attributeEmptyString,
);
}
case 'xlinkHref': {
if (
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
return;
case OVERLOADED_BOOLEAN:
if (value === true) {
target.push(
attributeSeparator,
attributeNameChunk,
attributeEmptyString,
);
} else if (value === false) {
// Ignored
} else {
target.push(
attributeSeparator,
attributeNameChunk,
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
case NUMERIC:
if (!isNaN(value)) {
target.push(
attributeSeparator,
attributeNameChunk,
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
break;
case POSITIVE_NUMERIC:
if (!isNaN(value) && (value: any) >= 1) {
target.push(
attributeSeparator,
attributeNameChunk,
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
break;
default:
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
if (propertyInfo.sanitizeURL) {
// We've already checked above.
// eslint-disable-next-line react-internal/safe-string-coercion
value = sanitizeURL('' + (value: any));
}
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
const sanitizedValue = sanitizeURL('' + value);
target.push(
attributeSeparator,
stringToChunk('xlink:href'),
attributeAssign,
stringToChunk(escapeTextForBrowser(sanitizedValue)),
attributeEnd,
);
return;
}
case 'contentEditable':
case 'spellCheck':
case 'draggable':
case 'value':
case 'autoReverse':
case 'externalResourcesRequired':
case 'focusable':
case 'preserveAlpha': {
// Booleanish String
// These are "enumerated" attributes that accept "true" and "false".
// In React, we let users pass `true` and `false` even though technically
// these aren't boolean attributes (they are coerced to strings).
if (typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
attributeNameChunk,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
} else if (isAttributeNameSafe(name)) {
// shouldRemoveAttribute
switch (typeof value) {
case 'function':
case 'symbol': // eslint-disable-line
return;
case 'boolean': {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix !== 'data-' && prefix !== 'aria-') {
return;
}
}
return;
}
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope': {
// Boolean
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
stringToChunk(name),
attributeEmptyString,
);
}
return;
}
case 'capture':
case 'download': {
// Overloaded Boolean
if (value === true) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeEmptyString,
);
} else if (value === false) {
// Ignored
} else if (typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'cols':
case 'rows':
case 'size':
case 'span': {
// These are HTML attributes that must be positive numbers.
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value) &&
(value: any) >= 1
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'rowSpan':
case 'start': {
// These are HTML attributes that must be numbers.
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value)
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
// A few React string attributes have a different name.
// This is a mapping from React prop names to the attribute names.
case 'acceptCharset':
pushStringAttribute(target, 'accept-charset', value);
return;
case 'className':
pushStringAttribute(target, 'class', value);
return;
case 'htmlFor':
pushStringAttribute(target, 'for', value);
return;
case 'httpEquiv':
pushStringAttribute(target, 'http-equiv', value);
return;
// HTML and SVG attributes, but the SVG attribute is case sensitive.
case 'tabIndex':
pushStringAttribute(target, 'tabindex', value);
return;
case 'crossOrigin':
pushStringAttribute(target, 'crossorigin', value);
return;
// This is a list of all SVG attributes that need special casing.
// Regular attributes that just accept strings.
case 'accentHeight':
pushStringAttribute(target, 'accent-height', value);
return;
case 'alignmentBaseline':
pushStringAttribute(target, 'alignment-baseline', value);
return;
case 'arabicForm':
pushStringAttribute(target, 'arabic-form', value);
return;
case 'baselineShift':
pushStringAttribute(target, 'baseline-shift', value);
return;
case 'capHeight':
pushStringAttribute(target, 'cap-height', value);
return;
case 'clipPath':
pushStringAttribute(target, 'clip-path', value);
return;
case 'clipRule':
pushStringAttribute(target, 'clip-rule', value);
return;
case 'colorInterpolation':
pushStringAttribute(target, 'color-interpolation', value);
return;
case 'colorInterpolationFilters':
pushStringAttribute(target, 'color-interpolation-filters', value);
return;
case 'colorProfile':
pushStringAttribute(target, 'color-profile', value);
return;
case 'colorRendering':
pushStringAttribute(target, 'color-rendering', value);
return;
case 'dominantBaseline':
pushStringAttribute(target, 'dominant-baseline', value);
return;
case 'enableBackground':
pushStringAttribute(target, 'enable-background', value);
return;
case 'fillOpacity':
pushStringAttribute(target, 'fill-opacity', value);
return;
case 'fillRule':
pushStringAttribute(target, 'fill-rule', value);
return;
case 'floodColor':
pushStringAttribute(target, 'flood-color', value);
return;
case 'floodOpacity':
pushStringAttribute(target, 'flood-opacity', value);
return;
case 'fontFamily':
pushStringAttribute(target, 'font-family', value);
return;
case 'fontSize':
pushStringAttribute(target, 'font-size', value);
return;
case 'fontSizeAdjust':
pushStringAttribute(target, 'font-size-adjust', value);
return;
case 'fontStretch':
pushStringAttribute(target, 'font-stretch', value);
return;
case 'fontStyle':
pushStringAttribute(target, 'font-style', value);
return;
case 'fontVariant':
pushStringAttribute(target, 'font-variant', value);
return;
case 'fontWeight':
pushStringAttribute(target, 'font-weight', value);
return;
case 'glyphName':
pushStringAttribute(target, 'glyph-name', value);
return;
case 'glyphOrientationHorizontal':
pushStringAttribute(target, 'glyph-orientation-horizontal', value);
return;
case 'glyphOrientationVertical':
pushStringAttribute(target, 'glyph-orientation-vertical', value);
return;
case 'horizAdvX':
pushStringAttribute(target, 'horiz-adv-x', value);
return;
case 'horizOriginX':
pushStringAttribute(target, 'horiz-origin-x', value);
return;
case 'imageRendering':
pushStringAttribute(target, 'image-rendering', value);
return;
case 'letterSpacing':
pushStringAttribute(target, 'letter-spacing', value);
return;
case 'lightingColor':
pushStringAttribute(target, 'lighting-color', value);
return;
case 'markerEnd':
pushStringAttribute(target, 'marker-end', value);
return;
case 'markerMid':
pushStringAttribute(target, 'marker-mid', value);
return;
case 'markerStart':
pushStringAttribute(target, 'marker-start', value);
return;
case 'overlinePosition':
pushStringAttribute(target, 'overline-position', value);
return;
case 'overlineThickness':
pushStringAttribute(target, 'overline-thickness', value);
return;
case 'paintOrder':
pushStringAttribute(target, 'paint-order', value);
return;
case 'panose-1':
pushStringAttribute(target, 'panose-1', value);
return;
case 'pointerEvents':
pushStringAttribute(target, 'pointer-events', value);
return;
case 'renderingIntent':
pushStringAttribute(target, 'rendering-intent', value);
return;
case 'shapeRendering':
pushStringAttribute(target, 'shape-rendering', value);
return;
case 'stopColor':
pushStringAttribute(target, 'stop-color', value);
return;
case 'stopOpacity':
pushStringAttribute(target, 'stop-opacity', value);
return;
case 'strikethroughPosition':
pushStringAttribute(target, 'strikethrough-position', value);
return;
case 'strikethroughThickness':
pushStringAttribute(target, 'strikethrough-thickness', value);
return;
case 'strokeDasharray':
pushStringAttribute(target, 'stroke-dasharray', value);
return;
case 'strokeDashoffset':
pushStringAttribute(target, 'stroke-dashoffset', value);
return;
case 'strokeLinecap':
pushStringAttribute(target, 'stroke-linecap', value);
return;
case 'strokeLinejoin':
pushStringAttribute(target, 'stroke-linejoin', value);
return;
case 'strokeMiterlimit':
pushStringAttribute(target, 'stroke-miterlimit', value);
return;
case 'strokeOpacity':
pushStringAttribute(target, 'stroke-opacity', value);
return;
case 'strokeWidth':
pushStringAttribute(target, 'stroke-width', value);
return;
case 'textAnchor':
pushStringAttribute(target, 'text-anchor', value);
return;
case 'textDecoration':
pushStringAttribute(target, 'text-decoration', value);
return;
case 'textRendering':
pushStringAttribute(target, 'text-rendering', value);
return;
case 'transformOrigin':
pushStringAttribute(target, 'transform-origin', value);
return;
case 'underlinePosition':
pushStringAttribute(target, 'underline-position', value);
return;
case 'underlineThickness':
pushStringAttribute(target, 'underline-thickness', value);
return;
case 'unicodeBidi':
pushStringAttribute(target, 'unicode-bidi', value);
return;
case 'unicodeRange':
pushStringAttribute(target, 'unicode-range', value);
return;
case 'unitsPerEm':
pushStringAttribute(target, 'units-per-em', value);
return;
case 'vAlphabetic':
pushStringAttribute(target, 'v-alphabetic', value);
return;
case 'vHanging':
pushStringAttribute(target, 'v-hanging', value);
return;
case 'vIdeographic':
pushStringAttribute(target, 'v-ideographic', value);
return;
case 'vMathematical':
pushStringAttribute(target, 'v-mathematical', value);
return;
case 'vectorEffect':
pushStringAttribute(target, 'vector-effect', value);
return;
case 'vertAdvY':
pushStringAttribute(target, 'vert-adv-y', value);
return;
case 'vertOriginX':
pushStringAttribute(target, 'vert-origin-x', value);
return;
case 'vertOriginY':
pushStringAttribute(target, 'vert-origin-y', value);
return;
case 'wordSpacing':
pushStringAttribute(target, 'word-spacing', value);
return;
case 'writingMode':
pushStringAttribute(target, 'writing-mode', value);
return;
case 'xmlnsXlink':
pushStringAttribute(target, 'xmlns:xlink', value);
return;
case 'xHeight':
pushStringAttribute(target, 'x-height', value);
return;
case 'xlinkActuate':
pushStringAttribute(target, 'xlink:actuate', value);
break;
case 'xlinkArcrole':
pushStringAttribute(target, 'xlink:arcrole', value);
break;
case 'xlinkRole':
pushStringAttribute(target, 'xlink:role', value);
break;
case 'xlinkShow':
pushStringAttribute(target, 'xlink:show', value);
break;
case 'xlinkTitle':
pushStringAttribute(target, 'xlink:title', value);
break;
case 'xlinkType':
pushStringAttribute(target, 'xlink:type', value);
break;
case 'xmlBase':
pushStringAttribute(target, 'xml:base', value);
break;
case 'xmlLang':
pushStringAttribute(target, 'xml:lang', value);
break;
case 'xmlSpace':
pushStringAttribute(target, 'xml:space', value);
break;
default:
if (
// shouldIgnoreAttribute
// We have already filtered out null/undefined and reserved words.
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}
if (isAttributeNameSafe(name)) {
// shouldRemoveAttribute
switch (typeof value) {
case 'function':
case 'symbol': // eslint-disable-line
return;
case 'boolean': {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix !== 'data-' && prefix !== 'aria-') {
return;
}
}
}
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
}
}
-411
View File
@@ -1,411 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
// A simple string attribute.
// Attributes that aren't in the filter are presumed to have this type.
export const STRING = 1;
// A string attribute that accepts booleans in React. In HTML, these are called
// "enumerated" attributes with "true" and "false" as possible values.
// When true, it should be set to a "true" string.
// When false, it should be set to a "false" string.
export const BOOLEANISH_STRING = 2;
// A real boolean attribute.
// When true, it should be present (set either to an empty string or its name).
// When false, it should be omitted.
export const BOOLEAN = 3;
// An attribute that can be used as a flag as well as with a value.
// When true, it should be present (set either to an empty string or its name).
// When false, it should be omitted.
// For any other value, should be present with that value.
export const OVERLOADED_BOOLEAN = 4;
// An attribute that must be numeric or parse as a numeric.
// When falsy, it should be removed.
export const NUMERIC = 5;
// An attribute that must be positive numeric or parse as a positive numeric.
// When falsy, it should be removed.
export const POSITIVE_NUMERIC = 6;
export type PropertyInfo = {
+acceptsBooleans: boolean,
+attributeName: string,
+attributeNamespace: string | null,
+type: PropertyType,
+sanitizeURL: boolean,
+removeEmptyString: boolean,
};
export function getPropertyInfo(name: string): PropertyInfo | null {
return properties.hasOwnProperty(name) ? properties[name] : null;
}
// $FlowFixMe[missing-this-annot]
function PropertyInfoRecord(
type: PropertyType,
attributeName: string,
attributeNamespace: string | null,
sanitizeURL: boolean,
removeEmptyString: boolean,
) {
this.acceptsBooleans =
type === BOOLEANISH_STRING ||
type === BOOLEAN ||
type === OVERLOADED_BOOLEAN;
this.attributeName = attributeName;
this.attributeNamespace = attributeNamespace;
this.type = type;
this.sanitizeURL = sanitizeURL;
this.removeEmptyString = removeEmptyString;
}
// When adding attributes to this list, be sure to also add them to
// the `possibleStandardNames` module to ensure casing and incorrect
// name warnings.
const properties: {[string]: $FlowFixMe} = {};
// A few React string attributes have a different name.
// This is a mapping from React prop names to the attribute names.
[
['acceptCharset', 'accept-charset'],
['className', 'class'],
['htmlFor', 'for'],
['httpEquiv', 'http-equiv'],
].forEach(([name, attributeName]) => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
STRING,
attributeName, // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are "enumerated" HTML attributes that accept "true" and "false".
// In React, we let users pass `true` and `false` even though technically
// these aren't boolean attributes (they are coerced to strings).
['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
BOOLEANISH_STRING,
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are "enumerated" SVG attributes that accept "true" and "false".
// In React, we let users pass `true` and `false` even though technically
// these aren't boolean attributes (they are coerced to strings).
// Since these are SVG attributes, their attribute names are case-sensitive.
[
'autoReverse',
'externalResourcesRequired',
'focusable',
'preserveAlpha',
].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
BOOLEANISH_STRING,
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are HTML boolean attributes.
[
'allowFullScreen',
'async',
// Note: there is a special case that prevents it from being written to the DOM
// on the client side because the browsers are inconsistent. Instead we call focus().
'autoFocus',
'autoPlay',
'controls',
'default',
'defer',
'disabled',
'disablePictureInPicture',
'disableRemotePlayback',
'formNoValidate',
'hidden',
'loop',
'noModule',
'noValidate',
'open',
'playsInline',
'readOnly',
'required',
'reversed',
'scoped',
'seamless',
// Microdata
'itemScope',
].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
BOOLEAN,
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are HTML attributes that are "overloaded booleans": they behave like
// booleans, but can also accept a string value.
[
'capture',
'download',
// NOTE: if you add a camelCased prop to this list,
// you'll need to set attributeName to name.toLowerCase()
// instead in the assignment below.
].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
OVERLOADED_BOOLEAN,
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are HTML attributes that must be positive numbers.
[
'cols',
'rows',
'size',
'span',
// NOTE: if you add a camelCased prop to this list,
// you'll need to set attributeName to name.toLowerCase()
// instead in the assignment below.
].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
POSITIVE_NUMERIC,
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These are HTML attributes that must be numbers.
['rowSpan', 'start'].forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
NUMERIC,
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
const CAMELIZE = /[\-\:]([a-z])/g;
const capitalize = (token: string) => token[1].toUpperCase();
// This is a list of all SVG attributes that need special casing, namespacing,
// or boolean value assignment. Regular attributes that just accept strings
// and have the same names are omitted, just like in the HTML attribute filter.
// Some of these attributes can be hard to find. This list was created by
// scraping the MDN documentation.
[
'accent-height',
'alignment-baseline',
'arabic-form',
'baseline-shift',
'cap-height',
'clip-path',
'clip-rule',
'color-interpolation',
'color-interpolation-filters',
'color-profile',
'color-rendering',
'dominant-baseline',
'enable-background',
'fill-opacity',
'fill-rule',
'flood-color',
'flood-opacity',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-weight',
'glyph-name',
'glyph-orientation-horizontal',
'glyph-orientation-vertical',
'horiz-adv-x',
'horiz-origin-x',
'image-rendering',
'letter-spacing',
'lighting-color',
'marker-end',
'marker-mid',
'marker-start',
'overline-position',
'overline-thickness',
'paint-order',
'panose-1',
'pointer-events',
'rendering-intent',
'shape-rendering',
'stop-color',
'stop-opacity',
'strikethrough-position',
'strikethrough-thickness',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-linecap',
'stroke-linejoin',
'stroke-miterlimit',
'stroke-opacity',
'stroke-width',
'text-anchor',
'text-decoration',
'text-rendering',
'transform-origin',
'underline-position',
'underline-thickness',
'unicode-bidi',
'unicode-range',
'units-per-em',
'v-alphabetic',
'v-hanging',
'v-ideographic',
'v-mathematical',
'vector-effect',
'vert-adv-y',
'vert-origin-x',
'vert-origin-y',
'word-spacing',
'writing-mode',
'xmlns:xlink',
'x-height',
// NOTE: if you add a camelCased prop to this list,
// you'll need to set attributeName to name.toLowerCase()
// instead in the assignment below.
].forEach(attributeName => {
const name = attributeName.replace(CAMELIZE, capitalize);
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
STRING,
attributeName,
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// String SVG attributes with the xlink namespace.
[
'xlink:actuate',
'xlink:arcrole',
'xlink:role',
'xlink:show',
'xlink:title',
'xlink:type',
// NOTE: if you add a camelCased prop to this list,
// you'll need to set attributeName to name.toLowerCase()
// instead in the assignment below.
].forEach(attributeName => {
const name = attributeName.replace(CAMELIZE, capitalize);
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
STRING,
attributeName,
'http://www.w3.org/1999/xlink',
false, // sanitizeURL
false, // removeEmptyString
);
});
// String SVG attributes with the xml namespace.
[
'xml:base',
'xml:lang',
'xml:space',
// NOTE: if you add a camelCased prop to this list,
// you'll need to set attributeName to name.toLowerCase()
// instead in the assignment below.
].forEach(attributeName => {
const name = attributeName.replace(CAMELIZE, capitalize);
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
STRING,
attributeName,
'http://www.w3.org/XML/1998/namespace',
false, // sanitizeURL
false, // removeEmptyString
);
});
// These attribute exists both in HTML and SVG.
// The attribute name is case-sensitive in SVG so we can't just use
// the React name like we do for attributes that exist only in HTML.
['tabIndex', 'crossOrigin'].forEach(attributeName => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[attributeName] = new PropertyInfoRecord(
STRING,
attributeName.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// These attributes accept URLs. These must not allow javascript: URLS.
// These will also need to accept Trusted Types object in the future.
const xlinkHref = 'xlinkHref';
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[xlinkHref] = new PropertyInfoRecord(
STRING,
'xlink:href',
'http://www.w3.org/1999/xlink',
true, // sanitizeURL
false, // removeEmptyString
);
const formAction = 'formAction';
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[formAction] = new PropertyInfoRecord(
STRING,
'formaction', // attributeName
null, // attributeNamespace
true, // sanitizeURL
false, // removeEmptyString
);
['src', 'href', 'action'].forEach(attributeName => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[attributeName] = new PropertyInfoRecord(
STRING,
attributeName.toLowerCase(), // attributeName
null, // attributeNamespace
true, // sanitizeURL
true, // removeEmptyString
);
});
@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import {BOOLEAN, getPropertyInfo} from './DOMProperty';
import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
import isCustomElement from './isCustomElement';
import possibleStandardNames from './possibleStandardNames';
@@ -131,8 +130,6 @@ function validateProperty(tagName, name, value, eventRegistry) {
return true;
}
const propertyInfo = getPropertyInfo(name);
// Known attributes should match the casing specified in the property config.
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
const standardName = possibleStandardNames[lowerCasedName];
@@ -184,20 +181,49 @@ function validateProperty(tagName, name, value, eventRegistry) {
switch (typeof value) {
case 'boolean': {
switch (name) {
case 'autoFocus':
case 'checked':
case 'selected':
case 'multiple':
case 'muted': {
case 'muted':
case 'selected':
case 'contentEditable':
case 'spellCheck':
case 'draggable':
case 'value':
case 'autoReverse':
case 'externalResourcesRequired':
case 'focusable':
case 'preserveAlpha':
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope':
case 'capture':
case 'download': {
// Boolean properties can accept boolean values
return true;
}
default: {
if (propertyInfo === null) {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix === 'data-' || prefix === 'aria-') {
return true;
}
} else if (propertyInfo.acceptsBooleans) {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix === 'data-' || prefix === 'aria-') {
return true;
}
if (value) {
@@ -244,13 +270,33 @@ function validateProperty(tagName, name, value, eventRegistry) {
case 'checked':
case 'selected':
case 'multiple':
case 'muted': {
case 'muted':
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope': {
break;
}
default: {
if (propertyInfo === null || propertyInfo.type !== BOOLEAN) {
return true;
}
return true;
}
}
console.error(
@@ -339,7 +339,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', (
// The hydration validation calls it one extra time.
// TODO: It would be good if we only called toString once for
// consistency but the code structure makes that hard right now.
expectedToStringCalls = 5;
expectedToStringCalls = 4;
} else if (__DEV__) {
// Checking for string coercion problems results in double the
// toString calls in DEV