Skip to content

Commit 9cb3aa4

Browse files
committed
feat: enhance token handling and style normalization with new stacking and layout functions
1 parent 37c9f0a commit 9cb3aa4

File tree

13 files changed

+171
-36
lines changed

13 files changed

+171
-36
lines changed

packages/extension/mcp/tools/code/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ export async function handleGetCode(
143143
let { code, truncated } = truncateCode(rawMarkup, MAX_CODE_CHARS)
144144

145145
// Token pipeline: detect -> transform -> rewrite -> detect
146-
const candidateIds = usedCandidateIds.size ? usedCandidateIds : mappings.variableIds
146+
const candidateIds = usedCandidateIds.size
147+
? new Set<string>([...mappings.variableIds, ...usedCandidateIds])
148+
: mappings.variableIds
147149
const variableCache = new Map<string, Variable | null>()
148150
const sourceIndex = buildSourceNameIndex(candidateIds, variableCache)
149151
const sourceNames = new Set(sourceIndex.keys())
@@ -165,7 +167,7 @@ export async function handleGetCode(
165167
const usedNamesFinal = extractTokenNames(code, sourceNames)
166168
const finalBridgeFiltered = filterBridge(finalBridge, usedNamesFinal)
167169

168-
const { usedTokens, tokensByCanonical, canonicalByFinal } = await buildUsedTokens(
170+
const { usedTokens, tokensByCanonical, canonicalById } = await buildUsedTokens(
169171
usedNamesFinal,
170172
finalBridgeFiltered,
171173
config,
@@ -176,9 +178,9 @@ export async function handleGetCode(
176178
let resolvedTokens: Record<string, string> | undefined
177179
if (resolveTokens) {
178180
const resolvedByFinal = buildResolvedTokenMap(
179-
usedNamesFinal,
181+
finalBridgeFiltered,
180182
tokensByCanonical,
181-
canonicalByFinal
183+
canonicalById
182184
)
183185
code = replaceTokensWithValues(code, resolvedByFinal)
184186
if (code.length > MAX_CODE_CHARS) {

packages/extension/mcp/tools/code/sanitize/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import type { VisibleTree } from '../model'
22

33
import { patchNegativeGapStyles } from './negative-gap'
44
import { ensureRelativeForAbsoluteChildren } from './relative-parent'
5+
import { applyAbsoluteStackingOrder } from './stacking'
56

67
type StyleMap = Map<string, Record<string, string>>
78

89
type StylePatch = (tree: VisibleTree, styles: StyleMap, svgRoots?: Set<string>) => void
910

10-
const STYLE_PATCHES: StylePatch[] = [patchNegativeGapStyles, ensureRelativeForAbsoluteChildren]
11+
const STYLE_PATCHES: StylePatch[] = [
12+
patchNegativeGapStyles,
13+
ensureRelativeForAbsoluteChildren,
14+
applyAbsoluteStackingOrder
15+
]
1116

1217
export function sanitizeStyles(tree: VisibleTree, styles: StyleMap, svgRoots?: Set<string>): void {
1318
STYLE_PATCHES.forEach((patch) => patch(tree, styles, svgRoots))

packages/extension/mcp/tools/code/sanitize/relative-parent.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ function collect(
2727
): void {
2828
const node = tree.nodes.get(nodeId)
2929
if (!node) return
30-
if (svgRoots?.has(node.id)) return
31-
3230
const style = styles.get(node.id)
3331
if (style?.position?.toLowerCase() === 'absolute') {
3432
const layoutParent = getLayoutParent(tree, node)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { VisibleTree } from '../model'
2+
3+
type StyleMap = Map<string, Record<string, string>>
4+
5+
export function applyAbsoluteStackingOrder(tree: VisibleTree, styles: StyleMap): void {
6+
tree.rootIds.forEach((rootId) => visit(rootId, tree, styles))
7+
}
8+
9+
function visit(nodeId: string, tree: VisibleTree, styles: StyleMap): void {
10+
const node = tree.nodes.get(nodeId)
11+
if (!node) return
12+
13+
const children = node.children ?? []
14+
if (children.length) {
15+
const needsIsolation = new Set<string>()
16+
17+
for (let i = 0; i < children.length; i += 1) {
18+
const childId = children[i]
19+
const childStyle = styles.get(childId)
20+
if (!isAbsolute(childStyle)) continue
21+
22+
const hasLaterInFlow = children
23+
.slice(i + 1)
24+
.some((siblingId) => !isAbsolute(styles.get(siblingId)))
25+
26+
if (!hasLaterInFlow) continue
27+
28+
if (!childStyle?.['z-index']) {
29+
styles.set(childId, { ...(childStyle ?? {}), 'z-index': '-1' })
30+
}
31+
needsIsolation.add(nodeId)
32+
}
33+
34+
needsIsolation.forEach((parentId) => {
35+
const style = styles.get(parentId) ?? {}
36+
if (style.isolation) return
37+
styles.set(parentId, { ...style, isolation: 'isolate' })
38+
})
39+
}
40+
41+
children.forEach((childId) => visit(childId, tree, styles))
42+
}
43+
44+
function isAbsolute(style?: Record<string, string>): boolean {
45+
return style?.position?.toLowerCase() === 'absolute'
46+
}

packages/extension/mcp/tools/code/styles/normalize.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function buildLayoutStyles(
124124
for (const [id, style] of styles.entries()) {
125125
let layout = layoutOnly(style)
126126
if (svgRoots?.has(id)) {
127-
layout = stripSvgSize(layout)
127+
layout = stripSvgLayout(layout)
128128
}
129129
out.set(id, layout)
130130
}
@@ -133,15 +133,30 @@ export function buildLayoutStyles(
133133

134134
export function styleToClassNames(style: Record<string, string>, config: CodegenConfig): string[] {
135135
const normalizedStyle = normalizeStyleValues(style, config)
136-
137136
return cssToClassNames(normalizedStyle)
138137
}
139138

140-
function stripSvgSize(style: Record<string, string>): Record<string, string> {
141-
if (!style.width && !style.height) return style
139+
function stripSvgLayout(style: Record<string, string>): Record<string, string> {
140+
if (
141+
!style.width &&
142+
!style.height &&
143+
!style.overflow &&
144+
!style['overflow-x'] &&
145+
!style['overflow-y']
146+
) {
147+
return style
148+
}
142149
const cleaned: Record<string, string> = {}
143150
for (const [key, value] of Object.entries(style)) {
144-
if (key === 'width' || key === 'height') continue
151+
if (
152+
key === 'width' ||
153+
key === 'height' ||
154+
key === 'overflow' ||
155+
key === 'overflow-x' ||
156+
key === 'overflow-y'
157+
) {
158+
continue
159+
}
145160
cleaned[key] = value
146161
}
147162
return cleaned

packages/extension/mcp/tools/code/styles/overflow.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { toDecimalPlace } from '@/utils/number'
22

3+
const VECTOR_LIKE_TYPES = new Set<SceneNode['type']>([
4+
'VECTOR',
5+
'BOOLEAN_OPERATION',
6+
'STAR',
7+
'LINE',
8+
'ELLIPSE',
9+
'POLYGON'
10+
])
11+
312
export function applyOverflowStyles(
413
style: Record<string, string>,
514
node?: SceneNode
615
): Record<string, string> {
716
if (!node || !('overflowDirection' in node)) return style
17+
if (VECTOR_LIKE_TYPES.has(node.type)) return style
818

919
const dir = (node as { overflowDirection?: string }).overflowDirection
1020
const next = style

packages/extension/mcp/tools/code/tokens/resolve.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,19 @@ function resolveConcreteValue(
4646
}
4747

4848
export function buildResolvedTokenMap(
49-
usedFinalNames: Set<string>,
49+
nameToId: Map<string, string>,
5050
tokensByCanonical: GetTokenDefsResult,
51-
canonicalByFinal: Map<string, string>
51+
canonicalById: Map<string, string>
5252
): Map<string, string | undefined> {
5353
const cache = new Map<string, string | undefined>()
5454
const resolved = new Map<string, string | undefined>()
5555

56-
usedFinalNames.forEach((finalName) => {
57-
const canonical = canonicalByFinal.get(finalName)
56+
nameToId.forEach((id, name) => {
57+
const canonical = canonicalById.get(id)
5858
if (!canonical) return
5959
const val = resolveConcreteValue(canonical, tokensByCanonical, cache)
6060
if (typeof val === 'string' && !val.startsWith('--')) {
61-
resolved.set(finalName, val)
61+
resolved.set(name, val)
6262
}
6363
})
6464

packages/extension/mcp/tools/code/tokens/source-index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export function buildSourceNameIndex(
2929
}
3030
}
3131
if (canonical) setIfEmpty(canonical, id)
32+
// Match the normalized name that may appear in var(--...) outputs.
33+
setIfEmpty(normalizeFigmaVarName(cs), id)
3234
// 非 var 的 codeSyntax 也需要被匹配到(如 rounded-2xl)
3335
setIfEmpty(cs, id)
3436
}

packages/extension/mcp/tools/code/tokens/transform.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,28 @@ export async function applyPluginTransformToNames(
4747
if (!ordered.length) return { rewriteMap, finalBridge }
4848

4949
let transformed: Array<string | undefined> = []
50+
const transformIndexMap = new Map<number, number>()
51+
const refs = ordered
52+
.map((name, idx) => {
53+
if (!name.startsWith('--')) return null
54+
transformIndexMap.set(idx, transformIndexMap.size)
55+
return {
56+
code: `var(${name})`,
57+
name: normalizeCustomPropertyBody(name)
58+
}
59+
})
60+
.filter(Boolean) as Array<{ code: string; name: string }>
5061

51-
if (pluginCode) {
52-
const refs = ordered.map((name) => ({
53-
code: `var(${name})`,
54-
name: normalizeCustomPropertyBody(name)
55-
}))
56-
62+
if (pluginCode && refs.length) {
5763
transformed = await runTransformVariableBatch(refs, workerUnitOptions(config), pluginCode)
5864
}
5965

6066
ordered.forEach((name, idx) => {
61-
const next = pluginCode ? normalizeTransformedName(transformed[idx], name) : name
67+
const transformIndex = transformIndexMap.get(idx)
68+
const transformedValue =
69+
transformIndex != null && transformIndex >= 0 ? transformed[transformIndex] : undefined
70+
const next =
71+
pluginCode && transformIndex != null ? normalizeTransformedName(transformedValue, name) : name
6272
rewriteMap.set(name, next)
6373

6474
const variableId = sourceIndex.get(name) ?? sourceIndex.get(next)

packages/extension/mcp/tools/code/tokens/used.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ export async function buildUsedTokens(
1818
usedTokens: Record<string, TokenEntry>
1919
tokensByCanonical: GetTokenDefsResult
2020
canonicalByFinal: Map<string, string>
21+
canonicalById: Map<string, string>
2122
}> {
2223
const usedTokens: Record<string, TokenEntry> = {}
2324
if (!usedFinalNames.size || !finalBridge.size) {
24-
return { usedTokens, tokensByCanonical: {}, canonicalByFinal: new Map() }
25+
return {
26+
usedTokens,
27+
tokensByCanonical: {},
28+
canonicalByFinal: new Map(),
29+
canonicalById: new Map()
30+
}
2531
}
2632

2733
const ids = Array.from(finalBridge.values())
@@ -66,7 +72,7 @@ export async function buildUsedTokens(
6672
usedTokens[finalName] = remapTokenAliases(entry, canonicalToFinal)
6773
})
6874

69-
return { usedTokens, tokensByCanonical, canonicalByFinal }
75+
return { usedTokens, tokensByCanonical, canonicalByFinal, canonicalById }
7076
}
7177

7278
function remapTokenAliases(entry: TokenEntry, canonicalToFinal: Map<string, string>): TokenEntry {

0 commit comments

Comments
 (0)