-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Mobile] - add CSS Unit parser to px (#34186)
* Add getPxFromCssUnit * Add support for css functions * Code clean up + adding support for lh and Q units * Fix numeric value return px value in calculation or not * Update packages/block-editor/src/utils/test/parse-css-unit-to-px.js Co-authored-by: Gerardo Pacheco <[email protected]> Co-authored-by: Gerardo Pacheco <[email protected]>
- Loading branch information
Showing
2 changed files
with
413 additions
and
0 deletions.
There are no files selected for viewing
230 changes: 230 additions & 0 deletions
230
packages/block-editor/src/utils/parse-css-unit-to-px.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
/** | ||
* Converts string to object { value, unit }. | ||
* | ||
* @param {string} cssUnit | ||
* @return {Object} parsedUnit | ||
*/ | ||
function parseUnit( cssUnit ) { | ||
const match = cssUnit | ||
?.trim() | ||
.match( | ||
/^(0?[-.]?\d+)(r?e[m|x]|v[h|w|min|max]+|p[x|t|c]|[c|m]m|%|in|ch|Q|lh)$/ | ||
); | ||
if ( ! isNaN( cssUnit ) && ! isNaN( parseFloat( cssUnit ) ) ) { | ||
return { value: parseFloat( cssUnit ), unit: 'px' }; | ||
} | ||
return match | ||
? { value: parseFloat( match[ 1 ] ) || match[ 1 ], unit: match[ 2 ] } | ||
: { value: cssUnit, unit: undefined }; | ||
} | ||
/** | ||
* Evaluate a math expression. | ||
* | ||
* @param {string} expression | ||
* @return {number} evaluated expression. | ||
*/ | ||
function calculate( expression ) { | ||
return Function( `'use strict'; return (${ expression })` )(); | ||
} | ||
|
||
/** | ||
* Calculates the css function value for the supported css functions such as max, min, clamp and calc. | ||
* | ||
* @param {string} functionUnitValue string should be in a particular format (for example min(12px,12px) ) no nested loops. | ||
* @param {Object} options | ||
* @return {string} unit containing the unit in PX. | ||
*/ | ||
function getFunctionUnitValue( functionUnitValue, options ) { | ||
const functionUnit = functionUnitValue.split( /[(),]/g ).filter( Boolean ); | ||
|
||
const units = functionUnit | ||
.slice( 1 ) | ||
.map( ( unit ) => parseUnit( getPxFromCssUnit( unit, options ) ).value ) | ||
.filter( Boolean ); | ||
|
||
switch ( functionUnit[ 0 ] ) { | ||
case 'min': | ||
return Math.min( ...units ) + 'px'; | ||
case 'max': | ||
return Math.max( ...units ) + 'px'; | ||
case 'clamp': | ||
if ( units.length !== 3 ) { | ||
return null; | ||
} | ||
if ( units[ 1 ] < units[ 0 ] ) { | ||
return units[ 0 ] + 'px'; | ||
} | ||
if ( units[ 1 ] > units[ 2 ] ) { | ||
return units[ 2 ] + 'px'; | ||
} | ||
return units[ 1 ] + 'px'; | ||
case 'calc': | ||
return units[ 0 ] + 'px'; | ||
} | ||
} | ||
|
||
/** | ||
* Take a css function such as min, max, calc, clamp and returns parsedUnit | ||
* | ||
* How this works for the nested function is that it first replaces the inner function call. | ||
* Then it tackles the outer onces. | ||
* So for example: min( max(25px, 35px), 40px ) | ||
* in the first pass we would replace max(25px, 35px) with 35px. | ||
* then we would try to evaluate min( 35px, 40px ) | ||
* and then finally return 35px. | ||
* | ||
* @param {string} cssUnit | ||
* @return {Object} parsedUnit object. | ||
*/ | ||
function parseUnitFunction( cssUnit ) { | ||
while ( true ) { | ||
const currentCssUnit = cssUnit; | ||
const regExp = /(max|min|calc|clamp)\(([^()]*)\)/g; | ||
const matches = regExp.exec( cssUnit ) || []; | ||
if ( matches[ 0 ] ) { | ||
const functionUnitValue = getFunctionUnitValue( matches[ 0 ] ); | ||
cssUnit = cssUnit.replace( matches[ 0 ], functionUnitValue ); | ||
} | ||
|
||
// if the unit hasn't been modified or we have a single value break free. | ||
if ( cssUnit === currentCssUnit || parseFloat( cssUnit ) ) { | ||
break; | ||
} | ||
} | ||
|
||
return parseUnit( cssUnit ); | ||
} | ||
/** | ||
* Return true if we think this is a math expression. | ||
* | ||
* @param {string} cssUnit the cssUnit value being evaluted. | ||
* @return {boolean} Whether the cssUnit is a math expression. | ||
*/ | ||
function isMathExpression( cssUnit ) { | ||
for ( let i = 0; i < cssUnit.length; i++ ) { | ||
if ( [ '+', '-', '/', '*' ].includes( cssUnit[ i ] ) ) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
* Evaluates the math expression and return a px value. | ||
* | ||
* @param {string} cssUnit the cssUnit value being evaluted. | ||
* @return {string} return a converfted value to px. | ||
*/ | ||
function evalMathExpression( cssUnit ) { | ||
let errorFound = false; | ||
// Convert every part of the expression to px values. | ||
const cssUnitsBits = cssUnit.split( /[+-/*/]/g ).filter( Boolean ); | ||
for ( const unit of cssUnitsBits ) { | ||
// Standardize the unit to px and extract the value. | ||
const parsedUnit = parseUnit( getPxFromCssUnit( unit ) ); | ||
if ( ! parseFloat( parsedUnit.value ) ) { | ||
errorFound = true; | ||
// end early since we are dealing with a null value. | ||
break; | ||
} | ||
cssUnit = cssUnit.replace( unit, parsedUnit.value ); | ||
} | ||
|
||
return errorFound ? null : calculate( cssUnit ).toFixed( 0 ) + 'px'; | ||
} | ||
/** | ||
* Convert a parsedUnit object to px value. | ||
* | ||
* @param {Object} parsedUnit | ||
* @param {Object} options | ||
* @return {string} or {null} returns the converted with in a px value format. | ||
*/ | ||
function convertParsedUnitToPx( parsedUnit, options ) { | ||
const PIXELS_PER_INCH = 96; | ||
const ONE_PERCENT = 0.01; | ||
|
||
const defaultProperties = { | ||
fontSize: 16, | ||
lineHeight: 16, | ||
width: 375, | ||
height: 812, | ||
type: 'font', | ||
}; | ||
|
||
const setOptions = Object.assign( {}, defaultProperties, options ); | ||
|
||
const relativeUnits = { | ||
em: setOptions.fontSize, | ||
rem: setOptions.fontSize, | ||
vh: setOptions.height * ONE_PERCENT, | ||
vw: setOptions.width * ONE_PERCENT, | ||
vmin: | ||
( setOptions.width < setOptions.height | ||
? setOptions.width | ||
: setOptions.height ) * ONE_PERCENT, | ||
vmax: | ||
( setOptions.width > setOptions.height | ||
? setOptions.width | ||
: setOptions.height ) * ONE_PERCENT, | ||
'%': | ||
( setOptions.type === 'font' | ||
? setOptions.fontSize | ||
: setOptions.width ) * ONE_PERCENT, | ||
ch: 8, // The advance measure (width) of the glyph "0" of the element's font. Approximate | ||
ex: 7.15625, // x-height of the element's font. Approximate | ||
lh: setOptions.lineHeight, | ||
}; | ||
|
||
const absoluteUnits = { | ||
in: PIXELS_PER_INCH, | ||
cm: PIXELS_PER_INCH / 2.54, | ||
mm: PIXELS_PER_INCH / 25.4, | ||
pt: PIXELS_PER_INCH / 72, | ||
pc: PIXELS_PER_INCH / 6, | ||
px: 1, | ||
Q: PIXELS_PER_INCH / 2.54 / 40, | ||
}; | ||
|
||
if ( relativeUnits[ parsedUnit.unit ] ) { | ||
return ( | ||
( relativeUnits[ parsedUnit.unit ] * parsedUnit.value ).toFixed( | ||
0 | ||
) + 'px' | ||
); | ||
} | ||
|
||
if ( absoluteUnits[ parsedUnit.unit ] ) { | ||
return ( | ||
( absoluteUnits[ parsedUnit.unit ] * parsedUnit.value ).toFixed( | ||
0 | ||
) + 'px' | ||
); | ||
} | ||
|
||
return null; | ||
} | ||
/** | ||
* Returns the px value of a cssUnit. | ||
* | ||
* @param {string} cssUnit | ||
* @param {string} options | ||
* @return {string} returns the cssUnit value in a simple px format. | ||
*/ | ||
export function getPxFromCssUnit( cssUnit, options = {} ) { | ||
if ( Number.isFinite( cssUnit ) ) { | ||
return cssUnit.toFixed( 0 ) + 'px'; | ||
} | ||
if ( cssUnit === undefined ) { | ||
return null; | ||
} | ||
let parsedUnit = parseUnit( cssUnit ); | ||
|
||
if ( ! parsedUnit.unit ) { | ||
parsedUnit = parseUnitFunction( cssUnit, options ); | ||
} | ||
|
||
if ( isMathExpression( cssUnit ) && ! parsedUnit.unit ) { | ||
return evalMathExpression( cssUnit ); | ||
} | ||
|
||
return convertParsedUnitToPx( parsedUnit, options ); | ||
} |
Oops, something went wrong.