diff --git a/src/handler/expand.ts b/src/handler/expand.ts index 000a1db8..45be94b1 100644 --- a/src/handler/expand.ts +++ b/src/handler/expand.ts @@ -12,7 +12,7 @@ import CssDimension from '../vendor/parse-css-dimension/index.js' import parseTransformOrigin, { ParsedTransformOrigin, } from '../transform-origin.js' -import { isString, lengthToNumber, v } from '../utils.js' +import { isString, lengthToNumber, v, splitEffects } from '../utils.js' import { MaskProperty, parseMask } from '../parser/mask.js' // https://react-cn.github.io/react/tips/style-props-value-px.html @@ -173,7 +173,7 @@ function handleSpecialCase( // Handle multiple text shadows if provided. value = value.toString().trim() if (value.includes(',')) { - const shadows = splitTextShadow(value) + const shadows = splitEffects(value) const result = {} for (const shadow of shadows) { const styles = getStylesForProperty('textShadow', shadow, true) @@ -192,29 +192,6 @@ function handleSpecialCase( return } -function splitTextShadow(str: string) { - const result: string[] = [] - let skip = false - let startPos = 0 - const len = str.length - - for (let i = 0; i < len; ++i) { - const t = str[i] - if (t === ')') skip = false - if (skip) continue - if (t === '(') skip = true - - if (t === ',') { - result.push(str.substring(startPos, i)) - startPos = i + 1 - } - } - - result.push(str.substring(startPos, len)) - - return result.map((s) => s.trim()) -} - function getErrorHint(name: string) { if (name === 'transform') { return ' Only absolute lengths such as `10px` are supported.' diff --git a/src/parser/mask.ts b/src/parser/mask.ts index c7d3669f..60ecf93a 100644 --- a/src/parser/mask.ts +++ b/src/parser/mask.ts @@ -1,4 +1,5 @@ import { getPropertyName } from 'css-to-react-native' +import { splitEffects } from '../utils.js' function getMaskProperty(style: Record, name: string) { const key = getPropertyName(`mask-${name}`) @@ -14,34 +15,6 @@ export interface MaskProperty { clip: string } -function splitMaskImages(maskImage) { - let maskImages = [] - let start = 0 - let parenCount = 0 - - for (let i = 0; i < maskImage.length; i++) { - if (maskImage[i] === '(') { - parenCount++ - } else if (maskImage[i] === ')') { - parenCount-- - } - - if (parenCount === 0 && maskImage[i] === ',') { - maskImages.push(maskImage.slice(start, i).trim()) - start = i + 1 - } - } - - maskImages.push(maskImage.slice(start).trim()) - - return maskImages -} - -/** - * url(https:a.png), linear-gradient(blue, red) => [url(https:a.png), linear-gradient(blue, red)] - * rgba(0,0,0,.7) => [rgba(0,0,0,.7)] - */ - export function parseMask( style: Record ): MaskProperty[] { @@ -55,7 +28,7 @@ export function parseMask( clip: getMaskProperty(style, 'origin') || 'border-box', } - let maskImages = splitMaskImages(maskImage).filter((v) => v && v !== 'none') + let maskImages = splitEffects(maskImage).filter((v) => v && v !== 'none') return maskImages.reverse().map((m) => ({ image: m, diff --git a/src/utils.ts b/src/utils.ts index ed93cbe8..c672766f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -318,3 +318,30 @@ export const midline = (s: string) => { (_, letter: string) => `-${letter.toLowerCase()}` ) } + +export function splitEffects( + input: string, + separator: string | RegExp = ',' +): string[] { + const result = [] + let l = 0 + let parenCount = 0 + separator = new RegExp(separator) + + for (let i = 0; i < input.length; i++) { + if (input[i] === '(') { + parenCount++ + } else if (input[i] === ')') { + parenCount-- + } + + if (parenCount === 0 && separator.test(input[i])) { + result.push(input.slice(l, i).trim()) + l = i + 1 + } + } + + result.push(input.slice(l).trim()) + + return result +} diff --git a/test/units.test.tsx b/test/units.test.tsx index 29a389db..01c74bb9 100644 --- a/test/units.test.tsx +++ b/test/units.test.tsx @@ -2,6 +2,7 @@ import { it, describe, expect } from 'vitest' import { initFonts, toImage } from './utils.js' import satori from '../src/index.js' +import { splitEffects } from '../src/utils.js' describe('Units', () => { let fonts @@ -133,4 +134,37 @@ describe('Units', () => { ) expect(toImage(svg, 100)).toMatchImageSnapshot() }) + + it('should support split multiple effect', () => { + const tests = { + 'url(https:a.png), linear-gradient(blue, red)': [ + 'url(https:a.png)', + 'linear-gradient(blue, red)', + ], + 'rgba(0,0,0,.7)': ['rgba(0,0,0,.7)'], + '1px 1px 2px black, 0 0 1em blue': ['1px 1px 2px black', '0 0 1em blue'], + '2px 2px red, 4px 4px #4bf542, 6px 6px rgba(186, 147, 17, 30%)': [ + '2px 2px red', + '4px 4px #4bf542', + '6px 6px rgba(186, 147, 17, 30%)', + ], + } + + for (const [k, v] of Object.entries(tests)) { + expect(splitEffects(k, ',')).toEqual(v) + } + + ;[' ', /\s{1}/].forEach((v) => { + expect( + splitEffects( + 'drop-shadow(4px 4px 10px blue) blur(4px) saturate(150%)', + v + ) + ).toEqual([ + 'drop-shadow(4px 4px 10px blue)', + 'blur(4px)', + 'saturate(150%)', + ]) + }) + }) })