view-contracts 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-contract.yaml +1 -1
- package/dist/preview.mjs +266 -153
- package/dist/preview.mjs.map +4 -4
- package/dist/view-contracts.bundle.mjs +308 -195
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +1 -1
- package/package.json +1 -1
- package/renderers/compose/README.md +7 -8
- package/renderers/compose/element-config.meta.json +20 -0
- package/renderers/compose/elementMap.ts +44 -0
- package/renderers/compose/elements.yaml +82 -0
- package/renderers/compose/index.ts +5 -0
- package/renderers/compose/lowering/lowerToCompose.ts +1062 -0
- package/renderers/compose/lowering/modifiers.ts +449 -0
- package/renderers/compose/properties.yaml +34 -0
- package/renderers/compose/renderComponents.ts +118 -0
- package/renderers/compose/renderRuntime.ts +78 -13
- package/renderers/compose/renderTheme.ts +304 -0
- package/renderers/compose/routes.yaml +7 -0
- package/renderers/compose/templates/Screen.kt.hbs +3 -4
- package/renderers/react/routes.yaml +3 -2
- package/renderers/registry.ts +12 -1
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
import { LOWERING_STYLE_PROPERTIES } from '../../../src/schema/dsl-catalog.js';
|
|
2
|
+
import type { ThemeIR, TokenIR } from '../../../src/design/types.js';
|
|
3
|
+
import { mergeVisualProps, type VisualPropSet } from '../../../src/renderer/style/mergeVisualProps.js';
|
|
4
|
+
import type {
|
|
5
|
+
IrBinding,
|
|
6
|
+
IrComponentNode,
|
|
7
|
+
IrConcat,
|
|
8
|
+
IrLiteral,
|
|
9
|
+
IrMapNode,
|
|
10
|
+
IrNode,
|
|
11
|
+
IrTextNode,
|
|
12
|
+
IrValue,
|
|
13
|
+
IrViewRefNode,
|
|
14
|
+
} from '../../../src/ir/types.js';
|
|
15
|
+
import type { ComposeElementConfig, ComposeElementMap } from '../elementMap.js';
|
|
16
|
+
import {
|
|
17
|
+
applyModifiers,
|
|
18
|
+
composeColorExpr,
|
|
19
|
+
composeContentAlignment,
|
|
20
|
+
dedupeModifiers,
|
|
21
|
+
extractApplyVcStyleExpr,
|
|
22
|
+
extractContentColor,
|
|
23
|
+
extractInheritedTextStyleParts,
|
|
24
|
+
extractClipRadiusDp,
|
|
25
|
+
extractPaddingModifiers,
|
|
26
|
+
extractTextMaxLines,
|
|
27
|
+
extractTextOverflow,
|
|
28
|
+
extractTextStyleModifiers,
|
|
29
|
+
hasFillMaxHeight,
|
|
30
|
+
hasStyledContainerModifiers,
|
|
31
|
+
hasTextFillMaxWidth,
|
|
32
|
+
hasWrapContentCenter,
|
|
33
|
+
paddingModifiersToPaddingValues,
|
|
34
|
+
stripBackgroundModifiers,
|
|
35
|
+
stripClipModifiers,
|
|
36
|
+
stripLayoutFrameModifiers,
|
|
37
|
+
stripPaddingModifiers,
|
|
38
|
+
visualPropSetToModifiers,
|
|
39
|
+
} from './modifiers.js';
|
|
40
|
+
|
|
41
|
+
export interface ComposeRenderContext {
|
|
42
|
+
map: ComposeElementMap;
|
|
43
|
+
theme: ThemeIR;
|
|
44
|
+
tokens: TokenIR;
|
|
45
|
+
allowlistExtra: string[];
|
|
46
|
+
propsRoot?: string;
|
|
47
|
+
/** How list `.map()` IR nodes should lower inside the current container. */
|
|
48
|
+
listScope?: 'none' | 'lazyColumn' | 'lazyGrid';
|
|
49
|
+
/** When true, Text leaves inherit table cell truncation behavior. */
|
|
50
|
+
cellTextBehavior?: boolean;
|
|
51
|
+
/** True when rendering inside a verticalScroll container. */
|
|
52
|
+
inVerticalScroll?: boolean;
|
|
53
|
+
/** True when rendering TableHeader column cells (compact vertical padding). */
|
|
54
|
+
inTableHeader?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type NodeRenderer = (node: IrNode, level: number, itemScope?: string) => string;
|
|
58
|
+
|
|
59
|
+
function indent(level: number): string {
|
|
60
|
+
return ' '.repeat(level);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function propLiteral(props: Record<string, IrValue>, key: string): unknown | undefined {
|
|
64
|
+
const value = props[key];
|
|
65
|
+
if (value === null || value === undefined) return undefined;
|
|
66
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
67
|
+
if (typeof value === 'object' && 'kind' in value && value.kind === 'literal') {
|
|
68
|
+
return (value as IrLiteral).value;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectInstanceVisualLiterals(props: Record<string, IrValue>): Record<string, unknown> {
|
|
74
|
+
const literals: Record<string, unknown> = {};
|
|
75
|
+
for (const [key, value] of Object.entries(props)) {
|
|
76
|
+
if (key === 'variant' || key === 'id') continue;
|
|
77
|
+
if (!LOWERING_STYLE_PROPERTIES.has(key)) continue;
|
|
78
|
+
const lit = propLiteral(props, key);
|
|
79
|
+
if (lit !== undefined) literals[key] = lit;
|
|
80
|
+
}
|
|
81
|
+
return literals;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dynamicVariantExpr(props: Record<string, IrValue>, ctx: ComposeRenderContext, itemScope?: string): string | null {
|
|
85
|
+
const v = props.variant;
|
|
86
|
+
if (!v || typeof v !== 'object' || !('kind' in v)) return null;
|
|
87
|
+
if (v.kind === 'binding') return bindingExpr(v as IrBinding, ctx, itemScope);
|
|
88
|
+
if (v.kind === 'concat') {
|
|
89
|
+
const concat = v as IrConcat;
|
|
90
|
+
const parts = concat.parts.map((p) => {
|
|
91
|
+
if (typeof p === 'string') return p;
|
|
92
|
+
return `\${${bindingExpr(p, ctx, itemScope)}}`;
|
|
93
|
+
}).join('');
|
|
94
|
+
return `"${parts}"`;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isDynamicVariant(props: Record<string, IrValue>): boolean {
|
|
100
|
+
const v = props.variant;
|
|
101
|
+
if (!v || typeof v !== 'object' || !('kind' in v)) return false;
|
|
102
|
+
return v.kind === 'binding' || v.kind === 'concat';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function mergedModifiersForProps(
|
|
106
|
+
elementName: string,
|
|
107
|
+
props: Record<string, IrValue>,
|
|
108
|
+
ctx: ComposeRenderContext,
|
|
109
|
+
itemScope?: string,
|
|
110
|
+
): string[] {
|
|
111
|
+
const entry = ctx.map.elements[elementName];
|
|
112
|
+
const variantLit = propLiteral(props, 'variant');
|
|
113
|
+
const instanceLiterals = collectInstanceVisualLiterals(props);
|
|
114
|
+
if (entry?.spacingProp && entry.spacingProp in instanceLiterals) {
|
|
115
|
+
delete instanceLiterals[entry.spacingProp];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isLeaf = Boolean(entry?.leaf);
|
|
119
|
+
|
|
120
|
+
if (isDynamicVariant(props)) {
|
|
121
|
+
const dynamicVariant = dynamicVariantExpr(props, ctx, itemScope)!;
|
|
122
|
+
const instanceOnly = mergeVisualProps({
|
|
123
|
+
elementName,
|
|
124
|
+
theme: { defaults: {}, variants: {} },
|
|
125
|
+
tokens: ctx.tokens,
|
|
126
|
+
instanceLiterals,
|
|
127
|
+
});
|
|
128
|
+
const instanceMods = visualPropSetToModifiers(instanceOnly, { isLeaf });
|
|
129
|
+
return [...instanceMods, `@runtimeStyle(VcTheme.resolve(${JSON.stringify(elementName)}, variant = ${dynamicVariant}))`];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const merged = mergeVisualProps({
|
|
133
|
+
elementName,
|
|
134
|
+
theme: ctx.theme,
|
|
135
|
+
tokens: ctx.tokens,
|
|
136
|
+
variantLiteral: typeof variantLit === 'string' ? variantLit : undefined,
|
|
137
|
+
instanceLiterals,
|
|
138
|
+
});
|
|
139
|
+
if (elementName === 'Box' && variantLit === 'card') {
|
|
140
|
+
delete merged.height;
|
|
141
|
+
}
|
|
142
|
+
return visualPropSetToModifiers(merged, { isLeaf });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolvedVisualProps(
|
|
146
|
+
elementName: string,
|
|
147
|
+
props: Record<string, IrValue>,
|
|
148
|
+
ctx: ComposeRenderContext,
|
|
149
|
+
): VisualPropSet {
|
|
150
|
+
const variantLit = propLiteral(props, 'variant');
|
|
151
|
+
const instanceLiterals = collectInstanceVisualLiterals(props);
|
|
152
|
+
return mergeVisualProps({
|
|
153
|
+
elementName,
|
|
154
|
+
theme: ctx.theme,
|
|
155
|
+
tokens: ctx.tokens,
|
|
156
|
+
variantLiteral: typeof variantLit === 'string' ? variantLit : undefined,
|
|
157
|
+
instanceLiterals,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseGridColumns(gridTemplateColumns: string | number | boolean | undefined): { kind: 'adaptive'; minWidth: number } | { kind: 'equal-columns'; count: number } | { kind: 'fixed-columns'; widths: string[] } | null {
|
|
162
|
+
if (!gridTemplateColumns || typeof gridTemplateColumns !== 'string') return null;
|
|
163
|
+
const equalRepeatMatch = gridTemplateColumns.match(/repeat\((\d+),\s*minmax\(0,\s*1fr\)\)/);
|
|
164
|
+
if (equalRepeatMatch) return { kind: 'equal-columns', count: Number(equalRepeatMatch[1]) };
|
|
165
|
+
const adaptiveMatch = gridTemplateColumns.match(/repeat\(auto-fit,\s*minmax\((\d+)px/);
|
|
166
|
+
if (adaptiveMatch) return { kind: 'adaptive', minWidth: Number(adaptiveMatch[1]) };
|
|
167
|
+
const parts = gridTemplateColumns.trim().split(/\s+/);
|
|
168
|
+
if (parts.length >= 2) return { kind: 'fixed-columns', widths: parts };
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveBackgroundColor(child: IrNode, ctx: ComposeRenderContext): string | null {
|
|
173
|
+
if (child.kind !== 'component') return null;
|
|
174
|
+
const resolved = resolvedVisualProps(child.name, (child as IrComponentNode).props, ctx);
|
|
175
|
+
const bg = resolved.background;
|
|
176
|
+
if (typeof bg !== 'string' || bg === 'transparent') return null;
|
|
177
|
+
return composeColorExpr(bg) ?? `Color(${JSON.stringify(bg)})`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveColorFromVisualProps(
|
|
181
|
+
elementName: string,
|
|
182
|
+
props: Record<string, IrValue>,
|
|
183
|
+
ctx: ComposeRenderContext,
|
|
184
|
+
channel: 'background' | 'foreground',
|
|
185
|
+
): string {
|
|
186
|
+
const resolved = resolvedVisualProps(elementName, props, ctx);
|
|
187
|
+
const value = resolved[channel];
|
|
188
|
+
if (typeof value !== 'string') {
|
|
189
|
+
return channel === 'background' ? 'Color.Transparent' : 'Color.Unspecified';
|
|
190
|
+
}
|
|
191
|
+
if (channel === 'background' && (value === 'transparent' || !value)) return 'Color.Transparent';
|
|
192
|
+
return composeColorExpr(value) ?? 'Color.Unspecified';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function alignToTextAlign(alignLit: unknown): string {
|
|
196
|
+
if (alignLit === 'end') return 'TextAlign.End';
|
|
197
|
+
if (alignLit === 'center') return 'TextAlign.Center';
|
|
198
|
+
return 'TextAlign.Start';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function alignToBoxContentAlignment(alignLit: unknown): string {
|
|
202
|
+
if (alignLit === 'end') return 'Alignment.CenterEnd';
|
|
203
|
+
if (alignLit === 'center') return 'Alignment.Center';
|
|
204
|
+
return 'Alignment.CenterStart';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderGridEqualColumns(
|
|
208
|
+
node: IrComponentNode,
|
|
209
|
+
level: number,
|
|
210
|
+
itemScope: string | undefined,
|
|
211
|
+
renderNested: NodeRenderer,
|
|
212
|
+
ctx: ComposeRenderContext,
|
|
213
|
+
modifiers: string[],
|
|
214
|
+
_columnCount: number,
|
|
215
|
+
): string {
|
|
216
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
217
|
+
const gap = typeof gapLit === 'number' ? `${gapLit}.dp` : '16.dp';
|
|
218
|
+
const modifierExpr = applyModifiers('Modifier.fillMaxWidth()', dedupeModifiers(modifiers));
|
|
219
|
+
|
|
220
|
+
// Row+weight is scroll-safe; LazyVerticalGrid crashes inside verticalScroll and in nested partials.
|
|
221
|
+
if (node.children.length === 1 && node.children[0]!.kind === 'map') {
|
|
222
|
+
const mapNode = node.children[0] as IrMapNode;
|
|
223
|
+
const item = mapNode.item;
|
|
224
|
+
const source = bindingExpr(mapNode.source, ctx, itemScope);
|
|
225
|
+
const body = renderNested(mapNode.body, level + 3, item);
|
|
226
|
+
return [
|
|
227
|
+
`${indent(level)}Row(modifier = ${modifierExpr}, horizontalArrangement = Arrangement.spacedBy(${gap}), verticalAlignment = Alignment.Top) {`,
|
|
228
|
+
`${indent(level + 1)}for (${item} in ${source}) {`,
|
|
229
|
+
`${indent(level + 2)}Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.TopStart) {`,
|
|
230
|
+
body,
|
|
231
|
+
`${indent(level + 2)}}`,
|
|
232
|
+
`${indent(level + 1)}}`,
|
|
233
|
+
`${indent(level)}}`,
|
|
234
|
+
].join('\n');
|
|
235
|
+
}
|
|
236
|
+
const parts = node.children.map((child) => {
|
|
237
|
+
let content = '';
|
|
238
|
+
if (child.kind === 'map') content = renderMap(child, level + 2, renderNested, ctx, 'none');
|
|
239
|
+
else if (child.kind === 'text') content = `${indent(level + 2)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
240
|
+
else if (child.kind === 'binding') content = `${indent(level + 2)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
241
|
+
else content = renderNested(child, level + 2, itemScope);
|
|
242
|
+
if (!content) return '';
|
|
243
|
+
return `${indent(level + 1)}Box(modifier = Modifier.weight(1f).fillMaxSize(), propagateMinConstraints = true, contentAlignment = Alignment.TopStart) {\n${content}\n${indent(level + 1)}}`;
|
|
244
|
+
}).filter(Boolean);
|
|
245
|
+
return [
|
|
246
|
+
`${indent(level)}Row(modifier = ${modifierExpr}.height(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy(${gap}), verticalAlignment = Alignment.Top) {`,
|
|
247
|
+
parts.join('\n'),
|
|
248
|
+
`${indent(level)}}`,
|
|
249
|
+
].join('\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderGridChildBlock(
|
|
253
|
+
children: IrNode[],
|
|
254
|
+
level: number,
|
|
255
|
+
renderNested: NodeRenderer,
|
|
256
|
+
ctx: ComposeRenderContext,
|
|
257
|
+
itemScope: string | undefined,
|
|
258
|
+
): string {
|
|
259
|
+
const parts = children.map((child) => {
|
|
260
|
+
if (child.kind === 'map') {
|
|
261
|
+
return renderMap(child, level, renderNested, ctx, 'lazyGrid');
|
|
262
|
+
}
|
|
263
|
+
let rendered = '';
|
|
264
|
+
if (child.kind === 'text') rendered = `${indent(level + 1)}Text(text = ${JSON.stringify((child as IrTextNode).value)}, modifier = Modifier.fillMaxWidth())`;
|
|
265
|
+
else if (child.kind === 'binding') rendered = `${indent(level + 1)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)}, modifier = Modifier.fillMaxWidth())`;
|
|
266
|
+
else rendered = renderNested(child, level + 1, itemScope);
|
|
267
|
+
if (!rendered) return '';
|
|
268
|
+
if (child.kind !== 'text' && child.kind !== 'binding') {
|
|
269
|
+
return `${indent(level)}item {\n${indent(level + 1)}Box(modifier = Modifier.fillMaxWidth()) {\n${rendered}\n${indent(level + 1)}}\n${indent(level)}}`;
|
|
270
|
+
}
|
|
271
|
+
return `${indent(level)}item {\n${rendered}\n${indent(level)}}`;
|
|
272
|
+
}).filter(Boolean);
|
|
273
|
+
return parts.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderGridFixedColumns(
|
|
277
|
+
node: IrComponentNode,
|
|
278
|
+
level: number,
|
|
279
|
+
itemScope: string | undefined,
|
|
280
|
+
renderNested: NodeRenderer,
|
|
281
|
+
ctx: ComposeRenderContext,
|
|
282
|
+
modifiers: string[],
|
|
283
|
+
): string {
|
|
284
|
+
const modifierExpr = applyModifiers('Modifier.fillMaxWidth()', dedupeModifiers(modifiers));
|
|
285
|
+
const resolved = resolvedVisualProps(node.name, node.props, ctx);
|
|
286
|
+
const parsed = parseGridColumns(resolved.gridTemplateColumns);
|
|
287
|
+
const widths = parsed?.kind === 'fixed-columns' ? parsed.widths : [];
|
|
288
|
+
const parts: string[] = [];
|
|
289
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
290
|
+
const child = node.children[i]!;
|
|
291
|
+
const widthToken = widths[i];
|
|
292
|
+
const pxMatch = widthToken?.match(/^(\d+)px$/);
|
|
293
|
+
if (pxMatch) {
|
|
294
|
+
let rendered = '';
|
|
295
|
+
if (child.kind === 'map') rendered = renderMap(child, level + 2, renderNested, ctx, 'none');
|
|
296
|
+
else if (child.kind === 'text') rendered = `${indent(level + 2)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
297
|
+
else if (child.kind === 'binding') rendered = `${indent(level + 2)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
298
|
+
else rendered = renderNested(child, level + 2, itemScope);
|
|
299
|
+
if (!rendered) continue;
|
|
300
|
+
const bgColor = resolveBackgroundColor(child, ctx);
|
|
301
|
+
const bgMod = bgColor ? `.background(${bgColor})` : '';
|
|
302
|
+
parts.push([
|
|
303
|
+
`${indent(level + 1)}Box(modifier = Modifier.width(${Number(pxMatch[1])}.dp).fillMaxHeight()${bgMod}) {`,
|
|
304
|
+
rendered,
|
|
305
|
+
`${indent(level + 1)}}`,
|
|
306
|
+
].join('\n'));
|
|
307
|
+
} else if (widthToken === '1fr') {
|
|
308
|
+
const prevScroll = ctx.inVerticalScroll;
|
|
309
|
+
ctx.inVerticalScroll = true;
|
|
310
|
+
let rendered = '';
|
|
311
|
+
if (child.kind === 'map') rendered = renderMap(child, level + 2, renderNested, ctx, 'none');
|
|
312
|
+
else if (child.kind === 'text') rendered = `${indent(level + 2)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
313
|
+
else if (child.kind === 'binding') rendered = `${indent(level + 2)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
314
|
+
else rendered = renderNested(child, level + 2, itemScope);
|
|
315
|
+
ctx.inVerticalScroll = prevScroll;
|
|
316
|
+
if (!rendered) continue;
|
|
317
|
+
parts.push([
|
|
318
|
+
`${indent(level + 1)}Box(modifier = Modifier.weight(1f).fillMaxHeight(), contentAlignment = Alignment.TopStart) {`,
|
|
319
|
+
`${indent(level + 2)}Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {`,
|
|
320
|
+
rendered,
|
|
321
|
+
`${indent(level + 2)}}`,
|
|
322
|
+
`${indent(level + 1)}}`,
|
|
323
|
+
].join('\n'));
|
|
324
|
+
} else {
|
|
325
|
+
let rendered = '';
|
|
326
|
+
if (child.kind === 'map') rendered = renderMap(child, level + 2, renderNested, ctx, 'none');
|
|
327
|
+
else if (child.kind === 'text') rendered = `${indent(level + 2)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
328
|
+
else if (child.kind === 'binding') rendered = `${indent(level + 2)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
329
|
+
else rendered = renderNested(child, level + 2, itemScope);
|
|
330
|
+
if (!rendered) continue;
|
|
331
|
+
parts.push(rendered);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return [
|
|
335
|
+
`${indent(level)}Row(modifier = ${modifierExpr}, horizontalArrangement = Arrangement.spacedBy(0.dp)) {`,
|
|
336
|
+
parts.join('\n'),
|
|
337
|
+
`${indent(level)}}`,
|
|
338
|
+
].join('\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function bindingExpr(binding: IrBinding, ctx: ComposeRenderContext, itemScope?: string): string {
|
|
342
|
+
const root = binding.scope ?? itemScope ?? ctx.propsRoot ?? 'vm';
|
|
343
|
+
return binding.path ? `${root}.${binding.path}` : root;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function wrapWithInheritedStyles(childBlock: string, modifiers: string[], level: number): string {
|
|
347
|
+
const contentColor = extractContentColor(modifiers);
|
|
348
|
+
const inheritParts = extractInheritedTextStyleParts(modifiers);
|
|
349
|
+
if (!contentColor && inheritParts.length === 0) return childBlock;
|
|
350
|
+
const providers: string[] = [];
|
|
351
|
+
if (contentColor) providers.push(`LocalContentColor provides ${contentColor}`);
|
|
352
|
+
if (inheritParts.length > 0) {
|
|
353
|
+
providers.push(`LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(${inheritParts.join(', ')}))`);
|
|
354
|
+
}
|
|
355
|
+
return [
|
|
356
|
+
`${indent(level)}CompositionLocalProvider(${providers.join(', ')}) {`,
|
|
357
|
+
childBlock,
|
|
358
|
+
`${indent(level)}}`,
|
|
359
|
+
].join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function headerCellPaddingMods(paddingMods: string[]): string[] {
|
|
363
|
+
return paddingMods.map((mod) => {
|
|
364
|
+
const horizontalVertical = mod.match(/\.padding\(horizontal = (\d+)\.dp, vertical = \d+\.dp\)/);
|
|
365
|
+
if (horizontalVertical) {
|
|
366
|
+
return `.padding(horizontal = ${horizontalVertical[1]}.dp)`;
|
|
367
|
+
}
|
|
368
|
+
const uniform = mod.match(/\.padding\((\d+)\.dp\)/);
|
|
369
|
+
if (uniform) {
|
|
370
|
+
return `.padding(horizontal = ${uniform[1]}.dp)`;
|
|
371
|
+
}
|
|
372
|
+
return mod;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function cellTextBehaviorMods(): string[] {
|
|
377
|
+
return ['@textMaxLines(1)', '@textOverflow(TextOverflow.Ellipsis)', '@textFillMaxWidth()'];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function withCellTextBehavior(modifiers: string[], ctx: ComposeRenderContext): string[] {
|
|
381
|
+
if (!ctx.cellTextBehavior) return modifiers;
|
|
382
|
+
return [...modifiers, ...cellTextBehaviorMods()];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function wrapWithContentCenter(inner: string, level: number): string {
|
|
386
|
+
return [
|
|
387
|
+
`${indent(level)}Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {`,
|
|
388
|
+
inner,
|
|
389
|
+
`${indent(level)}}`,
|
|
390
|
+
].join('\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renderStyledBox(
|
|
394
|
+
level: number,
|
|
395
|
+
modifiers: string[],
|
|
396
|
+
childBlock: string,
|
|
397
|
+
contentAlignParam = '',
|
|
398
|
+
): string {
|
|
399
|
+
const paddingMods = extractPaddingModifiers(modifiers);
|
|
400
|
+
const styleMods = stripPaddingModifiers(modifiers);
|
|
401
|
+
const outerModifier = applyModifiers('Modifier', styleMods);
|
|
402
|
+
const innerPaddingExpr = paddingMods.length > 0 && hasStyledContainerModifiers(styleMods)
|
|
403
|
+
? applyModifiers('Modifier', paddingMods)
|
|
404
|
+
: null;
|
|
405
|
+
let inner = wrapWithInheritedStyles(childBlock, modifiers, level + 1);
|
|
406
|
+
if (innerPaddingExpr) {
|
|
407
|
+
inner = [
|
|
408
|
+
`${indent(level + 1)}Box(modifier = ${innerPaddingExpr}) {`,
|
|
409
|
+
inner,
|
|
410
|
+
`${indent(level + 1)}}`,
|
|
411
|
+
].join('\n');
|
|
412
|
+
}
|
|
413
|
+
return [
|
|
414
|
+
`${indent(level)}Box(modifier = ${outerModifier}${contentAlignParam}) {`,
|
|
415
|
+
inner,
|
|
416
|
+
`${indent(level)}}`,
|
|
417
|
+
].join('\n');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function formatTextComposable(
|
|
421
|
+
pad: string,
|
|
422
|
+
content: string,
|
|
423
|
+
modifiers: string[],
|
|
424
|
+
ctx: ComposeRenderContext,
|
|
425
|
+
): string {
|
|
426
|
+
const paddingMods = extractPaddingModifiers(modifiers);
|
|
427
|
+
const runtimeStyleExpr = extractApplyVcStyleExpr(applyModifiers('Modifier', modifiers));
|
|
428
|
+
const layoutMods = dedupeModifiers(modifiers.filter((m) =>
|
|
429
|
+
!m.startsWith('@textStyle(')
|
|
430
|
+
&& !m.startsWith('@runtimeStyle(')
|
|
431
|
+
&& !m.includes('.padding(')
|
|
432
|
+
&& !m.startsWith('@textMaxLines(')
|
|
433
|
+
&& !m.startsWith('@textOverflow(')
|
|
434
|
+
&& !m.startsWith('@textAlign(')
|
|
435
|
+
&& m !== '@textFillMaxWidth()',
|
|
436
|
+
));
|
|
437
|
+
const modifierBase = hasTextFillMaxWidth(modifiers) ? 'Modifier.fillMaxWidth()' : 'Modifier';
|
|
438
|
+
const textStyleParts = extractTextStyleModifiers(modifiers);
|
|
439
|
+
let styleExpr: string | null = null;
|
|
440
|
+
if (runtimeStyleExpr) {
|
|
441
|
+
styleExpr = textStyleParts.length > 0
|
|
442
|
+
? `LocalTextStyle.current.merge(${runtimeStyleExpr}.asTextStyle()).merge(TextStyle(${textStyleParts.join(', ')}))`
|
|
443
|
+
: `${runtimeStyleExpr}.asTextStyle()`;
|
|
444
|
+
} else if (textStyleParts.length > 0) {
|
|
445
|
+
styleExpr = `LocalTextStyle.current.merge(TextStyle(${textStyleParts.join(', ')}))`;
|
|
446
|
+
}
|
|
447
|
+
const styleParam = styleExpr ? `, style = ${styleExpr}` : '';
|
|
448
|
+
const maxLines = extractTextMaxLines(modifiers);
|
|
449
|
+
const maxLinesParam = maxLines !== null ? `, maxLines = ${maxLines}` : '';
|
|
450
|
+
const overflow = extractTextOverflow(modifiers);
|
|
451
|
+
const overflowParam = overflow ? `, overflow = ${overflow}` : '';
|
|
452
|
+
const textLine = `${pad} Text(text = ${content}${maxLinesParam}${overflowParam}${styleParam})`;
|
|
453
|
+
|
|
454
|
+
if (runtimeStyleExpr) {
|
|
455
|
+
const styleBase = ctx.cellTextBehavior ? 'Modifier' : 'Modifier.fillMaxWidth()';
|
|
456
|
+
const styleModifier = `${styleBase}.applyVcStyle(${runtimeStyleExpr})`;
|
|
457
|
+
if (paddingMods.length > 0) {
|
|
458
|
+
const outerModifier = applyModifiers(modifierBase, [...layoutMods, ...paddingMods]);
|
|
459
|
+
return [
|
|
460
|
+
`${pad}Box(modifier = ${outerModifier}) {`,
|
|
461
|
+
`${pad} Box(modifier = ${styleModifier}) {`,
|
|
462
|
+
textLine,
|
|
463
|
+
`${pad} }`,
|
|
464
|
+
`${pad}}`,
|
|
465
|
+
].join('\n');
|
|
466
|
+
}
|
|
467
|
+
return [
|
|
468
|
+
`${pad}Box(modifier = ${styleModifier}) {`,
|
|
469
|
+
textLine,
|
|
470
|
+
`${pad}}`,
|
|
471
|
+
].join('\n');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const modifierExpr = applyModifiers(modifierBase, [...layoutMods, ...paddingMods]);
|
|
475
|
+
return `${pad}Text(text = ${content}, modifier = ${modifierExpr}${maxLinesParam}${overflowParam}${styleParam})`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function emitSelectLabelText(
|
|
479
|
+
props: Record<string, IrValue>,
|
|
480
|
+
ctx: ComposeRenderContext,
|
|
481
|
+
itemScope: string | undefined,
|
|
482
|
+
): string {
|
|
483
|
+
const options = props.options;
|
|
484
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'binding') {
|
|
485
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
486
|
+
return `${expr}.firstOrNull()?.label ?: ""`;
|
|
487
|
+
}
|
|
488
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
489
|
+
const items = (options as IrLiteral).value as Array<{ label: string }>;
|
|
490
|
+
return JSON.stringify(items[0]?.label ?? '');
|
|
491
|
+
}
|
|
492
|
+
return '""';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function emitPickerOptions(
|
|
496
|
+
props: Record<string, IrValue>,
|
|
497
|
+
ctx: ComposeRenderContext,
|
|
498
|
+
itemScope: string | undefined,
|
|
499
|
+
level: number,
|
|
500
|
+
): string {
|
|
501
|
+
const options = props.options;
|
|
502
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'binding') {
|
|
503
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
504
|
+
return `${indent(level + 1)}for (option in ${expr}) {\n${indent(level + 2)}DropdownMenuItem(text = { Text(text = option.label) }, onClick = { expanded = false })\n${indent(level + 1)}}`;
|
|
505
|
+
}
|
|
506
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
507
|
+
const items = (options as IrLiteral).value as Array<{ label: string; value: string }>;
|
|
508
|
+
return items.map((opt) =>
|
|
509
|
+
`${indent(level + 1)}DropdownMenuItem(text = { Text(text = ${JSON.stringify(opt.label)}) }, onClick = { expanded = false })`,
|
|
510
|
+
).join('\n');
|
|
511
|
+
}
|
|
512
|
+
return `${indent(level + 1)}DropdownMenuItem(text = { Text(text = "--") }, onClick = { expanded = false })`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function tokenMutedColorExpr(tokens: TokenIR): string {
|
|
516
|
+
const entry = tokens.entries.find((item) => item.path === 'ui.text-muted');
|
|
517
|
+
const value = entry?.value ?? '#6b7280';
|
|
518
|
+
const match = value.match(/^#([0-9a-fA-F]{6})$/);
|
|
519
|
+
if (!match) return `Color(${JSON.stringify(value)})`;
|
|
520
|
+
const hex = match[1]!;
|
|
521
|
+
return `Color(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt(hex.slice(4, 6), 16)})`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function tokenTextColorExpr(tokens: TokenIR): string {
|
|
525
|
+
const entry = tokens.entries.find((item) => item.path === 'ui.text');
|
|
526
|
+
const value = entry?.value ?? '#111827';
|
|
527
|
+
const match = value.match(/^#([0-9a-fA-F]{6})$/);
|
|
528
|
+
if (!match) return `Color(${JSON.stringify(value)})`;
|
|
529
|
+
const hex = match[1]!;
|
|
530
|
+
return `Color(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt(hex.slice(4, 6), 16)})`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function renderTextContent(children: IrNode[], ctx: ComposeRenderContext, itemScope?: string): string | null {
|
|
534
|
+
for (const child of children) {
|
|
535
|
+
if (child.kind === 'text') return JSON.stringify((child as IrTextNode).value);
|
|
536
|
+
if (child.kind === 'binding') return bindingExpr(child as IrBinding, ctx, itemScope);
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderChildren(
|
|
542
|
+
children: IrNode[],
|
|
543
|
+
level: number,
|
|
544
|
+
renderNested: NodeRenderer,
|
|
545
|
+
ctx: ComposeRenderContext,
|
|
546
|
+
itemScope?: string,
|
|
547
|
+
listScope: ComposeRenderContext['listScope'] = ctx.listScope ?? 'none',
|
|
548
|
+
): string {
|
|
549
|
+
const parts = children.map((child) => {
|
|
550
|
+
if (child.kind === 'map') return renderMap(child, level, renderNested, ctx, listScope);
|
|
551
|
+
if (child.kind === 'text') return `${indent(level)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
552
|
+
if (child.kind === 'binding') return `${indent(level)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
553
|
+
return renderNested(child, level, itemScope);
|
|
554
|
+
}).filter(Boolean);
|
|
555
|
+
return parts.join('\n');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function renderLazyGridChildren(
|
|
559
|
+
children: IrNode[],
|
|
560
|
+
level: number,
|
|
561
|
+
renderNested: NodeRenderer,
|
|
562
|
+
ctx: ComposeRenderContext,
|
|
563
|
+
itemScope?: string,
|
|
564
|
+
): string {
|
|
565
|
+
const parts = children.map((child) => {
|
|
566
|
+
let content: string;
|
|
567
|
+
if (child.kind === 'map') {
|
|
568
|
+
return renderMap(child, level, renderNested, ctx, 'lazyGrid');
|
|
569
|
+
}
|
|
570
|
+
if (child.kind === 'text') content = `${indent(level + 1)}Text(text = ${JSON.stringify((child as IrTextNode).value)})`;
|
|
571
|
+
else if (child.kind === 'binding') content = `${indent(level + 1)}Text(text = ${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
572
|
+
else content = renderNested(child, level + 1, itemScope);
|
|
573
|
+
if (!content) return '';
|
|
574
|
+
return `${indent(level)}item {\n${content}\n${indent(level)}}`;
|
|
575
|
+
}).filter(Boolean);
|
|
576
|
+
return parts.join('\n');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderMap(
|
|
580
|
+
node: IrMapNode,
|
|
581
|
+
level: number,
|
|
582
|
+
renderNested: NodeRenderer,
|
|
583
|
+
ctx: ComposeRenderContext,
|
|
584
|
+
listScope: ComposeRenderContext['listScope'] = 'none',
|
|
585
|
+
): string {
|
|
586
|
+
const source = bindingExpr(node.source, ctx);
|
|
587
|
+
const item = node.item;
|
|
588
|
+
const body = renderNested(node.body, level + 2, item);
|
|
589
|
+
if (listScope === 'lazyGrid') {
|
|
590
|
+
return `${indent(level)}gridItems(${source}, key = { it.id }) { ${item} ->\n${indent(level + 1)}Box(modifier = Modifier.fillMaxWidth()) {\n${body}\n${indent(level + 1)}}\n${indent(level)}}`;
|
|
591
|
+
}
|
|
592
|
+
if (listScope === 'lazyColumn') {
|
|
593
|
+
return `${indent(level)}lazyItems(${source}, key = { it.id }) { ${item} ->\n${body}\n${indent(level)}}`;
|
|
594
|
+
}
|
|
595
|
+
return `${indent(level)}for (${item} in ${source}) {\n${body}\n${indent(level)}}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderViewRef(node: IrViewRefNode, level: number, itemScope: string | undefined, ctx: ComposeRenderContext): string {
|
|
599
|
+
const props = Object.entries(node.props).map(([key, value]) => {
|
|
600
|
+
if (typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'binding') {
|
|
601
|
+
return `${key} = ${bindingExpr(value as IrBinding, ctx, itemScope)}`;
|
|
602
|
+
}
|
|
603
|
+
return `${key} = ${JSON.stringify(value)}`;
|
|
604
|
+
});
|
|
605
|
+
return `${indent(level)}${node.name}(${props.join(', ')})`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renderVcButton(
|
|
609
|
+
node: IrComponentNode,
|
|
610
|
+
level: number,
|
|
611
|
+
itemScope: string | undefined,
|
|
612
|
+
ctx: ComposeRenderContext,
|
|
613
|
+
modifiers: string[],
|
|
614
|
+
): string {
|
|
615
|
+
const pad = indent(level);
|
|
616
|
+
const label = propLiteral(node.props, 'label');
|
|
617
|
+
const textContent = renderTextContent(node.children, ctx, itemScope);
|
|
618
|
+
const title = textContent ?? (typeof label === 'string' ? JSON.stringify(label) : '"Button"');
|
|
619
|
+
let containerColor = resolveColorFromVisualProps(node.name, node.props, ctx, 'background');
|
|
620
|
+
let contentColor = resolveColorFromVisualProps(node.name, node.props, ctx, 'foreground');
|
|
621
|
+
if (isDynamicVariant(node.props)) {
|
|
622
|
+
const dynamicVariant = dynamicVariantExpr(node.props, ctx, itemScope)!;
|
|
623
|
+
const styleExpr = `VcTheme.resolve("Button", variant = ${dynamicVariant})`;
|
|
624
|
+
containerColor = `${styleExpr}.background ?: Color.Transparent`;
|
|
625
|
+
contentColor = `${styleExpr}.foreground ?: Color.Unspecified`;
|
|
626
|
+
}
|
|
627
|
+
const layoutMods = dedupeModifiers(stripBackgroundModifiers(stripClipModifiers(stripPaddingModifiers(modifiers))));
|
|
628
|
+
const buttonLayoutMods = ctx.cellTextBehavior
|
|
629
|
+
? [...layoutMods.filter((m) => m !== '.fillMaxWidth()'), '.wrapContentWidth()']
|
|
630
|
+
: layoutMods;
|
|
631
|
+
const modifierExpr = applyModifiers('Modifier', buttonLayoutMods);
|
|
632
|
+
const clipRadius = extractClipRadiusDp(modifiers);
|
|
633
|
+
const contentPadding = paddingModifiersToPaddingValues(extractPaddingModifiers(modifiers));
|
|
634
|
+
const shapeParam = clipRadius !== null ? `, shape = RoundedCornerShape(${clipRadius}.dp)` : '';
|
|
635
|
+
const paddingParam = contentPadding ? `, contentPadding = ${contentPadding}` : '';
|
|
636
|
+
const buttonTextStyle = extractTextStyleModifiers(modifiers);
|
|
637
|
+
const textStyleParam = buttonTextStyle.length > 0 ? `, style = TextStyle(${buttonTextStyle.join(', ')})` : '';
|
|
638
|
+
const cellTextParams = ctx.cellTextBehavior
|
|
639
|
+
? ', maxLines = 1, overflow = TextOverflow.Ellipsis'
|
|
640
|
+
: '';
|
|
641
|
+
return [
|
|
642
|
+
`${pad}VcButton(onClick = {}, modifier = ${modifierExpr}, containerColor = ${containerColor}, contentColor = ${contentColor}${shapeParam}${paddingParam}) {`,
|
|
643
|
+
`${pad} Text(text = ${title}${textStyleParam}${cellTextParams})`,
|
|
644
|
+
`${pad}}`,
|
|
645
|
+
].join('\n');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function renderLeaf(
|
|
649
|
+
node: IrComponentNode,
|
|
650
|
+
entry: ComposeElementConfig,
|
|
651
|
+
level: number,
|
|
652
|
+
itemScope: string | undefined,
|
|
653
|
+
ctx: ComposeRenderContext,
|
|
654
|
+
modifiers: string[],
|
|
655
|
+
): string {
|
|
656
|
+
const pad = indent(level);
|
|
657
|
+
const modifierExpr = applyModifiers('Modifier', modifiers);
|
|
658
|
+
|
|
659
|
+
if (entry.leaf === 'text') {
|
|
660
|
+
const content = renderTextContent(node.children, ctx, itemScope) ?? '""';
|
|
661
|
+
return formatTextComposable(pad, content, modifiers, ctx);
|
|
662
|
+
}
|
|
663
|
+
if (entry.leaf === 'button') {
|
|
664
|
+
return renderVcButton(node, level, itemScope, ctx, modifiers);
|
|
665
|
+
}
|
|
666
|
+
if (entry.leaf === 'textfield') {
|
|
667
|
+
const placeholder = propLiteral(node.props, 'placeholder');
|
|
668
|
+
const ph = typeof placeholder === 'string' ? JSON.stringify(placeholder) : '""';
|
|
669
|
+
const textColor = tokenTextColorExpr(ctx.tokens);
|
|
670
|
+
const mutedColor = tokenMutedColorExpr(ctx.tokens);
|
|
671
|
+
const fieldMods = dedupeModifiers(modifiers);
|
|
672
|
+
const modifierExpr = applyModifiers('Modifier', fieldMods);
|
|
673
|
+
const textStyleParts = extractTextStyleModifiers(modifiers);
|
|
674
|
+
const textStyleLine = textStyleParts.length > 0
|
|
675
|
+
? `${pad} textStyle = TextStyle(${textStyleParts.join(', ')}),`
|
|
676
|
+
: `${pad} textStyle = TextStyle(color = ${textColor}, fontSize = 14.sp),`;
|
|
677
|
+
return [
|
|
678
|
+
`${pad}BasicTextField(`,
|
|
679
|
+
`${pad} value = "",`,
|
|
680
|
+
`${pad} onValueChange = {},`,
|
|
681
|
+
`${pad} modifier = ${modifierExpr},`,
|
|
682
|
+
textStyleLine,
|
|
683
|
+
`${pad} decorationBox = { innerTextField ->`,
|
|
684
|
+
`${pad} Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {`,
|
|
685
|
+
`${pad} if ("".isEmpty()) {`,
|
|
686
|
+
`${pad} Text(text = ${ph}, style = TextStyle(color = ${mutedColor}, fontSize = 14.sp))`,
|
|
687
|
+
`${pad} }`,
|
|
688
|
+
`${pad} innerTextField()`,
|
|
689
|
+
`${pad} }`,
|
|
690
|
+
`${pad} },`,
|
|
691
|
+
`${pad})`,
|
|
692
|
+
].join('\n');
|
|
693
|
+
}
|
|
694
|
+
if (entry.leaf === 'texteditor') {
|
|
695
|
+
const placeholder = propLiteral(node.props, 'placeholder');
|
|
696
|
+
const ph = typeof placeholder === 'string' ? JSON.stringify(placeholder) : '""';
|
|
697
|
+
const mutedColor = tokenMutedColorExpr(ctx.tokens);
|
|
698
|
+
const minHeightMod = modifiers.find((mod) => mod.includes('heightIn(min ='));
|
|
699
|
+
const minHeight = minHeightMod?.match(/heightIn\(min = (\d+)\.dp\)/)?.[1] ?? '72';
|
|
700
|
+
const textMods = modifiers.filter((mod) => mod.startsWith('@textStyle('));
|
|
701
|
+
const containerMods = dedupeModifiers(modifiers.filter((mod) =>
|
|
702
|
+
!mod.startsWith('@textStyle(') && !mod.includes('heightIn(min ='),
|
|
703
|
+
));
|
|
704
|
+
const containerExpr = applyModifiers('Modifier', dedupeModifiers(containerMods));
|
|
705
|
+
const textStyleParts = extractTextStyleModifiers(textMods);
|
|
706
|
+
const textStyleSuffix = textStyleParts.length > 0 ? `, textStyle = TextStyle(${textStyleParts.join(', ')})` : '';
|
|
707
|
+
return [
|
|
708
|
+
`${pad}Box(modifier = ${containerExpr}.heightIn(min = ${minHeight}.dp)) {`,
|
|
709
|
+
`${pad} BasicTextField(`,
|
|
710
|
+
`${pad} value = "",`,
|
|
711
|
+
`${pad} onValueChange = {},`,
|
|
712
|
+
`${pad} modifier = Modifier.fillMaxWidth()${textStyleSuffix},`,
|
|
713
|
+
`${pad} decorationBox = { innerTextField ->`,
|
|
714
|
+
`${pad} Box(modifier = Modifier.fillMaxWidth()) {`,
|
|
715
|
+
`${pad} if ("".isEmpty()) {`,
|
|
716
|
+
`${pad} Text(text = ${ph}, modifier = Modifier.align(Alignment.TopStart).padding(top = 8.dp, start = 4.dp), style = TextStyle(color = ${mutedColor}, fontSize = 14.sp))`,
|
|
717
|
+
`${pad} }`,
|
|
718
|
+
`${pad} innerTextField()`,
|
|
719
|
+
`${pad} }`,
|
|
720
|
+
`${pad} },`,
|
|
721
|
+
`${pad} )`,
|
|
722
|
+
`${pad}}`,
|
|
723
|
+
].join('\n');
|
|
724
|
+
}
|
|
725
|
+
if (entry.leaf === 'picker') {
|
|
726
|
+
const widthLit = propLiteral(node.props, 'width');
|
|
727
|
+
const widthPx = typeof widthLit === 'number' ? widthLit : null;
|
|
728
|
+
const widthMod = widthPx !== null ? `.width(${widthPx}.dp)` : '';
|
|
729
|
+
const layoutMods = stripLayoutFrameModifiers(dedupeModifiers(modifiers));
|
|
730
|
+
const pickerModifier = applyModifiers(`Modifier${widthMod}`, layoutMods);
|
|
731
|
+
const labelText = emitSelectLabelText(node.props, ctx, itemScope);
|
|
732
|
+
const textColor = tokenTextColorExpr(ctx.tokens);
|
|
733
|
+
const mutedColor = tokenMutedColorExpr(ctx.tokens);
|
|
734
|
+
const optionsBlock = emitPickerOptions(node.props, ctx, itemScope, level);
|
|
735
|
+
return [
|
|
736
|
+
`${pad}Box(modifier = ${pickerModifier}.heightIn(min = 42.dp)) {`,
|
|
737
|
+
`${pad} var expanded by remember { mutableStateOf(false) }`,
|
|
738
|
+
`${pad} TextButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(horizontal = 12.dp, vertical = 9.dp)) {`,
|
|
739
|
+
`${pad} Text(text = ${labelText}, style = TextStyle(color = ${textColor}), maxLines = 1, modifier = Modifier.weight(1f))`,
|
|
740
|
+
`${pad} Icon(Icons.Default.ArrowDropDown, contentDescription = null, tint = ${mutedColor})`,
|
|
741
|
+
`${pad} }`,
|
|
742
|
+
`${pad} DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {`,
|
|
743
|
+
optionsBlock,
|
|
744
|
+
`${pad} }`,
|
|
745
|
+
`${pad}}`,
|
|
746
|
+
].join('\n');
|
|
747
|
+
}
|
|
748
|
+
if (entry.leaf === 'spacer') {
|
|
749
|
+
return `${pad}Spacer(modifier = ${modifierExpr})`;
|
|
750
|
+
}
|
|
751
|
+
if (entry.leaf === 'divider') {
|
|
752
|
+
return `${pad}HorizontalDivider(modifier = ${modifierExpr})`;
|
|
753
|
+
}
|
|
754
|
+
if (entry.leaf === 'image') {
|
|
755
|
+
const src = propLiteral(node.props, 'src') ?? propLiteral(node.props, 'name');
|
|
756
|
+
const desc = typeof src === 'string' ? JSON.stringify(src) : '"image"';
|
|
757
|
+
return `${pad}Icon(imageVector = Icons.Default.Image, contentDescription = ${desc}, modifier = ${modifierExpr})`;
|
|
758
|
+
}
|
|
759
|
+
if (entry.leaf === 'progressview') {
|
|
760
|
+
return `${pad}CircularProgressIndicator(modifier = ${modifierExpr})`;
|
|
761
|
+
}
|
|
762
|
+
if (entry.leaf === 'toggle') {
|
|
763
|
+
const label = propLiteral(node.props, 'label');
|
|
764
|
+
const text = typeof label === 'string' ? JSON.stringify(label) : '""';
|
|
765
|
+
return [
|
|
766
|
+
`${pad}Row(modifier = ${modifierExpr}, verticalAlignment = Alignment.CenterVertically) {`,
|
|
767
|
+
`${pad} Switch(checked = false, onCheckedChange = {})`,
|
|
768
|
+
`${pad} Text(text = ${text})`,
|
|
769
|
+
`${pad}}`,
|
|
770
|
+
].join('\n');
|
|
771
|
+
}
|
|
772
|
+
return `${pad}Text(text = "/* unsupported leaf: ${node.name} */")`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function renderContainer(
|
|
776
|
+
node: IrComponentNode,
|
|
777
|
+
entry: ComposeElementConfig,
|
|
778
|
+
level: number,
|
|
779
|
+
itemScope: string | undefined,
|
|
780
|
+
renderNested: NodeRenderer,
|
|
781
|
+
ctx: ComposeRenderContext,
|
|
782
|
+
modifiers: string[],
|
|
783
|
+
): string {
|
|
784
|
+
const childBlock = renderChildren(node.children, level + 1, renderNested, ctx, itemScope);
|
|
785
|
+
const modifierExpr = applyModifiers('Modifier', modifiers);
|
|
786
|
+
|
|
787
|
+
if (entry.container === 'vstack') {
|
|
788
|
+
const spacingLit = entry.spacingProp ? propLiteral(node.props, entry.spacingProp) : undefined;
|
|
789
|
+
const spacing = typeof spacingLit === 'number' ? `${spacingLit}.dp` : '0.dp';
|
|
790
|
+
const columnBlock = [
|
|
791
|
+
`${indent(level)}Column(modifier = ${modifierExpr}, verticalArrangement = Arrangement.spacedBy(${spacing})) {`,
|
|
792
|
+
wrapWithInheritedStyles(childBlock, modifiers, level + 1),
|
|
793
|
+
`${indent(level)}}`,
|
|
794
|
+
].join('\n');
|
|
795
|
+
return hasWrapContentCenter(modifiers) ? wrapWithContentCenter(columnBlock, level) : columnBlock;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (entry.container === 'hstack') {
|
|
799
|
+
const spacingLit = entry.spacingProp ? propLiteral(node.props, entry.spacingProp) : undefined;
|
|
800
|
+
const spacing = typeof spacingLit === 'number' ? `${spacingLit}.dp` : '0.dp';
|
|
801
|
+
const justifyLit = propLiteral(node.props, 'justify');
|
|
802
|
+
const wrapLit = propLiteral(node.props, 'wrap');
|
|
803
|
+
const arrangement = justifyLit === 'between'
|
|
804
|
+
? 'Arrangement.SpaceBetween'
|
|
805
|
+
: justifyLit === 'end'
|
|
806
|
+
? 'Arrangement.End'
|
|
807
|
+
: justifyLit === 'center'
|
|
808
|
+
? 'Arrangement.Center'
|
|
809
|
+
: `Arrangement.spacedBy(${spacing})`;
|
|
810
|
+
const alignLit = propLiteral(node.props, 'align');
|
|
811
|
+
const alignItemsLit = propLiteral(node.props, 'alignItems');
|
|
812
|
+
const heightLit = propLiteral(node.props, 'height');
|
|
813
|
+
const alignment = alignItemsLit === 'center' || alignLit === 'center' || typeof heightLit === 'number'
|
|
814
|
+
? 'Alignment.CenterVertically'
|
|
815
|
+
: alignItemsLit === 'end'
|
|
816
|
+
? 'Alignment.Bottom'
|
|
817
|
+
: 'Alignment.Top';
|
|
818
|
+
const isTableElement = node.name === 'TableHeader' || node.name === 'TableRow';
|
|
819
|
+
const flexLit = propLiteral(node.props, 'flex');
|
|
820
|
+
const shouldFill = isTableElement || justifyLit !== undefined || typeof flexLit === 'number';
|
|
821
|
+
const frameMods: string[] = [];
|
|
822
|
+
if (shouldFill) frameMods.push('.fillMaxWidth()');
|
|
823
|
+
if (typeof heightLit === 'number') {
|
|
824
|
+
frameMods.push(isTableElement && node.name === 'TableHeader'
|
|
825
|
+
? `.heightIn(min = ${heightLit}.dp)`
|
|
826
|
+
: `.height(${heightLit}.dp)`);
|
|
827
|
+
}
|
|
828
|
+
const layoutModifiers = isTableElement && typeof heightLit === 'number'
|
|
829
|
+
? stripLayoutFrameModifiers(modifiers)
|
|
830
|
+
: modifiers;
|
|
831
|
+
const rowModifier = applyModifiers('Modifier', dedupeModifiers([...layoutModifiers, ...frameMods]));
|
|
832
|
+
const prevTableHeader = ctx.inTableHeader;
|
|
833
|
+
if (node.name === 'TableHeader') ctx.inTableHeader = true;
|
|
834
|
+
const childContent = wrapWithInheritedStyles(renderChildren(node.children, level + 1, renderNested, ctx, itemScope), modifiers, level + 1);
|
|
835
|
+
if (node.name === 'TableHeader') ctx.inTableHeader = prevTableHeader;
|
|
836
|
+
|
|
837
|
+
if (wrapLit === true && justifyLit !== 'between') {
|
|
838
|
+
return [
|
|
839
|
+
`${indent(level)}FlowRow(modifier = ${rowModifier}, horizontalArrangement = Arrangement.spacedBy(${spacing}), verticalArrangement = Arrangement.spacedBy(${spacing})) {`,
|
|
840
|
+
childContent,
|
|
841
|
+
`${indent(level)}}`,
|
|
842
|
+
].join('\n');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return [
|
|
846
|
+
`${indent(level)}Row(modifier = ${rowModifier}, horizontalArrangement = ${arrangement}, verticalAlignment = ${alignment}) {`,
|
|
847
|
+
childContent,
|
|
848
|
+
`${indent(level)}}`,
|
|
849
|
+
].join('\n');
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (entry.container === 'group' || entry.container === 'navstack') {
|
|
853
|
+
const resolved = !isDynamicVariant(node.props) ? resolvedVisualProps(node.name, node.props, ctx) : undefined;
|
|
854
|
+
const parsedGrid = resolved?.display === 'grid' ? parseGridColumns(resolved.gridTemplateColumns) : null;
|
|
855
|
+
if (parsedGrid?.kind === 'equal-columns') {
|
|
856
|
+
return renderGridEqualColumns(node, level, itemScope, renderNested, ctx, modifiers, parsedGrid.count);
|
|
857
|
+
}
|
|
858
|
+
if (parsedGrid?.kind === 'adaptive') {
|
|
859
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
860
|
+
const gap = typeof gapLit === 'number' ? `${gapLit}.dp` : '16.dp';
|
|
861
|
+
return [
|
|
862
|
+
`${indent(level)}LazyVerticalGrid(columns = GridCells.Adaptive(minSize = ${parsedGrid.minWidth}.dp), modifier = ${modifierExpr}.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(${gap}), verticalArrangement = Arrangement.spacedBy(${gap})) {`,
|
|
863
|
+
renderGridChildBlock(node.children, level + 1, renderNested, ctx, itemScope),
|
|
864
|
+
`${indent(level)}}`,
|
|
865
|
+
].join('\n');
|
|
866
|
+
}
|
|
867
|
+
const contentAlignLit = propLiteral(node.props, 'contentAlign');
|
|
868
|
+
const fillHeightAlign = hasFillMaxHeight(modifiers) ? ', contentAlignment = Alignment.TopStart' : '';
|
|
869
|
+
const contentAlignParam = typeof contentAlignLit === 'string'
|
|
870
|
+
? `, contentAlignment = ${composeContentAlignment(contentAlignLit)}`
|
|
871
|
+
: fillHeightAlign;
|
|
872
|
+
const boxBlock = renderStyledBox(level, modifiers, childBlock, contentAlignParam);
|
|
873
|
+
return hasWrapContentCenter(modifiers) ? wrapWithContentCenter(boxBlock, level) : boxBlock;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (entry.container === 'scrollview') {
|
|
877
|
+
const prevScroll = ctx.inVerticalScroll;
|
|
878
|
+
ctx.inVerticalScroll = true;
|
|
879
|
+
const block = [
|
|
880
|
+
`${indent(level)}Column(modifier = ${modifierExpr}.verticalScroll(rememberScrollState())) {`,
|
|
881
|
+
childBlock,
|
|
882
|
+
`${indent(level)}}`,
|
|
883
|
+
].join('\n');
|
|
884
|
+
ctx.inVerticalScroll = prevScroll;
|
|
885
|
+
return block;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (entry.container === 'field') {
|
|
889
|
+
const label = propLiteral(node.props, 'label');
|
|
890
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
891
|
+
const gap = typeof gapLit === 'number' ? `${gapLit}.dp` : '4.dp';
|
|
892
|
+
const labelLine = typeof label === 'string'
|
|
893
|
+
? `${indent(level + 1)}Text(text = ${JSON.stringify(label)}, modifier = Modifier.applyVcStyle(VcTheme.resolve("Text", variant = "field-label")), style = VcTheme.resolve("Text", variant = "field-label").asTextStyle())\n`
|
|
894
|
+
: '';
|
|
895
|
+
return [
|
|
896
|
+
`${indent(level)}Column(modifier = ${modifierExpr}, verticalArrangement = Arrangement.spacedBy(${gap})) {`,
|
|
897
|
+
`${labelLine}${childBlock}`,
|
|
898
|
+
`${indent(level)}}`,
|
|
899
|
+
].join('\n');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (entry.container === 'tablestack') {
|
|
903
|
+
const separatorLit = propLiteral(node.props, 'separator');
|
|
904
|
+
const separatorColorLit = propLiteral(node.props, 'separatorColor');
|
|
905
|
+
const hasSeparator = (typeof separatorLit === 'number' && separatorLit > 0) || separatorLit === undefined;
|
|
906
|
+
const resolved = resolvedVisualProps(node.name, node.props, ctx);
|
|
907
|
+
const colorStr = resolved.separatorColor ?? separatorColorLit;
|
|
908
|
+
let colorExpr = 'Color.Gray.copy(alpha = 0.2f)';
|
|
909
|
+
if (hasSeparator && typeof colorStr === 'string') {
|
|
910
|
+
const hex = colorStr.match(/^#([0-9a-fA-F]{6})$/);
|
|
911
|
+
if (hex) {
|
|
912
|
+
colorExpr = `Color(${parseInt(hex[1]!.slice(0, 2), 16)}, ${parseInt(hex[1]!.slice(2, 4), 16)}, ${parseInt(hex[1]!.slice(4, 6), 16)})`;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const divLine = `${indent(level + 1)}HorizontalDivider(modifier = Modifier, color = ${colorExpr})`;
|
|
916
|
+
const childParts = node.children.map((child) => {
|
|
917
|
+
if (child.kind === 'map' && hasSeparator) {
|
|
918
|
+
const mapNode = child as IrMapNode;
|
|
919
|
+
const source = bindingExpr(mapNode.source, ctx);
|
|
920
|
+
const item = mapNode.item;
|
|
921
|
+
const mapBody = renderNested(mapNode.body, level + 3, item);
|
|
922
|
+
return [
|
|
923
|
+
`${indent(level + 1)}for (${item} in ${source}) {`,
|
|
924
|
+
`${indent(level + 2)}${divLine.trim()}`,
|
|
925
|
+
mapBody,
|
|
926
|
+
`${indent(level + 1)}}`,
|
|
927
|
+
].join('\n');
|
|
928
|
+
}
|
|
929
|
+
if (child.kind === 'map') return renderMap(child, level + 1, renderNested, ctx, 'none');
|
|
930
|
+
return renderNested(child, level + 1, itemScope);
|
|
931
|
+
}).filter(Boolean);
|
|
932
|
+
let body: string[];
|
|
933
|
+
if (hasSeparator) {
|
|
934
|
+
body = childParts;
|
|
935
|
+
} else {
|
|
936
|
+
body = childParts;
|
|
937
|
+
}
|
|
938
|
+
return [
|
|
939
|
+
`${indent(level)}Column(modifier = ${modifierExpr}) {`,
|
|
940
|
+
body.join('\n'),
|
|
941
|
+
`${indent(level)}}`,
|
|
942
|
+
].join('\n');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (entry.container === 'cellgroup') {
|
|
946
|
+
const widthLit = propLiteral(node.props, 'width');
|
|
947
|
+
const flexLit = propLiteral(node.props, 'flex');
|
|
948
|
+
const alignLit = propLiteral(node.props, 'align');
|
|
949
|
+
const textAlign = alignToTextAlign(alignLit);
|
|
950
|
+
const boxContentAlignment = alignToBoxContentAlignment(alignLit);
|
|
951
|
+
const isFlex = typeof flexLit === 'number';
|
|
952
|
+
const visualCellMods = stripLayoutFrameModifiers(modifiers);
|
|
953
|
+
const paddingMods = ctx.inTableHeader
|
|
954
|
+
? headerCellPaddingMods(extractPaddingModifiers(visualCellMods))
|
|
955
|
+
: extractPaddingModifiers(visualCellMods);
|
|
956
|
+
const inheritedMods = visualCellMods.filter((mod) =>
|
|
957
|
+
mod.startsWith('@contentColor(') || mod.startsWith('@inheritTextStyle('),
|
|
958
|
+
);
|
|
959
|
+
const layoutMods: string[] = [];
|
|
960
|
+
if (typeof widthLit === 'number') {
|
|
961
|
+
layoutMods.push(`.width(${widthLit}.dp)`);
|
|
962
|
+
} else if (isFlex) {
|
|
963
|
+
layoutMods.push('.weight(1f).fillMaxWidth()');
|
|
964
|
+
}
|
|
965
|
+
const cellModifier = applyModifiers('Modifier', dedupeModifiers([...layoutMods, ...stripPaddingModifiers(visualCellMods)]));
|
|
966
|
+
const prevCellTextBehavior = ctx.cellTextBehavior;
|
|
967
|
+
ctx.cellTextBehavior = true;
|
|
968
|
+
const cellChildParts = node.children.map((child) => {
|
|
969
|
+
if (child.kind === 'map') return renderMap(child, level + 1, renderNested, ctx, 'none');
|
|
970
|
+
if (child.kind === 'text') {
|
|
971
|
+
return formatTextComposable(
|
|
972
|
+
indent(level + 1),
|
|
973
|
+
JSON.stringify((child as IrTextNode).value),
|
|
974
|
+
dedupeModifiers([...cellTextBehaviorMods(), `@textAlign(${textAlign})`, ...paddingMods, ...inheritedMods.map((m) => m)]),
|
|
975
|
+
ctx,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (child.kind === 'binding') {
|
|
979
|
+
return formatTextComposable(
|
|
980
|
+
indent(level + 1),
|
|
981
|
+
bindingExpr(child as IrBinding, ctx, itemScope),
|
|
982
|
+
dedupeModifiers([...cellTextBehaviorMods(), `@textAlign(${textAlign})`, ...paddingMods, ...inheritedMods.map((m) => m)]),
|
|
983
|
+
ctx,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return renderNested(child, level + 1, itemScope);
|
|
987
|
+
}).filter(Boolean).join('\n');
|
|
988
|
+
ctx.cellTextBehavior = prevCellTextBehavior;
|
|
989
|
+
return [
|
|
990
|
+
`${indent(level)}Box(modifier = ${cellModifier}, contentAlignment = ${boxContentAlignment}) {`,
|
|
991
|
+
wrapWithInheritedStyles(cellChildParts, inheritedMods, level + 1),
|
|
992
|
+
`${indent(level)}}`,
|
|
993
|
+
].join('\n');
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const boxBlock = renderStyledBox(level, modifiers, childBlock);
|
|
997
|
+
return hasWrapContentCenter(modifiers) ? wrapWithContentCenter(boxBlock, level) : boxBlock;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function renderComponent(
|
|
1001
|
+
node: IrComponentNode,
|
|
1002
|
+
entry: ComposeElementConfig,
|
|
1003
|
+
level: number,
|
|
1004
|
+
itemScope: string | undefined,
|
|
1005
|
+
renderNested: NodeRenderer,
|
|
1006
|
+
ctx: ComposeRenderContext,
|
|
1007
|
+
): string {
|
|
1008
|
+
const modifiers = withCellTextBehavior(
|
|
1009
|
+
mergedModifiersForProps(node.name, node.props, ctx, itemScope),
|
|
1010
|
+
ctx,
|
|
1011
|
+
);
|
|
1012
|
+
if (entry.leaf) return renderLeaf(node, entry, level, itemScope, ctx, modifiers);
|
|
1013
|
+
|
|
1014
|
+
if (entry.container === 'group' && !isDynamicVariant(node.props)) {
|
|
1015
|
+
const resolved = resolvedVisualProps(node.name, node.props, ctx);
|
|
1016
|
+
const gridCols = resolved.gridTemplateColumns;
|
|
1017
|
+
const flexDir = resolved.flexDirection;
|
|
1018
|
+
if (resolved.display === 'grid' && gridCols) {
|
|
1019
|
+
const parsed = parseGridColumns(gridCols);
|
|
1020
|
+
if (parsed?.kind === 'equal-columns') {
|
|
1021
|
+
return renderGridEqualColumns(node, level, itemScope, renderNested, ctx, modifiers, parsed.count);
|
|
1022
|
+
}
|
|
1023
|
+
if (parsed?.kind === 'fixed-columns') {
|
|
1024
|
+
return renderGridFixedColumns(node, level, itemScope, renderNested, ctx, modifiers);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (resolved.display === 'flex' && flexDir === 'row') {
|
|
1028
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
1029
|
+
const gap = typeof gapLit === 'number' ? `${gapLit}.dp` : (typeof resolved.gap === 'number' ? `${resolved.gap}.dp` : '0.dp');
|
|
1030
|
+
const modifierExpr = applyModifiers('Modifier', modifiers);
|
|
1031
|
+
const childBlock = renderChildren(node.children, level + 2, renderNested, ctx, itemScope);
|
|
1032
|
+
return [
|
|
1033
|
+
`${indent(level)}Row(modifier = ${modifierExpr}, horizontalArrangement = Arrangement.spacedBy(${gap}), verticalAlignment = Alignment.Top) {`,
|
|
1034
|
+
childBlock,
|
|
1035
|
+
`${indent(level)}}`,
|
|
1036
|
+
].join('\n');
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (entry.container) return renderContainer(node, entry, level, itemScope, renderNested, ctx, modifiers);
|
|
1041
|
+
throw new Error(`Unsupported Compose element config for ${node.name}`);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
export function createComposeNodeRenderer(ctx: ComposeRenderContext): NodeRenderer {
|
|
1045
|
+
const renderNested: NodeRenderer = (node, level, itemScope) => {
|
|
1046
|
+
if (node.kind === 'viewRef') return renderViewRef(node, level, itemScope, ctx);
|
|
1047
|
+
if (node.kind !== 'component') return '';
|
|
1048
|
+
if (!ctx.map.elements[node.name]) {
|
|
1049
|
+
throw new Error(`Element "${node.name}" is not in Compose element map`);
|
|
1050
|
+
}
|
|
1051
|
+
return renderComponent(node, ctx.map.elements[node.name]!, level, itemScope, renderNested, ctx);
|
|
1052
|
+
};
|
|
1053
|
+
return renderNested;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export function lowerIrToComposeBody(root: IrNode, ctx: ComposeRenderContext, level = 2): string {
|
|
1057
|
+
const render = createComposeNodeRenderer(ctx);
|
|
1058
|
+
if (root.kind !== 'component') {
|
|
1059
|
+
throw new Error('Compose lowering root must be a component node');
|
|
1060
|
+
}
|
|
1061
|
+
return render(root, level);
|
|
1062
|
+
}
|