@reframejs/unframer 4.2.0-reframe.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.
Files changed (111) hide show
  1. package/README.md +389 -0
  2. package/bin.js +5 -0
  3. package/dist/babel-jsx.d.ts +24 -0
  4. package/dist/babel-jsx.js +299 -0
  5. package/dist/babel-plugin-imports.d.ts +15 -0
  6. package/dist/babel-plugin-imports.js +211 -0
  7. package/dist/babel-typedoc.d.ts +39 -0
  8. package/dist/babel-typedoc.js +76 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +3 -0
  11. package/dist/cli.d.ts +59 -0
  12. package/dist/cli.js +821 -0
  13. package/dist/compat.d.ts +2 -0
  14. package/dist/compat.js +23 -0
  15. package/dist/config.d.ts +18 -0
  16. package/dist/config.js +27 -0
  17. package/dist/css-core.d.ts +50 -0
  18. package/dist/css-core.js +231 -0
  19. package/dist/css.d.ts +15 -0
  20. package/dist/css.js +36 -0
  21. package/dist/esbuild.d.ts +31 -0
  22. package/dist/esbuild.js +317 -0
  23. package/dist/exporter.d.ts +96 -0
  24. package/dist/exporter.js +1151 -0
  25. package/dist/flat-cache-interceptor.d.ts +24 -0
  26. package/dist/flat-cache-interceptor.js +87 -0
  27. package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
  28. package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
  29. package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
  30. package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
  31. package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
  32. package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
  33. package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
  34. package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
  35. package/dist/framer-client.server.d.ts +4 -0
  36. package/dist/framer-client.server.js +45 -0
  37. package/dist/framer-package-versions.d.ts +781 -0
  38. package/dist/framer-package-versions.js +782 -0
  39. package/dist/framer-types.d.ts +30 -0
  40. package/dist/framer-types.js +35 -0
  41. package/dist/framer.d.ts +4990 -0
  42. package/dist/framer.js +64296 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +13 -0
  45. package/dist/package-manager.d.ts +10 -0
  46. package/dist/package-manager.js +138 -0
  47. package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
  48. package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
  49. package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
  50. package/dist/plugin-mcp-dist/lib/cms.js +58 -0
  51. package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
  52. package/dist/plugin-mcp-dist/lib/errors.js +48 -0
  53. package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
  54. package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
  55. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
  56. package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
  57. package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
  58. package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
  59. package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
  60. package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
  61. package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
  62. package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
  63. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
  64. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
  65. package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
  66. package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
  67. package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
  68. package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
  69. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
  70. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
  71. package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
  72. package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
  73. package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
  74. package/dist/plugin-mcp-dist/lib/schema.js +907 -0
  75. package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
  76. package/dist/plugin-mcp-dist/lib/store.js +10 -0
  77. package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
  78. package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
  79. package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
  80. package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
  81. package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
  82. package/dist/plugin-mcp-dist/lib/types.js +188 -0
  83. package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
  84. package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
  85. package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
  86. package/dist/plugin-mcp-dist/lib/utils.js +57 -0
  87. package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
  88. package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
  89. package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
  90. package/dist/plugin-mcp-dist/lib/xml.js +395 -0
  91. package/dist/react.d.ts +76 -0
  92. package/dist/react.js +280 -0
  93. package/dist/renamer.d.ts +11 -0
  94. package/dist/renamer.js +139 -0
  95. package/dist/sentry.d.ts +2 -0
  96. package/dist/sentry.js +5 -0
  97. package/dist/stackblitz.d.ts +9 -0
  98. package/dist/stackblitz.js +179 -0
  99. package/dist/styles/framer.css +920 -0
  100. package/dist/styles/reset.css +37 -0
  101. package/dist/typescript.d.ts +24 -0
  102. package/dist/typescript.js +197 -0
  103. package/dist/undici-dispatcher.d.ts +2 -0
  104. package/dist/undici-dispatcher.js +10 -0
  105. package/dist/unframer-loader.d.ts +2 -0
  106. package/dist/unframer-loader.js +12 -0
  107. package/dist/utils.d.ts +20 -0
  108. package/dist/utils.js +81 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.js +2 -0
  111. package/package.json +131 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Helper functions for picking the active upstream plugin socket and matching
3
+ * in-flight requests to the socket that sent them.
4
+ */
5
+ const WS_OPEN = 1;
6
+ export function getAttachmentConnectionId(attachment) {
7
+ return attachment?.connectionId || '';
8
+ }
9
+ export function getAttachmentConnectionOrdinal(attachment) {
10
+ return attachment?.connectionOrdinal || 0;
11
+ }
12
+ export function getAttachmentConnectedAt(attachment) {
13
+ return attachment?.connectedAt || 0;
14
+ }
15
+ export function isAttachmentReadyForRequests(attachment) {
16
+ return attachment?.readyAcked !== false;
17
+ }
18
+ export function isSocketUsable(socket) {
19
+ if (typeof socket.readyState === 'undefined') {
20
+ return true;
21
+ }
22
+ return socket.readyState === WS_OPEN;
23
+ }
24
+ export function pickActiveUpstreamSocket({ sockets, }) {
25
+ return sockets
26
+ .filter((socket) => {
27
+ return isSocketUsable(socket);
28
+ })
29
+ .filter((socket) => {
30
+ return isAttachmentReadyForRequests(socket.deserializeAttachment());
31
+ })
32
+ .sort((left, right) => {
33
+ const rightOrdinal = getAttachmentConnectionOrdinal(right.deserializeAttachment());
34
+ const leftOrdinal = getAttachmentConnectionOrdinal(left.deserializeAttachment());
35
+ if (rightOrdinal !== leftOrdinal) {
36
+ return rightOrdinal - leftOrdinal;
37
+ }
38
+ return (getAttachmentConnectedAt(right.deserializeAttachment()) -
39
+ getAttachmentConnectedAt(left.deserializeAttachment()));
40
+ })[0];
41
+ }
42
+ export function matchesPendingRequestSocket({ attachment, pendingRequest, }) {
43
+ const attachmentConnectionId = getAttachmentConnectionId(attachment);
44
+ if (attachmentConnectionId && pendingRequest.upstreamConnectionId) {
45
+ return attachmentConnectionId === pendingRequest.upstreamConnectionId;
46
+ }
47
+ const attachmentConnectionOrdinal = getAttachmentConnectionOrdinal(attachment);
48
+ if (attachmentConnectionOrdinal && pendingRequest.upstreamConnectionOrdinal) {
49
+ return attachmentConnectionOrdinal === pendingRequest.upstreamConnectionOrdinal;
50
+ }
51
+ const attachmentConnectedAt = getAttachmentConnectedAt(attachment);
52
+ const pendingConnectedAt = pendingRequest.upstreamConnectedAt || 0;
53
+ return attachmentConnectedAt === pendingConnectedAt;
54
+ }
55
+ //# sourceMappingURL=upstream-socket.js.map
56
+ //# sourceMappingURL=upstream-socket.js.map
@@ -0,0 +1,13 @@
1
+ export declare const pluginApiClient: import("spiceflow/client").SpiceflowFetch<import("spiceflow/dist/spiceflow").AnySpiceflow>;
2
+ export declare const noop: () => void;
3
+ export declare function isTruthy(val: any): boolean;
4
+ export declare var Paths: any;
5
+ export declare var RouteIds: any;
6
+ export declare const globalState: {};
7
+ export declare var LocalStorageKeys: any;
8
+ export declare function getMcpPluginData(): {
9
+ sessionKey: string;
10
+ };
11
+ export declare function withMode(path: any, params: any): string;
12
+ export declare function sleep(ms: any): Promise<unknown>;
13
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { redirect } from 'react-router';
2
+ import { createSpiceflowFetch } from 'spiceflow/client';
3
+ import { superjsonOnResponse } from 'website/src/lib/superjson-client';
4
+ const PUBLIC_URL = process.env.PUBLIC_URL || 'https://unframer.co';
5
+ export const pluginApiClient = createSpiceflowFetch(PUBLIC_URL, {
6
+ onResponse: [
7
+ async (response) => {
8
+ if (response.status === 401) {
9
+ console.log('clearing session because api returned 401');
10
+ localStorage.removeItem(LocalStorageKeys.sessionId);
11
+ throw redirect(withMode(Paths.login));
12
+ }
13
+ },
14
+ superjsonOnResponse,
15
+ ],
16
+ async onRequest() {
17
+ const { sessionKey } = getMcpPluginData();
18
+ return {
19
+ headers: {
20
+ sessionKey,
21
+ },
22
+ };
23
+ },
24
+ });
25
+ export const noop = () => { };
26
+ export function isTruthy(val) {
27
+ return Boolean(val);
28
+ }
29
+ export var Paths;
30
+ (function (Paths) {
31
+ Paths["login"] = "/login";
32
+ Paths["main"] = "/";
33
+ })(Paths || (Paths = {}));
34
+ export var RouteIds;
35
+ (function (RouteIds) {
36
+ RouteIds["root"] = "root";
37
+ })(RouteIds || (RouteIds = {}));
38
+ export const globalState = {};
39
+ export var LocalStorageKeys;
40
+ (function (LocalStorageKeys) {
41
+ LocalStorageKeys["sessionId"] = "framer-mcp-session-id";
42
+ })(LocalStorageKeys || (LocalStorageKeys = {}));
43
+ export function getMcpPluginData() {
44
+ const sessionKey = localStorage.getItem(LocalStorageKeys.sessionId);
45
+ return {
46
+ sessionKey: sessionKey || '',
47
+ };
48
+ }
49
+ export function withMode(path, params) {
50
+ const searchParams = new URLSearchParams(params);
51
+ return `${path}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
52
+ }
53
+ export function sleep(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ //# sourceMappingURL=utils.js.map
57
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1,10 @@
1
+ export declare function createWebsocketHandling({ ws, defaultTimeout }: {
2
+ defaultTimeout?: number | undefined;
3
+ ws: any;
4
+ }): {
5
+ send: ({ idempotenceKey, payload, }: {
6
+ idempotenceKey: any;
7
+ payload: any;
8
+ }) => Promise<unknown>;
9
+ };
10
+ //# sourceMappingURL=websocket-server.d.ts.map
@@ -0,0 +1,88 @@
1
+ export function createWebsocketHandling({ ws, defaultTimeout = 1000 * 5, }) {
2
+ const pendingRequests = new Map();
3
+ const usedIdempotenceIds = new Set();
4
+ const send = async ({ idempotenceKey, payload, }) => {
5
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
6
+ throw new Error('WebSocket instance not open.');
7
+ }
8
+ const id = crypto.randomUUID();
9
+ const message = {
10
+ id,
11
+ payload,
12
+ };
13
+ return new Promise((resolve, reject) => {
14
+ // If idempotenceKey is specified and already used, return resolved promise immediately
15
+ if (idempotenceKey && usedIdempotenceIds.has(idempotenceKey)) {
16
+ console.log(`Idempotence ID ${idempotenceKey} already used, skipping docs payload set payload`);
17
+ return Promise.resolve(undefined);
18
+ }
19
+ const timeout = setTimeout(() => {
20
+ pendingRequests.delete(id);
21
+ reject(new Error(`Request ${id} timed out after ${defaultTimeout}ms`));
22
+ }, defaultTimeout);
23
+ pendingRequests.set(id, { resolve, reject, timeout });
24
+ ws.send(JSON.stringify(message));
25
+ if (idempotenceKey) {
26
+ usedIdempotenceIds.add(idempotenceKey);
27
+ }
28
+ });
29
+ };
30
+ function onMessage(event) {
31
+ let msg;
32
+ try {
33
+ // Handle both string and Blob data
34
+ if (typeof event.data === 'string') {
35
+ msg = JSON.parse(event.data);
36
+ }
37
+ else if (event.data instanceof Blob) {
38
+ // Skip binary data
39
+ return;
40
+ }
41
+ }
42
+ catch (err) {
43
+ // ignore parse errors
44
+ return;
45
+ }
46
+ if (!msg)
47
+ return;
48
+ const { id, payload, error } = msg;
49
+ if (!id)
50
+ return;
51
+ const pending = pendingRequests.get(id);
52
+ if (!pending)
53
+ return;
54
+ pendingRequests.delete(id);
55
+ clearTimeout(pending.timeout);
56
+ if (error) {
57
+ pending.reject(new Error(error));
58
+ }
59
+ else {
60
+ pending.resolve(payload?.output ?? null);
61
+ }
62
+ }
63
+ // Attach ws 'message' event handler
64
+ ws.addEventListener('message', onMessage);
65
+ const cleanup = async () => {
66
+ ws.removeEventListener('message', onMessage);
67
+ send({
68
+ payload: { type: 'close' },
69
+ });
70
+ // Clean up any pending requests
71
+ for (const [id, pending] of Array.from(pendingRequests)) {
72
+ clearTimeout(pending.timeout);
73
+ pending.reject(new Error('RPC client cleanup'));
74
+ }
75
+ pendingRequests.clear();
76
+ usedIdempotenceIds.clear();
77
+ ws.close();
78
+ };
79
+ // Register cleanup on ws close
80
+ ws.addEventListener('close', () => {
81
+ cleanup();
82
+ });
83
+ return {
84
+ send,
85
+ };
86
+ }
87
+ //# sourceMappingURL=websocket-server.js.map
88
+ //# sourceMappingURL=websocket-server.js.map
@@ -0,0 +1,10 @@
1
+ export declare function rewriteXmlContentForTests({ xml: xml, newContent }: {
2
+ newContent: any;
3
+ xml: any;
4
+ }): string;
5
+ export declare const TEMP_NODE_ID_PREFIX = "_temp_";
6
+ export declare function extractObjectsFromXmlContent(xml: any, options: any): any[];
7
+ export declare function xmlToFramerLayersTree(xml: any): any;
8
+ export declare function framerLayersTreeToXml(tree: any, options?: {}): string;
9
+ export declare function addNodeCount(tree: any): any;
10
+ //# sourceMappingURL=xml.d.ts.map
@@ -0,0 +1,395 @@
1
+ import { DomHandler, Parser, ElementType } from 'htmlparser2';
2
+ import domSerializer from 'dom-serializer';
3
+ import camelCase from 'camelcase';
4
+ export function rewriteXmlContentForTests({ xml: xml, newContent, }) {
5
+ const handler = new DomHandler((error, dom) => {
6
+ if (error) {
7
+ console.error(error);
8
+ }
9
+ else {
10
+ const dfs = (node) => {
11
+ if (node.type === ElementType.Tag &&
12
+ node.attribs &&
13
+ node.attribs.nodeId) {
14
+ const nodeId = node.attribs.nodeId;
15
+ const matchingContent = newContent.find((item) => item.nodeId === nodeId);
16
+ if (matchingContent?.newContent) {
17
+ node.children.forEach((child) => {
18
+ if (child.type === 'text') {
19
+ const initialSpace = child.data.match(/^\s+/)?.toString() || '';
20
+ const endSpace = child.data.match(/\s+$/)?.toString() || '';
21
+ const text = matchingContent.newContent || '';
22
+ child.data = initialSpace + text + endSpace;
23
+ }
24
+ });
25
+ }
26
+ }
27
+ if (node.children) {
28
+ node.children.forEach(dfs);
29
+ }
30
+ };
31
+ dom.forEach(dfs);
32
+ }
33
+ });
34
+ const parser = new Parser(handler, { xmlMode: true });
35
+ parser.write(xml);
36
+ parser.end();
37
+ const serialized = domSerializer(handler.dom, {
38
+ xmlMode: true,
39
+ encodeEntities: false,
40
+ decodeEntities: false,
41
+ });
42
+ return serialized;
43
+ }
44
+ function deIndent(str) {
45
+ if (!str) {
46
+ return str;
47
+ }
48
+ const lines = str.split('\n').filter(Boolean);
49
+ if (lines.length === 0) {
50
+ return str;
51
+ }
52
+ const minIndent = Math.min(...lines.map((line) => {
53
+ const match = line.match(/^\s*/);
54
+ return match ? match[0].length : 0;
55
+ }));
56
+ return lines.map((line) => line.slice(minIndent)).join('\n');
57
+ }
58
+ // Constant for temporary node ID prefix
59
+ export const TEMP_NODE_ID_PREFIX = '_temp_';
60
+ function determineNodeType(node, attributes) {
61
+ // Check if it has direct text content (Text node)
62
+ if (node.children) {
63
+ const hasDirectText = node.children.some((child) => child.type === 'text' && child.data.trim());
64
+ // If it has text content and no other identifying attributes, it's a Text node
65
+ if (hasDirectText &&
66
+ !attributes.layout &&
67
+ !attributes.svg &&
68
+ !attributes.componentId) {
69
+ return 'Text';
70
+ }
71
+ }
72
+ // Check for component instance
73
+ if (attributes.componentId || attributes.insertUrl) {
74
+ return 'ComponentInstance';
75
+ }
76
+ // Check for SVG
77
+ if (attributes.svg) {
78
+ return 'SVG';
79
+ }
80
+ // Otherwise it's a Frame (including those with layout="stack" or layout="grid")
81
+ return 'Frame';
82
+ }
83
+ export function extractObjectsFromXmlContent(xml, options) {
84
+ const results = [];
85
+ let tempIdCounter = 0;
86
+ const enableNodeCreation = options?.enableNodeCreation ?? false;
87
+ const handler = new DomHandler((error, dom) => {
88
+ if (error) {
89
+ console.error('error', error);
90
+ }
91
+ else {
92
+ const dfs = (node, lastParentWithId) => {
93
+ let currentParentId = lastParentWithId;
94
+ if (node.type === ElementType.Tag) {
95
+ let nodeId = node.attribs?.nodeId;
96
+ const hasExistingId = !!nodeId;
97
+ // Only process nodes that have an ID or when node creation is enabled
98
+ if (!hasExistingId && !enableNodeCreation) {
99
+ // Skip this node but continue processing children
100
+ if (node.children) {
101
+ node.children.forEach((child) => dfs(child, lastParentWithId));
102
+ }
103
+ return;
104
+ }
105
+ // Assign temporary ID for new nodes when creation is enabled
106
+ if (!nodeId && enableNodeCreation) {
107
+ nodeId = `${TEMP_NODE_ID_PREFIX}${++tempIdCounter}`;
108
+ }
109
+ let text = '';
110
+ // Only get direct text children, not all descendants
111
+ if (node.children) {
112
+ node.children.forEach((child) => {
113
+ if (child.type === 'text') {
114
+ text += child.data;
115
+ }
116
+ });
117
+ }
118
+ const attributes = { ...node.attribs };
119
+ delete attributes.nodeId;
120
+ const extractedNode = {
121
+ nodeId: nodeId,
122
+ newContent: deIndent(text).trim(),
123
+ attributes,
124
+ };
125
+ // Determine node type for new nodes
126
+ if (!hasExistingId && enableNodeCreation) {
127
+ extractedNode.nodeType = determineNodeType(node, attributes);
128
+ }
129
+ // Add parent information if available
130
+ if (lastParentWithId) {
131
+ extractedNode.parentId = lastParentWithId;
132
+ }
133
+ // This node becomes the parent for its children
134
+ currentParentId = nodeId;
135
+ results.push(extractedNode);
136
+ }
137
+ if (node.children) {
138
+ // Pass the current parent ID (either this node's ID or the last parent with ID)
139
+ node.children.forEach((child) => dfs(child, currentParentId));
140
+ }
141
+ };
142
+ dom.forEach((node) => dfs(node));
143
+ // Group nodes by their parent
144
+ const nodesByParent = new Map();
145
+ results.forEach((node) => {
146
+ const parentId = node.parentId;
147
+ if (!nodesByParent.has(parentId)) {
148
+ nodesByParent.set(parentId, []);
149
+ }
150
+ nodesByParent.get(parentId).push(node);
151
+ });
152
+ // Calculate siblings for each group
153
+ nodesByParent.forEach((siblings) => {
154
+ siblings.forEach((node, index) => {
155
+ // Set beforeNodeId if there's a previous sibling
156
+ if (index > 0) {
157
+ node.beforeNodeId = siblings[index - 1].nodeId;
158
+ }
159
+ // Set afterNodeId if there's a next sibling
160
+ if (index < siblings.length - 1) {
161
+ node.afterNodeId = siblings[index + 1].nodeId;
162
+ }
163
+ });
164
+ });
165
+ }
166
+ });
167
+ const parser = new Parser(handler, { xmlMode: true });
168
+ parser.write(xml);
169
+ parser.end();
170
+ return results;
171
+ }
172
+ export function xmlToFramerLayersTree(xml) {
173
+ const handler = new DomHandler();
174
+ const parser = new Parser(handler, { xmlMode: true });
175
+ parser.write(xml);
176
+ parser.end();
177
+ function processNode(node) {
178
+ if (node.type !== 'tag') {
179
+ return null;
180
+ }
181
+ const result = {
182
+ name: node.name,
183
+ };
184
+ if (node.attribs) {
185
+ const { nodeId, ...attrs } = node.attribs;
186
+ if (nodeId) {
187
+ result.nodeId = nodeId;
188
+ }
189
+ if (Object.keys(attrs).length > 0) {
190
+ result.attributes = attrs;
191
+ }
192
+ }
193
+ if (node.children) {
194
+ const textNodes = node.children.filter((child) => child.type === 'text');
195
+ if (textNodes.length > 0) {
196
+ result.content = textNodes
197
+ .map((node) => node.data.trim())
198
+ .join('\n')
199
+ .trim();
200
+ }
201
+ const childElements = node.children.filter((child) => child.type === 'tag');
202
+ if (childElements.length > 0) {
203
+ const children = childElements
204
+ .map(processNode)
205
+ .filter((n) => n !== null);
206
+ if (children.length > 0) {
207
+ result.children = children;
208
+ }
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+ const rootNodes = handler.dom
214
+ .filter((node) => node.type === 'tag')
215
+ .map(processNode)
216
+ .filter((n) => n !== null);
217
+ return addNodeCount(rootNodes);
218
+ }
219
+ export function framerLayersTreeToXml(tree, options = {}) {
220
+ const { shouldAddNodeIdAlways = false, indent = '', maxCharacters = 15000, currentDepth = 0, currentCharCount = 0, } = options;
221
+ let xml = '';
222
+ let charCount = currentCharCount;
223
+ const seenComponentIds = new Set();
224
+ for (const node of tree) {
225
+ if (!node) {
226
+ continue;
227
+ }
228
+ if (node.name === '') {
229
+ if (node.content) {
230
+ const line = `${indent}${escapeXml(node.content)}\n`;
231
+ xml += line;
232
+ charCount += line.length;
233
+ }
234
+ if (node.children && node.children.length > 0) {
235
+ const childXml = framerLayersTreeToXml(node.children, {
236
+ shouldAddNodeIdAlways,
237
+ indent,
238
+ maxCharacters,
239
+ currentDepth: currentDepth + 1,
240
+ currentCharCount: charCount,
241
+ });
242
+ xml += childXml;
243
+ charCount += childXml.length;
244
+ }
245
+ continue;
246
+ }
247
+ let name = node.name || 'Container';
248
+ let nodeName = camelCase(name?.replace(/[^a-zA-Z0-9\s_-]+/g, ' ') || 'None', {
249
+ pascalCase: true,
250
+ }) || 'Node';
251
+ let max = 60;
252
+ if (nodeName.length > max) {
253
+ const lastUnderscoreIndex = nodeName.indexOf('_', max);
254
+ if (lastUnderscoreIndex > 0) {
255
+ nodeName = nodeName.substring(0, lastUnderscoreIndex);
256
+ }
257
+ else {
258
+ nodeName = nodeName.substring(0, max);
259
+ }
260
+ }
261
+ const attributes = [];
262
+ let shouldAddNodeId = shouldAddNodeIdAlways || !node?.children?.length;
263
+ if (shouldAddNodeId && node.nodeId) {
264
+ attributes.push(`nodeId="${node.nodeId}"`);
265
+ }
266
+ // Track componentId to avoid duplicate comments
267
+ const componentId = node.attributes?.componentId;
268
+ const shouldShowComments = !componentId || !seenComponentIds.has(componentId);
269
+ if (componentId && shouldShowComments) {
270
+ seenComponentIds.add(componentId);
271
+ }
272
+ let hasComments = false;
273
+ if (node.attributes) {
274
+ for (const [key, value] of Object.entries(node.attributes)) {
275
+ if (value !== undefined && value !== null) {
276
+ const comment = node.attrControlsComments?.[key];
277
+ // Only show comments if this is the first instance of this componentId
278
+ // or if it's not a component-specific attribute comment
279
+ const isComponentSpecificComment = comment &&
280
+ ![
281
+ 'componentId',
282
+ 'inlineTextStyle',
283
+ 'backgroundImage',
284
+ 'backgroundColor',
285
+ ].includes(key);
286
+ if (comment != null &&
287
+ comment &&
288
+ (shouldShowComments || !isComponentSpecificComment)) {
289
+ hasComments = true;
290
+ attributes.push(`<!-- ${comment} -->\n${indent} ${key}="${value}"`);
291
+ }
292
+ else {
293
+ attributes.push(`${key}="${value}"`);
294
+ }
295
+ }
296
+ }
297
+ }
298
+ const attributesString = attributes.length > 0
299
+ ? hasComments || attributes.length >= 3
300
+ ? '\n' +
301
+ indent +
302
+ ' ' +
303
+ attributes.join('\n' + indent + ' ') +
304
+ '\n' +
305
+ indent
306
+ : ' ' + attributes.join(' ')
307
+ : '';
308
+ // Add node comment before the element if present
309
+ if (node.comment) {
310
+ xml += `${indent}<!-- ${node.comment} -->\n`;
311
+ }
312
+ // Check if this should be a self-closing tag
313
+ const hasContent = node.content && node.content.trim() !== '';
314
+ const hasChildren = node.children && node.children.length > 0;
315
+ const isSelfClosing = !hasContent && !hasChildren && !node.disableSelfClosing;
316
+ if (isSelfClosing) {
317
+ const line = `${indent}<${nodeName}${attributesString} />\n`;
318
+ xml += line;
319
+ charCount += line.length;
320
+ }
321
+ else {
322
+ const openTag = `${indent}<${nodeName}${attributesString}>\n`;
323
+ xml += openTag;
324
+ charCount += openTag.length;
325
+ if (node.content) {
326
+ const contentLine = `${indent} ${escapeXml(node.content)}\n`;
327
+ xml += contentLine;
328
+ charCount += contentLine.length;
329
+ }
330
+ if (node.children && node.children.length > 0) {
331
+ // If we're at depth 1 and we've already exceeded the limit, add comment instead of rendering children
332
+ if (currentDepth === 1 && charCount > maxCharacters) {
333
+ const comment = `${indent} <!-- Call getNodeXml on this node to get more details, character limit was reached -->\n`;
334
+ xml += comment;
335
+ charCount += comment.length;
336
+ }
337
+ else {
338
+ // Otherwise render children normally
339
+ const childXml = framerLayersTreeToXml(node.children, {
340
+ shouldAddNodeIdAlways,
341
+ indent: indent + ' ',
342
+ maxCharacters,
343
+ currentDepth: currentDepth + 1,
344
+ currentCharCount: charCount,
345
+ });
346
+ xml += childXml;
347
+ charCount += childXml.length;
348
+ }
349
+ }
350
+ const closeTag = `${indent}</${nodeName}>\n`;
351
+ xml += closeTag;
352
+ charCount += closeTag.length;
353
+ }
354
+ }
355
+ return xml;
356
+ }
357
+ function escapeXml(unsafe) {
358
+ return unsafe.replace(/[<>&'"]/g, (c) => {
359
+ switch (c) {
360
+ case '<':
361
+ return '&lt;';
362
+ case '>':
363
+ return '&gt;';
364
+ case '&':
365
+ return '&amp;';
366
+ case "'":
367
+ return '&apos;';
368
+ case '"':
369
+ return '&quot;';
370
+ default:
371
+ return c;
372
+ }
373
+ });
374
+ }
375
+ export function addNodeCount(tree) {
376
+ const result = [];
377
+ if (tree?.length === 0)
378
+ return result;
379
+ function countNodes(node) {
380
+ let count = 1;
381
+ if (node.children) {
382
+ for (const child of node.children) {
383
+ count += countNodes(child);
384
+ }
385
+ }
386
+ node.count = count;
387
+ return count;
388
+ }
389
+ for (const rootNode of tree) {
390
+ countNodes(rootNode);
391
+ }
392
+ return tree;
393
+ }
394
+ //# sourceMappingURL=xml.js.map
395
+ //# sourceMappingURL=xml.js.map