import { compose, castArray, parseFloat as parseFloat$1, matchPercent } from '@react-pdf/fns'; import matchMedia from 'media-engine'; import hlsToHex from 'hsl-to-hex'; import colorString from 'color-string'; import parse$1 from 'postcss-value-parser/lib/parse.js'; import parseUnit from 'postcss-value-parser/lib/unit.js'; /** * Remove nil values from array * * @param array - Style array * @returns Style array without nils */ const compact = (array) => array.filter(Boolean); /** * Merges style objects array * * @param styles - Style array * @returns Merged style object */ const mergeStyles = (styles) => styles.reduce((acc, style) => { const s = Array.isArray(style) ? flatten(style) : style; Object.keys(s).forEach((key) => { if (s[key] !== null && s[key] !== undefined) { acc[key] = s[key]; } }); return acc; }, {}); /** * Flattens an array of style objects, into one aggregated style object. * * @param styles - Style or style array * @returns Flattened style object */ const flatten = compose(mergeStyles, compact, (castArray)); /** * Resolves media queries in styles object * * @param container - Container for which styles are resolved * @param style - Style description * @returns Resolved style object */ const resolveMediaQueries = (container, style) => { return Object.keys(style).reduce((acc, key) => { if (/@media/.test(key)) { return { ...acc, ...matchMedia({ [key]: style[key] }, container), }; } return { ...acc, [key]: style[key] }; }, {}); }; const isRgb = (value) => /rgba?/g.test(value); const isHsl = (value) => /hsla?/g.test(value); /** * Transform rgb color to hexa * * @param value - Styles value * @returns Transformed value */ const parseRgb = (value) => { const rgb = colorString.get.rgb(value); return colorString.to.hex(rgb); }; /** * Transform Hsl color to hexa * * @param value - Styles value * @returns Transformed value */ const parseHsl = (value) => { const hsl = colorString.get.hsl(value).map(Math.round); const hex = hlsToHex(...hsl); return hex.toUpperCase(); }; /** * Transform given color to hexa * * @param value - Styles value * @returns Transformed value */ const transformColor = (value) => { if (isRgb(value)) return parseRgb(value); if (isHsl(value)) return parseHsl(value); return value; }; /** * Parses scalar value in value and unit pairs * * @param value - Scalar value * @returns Parsed value */ const parseValue = (value) => { if (typeof value === 'number') return { value, unit: undefined }; const match = /^(-?\d*\.?\d+)(in|mm|cm|pt|vh|vw|px|rem)?$/g.exec(value); return match ? { value: parseFloat(match[1]), unit: match[2] || 'pt' } : { value, unit: undefined }; }; /** * Transform given scalar value * * @param container * @param value - Styles value * @returns Transformed value */ const transformUnit = (container, value) => { const scalar = parseValue(value); const outputDpi = 72; const inputDpi = container.dpi || 72; const mmFactor = (1 / 25.4) * outputDpi; const cmFactor = (1 / 2.54) * outputDpi; if (typeof scalar.value !== 'number') return scalar.value; switch (scalar.unit) { case 'rem': return scalar.value * (container.remBase || 18); case 'in': return scalar.value * outputDpi; case 'mm': return scalar.value * mmFactor; case 'cm': return scalar.value * cmFactor; case 'vh': return scalar.value * (container.height / 100); case 'vw': return scalar.value * (container.width / 100); case 'px': return Math.round(scalar.value * (outputDpi / inputDpi)); default: return scalar.value; } }; const processNumberValue = (key, value) => ({ [key]: parseFloat$1(value), }); const processUnitValue = (key, value, container) => ({ [key]: transformUnit(container, value), }); const processColorValue = (key, value) => { const result = { [key]: transformColor(value) }; return result; }; const processNoopValue = (key, value) => ({ [key]: value, }); const BORDER_SHORTHAND_REGEX = /(-?\d+(\.\d+)?(in|mm|cm|pt|vw|vh|px|rem)?)\s(\S+)\s(.+)/; const matchBorderShorthand = (value) => value.match(BORDER_SHORTHAND_REGEX) || []; const resolveBorderShorthand = (key, value, container) => { const match = matchBorderShorthand(`${value}`); if (match) { const widthMatch = match[1] || value; const styleMatch = match[4] || value; const colorMatch = match[5] || value; const style = styleMatch; const color = colorMatch ? transformColor(colorMatch) : undefined; const width = widthMatch ? transformUnit(container, widthMatch) : undefined; if (key.match(/(Top|Right|Bottom|Left)$/)) { return { [`${key}Color`]: color, [`${key}Style`]: style, [`${key}Width`]: width, }; } if (key.match(/Color$/)) { return { borderTopColor: color, borderRightColor: color, borderBottomColor: color, borderLeftColor: color, }; } if (key.match(/Style$/)) { if (typeof style === 'number') throw new Error(`Invalid border style: ${style}`); return { borderTopStyle: style, borderRightStyle: style, borderBottomStyle: style, borderLeftStyle: style, }; } if (key.match(/Width$/)) { if (typeof width !== 'number') throw new Error(`Invalid border width: ${width}`); return { borderTopWidth: width, borderRightWidth: width, borderBottomWidth: width, borderLeftWidth: width, }; } if (key.match(/Radius$/)) { const radius = value ? transformUnit(container, value) : undefined; if (typeof radius !== 'number') throw new Error(`Invalid border radius: ${radius}`); return { borderTopLeftRadius: radius, borderTopRightRadius: radius, borderBottomRightRadius: radius, borderBottomLeftRadius: radius, }; } if (typeof width !== 'number') throw new Error(`Invalid border width: ${width}`); if (typeof style === 'number') throw new Error(`Invalid border style: ${style}`); return { borderTopColor: color, borderTopStyle: style, borderTopWidth: width, borderRightColor: color, borderRightStyle: style, borderRightWidth: width, borderBottomColor: color, borderBottomStyle: style, borderBottomWidth: width, borderLeftColor: color, borderLeftStyle: style, borderLeftWidth: width, }; } return { [key]: value }; }; const handlers$b = { border: (resolveBorderShorthand), borderBottom: (resolveBorderShorthand), borderBottomColor: (processColorValue), borderBottomLeftRadius: (processUnitValue), borderBottomRightRadius: (processUnitValue), borderBottomStyle: (processNoopValue), borderBottomWidth: (processUnitValue), borderColor: (resolveBorderShorthand), borderLeft: (resolveBorderShorthand), borderLeftColor: (processColorValue), borderLeftStyle: (processNoopValue), borderLeftWidth: (processUnitValue), borderRadius: (resolveBorderShorthand), borderRight: (resolveBorderShorthand), borderRightColor: (processColorValue), borderRightStyle: (processNoopValue), borderRightWidth: (processUnitValue), borderStyle: (resolveBorderShorthand), borderTop: (resolveBorderShorthand), borderTopColor: (processColorValue), borderTopLeftRadius: (processUnitValue), borderTopRightRadius: (processUnitValue), borderTopStyle: (processNoopValue), borderTopWidth: (processUnitValue), borderWidth: (resolveBorderShorthand), }; const handlers$a = { backgroundColor: (processColorValue), color: (processColorValue), opacity: (processNumberValue), }; const handlers$9 = { height: (processUnitValue), maxHeight: (processUnitValue), maxWidth: (processUnitValue), minHeight: (processUnitValue), minWidth: (processUnitValue), width: (processUnitValue), }; // https://developer.mozilla.org/en-US/docs/Web/CSS/flex#values // TODO: change flex defaults to [0, 1, 'auto'] as in spec in next major release const flexDefaults = [1, 1, 0]; const flexAuto = [1, 1, 'auto']; const processFlexShorthand = (key, value, container) => { let defaults = flexDefaults; let matches = []; if (value === 'auto') { defaults = flexAuto; } else { matches = `${value}`.split(' '); } const flexGrow = parseFloat$1(matches[0] || defaults[0]); const flexShrink = parseFloat$1(matches[1] || defaults[1]); const flexBasis = transformUnit(container, matches[2] || defaults[2]); return { flexGrow, flexShrink, flexBasis }; }; const handlers$8 = { alignContent: (processNoopValue), alignItems: (processNoopValue), alignSelf: (processNoopValue), flex: (processFlexShorthand), flexBasis: (processUnitValue), flexDirection: (processNoopValue), flexFlow: (processNoopValue), flexGrow: (processNumberValue), flexShrink: (processNumberValue), flexWrap: (processNoopValue), justifyContent: (processNoopValue), justifySelf: (processNoopValue), }; const processGapShorthand = (key, value, container) => { const match = `${value}`.split(' '); const rowGap = transformUnit(container, match?.[0] || value); const columnGap = transformUnit(container, match?.[1] || value); return { rowGap, columnGap }; }; const handlers$7 = { gap: (processGapShorthand), columnGap: (processUnitValue), rowGap: (processUnitValue), }; const handlers$6 = { aspectRatio: (processNumberValue), bottom: (processUnitValue), display: (processNoopValue), left: (processUnitValue), position: (processNoopValue), right: (processUnitValue), top: (processUnitValue), overflow: (processNoopValue), zIndex: (processNumberValue), }; const BOX_MODEL_UNITS = 'px,in,mm,cm,pt,%,vw,vh'; const logError = (style, value) => { const name = style.toString(); // eslint-disable-next-line no-console console.error(` @react-pdf/stylesheet parsing error: ${name}: ${value}, ${' '.repeat(name.length + 2)}^ Unsupported ${name} value format `); }; /** * @param options * @param [options.expandsTo] * @param [options.maxValues] * @param [options.autoSupported] */ const expandBoxModel = ({ expandsTo, maxValues = 1, autoSupported = false, } = {}) => (model, value, container) => { const nodes = parse$1(`${value}`); const parts = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; // value contains `calc`, `url` or other css function // `,`, `/` or strings that unsupported by margin and padding if (node.type === 'function' || node.type === 'string' || node.type === 'div') { logError(model, value); return {}; } if (node.type === 'word') { if (node.value === 'auto' && autoSupported) { parts.push(node.value); } else { const result = parseUnit(node.value); // when unit isn't specified this condition is true if (result && BOX_MODEL_UNITS.includes(result.unit)) { parts.push(node.value); } else { logError(model, value); return {}; } } } } // checks that we have enough parsed values if (parts.length > maxValues) { logError(model, value); return {}; } const first = transformUnit(container, parts[0]); if (expandsTo) { const second = transformUnit(container, parts[1] || parts[0]); const third = transformUnit(container, parts[2] || parts[0]); const fourth = transformUnit(container, parts[3] || parts[1] || parts[0]); return expandsTo({ first, second, third, fourth }); } return { [model]: first, }; }; const processMargin = expandBoxModel({ expandsTo: ({ first, second, third, fourth }) => ({ marginTop: first, marginRight: second, marginBottom: third, marginLeft: fourth, }), maxValues: 4, autoSupported: true, }); const processMarginVertical = expandBoxModel({ expandsTo: ({ first, second }) => ({ marginTop: first, marginBottom: second, }), maxValues: 2, autoSupported: true, }); const processMarginHorizontal = expandBoxModel({ expandsTo: ({ first, second }) => ({ marginRight: first, marginLeft: second, }), maxValues: 2, autoSupported: true, }); const processMarginSingle = expandBoxModel({ autoSupported: true, }); const handlers$5 = { margin: (processMargin), marginBottom: (processMarginSingle), marginHorizontal: (processMarginHorizontal), marginLeft: (processMarginSingle), marginRight: (processMarginSingle), marginTop: (processMarginSingle), marginVertical: (processMarginVertical), }; const processPadding = expandBoxModel({ expandsTo: ({ first, second, third, fourth }) => ({ paddingTop: first, paddingRight: second, paddingBottom: third, paddingLeft: fourth, }), maxValues: 4, }); const processPaddingVertical = expandBoxModel({ expandsTo: ({ first, second }) => ({ paddingTop: first, paddingBottom: second, }), maxValues: 2, }); const processPaddingHorizontal = expandBoxModel({ expandsTo: ({ first, second }) => ({ paddingRight: first, paddingLeft: second, }), maxValues: 2, }); const processPaddingSingle = expandBoxModel(); const handlers$4 = { padding: (processPadding), paddingBottom: (processPaddingSingle), paddingHorizontal: (processPaddingHorizontal), paddingLeft: (processPaddingSingle), paddingRight: (processPaddingSingle), paddingTop: (processPaddingSingle), paddingVertical: (processPaddingVertical), }; const offsetKeyword = (value) => { switch (value) { case 'top': case 'left': return '0%'; case 'right': case 'bottom': return '100%'; case 'center': return '50%'; default: return value; } }; const processObjectPosition = (key, value, container) => { const match = `${value}`.split(' '); const objectPositionX = offsetKeyword(transformUnit(container, match?.[0] || value)); const objectPositionY = offsetKeyword(transformUnit(container, match?.[1] || value)); return { objectPositionX, objectPositionY }; }; const processObjectPositionValue = (key, value, container) => ({ [key]: offsetKeyword(transformUnit(container, value)), }); const handlers$3 = { objectPosition: (processObjectPosition), objectPositionX: (processObjectPositionValue), objectPositionY: (processObjectPositionValue), objectFit: (processNoopValue), }; const castInt = (value) => { if (typeof value === 'number') return value; return parseInt(value, 10); }; const FONT_WEIGHTS = { thin: 100, hairline: 100, ultralight: 200, extralight: 200, light: 300, normal: 400, medium: 500, semibold: 600, demibold: 600, bold: 700, ultrabold: 800, extrabold: 800, heavy: 900, black: 900, }; const transformFontWeight = (value) => { if (!value) return FONT_WEIGHTS.normal; if (typeof value === 'number') return value; const lv = value.toLowerCase(); if (FONT_WEIGHTS[lv]) return FONT_WEIGHTS[lv]; return castInt(value); }; const processFontWeight = (key, value) => { return { [key]: transformFontWeight(value) }; }; const transformLineHeight = (value, styles, container) => { if (value === '') return value; const fontSize = transformUnit(container, styles.fontSize || 18); const lineHeight = transformUnit(container, value); // Percent values: use this number multiplied by the element's font size const { percent } = matchPercent(lineHeight) || {}; if (percent) return percent * fontSize; // Unitless values: use this number multiplied by the element's font size return isNaN(value) ? lineHeight : lineHeight * fontSize; }; const processLineHeight = (key, value, container, styles) => { return { [key]: transformLineHeight(value, styles, container), }; }; const handlers$2 = { direction: (processNoopValue), fontFamily: (processNoopValue), fontSize: (processUnitValue), fontStyle: (processNoopValue), fontWeight: (processFontWeight), letterSpacing: (processUnitValue), lineHeight: (processLineHeight), maxLines: (processNumberValue), textAlign: (processNoopValue), textDecoration: (processNoopValue), textDecorationColor: (processColorValue), textDecorationStyle: (processNoopValue), textIndent: (processNoopValue), textOverflow: (processNoopValue), textTransform: (processNoopValue), verticalAlign: (processNoopValue), }; const matchNumber = (value) => typeof value === 'string' && /^-?\d*\.?\d*$/.test(value); const castFloat = (value) => { if (typeof value !== 'string') return value; if (matchNumber(value)) return parseFloat(value); return value; }; const parse = (transformString) => { const transforms = transformString.trim().split(/\)[ ,]|\)/); // Handle "initial", "inherit", "unset". if (transforms.length === 1) { return [[transforms[0], true]]; } const parsed = []; for (let i = 0; i < transforms.length; i += 1) { const transform = transforms[i]; if (transform) { const [name, rawValue] = transform.split('('); const splitChar = rawValue.indexOf(',') >= 0 ? ',' : ' '; const value = rawValue.split(splitChar).map((val) => val.trim()); parsed.push({ operation: name.trim(), value }); } } return parsed; }; const parseAngle = (value) => { const unitsRegexp = /(-?\d*\.?\d*)(\w*)?/i; const [, angle, unit] = unitsRegexp.exec(value); const number = Number.parseFloat(angle); return unit === 'rad' ? (number * 180) / Math.PI : number; }; const normalizeTransformOperation = ({ operation, value }) => { switch (operation) { case 'scale': { const [scaleX, scaleY = scaleX] = value.map((num) => Number.parseFloat(num)); return { operation: 'scale', value: [scaleX, scaleY] }; } case 'scaleX': { return { operation: 'scale', value: [Number.parseFloat(value), 1] }; } case 'scaleY': { return { operation: 'scale', value: [1, Number.parseFloat(value)] }; } case 'rotate': { return { operation: 'rotate', value: [parseAngle(value)] }; } case 'translate': { return { operation: 'translate', value: value.map((num) => Number.parseFloat(num)), }; } case 'translateX': { return { operation: 'translate', value: [Number.parseFloat(value), 0], }; } case 'translateY': { return { operation: 'translate', value: [0, Number.parseFloat(value)] }; } case 'skew': { return { operation: 'skew', value: value.map(parseAngle) }; } case 'skewX': { return { operation: 'skew', value: [parseAngle(value), 0] }; } case 'skewY': { return { operation: 'skew', value: [0, parseAngle(value)] }; } default: { return { operation, value: value.map((num) => Number.parseFloat(num)) }; } } }; const normalize = (operations) => { return operations.map((operation) => normalizeTransformOperation(operation)); }; const processTransform = (key, value) => { if (typeof value !== 'string') return { [key]: value }; return { [key]: normalize(parse(value)) }; }; const Y_AXIS_SHORTHANDS = { top: true, bottom: true }; const sortTransformOriginPair = (a, b) => { if (Y_AXIS_SHORTHANDS[a]) return 1; if (Y_AXIS_SHORTHANDS[b]) return -1; return 0; }; const getTransformOriginPair = (values) => { if (!values || values.length === 0) return ['center', 'center']; const pair = values.length === 1 ? [values[0], 'center'] : values; return pair.sort(sortTransformOriginPair); }; // Transforms shorthand transformOrigin values const processTransformOriginShorthand = (key, value, container) => { const match = `${value}`.split(' '); const pair = getTransformOriginPair(match); const transformOriginX = transformUnit(container, pair[0]); const transformOriginY = transformUnit(container, pair[1]); return { transformOriginX: offsetKeyword(transformOriginX) || castFloat(transformOriginX), transformOriginY: offsetKeyword(transformOriginY) || castFloat(transformOriginY), }; }; const processTransformOriginValue = (key, value, container) => { const v = transformUnit(container, value); return { [key]: offsetKeyword(v) || castFloat(v) }; }; const handlers$1 = { transform: processTransform, gradientTransform: processTransform, transformOrigin: (processTransformOriginShorthand), transformOriginX: (processTransformOriginValue), transformOriginY: (processTransformOriginValue), }; const handlers = { fill: (processColorValue), stroke: (processColorValue), strokeDasharray: (processNoopValue), strokeWidth: (processUnitValue), fillOpacity: (processNumberValue), strokeOpacity: (processNumberValue), fillRule: (processNoopValue), textAnchor: (processNoopValue), strokeLinecap: (processNoopValue), strokeLinejoin: (processNoopValue), visibility: (processNoopValue), clipPath: (processNoopValue), dominantBaseline: (processNoopValue), }; const shorthands = { ...handlers$b, ...handlers$a, ...handlers$9, ...handlers$8, ...handlers$7, ...handlers$6, ...handlers$5, ...handlers$4, ...handlers$3, ...handlers$2, ...handlers$1, ...handlers, }; /** * Expand the shorthand properties. * * @param style - Style object * @returns Expanded style object */ const resolve = (container) => (style) => { const propsArray = Object.keys(style); const resolvedStyle = {}; for (let i = 0; i < propsArray.length; i += 1) { const key = propsArray[i]; const value = style[key]; if (!shorthands[key]) { resolvedStyle[key] = value; continue; } const resolved = shorthands[key](key, value, container, style); const keys = Object.keys(resolved); for (let j = 0; j < keys.length; j += 1) { const propName = keys[j]; const propValue = resolved[propName]; resolvedStyle[propName] = propValue; } } return resolvedStyle; }; /** * Resolves styles * * @param container * @param style - Style * @returns Resolved style */ const resolveStyles = (container, style) => { const computeMediaQueries = (value) => resolveMediaQueries(container, value); return compose(resolve(container), computeMediaQueries, flatten)(style); }; export { resolveStyles as default, flatten, transformColor };