From 461b0ef94fe6f693ead8d968b81653af4bf886e9 Mon Sep 17 00:00:00 2001 From: LuciNyan <22126563+LuciNyan@users.noreply.github.com> Date: Fri, 6 Oct 2023 23:12:54 +0800 Subject: [PATCH] fix: text-shadow (#555) ### Description - Fix text-shadow. - Refactor the **text** to improve readability. Since the refactoring changes may be extensive, to prevent bugs caused by refactoring from being hard to locate, it will be implemented in multiple PRs step-by-step. --- src/builder/shadow.ts | 9 +- src/font.ts | 51 +++- src/handler/expand.ts | 66 ++++- src/index.ts | 2 +- src/layout.ts | 2 +- src/{ => text}/characters.ts | 0 src/{text.ts => text/index.ts} | 251 +++--------------- src/text/measurer.ts | 54 ++++ src/text/processor.ts | 178 +++++++++++++ ...ld-support-multiple-box-shadows-1-snap.png | Bin 2746 -> 0 bytes ...d-support-multiple-text-shadows-1-snap.png | Bin 0 -> 6827 bytes test/shadow.test.tsx | 4 +- 12 files changed, 381 insertions(+), 236 deletions(-) rename src/{ => text}/characters.ts (100%) rename src/{text.ts => text/index.ts} (79%) create mode 100644 src/text/measurer.ts create mode 100644 src/text/processor.ts delete mode 100644 test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-box-shadows-1-snap.png create mode 100644 test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-text-shadows-1-snap.png diff --git a/src/builder/shadow.ts b/src/builder/shadow.ts index f984e582..f199ca48 100644 --- a/src/builder/shadow.ts +++ b/src/builder/shadow.ts @@ -24,7 +24,14 @@ const SCALE = 1.1 export function buildDropShadow( { id, width, height }: { id: string; width: number; height: number }, - style: Record + style: { + shadowColor: string[] + shadowOffset: { + width: number + height: number + }[] + shadowRadius: number[] + } ) { if ( !style.shadowColor || diff --git a/src/font.ts b/src/font.ts index 34062eb3..cf03a395 100644 --- a/src/font.ts +++ b/src/font.ts @@ -5,15 +5,16 @@ import opentype from '@shuding/opentype.js' import { Locale, locales, isValidLocale } from './language.js' export type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 -type WeightName = 'normal' | 'bold' -export type Style = 'normal' | 'italic' +export type WeightName = 'normal' | 'bold' +export type FontWeight = Weight | WeightName +export type FontStyle = 'normal' | 'italic' const SUFFIX_WHEN_LANG_NOT_SET = 'unknown' export interface FontOptions { data: Buffer | ArrayBuffer name: string weight?: Weight - style?: Style + style?: FontStyle lang?: string } @@ -21,8 +22,22 @@ export type FontEngine = { has: (s: string) => boolean baseline: (s?: string, resolvedFont?: any) => number height: (s?: string, resolvedFont?: any) => number - measure: (s: string, style: any) => number - getSVG: (s: string, style: any) => string + measure: ( + s: string, + style: { + fontSize: number + letterSpacing: number + } + ) => number + getSVG: ( + s: string, + style: { + fontSize: number + top: number + left: number + letterSpacing: number + } + ) => string } function compareFont( @@ -74,7 +89,7 @@ function compareFont( export default class FontLoader { defaultFont: opentype.Font - fonts = new Map() + fonts = new Map() constructor(fontOptions: FontOptions[]) { this.addFonts(fontOptions) } @@ -87,7 +102,7 @@ export default class FontLoader { }: { name: string weight: Weight | WeightName - style: Style + style: FontStyle }) { if (!this.fonts.has(name)) { return null @@ -175,8 +190,8 @@ export default class FontLoader { fontStyle = 'normal', }: { fontFamily?: string | string[] - fontWeight?: Weight | WeightName - fontStyle?: Style + fontWeight?: FontWeight + fontStyle?: FontStyle }, locale: Locale | undefined ): FontEngine { @@ -351,10 +366,24 @@ export default class FontLoader { (lineHeight / 1.2) ) }, - measure: (s: string, style: any) => { + measure: ( + s: string, + style: { + fontSize: number + letterSpacing: number + } + ) => { return this.measure(resolveFont, s, style) }, - getSVG: (s: string, style: any) => { + getSVG: ( + s: string, + style: { + fontSize: number + top: number + left: number + letterSpacing: number + } + ) => { return this.getSVG(resolveFont, s, style) }, } diff --git a/src/handler/expand.ts b/src/handler/expand.ts index 45be94b1..92ba9d9f 100644 --- a/src/handler/expand.ts +++ b/src/handler/expand.ts @@ -14,6 +14,7 @@ import parseTransformOrigin, { } from '../transform-origin.js' import { isString, lengthToNumber, v, splitEffects } from '../utils.js' import { MaskProperty, parseMask } from '../parser/mask.js' +import { FontWeight, FontStyle } from '../font.js' // https://react-cn.github.io/react/tips/style-props-value-px.html const optOutPx = new Set([ @@ -172,21 +173,22 @@ function handleSpecialCase( if (name === 'textShadow') { // Handle multiple text shadows if provided. value = value.toString().trim() - if (value.includes(',')) { - const shadows = splitEffects(value) - const result = {} - for (const shadow of shadows) { - const styles = getStylesForProperty('textShadow', shadow, true) - for (const k in styles) { - if (!result[k]) { - result[k] = [styles[k]] - } else { - result[k].push(styles[k]) - } + const result = {} + + const shadows = splitEffects(value) + + for (const shadow of shadows) { + const styles = getStylesForProperty('textShadow', shadow, true) + for (const k in styles) { + if (!result[k]) { + result[k] = [styles[k]] + } else { + result[k].push(styles[k]) } } - return result } + + return result } return @@ -232,6 +234,11 @@ type MainStyle = { wordBreak: string textAlign: string lineHeight: number + letterSpacing: number + + fontFamily: string | string[] + fontWeight: FontWeight + fontStyle: FontStyle borderTopWidth: number borderLeftWidth: number @@ -249,6 +256,13 @@ type MainStyle = { gap: number rowGap: number columnGap: number + + textShadowOffset: { + width: number + height: number + }[] + textShadowColor: string[] + textShadowRadius: number[] } type OtherStyle = Exclude, keyof MainStyle> @@ -391,6 +405,34 @@ export default function expand( transform[type] = len } } + + if (prop === 'textShadowRadius') { + const textShadowRadius = value as unknown as Array + + serializedStyle.textShadowRadius = textShadowRadius.map((_v) => + lengthToNumber(_v, baseFontSize, 0, inheritedStyle, false) + ) + } + + if (prop === 'textShadowOffset') { + const textShadowOffset = value as unknown as Array<{ + width: number | string + height: number | string + }> + + serializedStyle.textShadowOffset = textShadowOffset.map( + ({ height, width }) => ({ + height: lengthToNumber( + height, + baseFontSize, + 0, + inheritedStyle, + false + ), + width: lengthToNumber(width, baseFontSize, 0, inheritedStyle, false), + }) + ) + } } return serializedStyle diff --git a/src/index.ts b/src/index.ts index dc0f4afa..11161a6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ export type { FontOptions as Font, Weight as FontWeight, - Style as FontStyle, + FontStyle, } from './font.js' export type { Locale } from './language.js' diff --git a/src/layout.ts b/src/layout.ts index ae1404ff..70734036 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -16,7 +16,7 @@ import { import { SVGNodeToImage } from './handler/preprocess.js' import computeStyle from './handler/compute.js' import FontLoader from './font.js' -import buildTextNodes from './text.js' +import buildTextNodes from './text/index.js' import rect from './builder/rect.js' import { Locale, normalizeLocale } from './language.js' import { SerializedStyle } from './handler/expand.js' diff --git a/src/characters.ts b/src/text/characters.ts similarity index 100% rename from src/characters.ts rename to src/text/characters.ts diff --git a/src/text.ts b/src/text/index.ts similarity index 79% rename from src/text.ts rename to src/text/index.ts index f789c756..9204d787 100644 --- a/src/text.ts +++ b/src/text/index.ts @@ -2,26 +2,25 @@ * This module calculates the layout of a text string. Currently the only * supported inline node is text. All other nodes are using block layout. */ -import type { LayoutContext } from './layout.js' +import type { LayoutContext } from '../layout.js' import type { Yoga } from 'yoga-wasm-web' -import getYoga from './yoga/index.js' +import getYoga from '../yoga/index.js' import { v, segment, wordSeparators, buildXMLString, - splitByBreakOpportunities, isUndefined, isString, lengthToNumber, - isNumber, -} from './utils.js' -import buildText, { container } from './builder/text.js' -import { buildDropShadow } from './builder/shadow.js' -import buildDecoration from './builder/text-decoration.js' -import { Locale } from './language.js' -import { FontEngine } from './font.js' +} from '../utils.js' +import buildText, { container } from '../builder/text.js' +import { buildDropShadow } from '../builder/shadow.js' +import buildDecoration from '../builder/text-decoration.js' +import { Locale } from '../language.js' import { HorizontalEllipsis, Space, Tab } from './characters.js' +import { genMeasurer } from './measurer.js' +import { preprocess } from './processor.js' const skippedWordWhenFindingMissingFont = new Set([Tab]) @@ -51,49 +50,41 @@ export default async function* buildTextNodes( const { textAlign, - whiteSpace, - wordBreak, lineHeight, - textTransform, textWrap, fontSize, filter: cssFilter, tabSize = 8, + letterSpacing, _inheritedBackgroundClipTextPath, + flexShrink, } = parentStyle - content = processTextTransform(content, textTransform, locale) - const { - content: _content, - shouldCollapseTabsAndSpaces, + words, + requiredBreaks, allowSoftWrap, - } = processWhiteSpace(content, whiteSpace) - - const { words, requiredBreaks, allowBreakWord } = processWordBreak( - _content, - wordBreak - ) - - const [lineLimit, blockEllipsis] = processTextOverflow( - parentStyle, - allowSoftWrap - ) + allowBreakWord, + processedContent, + shouldCollapseTabsAndSpaces, + lineLimit, + blockEllipsis, + } = preprocess(content, parentStyle, locale) const textContainer = createTextContainerNode(Yoga, textAlign) parent.insertChild(textContainer, parent.getChildCount()) - if (isUndefined(parentStyle.flexShrink)) { + if (isUndefined(flexShrink)) { parent.setFlexShrink(1) } // Get the correct font according to the container style. // https://www.w3.org/TR/CSS2/visudet.html - let engine = font.getEngine(fontSize, lineHeight, parentStyle as any, locale) + let engine = font.getEngine(fontSize, lineHeight, parentStyle, locale) // Yield segments that are missing a font. const wordsMissingFont = canLoadAdditionalAssets - ? segment(_content, 'grapheme').filter( + ? segment(processedContent, 'grapheme').filter( (word) => !shouldSkipWhenFindingMissingFont(word) && !engine.has(word) ) : [] @@ -107,34 +98,21 @@ export default async function* buildTextNodes( if (wordsMissingFont.length) { // Reload the engine with additional fonts. - engine = font.getEngine(fontSize, lineHeight, parentStyle as any, locale) + engine = font.getEngine(fontSize, lineHeight, parentStyle, locale) } function isImage(s: string): boolean { return !!(graphemeImages && graphemeImages[s]) } - // We can cache the measured width of each word as the measure function will be - // called multiple times. - const measureGrapheme = genMeasureGrapheme(engine, parentStyle) - - function measureGraphemeArray(segments: string[]): number { - let width = 0 - - for (const s of segments) { - if (isImage(s)) { - width += fontSize - } else { - width += measureGrapheme(s) - } + const { measureGrapheme, measureGraphemeArray, measureText } = genMeasurer( + engine, + isImage, + { + fontSize, + letterSpacing, } - - return width - } - - function measureText(text: string): number { - return measureGraphemeArray(segment(text, 'grapheme')) - } + ) const tabWidth = isString(tabSize) ? lengthToNumber(tabSize, fontSize, 1, parentStyle) @@ -157,11 +135,11 @@ export default async function* buildTextNodes( } const { index, tabCount } = detectTabs(text) + let originWidth = 0 - let textBeforeTab = '' if (tabCount > 0) { - textBeforeTab = text.slice(0, index) + const textBeforeTab = text.slice(0, index) const textAfterTab = text.slice(index + tabCount) const textWidthBeforeTab = measureText(textBeforeTab) const offsetBeforeTab = textWidthBeforeTab + currentWidth @@ -189,7 +167,7 @@ export default async function* buildTextNodes( let lineWidths = [] let baselines = [] let lineSegmentNumber = [] - let texts = [] + let texts: string[] = [] let wordPositionInLayout: (null | { x: number y: number @@ -287,7 +265,7 @@ export default async function* buildTextNodes( if (forceBreak || willWrap) { // Start a new line, spaces can be ignored. // @TODO Lack of support for Japanese spacing - if (shouldCollapseTabsAndSpaces && word === ' ') { + if (shouldCollapseTabsAndSpaces && word === Space) { w = 0 } @@ -408,16 +386,18 @@ export default async function* buildTextNodes( } } flow(r) - measuredTextSize = { width: r, height } - return { width: Math.ceil(r), height } + const _width = Math.ceil(r) + measuredTextSize = { width: _width, height } + return { width: _width, height } } - measuredTextSize = { width, height } + const _width = Math.ceil(width) + measuredTextSize = { width: _width, height } // This may be a temporary fix, I didn't dig deep into yoga. // But when the return value of width here doesn't change (assuming the value of width is 216.9), // when we later get the width through `parent.getComputedWidth()`, sometimes it returns 216 and sometimes 217. // I'm not sure if this is a yoga bug, but it seems related to the entire page width. // So I use Math.ceil. - return { width: Math.ceil(width), height } + return { width: _width, height } }) const [x, y] = yield @@ -459,13 +439,7 @@ export default async function* buildTextNodes( let filter = '' if (parentStyle.textShadowOffset) { - let { textShadowColor, textShadowOffset, textShadowRadius } = - parentStyle as any - if (!Array.isArray(parentStyle.textShadowOffset)) { - textShadowColor = [textShadowColor] - textShadowOffset = [textShadowOffset] - textShadowRadius = [textShadowRadius] - } + const { textShadowColor, textShadowOffset, textShadowRadius } = parentStyle filter = buildDropShadow( { @@ -655,11 +629,11 @@ export default async function* buildTextNodes( const finalizedWidth = layout.width + leftOffset - finalizedLeftOffset path = engine.getSVG(finalizedSegment.replace(/(\t)+/g, ''), { - ...parentStyle, + fontSize, left: left + finalizedLeftOffset, // Since we need to pass the baseline position, add the ascender to the top. top: top + topOffset + baselineOfWord + baselineDelta, - letterSpacing: parentStyle.letterSpacing, + letterSpacing, }) wordBuffer = null @@ -790,104 +764,6 @@ export default async function* buildTextNodes( return result } -function processTextTransform( - content: string, - textTransform: string, - locale?: Locale -): string { - if (textTransform === 'uppercase') { - content = content.toLocaleUpperCase(locale) - } else if (textTransform === 'lowercase') { - content = content.toLocaleLowerCase(locale) - } else if (textTransform === 'capitalize') { - content = segment(content, 'word', locale) - // For each word... - .map((word) => { - // ...split into graphemes... - return segment(word, 'grapheme', locale) - .map((grapheme, index) => { - // ...and make the first grapheme uppercase - return index === 0 ? grapheme.toLocaleUpperCase(locale) : grapheme - }) - .join('') - }) - .join('') - } - - return content -} - -function processTextOverflow( - parentStyle: Record, - allowSoftWrap: boolean -): [number, string?] { - const { - textOverflow, - lineClamp, - WebkitLineClamp, - WebkitBoxOrient, - overflow, - display, - } = parentStyle - - if (display === 'block' && lineClamp) { - const [lineLimit, blockEllipsis = HorizontalEllipsis] = - parseLineClamp(lineClamp) - if (lineLimit) { - return [lineLimit, blockEllipsis] - } - } - - if ( - textOverflow === 'ellipsis' && - display === '-webkit-box' && - WebkitBoxOrient === 'vertical' && - isNumber(WebkitLineClamp) && - WebkitLineClamp > 0 - ) { - return [WebkitLineClamp, HorizontalEllipsis] - } - - if (textOverflow === 'ellipsis' && overflow === 'hidden' && !allowSoftWrap) { - return [1, HorizontalEllipsis] - } - - return [Infinity] -} - -function processWordBreak(content, wordBreak: string) { - const allowBreakWord = ['break-all', 'break-word'].includes(wordBreak) - - const { words, requiredBreaks } = splitByBreakOpportunities( - content, - wordBreak - ) - - return { words, requiredBreaks, allowBreakWord } -} - -function processWhiteSpace(content: string, whiteSpace: string) { - const shouldKeepLinebreak = ['pre', 'pre-wrap', 'pre-line'].includes( - whiteSpace - ) - - const shouldCollapseTabsAndSpaces = ['normal', 'nowrap', 'pre-line'].includes( - whiteSpace - ) - - const allowSoftWrap = !['pre', 'nowrap'].includes(whiteSpace) - - if (!shouldKeepLinebreak) { - content = content.replace(/\n/g, Space) - } - - if (shouldCollapseTabsAndSpaces) { - content = content.replace(/([ ]|\t)+/g, Space).trim() - } - - return { content, shouldCollapseTabsAndSpaces, allowSoftWrap } -} - function createTextContainerNode( Yoga: Yoga, textAlign: string @@ -915,24 +791,6 @@ function createTextContainerNode( return textContainer } -function genMeasureGrapheme( - engine: FontEngine, - parentStyle: any -): (s: string) => number { - const cache = new Map() - - return function measureGrapheme(s: string): number { - if (cache.has(s)) { - return cache.get(s) - } - - const width = engine.measure(s, parentStyle) - cache.set(s, width) - - return width - } -} - function detectTabs(text: string): | { index: null @@ -953,26 +811,3 @@ function detectTabs(text: string): tabCount: 0, } } - -function parseLineClamp(input: number | string): [number?, string?] { - if (typeof input === 'number') return [input] - - const regex1 = /^(\d+)\s*"(.*)"$/ - const regex2 = /^(\d+)\s*'(.*)'$/ - const match1 = regex1.exec(input) - const match2 = regex2.exec(input) - - if (match1) { - const number = +match1[1] - const text = match1[2] - - return [number, text] - } else if (match2) { - const number = +match2[1] - const text = match2[2] - - return [number, text] - } - - return [] -} diff --git a/src/text/measurer.ts b/src/text/measurer.ts new file mode 100644 index 00000000..09bf224d --- /dev/null +++ b/src/text/measurer.ts @@ -0,0 +1,54 @@ +import { FontEngine } from '../font.js' +import { segment } from '../utils.js' + +export function genMeasurer( + engine: FontEngine, + isImage: (grapheme: string) => boolean, + style: { + fontSize: number + letterSpacing: number + } +): { + measureGrapheme: (grapheme: string) => number + measureGraphemeArray: (graphemes: string[]) => number + measureText: (text: string) => number +} { + const { fontSize, letterSpacing } = style + + const cache = new Map() + + function measureGrapheme(grapheme: string): number { + if (cache.has(grapheme)) { + return cache.get(grapheme) + } + + const width = engine.measure(grapheme, { fontSize, letterSpacing }) + cache.set(grapheme, width) + + return width + } + + function measureGraphemeArray(graphemes: string[]): number { + let width = 0 + + for (const grapheme of graphemes) { + if (isImage(grapheme)) { + width += fontSize + } else { + width += measureGrapheme(grapheme) + } + } + + return width + } + + function measureText(text: string): number { + return measureGraphemeArray(segment(text, 'grapheme')) + } + + return { + measureGrapheme, + measureGraphemeArray, + measureText, + } +} diff --git a/src/text/processor.ts b/src/text/processor.ts new file mode 100644 index 00000000..17784155 --- /dev/null +++ b/src/text/processor.ts @@ -0,0 +1,178 @@ +import { Locale } from '../language.js' +import { isNumber, segment, splitByBreakOpportunities } from '../utils.js' +import { HorizontalEllipsis, Space } from './characters.js' +import { SerializedStyle } from '../handler/expand.js' + +export function preprocess( + content: string, + style: SerializedStyle, + locale?: Locale +): { + words: string[] + requiredBreaks: boolean[] + allowSoftWrap: boolean + allowBreakWord: boolean + processedContent: string + shouldCollapseTabsAndSpaces: boolean + lineLimit: number + blockEllipsis: string +} { + const { textTransform, whiteSpace, wordBreak } = style + + content = processTextTransform(content, textTransform, locale) + + const { + content: processedContent, + shouldCollapseTabsAndSpaces, + allowSoftWrap, + } = processWhiteSpace(content, whiteSpace) + + const { words, requiredBreaks, allowBreakWord } = processWordBreak( + processedContent, + wordBreak + ) + + const [lineLimit, blockEllipsis] = processTextOverflow(style, allowSoftWrap) + + return { + words, + requiredBreaks, + allowSoftWrap, + allowBreakWord, + processedContent, + shouldCollapseTabsAndSpaces, + lineLimit, + blockEllipsis, + } +} + +function processTextTransform( + content: string, + textTransform: string, + locale?: Locale +): string { + if (textTransform === 'uppercase') { + content = content.toLocaleUpperCase(locale) + } else if (textTransform === 'lowercase') { + content = content.toLocaleLowerCase(locale) + } else if (textTransform === 'capitalize') { + content = segment(content, 'word', locale) + // For each word... + .map((word) => { + // ...split into graphemes... + return segment(word, 'grapheme', locale) + .map((grapheme, index) => { + // ...and make the first grapheme uppercase + return index === 0 ? grapheme.toLocaleUpperCase(locale) : grapheme + }) + .join('') + }) + .join('') + } + + return content +} + +function processTextOverflow( + style: SerializedStyle, + allowSoftWrap: boolean +): [number, string?] { + const { + textOverflow, + lineClamp, + WebkitLineClamp, + WebkitBoxOrient, + overflow, + display, + } = style + + if (display === 'block' && lineClamp) { + const [lineLimit, blockEllipsis = HorizontalEllipsis] = + parseLineClamp(lineClamp) + if (lineLimit) { + return [lineLimit, blockEllipsis] + } + } + + if ( + textOverflow === 'ellipsis' && + display === '-webkit-box' && + WebkitBoxOrient === 'vertical' && + isNumber(WebkitLineClamp) && + WebkitLineClamp > 0 + ) { + return [WebkitLineClamp, HorizontalEllipsis] + } + + if (textOverflow === 'ellipsis' && overflow === 'hidden' && !allowSoftWrap) { + return [1, HorizontalEllipsis] + } + + return [Infinity] +} + +function processWordBreak( + content, + wordBreak: string +): { words: string[]; requiredBreaks: boolean[]; allowBreakWord: boolean } { + const allowBreakWord = ['break-all', 'break-word'].includes(wordBreak) + + const { words, requiredBreaks } = splitByBreakOpportunities( + content, + wordBreak + ) + + return { words, requiredBreaks, allowBreakWord } +} + +function processWhiteSpace( + content: string, + whiteSpace: string +): { + content: string + shouldCollapseTabsAndSpaces: boolean + allowSoftWrap: boolean +} { + const shouldKeepLinebreak = ['pre', 'pre-wrap', 'pre-line'].includes( + whiteSpace + ) + + const shouldCollapseTabsAndSpaces = ['normal', 'nowrap', 'pre-line'].includes( + whiteSpace + ) + + const allowSoftWrap = !['pre', 'nowrap'].includes(whiteSpace) + + if (!shouldKeepLinebreak) { + content = content.replace(/\n/g, Space) + } + + if (shouldCollapseTabsAndSpaces) { + content = content.replace(/([ ]|\t)+/g, Space).trim() + } + + return { content, shouldCollapseTabsAndSpaces, allowSoftWrap } +} + +function parseLineClamp(input: number | string): [number?, string?] { + if (typeof input === 'number') return [input] + + const regex1 = /^(\d+)\s*"(.*)"$/ + const regex2 = /^(\d+)\s*'(.*)'$/ + const match1 = regex1.exec(input) + const match2 = regex2.exec(input) + + if (match1) { + const number = +match1[1] + const text = match1[2] + + return [number, text] + } else if (match2) { + const number = +match2[1] + const text = match2[2] + + return [number, text] + } + + return [] +} diff --git a/test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-box-shadows-1-snap.png b/test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-box-shadows-1-snap.png deleted file mode 100644 index 4931a5bd112fea4aedcac387d20576622a44bed8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2746 zcmd6pX*d)L7ssW@PSUlEFoa>0eJc_(29X#hRI-dMV;>PQuF;HL6qDV>*ix9Rld(;f zv6Csp#bmp7V=0Qf?)&}y{(d;;d^qR)p8s<`oaY~JWog38CC0_V!oq823Wc9mw||3! z{WJ%B%dll(;fgkc8rnY1-N=sg5$X}`mdxP?2)0WtumDM%QmKmA!c-_dT@xt%I0HKc z=dtA#5U?DxD!1a`6S{G!LLq)YQqzhJYn=->iO7;;HR>~Qzbz|-<%Abv!yW6{9$c7+ z;E267z1+h5`fRPar8e?4Kz0{0%p3|24e`hi^EB9ena#=b)tP6M2N~xf0@3&=QY6|f zFsOp}!mC2nTSM^lFJFq@zkiwLdUU{~4Gnn+Ss)OXEDLT9nv#N7;2635>O3Go+plwH zv0qf7)FKOu>`CcJba&9f9O=m5u6-16qcsK2J5OneS2;F!52w_6jY_Z2wB)3@_7kpV z>Oa#eKA6`e>VUbyWL{CxTOM-aO4Y!W#~LbW=R`lXMMY_7XsE!Oe$#&hQ zRD&^lP&5~BF#}H0Ai0Dgr4oz5!W?_8rXDYzkno`pvcuUTtrWK1*)HeXg|IEz0e<$~aWy zaiBa^z4nL_r|vmaLrm2g%GTsICn&{FzzphCiA6=yDs!Jk1Fp!*x-1Q3P1Jhwn-|8H z0d;kCqdCz(HXR8@7w-U$N2mg-$e~=_w{2M^w1}-VlkJWY^Q5mAl_9Vc@s%466jOlq zW!+B16_ohXwNG*GypcN~R7ydxIo$@w&H1@k73|`TzDo!7F$`p8y3Y&ClZq>p0w>!o zj|3`36fCXymn4k4CAj%WT32*+vq*}1hL?%r>YF8&os;=&IuViL<_SGaZ3>>OL$%Me zJbpaDPRw)c0I9X&JGoBrE2G6Gs1PAp`T21tOmAFF;0frh`Slb{Z3$k5?VlDFMKMsArx&4N4#(tvkNs{n*XcJ=8Vq#Zoay}PK7m-gb8q@MyX!snMP+ezCKx$(uc(wA1WwEY zwuI@U^Kn=1-h*RpUaN#C)%Z)MX2nZQBMXA%R)hE7)`cDA$KzC>?s37TG7(96;s=eQ3O?nAXn>mM zkn=-w5bi-Uo)_^$ajCCYVdbILH)Nd6a9b8wbR9QGzr+QG9w!fEXe{-8-|&v-Hnzp< zBqvE^ZTTh=cs0HJAVKJu;m-~}=f)l3f=&=Of@l3g!ay*&3P;_kaH@)TcjqvRE+dr&ZJVQbCKB zgi{$>wVC(LI5&Sn(o`THLaV6@y|2g!YsvE>O3FKL|2jz8!<4L3&hxC(2vXa%`-Mk%Yidxt1I+=pS@Sp z;A!%Lw$ppM?CEP_P;nC+A-yJ!EM5KJXmdo+FHVUY)$^h!rP9tJ zA^%U&{RjnRe%771nH}OKeh91YLgyqS93l`OPyc0mUAC#fiAZKVD!4K&5Sw5alWJ$z z15(@S{V`q;*=-uQaU;j-*Dqn#ctL*1IaT*x+YHdRP+%J9pP9?bV-!phPWi)~j@lo5 zY@4jOY%wYIi$G2_=oj;;1}3^yd9|PG^J44`++#N#x4hT0--q0@4J@wCX=N#3@Bwmj zZL^NaQ4#mWo(ebQ2g&#zp4q?Jw^ASH(Np&-eRPZf(%j-(N~@IxP#efyzQSw+;P*WfSgdzl z?Wp}!Z0I&Ot9sY4z<1%TW8#I6`~_5zrQ09m?N%oVZR&R4)$MXqxC}iweA@QrckxPe z1V8K&+Fb%&k_QYDSgcdcZoLgS(JTDp#iKXh)O-1eBR;Wf0~ul#i#zFq*=kM3Ko6RD zZy#25#L|U6O%V3GilXPZl8L|3nyMo&Cc!5$p2xVl7QJR|Mx9s6wmqM Vn^{b+fKyM(VrFa!tuk`P{s*Zp8vy_S diff --git a/test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-text-shadows-1-snap.png b/test/__image_snapshots__/shadow-test-tsx-test-shadow-test-tsx-shadow-box-shadow-should-support-multiple-text-shadows-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..e796cab6c59281b26b5805fa3630f12aa56cc157 GIT binary patch literal 6827 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 literal 0 HcmV?d00001 diff --git a/test/shadow.test.tsx b/test/shadow.test.tsx index df9cdb6a..95b2c856 100644 --- a/test/shadow.test.tsx +++ b/test/shadow.test.tsx @@ -195,7 +195,7 @@ describe('Shadow', () => { expect(toImage(svg, 100)).toMatchImageSnapshot() }) - it('should support multiple box shadows', async () => { + it('should support multiple text shadows', async () => { const svg = await satori(
{ width: 100, height: 100, fontSize: 40, - textShadow: '2px 2px red, 4px 4px blue', + textShadow: '2px 2px 2px red, 4px .25rem .25rem blue', }} > Hello