@upstart.gg/vite-plugins 0.1.29 → 0.1.31

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 (50) hide show
  1. package/dist/upstart-editor-api.d.ts +13 -1
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +59 -1
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts +12 -7
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +195 -3
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-branding/plugin.d.ts +3 -3
  10. package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -1
  11. package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -1
  12. package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -1
  13. package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
  14. package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
  15. package/dist/vite-plugin-upstart-editor/plugin.js +4 -1
  16. package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +27 -16
  18. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  19. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
  21. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +1 -1
  23. package/dist/vite-plugin-upstart-editor/runtime/index.js +14 -2
  24. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  25. package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.d.ts +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/state.d.ts.map +1 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/state.js.map +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  29. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +212 -28
  30. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  31. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +16 -3
  32. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  33. package/dist/vite-plugin-upstart-editor/runtime/utils.js.map +1 -1
  34. package/dist/vite-plugin-upstart-theme.d.ts +3 -3
  35. package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
  36. package/dist/vite-plugin-upstart-theme.js +2 -2
  37. package/dist/vite-plugin-upstart-theme.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/tests/vite-plugin-upstart-attrs.test.ts +298 -37
  40. package/src/upstart-editor-api.ts +71 -0
  41. package/src/vite-plugin-upstart-attrs.ts +293 -5
  42. package/src/vite-plugin-upstart-editor/plugin.ts +11 -1
  43. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +35 -21
  44. package/src/vite-plugin-upstart-editor/runtime/index.ts +21 -1
  45. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +260 -41
  46. package/src/vite-plugin-upstart-editor/runtime/types.ts +17 -4
  47. package/src/vite-plugin-upstart-theme.ts +4 -1
  48. package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts.map +0 -1
  49. package/dist/src/vite-plugin-upstart-editor/runtime/state.js.map +0 -1
  50. /package/dist/{src/vite-plugin-upstart-editor → vite-plugin-upstart-editor}/runtime/state.js +0 -0
@@ -10,6 +10,12 @@ declare const payloadEditText: z.ZodObject<{
10
10
  content: z.ZodString;
11
11
  }, z.core.$strip>;
12
12
  type PayloadEditText = z.infer<typeof payloadEditText>;
13
+ declare const payloadEditTextDirect: z.ZodObject<{
14
+ action: z.ZodLiteral<"editTextDirect">;
15
+ id: z.ZodString;
16
+ content: z.ZodString;
17
+ }, z.core.$strip>;
18
+ type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;
13
19
  declare const payloadEditClassName: z.ZodObject<{
14
20
  action: z.ZodLiteral<"editClassName">;
15
21
  id: z.ZodString;
@@ -53,11 +59,17 @@ declare class UpstartEditorAPI {
53
59
  * Only updates existing keys — returns an error if the key is not found.
54
60
  */
55
61
  editText(params: PayloadEditText): Promise<EditResult>;
62
+ /**
63
+ * Edit a plain-text or rich-text JSX node directly in the source TSX file.
64
+ * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.
65
+ */
66
+ editTextDirect(params: PayloadEditTextDirect): Promise<EditResult>;
56
67
  /**
57
68
  * Edit the className of an element
58
69
  */
59
70
  editClassName(params: PayloadEditClassName): Promise<EditResult>;
60
71
  private applyEdit;
72
+ private applyMixedTextEdit;
61
73
  /**
62
74
  * Get element info by ID
63
75
  */
@@ -72,5 +84,5 @@ declare class UpstartEditorAPI {
72
84
  getElementsByFile(file: string): Record<string, EditableEntry>;
73
85
  }
74
86
  //#endregion
75
- export { EditResult, EditableRegistry, PayloadEditClassName, PayloadEditText, UpstartEditorAPI, payloadEditClassName, payloadEditText };
87
+ export { EditResult, EditableRegistry, PayloadEditClassName, PayloadEditText, PayloadEditTextDirect, UpstartEditorAPI, payloadEditClassName, payloadEditText, payloadEditTextDirect };
76
88
  //# sourceMappingURL=upstart-editor-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":";;;;cAMa,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;KAWhB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,cAEhC,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;KAMrB,oBAAA,GAAuB,CAAA,CAAE,KAAA,QAAa,oBAAA;AAAA,UAEjC,gBAAA;EACf,OAAA;EACA,WAAA;EACA,QAAA,EAAU,MAAA,SAAe,aAAA;AAAA;AAAA,KAGf,UAAA;EAEN,OAAA;EACA,KAAA;EACA,QAAA;AAAA;EAGA,OAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,cAGO,gBAAA;EAAA,QACH,QAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAER,WAAA,CAAY,WAAA,UAAqB,YAAA;;;;EAQ3B,YAAA,CAAA,GAAgB,OAAA;EAzCG;;;EAiDzB,WAAA,CAAA,GAAe,gBAAA;EAjDe;;;EAwD9B,WAAA,CAAY,QAAA,EAAU,gBAAA;EAtDX;;;;;EA+DL,QAAA,CAAS,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;EAwD3C,aAAA,CAAc,MAAA,EAAQ,oBAAA,GAAuB,OAAA,CAAQ,UAAA;EAAA,QA6B7C,SAAA;EApJiB;;;EAgN/B,UAAA,CAAW,EAAA,WAAa,aAAA;;;;EAOxB,iBAAA,CAAkB,IAAA,yBAA6B,MAAA,SAAe,aAAA;;;;EAiB9D,iBAAA,CAAkB,IAAA,WAAe,MAAA,SAAe,aAAA;AAAA"}
1
+ {"version":3,"file":"upstart-editor-api.d.ts","names":[],"sources":["../src/upstart-editor-api.ts"],"mappings":";;;;cAMa,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;KAWhB,eAAA,GAAkB,CAAA,CAAE,KAAA,QAAa,eAAA;AAAA,cAEhC,qBAAA,EAAqB,CAAA,CAAA,SAAA;;;;;KAMtB,qBAAA,GAAwB,CAAA,CAAE,KAAA,QAAa,qBAAA;AAAA,cAEtC,oBAAA,EAAoB,CAAA,CAAA,SAAA;;;;;KAMrB,oBAAA,GAAuB,CAAA,CAAE,KAAA,QAAa,oBAAA;AAAA,UAEjC,gBAAA;EACf,OAAA;EACA,WAAA;EACA,QAAA,EAAU,MAAA,SAAe,aAAA;AAAA;AAAA,KAGf,UAAA;EAEN,OAAA;EACA,KAAA;EACA,QAAA;AAAA;EAGA,OAAA;EACA,KAAA;EACA,QAAA;AAAA;AAAA,cAGO,gBAAA;EAAA,QACH,QAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAER,WAAA,CAAY,WAAA,UAAqB,YAAA;EAzCE;;;EAiD7B,YAAA,CAAA,GAAgB,OAAA;EAjDoC;;AAE5D;EAuDE,WAAA,CAAA,GAAe,gBAAA;;;;EAOf,WAAA,CAAY,QAAA,EAAU,gBAAA;;;;;;EAShB,QAAA,CAAS,MAAA,EAAQ,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;EAyD3C,cAAA,CAAe,MAAA,EAAQ,qBAAA,GAAwB,OAAA,CAAQ,UAAA;;;;EA6BvD,aAAA,CAAc,MAAA,EAAQ,oBAAA,GAAuB,OAAA,CAAQ,UAAA;EAAA,QA6B7C,SAAA;EAAA,QAiEA,kBAAA;;;;EA4Bd,UAAA,CAAW,EAAA,WAAa,aAAA;EAjRO;;;EAwR/B,iBAAA,CAAkB,IAAA,yBAA6B,MAAA,SAAe,aAAA;EAxR1B;;;EAySpC,iBAAA,CAAkB,IAAA,WAAe,MAAA,SAAe,aAAA;AAAA"}
@@ -10,6 +10,11 @@ const payloadEditText = z.object({
10
10
  key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),
11
11
  content: z.string()
12
12
  });
13
+ const payloadEditTextDirect = z.object({
14
+ action: z.literal("editTextDirect"),
15
+ id: z.string().min(1),
16
+ content: z.string()
17
+ });
13
18
  const payloadEditClassName = z.object({
14
19
  action: z.literal("editClassName"),
15
20
  id: z.string().min(1),
@@ -106,6 +111,37 @@ var UpstartEditorAPI = class {
106
111
  };
107
112
  }
108
113
  /**
114
+ * Edit a plain-text or rich-text JSX node directly in the source TSX file.
115
+ * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.
116
+ */
117
+ async editTextDirect(params) {
118
+ const parsed = payloadEditTextDirect.safeParse(params);
119
+ if (!parsed.success) return {
120
+ success: false,
121
+ error: `Invalid payload: ${parsed.error.message}`
122
+ };
123
+ const { id, content } = parsed.data;
124
+ if (!this.registry) try {
125
+ await this.loadRegistry();
126
+ } catch (err) {
127
+ return {
128
+ success: false,
129
+ error: `Failed to load registry: ${err}`
130
+ };
131
+ }
132
+ const entry = this.registry.elements[id];
133
+ if (!entry) return {
134
+ success: false,
135
+ error: `Element ${id} not found in registry`
136
+ };
137
+ if (entry.type !== "text" && entry.type !== "rich-text" && entry.type !== "mixed-text") return {
138
+ success: false,
139
+ error: `Element ${id} is not a text element (type: ${entry.type})`
140
+ };
141
+ if (entry.type === "mixed-text") return this.applyMixedTextEdit(id, entry, content);
142
+ return this.applyEdit(id, entry, content);
143
+ }
144
+ /**
109
145
  * Edit the className of an element
110
146
  */
111
147
  async editClassName(params) {
@@ -177,6 +213,28 @@ var UpstartEditorAPI = class {
177
213
  }
178
214
  }
179
215
  /**
216
+ * Reconstruct JSX children from a user-edited template and the original expression segments,
217
+ * then write the result back to the TSX source file.
218
+ *
219
+ * `template` uses `{{N}}` placeholders for expressions, e.g.:
220
+ * "© {{0}} Alex's Kitchen. All rights reserved."
221
+ * The server replaces each placeholder with the original expression source from the registry.
222
+ */
223
+ async applyMixedTextEdit(id, entry, template) {
224
+ const exprSegments = (entry.segments ?? []).filter((s) => s.type === "expr");
225
+ const parts = template.split(/\{\{(\d+)\}\}/);
226
+ let inner = "";
227
+ for (let i = 0; i < parts.length; i++) if (i % 2 === 0) inner += parts[i];
228
+ else {
229
+ const exprSeg = exprSegments[parseInt(parts[i])];
230
+ inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;
231
+ }
232
+ const leadingWS = entry.originalContent.match(/^(\s*)/)?.[1] ?? "";
233
+ const trailingWS = entry.originalContent.match(/(\s*)$/)?.[1] ?? "";
234
+ const newContent = leadingWS + inner + trailingWS;
235
+ return this.applyEdit(id, entry, newContent);
236
+ }
237
+ /**
180
238
  * Get element info by ID
181
239
  */
182
240
  getElement(id) {
@@ -202,6 +260,6 @@ var UpstartEditorAPI = class {
202
260
  }
203
261
  };
204
262
  //#endregion
205
- export { UpstartEditorAPI, payloadEditClassName, payloadEditText };
263
+ export { UpstartEditorAPI, payloadEditClassName, payloadEditText, payloadEditTextDirect };
206
264
 
207
265
  //# sourceMappingURL=upstart-editor-api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching\n const searchIndex = code.indexOf(entry.originalContent);\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString\n const s = new MagicString(code);\n s.overwrite(actualStart, actualEnd, newContent);\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditTextDirect = z.object({\n action: z.literal(\"editTextDirect\"),\n id: z.string().min(1),\n content: z.string(),\n});\n\nexport type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit a plain-text or rich-text JSX node directly in the source TSX file.\n * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.\n */\n async editTextDirect(params: PayloadEditTextDirect): Promise<EditResult> {\n const parsed = payloadEditTextDirect.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, content } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n if (entry.type !== \"text\" && entry.type !== \"rich-text\" && entry.type !== \"mixed-text\") {\n return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };\n }\n if (entry.type === \"mixed-text\") {\n return this.applyMixedTextEdit(id, entry, content);\n }\n return this.applyEdit(id, entry, content);\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching\n const searchIndex = code.indexOf(entry.originalContent);\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString\n const s = new MagicString(code);\n s.overwrite(actualStart, actualEnd, newContent);\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Reconstruct JSX children from a user-edited template and the original expression segments,\n * then write the result back to the TSX source file.\n *\n * `template` uses `{{N}}` placeholders for expressions, e.g.:\n * \"© {{0}} Alex's Kitchen. All rights reserved.\"\n * The server replaces each placeholder with the original expression source from the registry.\n */\n private async applyMixedTextEdit(id: string, entry: EditableEntry, template: string): Promise<EditResult> {\n const exprSegments = (entry.segments ?? []).filter((s) => s.type === \"expr\");\n\n // Split template by {{N}} placeholders — odd indices are expression indices\n const parts = template.split(/\\{\\{(\\d+)\\}\\}/);\n\n // Reconstruct inner JSX: text parts interleaved with original expression sources\n let inner = \"\";\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n inner += parts[i];\n } else {\n const exprSeg = exprSegments[parseInt(parts[i])];\n inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;\n }\n }\n\n // Preserve leading/trailing JSX whitespace (indentation) from the original source\n const leadingWS = entry.originalContent.match(/^(\\s*)/)?.[1] ?? \"\";\n const trailingWS = entry.originalContent.match(/(\\s*)$/)?.[1] ?? \"\";\n const newContent = leadingWS + inner + trailingWS;\n\n return this.applyEdit(id, entry, newContent);\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
@@ -1,29 +1,34 @@
1
- import * as magic_string0 from "magic-string";
2
- import * as unplugin from "unplugin";
1
+ import * as _$magic_string0 from "magic-string";
2
+ import * as _$unplugin from "unplugin";
3
3
 
4
4
  //#region src/vite-plugin-upstart-attrs.d.ts
5
5
  interface Options {
6
6
  enabled: boolean;
7
7
  emitRegistry?: boolean;
8
8
  }
9
+ interface EditableSegment {
10
+ type: "text" | "expr";
11
+ raw: string;
12
+ }
9
13
  interface EditableEntry {
10
14
  file: string;
11
- type: "text" | "className";
15
+ type: "text" | "rich-text" | "className" | "mixed-text";
12
16
  startOffset: number;
13
17
  endOffset: number;
14
18
  originalContent: string;
19
+ segments?: EditableSegment[];
15
20
  context: {
16
21
  parentTag: string;
17
22
  };
18
23
  }
19
24
  declare function getRegistry(): Record<string, EditableEntry>;
20
25
  declare function clearRegistry(): void;
21
- declare const upstartEditor: unplugin.UnpluginInstance<Options, boolean>;
26
+ declare const upstartEditor: _$unplugin.UnpluginInstance<Options, boolean>;
22
27
  declare function transformWithOxc(code: string, filePath: string): {
23
28
  code: string;
24
- map: magic_string0.SourceMap;
29
+ map: _$magic_string0.SourceMap;
25
30
  } | null;
26
- declare const _default: (options: Options) => unplugin.VitePlugin<any>[] | unplugin.VitePlugin<any>;
31
+ declare const _default: (options: Options) => _$unplugin.VitePlugin<any>[] | _$unplugin.VitePlugin<any>;
27
32
  //#endregion
28
- export { EditableEntry, clearRegistry, _default as default, getRegistry, transformWithOxc, upstartEditor };
33
+ export { EditableEntry, EditableSegment, clearRegistry, _default as default, getRegistry, transformWithOxc, upstartEditor };
29
34
  //# sourceMappingURL=vite-plugin-upstart-attrs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"mappings":";;;;UAeU,OAAA;EACR,OAAA;EACA,YAAA;AAAA;AAAA,UAwCe,aAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,OAAA;IAAW,SAAA;EAAA;AAAA;AAAA,iBAcG,WAAA,CAAA,GAAe,MAAA,SAAe,aAAA;AAAA,iBAK9B,aAAA,CAAA;AAAA,cAeH,aAAA,EAAa,QAAA,CAAA,gBAAA,CAAA,OAAA;AAAA,iBAoEV,gBAAA,CAAiB,IAAA,UAAc,QAAA;;OAAf,aAAA,CAAA,SAAA;AAAA;AAAA,cAmP/B,QAAA"}
1
+ {"version":3,"file":"vite-plugin-upstart-attrs.d.ts","names":[],"sources":["../src/vite-plugin-upstart-attrs.ts"],"mappings":";;;;UAgBU,OAAA;EACR,OAAA;EACA,YAAA;AAAA;AAAA,UA0Ce,eAAA;EACf,IAAA;EAGA,GAAA;AAAA;AAAA,UAIe,aAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EAEA,QAAA,GAAW,eAAA;EACX,OAAA;IAAW,SAAA;EAAA;AAAA;AAAA,iBAcG,WAAA,CAAA,GAAe,MAAA,SAAe,aAAA;AAAA,iBAK9B,aAAA,CAAA;AAAA,cAeH,aAAA,EAAa,UAAA,CAAA,gBAAA,CAAA,OAAA;AAAA,iBA8FV,gBAAA,CAAiB,IAAA,UAAc,QAAA;;OAAf,eAAA,CAAA,SAAA;AAAA;AAAA,cAiU/B,QAAA"}
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { createUnplugin } from "unplugin";
4
4
  import { parseSync } from "oxc-parser";
5
5
  import { walk } from "zimmerframe";
6
+ import fs from "node:fs";
6
7
  //#region src/vite-plugin-upstart-attrs.ts
7
8
  function hasRange(node) {
8
9
  return node && typeof node.start === "number" && typeof node.end === "number";
@@ -26,12 +27,35 @@ const upstartEditor = createUnplugin((options) => {
26
27
  if (!options.enabled) return { name: "upstart-editor-disabled" };
27
28
  const emitRegistry = options.emitRegistry ?? true;
28
29
  let root = process.cwd();
30
+ let isDevMode = false;
31
+ let devWriteTimer = null;
32
+ const flushDevRegistry = () => {
33
+ if (!isDevMode || editableRegistry.size === 0) return;
34
+ const registryDir = path.join(root, "build", "server");
35
+ const registryPath = path.join(registryDir, "upstart-registry.json");
36
+ const registry = {
37
+ version: 1,
38
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
39
+ elements: Object.fromEntries(editableRegistry)
40
+ };
41
+ fs.mkdirSync(registryDir, { recursive: true });
42
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
43
+ };
44
+ const scheduleDevRegistryWrite = () => {
45
+ if (!isDevMode) return;
46
+ if (devWriteTimer) clearTimeout(devWriteTimer);
47
+ devWriteTimer = setTimeout(flushDevRegistry, 150);
48
+ };
29
49
  return {
30
50
  name: "upstart-editor",
31
51
  enforce: "pre",
32
52
  vite: {
33
53
  configResolved(config) {
34
54
  root = config.root;
55
+ isDevMode = config.command === "serve";
56
+ },
57
+ configureServer(_server) {
58
+ isDevMode = true;
35
59
  },
36
60
  generateBundle() {
37
61
  if (!emitRegistry || editableRegistry.size === 0) return;
@@ -56,6 +80,7 @@ const upstartEditor = createUnplugin((options) => {
56
80
  try {
57
81
  const result = transformWithOxc(code, path.relative(root, id));
58
82
  if (!result) return null;
83
+ scheduleDevRegistryWrite();
59
84
  return {
60
85
  code: result.code,
61
86
  map: result.map
@@ -116,7 +141,7 @@ function transformWithOxc(code, filePath) {
116
141
  const tCallChildren = findTCallsInChildren(jsxNode, state.code, state);
117
142
  const allI18nKeys = [...transChildren, ...tCallChildren];
118
143
  const hasI18n = allI18nKeys.length > 0;
119
- if (hasI18n) {
144
+ if (hasI18n && !hasMixedNonI18nContent(jsxNode, state.code, state, state.constants)) {
120
145
  attributes.push("data-upstart-editable-text=\"true\"");
121
146
  attributes.push("data-upstart-editable-text-mode=\"plain\"");
122
147
  if (allI18nKeys.some((t) => t.keyExpr || t.nsExpr)) {
@@ -128,9 +153,14 @@ function transformWithOxc(code, filePath) {
128
153
  const keys = allI18nKeys.map((t) => escapeProp(t.fullKey)).join(",");
129
154
  attributes.push(`data-upstart-i18n="${keys}"`);
130
155
  }
156
+ const allValueKeys = [...new Set(allI18nKeys.flatMap((t) => t.valueKeys ?? []))];
157
+ if (allValueKeys.length > 0) attributes.push(`data-i18n-values="${allValueKeys.join(",")}"`);
131
158
  }
132
159
  if (isTextLeafElement(jsxNode)) {
133
- if (!hasI18n) attributes.push("data-upstart-editable-text=\"false\"");
160
+ if (!hasI18n) {
161
+ attributes.push("data-upstart-editable-text=\"true\"");
162
+ attributes.push("data-upstart-editable-text-mode=\"direct\"");
163
+ }
134
164
  const textChild = jsxNode.children.find((c) => c.type === "JSXText" && c.value?.trim());
135
165
  if (textChild && hasRange(textChild)) {
136
166
  const id = generateId(state.filePath, textChild);
@@ -147,6 +177,66 @@ function transformWithOxc(code, filePath) {
147
177
  });
148
178
  attributes.push(`data-upstart-id="${id}"`);
149
179
  }
180
+ } else if (!hasI18n && isAlmostLeafElement(jsxNode)) {
181
+ attributes.push("data-upstart-editable-text=\"true\"");
182
+ attributes.push("data-upstart-editable-text-mode=\"rich-panel\"");
183
+ const children = jsxNode.children.filter((c) => hasRange(c));
184
+ const firstChild = children[0];
185
+ const lastChild = children[children.length - 1];
186
+ if (firstChild && lastChild && hasRange(firstChild) && hasRange(lastChild)) {
187
+ const id = generateId(state.filePath, firstChild);
188
+ const originalContent = state.code.slice(firstChild.start, lastChild.end);
189
+ editableRegistry.set(id, {
190
+ file: state.filePath,
191
+ type: "rich-text",
192
+ startOffset: firstChild.start,
193
+ endOffset: lastChild.end,
194
+ originalContent,
195
+ context: { parentTag: tagName || "unknown" }
196
+ });
197
+ attributes.push(`data-upstart-id="${id}"`);
198
+ }
199
+ } else if (!hasI18n && isMixedTextLeafElement(jsxNode)) {
200
+ attributes.push("data-upstart-editable-text=\"true\"");
201
+ attributes.push("data-upstart-editable-text-mode=\"plain\"");
202
+ const rangedChildren = jsxNode.children.filter((c) => hasRange(c));
203
+ const firstChild = rangedChildren[0];
204
+ const lastChild = rangedChildren[rangedChildren.length - 1];
205
+ if (firstChild && lastChild && hasRange(firstChild) && hasRange(lastChild)) {
206
+ const segments = [];
207
+ let exprIndex = 0;
208
+ let templateStr = "";
209
+ for (const child of jsxNode.children) if (child.type === "JSXText") {
210
+ const normalized = normalizeJSXText(child.value);
211
+ segments.push({
212
+ type: "text",
213
+ raw: child.value
214
+ });
215
+ templateStr += normalized;
216
+ } else if (child.type === "JSXExpressionContainer") {
217
+ const expr = child.expression;
218
+ if (!expr || expr.type === "JSXEmptyExpression") continue;
219
+ const raw = hasRange(child) ? state.code.slice(child.start, child.end) : "";
220
+ segments.push({
221
+ type: "expr",
222
+ raw
223
+ });
224
+ templateStr += `{{${exprIndex++}}}`;
225
+ }
226
+ const id = generateId(state.filePath, firstChild);
227
+ const originalContent = state.code.slice(firstChild.start, lastChild.end);
228
+ editableRegistry.set(id, {
229
+ file: state.filePath,
230
+ type: "mixed-text",
231
+ startOffset: firstChild.start,
232
+ endOffset: lastChild.end,
233
+ originalContent,
234
+ segments,
235
+ context: { parentTag: tagName || "unknown" }
236
+ });
237
+ attributes.push(`data-upstart-id="${id}"`);
238
+ attributes.push(`data-upstart-mixed-template="${escapeProp(templateStr.trim())}"`);
239
+ }
150
240
  } else if (!hasI18n && hasVisibleTextContent(jsxNode)) attributes.push("data-upstart-editable-text=\"false\"");
151
241
  const classNameAttr = opening.attributes.find((attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === "className" && attr.value?.type === "Literal" && typeof attr.value.value === "string");
152
242
  if (classNameAttr && classNameAttr.value && hasRange(classNameAttr.value)) {
@@ -355,11 +445,26 @@ function detectTransComponent(jsxElement, code, constants) {
355
445
  if (getJSXElementName(opening) !== "Trans") return null;
356
446
  let keyResult = null;
357
447
  let nsResult = null;
448
+ let valueKeys;
358
449
  for (const attr of opening.attributes) {
359
450
  if (attr.type !== "JSXAttribute" || attr.name.type !== "JSXIdentifier") continue;
360
451
  const attrName = attr.name.name;
361
452
  if (attrName === "i18nKey") keyResult = resolveJSXAttrValue(attr.value, code, constants);
362
453
  if (attrName === "ns") nsResult = resolveJSXAttrValue(attr.value, code, constants);
454
+ if (attrName === "values") {
455
+ const container = attr.value;
456
+ if (container?.type === "JSXExpressionContainer") {
457
+ const expr = container.expression;
458
+ if (expr?.type === "ObjectExpression") {
459
+ const keys = [];
460
+ for (const prop of expr.properties) if ((prop.type === "Property" || prop.type === "ObjectProperty") && prop.key) {
461
+ if (prop.key.type === "Identifier") keys.push(prop.key.name);
462
+ else if (prop.key.type === "Literal" || prop.key.type === "StringLiteral") keys.push(prop.key.value);
463
+ }
464
+ if (keys.length > 0) valueKeys = keys;
465
+ }
466
+ }
467
+ }
363
468
  }
364
469
  if (!keyResult) return null;
365
470
  const key = keyResult.value;
@@ -371,7 +476,8 @@ function detectTransComponent(jsxElement, code, constants) {
371
476
  key,
372
477
  namespace,
373
478
  keyExpr,
374
- nsExpr
479
+ nsExpr,
480
+ valueKeys
375
481
  };
376
482
  }
377
483
  function findTransInChildren(jsxElement, code, constants) {
@@ -427,6 +533,24 @@ function findTCallsInChildren(jsxElement, code, state) {
427
533
  }
428
534
  return results;
429
535
  }
536
+ function hasMixedNonI18nContent(jsxNode, code, state, constants) {
537
+ for (const child of jsxNode.children) if (child.type === "JSXText") {
538
+ if (child.value.trim().length > 0) return true;
539
+ } else if (child.type === "JSXExpressionContainer") {
540
+ const expr = child.expression;
541
+ if (!expr || expr.type === "JSXEmptyExpression") continue;
542
+ if (expr.type === "Literal" && typeof expr.value === "string" && expr.value.trim() === "") continue;
543
+ const tResults = [];
544
+ findTCallInExpression(expr, code, tResults, state);
545
+ if (tResults.length > 0) continue;
546
+ const transResults = [];
547
+ findTransInExpression(expr, code, transResults, constants);
548
+ if (transResults.length > 0) continue;
549
+ const type = expr.type;
550
+ if (type === "Identifier" || type === "MemberExpression" || type === "Literal" || type === "TemplateLiteral") return true;
551
+ }
552
+ return false;
553
+ }
430
554
  function findTCallInExpression(expr, code, results, state) {
431
555
  if (!expr || !expr.type) return;
432
556
  if (expr.type === "CallExpression") {
@@ -503,6 +627,74 @@ function isTextLeafElement(jsxElement) {
503
627
  } else if (child.type === "JSXElement" || child.type === "JSXFragment" || child.type === "JSXExpressionContainer" || child.type === "JSXSpreadChild") return false;
504
628
  return hasText;
505
629
  }
630
+ const INLINE_FORMATTING_TAGS = new Set([
631
+ "b",
632
+ "i",
633
+ "em",
634
+ "strong",
635
+ "u",
636
+ "s",
637
+ "del",
638
+ "ins",
639
+ "mark",
640
+ "small",
641
+ "sub",
642
+ "sup",
643
+ "code",
644
+ "kbd",
645
+ "abbr",
646
+ "cite",
647
+ "time",
648
+ "span",
649
+ "a"
650
+ ]);
651
+ function isAlmostLeafElement(jsxElement) {
652
+ let hasInlineElement = false;
653
+ for (const child of jsxElement.children) {
654
+ if (child.type === "JSXText") continue;
655
+ if (child.type === "JSXElement") {
656
+ const childEl = child;
657
+ const tagName = getJSXElementName(childEl.openingElement);
658
+ if (!tagName || !INLINE_FORMATTING_TAGS.has(tagName.toLowerCase())) return false;
659
+ if (childEl.openingElement.attributes.length > 0) return false;
660
+ for (const grandchild of childEl.children) if (grandchild.type !== "JSXText") return false;
661
+ hasInlineElement = true;
662
+ } else return false;
663
+ }
664
+ return hasInlineElement;
665
+ }
666
+ function normalizeJSXText(raw) {
667
+ const lines = raw.split(/\r\n|\n|\r/);
668
+ const lastNonEmptyIdx = lines.reduce((acc, line, i) => /[^ \t]/.test(line) ? i : acc, -1);
669
+ let result = "";
670
+ for (let i = 0; i < lines.length; i++) {
671
+ let line = lines[i].replace(/\t/g, " ");
672
+ if (i > 0) line = line.replace(/^[ ]+/, "");
673
+ if (i < lines.length - 1) line = line.replace(/[ ]+$/, "");
674
+ if (!line) continue;
675
+ if (i !== lastNonEmptyIdx) line += " ";
676
+ result += line;
677
+ }
678
+ return result;
679
+ }
680
+ function isMixedTextLeafElement(jsxElement) {
681
+ let hasText = false;
682
+ let hasExpr = false;
683
+ for (const child of jsxElement.children) {
684
+ if (child.type === "JSXText") {
685
+ if (normalizeJSXText(child.value).length > 0) hasText = true;
686
+ continue;
687
+ }
688
+ if (child.type === "JSXExpressionContainer") {
689
+ const expr = child.expression;
690
+ if (!expr || expr.type === "JSXEmptyExpression") continue;
691
+ hasExpr = true;
692
+ continue;
693
+ }
694
+ return false;
695
+ }
696
+ return hasText && hasExpr;
697
+ }
506
698
  function hasVisibleTextContent(jsxElement) {
507
699
  for (const child of jsxElement.children) {
508
700
  if (child.type === "JSXText") {