@nnao45/figma-use 0.1.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.
@@ -0,0 +1,253 @@
1
+ import { evaluateXPathToNodes, evaluateXPathToBoolean } from 'fontoxpath'
2
+
3
+ const NODE_TYPES = {
4
+ ELEMENT_NODE: 1,
5
+ ATTRIBUTE_NODE: 2,
6
+ TEXT_NODE: 3,
7
+ DOCUMENT_NODE: 9
8
+ }
9
+
10
+ interface FigmaDocument {
11
+ nodeType: number
12
+ nodeName: string
13
+ documentElement: FigmaNode
14
+ _children?: FigmaNode[]
15
+ }
16
+
17
+ const QUERYABLE_ATTRS = [
18
+ 'name',
19
+ 'width',
20
+ 'height',
21
+ 'x',
22
+ 'y',
23
+ 'visible',
24
+ 'opacity',
25
+ 'cornerRadius',
26
+ 'characters',
27
+ 'fontSize',
28
+ 'layoutMode',
29
+ 'itemSpacing',
30
+ 'paddingTop',
31
+ 'paddingBottom',
32
+ 'paddingLeft',
33
+ 'paddingRight',
34
+ 'strokeWeight',
35
+ 'rotation',
36
+ 'constrainProportions'
37
+ ]
38
+
39
+ interface FigmaAttr {
40
+ nodeType: number
41
+ nodeName: string
42
+ name: string
43
+ localName: string
44
+ namespaceURI: null
45
+ prefix: null
46
+ value: string
47
+ ownerElement: FigmaNode
48
+ }
49
+
50
+ interface FigmaNode {
51
+ nodeType: number
52
+ nodeName: string
53
+ localName: string
54
+ namespaceURI: null
55
+ prefix: null
56
+ _figmaNode: SceneNode | PageNode
57
+ _attrs?: FigmaAttr[]
58
+ _parent?: FigmaNode | null
59
+ _children?: FigmaNode[]
60
+ }
61
+
62
+ function wrapNode(
63
+ node: SceneNode | PageNode,
64
+ parent?: FigmaNode | FigmaDocument | null
65
+ ): FigmaNode {
66
+ const wrapped: FigmaNode = {
67
+ nodeType: NODE_TYPES.ELEMENT_NODE,
68
+ nodeName: node.type,
69
+ localName: node.type,
70
+ namespaceURI: null,
71
+ prefix: null,
72
+ _figmaNode: node,
73
+ _parent: parent as FigmaNode | null
74
+ }
75
+ return wrapped
76
+ }
77
+
78
+ function createDocument(rootNode: SceneNode | PageNode): FigmaDocument {
79
+ const doc: FigmaDocument = {
80
+ nodeType: NODE_TYPES.DOCUMENT_NODE,
81
+ nodeName: '#document',
82
+ documentElement: null as unknown as FigmaNode
83
+ }
84
+ const root = wrapNode(rootNode, doc as unknown as FigmaNode)
85
+ doc.documentElement = root
86
+ doc._children = [root]
87
+ return doc
88
+ }
89
+
90
+ function getAttrs(wrapped: FigmaNode): FigmaAttr[] {
91
+ if (wrapped._attrs) return wrapped._attrs
92
+
93
+ const node = wrapped._figmaNode
94
+ const attrs: FigmaAttr[] = []
95
+
96
+ for (const attrName of QUERYABLE_ATTRS) {
97
+ if (attrName in node) {
98
+ const value = (node as unknown as Record<string, unknown>)[attrName]
99
+ // Skip figma.mixed symbol and undefined/null
100
+ if (value === undefined || value === null || typeof value === 'symbol') {
101
+ continue
102
+ }
103
+ attrs.push({
104
+ nodeType: NODE_TYPES.ATTRIBUTE_NODE,
105
+ nodeName: attrName,
106
+ name: attrName,
107
+ localName: attrName,
108
+ namespaceURI: null,
109
+ prefix: null,
110
+ value: String(value),
111
+ ownerElement: wrapped
112
+ })
113
+ }
114
+ }
115
+
116
+ wrapped._attrs = attrs
117
+ return attrs
118
+ }
119
+
120
+ function getChildren(wrapped: FigmaNode): FigmaNode[] {
121
+ if (wrapped._children) return wrapped._children
122
+
123
+ const node = wrapped._figmaNode
124
+ if (!('children' in node) || !node.children) {
125
+ wrapped._children = []
126
+ return []
127
+ }
128
+
129
+ wrapped._children = node.children.map((child) => wrapNode(child, wrapped))
130
+ return wrapped._children
131
+ }
132
+
133
+ function isDocument(node: unknown): node is FigmaDocument {
134
+ return (node as FigmaDocument)?.nodeType === NODE_TYPES.DOCUMENT_NODE
135
+ }
136
+
137
+ const figmaDomFacade = {
138
+ getAllAttributes(node: FigmaNode | FigmaDocument): FigmaAttr[] {
139
+ if (isDocument(node)) return []
140
+ return getAttrs(node)
141
+ },
142
+
143
+ getAttribute(node: FigmaNode | FigmaDocument, attributeName: string): string | null {
144
+ if (isDocument(node)) return null
145
+ const figmaNode = node._figmaNode
146
+ if (attributeName in figmaNode) {
147
+ const value = (figmaNode as unknown as Record<string, unknown>)[attributeName]
148
+ // Skip figma.mixed symbol and undefined/null
149
+ if (value === undefined || value === null || typeof value === 'symbol') {
150
+ return null
151
+ }
152
+ return String(value)
153
+ }
154
+ return null
155
+ },
156
+
157
+ getChildNodes(node: FigmaNode | FigmaDocument): FigmaNode[] {
158
+ if (isDocument(node)) return node._children || []
159
+ return getChildren(node)
160
+ },
161
+
162
+ getData(node: FigmaAttr): string {
163
+ return node.value
164
+ },
165
+
166
+ getFirstChild(node: FigmaNode | FigmaDocument): FigmaNode | null {
167
+ if (isDocument(node)) return node.documentElement
168
+ const children = getChildren(node)
169
+ return children[0] || null
170
+ },
171
+
172
+ getLastChild(node: FigmaNode | FigmaDocument): FigmaNode | null {
173
+ if (isDocument(node)) return node.documentElement
174
+ const children = getChildren(node)
175
+ return children[children.length - 1] || null
176
+ },
177
+
178
+ getNextSibling(node: FigmaNode | FigmaDocument): FigmaNode | null {
179
+ if (isDocument(node)) return null
180
+ const parent = node._parent
181
+ if (!parent) return null
182
+
183
+ const siblings = getChildren(parent)
184
+ const idx = siblings.indexOf(node)
185
+ return siblings[idx + 1] || null
186
+ },
187
+
188
+ getParentNode(node: FigmaNode | FigmaDocument): FigmaNode | FigmaDocument | null {
189
+ if (isDocument(node)) return null
190
+ return node._parent || null
191
+ },
192
+
193
+ getPreviousSibling(node: FigmaNode | FigmaDocument): FigmaNode | null {
194
+ if (isDocument(node)) return null
195
+ const parent = node._parent
196
+ if (!parent) return null
197
+
198
+ const siblings = getChildren(parent)
199
+ const idx = siblings.indexOf(node)
200
+ return idx > 0 ? siblings[idx - 1] : null
201
+ }
202
+ }
203
+
204
+ interface QueryOptions {
205
+ limit?: number
206
+ }
207
+
208
+ export function queryNodes(
209
+ selector: string,
210
+ root: BaseNode | BaseNode[],
211
+ options: QueryOptions = {}
212
+ ): SceneNode[] {
213
+ const { limit = 1000 } = options
214
+ const roots = Array.isArray(root) ? root : [root]
215
+ const results: SceneNode[] = []
216
+
217
+ console.log('[xpath] Query:', selector)
218
+
219
+ try {
220
+ for (const rootNode of roots) {
221
+ const doc = createDocument(rootNode as PageNode)
222
+
223
+ console.log('[xpath] Evaluating on:', rootNode.name)
224
+
225
+ const nodes = evaluateXPathToNodes(selector, doc, figmaDomFacade)
226
+
227
+ console.log('[xpath] Found:', nodes.length)
228
+
229
+ for (const node of nodes) {
230
+ if (results.length >= limit) break
231
+ const figmaNode = (node as FigmaNode)._figmaNode
232
+ if (figmaNode && 'type' in figmaNode && figmaNode.type !== 'PAGE') {
233
+ results.push(figmaNode as SceneNode)
234
+ }
235
+ }
236
+ }
237
+ } catch (err) {
238
+ console.error('[xpath] Error:', err)
239
+ throw err
240
+ }
241
+
242
+ console.log('[xpath] Total results:', results.length)
243
+ return results
244
+ }
245
+
246
+ export function matchNode(selector: string, node: SceneNode): boolean {
247
+ try {
248
+ const wrapped = wrapNode(node)
249
+ return evaluateXPathToBoolean(`self::*[${selector}]`, wrapped, figmaDomFacade)
250
+ } catch {
251
+ return false
252
+ }
253
+ }