11import type { ContainerNode , ImageNode , Node , TextNode } from '@takumi-rs/core'
22import type { OgImageRenderEventContext , VNode } from '../../../types'
3- import { createVNodes , SVG_CAMEL_ATTR_VALUES } from '../core/vnodes'
3+ import { createVNodes , resolveSvgDimension , SVG_CAMEL_ATTR_VALUES } from '../core/vnodes'
4+
5+ const RE_RELATIVE_UNIT = / ^ ( [ \d . ] + ) ( e m | r e m ) $ /
6+ const RE_TW_TEXT_ARBITRARY = / (?: ^ | \s ) t e x t - \[ ( \d + (?: \. \d + ) ? ) ( p x | r e m | e m ) \] /
7+ const RE_FONT_SIZE_PX = / ^ ( \d + (?: \. \d + ) ? ) ( p x ) ? $ /
8+
9+ const DEFAULT_FONT_SIZE = 16
410
511const RE_UPPERCASE = / [ A - Z ] / g
612const RE_DQUOTE = / " / g
@@ -10,7 +16,7 @@ const RE_GT = />/g
1016
1117export async function createTakumiNodes ( ctx : OgImageRenderEventContext ) : Promise < Node > {
1218 const vnodeTree = await createVNodes ( ctx )
13- return await vnodeToTakumiNode ( vnodeTree )
19+ return await vnodeToTakumiNode ( vnodeTree , DEFAULT_FONT_SIZE )
1420}
1521
1622// Extract numeric width/height from HTML attributes
@@ -22,7 +28,41 @@ function pickNumericDimension(props: Record<string, any>, key: 'width' | 'height
2228 return Number . isNaN ( n ) ? undefined : n
2329}
2430
25- async function vnodeToTakumiNode ( vnode : VNode ) : Promise < Node > {
31+ /**
32+ * Resolve an em/rem value to pixels given the inherited font size.
33+ */
34+ function resolveRelativeUnit ( value : string | number | undefined , inheritedFontSize : number ) : number | undefined {
35+ if ( value == null )
36+ return undefined
37+ const match = String ( value ) . match ( RE_RELATIVE_UNIT )
38+ if ( ! match )
39+ return undefined
40+ const n = Number . parseFloat ( match [ 1 ] ! )
41+ return match [ 2 ] === 'rem' ? n * DEFAULT_FONT_SIZE : n * inheritedFontSize
42+ }
43+
44+ /**
45+ * Extract font size in px from a vnode's style or tailwind classes.
46+ */
47+ function extractFontSize ( props : Record < string , any > , style : Record < string , any > | undefined ) : number | undefined {
48+ // 1. Inline style fontSize
49+ if ( style ?. fontSize != null ) {
50+ const m = String ( style . fontSize ) . match ( RE_FONT_SIZE_PX )
51+ if ( m )
52+ return Number . parseFloat ( m [ 1 ] ! )
53+ }
54+ // 2. Tailwind arbitrary text-[Npx] from class or tw
55+ const twStr = props . tw || props . class || ''
56+ const twMatch = twStr . match ( RE_TW_TEXT_ARBITRARY )
57+ if ( twMatch ) {
58+ const val = Number . parseFloat ( twMatch [ 1 ] ! )
59+ if ( twMatch [ 2 ] === 'px' )
60+ return val
61+ }
62+ return undefined
63+ }
64+
65+ async function vnodeToTakumiNode ( vnode : VNode , inheritedFontSize : number ) : Promise < Node > {
2666 const { style, children, class : cls , tw, src, ...rest } = vnode . props
2767
2868 const baseMetadata = {
@@ -32,12 +72,25 @@ async function vnodeToTakumiNode(vnode: VNode): Promise<Node> {
3272
3373 // SVG elements → convert to SVG string
3474 if ( vnode . type === 'svg' ) {
75+ // Only resolve em/rem to pixels when we have an explicit font size from a parent
76+ // (e.g. text-[80px]). When using the default 16px, leave dimensions unset so
77+ // takumi can handle the SVG's native 1em sizing and keep inline layout.
78+ const hasExplicitFontSize = inheritedFontSize !== DEFAULT_FONT_SIZE
79+ const isRelativeW = RE_RELATIVE_UNIT . test ( String ( rest . width ?? '' ) )
80+ const isRelativeH = RE_RELATIVE_UNIT . test ( String ( rest . height ?? '' ) )
3581 return {
3682 ...baseMetadata ,
3783 type : 'image' ,
3884 src : vnodeToHtmlString ( vnode ) ,
39- width : pickNumericDimension ( rest , 'width' ) ,
40- height : pickNumericDimension ( rest , 'height' ) ,
85+ // When em/rem + explicit parent font size → resolve to px.
86+ // When em/rem + default font size → leave undefined (takumi handles natively).
87+ // Otherwise → use standard resolution chain (numeric attrs → style → viewBox).
88+ width : isRelativeW
89+ ? ( hasExplicitFontSize ? resolveRelativeUnit ( rest . width , inheritedFontSize ) : undefined )
90+ : resolveSvgDimension ( rest , style , 'width' ) ,
91+ height : isRelativeH
92+ ? ( hasExplicitFontSize ? resolveRelativeUnit ( rest . height , inheritedFontSize ) : undefined )
93+ : resolveSvgDimension ( rest , style , 'height' ) ,
4194 } satisfies ImageNode
4295 }
4396
@@ -51,6 +104,10 @@ async function vnodeToTakumiNode(vnode: VNode): Promise<Node> {
51104 } satisfies ImageNode
52105 }
53106
107+ // Compute inherited font size for children
108+ const nodeFontSize = extractFontSize ( vnode . props , style )
109+ const childFontSize = nodeFontSize ?? inheritedFontSize
110+
54111 // For non-image nodes, merge any explicit width/height into style
55112 const containerStyle = { ...style }
56113 const w = pickNumericDimension ( rest , 'width' )
@@ -87,7 +144,7 @@ async function vnodeToTakumiNode(vnode: VNode): Promise<Node> {
87144 const takumiChildren : Node [ ] = [ ]
88145 for ( const child of children ) {
89146 if ( child && typeof child === 'object' )
90- takumiChildren . push ( await vnodeToTakumiNode ( child ) )
147+ takumiChildren . push ( await vnodeToTakumiNode ( child , childFontSize ) )
91148 else if ( typeof child === 'string' && child . trim ( ) )
92149 takumiChildren . push ( { type : 'text' , text : child . trim ( ) } )
93150 }
0 commit comments