Skip to content

Commit

Permalink
Render SVG props that have dashes correctly (#38936)
Browse files Browse the repository at this point in the history
* Add failing test to highlight the issue.

* Serialize dashed SVG props properly

* Fix panose-1 prop casing

* Add failing tests for more attribute types

1. For case-sensitive attributes (e.g: `viewBox`).
2. For attributes with colons (e.g: `xlink:show`).
3. Badly-cased attributes like VIEWBox.

* Render more attribute types correctly:

1. Case-sensitive attributes (e.g: `viewBox`).
2. attributes with colons (e.g: `xlink:show`).
3. Badly-cased attributes (e.g: `VIEWBox`).
  • Loading branch information
alshakerM committed Mar 1, 2022
1 parent 98048bf commit 00d164b
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 1 deletion.
197 changes: 196 additions & 1 deletion packages/element/src/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,190 @@ function getNormalAttributeValue( attribute, 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.
Expand All @@ -297,8 +481,19 @@ function getNormalAttributeName( attribute ) {
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 attribute.toLowerCase();
return attributeLowerCase;
}

/**
Expand Down
36 changes: 36 additions & 0 deletions packages/element/src/test/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,42 @@ describe( 'renderElement()', () => {
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/>' );

Expand Down

0 comments on commit 00d164b

Please sign in to comment.