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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { functionRegistry, toString, type FunctionHandler } from "../registry";
|
|
6
|
+
|
|
7
|
+
const concatenateHandler: FunctionHandler = (args, context) => {
|
|
8
|
+
if (args.length === 0) throw new Error("CONCATENATE requires at least 1 argument");
|
|
9
|
+
|
|
10
|
+
return args
|
|
11
|
+
.map((arg) => {
|
|
12
|
+
const value = context.evaluateFormula(arg.trim());
|
|
13
|
+
return toString(value);
|
|
14
|
+
})
|
|
15
|
+
.join("");
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const concatHandler: FunctionHandler = concatenateHandler; // Alias
|
|
19
|
+
|
|
20
|
+
const leftHandler: FunctionHandler = (args, context) => {
|
|
21
|
+
if (args.length < 1 || args.length > 2) {
|
|
22
|
+
throw new Error("LEFT requires 1 or 2 arguments");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
26
|
+
const numChars = args.length === 2 ? Number(context.evaluateFormula(args[1])) : 1;
|
|
27
|
+
|
|
28
|
+
return text.substring(0, numChars);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const rightHandler: FunctionHandler = (args, context) => {
|
|
32
|
+
if (args.length < 1 || args.length > 2) {
|
|
33
|
+
throw new Error("RIGHT requires 1 or 2 arguments");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
37
|
+
const numChars = args.length === 2 ? Number(context.evaluateFormula(args[1])) : 1;
|
|
38
|
+
|
|
39
|
+
return text.substring(text.length - numChars);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const midHandler: FunctionHandler = (args, context) => {
|
|
43
|
+
if (args.length !== 3) throw new Error("MID requires 3 arguments");
|
|
44
|
+
|
|
45
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
46
|
+
const start = Number(context.evaluateFormula(args[1])) - 1; // 1-indexed to 0-indexed
|
|
47
|
+
const numChars = Number(context.evaluateFormula(args[2]));
|
|
48
|
+
|
|
49
|
+
return text.substring(start, start + numChars);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const lenHandler: FunctionHandler = (args, context) => {
|
|
53
|
+
if (args.length !== 1) throw new Error("LEN requires 1 argument");
|
|
54
|
+
|
|
55
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
56
|
+
return text.length;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const upperHandler: FunctionHandler = (args, context) => {
|
|
60
|
+
if (args.length !== 1) throw new Error("UPPER requires 1 argument");
|
|
61
|
+
|
|
62
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
63
|
+
return text.toUpperCase();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const lowerHandler: FunctionHandler = (args, context) => {
|
|
67
|
+
if (args.length !== 1) throw new Error("LOWER requires 1 argument");
|
|
68
|
+
|
|
69
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
70
|
+
return text.toLowerCase();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const properHandler: FunctionHandler = (args, context) => {
|
|
74
|
+
if (args.length !== 1) throw new Error("PROPER requires 1 argument");
|
|
75
|
+
|
|
76
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
77
|
+
return text
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.split(" ")
|
|
80
|
+
.map((word) => (word.length > 0 ? word[0].toUpperCase() + word.slice(1) : ""))
|
|
81
|
+
.join(" ");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const trimHandler: FunctionHandler = (args, context) => {
|
|
85
|
+
if (args.length !== 1) throw new Error("TRIM requires 1 argument");
|
|
86
|
+
|
|
87
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
88
|
+
// Trim leading/trailing spaces and replace multiple spaces with single space
|
|
89
|
+
return text.trim().replace(/\s+/g, " ");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const substituteHandler: FunctionHandler = (args, context) => {
|
|
93
|
+
if (args.length < 3 || args.length > 4) {
|
|
94
|
+
throw new Error("SUBSTITUTE requires 3 or 4 arguments");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
98
|
+
const oldText = toString(context.evaluateFormula(args[1]));
|
|
99
|
+
const newText = toString(context.evaluateFormula(args[2]));
|
|
100
|
+
|
|
101
|
+
if (args.length === 4) {
|
|
102
|
+
// Replace specific instance
|
|
103
|
+
const instance = Number(context.evaluateFormula(args[3]));
|
|
104
|
+
let count = 0;
|
|
105
|
+
let index = 0;
|
|
106
|
+
|
|
107
|
+
while (index < text.length) {
|
|
108
|
+
const pos = text.indexOf(oldText, index);
|
|
109
|
+
if (pos === -1) break;
|
|
110
|
+
|
|
111
|
+
count++;
|
|
112
|
+
if (count === instance) {
|
|
113
|
+
return text.substring(0, pos) + newText + text.substring(pos + oldText.length);
|
|
114
|
+
}
|
|
115
|
+
index = pos + 1;
|
|
116
|
+
}
|
|
117
|
+
return text; // Instance not found
|
|
118
|
+
} else {
|
|
119
|
+
// Replace all instances
|
|
120
|
+
return text.split(oldText).join(newText);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const replaceHandler: FunctionHandler = (args, context) => {
|
|
125
|
+
if (args.length !== 4) throw new Error("REPLACE requires 4 arguments");
|
|
126
|
+
|
|
127
|
+
const oldText = toString(context.evaluateFormula(args[0]));
|
|
128
|
+
const startPos = Number(context.evaluateFormula(args[1])) - 1; // 1-indexed to 0-indexed
|
|
129
|
+
const numChars = Number(context.evaluateFormula(args[2]));
|
|
130
|
+
const newText = toString(context.evaluateFormula(args[3]));
|
|
131
|
+
|
|
132
|
+
return oldText.substring(0, startPos) + newText + oldText.substring(startPos + numChars);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const findHandler: FunctionHandler = (args, context) => {
|
|
136
|
+
if (args.length < 2 || args.length > 3) {
|
|
137
|
+
throw new Error("FIND requires 2 or 3 arguments");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const findText = toString(context.evaluateFormula(args[0]));
|
|
141
|
+
const withinText = toString(context.evaluateFormula(args[1]));
|
|
142
|
+
const startPos = args.length === 3 ? Number(context.evaluateFormula(args[2])) - 1 : 0;
|
|
143
|
+
|
|
144
|
+
const index = withinText.indexOf(findText, startPos);
|
|
145
|
+
return index === -1 ? "#VALUE!" : index + 1; // Return 1-indexed position
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const searchHandler: FunctionHandler = (args, context) => {
|
|
149
|
+
if (args.length < 2 || args.length > 3) {
|
|
150
|
+
throw new Error("SEARCH requires 2 or 3 arguments");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const findText = toString(context.evaluateFormula(args[0]));
|
|
154
|
+
const withinText = toString(context.evaluateFormula(args[1]));
|
|
155
|
+
const startPos = args.length === 3 ? Number(context.evaluateFormula(args[2])) - 1 : 0;
|
|
156
|
+
|
|
157
|
+
// SEARCH is case-insensitive
|
|
158
|
+
const lowerFind = findText.toLowerCase();
|
|
159
|
+
const lowerWithin = withinText.toLowerCase();
|
|
160
|
+
|
|
161
|
+
const index = lowerWithin.indexOf(lowerFind, startPos);
|
|
162
|
+
return index === -1 ? "#VALUE!" : index + 1; // Return 1-indexed position
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const textHandler: FunctionHandler = (args, context) => {
|
|
166
|
+
if (args.length !== 2) throw new Error("TEXT requires 2 arguments");
|
|
167
|
+
|
|
168
|
+
const value = context.evaluateFormula(args[0]);
|
|
169
|
+
const format = toString(context.evaluateFormula(args[1])).replace(
|
|
170
|
+
// eslint-disable -- sonarjs/anchor-precedence
|
|
171
|
+
/^["']|["']$/g,
|
|
172
|
+
"",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Simple format code handling
|
|
176
|
+
if (typeof value === "number") {
|
|
177
|
+
// Handle common format codes
|
|
178
|
+
if (format.includes("$")) {
|
|
179
|
+
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
180
|
+
return "$" + value.toFixed(decimals >= 0 ? decimals : 2);
|
|
181
|
+
}
|
|
182
|
+
if (format.includes("%")) {
|
|
183
|
+
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
184
|
+
return (value * 100).toFixed(decimals >= 0 ? decimals : 2) + "%";
|
|
185
|
+
}
|
|
186
|
+
if (format.includes("0")) {
|
|
187
|
+
const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
|
|
188
|
+
return value.toFixed(decimals >= 0 ? decimals : 0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return toString(value);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const valueHandler: FunctionHandler = (args, context) => {
|
|
196
|
+
if (args.length !== 1) throw new Error("VALUE requires 1 argument");
|
|
197
|
+
|
|
198
|
+
const text = toString(context.evaluateFormula(args[0]));
|
|
199
|
+
|
|
200
|
+
// Remove currency symbols and commas
|
|
201
|
+
const cleaned = text.replace(/[$,]/g, "").trim();
|
|
202
|
+
|
|
203
|
+
// Handle percentages
|
|
204
|
+
if (cleaned.includes("%")) {
|
|
205
|
+
const num = parseFloat(cleaned.replace("%", ""));
|
|
206
|
+
return isNaN(num) ? "#VALUE!" : num / 100;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const num = parseFloat(cleaned);
|
|
210
|
+
return isNaN(num) ? "#VALUE!" : num;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const exactHandler: FunctionHandler = (args, context) => {
|
|
214
|
+
if (args.length !== 2) throw new Error("EXACT requires 2 arguments");
|
|
215
|
+
|
|
216
|
+
const text1 = toString(context.evaluateFormula(args[0]));
|
|
217
|
+
const text2 = toString(context.evaluateFormula(args[1]));
|
|
218
|
+
|
|
219
|
+
return text1 === text2;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Register all text functions
|
|
223
|
+
functionRegistry.register({
|
|
224
|
+
name: "CONCATENATE",
|
|
225
|
+
handler: concatenateHandler,
|
|
226
|
+
minArgs: 1,
|
|
227
|
+
description: "Joins several text strings into one string",
|
|
228
|
+
examples: ['CONCATENATE("Hello", " ", "World")', "CONCATENATE(A1, B1)"],
|
|
229
|
+
category: "Text",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
functionRegistry.register({
|
|
233
|
+
name: "CONCAT",
|
|
234
|
+
handler: concatHandler,
|
|
235
|
+
minArgs: 1,
|
|
236
|
+
description: "Joins several text strings into one string (same as CONCATENATE)",
|
|
237
|
+
examples: ['CONCAT("Hello", " ", "World")', "CONCAT(A1, B1)"],
|
|
238
|
+
category: "Text",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
functionRegistry.register({
|
|
242
|
+
name: "LEFT",
|
|
243
|
+
handler: leftHandler,
|
|
244
|
+
minArgs: 1,
|
|
245
|
+
maxArgs: 2,
|
|
246
|
+
description: "Returns the leftmost characters from a text string",
|
|
247
|
+
examples: ['LEFT("Hello", 2)', "LEFT(A1, 3)"],
|
|
248
|
+
category: "Text",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
functionRegistry.register({
|
|
252
|
+
name: "RIGHT",
|
|
253
|
+
handler: rightHandler,
|
|
254
|
+
minArgs: 1,
|
|
255
|
+
maxArgs: 2,
|
|
256
|
+
description: "Returns the rightmost characters from a text string",
|
|
257
|
+
examples: ['RIGHT("Hello", 2)', "RIGHT(A1, 3)"],
|
|
258
|
+
category: "Text",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
functionRegistry.register({
|
|
262
|
+
name: "MID",
|
|
263
|
+
handler: midHandler,
|
|
264
|
+
minArgs: 3,
|
|
265
|
+
maxArgs: 3,
|
|
266
|
+
description: "Returns characters from the middle of a text string",
|
|
267
|
+
examples: ['MID("Hello", 2, 3)', "MID(A1, 1, 5)"],
|
|
268
|
+
category: "Text",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
functionRegistry.register({
|
|
272
|
+
name: "LEN",
|
|
273
|
+
handler: lenHandler,
|
|
274
|
+
minArgs: 1,
|
|
275
|
+
maxArgs: 1,
|
|
276
|
+
description: "Returns the number of characters in a text string",
|
|
277
|
+
examples: ['LEN("Hello")', "LEN(A1)"],
|
|
278
|
+
category: "Text",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
functionRegistry.register({
|
|
282
|
+
name: "UPPER",
|
|
283
|
+
handler: upperHandler,
|
|
284
|
+
minArgs: 1,
|
|
285
|
+
maxArgs: 1,
|
|
286
|
+
description: "Converts text to uppercase",
|
|
287
|
+
examples: ['UPPER("hello")', "UPPER(A1)"],
|
|
288
|
+
category: "Text",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
functionRegistry.register({
|
|
292
|
+
name: "LOWER",
|
|
293
|
+
handler: lowerHandler,
|
|
294
|
+
minArgs: 1,
|
|
295
|
+
maxArgs: 1,
|
|
296
|
+
description: "Converts text to lowercase",
|
|
297
|
+
examples: ['LOWER("HELLO")', "LOWER(A1)"],
|
|
298
|
+
category: "Text",
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
functionRegistry.register({
|
|
302
|
+
name: "PROPER",
|
|
303
|
+
handler: properHandler,
|
|
304
|
+
minArgs: 1,
|
|
305
|
+
maxArgs: 1,
|
|
306
|
+
description: "Capitalizes the first letter of each word",
|
|
307
|
+
examples: ['PROPER("hello world")', "PROPER(A1)"],
|
|
308
|
+
category: "Text",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
functionRegistry.register({
|
|
312
|
+
name: "TRIM",
|
|
313
|
+
handler: trimHandler,
|
|
314
|
+
minArgs: 1,
|
|
315
|
+
maxArgs: 1,
|
|
316
|
+
description: "Removes extra spaces from text",
|
|
317
|
+
examples: ['TRIM(" hello world ")', "TRIM(A1)"],
|
|
318
|
+
category: "Text",
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
functionRegistry.register({
|
|
322
|
+
name: "SUBSTITUTE",
|
|
323
|
+
handler: substituteHandler,
|
|
324
|
+
minArgs: 3,
|
|
325
|
+
maxArgs: 4,
|
|
326
|
+
description: "Replaces old text with new text in a string",
|
|
327
|
+
examples: ['SUBSTITUTE("Hello World", "World", "Earth")', 'SUBSTITUTE(A1, "old", "new", 1)'],
|
|
328
|
+
category: "Text",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
functionRegistry.register({
|
|
332
|
+
name: "REPLACE",
|
|
333
|
+
handler: replaceHandler,
|
|
334
|
+
minArgs: 4,
|
|
335
|
+
maxArgs: 4,
|
|
336
|
+
description: "Replaces part of a text string with a different text string",
|
|
337
|
+
examples: ['REPLACE("Hello World", 7, 5, "Earth")', 'REPLACE(A1, 1, 3, "New")'],
|
|
338
|
+
category: "Text",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
functionRegistry.register({
|
|
342
|
+
name: "FIND",
|
|
343
|
+
handler: findHandler,
|
|
344
|
+
minArgs: 2,
|
|
345
|
+
maxArgs: 3,
|
|
346
|
+
description: "Finds one text string within another (case-sensitive)",
|
|
347
|
+
examples: ['FIND("o", "Hello")', 'FIND("World", A1)'],
|
|
348
|
+
category: "Text",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
functionRegistry.register({
|
|
352
|
+
name: "SEARCH",
|
|
353
|
+
handler: searchHandler,
|
|
354
|
+
minArgs: 2,
|
|
355
|
+
maxArgs: 3,
|
|
356
|
+
description: "Finds one text string within another (case-insensitive)",
|
|
357
|
+
examples: ['SEARCH("O", "Hello")', 'SEARCH("world", A1)'],
|
|
358
|
+
category: "Text",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
functionRegistry.register({
|
|
362
|
+
name: "TEXT",
|
|
363
|
+
handler: textHandler,
|
|
364
|
+
minArgs: 2,
|
|
365
|
+
maxArgs: 2,
|
|
366
|
+
description: "Formats a number and converts it to text",
|
|
367
|
+
examples: ['TEXT(1234.5, "$#,##0.00")', 'TEXT(0.5, "0%")'],
|
|
368
|
+
category: "Text",
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
functionRegistry.register({
|
|
372
|
+
name: "VALUE",
|
|
373
|
+
handler: valueHandler,
|
|
374
|
+
minArgs: 1,
|
|
375
|
+
maxArgs: 1,
|
|
376
|
+
description: "Converts a text string to a number",
|
|
377
|
+
examples: ['VALUE("123")', 'VALUE("$1,234.56")'],
|
|
378
|
+
category: "Text",
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
functionRegistry.register({
|
|
382
|
+
name: "EXACT",
|
|
383
|
+
handler: exactHandler,
|
|
384
|
+
minArgs: 2,
|
|
385
|
+
maxArgs: 2,
|
|
386
|
+
description: "Checks if two text strings are exactly the same",
|
|
387
|
+
examples: ['EXACT("Hello", "hello")', "EXACT(A1, B1)"],
|
|
388
|
+
category: "Text",
|
|
389
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spreadsheet Engine
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic spreadsheet calculation engine
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Export types
|
|
8
|
+
export * from "./types";
|
|
9
|
+
|
|
10
|
+
// Export utilities
|
|
11
|
+
export * from "./parser";
|
|
12
|
+
export * from "./formatter";
|
|
13
|
+
export * from "./evaluator";
|
|
14
|
+
export * from "./calculator";
|
|
15
|
+
export * from "./formulaRefs";
|
|
16
|
+
export * from "./cellBuilder";
|
|
17
|
+
export * from "./responseDecoder";
|
|
18
|
+
export * from "./jsonCellLocator";
|
|
19
|
+
|
|
20
|
+
// Export function registry
|
|
21
|
+
export * from "./registry";
|
|
22
|
+
|
|
23
|
+
// Load all built-in functions
|
|
24
|
+
import "./functions";
|
|
25
|
+
|
|
26
|
+
// Export main SpreadsheetEngine class
|
|
27
|
+
export { SpreadsheetEngine } from "./engine";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the start offset of a specific cell value inside a
|
|
3
|
+
* pretty-printed JSON spreadsheet document. Extracted from
|
|
4
|
+
* `handleTableClick` in `src/plugins/spreadsheet/View.vue` where the
|
|
5
|
+
* inline scanner pushed cognitive complexity to 163.
|
|
6
|
+
*
|
|
7
|
+
* The scanner walks the raw editor text character-by-character,
|
|
8
|
+
* tracking string boundaries, bracket depth, and object depth to
|
|
9
|
+
* find the n-th cell of the m-th row within a named sheet's `data`
|
|
10
|
+
* array. We deliberately do NOT parse the JSON — we need the
|
|
11
|
+
* character offset inside the user's text buffer (which may not be
|
|
12
|
+
* valid JSON mid-edit), so a positional scan is required.
|
|
13
|
+
*
|
|
14
|
+
* Pure — no refs, no DOM. Returns -1 if the cell can't be located.
|
|
15
|
+
* Tested in `test/plugins/spreadsheet/engine/test_jsonCellLocator.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Advance `pos` through `text` until we reach the `rowIndex`-th
|
|
19
|
+
// opening `[` after `startPos` (counting from -1 so the first `[`
|
|
20
|
+
// encountered is index 0). Returns the position just after that
|
|
21
|
+
// opening bracket, or -1 if we ran off the end.
|
|
22
|
+
function findRowOpenBracket(text: string, startPos: number, rowIndex: number): number {
|
|
23
|
+
let currentRow = -1;
|
|
24
|
+
let inString = false;
|
|
25
|
+
for (let i = startPos; i < text.length; i++) {
|
|
26
|
+
const c = text[i];
|
|
27
|
+
const prevChar = i > 0 ? text[i - 1] : "";
|
|
28
|
+
// Track string literal boundaries so that a `[` inside a cell
|
|
29
|
+
// value like `"has [bracket]"` doesn't get mistaken for a row
|
|
30
|
+
// opener and throw off the row offset.
|
|
31
|
+
if (c === '"' && prevChar !== "\\") {
|
|
32
|
+
inString = !inString;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!inString && c === "[") {
|
|
36
|
+
currentRow++;
|
|
37
|
+
if (currentRow === rowIndex) return i + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Starting just inside the row's `[`, scan for the start offset of
|
|
44
|
+
// the `colIndex`-th cell. Tracks string/object/bracket state so
|
|
45
|
+
// commas inside cell objects don't miscounted as cell separators.
|
|
46
|
+
// Returns -1 if the row ends before we reach colIndex.
|
|
47
|
+
function findCellStartWithinRow(text: string, rowStart: number, colIndex: number): number {
|
|
48
|
+
let currentCol = 0;
|
|
49
|
+
let inString = false;
|
|
50
|
+
let inObject = 0;
|
|
51
|
+
let bracketDepth = 1; // we already stepped past one `[`
|
|
52
|
+
|
|
53
|
+
for (let i = rowStart; i < text.length; i++) {
|
|
54
|
+
const c = text[i];
|
|
55
|
+
const prevChar = i > 0 ? text[i - 1] : "";
|
|
56
|
+
|
|
57
|
+
if (c === '"' && prevChar !== "\\") {
|
|
58
|
+
inString = !inString;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!inString) {
|
|
62
|
+
if (c === "[") bracketDepth++;
|
|
63
|
+
if (c === "]") {
|
|
64
|
+
bracketDepth--;
|
|
65
|
+
if (bracketDepth === 0) return -1; // row ended before colIndex
|
|
66
|
+
}
|
|
67
|
+
if (c === "{") inObject++;
|
|
68
|
+
if (c === "}") inObject--;
|
|
69
|
+
if (c === "," && inObject === 0 && bracketDepth === 1) {
|
|
70
|
+
currentCol++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Once currentCol matches, skip any structural whitespace /
|
|
75
|
+
// opening bracket / comma and return the first content char.
|
|
76
|
+
if (currentCol === colIndex) {
|
|
77
|
+
if (c !== " " && c !== "\n" && c !== "\t" && c !== "[" && c !== ",") {
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Given the pretty-printed JSON editor text, a sheet name, and
|
|
87
|
+
* (rowIndex, colIndex), return the character offset of that cell's
|
|
88
|
+
* JSON token. The returned offset can be fed into
|
|
89
|
+
* `textarea.setSelectionRange(offset, offset + cellJsonLength)` to
|
|
90
|
+
* highlight the cell.
|
|
91
|
+
*
|
|
92
|
+
* Returns -1 if the sheet isn't found or the (row, col) is out of
|
|
93
|
+
* range. Never throws.
|
|
94
|
+
*/
|
|
95
|
+
export function findCellJsonPosition(editorText: string, sheetName: string, rowIndex: number, colIndex: number): number {
|
|
96
|
+
// JSON.stringify escapes embedded quotes/backslashes so the marker
|
|
97
|
+
// matches the way the sheet name actually appears in editorText.
|
|
98
|
+
const sheetStartMarker = `"name": ${JSON.stringify(sheetName)}`;
|
|
99
|
+
const dataStartMarker = `"data": [`;
|
|
100
|
+
|
|
101
|
+
const sheetPos = editorText.indexOf(sheetStartMarker);
|
|
102
|
+
if (sheetPos === -1) return -1;
|
|
103
|
+
|
|
104
|
+
const dataPos = editorText.indexOf(dataStartMarker, sheetPos);
|
|
105
|
+
if (dataPos === -1) return -1;
|
|
106
|
+
|
|
107
|
+
const rowStart = findRowOpenBracket(editorText, dataPos + dataStartMarker.length, rowIndex);
|
|
108
|
+
if (rowStart === -1) return -1;
|
|
109
|
+
|
|
110
|
+
return findCellStartWithinRow(editorText, rowStart, colIndex);
|
|
111
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell and Range Reference Parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles Excel A1 notation parsing and conversion
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CellRef, RangeRef } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert Excel column letters to 0-based index
|
|
11
|
+
* A=0, B=1, ..., Z=25, AA=26, etc.
|
|
12
|
+
*
|
|
13
|
+
* @param col - Column letters (e.g., "A", "Z", "AA")
|
|
14
|
+
* @returns 0-based column index
|
|
15
|
+
*/
|
|
16
|
+
export function columnToIndex(col: string): number {
|
|
17
|
+
let result = 0;
|
|
18
|
+
for (let i = 0; i < col.length; i++) {
|
|
19
|
+
result = result * 26 + (col.charCodeAt(i) - 64); // A=1, B=2, etc.
|
|
20
|
+
}
|
|
21
|
+
return result - 1; // Convert to 0-based
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert 0-based index to Excel column letters
|
|
26
|
+
* 0=A, 1=B, ..., 25=Z, 26=AA, etc.
|
|
27
|
+
*
|
|
28
|
+
* @param index - 0-based column index
|
|
29
|
+
* @returns Column letters (e.g., "A", "Z", "AA")
|
|
30
|
+
*/
|
|
31
|
+
export function indexToColumn(index: number): string {
|
|
32
|
+
let col = "";
|
|
33
|
+
let num = index + 1; // Convert to 1-based
|
|
34
|
+
while (num > 0) {
|
|
35
|
+
const remainder = (num - 1) % 26;
|
|
36
|
+
col = String.fromCharCode(65 + remainder) + col;
|
|
37
|
+
num = Math.floor((num - 1) / 26);
|
|
38
|
+
}
|
|
39
|
+
return col;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a cell reference to its components
|
|
44
|
+
* Supports: A1, $A$1, $A1, A$1, Sheet1!A1, 'My Sheet'!A1
|
|
45
|
+
*
|
|
46
|
+
* @param ref - Cell reference string
|
|
47
|
+
* @returns Parsed cell reference object
|
|
48
|
+
*/
|
|
49
|
+
export function parseCellRef(ref: string): CellRef {
|
|
50
|
+
let cellRef = ref;
|
|
51
|
+
let sheetName: string | undefined;
|
|
52
|
+
|
|
53
|
+
// Check for cross-sheet reference (e.g., 'Sheet Name'!B2 or Sheet1!B2)
|
|
54
|
+
const sheetMatch = ref.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
|
|
55
|
+
if (sheetMatch) {
|
|
56
|
+
sheetName = sheetMatch[1] || sheetMatch[2]; // Quoted or unquoted sheet name
|
|
57
|
+
cellRef = sheetMatch[3]; // Cell reference part
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse absolute references ($A$1)
|
|
61
|
+
const absoluteRow = cellRef.includes("$") && cellRef.match(/\$\d+/);
|
|
62
|
+
const absoluteCol = cellRef.includes("$") && cellRef.match(/\$[A-Z]+/);
|
|
63
|
+
|
|
64
|
+
// Remove $ symbols
|
|
65
|
+
const cleanRef = cellRef.replace(/\$/g, "");
|
|
66
|
+
const match = cleanRef.match(/^([A-Z]+)(\d+)$/);
|
|
67
|
+
|
|
68
|
+
if (!match) {
|
|
69
|
+
throw new Error(`Invalid cell reference: ${ref}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const col = columnToIndex(match[1]);
|
|
73
|
+
const row = parseInt(match[2]) - 1; // 1-indexed to 0-indexed
|
|
74
|
+
|
|
75
|
+
const result: CellRef = { row, col };
|
|
76
|
+
|
|
77
|
+
if (sheetName) {
|
|
78
|
+
result.sheet = sheetName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (absoluteRow || absoluteCol) {
|
|
82
|
+
result.absolute = {
|
|
83
|
+
row: !!absoluteRow,
|
|
84
|
+
col: !!absoluteCol,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse a range reference to its components
|
|
93
|
+
* Supports: A1:B10, $A$1:$B$10, Sheet1!A1:B10
|
|
94
|
+
*
|
|
95
|
+
* @param range - Range reference string
|
|
96
|
+
* @returns Parsed range reference object
|
|
97
|
+
*/
|
|
98
|
+
export function parseRangeRef(range: string): RangeRef {
|
|
99
|
+
// Use non-greedy match to improve performance
|
|
100
|
+
const colonIndex = range.lastIndexOf(":");
|
|
101
|
+
if (colonIndex === -1) {
|
|
102
|
+
throw new Error(`Invalid range reference: ${range}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const start = parseCellRef(range.substring(0, colonIndex));
|
|
106
|
+
const end = parseCellRef(range.substring(colonIndex + 1));
|
|
107
|
+
|
|
108
|
+
return { start, end };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert a cell reference object back to A1 notation
|
|
113
|
+
*
|
|
114
|
+
* @param ref - Cell reference object
|
|
115
|
+
* @returns A1 notation string
|
|
116
|
+
*/
|
|
117
|
+
export function cellRefToA1(ref: CellRef): string {
|
|
118
|
+
const col = indexToColumn(ref.col);
|
|
119
|
+
const row = ref.row + 1; // 0-based to 1-based
|
|
120
|
+
|
|
121
|
+
let result = "";
|
|
122
|
+
|
|
123
|
+
if (ref.absolute?.col) {
|
|
124
|
+
result += "$";
|
|
125
|
+
}
|
|
126
|
+
result += col;
|
|
127
|
+
|
|
128
|
+
if (ref.absolute?.row) {
|
|
129
|
+
result += "$";
|
|
130
|
+
}
|
|
131
|
+
result += row;
|
|
132
|
+
|
|
133
|
+
if (ref.sheet) {
|
|
134
|
+
// Quote sheet name if it contains spaces
|
|
135
|
+
if (ref.sheet.includes(" ")) {
|
|
136
|
+
result = `'${ref.sheet}'!${result}`;
|
|
137
|
+
} else {
|
|
138
|
+
result = `${ref.sheet}!${result}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|