@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE,
|
|
3
|
+
escHtml,
|
|
4
|
+
workspaceRootName,
|
|
5
|
+
workspaceRootEntrySet,
|
|
6
|
+
WORKSPACE_FILE_EXTENSIONS,
|
|
7
|
+
serverPlatform,
|
|
8
|
+
} from './store.js';
|
|
9
|
+
|
|
10
|
+
export function workspaceMentionRegex() {
|
|
11
|
+
return /(?:[A-Za-z]:[\\/])?(?:\.{1,2}[\\/])?(?:[A-Za-z0-9._-]+[\\/])*[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,16}/g;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeWorkspaceMentionPath(rawText) {
|
|
15
|
+
const original = String(rawText || '');
|
|
16
|
+
const cleaned = original
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/^['"`([{<]+/, '')
|
|
19
|
+
.replace(/[)\]}>:;,.!?'"`]+$/, '')
|
|
20
|
+
.replace(/\\/g, '/')
|
|
21
|
+
.replace(/^\/+/, '')
|
|
22
|
+
.replace(/\/+/g, '/');
|
|
23
|
+
if (!cleaned) return '';
|
|
24
|
+
const lowered = cleaned.toLowerCase();
|
|
25
|
+
if (lowered.startsWith('http://') || lowered.startsWith('https://') || lowered.startsWith('api/')) return '';
|
|
26
|
+
const hadDrivePrefix = /^[A-Za-z]:[\\/]/.test(original.trim());
|
|
27
|
+
const hadLeadingSlash = /^[\\/]/.test(original.trim());
|
|
28
|
+
let parts = cleaned.split('/').filter(Boolean);
|
|
29
|
+
if (!parts.length) return '';
|
|
30
|
+
if (/^[a-z]:$/i.test(parts[0])) {
|
|
31
|
+
parts = parts.slice(1);
|
|
32
|
+
}
|
|
33
|
+
if (!parts.length) return '';
|
|
34
|
+
if (workspaceRootName) {
|
|
35
|
+
const idx = parts.findIndex((part) => part.toLowerCase() === workspaceRootName);
|
|
36
|
+
if (idx >= 0) {
|
|
37
|
+
parts = parts.slice(idx + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!parts.length) return '';
|
|
41
|
+
if ((hadDrivePrefix || hadLeadingSlash) && !workspaceRootEntrySet.has(parts[0].toLowerCase())) {
|
|
42
|
+
const firstRootIdx = parts.findIndex((part) => workspaceRootEntrySet.has(part.toLowerCase()));
|
|
43
|
+
if (firstRootIdx > 0) {
|
|
44
|
+
parts = parts.slice(firstRootIdx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!parts.length) return '';
|
|
48
|
+
if (parts.some((part) => part === '.' || part === '..')) return '';
|
|
49
|
+
return parts.join('/');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isLikelyWorkspaceFileMention(rawText) {
|
|
53
|
+
const normalized = normalizeWorkspaceMentionPath(rawText);
|
|
54
|
+
if (!normalized) return false;
|
|
55
|
+
const ext = normalized.includes('.') ? normalized.slice(normalized.lastIndexOf('.') + 1).toLowerCase() : '';
|
|
56
|
+
return WORKSPACE_FILE_EXTENSIONS.has(ext);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function workspaceFilePathFromText(rawText) {
|
|
60
|
+
if (!isLikelyWorkspaceFileMention(rawText)) return '';
|
|
61
|
+
return normalizeWorkspaceMentionPath(rawText);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function workspaceFileHrefFromPath(rawPath) {
|
|
65
|
+
const normalized = normalizeWorkspaceMentionPath(rawPath);
|
|
66
|
+
if (!normalized) return '';
|
|
67
|
+
const encodedPath = normalized.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
|
68
|
+
if (!encodedPath) return '';
|
|
69
|
+
return `${BASE}/api/files/${encodedPath}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function workspaceFileHref(rawText) {
|
|
73
|
+
const normalized = workspaceFilePathFromText(rawText);
|
|
74
|
+
return normalized ? workspaceFileHrefFromPath(normalized) : '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function workspacePreviewApiPath(rawPath) {
|
|
78
|
+
const normalized = normalizeWorkspaceMentionPath(rawPath);
|
|
79
|
+
if (!normalized) return '';
|
|
80
|
+
const encodedPath = normalized.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
|
81
|
+
return `/api/files-preview/${encodedPath}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function normalizeDriveBrowserPath(rawPath) {
|
|
85
|
+
const value = String(rawPath || '').trim();
|
|
86
|
+
if (!value) return '';
|
|
87
|
+
let decoded = value;
|
|
88
|
+
try {
|
|
89
|
+
decoded = decodeURIComponent(value);
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
const withoutNulls = decoded.replace(/\0/g, '');
|
|
94
|
+
|
|
95
|
+
if (serverPlatform !== 'win32') {
|
|
96
|
+
const linuxNorm = withoutNulls
|
|
97
|
+
.replace(/\\/g, '/')
|
|
98
|
+
.replace(/\/+/g, '/')
|
|
99
|
+
.trimEnd()
|
|
100
|
+
.replace(/(.)\/$/, '$1');
|
|
101
|
+
if (!linuxNorm.startsWith('/')) return '';
|
|
102
|
+
if (linuxNorm.includes('/../') || linuxNorm.endsWith('/..')) return '';
|
|
103
|
+
return linuxNorm || '/';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const normalized = withoutNulls
|
|
107
|
+
.replace(/\\/g, '/')
|
|
108
|
+
.replace(/\/+/g, '/')
|
|
109
|
+
.trim()
|
|
110
|
+
.replace(/\/+$/, '');
|
|
111
|
+
if (!normalized) return '';
|
|
112
|
+
if (!/^[A-Za-z]:(?:\/.*)?$/.test(normalized)) return '';
|
|
113
|
+
const drive = `${normalized.slice(0, 1).toUpperCase()}:`;
|
|
114
|
+
const rest = normalized.slice(2).replace(/^\/+/, '');
|
|
115
|
+
return rest ? `${drive}/${rest}` : drive;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function driveFileHrefFromPath(rawPath) {
|
|
119
|
+
const normalized = normalizeDriveBrowserPath(rawPath);
|
|
120
|
+
if (!normalized) return '';
|
|
121
|
+
return `${BASE}/api/drives/file?path=${encodeURIComponent(normalized)}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function drivePreviewApiPath(rawPath) {
|
|
125
|
+
const normalized = normalizeDriveBrowserPath(rawPath);
|
|
126
|
+
if (!normalized) return '';
|
|
127
|
+
return `/api/drives/files-preview?path=${encodeURIComponent(normalized)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeReferencePathForToken(kind, rawPath, source = 'workspace') {
|
|
131
|
+
const tokenKind = String(kind || '').trim().toLowerCase();
|
|
132
|
+
if (tokenKind !== 'file' && tokenKind !== 'folder') return '';
|
|
133
|
+
const root = String(source || '').trim().toLowerCase();
|
|
134
|
+
if (root === 'drives' || root === 'session') return normalizeDriveBrowserPath(rawPath);
|
|
135
|
+
return normalizeWorkspaceMentionPath(rawPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildReferenceToken(kind, rawPath, source = 'workspace') {
|
|
139
|
+
const tokenKind = String(kind || '').trim().toLowerCase();
|
|
140
|
+
if (tokenKind !== 'file' && tokenKind !== 'folder') return '';
|
|
141
|
+
const normalizedPath = normalizeReferencePathForToken(tokenKind, rawPath, source);
|
|
142
|
+
if (!normalizedPath) return '';
|
|
143
|
+
return `@${tokenKind}:${normalizedPath}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function buildWrappedReferenceToken(kind, rawPath, source = 'workspace') {
|
|
147
|
+
const token = buildReferenceToken(kind, rawPath, source);
|
|
148
|
+
if (!token) return '';
|
|
149
|
+
return `\`${token}\``;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function copyTextToClipboard(value) {
|
|
153
|
+
const text = String(value || '');
|
|
154
|
+
if (!text) return false;
|
|
155
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
156
|
+
await navigator.clipboard.writeText(text);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const input = document.createElement('textarea');
|
|
160
|
+
input.value = text;
|
|
161
|
+
input.setAttribute('readonly', 'readonly');
|
|
162
|
+
input.style.position = 'fixed';
|
|
163
|
+
input.style.left = '-9999px';
|
|
164
|
+
document.body.appendChild(input);
|
|
165
|
+
input.select();
|
|
166
|
+
try {
|
|
167
|
+
document.execCommand('copy');
|
|
168
|
+
return true;
|
|
169
|
+
} finally {
|
|
170
|
+
document.body.removeChild(input);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function copyReferenceTokenToClipboard(kind, rawPath, source = 'workspace') {
|
|
175
|
+
const wrapped = buildWrappedReferenceToken(kind, rawPath, source);
|
|
176
|
+
if (!wrapped) return false;
|
|
177
|
+
try {
|
|
178
|
+
await copyTextToClipboard(wrapped);
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function isSafePreviewUrl(url, allowDataImage = false) {
|
|
186
|
+
const value = String(url || '').trim();
|
|
187
|
+
if (!value) return false;
|
|
188
|
+
if (value.startsWith('#') || value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) return true;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = new URL(value, window.location.origin);
|
|
191
|
+
const protocol = String(parsed.protocol || '').toLowerCase();
|
|
192
|
+
if (protocol === 'http:' || protocol === 'https:' || protocol === 'mailto:') return true;
|
|
193
|
+
if (allowDataImage && protocol === 'data:' && /^data:image\/(?:avif|gif|jpe?g|png|webp);/i.test(value)) return true;
|
|
194
|
+
} catch {}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const PREVIEW_ALLOWED_TAGS = new Set([
|
|
199
|
+
'a', 'article', 'b', 'blockquote', 'br', 'code', 'del', 'details', 'div', 'em',
|
|
200
|
+
'figcaption', 'figure', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
|
|
201
|
+
'kbd', 'li', 'mark', 'ol', 'p', 'pre', 's', 'small', 'span', 'strong', 'sub',
|
|
202
|
+
'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul',
|
|
203
|
+
]);
|
|
204
|
+
const PREVIEW_GLOBAL_ATTRIBUTES = new Set(['dir', 'lang', 'title']);
|
|
205
|
+
const PREVIEW_ALIGNMENT_VALUES = new Set(['left', 'center', 'right', 'justify']);
|
|
206
|
+
const PREVIEW_VERTICAL_ALIGNMENT_VALUES = new Set(['top', 'middle', 'bottom', 'baseline']);
|
|
207
|
+
const PREVIEW_DIMENSION_VALUE = /^(?:[1-9]\d{0,3}|100(?:\.0+)?%)$/;
|
|
208
|
+
const PREVIEW_SPAN_VALUE = /^[1-9]\d{0,2}$/;
|
|
209
|
+
|
|
210
|
+
function isAllowedPreviewAttribute(tagName, name, value) {
|
|
211
|
+
if (PREVIEW_GLOBAL_ATTRIBUTES.has(name)) return true;
|
|
212
|
+
if (name === 'href') return tagName === 'a' && isSafePreviewUrl(value, false);
|
|
213
|
+
if (name === 'src') return tagName === 'img' && isSafePreviewUrl(value, true);
|
|
214
|
+
if (name === 'alt') return tagName === 'img';
|
|
215
|
+
if (name === 'target') return tagName === 'a' && value === '_blank';
|
|
216
|
+
if (name === 'rel') return tagName === 'a';
|
|
217
|
+
if (name === 'align') return ['div', 'p', 'table', 'td', 'th', 'tr'].includes(tagName)
|
|
218
|
+
&& PREVIEW_ALIGNMENT_VALUES.has(value.toLowerCase());
|
|
219
|
+
if (name === 'valign') return ['td', 'th', 'tr'].includes(tagName)
|
|
220
|
+
&& PREVIEW_VERTICAL_ALIGNMENT_VALUES.has(value.toLowerCase());
|
|
221
|
+
if (name === 'width' || name === 'height') return ['img', 'table', 'td', 'th'].includes(tagName)
|
|
222
|
+
&& PREVIEW_DIMENSION_VALUE.test(value);
|
|
223
|
+
if (name === 'rowspan' || name === 'colspan') return ['td', 'th'].includes(tagName)
|
|
224
|
+
&& PREVIEW_SPAN_VALUE.test(value);
|
|
225
|
+
if (name === 'cellspacing' || name === 'cellpadding') return tagName === 'table'
|
|
226
|
+
&& /^(?:0|[1-9]\d{0,2})$/.test(value);
|
|
227
|
+
if (name === 'loading') return tagName === 'img' && ['eager', 'lazy'].includes(value.toLowerCase());
|
|
228
|
+
if (name === 'decoding') return tagName === 'img' && ['async', 'auto', 'sync'].includes(value.toLowerCase());
|
|
229
|
+
if (name === 'open') return tagName === 'details' && value === '';
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function sanitizePreviewHtml(html) {
|
|
234
|
+
const template = document.createElement('template');
|
|
235
|
+
template.innerHTML = String(html || '');
|
|
236
|
+
const nodes = Array.from(template.content.querySelectorAll('*'));
|
|
237
|
+
for (const el of nodes) {
|
|
238
|
+
const tagName = String(el.tagName || '').toLowerCase();
|
|
239
|
+
if (!PREVIEW_ALLOWED_TAGS.has(tagName)) {
|
|
240
|
+
el.replaceWith(document.createTextNode(el.textContent || ''));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const attrs = Array.from(el.attributes || []);
|
|
244
|
+
for (const attr of attrs) {
|
|
245
|
+
const name = String(attr.name || '').toLowerCase();
|
|
246
|
+
const value = String(attr.value || '').trim();
|
|
247
|
+
if (!isAllowedPreviewAttribute(tagName, name, value)) {
|
|
248
|
+
el.removeAttribute(attr.name);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (tagName === 'a') {
|
|
252
|
+
el.setAttribute('target', '_blank');
|
|
253
|
+
el.setAttribute('rel', 'noopener noreferrer');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return template.innerHTML;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const COMPATIBILITY_DISPLAY_MATH_COMMAND = /\\(?:frac|sqrt|sum|prod|int|iint|iiint|lim|log|ln|sin|cos|tan|theta|alpha|beta|gamma|delta|lambda|mu|pi|sigma|phi|omega|vec|hat|bar|overline|underline|left|right|begin|end|text|mathrm|mathbf|mathbb|mathcal|ce)\b/i;
|
|
260
|
+
const INLINE_MATH_COMMAND = /\\(?:frac|sqrt|sum|prod|int|iint|iiint|lim|log|ln|sin|cos|tan|cot|sec|csc|arcsin|arccos|theta|alpha|beta|gamma|delta|lambda|mu|pi|rho|sigma|phi|omega|Omega|Gamma|Delta|Lambda|Pi|Sigma|Phi|Psi|Omega|vec|hat|bar|overline|underline|left|right|text|mathrm|mathbf|mathbb|mathcal|operatorname|circ|degree|times|cdot|pm|mp|leq|geq|neq|approx|equiv|infty|partial|nabla|forall|exists|rightarrow|longmapsto|to|mapsto|ce)\b/;
|
|
261
|
+
const STRICT_INLINE_SCRIPT = /(?:\^[{]|\^[-+]?\d|_[{]|_[-+]?\d)/;
|
|
262
|
+
const URL_LIKE_TEXT = /\b(?:https?|ftp):\/\/|www\./i;
|
|
263
|
+
const MARKDOWN_ESCAPABLE_TEX_CHARACTERS = new Set(Array.from('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'));
|
|
264
|
+
|
|
265
|
+
function isStrictInlineMathCandidate(value) {
|
|
266
|
+
const text = String(value || '').trim();
|
|
267
|
+
if (!text || text.length > 240 || URL_LIKE_TEXT.test(text)) return false;
|
|
268
|
+
return INLINE_MATH_COMMAND.test(text) || STRICT_INLINE_SCRIPT.test(text);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function findClosingParenthesis(source, openingIndex) {
|
|
272
|
+
let depth = 0;
|
|
273
|
+
for (let index = openingIndex; index < source.length; index += 1) {
|
|
274
|
+
if (source[index] === '\\') {
|
|
275
|
+
index += 1;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (source[index] === '(') depth += 1;
|
|
279
|
+
if (source[index] === ')') {
|
|
280
|
+
depth -= 1;
|
|
281
|
+
if (depth === 0) return index;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return -1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeInlineMathDelimiters(line) {
|
|
288
|
+
let output = '';
|
|
289
|
+
let index = 0;
|
|
290
|
+
let inCode = false;
|
|
291
|
+
let inDollarMath = false;
|
|
292
|
+
|
|
293
|
+
while (index < line.length) {
|
|
294
|
+
const char = line[index];
|
|
295
|
+
if (char === '`') {
|
|
296
|
+
inCode = !inCode;
|
|
297
|
+
output += char;
|
|
298
|
+
index += 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (inCode) {
|
|
302
|
+
output += char;
|
|
303
|
+
index += 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (char === '$' && line[index - 1] !== '\\') {
|
|
307
|
+
inDollarMath = !inDollarMath;
|
|
308
|
+
output += char;
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (inDollarMath) {
|
|
313
|
+
output += char;
|
|
314
|
+
index += 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === '\\' && line[index + 1] === '(') {
|
|
318
|
+
const closeIndex = line.indexOf('\\)', index + 2);
|
|
319
|
+
if (closeIndex >= 0) {
|
|
320
|
+
output += `$${line.slice(index + 2, closeIndex).trim()}$`;
|
|
321
|
+
index = closeIndex + 2;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (char !== '(' || line[index - 1] === '\\') {
|
|
326
|
+
output += char;
|
|
327
|
+
index += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const closeIndex = findClosingParenthesis(line, index);
|
|
331
|
+
if (closeIndex < 0) {
|
|
332
|
+
output += char;
|
|
333
|
+
index += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const candidate = line.slice(index + 1, closeIndex);
|
|
337
|
+
if (!isStrictInlineMathCandidate(candidate)) {
|
|
338
|
+
output += char;
|
|
339
|
+
index += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
output += `$${candidate.trim()}$`;
|
|
343
|
+
index = closeIndex + 1;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return output;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function protectMarkdownEscapesInMath(source) {
|
|
350
|
+
const lines = String(source ?? '').split('\n');
|
|
351
|
+
let inFence = false;
|
|
352
|
+
|
|
353
|
+
return lines.map((line) => {
|
|
354
|
+
if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
|
|
355
|
+
inFence = !inFence;
|
|
356
|
+
return line;
|
|
357
|
+
}
|
|
358
|
+
if (inFence) return line;
|
|
359
|
+
|
|
360
|
+
let output = '';
|
|
361
|
+
let index = 0;
|
|
362
|
+
let inCode = false;
|
|
363
|
+
while (index < line.length) {
|
|
364
|
+
if (line[index] === '`') {
|
|
365
|
+
inCode = !inCode;
|
|
366
|
+
output += line[index];
|
|
367
|
+
index += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (inCode || line[index] !== '$' || line[index - 1] === '\\') {
|
|
371
|
+
output += line[index];
|
|
372
|
+
index += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const delimiter = line[index + 1] === '$' ? '$$' : '$';
|
|
376
|
+
const closeIndex = line.indexOf(delimiter, index + delimiter.length);
|
|
377
|
+
if (closeIndex < 0) {
|
|
378
|
+
output += line[index];
|
|
379
|
+
index += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const formula = line.slice(index + delimiter.length, closeIndex);
|
|
383
|
+
const protectedFormula = formula.replace(/\\(.)/g, (match, character) => (
|
|
384
|
+
MARKDOWN_ESCAPABLE_TEX_CHARACTERS.has(character) ? `\\\\${character}` : match
|
|
385
|
+
));
|
|
386
|
+
output += `${delimiter}${protectedFormula}${delimiter}`;
|
|
387
|
+
index = closeIndex + delimiter.length;
|
|
388
|
+
}
|
|
389
|
+
return output;
|
|
390
|
+
}).join('\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function normalizeMathDelimiters(source) {
|
|
394
|
+
const lines = String(source ?? '').split('\n');
|
|
395
|
+
const normalized = [];
|
|
396
|
+
let inFence = false;
|
|
397
|
+
|
|
398
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
399
|
+
let line = lines[index];
|
|
400
|
+
if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
|
|
401
|
+
inFence = !inFence;
|
|
402
|
+
normalized.push(line);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (inFence || /^(?: {4}|\t)/.test(line)) {
|
|
406
|
+
normalized.push(line);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
line = normalizeInlineMathDelimiters(line)
|
|
411
|
+
.replace(/\\\[([^\n]+?)\\\]/g, (_match, formula) => `$$${formula.trim()}$$`);
|
|
412
|
+
const delimiter = line.trim();
|
|
413
|
+
const closingDelimiter = delimiter === '$$'
|
|
414
|
+
? '$$'
|
|
415
|
+
: delimiter === '\\['
|
|
416
|
+
? '\\]'
|
|
417
|
+
: delimiter === '['
|
|
418
|
+
? ']'
|
|
419
|
+
: '';
|
|
420
|
+
if (!closingDelimiter) {
|
|
421
|
+
normalized.push(line);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let closingIndex = index + 1;
|
|
426
|
+
while (closingIndex < lines.length && lines[closingIndex].trim() !== closingDelimiter) closingIndex += 1;
|
|
427
|
+
if (closingIndex >= lines.length) {
|
|
428
|
+
normalized.push(line);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const formula = lines.slice(index + 1, closingIndex).join('\n');
|
|
433
|
+
if (delimiter === '[' && !COMPATIBILITY_DISPLAY_MATH_COMMAND.test(formula)) {
|
|
434
|
+
normalized.push(line);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
normalized.push(`$$${formula.trim().replace(/[ \t]*\n[ \t]*/g, ' ')}$$`);
|
|
438
|
+
index = closingIndex;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return protectMarkdownEscapesInMath(normalized.join('\n'));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function renderMathInHtml(html) {
|
|
445
|
+
const renderMathInElement = globalThis.renderMathInElement;
|
|
446
|
+
if (typeof renderMathInElement !== 'function' || !globalThis.document?.createElement) return html;
|
|
447
|
+
|
|
448
|
+
const container = document.createElement('div');
|
|
449
|
+
container.innerHTML = html;
|
|
450
|
+
try {
|
|
451
|
+
renderMathInElement(container, {
|
|
452
|
+
delimiters: [
|
|
453
|
+
{ left: '$$', right: '$$', display: true },
|
|
454
|
+
{ left: '\\[', right: '\\]', display: true },
|
|
455
|
+
{ left: '\\(', right: '\\)', display: false },
|
|
456
|
+
{ left: '$', right: '$', display: false },
|
|
457
|
+
],
|
|
458
|
+
ignoredTags: ['pre', 'code', 'script', 'style', 'textarea', 'option', 'a'],
|
|
459
|
+
throwOnError: false,
|
|
460
|
+
strict: 'ignore',
|
|
461
|
+
trust: false,
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.warn('[markdown] could not render mathematical notation', error);
|
|
465
|
+
return html;
|
|
466
|
+
}
|
|
467
|
+
// The original Markdown was sanitized before this pass. KaTeX generates its own
|
|
468
|
+
// layout styles, which are necessary for fractions, scripts, and matrices.
|
|
469
|
+
return container.innerHTML;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function renderMarkdownPreview(source, allowEmbeddedHtml) {
|
|
473
|
+
const sourceText = normalizeMathDelimiters(source);
|
|
474
|
+
const fallbackEscaped = `<p>${escHtml(sourceText).replace(/\n/g, '<br>')}</p>`;
|
|
475
|
+
const markdown = globalThis.marked;
|
|
476
|
+
if (!markdown || typeof markdown.parse !== 'function' || typeof markdown.Renderer !== 'function') {
|
|
477
|
+
return fallbackEscaped;
|
|
478
|
+
}
|
|
479
|
+
if (allowEmbeddedHtml) {
|
|
480
|
+
const parsed = markdown.parse(sourceText);
|
|
481
|
+
if (typeof parsed !== 'string') return fallbackEscaped;
|
|
482
|
+
return renderMathInHtml(sanitizePreviewHtml(parsed));
|
|
483
|
+
}
|
|
484
|
+
const renderer = new markdown.Renderer();
|
|
485
|
+
renderer.html = (htmlToken) => {
|
|
486
|
+
if (typeof htmlToken === 'string') return escHtml(htmlToken);
|
|
487
|
+
if (htmlToken && typeof htmlToken === 'object') {
|
|
488
|
+
return escHtml(htmlToken.raw ?? htmlToken.text ?? '');
|
|
489
|
+
}
|
|
490
|
+
return '';
|
|
491
|
+
};
|
|
492
|
+
const parsed = markdown.parse(sourceText, { renderer });
|
|
493
|
+
if (typeof parsed !== 'string') return fallbackEscaped;
|
|
494
|
+
return renderMathInHtml(sanitizePreviewHtml(parsed));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function eventTargetElement(event) {
|
|
498
|
+
const target = event?.target;
|
|
499
|
+
if (target instanceof Element) return target;
|
|
500
|
+
if (target && target.nodeType === Node.TEXT_NODE && target.parentElement) return target.parentElement;
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function eventClosest(event, selector) {
|
|
505
|
+
const target = eventTargetElement(event);
|
|
506
|
+
if (target) {
|
|
507
|
+
const closest = target.closest(selector);
|
|
508
|
+
if (closest) return closest;
|
|
509
|
+
}
|
|
510
|
+
const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
|
|
511
|
+
for (const entry of path) {
|
|
512
|
+
if (entry instanceof Element && entry.matches(selector)) return entry;
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function buildLinkedTextFragment(text) {
|
|
518
|
+
const source = String(text || '');
|
|
519
|
+
const regex = workspaceMentionRegex();
|
|
520
|
+
const fragment = document.createDocumentFragment();
|
|
521
|
+
let lastIndex = 0;
|
|
522
|
+
let changed = false;
|
|
523
|
+
let match = null;
|
|
524
|
+
while ((match = regex.exec(source)) !== null) {
|
|
525
|
+
const mention = match[0];
|
|
526
|
+
const workspacePath = workspaceFilePathFromText(mention);
|
|
527
|
+
const href = workspacePath ? workspaceFileHrefFromPath(workspacePath) : '';
|
|
528
|
+
if (!workspacePath || !href) continue;
|
|
529
|
+
if (match.index > lastIndex) {
|
|
530
|
+
fragment.appendChild(document.createTextNode(source.slice(lastIndex, match.index)));
|
|
531
|
+
}
|
|
532
|
+
const anchor = document.createElement('a');
|
|
533
|
+
anchor.href = href;
|
|
534
|
+
anchor.className = 'workspace-file-link';
|
|
535
|
+
anchor.dataset.workspacePath = workspacePath;
|
|
536
|
+
anchor.target = '_blank';
|
|
537
|
+
anchor.rel = 'noopener noreferrer';
|
|
538
|
+
anchor.textContent = mention;
|
|
539
|
+
fragment.appendChild(anchor);
|
|
540
|
+
lastIndex = match.index + mention.length;
|
|
541
|
+
changed = true;
|
|
542
|
+
}
|
|
543
|
+
if (!changed) return null;
|
|
544
|
+
if (lastIndex < source.length) {
|
|
545
|
+
fragment.appendChild(document.createTextNode(source.slice(lastIndex)));
|
|
546
|
+
}
|
|
547
|
+
return fragment;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function linkifyWorkspaceMentionsInNode(root) {
|
|
551
|
+
if (!root) return;
|
|
552
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
553
|
+
acceptNode(node) {
|
|
554
|
+
if (!node?.nodeValue || !String(node.nodeValue).trim()) return NodeFilter.FILTER_REJECT;
|
|
555
|
+
const parent = node.parentElement;
|
|
556
|
+
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
557
|
+
const tag = String(parent.tagName || '').toUpperCase();
|
|
558
|
+
if (tag === 'A' || tag === 'CODE' || tag === 'PRE' || tag === 'SCRIPT' || tag === 'STYLE') {
|
|
559
|
+
return NodeFilter.FILTER_REJECT;
|
|
560
|
+
}
|
|
561
|
+
if (parent.closest?.('.msg-attachment')) return NodeFilter.FILTER_REJECT;
|
|
562
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const textNodes = [];
|
|
567
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
568
|
+
for (const node of textNodes) {
|
|
569
|
+
const fragment = buildLinkedTextFragment(node.nodeValue || '');
|
|
570
|
+
if (fragment) node.parentNode.replaceChild(fragment, node);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function shouldRewriteLocalAssetUrl(rawValue) {
|
|
575
|
+
const value = String(rawValue || '').trim();
|
|
576
|
+
if (!value) return false;
|
|
577
|
+
const lowered = value.toLowerCase();
|
|
578
|
+
const normalizedBase = String(BASE || '').trim().replace(/\/+$/, '').toLowerCase();
|
|
579
|
+
if (
|
|
580
|
+
lowered.startsWith('/api/')
|
|
581
|
+
|| lowered.startsWith('api/')
|
|
582
|
+
|| (normalizedBase && lowered.startsWith(`${normalizedBase}/api/`))
|
|
583
|
+
) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (
|
|
587
|
+
lowered.startsWith('http://')
|
|
588
|
+
|| lowered.startsWith('https://')
|
|
589
|
+
|| lowered.startsWith('data:')
|
|
590
|
+
|| lowered.startsWith('blob:')
|
|
591
|
+
|| lowered.startsWith('#')
|
|
592
|
+
) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function resolveLocalAssetUrl(rawValue, { preferDrive = false } = {}) {
|
|
599
|
+
const value = String(rawValue || '').trim();
|
|
600
|
+
if (!shouldRewriteLocalAssetUrl(value)) return '';
|
|
601
|
+
const looksAbsolute = /^[\\/]/.test(value) || /^[A-Za-z]:[\\/]/.test(value);
|
|
602
|
+
if (preferDrive || looksAbsolute) {
|
|
603
|
+
const drivePath = normalizeDriveBrowserPath(value);
|
|
604
|
+
if (drivePath) return driveFileHrefFromPath(drivePath);
|
|
605
|
+
}
|
|
606
|
+
const workspacePath = normalizeWorkspaceMentionPath(value);
|
|
607
|
+
if (workspacePath) return workspaceFileHrefFromPath(workspacePath);
|
|
608
|
+
if (!looksAbsolute) {
|
|
609
|
+
const drivePath = normalizeDriveBrowserPath(value);
|
|
610
|
+
if (drivePath) return driveFileHrefFromPath(drivePath);
|
|
611
|
+
}
|
|
612
|
+
return '';
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function rewriteLocalAssetUrlsInNode(root, options = {}) {
|
|
616
|
+
if (!(root instanceof Element)) return;
|
|
617
|
+
const preferDrive = options?.preferDrive === true;
|
|
618
|
+
const rewriteAnchors = options?.rewriteAnchors !== false;
|
|
619
|
+
const images = Array.from(root.querySelectorAll('img[src]'));
|
|
620
|
+
for (const img of images) {
|
|
621
|
+
const rawSrc = img.getAttribute('src') || '';
|
|
622
|
+
const nextSrc = resolveLocalAssetUrl(rawSrc, { preferDrive });
|
|
623
|
+
if (nextSrc) img.setAttribute('src', nextSrc);
|
|
624
|
+
}
|
|
625
|
+
if (!rewriteAnchors) return;
|
|
626
|
+
const anchors = Array.from(root.querySelectorAll('a[href]'));
|
|
627
|
+
for (const anchor of anchors) {
|
|
628
|
+
const rawHref = anchor.getAttribute('href') || '';
|
|
629
|
+
const nextHref = resolveLocalAssetUrl(rawHref, { preferDrive });
|
|
630
|
+
if (!nextHref) continue;
|
|
631
|
+
anchor.setAttribute('href', nextHref);
|
|
632
|
+
anchor.setAttribute('target', '_blank');
|
|
633
|
+
anchor.setAttribute('rel', 'noopener noreferrer');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function renderLinkedPlainText(text) {
|
|
638
|
+
const source = String(text || '');
|
|
639
|
+
const regex = workspaceMentionRegex();
|
|
640
|
+
let html = '';
|
|
641
|
+
let lastIndex = 0;
|
|
642
|
+
let match = null;
|
|
643
|
+
while ((match = regex.exec(source)) !== null) {
|
|
644
|
+
const mention = match[0];
|
|
645
|
+
const workspacePath = workspaceFilePathFromText(mention);
|
|
646
|
+
const href = workspacePath ? workspaceFileHrefFromPath(workspacePath) : '';
|
|
647
|
+
html += escHtml(source.slice(lastIndex, match.index));
|
|
648
|
+
if (workspacePath && href) {
|
|
649
|
+
html += `<a href="${escHtml(href)}" class="workspace-file-link" data-workspace-path="${escHtml(workspacePath)}" target="_blank" rel="noopener noreferrer">${escHtml(mention)}</a>`;
|
|
650
|
+
} else {
|
|
651
|
+
html += escHtml(mention);
|
|
652
|
+
}
|
|
653
|
+
lastIndex = match.index + mention.length;
|
|
654
|
+
}
|
|
655
|
+
html += escHtml(source.slice(lastIndex));
|
|
656
|
+
return html;
|
|
657
|
+
}
|