diff --git a/src/TokenStream.js b/src/TokenStream.js index 721eb87..59a3e1a 100644 --- a/src/TokenStream.js +++ b/src/TokenStream.js @@ -1,9 +1,10 @@ const SYMBOL_MATCH = 'SYMBOL_MATCH' export default class TokenStream { - constructor(nodes, parent) { + constructor(nodes, units, parent) { this.index = 0 this.nodes = nodes + this.units = units this.functionName = parent != null ? parent.value : null this.lastValue = null this.rewindIndex = -1 @@ -20,7 +21,7 @@ export default class TokenStream { for (let i = 0; i < tokenDescriptors.length; i += 1) { const tokenDescriptor = tokenDescriptors[i] - const value = tokenDescriptor(node) + const value = tokenDescriptor(node, this.units) if (value !== null) { this.index += 1 this.lastValue = value @@ -43,7 +44,7 @@ export default class TokenStream { matchesFunction() { const node = this.nodes[this.index] if (node.type !== 'function') return null - const value = new TokenStream(node.nodes, node) + const value = new TokenStream(node.nodes, this.units, node) this.index += 1 this.lastValue = null return value diff --git a/src/__tests__/flex.js b/src/__tests__/flex.js index 2e1d594..21f8461 100644 --- a/src/__tests__/flex.js +++ b/src/__tests__/flex.js @@ -1,5 +1,17 @@ import transformCss from '..' +it('transforms with viewport units', () => { + expect( + transformCss([['flex', '1 2 10vw']], undefined, { + vw: 3.2, + }) + ).toEqual({ + flexGrow: 1, + flexShrink: 2, + flexBasis: 32, + }) +}) + it('transforms flex shorthand with 3 values', () => { expect(transformCss([['flex', '1 2 3px']])).toEqual({ flexGrow: 1, diff --git a/src/index.js b/src/index.js index 0afce0a..7860d81 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import camelizeStyleName from 'camelize' import transforms from './transforms/index' import devPropertiesWithoutUnitsRegExp from './devPropertiesWithoutUnitsRegExp' import TokenStream from './TokenStream' +import { userUnitRe } from './tokenTypes' // Note if this is wrong, you'll need to change tokenTypes.js too const numberOrLengthRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?)(?:px)?$/i @@ -13,7 +14,7 @@ const nullRe = /^null$/i const undefinedRe = /^undefined$/i // Undocumented export -export const transformRawValue = (propName, value) => { +export const transformRawValue = (propName, value, units) => { if (process.env.NODE_ENV !== 'production') { const needsUnit = !devPropertiesWithoutUnitsRegExp.test(propName) const isNumberWithoutUnit = numberOnlyRe.test(value) @@ -30,6 +31,12 @@ export const transformRawValue = (propName, value) => { const numberMatch = value.match(numberOrLengthRe) if (numberMatch !== null) return Number(numberMatch[1]) + const userUnitsMatch = value.match(userUnitRe) + if (userUnitsMatch != null) { + const { 1: length, 2: unit } = userUnitsMatch + return unit in units ? Number(length) * units[unit] : null + } + const boolMatch = value.match(boolRe) if (boolMatch !== null) return boolMatch[0].toLowerCase() === 'true' @@ -42,30 +49,35 @@ export const transformRawValue = (propName, value) => { return value } -const baseTransformShorthandValue = (propName, value) => { +const baseTransformShorthandValue = (propName, value, units) => { const ast = parse(value) - const tokenStream = new TokenStream(ast.nodes) + const tokenStream = new TokenStream(ast.nodes, units) return transforms[propName](tokenStream) } const transformShorthandValue = process.env.NODE_ENV === 'production' ? baseTransformShorthandValue - : (propName, value) => { + : (propName, value, units) => { try { - return baseTransformShorthandValue(propName, value) + return baseTransformShorthandValue(propName, value, units) } catch (e) { throw new Error(`Failed to parse declaration "${propName}: ${value}"`) } } -export const getStylesForProperty = (propName, inputValue, allowShorthand) => { +export const getStylesForProperty = ( + propName, + inputValue, + allowShorthand, + units +) => { const isRawValue = allowShorthand === false || !(propName in transforms) const value = inputValue.trim() const propValues = isRawValue - ? { [propName]: transformRawValue(propName, value) } - : transformShorthandValue(propName, value) + ? { [propName]: transformRawValue(propName, value, units) } + : transformShorthandValue(propName, value, units) return propValues } @@ -78,13 +90,13 @@ export const getPropertyName = propName => { return camelizeStyleName(propName) } -export default (rules, shorthandBlacklist = []) => +export default (rules, shorthandBlacklist = [], units = {}) => rules.reduce((accum, rule) => { const propertyName = getPropertyName(rule[0]) const value = rule[1] const allowShorthand = shorthandBlacklist.indexOf(propertyName) === -1 return Object.assign( accum, - getStylesForProperty(propertyName, value, allowShorthand) + getStylesForProperty(propertyName, value, allowShorthand, units) ) }, {}) diff --git a/src/tokenTypes.js b/src/tokenTypes.js index 497001d..0b2bab7 100644 --- a/src/tokenTypes.js +++ b/src/tokenTypes.js @@ -34,7 +34,7 @@ const identRe = /(^-?[_a-z][_a-z0-9-]*$)/i const numberRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?)$/i // Note lengthRe is sneaky: you can omit units for 0 const lengthRe = /^(0$|(?:[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?)(?=px$))/i -const unsupportedUnitRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(ch|em|ex|rem|vh|vw|vmin|vmax|cm|mm|in|pc|pt))$/i +export const userUnitRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?)(ch|em|ex|rem|vh|vw|vmin|vmax|cm|mm|in|pc|pt)$/i const angleRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?:deg|rad))$/i const percentRe = /^([+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?%)$/i @@ -66,7 +66,16 @@ export const NONE = regExpToken(noneRe) export const AUTO = regExpToken(autoRe) export const NUMBER = regExpToken(numberRe, Number) export const LENGTH = regExpToken(lengthRe, Number) -export const UNSUPPORTED_LENGTH_UNIT = regExpToken(unsupportedUnitRe) +export const USER_LENGTH_UNIT = (node, units) => { + if (node.type !== 'word') return null + + const match = node.value.match(userUnitRe) + if (match === null) return null + + const { 1: length, 2: unit } = match + + return unit in units ? Number(length) * units[unit] : null +} export const ANGLE = regExpToken(angleRe, angle => angle.toLowerCase()) export const PERCENT = regExpToken(percentRe) export const IDENT = regExpToken(identRe) diff --git a/src/transforms/border.js b/src/transforms/border.js index d4c77b4..c846624 100644 --- a/src/transforms/border.js +++ b/src/transforms/border.js @@ -3,7 +3,7 @@ import { NONE, COLOR, LENGTH, - UNSUPPORTED_LENGTH_UNIT, + USER_LENGTH_UNIT, SPACE, } from '../tokenTypes' @@ -29,7 +29,7 @@ export default tokenStream => { if ( borderWidth === undefined && - tokenStream.matches(LENGTH, UNSUPPORTED_LENGTH_UNIT) + tokenStream.matches(LENGTH, USER_LENGTH_UNIT) ) { borderWidth = tokenStream.lastValue } else if (borderColor === undefined && tokenStream.matches(COLOR)) { diff --git a/src/transforms/flex.js b/src/transforms/flex.js index cf9bdbf..3fb9114 100644 --- a/src/transforms/flex.js +++ b/src/transforms/flex.js @@ -3,7 +3,7 @@ import { AUTO, NUMBER, LENGTH, - UNSUPPORTED_LENGTH_UNIT, + USER_LENGTH_UNIT, PERCENT, SPACE, } from '../tokenTypes' @@ -43,7 +43,7 @@ export default tokenStream => { } } else if ( flexBasis === undefined && - tokenStream.matches(LENGTH, UNSUPPORTED_LENGTH_UNIT, PERCENT) + tokenStream.matches(LENGTH, USER_LENGTH_UNIT, PERCENT) ) { flexBasis = tokenStream.lastValue } else if (flexBasis === undefined && tokenStream.matches(AUTO)) { diff --git a/src/transforms/font.js b/src/transforms/font.js index ae5e5dd..01503da 100644 --- a/src/transforms/font.js +++ b/src/transforms/font.js @@ -3,7 +3,7 @@ import { regExpToken, SPACE, LENGTH, - UNSUPPORTED_LENGTH_UNIT, + USER_LENGTH_UNIT, SLASH, } from '../tokenTypes' @@ -42,10 +42,10 @@ export default tokenStream => { numStyleWeightVariantMatched += 1 } - const fontSize = tokenStream.expect(LENGTH, UNSUPPORTED_LENGTH_UNIT) + const fontSize = tokenStream.expect(LENGTH, USER_LENGTH_UNIT) if (tokenStream.matches(SLASH)) { - lineHeight = tokenStream.expect(LENGTH, UNSUPPORTED_LENGTH_UNIT) + lineHeight = tokenStream.expect(LENGTH, USER_LENGTH_UNIT) } tokenStream.expect(SPACE) diff --git a/src/transforms/index.js b/src/transforms/index.js index 66320ae..ca55ba9 100644 --- a/src/transforms/index.js +++ b/src/transforms/index.js @@ -3,7 +3,7 @@ import { WORD, COLOR, LENGTH, - UNSUPPORTED_LENGTH_UNIT, + USER_LENGTH_UNIT, PERCENT, AUTO, } from '../tokenTypes' @@ -35,7 +35,7 @@ const borderRadius = directionFactory({ }) const borderWidth = directionFactory({ prefix: 'border', suffix: 'Width' }) const margin = directionFactory({ - types: [LENGTH, UNSUPPORTED_LENGTH_UNIT, PERCENT, AUTO], + types: [LENGTH, USER_LENGTH_UNIT, PERCENT, AUTO], prefix: 'margin', }) const padding = directionFactory({ prefix: 'padding' }) diff --git a/src/transforms/util.js b/src/transforms/util.js index b195567..512458a 100644 --- a/src/transforms/util.js +++ b/src/transforms/util.js @@ -1,6 +1,6 @@ import { LENGTH, - UNSUPPORTED_LENGTH_UNIT, + USER_LENGTH_UNIT, PERCENT, COLOR, SPACE, @@ -8,7 +8,7 @@ import { } from '../tokenTypes' export const directionFactory = ({ - types = [LENGTH, UNSUPPORTED_LENGTH_UNIT, PERCENT], + types = [LENGTH, USER_LENGTH_UNIT, PERCENT], directions = ['Top', 'Right', 'Bottom', 'Left'], prefix = '', suffix = '', @@ -65,16 +65,16 @@ export const parseShadow = tokenStream => { if ( offsetX === undefined && - tokenStream.matches(LENGTH, UNSUPPORTED_LENGTH_UNIT) + tokenStream.matches(LENGTH, USER_LENGTH_UNIT) ) { offsetX = tokenStream.lastValue tokenStream.expect(SPACE) - offsetY = tokenStream.expect(LENGTH, UNSUPPORTED_LENGTH_UNIT) + offsetY = tokenStream.expect(LENGTH, USER_LENGTH_UNIT) tokenStream.saveRewindPoint() if ( tokenStream.matches(SPACE) && - tokenStream.matches(LENGTH, UNSUPPORTED_LENGTH_UNIT) + tokenStream.matches(LENGTH, USER_LENGTH_UNIT) ) { radius = tokenStream.lastValue } else {