@zseven-w/pen-core 0.6.0 → 0.7.1

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -25
  3. package/package.json +19 -5
  4. package/src/__tests__/arc-path.test.ts +23 -23
  5. package/src/__tests__/codegen-utils.test.ts +50 -0
  6. package/src/__tests__/design-md-parser.test.ts +49 -0
  7. package/src/__tests__/font-utils.test.ts +15 -15
  8. package/src/__tests__/layout-engine.test.ts +169 -83
  9. package/src/__tests__/merge-helpers.test.ts +143 -0
  10. package/src/__tests__/node-diff.test.ts +139 -0
  11. package/src/__tests__/node-helpers.test.ts +19 -19
  12. package/src/__tests__/node-merge.test.ts +425 -0
  13. package/src/__tests__/normalize-stroke-fill-schema.test.ts +416 -0
  14. package/src/__tests__/normalize-tree-layout.test.ts +294 -0
  15. package/src/__tests__/normalize.test.ts +119 -80
  16. package/src/__tests__/path-anchors.test.ts +98 -0
  17. package/src/__tests__/resolve-variables-recursive.test.ts +109 -54
  18. package/src/__tests__/strip-redundant-section-fills.test.ts +278 -0
  19. package/src/__tests__/text-measure.test.ts +84 -79
  20. package/src/__tests__/tree-utils.test.ts +133 -102
  21. package/src/__tests__/unwrap-fake-phone-mockup.test.ts +322 -0
  22. package/src/__tests__/variables.test.ts +68 -65
  23. package/src/arc-path.ts +35 -35
  24. package/src/boolean-ops.ts +131 -142
  25. package/src/constants.ts +36 -36
  26. package/src/design-md-parser.ts +363 -0
  27. package/src/font-utils.ts +30 -15
  28. package/src/id.ts +1 -1
  29. package/src/index.ts +47 -13
  30. package/src/layout/engine.ts +255 -224
  31. package/src/layout/normalize-tree.ts +140 -0
  32. package/src/layout/strip-redundant-section-fills.ts +155 -0
  33. package/src/layout/text-measure.ts +130 -106
  34. package/src/layout/unwrap-fake-phone-mockup.ts +147 -0
  35. package/src/merge/index.ts +16 -0
  36. package/src/merge/merge-helpers.ts +113 -0
  37. package/src/merge/node-diff.ts +123 -0
  38. package/src/merge/node-merge.ts +651 -0
  39. package/src/node-helpers.ts +18 -5
  40. package/src/normalize/normalize-stroke-fill-schema.ts +297 -0
  41. package/src/normalize.ts +75 -81
  42. package/src/path-anchors.ts +331 -0
  43. package/src/sync-lock.ts +3 -3
  44. package/src/tree-utils.ts +180 -158
  45. package/src/variables/replace-refs.ts +63 -60
  46. package/src/variables/resolve.ts +98 -106
@@ -4,10 +4,10 @@
4
4
  * Used when renaming or deleting a variable to keep the tree consistent.
5
5
  */
6
6
 
7
- import type { PenNode } from '@zseven-w/pen-types'
8
- import type { PenFill } from '@zseven-w/pen-types'
9
- import type { VariableDefinition } from '@zseven-w/pen-types'
10
- import { resolveVariableRef } from './resolve.js'
7
+ import type { PenNode } from '@zseven-w/pen-types';
8
+ import type { PenFill } from '@zseven-w/pen-types';
9
+ import type { VariableDefinition } from '@zseven-w/pen-types';
10
+ import { resolveVariableRef } from './resolve.js';
11
11
 
12
12
  /**
13
13
  * Replace all occurrences of `$oldRef` with `$newRef` in the node tree.
@@ -20,130 +20,133 @@ export function replaceVariableRefsInTree(
20
20
  variables: Record<string, VariableDefinition>,
21
21
  activeTheme: Record<string, string>,
22
22
  ): PenNode[] {
23
- const oldToken = `$${oldRef}`
24
- const replacement = newRef ? `$${newRef}` : undefined
23
+ const oldToken = `$${oldRef}`;
24
+ const replacement = newRef ? `$${newRef}` : undefined;
25
25
 
26
26
  function resolveOrReplace(val: string): string {
27
- if (val !== oldToken) return val
28
- if (replacement) return replacement
29
- const resolved = resolveVariableRef(oldToken, variables, activeTheme)
30
- return typeof resolved === 'string' ? resolved : val
27
+ if (val !== oldToken) return val;
28
+ if (replacement) return replacement;
29
+ const resolved = resolveVariableRef(oldToken, variables, activeTheme);
30
+ return typeof resolved === 'string' ? resolved : val;
31
31
  }
32
32
 
33
33
  function resolveOrReplaceNumeric(val: string | number): string | number {
34
- if (typeof val !== 'string' || val !== oldToken) return val
35
- if (replacement) return replacement
36
- const resolved = resolveVariableRef(oldToken, variables, activeTheme)
37
- return typeof resolved === 'number' ? resolved : val
34
+ if (typeof val !== 'string' || val !== oldToken) return val;
35
+ if (replacement) return replacement;
36
+ const resolved = resolveVariableRef(oldToken, variables, activeTheme);
37
+ return typeof resolved === 'number' ? resolved : val;
38
38
  }
39
39
 
40
40
  function replaceFills(fills: PenFill[] | string | undefined): PenFill[] | string | undefined {
41
- if (!fills || typeof fills === 'string') return fills
41
+ if (!fills || typeof fills === 'string') return fills;
42
42
  return fills.map((f) => {
43
43
  if (f.type === 'solid' && f.color === oldToken) {
44
- return { ...f, color: resolveOrReplace(f.color) }
44
+ return { ...f, color: resolveOrReplace(f.color) };
45
45
  }
46
46
  if (f.type === 'linear_gradient' || f.type === 'radial_gradient') {
47
47
  const newStops = f.stops.map((s) =>
48
48
  s.color === oldToken ? { ...s, color: resolveOrReplace(s.color) } : s,
49
- )
50
- return { ...f, stops: newStops }
49
+ );
50
+ return { ...f, stops: newStops };
51
51
  }
52
- return f
53
- })
52
+ return f;
53
+ });
54
54
  }
55
55
 
56
56
  function replaceInNode(node: PenNode): PenNode {
57
- const out: Record<string, unknown> = { ...node }
58
- let changed = false
57
+ const out: Record<string, unknown> = { ...node };
58
+ let changed = false;
59
59
 
60
60
  // Opacity
61
61
  if (typeof node.opacity === 'string' && node.opacity === oldToken) {
62
- out.opacity = resolveOrReplaceNumeric(node.opacity)
63
- changed = true
62
+ out.opacity = resolveOrReplaceNumeric(node.opacity);
63
+ changed = true;
64
64
  }
65
65
 
66
66
  // Gap
67
67
  if ('gap' in node && (node as unknown as Record<string, unknown>).gap === oldToken) {
68
- out.gap = resolveOrReplaceNumeric(oldToken)
69
- changed = true
68
+ out.gap = resolveOrReplaceNumeric(oldToken);
69
+ changed = true;
70
70
  }
71
71
 
72
72
  // Padding
73
73
  if ('padding' in node && (node as unknown as Record<string, unknown>).padding === oldToken) {
74
- out.padding = resolveOrReplaceNumeric(oldToken)
75
- changed = true
74
+ out.padding = resolveOrReplaceNumeric(oldToken);
75
+ changed = true;
76
76
  }
77
77
 
78
78
  // Fill
79
79
  if ('fill' in node && (node as unknown as Record<string, unknown>).fill) {
80
- const fills = (node as unknown as Record<string, unknown>).fill as PenFill[]
81
- const newFills = replaceFills(fills)
80
+ const fills = (node as unknown as Record<string, unknown>).fill as PenFill[];
81
+ const newFills = replaceFills(fills);
82
82
  if (newFills !== fills) {
83
- out.fill = newFills
84
- changed = true
83
+ out.fill = newFills;
84
+ changed = true;
85
85
  }
86
86
  }
87
87
 
88
88
  // Stroke fill & thickness
89
89
  if ('stroke' in node && (node as unknown as Record<string, unknown>).stroke) {
90
- const stroke = (node as unknown as Record<string, unknown>).stroke as Record<string, unknown>
91
- const newStroke = { ...stroke }
92
- let strokeChanged = false
90
+ const stroke = (node as unknown as Record<string, unknown>).stroke as Record<string, unknown>;
91
+ const newStroke = { ...stroke };
92
+ let strokeChanged = false;
93
93
  if (typeof stroke.thickness === 'string' && stroke.thickness === oldToken) {
94
- newStroke.thickness = resolveOrReplaceNumeric(oldToken)
95
- strokeChanged = true
94
+ newStroke.thickness = resolveOrReplaceNumeric(oldToken);
95
+ strokeChanged = true;
96
96
  }
97
97
  if (stroke.fill) {
98
- const newFill = replaceFills(stroke.fill as PenFill[])
98
+ const newFill = replaceFills(stroke.fill as PenFill[]);
99
99
  if (newFill !== stroke.fill) {
100
- newStroke.fill = newFill
101
- strokeChanged = true
100
+ newStroke.fill = newFill;
101
+ strokeChanged = true;
102
102
  }
103
103
  }
104
104
  if (strokeChanged) {
105
- out.stroke = newStroke
106
- changed = true
105
+ out.stroke = newStroke;
106
+ changed = true;
107
107
  }
108
108
  }
109
109
 
110
110
  // Effects
111
111
  if ('effects' in node && Array.isArray((node as unknown as Record<string, unknown>).effects)) {
112
- const effects = (node as unknown as Record<string, unknown>).effects as Record<string, unknown>[]
112
+ const effects = (node as unknown as Record<string, unknown>).effects as Record<
113
+ string,
114
+ unknown
115
+ >[];
113
116
  const newEffects = effects.map((e) => {
114
- const ne = { ...e }
115
- let ec = false
117
+ const ne = { ...e };
118
+ let ec = false;
116
119
  if (typeof e.color === 'string' && e.color === oldToken) {
117
- ne.color = resolveOrReplace(e.color as string)
118
- ec = true
120
+ ne.color = resolveOrReplace(e.color as string);
121
+ ec = true;
119
122
  }
120
123
  for (const key of ['blur', 'offsetX', 'offsetY', 'spread']) {
121
124
  if (typeof e[key] === 'string' && e[key] === oldToken) {
122
- ne[key] = resolveOrReplaceNumeric(oldToken)
123
- ec = true
125
+ ne[key] = resolveOrReplaceNumeric(oldToken);
126
+ ec = true;
124
127
  }
125
128
  }
126
- return ec ? ne : e
127
- })
128
- out.effects = newEffects
129
- changed = true
129
+ return ec ? ne : e;
130
+ });
131
+ out.effects = newEffects;
132
+ changed = true;
130
133
  }
131
134
 
132
135
  // Text content
133
136
  if (node.type === 'text' && typeof node.content === 'string' && node.content === oldToken) {
134
- out.content = resolveOrReplace(node.content)
135
- changed = true
137
+ out.content = resolveOrReplace(node.content);
138
+ changed = true;
136
139
  }
137
140
 
138
141
  // Recurse children
139
142
  if ('children' in node && (node as unknown as Record<string, unknown>).children) {
140
- const children = (node as unknown as Record<string, unknown>).children as PenNode[]
141
- out.children = replaceVariableRefsInTree(children, oldRef, newRef, variables, activeTheme)
142
- changed = true
143
+ const children = (node as unknown as Record<string, unknown>).children as PenNode[];
144
+ out.children = replaceVariableRefsInTree(children, oldRef, newRef, variables, activeTheme);
145
+ changed = true;
143
146
  }
144
147
 
145
- return changed ? (out as unknown as PenNode) : node
148
+ return changed ? (out as unknown as PenNode) : node;
146
149
  }
147
150
 
148
- return nodes.map(replaceInNode)
151
+ return nodes.map(replaceInNode);
149
152
  }
@@ -5,12 +5,12 @@
5
5
  * optionally matching themed values to an active theme context.
6
6
  */
7
7
 
8
- import type { PenNode } from '@zseven-w/pen-types'
9
- import type { PenFill, PenStroke, PenEffect } from '@zseven-w/pen-types'
10
- import type { VariableDefinition, ThemedValue } from '@zseven-w/pen-types'
8
+ import type { PenNode } from '@zseven-w/pen-types';
9
+ import type { PenFill, PenStroke, PenEffect } from '@zseven-w/pen-types';
10
+ import type { VariableDefinition, ThemedValue } from '@zseven-w/pen-types';
11
11
 
12
- type Vars = Record<string, VariableDefinition>
13
- type Theme = Record<string, string>
12
+ type Vars = Record<string, VariableDefinition>;
13
+ type Theme = Record<string, string>;
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
16
  // Helpers
@@ -18,19 +18,17 @@ type Theme = Record<string, string>
18
18
 
19
19
  /** Check whether a value is a `$variable` reference string. */
20
20
  export function isVariableRef(value: unknown): value is string {
21
- return typeof value === 'string' && value.startsWith('$')
21
+ return typeof value === 'string' && value.startsWith('$');
22
22
  }
23
23
 
24
24
  /** Build the default theme map (first value per axis) from PenDocument.themes. */
25
- export function getDefaultTheme(
26
- themes: Record<string, string[]> | undefined,
27
- ): Theme {
28
- const result: Theme = {}
29
- if (!themes) return result
25
+ export function getDefaultTheme(themes: Record<string, string[]> | undefined): Theme {
26
+ const result: Theme = {};
27
+ if (!themes) return result;
30
28
  for (const [key, values] of Object.entries(themes)) {
31
- if (values.length > 0) result[key] = values[0]
29
+ if (values.length > 0) result[key] = values[0];
32
30
  }
33
- return result
31
+ return result;
34
32
  }
35
33
 
36
34
  // ---------------------------------------------------------------------------
@@ -44,14 +42,12 @@ function resolveThemedValue(
44
42
  ): string | number | boolean | undefined {
45
43
  if (activeTheme && Object.keys(activeTheme).length > 0) {
46
44
  const match = values.find((v) => {
47
- if (!v.theme) return false
48
- return Object.entries(activeTheme).every(
49
- ([key, expected]) => v.theme?.[key] === expected,
50
- )
51
- })
52
- if (match) return match.value
45
+ if (!v.theme) return false;
46
+ return Object.entries(activeTheme).every(([key, expected]) => v.theme?.[key] === expected);
47
+ });
48
+ if (match) return match.value;
53
49
  }
54
- return values[0]?.value
50
+ return values[0]?.value;
55
51
  }
56
52
 
57
53
  /**
@@ -63,21 +59,21 @@ export function resolveVariableRef(
63
59
  variables: Vars,
64
60
  activeTheme?: Theme,
65
61
  ): string | number | boolean | undefined {
66
- if (!ref.startsWith('$')) return undefined
67
- const name = ref.slice(1)
68
- const def = variables[name]
69
- if (!def) return undefined
62
+ if (!ref.startsWith('$')) return undefined;
63
+ const name = ref.slice(1);
64
+ const def = variables[name];
65
+ if (!def) return undefined;
70
66
 
71
- const val = def.value
67
+ const val = def.value;
72
68
  if (Array.isArray(val)) {
73
- const resolved = resolveThemedValue(val, activeTheme)
69
+ const resolved = resolveThemedValue(val, activeTheme);
74
70
  // Circular guard: if resolved value is also a $ref, stop
75
- if (typeof resolved === 'string' && resolved.startsWith('$')) return undefined
76
- return resolved
71
+ if (typeof resolved === 'string' && resolved.startsWith('$')) return undefined;
72
+ return resolved;
77
73
  }
78
74
  // Circular guard
79
- if (typeof val === 'string' && val.startsWith('$')) return undefined
80
- return val
75
+ if (typeof val === 'string' && val.startsWith('$')) return undefined;
76
+ return val;
81
77
  }
82
78
 
83
79
  /**
@@ -89,10 +85,10 @@ export function resolveColorRef(
89
85
  variables: Vars,
90
86
  activeTheme?: Theme,
91
87
  ): string | undefined {
92
- if (color === undefined) return undefined
93
- if (!isVariableRef(color)) return color
94
- const resolved = resolveVariableRef(color, variables, activeTheme)
95
- return typeof resolved === 'string' ? resolved : undefined
88
+ if (color === undefined) return undefined;
89
+ if (!isVariableRef(color)) return color;
90
+ const resolved = resolveVariableRef(color, variables, activeTheme);
91
+ return typeof resolved === 'string' ? resolved : undefined;
96
92
  }
97
93
 
98
94
  /**
@@ -104,16 +100,16 @@ export function resolveNumericRef(
104
100
  variables: Vars,
105
101
  activeTheme?: Theme,
106
102
  ): number | undefined {
107
- if (typeof value === 'number') return value
103
+ if (typeof value === 'number') return value;
108
104
  if (typeof value === 'string') {
109
105
  if (isVariableRef(value)) {
110
- const resolved = resolveVariableRef(value, variables, activeTheme)
111
- return typeof resolved === 'number' ? resolved : undefined
106
+ const resolved = resolveVariableRef(value, variables, activeTheme);
107
+ return typeof resolved === 'number' ? resolved : undefined;
112
108
  }
113
- const num = parseFloat(value)
114
- return isNaN(num) ? undefined : num
109
+ const num = parseFloat(value);
110
+ return isNaN(num) ? undefined : num;
115
111
  }
116
- return undefined
112
+ return undefined;
117
113
  }
118
114
 
119
115
  // ---------------------------------------------------------------------------
@@ -125,22 +121,22 @@ function resolveFillsForCanvas(
125
121
  vars: Vars,
126
122
  theme?: Theme,
127
123
  ): PenFill[] | string | undefined {
128
- if (!fills) return fills
129
- if (typeof fills === 'string') return fills
124
+ if (!fills) return fills;
125
+ if (typeof fills === 'string') return fills;
130
126
  return fills.map((fill) => {
131
127
  if (fill.type === 'solid') {
132
- const color = resolveColorRef(fill.color, vars, theme)
133
- return color !== fill.color ? { ...fill, color: color ?? '#000000' } : fill
128
+ const color = resolveColorRef(fill.color, vars, theme);
129
+ return color !== fill.color ? { ...fill, color: color ?? '#000000' } : fill;
134
130
  }
135
131
  if (fill.type === 'linear_gradient' || fill.type === 'radial_gradient') {
136
132
  const newStops = fill.stops.map((stop) => {
137
- const color = resolveColorRef(stop.color, vars, theme)
138
- return color !== stop.color ? { ...stop, color: color ?? '#000000' } : stop
139
- })
140
- return newStops !== fill.stops ? { ...fill, stops: newStops } : fill
133
+ const color = resolveColorRef(stop.color, vars, theme);
134
+ return color !== stop.color ? { ...stop, color: color ?? '#000000' } : stop;
135
+ });
136
+ return newStops !== fill.stops ? { ...fill, stops: newStops } : fill;
141
137
  }
142
- return fill
143
- })
138
+ return fill;
139
+ });
144
140
  }
145
141
 
146
142
  function resolveStrokeForCanvas(
@@ -148,26 +144,26 @@ function resolveStrokeForCanvas(
148
144
  vars: Vars,
149
145
  theme?: Theme,
150
146
  ): PenStroke | undefined {
151
- if (!stroke) return stroke
152
- let changed = false
153
- const out: Record<string, unknown> = { ...stroke }
147
+ if (!stroke) return stroke;
148
+ let changed = false;
149
+ const out: Record<string, unknown> = { ...stroke };
154
150
 
155
151
  // Resolve thickness
156
152
  if (typeof stroke.thickness === 'string' && isVariableRef(stroke.thickness)) {
157
- out.thickness = resolveNumericRef(stroke.thickness, vars, theme) ?? 1
158
- changed = true
153
+ out.thickness = resolveNumericRef(stroke.thickness, vars, theme) ?? 1;
154
+ changed = true;
159
155
  }
160
156
 
161
157
  // Resolve stroke fill colors
162
158
  if (stroke.fill) {
163
- const resolved = resolveFillsForCanvas(stroke.fill, vars, theme)
159
+ const resolved = resolveFillsForCanvas(stroke.fill, vars, theme);
164
160
  if (resolved !== stroke.fill) {
165
- out.fill = resolved
166
- changed = true
161
+ out.fill = resolved;
162
+ changed = true;
167
163
  }
168
164
  }
169
165
 
170
- return changed ? (out as unknown as PenStroke) : stroke
166
+ return changed ? (out as unknown as PenStroke) : stroke;
171
167
  }
172
168
 
173
169
  function resolveEffectsForCanvas(
@@ -175,26 +171,26 @@ function resolveEffectsForCanvas(
175
171
  vars: Vars,
176
172
  theme?: Theme,
177
173
  ): PenEffect[] | undefined {
178
- if (!effects) return effects
174
+ if (!effects) return effects;
179
175
  return effects.map((effect) => {
180
- if (effect.type !== 'shadow') return effect
181
- let changed = false
182
- const out: Record<string, unknown> = { ...effect }
176
+ if (effect.type !== 'shadow') return effect;
177
+ let changed = false;
178
+ const out: Record<string, unknown> = { ...effect };
183
179
 
184
180
  if (typeof effect.color === 'string' && isVariableRef(effect.color)) {
185
- out.color = resolveColorRef(effect.color, vars, theme) ?? '#000000'
186
- changed = true
181
+ out.color = resolveColorRef(effect.color, vars, theme) ?? '#000000';
182
+ changed = true;
187
183
  }
188
184
  for (const key of ['blur', 'offsetX', 'offsetY', 'spread'] as const) {
189
- const val = effect[key]
185
+ const val = effect[key];
190
186
  if (typeof val === 'string' && isVariableRef(val)) {
191
- out[key] = resolveNumericRef(val, vars, theme) ?? 0
192
- changed = true
187
+ out[key] = resolveNumericRef(val, vars, theme) ?? 0;
188
+ changed = true;
193
189
  }
194
190
  }
195
191
 
196
- return changed ? (out as unknown as PenEffect) : effect
197
- })
192
+ return changed ? (out as unknown as PenEffect) : effect;
193
+ });
198
194
  }
199
195
 
200
196
  // ---------------------------------------------------------------------------
@@ -207,91 +203,87 @@ function resolveEffectsForCanvas(
207
203
  *
208
204
  * Returns the same object reference when no variables are present.
209
205
  */
210
- export function resolveNodeForCanvas(
211
- node: PenNode,
212
- variables: Vars,
213
- activeTheme?: Theme,
214
- ): PenNode {
215
- if (!variables || Object.keys(variables).length === 0) return node
206
+ export function resolveNodeForCanvas(node: PenNode, variables: Vars, activeTheme?: Theme): PenNode {
207
+ if (!variables || Object.keys(variables).length === 0) return node;
216
208
 
217
- let changed = false
218
- const out: Record<string, unknown> = { ...node }
209
+ let changed = false;
210
+ const out: Record<string, unknown> = { ...node };
219
211
 
220
212
  // Opacity
221
213
  if (typeof node.opacity === 'string' && isVariableRef(node.opacity)) {
222
- out.opacity = resolveNumericRef(node.opacity, variables, activeTheme) ?? 1
223
- changed = true
214
+ out.opacity = resolveNumericRef(node.opacity, variables, activeTheme) ?? 1;
215
+ changed = true;
224
216
  }
225
217
 
226
218
  // Gap
227
219
  if ('gap' in node && typeof (node as unknown as Record<string, unknown>).gap === 'string') {
228
- const gap = (node as unknown as Record<string, unknown>).gap as string
220
+ const gap = (node as unknown as Record<string, unknown>).gap as string;
229
221
  if (isVariableRef(gap)) {
230
- out.gap = resolveNumericRef(gap, variables, activeTheme) ?? 0
231
- changed = true
222
+ out.gap = resolveNumericRef(gap, variables, activeTheme) ?? 0;
223
+ changed = true;
232
224
  }
233
225
  }
234
226
 
235
227
  // Padding
236
228
  if ('padding' in node) {
237
- const padding = (node as unknown as Record<string, unknown>).padding
229
+ const padding = (node as unknown as Record<string, unknown>).padding;
238
230
  if (typeof padding === 'string' && isVariableRef(padding)) {
239
- out.padding = resolveNumericRef(padding, variables, activeTheme) ?? 0
240
- changed = true
231
+ out.padding = resolveNumericRef(padding, variables, activeTheme) ?? 0;
232
+ changed = true;
241
233
  }
242
234
  }
243
235
 
244
236
  // Fill
245
237
  if ('fill' in node && (node as unknown as Record<string, unknown>).fill) {
246
- const fills = (node as unknown as Record<string, unknown>).fill as PenFill[] | string
247
- const resolved = resolveFillsForCanvas(fills, variables, activeTheme)
238
+ const fills = (node as unknown as Record<string, unknown>).fill as PenFill[] | string;
239
+ const resolved = resolveFillsForCanvas(fills, variables, activeTheme);
248
240
  if (resolved !== fills) {
249
- out.fill = resolved
250
- changed = true
241
+ out.fill = resolved;
242
+ changed = true;
251
243
  }
252
244
  }
253
245
 
254
246
  // Stroke
255
247
  if ('stroke' in node && (node as unknown as Record<string, unknown>).stroke) {
256
- const stroke = (node as unknown as Record<string, unknown>).stroke as PenStroke
257
- const resolved = resolveStrokeForCanvas(stroke, variables, activeTheme)
248
+ const stroke = (node as unknown as Record<string, unknown>).stroke as PenStroke;
249
+ const resolved = resolveStrokeForCanvas(stroke, variables, activeTheme);
258
250
  if (resolved !== stroke) {
259
- out.stroke = resolved
260
- changed = true
251
+ out.stroke = resolved;
252
+ changed = true;
261
253
  }
262
254
  }
263
255
 
264
256
  // Effects
265
257
  if ('effects' in node && (node as unknown as Record<string, unknown>).effects) {
266
- const effects = (node as unknown as Record<string, unknown>).effects as PenEffect[]
267
- const resolved = resolveEffectsForCanvas(effects, variables, activeTheme)
258
+ const effects = (node as unknown as Record<string, unknown>).effects as PenEffect[];
259
+ const resolved = resolveEffectsForCanvas(effects, variables, activeTheme);
268
260
  if (resolved !== effects) {
269
- out.effects = resolved
270
- changed = true
261
+ out.effects = resolved;
262
+ changed = true;
271
263
  }
272
264
  }
273
265
 
274
266
  // Text content
275
267
  if (node.type === 'text' && typeof node.content === 'string' && isVariableRef(node.content)) {
276
- const resolved = resolveVariableRef(node.content, variables, activeTheme)
268
+ const resolved = resolveVariableRef(node.content, variables, activeTheme);
277
269
  if (typeof resolved === 'string') {
278
- out.content = resolved
279
- changed = true
270
+ out.content = resolved;
271
+ changed = true;
280
272
  }
281
273
  }
282
274
 
283
275
  // Recurse into children
284
276
  if ('children' in node && node.children) {
285
- const children = node.children
277
+ const children = node.children;
286
278
  const resolvedChildren = children.map((child) =>
287
279
  resolveNodeForCanvas(child, variables, activeTheme),
288
- )
280
+ );
289
281
  // Only allocate new array if any child actually changed
290
282
  if (resolvedChildren.some((rc, i) => rc !== children[i])) {
291
- out.children = resolvedChildren
292
- changed = true
283
+ out.children = resolvedChildren;
284
+ changed = true;
293
285
  }
294
286
  }
295
287
 
296
- return changed ? (out as unknown as PenNode) : node
288
+ return changed ? (out as unknown as PenNode) : node;
297
289
  }