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.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- 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
|
+
}
|