diff --git a/src/handler/index.ts b/src/handler/compute.ts similarity index 85% rename from src/handler/index.ts rename to src/handler/compute.ts index 6f6c5a32..a378f58f 100644 --- a/src/handler/index.ts +++ b/src/handler/compute.ts @@ -9,23 +9,23 @@ import type { Node as YogaNode } from 'yoga-wasm-web' import getYoga from '../yoga/index.js' import presets from './presets.js' import inheritable from './inheritable.js' -import expand from './expand.js' +import expand, { SerializedStyle } from './expand.js' import { lengthToNumber, parseViewBox, v } from '../utils.js' import { resolveImageData } from './image.js' type SatoriElement = keyof typeof presets -export default async function handler( +export default async function compute( node: YogaNode, type: SatoriElement | string, - inheritedStyle: Record, + inheritedStyle: SerializedStyle, definedStyle: Record, props: Record -): Promise<[Record, Record]> { +): Promise<[SerializedStyle, SerializedStyle]> { const Yoga = await getYoga() // Extend the default style with defined and inherited styles. - const style = { + const style: SerializedStyle = { ...inheritedStyle, ...expand(presets[type], inheritedStyle), ...expand(definedStyle, inheritedStyle), @@ -52,15 +52,15 @@ export default async function handler( // we must subtract the padding and border due to how box model works. // TODO: Ensure these are absolute length values, not relative values. let extraHorizontal = - ((style.borderLeftWidth as number) || 0) + - ((style.borderRightWidth as number) || 0) + - ((style.paddingLeft as number) || 0) + - ((style.paddingRight as number) || 0) + (style.borderLeftWidth || 0) + + (style.borderRightWidth || 0) + + (style.paddingLeft || 0) + + (style.paddingRight || 0) let extraVertical = - ((style.borderTopWidth as number) || 0) + - ((style.borderBottomWidth as number) || 0) + - ((style.paddingTop as number) || 0) + - ((style.paddingBottom as number) || 0) + (style.borderTopWidth || 0) + + (style.borderBottomWidth || 0) + + (style.paddingTop || 0) + + (style.paddingBottom || 0) let contentBoxWidth = style.width || props.width let contentBoxHeight = style.height || props.height @@ -122,7 +122,7 @@ export default async function handler( } else { height = lengthToNumber( height, - inheritedStyle.fontSize as number, + inheritedStyle.fontSize, 1, inheritedStyle ) @@ -136,7 +136,7 @@ export default async function handler( } else { width = lengthToNumber( width, - inheritedStyle.fontSize as number, + inheritedStyle.fontSize, 1, inheritedStyle ) @@ -145,21 +145,13 @@ export default async function handler( } else { if (typeof width !== 'undefined') { width = - lengthToNumber( - width, - inheritedStyle.fontSize as number, - 1, - inheritedStyle - ) || width + lengthToNumber(width, inheritedStyle.fontSize, 1, inheritedStyle) || + width } if (typeof height !== 'undefined') { height = - lengthToNumber( - height, - inheritedStyle.fontSize as number, - 1, - inheritedStyle - ) || height + lengthToNumber(height, inheritedStyle.fontSize, 1, inheritedStyle) || + height } width ||= viewBoxSize?.[2] height ||= viewBoxSize?.[3] @@ -275,15 +267,15 @@ export default async function handler( ) if (typeof style.gap !== 'undefined') { - node.setGap(Yoga.GUTTER_ALL, style.gap as number) + node.setGap(Yoga.GUTTER_ALL, style.gap) } if (typeof style.rowGap !== 'undefined') { - node.setGap(Yoga.GUTTER_ROW, style.rowGap as number) + node.setGap(Yoga.GUTTER_ROW, style.rowGap) } if (typeof style.columnGap !== 'undefined') { - node.setGap(Yoga.GUTTER_COLUMN, style.columnGap as number) + node.setGap(Yoga.GUTTER_COLUMN, style.columnGap) } // @TODO: node.setFlex @@ -291,11 +283,9 @@ export default async function handler( if (typeof style.flexBasis !== 'undefined') { node.setFlexBasis(style.flexBasis) } - node.setFlexGrow( - typeof style.flexGrow === 'undefined' ? 0 : (style.flexGrow as number) - ) + node.setFlexGrow(typeof style.flexGrow === 'undefined' ? 0 : style.flexGrow) node.setFlexShrink( - typeof style.flexShrink === 'undefined' ? 0 : (style.flexShrink as number) + typeof style.flexShrink === 'undefined' ? 0 : style.flexShrink ) if (typeof style.maxHeight !== 'undefined') { @@ -328,10 +318,10 @@ export default async function handler( node.setMargin(Yoga.EDGE_LEFT, style.marginLeft || 0) node.setMargin(Yoga.EDGE_RIGHT, style.marginRight || 0) - node.setBorder(Yoga.EDGE_TOP, (style.borderTopWidth as number) || 0) - node.setBorder(Yoga.EDGE_BOTTOM, (style.borderBottomWidth as number) || 0) - node.setBorder(Yoga.EDGE_LEFT, (style.borderLeftWidth as number) || 0) - node.setBorder(Yoga.EDGE_RIGHT, (style.borderRightWidth as number) || 0) + node.setBorder(Yoga.EDGE_TOP, style.borderTopWidth || 0) + node.setBorder(Yoga.EDGE_BOTTOM, style.borderBottomWidth || 0) + node.setBorder(Yoga.EDGE_LEFT, style.borderLeftWidth || 0) + node.setBorder(Yoga.EDGE_RIGHT, style.borderRightWidth || 0) node.setPadding(Yoga.EDGE_TOP, style.paddingTop || 0) node.setPadding(Yoga.EDGE_BOTTOM, style.paddingBottom || 0) diff --git a/src/handler/expand.ts b/src/handler/expand.ts index 7457ce4b..fc668ea5 100644 --- a/src/handler/expand.ts +++ b/src/handler/expand.ts @@ -9,9 +9,11 @@ import { parse as parseBoxShadow } from 'css-box-shadow' import cssColorParse from 'parse-css-color' import CssDimension from '../vendor/parse-css-dimension/index.js' -import parseTransformOrigin from '../transform-origin.js' +import parseTransformOrigin, { + ParsedTransformOrigin, +} from '../transform-origin.js' import { isString, lengthToNumber, v } from '../utils.js' -import { parseMask } from '../parser/mask.js' +import { MaskProperty, parseMask } from '../parser/mask.js' // https://react-cn.github.io/react/tips/style-props-value-px.html const optOutPx = new Set([ @@ -243,24 +245,58 @@ function normalizeColor(value: string | object) { return value } +type MainStyle = { + color: string + fontSize: number + transformOrigin: ParsedTransformOrigin + maskImage: MaskProperty[] + opacity: number + textTransform: string + whiteSpace: string + wordBreak: string + textAlign: string + lineHeight: number + + borderTopWidth: number + borderLeftWidth: number + borderRightWidth: number + borderBottomWidth: number + + paddingTop: number + paddingLeft: number + paddingRight: number + paddingBottom: number + + flexGrow: number + flexShrink: number + + gap: number + rowGap: number + columnGap: number +} + +type OtherStyle = Exclude, keyof MainStyle> + +export type SerializedStyle = Partial + export default function expand( style: Record | undefined, - inheritedStyle: Record -): Record { - const transformedStyle = {} as any + inheritedStyle: SerializedStyle +): SerializedStyle { + const serializedStyle: SerializedStyle = {} if (style) { const currentColor = getCurrentColor( style.color as string, - inheritedStyle.color as string + inheritedStyle.color ) - transformedStyle.color = currentColor + serializedStyle.color = currentColor for (const prop in style) { // Internal properties. if (prop.startsWith('_')) { - transformedStyle[prop] = style[prop] + serializedStyle[prop] = style[prop] continue } @@ -281,7 +317,7 @@ export default function expand( currentColor ) - Object.assign(transformedStyle, resolvedStyle) + Object.assign(serializedStyle, resolvedStyle) } catch (err) { throw new Error( err.message + @@ -296,39 +332,38 @@ export default function expand( } // Parse background images. - if (transformedStyle.backgroundImage) { - const { backgrounds } = parseElementStyle(transformedStyle) - transformedStyle.backgroundImage = backgrounds + if (serializedStyle.backgroundImage) { + const { backgrounds } = parseElementStyle(serializedStyle) + serializedStyle.backgroundImage = backgrounds } - if (transformedStyle.maskImage || transformedStyle['WebkitMaskImage']) { - const mask = parseMask(transformedStyle) - transformedStyle.maskImage = mask + if (serializedStyle.maskImage || serializedStyle['WebkitMaskImage']) { + serializedStyle.maskImage = parseMask(serializedStyle) } // Calculate the base font size. const baseFontSize = calcBaseFontSize( - transformedStyle.fontSize, - inheritedStyle.fontSize as number + serializedStyle.fontSize, + inheritedStyle.fontSize ) - if (typeof transformedStyle.fontSize !== 'undefined') { - transformedStyle.fontSize = baseFontSize + if (typeof serializedStyle.fontSize !== 'undefined') { + serializedStyle.fontSize = baseFontSize } - if (transformedStyle.transformOrigin) { - transformedStyle.transformOrigin = parseTransformOrigin( - transformedStyle.transformOrigin, + if (serializedStyle.transformOrigin) { + serializedStyle.transformOrigin = parseTransformOrigin( + serializedStyle.transformOrigin as any, baseFontSize ) } - for (const prop in transformedStyle) { - let value = transformedStyle[prop] + for (const prop in serializedStyle) { + let value = serializedStyle[prop] // Line height needs to be relative. if (prop === 'lineHeight') { if (typeof value === 'string') { - value = transformedStyle[prop] = + value = serializedStyle[prop] = lengthToNumber( value, baseFontSize, @@ -346,25 +381,26 @@ export default function expand( baseFontSize, inheritedStyle ) - if (typeof len !== 'undefined') transformedStyle[prop] = len - value = transformedStyle[prop] + if (typeof len !== 'undefined') serializedStyle[prop] = len + value = serializedStyle[prop] } if (typeof value === 'string' || typeof value === 'object') { const color = normalizeColor(value) - if (color) transformedStyle[prop] = color - value = transformedStyle[prop] + if (color) { + serializedStyle[prop] = color as any + } + value = serializedStyle[prop] } } // Inherit the opacity. - if (prop === 'opacity') { - value = transformedStyle[prop] = - value * (inheritedStyle.opacity as number) + if (prop === 'opacity' && typeof value === 'number') { + serializedStyle.opacity = value * inheritedStyle.opacity } if (prop === 'transform') { - const transforms = value as { [type: string]: number | string }[] + const transforms = value as any as { [type: string]: number | string }[] for (const transform of transforms) { const type = Object.keys(transform)[0] @@ -381,10 +417,13 @@ export default function expand( } } - return transformedStyle + return serializedStyle } -function calcBaseFontSize(size: number | string, inheritedSize: number) { +function calcBaseFontSize( + size: number | string, + inheritedSize: number +): number { if (typeof size === 'number') return size try { @@ -416,7 +455,10 @@ function refineHSL(color: string) { return color } -function getCurrentColor(color: string | undefined, inheritedColor: string) { +function getCurrentColor( + color: string | undefined, + inheritedColor: string +): string { if (color && color.toLowerCase() !== 'currentcolor') { return refineHSL(color) } diff --git a/src/handler/inheritable.ts b/src/handler/inheritable.ts index c0e3bf50..07996e72 100644 --- a/src/handler/inheritable.ts +++ b/src/handler/inheritable.ts @@ -1,3 +1,5 @@ +import { SerializedStyle } from './expand.js' + const list = new Set([ 'color', 'font', @@ -33,8 +35,8 @@ const list = new Set([ '_inheritedBackgroundClipTextPath', ]) -export default function inheritable(style: Record) { - const inheritedStyle: Record = {} +export default function inheritable(style: SerializedStyle): SerializedStyle { + const inheritedStyle: SerializedStyle = {} for (const prop in style) { if (list.has(prop)) { inheritedStyle[prop] = style[prop] diff --git a/src/layout.ts b/src/layout.ts index 6ab3e125..ae1404ff 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -14,16 +14,17 @@ import { hasDangerouslySetInnerHTMLProp, } from './utils.js' import { SVGNodeToImage } from './handler/preprocess.js' -import handler from './handler/index.js' +import computeStyle from './handler/compute.js' import FontLoader from './font.js' -import layoutText from './text.js' +import buildTextNodes from './text.js' import rect from './builder/rect.js' import { Locale, normalizeLocale } from './language.js' +import { SerializedStyle } from './handler/expand.js' export interface LayoutContext { id: string - parentStyle: Record - inheritedStyle: Record + parentStyle: SerializedStyle + inheritedStyle: SerializedStyle isInheritingTransform?: boolean parent: YogaNode font: FontLoader @@ -83,7 +84,7 @@ export default async function* layout( if (!isReactElement(element)) { // Process as text node. - iter = layoutText(String(element), context) + iter = buildTextNodes(String(element), context) yield (await iter.next()).value as { word: string; locale?: Locale }[] } else { if (isClass(element.type as Function)) { @@ -121,7 +122,7 @@ export default async function* layout( const node = Yoga.Node.create() parent.insertChild(node, parent.getChildCount()) - const [computedStyle, newInheritableStyle] = await handler( + const [computedStyle, newInheritableStyle] = await computeStyle( node, type, inheritedStyle, @@ -238,7 +239,7 @@ export default async function* layout( } else if (type === 'svg') { // When entering a node, we need to convert it to a with the // SVG data URL embedded. - const currentColor = computedStyle.color as string + const currentColor = computedStyle.color const src = await SVGNodeToImage(element, currentColor) baseRenderResult = await rect( { diff --git a/src/text.ts b/src/text.ts index 72bbfdfb..0fabad69 100644 --- a/src/text.ts +++ b/src/text.ts @@ -62,17 +62,17 @@ export default async function* buildTextNodes( _inheritedBackgroundClipTextPath, } = parentStyle - content = processTextTransform(content, textTransform as string, locale) + content = processTextTransform(content, textTransform, locale) const { content: _content, shouldCollapseTabsAndSpaces, allowSoftWrap, - } = processWhiteSpace(content, whiteSpace as string) + } = processWhiteSpace(content, whiteSpace) const { words, requiredBreaks, allowBreakWord } = processWordBreak( _content, - wordBreak as string + wordBreak ) const [lineLimit, blockEllipsis] = processTextOverflow( @@ -80,7 +80,7 @@ export default async function* buildTextNodes( allowSoftWrap ) - const textContainer = createTextContainerNode(Yoga, textAlign as string) + const textContainer = createTextContainerNode(Yoga, textAlign) parent.insertChild(textContainer, parent.getChildCount()) if (isUndefined(parentStyle.flexShrink)) { @@ -89,12 +89,7 @@ export default async function* buildTextNodes( // Get the correct font according to the container style. // https://www.w3.org/TR/CSS2/visudet.html - let engine = font.getEngine( - fontSize as number, - lineHeight as number, - parentStyle as any, - locale - ) + let engine = font.getEngine(fontSize, lineHeight, parentStyle as any, locale) // Yield segments that are missing a font. const wordsMissingFont = canLoadAdditionalAssets @@ -112,12 +107,7 @@ export default async function* buildTextNodes( if (wordsMissingFont.length) { // Reload the engine with additional fonts. - engine = font.getEngine( - fontSize as number, - lineHeight as number, - parentStyle as any, - locale - ) + engine = font.getEngine(fontSize, lineHeight, parentStyle as any, locale) } function isImage(s: string): boolean { @@ -133,7 +123,7 @@ export default async function* buildTextNodes( for (const s of segments) { if (isImage(s)) { - width += fontSize as number + width += fontSize } else { width += measureGrapheme(s) } @@ -147,8 +137,8 @@ export default async function* buildTextNodes( } const tabWidth = isString(tabSize) - ? lengthToNumber(tabSize, fontSize as number, 1, parentStyle) - : measureGrapheme(Space) * (tabSize as number) + ? lengthToNumber(tabSize, fontSize, 1, parentStyle) + : measureGrapheme(Space) * tabSize const calc = ( text: string, @@ -357,7 +347,7 @@ export default async function* buildTextNodes( let _isImage = false if (isImage(_text)) { - _width = fontSize as number + _width = fontSize _isImage = true } else { _width = measureGrapheme(_text)