mulmoclaude 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.
Files changed (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Formula Evaluator
3
+ *
4
+ * Evaluates spreadsheet formulas including functions, cell references, and arithmetic
5
+ */
6
+
7
+ import { functionRegistry } from "./registry";
8
+ import type { CellValue } from "./types";
9
+ import { parseDate } from "./date-parser";
10
+
11
+ /**
12
+ * Evaluation context for formulas
13
+ */
14
+ export interface EvaluatorContext {
15
+ getCellValue: (ref: string) => CellValue;
16
+ getRangeValues: (range: string) => CellValue[];
17
+ getRangeValuesRaw?: (range: string) => CellValue[];
18
+ evaluateFormula: (formula: string) => CellValue;
19
+ }
20
+
21
+ /**
22
+ * Parse function arguments, handling nested functions and quoted strings
23
+ *
24
+ * @param argsStr - String containing function arguments
25
+ * @returns Array of argument strings
26
+ */
27
+ export function parseFunctionArgs(argsStr: string): string[] {
28
+ const args: string[] = [];
29
+ let currentArg = "";
30
+ let depth = 0;
31
+ let inString = false;
32
+ let stringChar = "";
33
+
34
+ for (let i = 0; i < argsStr.length; i++) {
35
+ const char = argsStr[i];
36
+ const prevChar = i > 0 ? argsStr[i - 1] : "";
37
+
38
+ // Handle string boundaries
39
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
40
+ if (!inString) {
41
+ inString = true;
42
+ stringChar = char;
43
+ } else if (char === stringChar) {
44
+ inString = false;
45
+ stringChar = "";
46
+ }
47
+ currentArg += char;
48
+ continue;
49
+ }
50
+
51
+ // Track parentheses depth (for nested functions)
52
+ if (!inString) {
53
+ if (char === "(") depth++;
54
+ if (char === ")") depth--;
55
+
56
+ // Split on comma only at depth 0 and not in string
57
+ if (char === "," && depth === 0) {
58
+ args.push(currentArg.trim());
59
+ currentArg = "";
60
+ continue;
61
+ }
62
+ }
63
+
64
+ currentArg += char;
65
+ }
66
+
67
+ if (currentArg.trim()) {
68
+ args.push(currentArg.trim());
69
+ }
70
+
71
+ return args;
72
+ }
73
+
74
+ /**
75
+ * Evaluate a formula string
76
+ *
77
+ * Supports:
78
+ * - Function calls: SUM(A1:A10), ROUND(B2, 2)
79
+ * - Cell references: A1, B2, Sheet1!A1
80
+ * - Arithmetic: 2+3, A1*B1, (A1+B1)/2
81
+ * - Nested expressions: ROUND(SUM(A1:A10)/COUNT(A1:A10), 2)
82
+ *
83
+ * @param formula - Formula string (without leading =)
84
+ * @param context - Evaluation context with cell/range accessors
85
+ * @returns Evaluated result (number or string)
86
+ */
87
+ export function evaluateFormula(formula: string, context: EvaluatorContext): CellValue {
88
+ try {
89
+ // Handle string literals - remove surrounding quotes
90
+ // But NOT string concatenations (which contain & operators)
91
+ const trimmed = formula.trim();
92
+ if (
93
+ ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) &&
94
+ !trimmed.includes("&") // Exclude string concatenations
95
+ ) {
96
+ const stringValue = trimmed.slice(1, -1); // Remove first and last character (quotes)
97
+
98
+ // Auto-parse date strings to serial numbers for compatibility with date arithmetic
99
+ // This allows formulas like =HLOOKUP("6/1/2024", ...) to work with parsed date cells
100
+ const dateSerial = parseDate(stringValue);
101
+ if (dateSerial !== null) {
102
+ return dateSerial;
103
+ }
104
+
105
+ return stringValue;
106
+ }
107
+
108
+ // Check if it's a SIMPLE function call (not a complex expression)
109
+ // We need to ensure the formula is JUST a function, not "FUNC(...) + something"
110
+ const funcMatch = formula.match(/^([A-Z]+)\((.*)\)$/i);
111
+ if (funcMatch) {
112
+ const [, funcName, argsStr] = funcMatch;
113
+
114
+ // Check that the closing paren is actually the end of the function
115
+ // by counting parentheses in argsStr
116
+ let parenDepth = 0;
117
+ let isValidFunction = true;
118
+ for (const char of argsStr) {
119
+ if (char === "(") parenDepth++;
120
+ else if (char === ")") {
121
+ parenDepth--;
122
+ if (parenDepth < 0) {
123
+ // More closing parens than opening - this means we matched too much
124
+ isValidFunction = false;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+
130
+ // Normalize function name to uppercase for registry lookup
131
+ const normalizedFuncName = funcName.toUpperCase();
132
+ const func = functionRegistry.get(normalizedFuncName);
133
+
134
+ if (func && isValidFunction) {
135
+ const args = parseFunctionArgs(argsStr);
136
+
137
+ // Validate argument count
138
+ if (func.minArgs !== undefined && args.length < func.minArgs) {
139
+ throw new Error(`${normalizedFuncName} requires at least ${func.minArgs} argument${func.minArgs !== 1 ? "s" : ""}`);
140
+ }
141
+ if (func.maxArgs !== undefined && args.length > func.maxArgs) {
142
+ throw new Error(`${normalizedFuncName} accepts at most ${func.maxArgs} argument${func.maxArgs !== 1 ? "s" : ""}`);
143
+ }
144
+
145
+ // Execute function with context
146
+ return func.handler(args, {
147
+ getCellValue: context.getCellValue,
148
+ getRangeValues: context.getRangeValues,
149
+ getRangeValuesRaw: context.getRangeValuesRaw,
150
+ evaluateFormula: context.evaluateFormula,
151
+ });
152
+ }
153
+ }
154
+
155
+ // Handle simple arithmetic expressions with cell references
156
+ // First, replace any function calls within the expression
157
+ let expr = formula;
158
+
159
+ // Find and evaluate function calls (e.g., TODAY(), SUM(A1:A10), LOWER(A1), etc.)
160
+ // Use a simpler approach: find function names followed by parentheses
161
+ // and manually parse the matching closing parenthesis
162
+ let searchIndex = 0;
163
+ const maxIterations = 100; // Prevent infinite loops
164
+ let iterations = 0;
165
+
166
+ while (searchIndex < expr.length && iterations < maxIterations) {
167
+ iterations++;
168
+ const funcNameMatch = expr.substring(searchIndex).match(/^([A-Z]+)\(/i);
169
+ if (!funcNameMatch) {
170
+ // No more functions found, move to next character
171
+ searchIndex++;
172
+ if (searchIndex >= expr.length) break;
173
+ continue;
174
+ }
175
+
176
+ const funcStartIndex = searchIndex;
177
+ const funcName = funcNameMatch[1];
178
+ const argsStartIndex = searchIndex + funcName.length + 1;
179
+
180
+ // Find matching closing parenthesis
181
+ let depth = 1;
182
+ let argsEndIndex = argsStartIndex;
183
+ let inString = false;
184
+ let stringChar = "";
185
+
186
+ while (argsEndIndex < expr.length && depth > 0) {
187
+ const char = expr[argsEndIndex];
188
+ const prevChar = argsEndIndex > 0 ? expr[argsEndIndex - 1] : "";
189
+
190
+ // Track string boundaries
191
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
192
+ if (!inString) {
193
+ inString = true;
194
+ stringChar = char;
195
+ } else if (char === stringChar) {
196
+ inString = false;
197
+ stringChar = "";
198
+ }
199
+ }
200
+
201
+ // Only count parens outside of strings
202
+ if (!inString) {
203
+ if (char === "(") depth++;
204
+ else if (char === ")") depth--;
205
+ }
206
+ argsEndIndex++;
207
+ }
208
+
209
+ if (depth === 0) {
210
+ const fullMatch = expr.substring(funcStartIndex, argsEndIndex);
211
+ const result = context.evaluateFormula(fullMatch);
212
+ // For string results, wrap in quotes; for numbers, wrap in parentheses
213
+ const replacement = typeof result === "string" ? `"${result}"` : `(${result})`;
214
+ expr = expr.substring(0, funcStartIndex) + replacement + expr.substring(argsEndIndex);
215
+ // Continue from after the replacement
216
+ searchIndex = funcStartIndex + replacement.length;
217
+ } else {
218
+ searchIndex++;
219
+ }
220
+ }
221
+
222
+ // Then replace cell references with their values
223
+ // Match cell references manually to avoid complex regex
224
+ const cellRefs: string[] = [];
225
+ let i = 0;
226
+ while (i < expr.length) {
227
+ // Check for cross-sheet reference (quoted or unquoted)
228
+ let ref = "";
229
+ if (expr[i] === "'") {
230
+ // Quoted sheet name
231
+ const endQuote = expr.indexOf("'", i + 1);
232
+ if (endQuote !== -1 && expr[endQuote + 1] === "!") {
233
+ const cellPart = expr.substring(endQuote + 2).match(/^(\$?[A-Z]+\$?\d+)/);
234
+ if (cellPart) {
235
+ ref = expr.substring(i, endQuote + 2 + cellPart[0].length);
236
+ cellRefs.push(ref);
237
+ i += ref.length;
238
+ continue;
239
+ }
240
+ }
241
+ } else {
242
+ // Unquoted sheet name or simple cell ref
243
+ const sheetMatch = expr.substring(i).match(/^([A-Z][A-Z0-9]*)!/i);
244
+ if (sheetMatch) {
245
+ const cellPart = expr.substring(i + sheetMatch[0].length).match(/^(\$?[A-Z]+\$?\d+)/);
246
+ if (cellPart) {
247
+ ref = sheetMatch[0] + cellPart[0];
248
+ cellRefs.push(ref);
249
+ i += ref.length;
250
+ continue;
251
+ }
252
+ }
253
+ // Simple cell reference
254
+ const cellMatch = expr.substring(i).match(/^(\$?[A-Z]+\$?\d+)/);
255
+ if (cellMatch) {
256
+ ref = cellMatch[0];
257
+ cellRefs.push(ref);
258
+ i += ref.length;
259
+ continue;
260
+ }
261
+ }
262
+ i++;
263
+ }
264
+
265
+ if (cellRefs.length > 0) {
266
+ for (const ref of cellRefs) {
267
+ const value = context.getCellValue(ref);
268
+ // Escape special regex characters
269
+ const escapedRef = ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
270
+ // Wrap string values in quotes for proper evaluation
271
+ // Handle null/undefined values by treating them as 0
272
+ let replacement: string;
273
+ if (value === null || value === undefined) {
274
+ replacement = "0";
275
+ } else if (typeof value === "string") {
276
+ replacement = `"${value}"`;
277
+ } else {
278
+ replacement = value.toString();
279
+ }
280
+ expr = expr.replace(new RegExp(escapedRef, "g"), replacement);
281
+ }
282
+ }
283
+
284
+ // Parse date strings in arithmetic expressions (e.g., "06/01/2025" → serial number)
285
+ // This allows formulas like =B3-"06/01/2025" to work correctly
286
+ expr = expr.replace(/"([^"]+)"/g, (match, dateStr) => {
287
+ const dateSerial = parseDate(dateStr);
288
+ if (dateSerial !== null) {
289
+ return dateSerial.toString();
290
+ }
291
+ return match; // Keep original if not a date
292
+ });
293
+
294
+ // Replace ^ with ** for exponentiation
295
+ expr = expr.replace(/\^/g, "**");
296
+
297
+ // Check if this is a string concatenation expression (contains & and quoted strings)
298
+ const hasStringConcat = expr.includes("&");
299
+ const hasQuotedStrings = /["']/.test(expr);
300
+
301
+ // If it contains string concatenation, handle it specially
302
+ if (hasStringConcat && hasQuotedStrings) {
303
+ try {
304
+ // Convert & to + for JavaScript string concatenation
305
+ // We need to be careful to only replace & that are not inside strings
306
+ let inString = false;
307
+ let stringChar = "";
308
+ let result = "";
309
+
310
+ for (let index = 0; index < expr.length; index++) {
311
+ const char = expr[index];
312
+ const prevChar = index > 0 ? expr[index - 1] : "";
313
+
314
+ // Handle string boundaries
315
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
316
+ if (!inString) {
317
+ inString = true;
318
+ stringChar = char;
319
+ } else if (char === stringChar) {
320
+ inString = false;
321
+ stringChar = "";
322
+ }
323
+ }
324
+
325
+ // Replace & with + when not in a string
326
+ if (char === "&" && !inString) {
327
+ result += "+";
328
+ } else {
329
+ result += char;
330
+ }
331
+ }
332
+
333
+ // Validate the expression contains only safe characters
334
+ // Allow: numbers, letters, strings (with quotes), operators, parentheses, whitespace, @, ., comma
335
+ if (/^[a-zA-Z0-9+\-*/(). "'@,]+$/.test(result)) {
336
+ // eslint-disable -- sonarjs/code-eval
337
+ const evalResult = new Function(`return (${result})`)();
338
+ return evalResult;
339
+ }
340
+ } catch (error) {
341
+ console.error(`Failed to evaluate string concatenation: ${expr}`, error);
342
+ return formula;
343
+ }
344
+ }
345
+
346
+ // Safely evaluate comparison expressions (e.g., 5=6, (5)>(6))
347
+ // Allow numbers, comparison operators (=, !=, <, >, <=, >=), parentheses, whitespace
348
+ if (/^[\d+\-*/(). <>!=]+$/.test(expr)) {
349
+ try {
350
+ // Replace = with == for JavaScript comparison (but not <= or >=)
351
+ const jsExpr = expr.replace(/([^<>!])=([^=])/g, "$1==$2");
352
+
353
+ // Use Function constructor which is safer than eval
354
+ // eslint-disable -- sonarjs/code-eval
355
+ const result = new Function(`return (${jsExpr})`)();
356
+ return result;
357
+ } catch {
358
+ return formula;
359
+ }
360
+ }
361
+
362
+ // Safely evaluate arithmetic expressions using Function constructor instead of eval
363
+ // Allow numbers, operators, parentheses, whitespace, and decimal points
364
+ if (/^[\d+\-*/(). ]+$/.test(expr)) {
365
+ try {
366
+ // Use Function constructor which is safer than eval because:
367
+ // 1. The expression is strictly validated (only numbers and math operators)
368
+ // 2. No access to local scope variables
369
+ // 3. No this binding issues
370
+ // This is safe because we validate the expression first
371
+ // eslint-disable -- sonarjs/code-eval
372
+ const result = new Function(`return (${expr})`)();
373
+ return result;
374
+ } catch {
375
+ return formula;
376
+ }
377
+ }
378
+
379
+ // If the final expression is a quoted string literal, unwrap it
380
+ const trimmedExpr = expr.trim();
381
+ if ((trimmedExpr.startsWith('"') && trimmedExpr.endsWith('"')) || (trimmedExpr.startsWith("'") && trimmedExpr.endsWith("'"))) {
382
+ return trimmedExpr.slice(1, -1); // Remove quotes
383
+ }
384
+
385
+ return expr; // Return processed expression (with cell refs replaced, etc.)
386
+ } catch (error) {
387
+ console.error(`Failed to evaluate formula: ${formula}`, error);
388
+ return formula;
389
+ }
390
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Number Formatting Utilities
3
+ *
4
+ * Handles Excel-style format codes for currency, percentages, decimals, dates, etc.
5
+ */
6
+
7
+ import { serialToDate, MONTH_NAMES_SHORT, MONTH_NAMES_FULL, DAY_NAMES_SHORT, DAY_NAMES_FULL } from "./date-utils";
8
+
9
+ /**
10
+ * Check if a format code is for dates
11
+ */
12
+ function isDateFormat(format: string): boolean {
13
+ // Date formats contain date/time tokens: Y, M, D, h, m, s
14
+ // But not percentage (which also has 'm' in format like #,##0)
15
+ // Look for specific date patterns
16
+ return /[YMD]|MMM|DD|YYYY|h:mm|AM\/PM/i.test(format);
17
+ }
18
+
19
+ /**
20
+ * Format a number as a date according to Excel format code
21
+ *
22
+ * Supported formats:
23
+ * - MM/DD/YYYY, M/D/YYYY
24
+ * - DD/MM/YYYY, D/M/YYYY
25
+ * - YYYY-MM-DD
26
+ * - DD-MMM-YYYY, D-MMM-YYYY
27
+ * - MMM D, YYYY, MMMM D, YYYY
28
+ * - h:mm AM/PM, HH:mm:ss
29
+ *
30
+ * @param serial - Excel serial number
31
+ * @param format - Date format code
32
+ * @returns Formatted date string
33
+ */
34
+ function formatDate(serial: number, format: string): string {
35
+ const date = serialToDate(serial);
36
+
37
+ const year = date.getUTCFullYear();
38
+ const month = date.getUTCMonth(); // 0-11
39
+ const day = date.getUTCDate();
40
+ const hours = date.getUTCHours();
41
+ const minutes = date.getUTCMinutes();
42
+ const seconds = date.getUTCSeconds();
43
+ const dayOfWeek = date.getUTCDay(); // 0-6
44
+
45
+ let result = format;
46
+
47
+ // Replace year tokens
48
+ result = result.replace(/YYYY/g, year.toString());
49
+ result = result.replace(/YY/g, (year % 100).toString().padStart(2, "0"));
50
+
51
+ // Replace month tokens (order matters - do longer patterns first)
52
+ result = result.replace(/MMMM/g, MONTH_NAMES_FULL[month] || MONTH_NAMES_FULL[0]);
53
+ result = result.replace(/MMM/g, MONTH_NAMES_SHORT[month] || MONTH_NAMES_SHORT[0]);
54
+ result = result.replace(/MM/g, (month + 1).toString().padStart(2, "0"));
55
+ result = result.replace(/M/g, (month + 1).toString());
56
+
57
+ // Replace day tokens
58
+ result = result.replace(/DD/g, day.toString().padStart(2, "0"));
59
+ result = result.replace(/D/g, day.toString());
60
+
61
+ // Replace day of week tokens
62
+ result = result.replace(/dddd/g, DAY_NAMES_FULL[dayOfWeek] || DAY_NAMES_FULL[0]);
63
+ result = result.replace(/ddd/g, DAY_NAMES_SHORT[dayOfWeek] || DAY_NAMES_SHORT[0]);
64
+
65
+ // Replace time tokens
66
+ // Handle 12-hour format with AM/PM
67
+ if (result.includes("AM/PM") || result.includes("am/pm")) {
68
+ const isPM = hours >= 12;
69
+ const hours12 = hours % 12 || 12; // 0 becomes 12
70
+
71
+ result = result.replace(/h/g, hours12.toString());
72
+ result = result.replace(/AM\/PM/g, isPM ? "PM" : "AM");
73
+ result = result.replace(/am\/pm/g, isPM ? "pm" : "am");
74
+ } else {
75
+ // 24-hour format
76
+ result = result.replace(/HH/g, hours.toString().padStart(2, "0"));
77
+ result = result.replace(/H/g, hours.toString());
78
+ result = result.replace(/h/g, hours.toString());
79
+ }
80
+
81
+ result = result.replace(/mm/g, minutes.toString().padStart(2, "0"));
82
+ result = result.replace(/ss/g, seconds.toString().padStart(2, "0"));
83
+
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Format a number according to Excel format code
89
+ *
90
+ * Supported formats:
91
+ * - Currency: $#,##0.00, $#,##0
92
+ * - Percentage: 0.00%, 0.0%
93
+ * - Integer with commas: #,##0
94
+ * - Decimal: 0.00, 0.000
95
+ * - Dates: MM/DD/YYYY, DD-MMM-YYYY, etc.
96
+ *
97
+ * @param value - The numeric value to format
98
+ * @param format - The Excel format code
99
+ * @returns Formatted string representation
100
+ */
101
+ export function formatNumber(value: number, format: string): string {
102
+ if (!format) return value.toString();
103
+
104
+ try {
105
+ // Check if it's a date format
106
+ if (isDateFormat(format)) {
107
+ return formatDate(value, format);
108
+ }
109
+
110
+ // Handle currency formats
111
+ if (format.includes("$")) {
112
+ const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
113
+ const hasComma = format.includes(",");
114
+
115
+ let formatted = Math.abs(value).toFixed(decimals >= 0 ? decimals : 0);
116
+ if (hasComma) {
117
+ // Add thousand separators without regex to avoid performance issues
118
+ const parts = formatted.split(".");
119
+ const integerPart = parts[0];
120
+ let result = "";
121
+ for (let i = integerPart.length - 1, count = 0; i >= 0; i--, count++) {
122
+ if (count > 0 && count % 3 === 0) {
123
+ result = "," + result;
124
+ }
125
+ result = integerPart[i] + result;
126
+ }
127
+ parts[0] = result;
128
+ formatted = parts.join(".");
129
+ }
130
+ formatted = "$" + formatted;
131
+ if (value < 0) formatted = "-" + formatted;
132
+ return formatted;
133
+ }
134
+
135
+ // Handle percentage
136
+ if (format.includes("%")) {
137
+ const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
138
+ return (value * 100).toFixed(decimals >= 0 ? decimals : 2) + "%";
139
+ }
140
+
141
+ // Handle comma separator
142
+ if (format.includes(",")) {
143
+ const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
144
+ let formatted = Math.abs(value).toFixed(decimals >= 0 ? decimals : 0);
145
+ // Add thousand separators without regex to avoid performance issues
146
+ const parts = formatted.split(".");
147
+ const integerPart = parts[0];
148
+ let result = "";
149
+ for (let i = integerPart.length - 1, count = 0; i >= 0; i--, count++) {
150
+ if (count > 0 && count % 3 === 0) {
151
+ result = "," + result;
152
+ }
153
+ result = integerPart[i] + result;
154
+ }
155
+ parts[0] = result;
156
+ formatted = parts.join(".");
157
+ if (value < 0) formatted = "-" + formatted;
158
+ return formatted;
159
+ }
160
+
161
+ // Handle decimal places
162
+ const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
163
+ if (decimals >= 0) {
164
+ return value.toFixed(decimals);
165
+ }
166
+
167
+ return value.toString();
168
+ } catch (error) {
169
+ console.error("Format error:", error);
170
+ return value.toString();
171
+ }
172
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Extract the set of cells that a formula references.
3
+ *
4
+ * Extracted from `src/plugins/spreadsheet/View.vue` (was the body of
5
+ * `extractCellReferences`, cognitive complexity 32). The original
6
+ * function combined regex scanning, range expansion, single-cell
7
+ * parsing, and deduplication all in one body; splitting each concern
8
+ * into a named helper brings the top-level function well under the
9
+ * sonarjs/cognitive-complexity threshold of 15 and makes the pure
10
+ * logic unit-testable in isolation (see
11
+ * `test/plugins/spreadsheet/engine/test_formulaRefs.ts`).
12
+ *
13
+ * Tracks #175. No behavioural change — the wrapper in View.vue
14
+ * still returns exactly the same `{ row, col }` list as before.
15
+ */
16
+
17
+ import { columnToIndex } from "./parser.js";
18
+
19
+ export interface CellCoord {
20
+ row: number;
21
+ col: number;
22
+ }
23
+
24
+ // `A1:B10`, `$A$1:$B$10`, `Sheet` refs are out of scope here — the
25
+ // caller only passes the formula body, and cross-sheet ranges never
26
+ // reached the original regex anyway. Keeping the patterns identical
27
+ // to the pre-refactor code preserves behaviour exactly.
28
+ const RANGE_REGEX = /\$?[A-Z]+\$?\d+:\$?[A-Z]+\$?\d+/g;
29
+ const CELL_REGEX = /\$?[A-Z]+\$?\d+/g;
30
+
31
+ // Excel formulas start with `=`. Strip it for uniform handling.
32
+ // Keeps any inner `=` intact (Excel does not allow them but the
33
+ // caller may pass partial text during live editing).
34
+ export function stripFormulaPrefix(formula: string): string {
35
+ return formula.startsWith("=") ? formula.slice(1) : formula;
36
+ }
37
+
38
+ // Expand a single range token (`A1:B3`, `$A$1:$C$5`) into every
39
+ // coordinate the range covers. Returns an empty array for malformed
40
+ // input so callers never have to handle exceptions; the worst case
41
+ // is "we silently ignored a weird-looking substring," which matches
42
+ // the original inline behaviour.
43
+ export function expandRange(rangeStr: string): CellCoord[] {
44
+ const cleanRange = rangeStr.replace(/\$/g, "");
45
+ const match = cleanRange.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
46
+ if (!match) return [];
47
+ const startCol = columnToIndex(match[1]);
48
+ const startRow = parseInt(match[2], 10) - 1;
49
+ const endCol = columnToIndex(match[3]);
50
+ const endRow = parseInt(match[4], 10) - 1;
51
+ const cells: CellCoord[] = [];
52
+ for (let row = startRow; row <= endRow; row++) {
53
+ for (let col = startCol; col <= endCol; col++) {
54
+ cells.push({ row, col });
55
+ }
56
+ }
57
+ return cells;
58
+ }
59
+
60
+ // Parse a single cell ref (`A1`, `$A$1`, `AA100`) into a coord.
61
+ // Returns null for malformed input rather than throwing — keeps the
62
+ // caller's loop flat (the engine-layer `parseCellRef` throws, which
63
+ // is fine for the evaluator but wrong for a best-effort scanner).
64
+ export function parseSingleCellRef(refStr: string): CellCoord | null {
65
+ const cleanRef = refStr.replace(/\$/g, "");
66
+ const match = cleanRef.match(/^([A-Z]+)(\d+)$/);
67
+ if (!match) return null;
68
+ return {
69
+ col: columnToIndex(match[1]),
70
+ row: parseInt(match[2], 10) - 1,
71
+ };
72
+ }
73
+
74
+ // Top-level: scan the formula, expand any ranges, then pick up
75
+ // remaining single-cell refs, deduplicating as we go. Kept short
76
+ // (~15 lines) so the cognitive-complexity signal lands on the
77
+ // helpers if anything grows here.
78
+ export function extractCellReferences(formula: string): CellCoord[] {
79
+ const clean = stripFormulaPrefix(formula);
80
+ const refs: CellCoord[] = [];
81
+ const seen = new Set<string>();
82
+ const addUnique = (coord: CellCoord): void => {
83
+ const key = `${coord.row},${coord.col}`;
84
+ if (seen.has(key)) return;
85
+ seen.add(key);
86
+ refs.push(coord);
87
+ };
88
+
89
+ for (const range of clean.match(RANGE_REGEX) ?? []) {
90
+ for (const coord of expandRange(range)) addUnique(coord);
91
+ }
92
+ // Strip matched ranges so the cell-regex doesn't re-emit their
93
+ // endpoints as standalone refs (mirrors the original's second
94
+ // `.replace(rangeRegex, "")` pass).
95
+ const withoutRanges = clean.replace(RANGE_REGEX, "");
96
+ for (const cellStr of withoutRanges.match(CELL_REGEX) ?? []) {
97
+ const coord = parseSingleCellRef(cellStr);
98
+ if (coord) addUnique(coord);
99
+ }
100
+ return refs;
101
+ }