codex-overleaf-link 1.8.6 → 1.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/README.md +45 -21
- package/bin/codex-overleaf-link.mjs +58 -0
- package/extension/assets/icons/codex-overleaf-dialog-icon.png +0 -0
- package/extension/assets/icons/codex-overleaf-icon.png +0 -0
- package/extension/assets/icons/icon128.png +0 -0
- package/extension/assets/icons/icon16.png +0 -0
- package/extension/assets/icons/icon32.png +0 -0
- package/extension/assets/icons/icon48.png +0 -0
- package/extension/bootstrap/background.js +404 -0
- package/extension/bootstrap/manifest.template.json +51 -0
- package/extension/bootstrap/popup.css +26 -0
- package/extension/bootstrap/popup.html +38 -0
- package/extension/bootstrap/popup.js +44 -0
- package/extension/bootstrap/runtimeContext.js +2 -0
- package/extension/runtime-manifest.json +60 -0
- package/extension/src/background.js +532 -0
- package/extension/src/content/applyResultFormatters.js +387 -0
- package/extension/src/content/composerAttachments.js +522 -0
- package/extension/src/content/composerPanel.js +254 -0
- package/extension/src/content/contentRuntime.js +8895 -0
- package/extension/src/content/contextTray.js +686 -0
- package/extension/src/content/diagnosticsController.js +659 -0
- package/extension/src/content/diagnosticsPanel.js +342 -0
- package/extension/src/content/diffReviewPanel.js +797 -0
- package/extension/src/content/localSkillsPanel.js +280 -0
- package/extension/src/content/markdownText.js +606 -0
- package/extension/src/content/mirrorHealth.js +92 -0
- package/extension/src/content/modelPicker.js +489 -0
- package/extension/src/content/nativeChannel.js +62 -0
- package/extension/src/content/otWarmMirror.js +964 -0
- package/extension/src/content/otWarmMirrorController.js +174 -0
- package/extension/src/content/panelMaintenance.js +367 -0
- package/extension/src/content/panelRenderer.js +253 -0
- package/extension/src/content/recentProjects.js +1063 -0
- package/extension/src/content/reviewHunks.js +247 -0
- package/extension/src/content/runController.js +242 -0
- package/extension/src/content/runTimelineView.js +1126 -0
- package/extension/src/content/sessionManager.js +487 -0
- package/extension/src/content/sessionPanel.js +263 -0
- package/extension/src/content/settingsPanel.js +527 -0
- package/extension/src/content/themeController.js +91 -0
- package/extension/src/content/updateIdle.js +67 -0
- package/extension/src/content/writebackController.js +300 -0
- package/extension/src/content/writebackOrchestrator.js +850 -0
- package/extension/src/contentScript.js +8 -0
- package/extension/src/page/compileBridge.js +460 -0
- package/extension/src/page/overleafCapabilities.js +100 -0
- package/extension/src/page/overleafEditor.js +282 -0
- package/extension/src/page/overleafProjectSnapshot.js +234 -0
- package/extension/src/page/overleafRealtimeObserver.js +491 -0
- package/extension/src/page/pageBridgeCapability.js +71 -0
- package/extension/src/page/snapshotRouter.js +948 -0
- package/extension/src/page/trackedChangesLifecycle.js +1506 -0
- package/extension/src/page/treeOperations.js +1584 -0
- package/extension/src/page/writeGuard.js +142 -0
- package/extension/src/page/writebackRouter.js +1934 -0
- package/extension/src/pageBridge.js +2224 -0
- package/extension/src/popup.js +247 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/styles/panel.css +4048 -0
- package/native-host/src/index.js +9 -2
- package/native-host/src/managedInstall.js +393 -0
- package/native-host/src/managedLauncherRuntime.js +71 -0
- package/native-host/src/nativeHostPlatform.js +16 -0
- package/native-host/src/runtimeInstaller.js +6 -1
- package/native-host/src/taskRunnerRuntime.js +8 -0
- package/native-host/src/updateArchive.js +253 -0
- package/native-host/src/updateManager.js +548 -0
- package/native-host/src/updateTrust.js +156 -0
- package/package.json +11 -2
- package/scripts/install-managed.mjs +64 -0
- package/scripts/uninstall-managed.mjs +48 -0
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
(function initCodexOverleafTreeOperations(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafTreeOperations = factory();
|
|
6
|
+
}
|
|
7
|
+
})(typeof window !== 'undefined' ? window : globalThis, function treeOperationsFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const TEXT_PATH_EXTENSION_PATTERN = '(?:tex|bib|sty|cls|bst|bbx|cbx|lbx|cfg|def|clo|ist|txt|md|latex)';
|
|
11
|
+
|
|
12
|
+
function create({
|
|
13
|
+
window: windowDep,
|
|
14
|
+
document: documentDep,
|
|
15
|
+
normalizePath = fallbackNormalizeSafeProjectPath,
|
|
16
|
+
readActiveEditorText = () => '',
|
|
17
|
+
getActiveEditorIdentity = () => null,
|
|
18
|
+
activeEditorIdentityChanged = () => false,
|
|
19
|
+
getActiveFilePathFromEditorStore: readActiveFilePathFromEditorStore = () => ''
|
|
20
|
+
} = {}) {
|
|
21
|
+
const window = windowDep || (typeof globalThis !== 'undefined' ? globalThis : {});
|
|
22
|
+
const document = documentDep || window.document || {};
|
|
23
|
+
const normalizeSafeProjectPath = normalizePath;
|
|
24
|
+
const NodeCtor = window.Node || (typeof Node !== 'undefined' ? Node : null);
|
|
25
|
+
const EventTargetCtor = window.EventTarget || (typeof EventTarget !== 'undefined' ? EventTarget : null);
|
|
26
|
+
let internalDocPathByIdCache = null;
|
|
27
|
+
let internalDocPathByIdCacheTime = 0;
|
|
28
|
+
let domProjectPathCache = null;
|
|
29
|
+
let domProjectPathCacheTime = 0;
|
|
30
|
+
let lastFileTreeSelection = null;
|
|
31
|
+
|
|
32
|
+
function collectElements(selector, limit) {
|
|
33
|
+
const result = [];
|
|
34
|
+
const roots = collectSearchRoots();
|
|
35
|
+
for (let rootIndex = 0; rootIndex < roots.length && result.length < limit; rootIndex += 1) {
|
|
36
|
+
let nodes = [];
|
|
37
|
+
try {
|
|
38
|
+
nodes = Array.from(roots[rootIndex].querySelectorAll(selector));
|
|
39
|
+
} catch (_error) {
|
|
40
|
+
nodes = [];
|
|
41
|
+
}
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
result.push(node);
|
|
44
|
+
if (result.length >= limit) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectSearchRoots() {
|
|
53
|
+
const roots = [];
|
|
54
|
+
const seenRoots = new Set();
|
|
55
|
+
const queue = [document];
|
|
56
|
+
|
|
57
|
+
for (let index = 0; index < queue.length && roots.length < 80; index += 1) {
|
|
58
|
+
const root = queue[index];
|
|
59
|
+
if (!root || seenRoots.has(root)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
seenRoots.add(root);
|
|
63
|
+
roots.push(root);
|
|
64
|
+
|
|
65
|
+
let allNodes = [];
|
|
66
|
+
try {
|
|
67
|
+
allNodes = Array.from(root.querySelectorAll('*')).slice(0, 4000);
|
|
68
|
+
} catch (_error) {
|
|
69
|
+
allNodes = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const node of allNodes) {
|
|
73
|
+
if (node.shadowRoot && !seenRoots.has(node.shadowRoot)) {
|
|
74
|
+
queue.push(node.shadowRoot);
|
|
75
|
+
}
|
|
76
|
+
if (node.tagName === 'IFRAME') {
|
|
77
|
+
try {
|
|
78
|
+
if (node.contentDocument && !seenRoots.has(node.contentDocument)) {
|
|
79
|
+
queue.push(node.contentDocument);
|
|
80
|
+
}
|
|
81
|
+
} catch (_error) {
|
|
82
|
+
// Cross-origin frames cannot be inspected from the page context.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return roots;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function uniqueNodes(nodes) {
|
|
92
|
+
return Array.from(new Set(nodes));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getProjectId() {
|
|
96
|
+
const match = window.location.pathname.match(/\/project\/([^/]+)/);
|
|
97
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getActiveFilePath() {
|
|
101
|
+
const editorStorePath = getEditorStoreFilePath({ disambiguateBasename: true });
|
|
102
|
+
if (editorStorePath && window.CodexOverleafProjectFiles.isTextProjectPath(editorStorePath) && !hasMultipleTextPathExtensions(editorStorePath)) {
|
|
103
|
+
return editorStorePath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const breadcrumbPath = readActiveFilePathFromEditorBreadcrumb();
|
|
107
|
+
if (breadcrumbPath) {
|
|
108
|
+
return breadcrumbPath;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selectors = [
|
|
112
|
+
'[aria-selected="true"][role="treeitem"]',
|
|
113
|
+
'[aria-selected="true"][role="row"]',
|
|
114
|
+
'.selected[role="treeitem"]',
|
|
115
|
+
'.selected[role="row"]',
|
|
116
|
+
'.file-tree .selected',
|
|
117
|
+
'.project-tree .selected'
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const candidates = [];
|
|
121
|
+
for (const selector of selectors) {
|
|
122
|
+
const nodes = Array.from(document.querySelectorAll?.(selector) || []);
|
|
123
|
+
for (const node of nodes) {
|
|
124
|
+
const path = node ? readProjectPathFromNode(node) : '';
|
|
125
|
+
if (isActiveFilePathCandidate(path, node)) {
|
|
126
|
+
candidates.push({
|
|
127
|
+
node,
|
|
128
|
+
path,
|
|
129
|
+
score: scoreActiveFilePathCandidate(path, node)
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (candidates.length) {
|
|
136
|
+
candidates.sort((left, right) => right.score - left.score);
|
|
137
|
+
return candidates[0].path;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getEditorStoreFilePath(options = {}) {
|
|
144
|
+
return normalizeSafeProjectPath(readActiveFilePathFromEditorStore(options));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function scoreActiveFilePathCandidate(path, node) {
|
|
148
|
+
let score = 0;
|
|
149
|
+
const selected = getSettledRecentFileTreeSelection();
|
|
150
|
+
if (selected?.path === path) {
|
|
151
|
+
score += 1000;
|
|
152
|
+
}
|
|
153
|
+
if (node?.getAttribute?.('aria-selected') === 'true') {
|
|
154
|
+
score += 100;
|
|
155
|
+
}
|
|
156
|
+
const className = normalizeDomText(node?.className || '');
|
|
157
|
+
if (/\b(?:active|current|selected)\b/i.test(className)) {
|
|
158
|
+
score += 50;
|
|
159
|
+
}
|
|
160
|
+
if (isVisibleNode(node)) {
|
|
161
|
+
score += 10;
|
|
162
|
+
}
|
|
163
|
+
return score;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getSettledRecentFileTreeSelection() {
|
|
167
|
+
if (!lastFileTreeSelection?.path) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const elapsedMs = Date.now() - lastFileTreeSelection.time;
|
|
171
|
+
if (elapsedMs < 0 || elapsedMs > 60000) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (lastFileTreeSelection.editorIdentityBefore && activeEditorIdentityChanged(lastFileTreeSelection.editorIdentityBefore)) {
|
|
175
|
+
return lastFileTreeSelection;
|
|
176
|
+
}
|
|
177
|
+
const beforeSignature = lastFileTreeSelection.editorTextSignatureBefore || '';
|
|
178
|
+
if (beforeSignature && contentSignature(readActiveEditorText()) !== beforeSignature) {
|
|
179
|
+
return lastFileTreeSelection;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isVisibleNode(node) {
|
|
185
|
+
if (!node) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (typeof node.getClientRects === 'function' && node.getClientRects().length > 0) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return node.offsetParent != null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function openFileByPath(filePath, options = {}) {
|
|
195
|
+
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
196
|
+
if (!normalizedPath) {
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
reason: 'Invalid file path'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.force !== true && getActiveFilePath() === normalizedPath) {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
method: 'already-active'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const diagnostics = {
|
|
211
|
+
path: normalizedPath,
|
|
212
|
+
initialActivePath: getActiveFilePath(),
|
|
213
|
+
initialDomNodeFound: false,
|
|
214
|
+
ancestorFolders: [],
|
|
215
|
+
postExpandDomNodeFound: false,
|
|
216
|
+
domClickActivePath: '',
|
|
217
|
+
managerMethods: []
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
let node = findFileTreeNode(normalizedPath, { invalidateCache: true });
|
|
221
|
+
diagnostics.initialDomNodeFound = Boolean(node);
|
|
222
|
+
if (!node && normalizedPath.includes('/')) {
|
|
223
|
+
diagnostics.ancestorFolders = await ensureAncestorFoldersVisible(normalizedPath);
|
|
224
|
+
node = findFileTreeNode(normalizedPath, { invalidateCache: true });
|
|
225
|
+
diagnostics.postExpandDomNodeFound = Boolean(node);
|
|
226
|
+
}
|
|
227
|
+
if (node) {
|
|
228
|
+
dispatchFileTreeOpenClick(node);
|
|
229
|
+
await delay(250);
|
|
230
|
+
const active = await waitForActiveFile(normalizedPath, 5000);
|
|
231
|
+
diagnostics.domClickActivePath = getActiveFilePath();
|
|
232
|
+
if (active) {
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
method: 'dom-click'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const manager = findFileTreeManager();
|
|
241
|
+
const openMethods = ['openDoc', 'openFile', 'selectFile', 'selectEntity'];
|
|
242
|
+
const openArgs = buildManagerOpenArgs(normalizedPath);
|
|
243
|
+
|
|
244
|
+
for (const methodName of openMethods) {
|
|
245
|
+
const method = manager?.[methodName];
|
|
246
|
+
if (typeof method !== 'function') {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const openArg of openArgs) {
|
|
251
|
+
const methodDiagnostics = {
|
|
252
|
+
method: methodName,
|
|
253
|
+
argType: openArg.type,
|
|
254
|
+
ok: false,
|
|
255
|
+
activePath: ''
|
|
256
|
+
};
|
|
257
|
+
try {
|
|
258
|
+
await method.call(manager, openArg.value);
|
|
259
|
+
const active = await waitForActiveFile(normalizedPath, 5000);
|
|
260
|
+
methodDiagnostics.ok = active;
|
|
261
|
+
methodDiagnostics.activePath = getActiveFilePath();
|
|
262
|
+
diagnostics.managerMethods.push(methodDiagnostics);
|
|
263
|
+
if (active) {
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
method: `fileTreeManager.${methodName}:${openArg.type}`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
methodDiagnostics.error = error?.message || String(error || '');
|
|
271
|
+
methodDiagnostics.activePath = getActiveFilePath();
|
|
272
|
+
diagnostics.managerMethods.push(methodDiagnostics);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
reason: `Could not open ${normalizedPath}; ${formatOpenDiagnostics(diagnostics)}`,
|
|
280
|
+
diagnostics
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildManagerOpenArgs(filePath) {
|
|
285
|
+
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
286
|
+
const record = collectDocRecords({ includeWindowGlobals: true }).find(item => item.path === normalizedPath);
|
|
287
|
+
const args = [
|
|
288
|
+
{ type: 'path', value: normalizedPath }
|
|
289
|
+
];
|
|
290
|
+
if (record?.id) {
|
|
291
|
+
args.unshift({ type: 'doc-id', value: record.id });
|
|
292
|
+
args.push({ type: 'doc-record', value: { ...record } });
|
|
293
|
+
args.push({ type: 'doc-record-with-path', value: { ...record, path: normalizedPath, name: normalizedPath.split('/').pop() || normalizedPath } });
|
|
294
|
+
}
|
|
295
|
+
return args;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatOpenDiagnostics(diagnostics = {}) {
|
|
299
|
+
const folders = (diagnostics.ancestorFolders || [])
|
|
300
|
+
.map(item => `${item.path}:found=${item.found ? 'yes' : 'no'},expanded=${item.expandedBefore ? 'yes' : 'no'},visible=${item.visibleAfter ? 'yes' : 'no'}`)
|
|
301
|
+
.join('|') || 'none';
|
|
302
|
+
const managers = (diagnostics.managerMethods || [])
|
|
303
|
+
.map(item => `${item.method}/${item.argType}:ok=${item.ok ? 'yes' : 'no'},active=${item.activePath || 'unknown'}${item.error ? ',error=' + item.error : ''}`)
|
|
304
|
+
.slice(0, 8)
|
|
305
|
+
.join('|') || 'none';
|
|
306
|
+
return `open diagnostics initial=${diagnostics.initialActivePath || 'unknown'}, initialNode=${diagnostics.initialDomNodeFound ? 'yes' : 'no'}, folders=${folders}, postExpandNode=${diagnostics.postExpandDomNodeFound ? 'yes' : 'no'}, domClickActive=${diagnostics.domClickActivePath || 'unknown'}, manager=${managers}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function dispatchFileTreeOpenClick(node) {
|
|
310
|
+
const path = readProjectPathFromNode(node);
|
|
311
|
+
if (isActiveFilePathCandidate(path, node)) {
|
|
312
|
+
recordFileTreeSelection(path, 'script');
|
|
313
|
+
}
|
|
314
|
+
const target = findFileTreeOpenClickTarget(node) || node;
|
|
315
|
+
dispatchActivationSequence(target);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function dispatchActivationSequence(node) {
|
|
319
|
+
if (!node) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
node.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
324
|
+
} catch (_error) {
|
|
325
|
+
// Ignore scroll failures in synthetic DOM test harnesses.
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
node.focus?.({ preventScroll: true });
|
|
329
|
+
} catch (_error) {
|
|
330
|
+
try {
|
|
331
|
+
node.focus?.();
|
|
332
|
+
} catch (_nestedError) {
|
|
333
|
+
// Ignore focus failures.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (const type of ['pointerdown', 'mousedown', 'mouseup', 'click']) {
|
|
337
|
+
try {
|
|
338
|
+
node.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
339
|
+
} catch (_error) {
|
|
340
|
+
// Keep trying the rest of the activation sequence.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function findFileTreeOpenClickTarget(node) {
|
|
346
|
+
const selectors = [
|
|
347
|
+
'.file-tree-entity-details',
|
|
348
|
+
'.item-name',
|
|
349
|
+
'.entity-name',
|
|
350
|
+
'.file-tree-entity-button',
|
|
351
|
+
'[role="button"]',
|
|
352
|
+
'button'
|
|
353
|
+
];
|
|
354
|
+
for (const selector of selectors) {
|
|
355
|
+
let candidates = [];
|
|
356
|
+
try {
|
|
357
|
+
candidates = Array.from(node.querySelectorAll?.(selector) || []);
|
|
358
|
+
} catch (_error) {
|
|
359
|
+
candidates = [];
|
|
360
|
+
}
|
|
361
|
+
const candidate = candidates.find(item => !isFileTreeMenuControl(item));
|
|
362
|
+
if (candidate) {
|
|
363
|
+
return candidate;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return node;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isFileTreeMenuControl(node) {
|
|
370
|
+
if (!node || typeof node.getAttribute !== 'function') {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const role = node.getAttribute('role') || '';
|
|
374
|
+
if (/^(?:treeitem|row)$/i.test(role)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
const signal = [
|
|
378
|
+
node?.id || '',
|
|
379
|
+
node?.className || '',
|
|
380
|
+
node?.getAttribute?.('aria-label') || '',
|
|
381
|
+
node?.getAttribute?.('title') || ''
|
|
382
|
+
].join(' ');
|
|
383
|
+
if (/\b(?:menu|more_vert|action)\b/i.test(signal)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const text = normalizeDomText(node.textContent || '');
|
|
387
|
+
return /^(?:more_vert|menu|action menu)$/i.test(text);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function installFileTreeSelectionTracking() {
|
|
391
|
+
const handlerKey = '__codexOverleafFileTreeSelectionHandler';
|
|
392
|
+
if (typeof window.removeEventListener === 'function' && typeof window[handlerKey] === 'function') {
|
|
393
|
+
window.removeEventListener('click', window[handlerKey], true);
|
|
394
|
+
}
|
|
395
|
+
const handler = event => {
|
|
396
|
+
const path = readFileTreePathFromClickEvent(event);
|
|
397
|
+
if (path) {
|
|
398
|
+
recordFileTreeSelection(path, event?.isTrusted === true ? 'user' : 'script');
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
window[handlerKey] = handler;
|
|
402
|
+
if (typeof window.addEventListener === 'function') {
|
|
403
|
+
window.addEventListener('click', handler, true);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function readFileTreePathFromClickEvent(event) {
|
|
408
|
+
const pathNodes = typeof event?.composedPath === 'function'
|
|
409
|
+
? event.composedPath()
|
|
410
|
+
: [];
|
|
411
|
+
for (const node of pathNodes) {
|
|
412
|
+
if (!node || typeof node.getAttribute !== 'function') {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const path = readProjectPathFromNode(node);
|
|
416
|
+
if (isActiveFilePathCandidate(path, node)) {
|
|
417
|
+
return path;
|
|
418
|
+
}
|
|
419
|
+
if (isFileTreeMenuControl(node)) {
|
|
420
|
+
return '';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const targetNode = event?.target;
|
|
425
|
+
const treeNode = targetNode?.closest?.('[role="treeitem"], [role="row"], .file-tree li, .project-tree li');
|
|
426
|
+
if (treeNode && !isFileTreeMenuControl(targetNode)) {
|
|
427
|
+
const path = readProjectPathFromNode(treeNode);
|
|
428
|
+
if (isActiveFilePathCandidate(path, treeNode)) {
|
|
429
|
+
return path;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function recordFileTreeSelection(path, source) {
|
|
436
|
+
const normalizedPath = normalizeSafeProjectPath(path);
|
|
437
|
+
if (!window.CodexOverleafProjectFiles.isTextProjectPath(normalizedPath)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
lastFileTreeSelection = {
|
|
441
|
+
path: normalizedPath,
|
|
442
|
+
source,
|
|
443
|
+
time: Date.now(),
|
|
444
|
+
editorIdentityBefore: getActiveEditorIdentity(),
|
|
445
|
+
editorTextSignatureBefore: contentSignature(readActiveEditorText())
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getRecentFileTreeSelectionPath() {
|
|
450
|
+
return getSettledRecentFileTreeSelection()?.path || '';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function readActiveFilePathFromEditorBreadcrumb() {
|
|
454
|
+
const roots = document.querySelectorAll?.(
|
|
455
|
+
'.ol-cm-breadcrumbs, [data-testid="editor-breadcrumbs"], .editor-breadcrumbs'
|
|
456
|
+
) || [];
|
|
457
|
+
for (const root of roots) {
|
|
458
|
+
const path = readProjectPathFromBreadcrumbRoot(root);
|
|
459
|
+
if (path) {
|
|
460
|
+
return path;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return '';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function readProjectPathFromBreadcrumbRoot(root) {
|
|
467
|
+
const rawParts = Array.from(root.querySelectorAll?.('*') || [])
|
|
468
|
+
.map(node => normalizeDomText(node.innerText || node.textContent || ''))
|
|
469
|
+
.filter(Boolean);
|
|
470
|
+
const parts = [];
|
|
471
|
+
for (const part of rawParts) {
|
|
472
|
+
if (isBreadcrumbIconText(part)) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (parts[parts.length - 1] === part) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
parts.push(part);
|
|
479
|
+
}
|
|
480
|
+
if (!parts.length) {
|
|
481
|
+
return '';
|
|
482
|
+
}
|
|
483
|
+
for (let start = 0; start < parts.length; start += 1) {
|
|
484
|
+
const candidate = normalizeSafeProjectPath(parts.slice(start).join('/'));
|
|
485
|
+
if (window.CodexOverleafProjectFiles.isTextProjectPath(candidate) && !hasMultipleTextPathExtensions(candidate)) {
|
|
486
|
+
return candidate;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return '';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function isBreadcrumbIconText(text) {
|
|
493
|
+
return /^(?:chevron_right|description|book_5|image|insert_drive_file|article|folder|expand_more|keyboard_arrow_down)$/i
|
|
494
|
+
.test(String(text || '').trim());
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function findFileTreeNode(filePath, options = {}) {
|
|
498
|
+
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
499
|
+
if (!normalizedPath) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (normalizedPath.includes('/') && options.invalidateCache === true) {
|
|
503
|
+
invalidateDomProjectPathCache();
|
|
504
|
+
}
|
|
505
|
+
const fileName = normalizedPath.split('/').pop();
|
|
506
|
+
const allowBasenameFallback = !normalizedPath.includes('/');
|
|
507
|
+
const basenameMatches = [];
|
|
508
|
+
let firstPathMatch = null;
|
|
509
|
+
const candidates = document.querySelectorAll('[role="treeitem"], [role="row"], .file-tree *, .project-tree *');
|
|
510
|
+
for (const node of candidates) {
|
|
511
|
+
const path = readProjectPathFromNode(node);
|
|
512
|
+
const text = normalizeDomText(node.textContent || '');
|
|
513
|
+
if (path === normalizedPath || text === normalizedPath) {
|
|
514
|
+
const role = node.getAttribute?.('role') || '';
|
|
515
|
+
if (/^(treeitem|row)$/i.test(role)) {
|
|
516
|
+
return node;
|
|
517
|
+
}
|
|
518
|
+
if (!firstPathMatch) {
|
|
519
|
+
firstPathMatch = node;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (allowBasenameFallback && (path === fileName || text === fileName)) {
|
|
523
|
+
basenameMatches.push(node);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (firstPathMatch) {
|
|
527
|
+
return firstPathMatch;
|
|
528
|
+
}
|
|
529
|
+
if (basenameMatches.length === 1) {
|
|
530
|
+
return basenameMatches[0];
|
|
531
|
+
}
|
|
532
|
+
if (normalizedPath.includes('/')) {
|
|
533
|
+
return findFileTreeNodeByDocId(normalizedPath, fileName);
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function findFileTreeNodeByDocId(filePath, fileName) {
|
|
539
|
+
const record = collectDocRecords({ includeWindowGlobals: true }).find(item => item.path === filePath);
|
|
540
|
+
if (!record?.id) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const idSelectors = [
|
|
544
|
+
`[data-entity-id="${record.id}"]`,
|
|
545
|
+
`[data-doc-id="${record.id}"]`,
|
|
546
|
+
`[data-file-id="${record.id}"]`,
|
|
547
|
+
`[data-id="${record.id}"]`
|
|
548
|
+
];
|
|
549
|
+
for (const selector of idSelectors) {
|
|
550
|
+
let nodes = [];
|
|
551
|
+
try {
|
|
552
|
+
nodes = Array.from(document.querySelectorAll(selector));
|
|
553
|
+
} catch (_error) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
for (const node of nodes) {
|
|
557
|
+
const role = node.getAttribute?.('role') || '';
|
|
558
|
+
if (/^(treeitem|row)$/i.test(role)) {
|
|
559
|
+
return node;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (nodes.length) {
|
|
563
|
+
return nodes[0];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const allNodes = document.querySelectorAll('[role="treeitem"], [role="row"], .file-tree *, .project-tree *');
|
|
567
|
+
for (const node of allNodes) {
|
|
568
|
+
const nodeId = readDomDocId(node);
|
|
569
|
+
if (nodeId === record.id) {
|
|
570
|
+
return node;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function ensureAncestorFoldersVisible(filePath) {
|
|
577
|
+
const parts = String(filePath || '').split('/').filter(Boolean);
|
|
578
|
+
const diagnostics = [];
|
|
579
|
+
if (parts.length < 2) {
|
|
580
|
+
return diagnostics;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let folderPath = '';
|
|
584
|
+
for (const folderName of parts.slice(0, -1)) {
|
|
585
|
+
folderPath = folderPath ? `${folderPath}/${folderName}` : folderName;
|
|
586
|
+
const folderNode = findFolderTreeNode(folderPath);
|
|
587
|
+
const entry = {
|
|
588
|
+
path: folderPath,
|
|
589
|
+
found: Boolean(folderNode),
|
|
590
|
+
expandedBefore: folderNode ? isFolderExpanded(folderNode) : false,
|
|
591
|
+
visibleAfter: false
|
|
592
|
+
};
|
|
593
|
+
if (!folderNode || isFolderExpanded(folderNode)) {
|
|
594
|
+
entry.visibleAfter = hasVisibleFolderDescendant(folderPath);
|
|
595
|
+
diagnostics.push(entry);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
dispatchFolderExpandClick(folderNode);
|
|
599
|
+
invalidateDomProjectPathCache();
|
|
600
|
+
const visible = await waitForFolderVisible(folderPath, 2000);
|
|
601
|
+
entry.visibleAfter = visible;
|
|
602
|
+
if (!visible) {
|
|
603
|
+
const refreshedFolderNode = findFolderTreeNode(folderPath) || folderNode;
|
|
604
|
+
if (refreshedFolderNode && refreshedFolderNode !== folderNode) {
|
|
605
|
+
dispatchFolderExpandClick(refreshedFolderNode);
|
|
606
|
+
} else {
|
|
607
|
+
dispatchActivationSequence(folderNode);
|
|
608
|
+
}
|
|
609
|
+
invalidateDomProjectPathCache();
|
|
610
|
+
entry.visibleAfter = await waitForFolderVisible(folderPath, 2500);
|
|
611
|
+
}
|
|
612
|
+
diagnostics.push(entry);
|
|
613
|
+
}
|
|
614
|
+
return diagnostics;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function findFolderTreeNode(folderPath) {
|
|
618
|
+
const normalizedPath = normalizeSafeProjectPath(folderPath);
|
|
619
|
+
if (!normalizedPath || window.CodexOverleafProjectFiles.isTextProjectPath(normalizedPath)) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const folderName = normalizedPath.split('/').pop() || '';
|
|
623
|
+
const matches = [];
|
|
624
|
+
const candidates = document.querySelectorAll('[role="treeitem"], [role="row"], .file-tree li, .project-tree li');
|
|
625
|
+
for (const node of candidates) {
|
|
626
|
+
const name = readFolderNameFromNode(node);
|
|
627
|
+
if (name !== folderName) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const inferredPath = inferFolderPathFromTreeAncestors(node, name) || name;
|
|
631
|
+
if (inferredPath === normalizedPath) {
|
|
632
|
+
return node;
|
|
633
|
+
}
|
|
634
|
+
if (name === folderName) {
|
|
635
|
+
matches.push(node);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return matches.length === 1 ? matches[0] : null;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function isFolderExpanded(node) {
|
|
642
|
+
const expanded = node?.getAttribute?.('aria-expanded');
|
|
643
|
+
if (expanded === 'true') {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
if (expanded === 'false') {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
const signal = normalizeDomText(node?.textContent || '');
|
|
650
|
+
return /expand_more|folder_open/i.test(signal);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function dispatchFolderExpandClick(node) {
|
|
654
|
+
const target = findFolderExpandClickTarget(node) || node;
|
|
655
|
+
dispatchActivationSequence(target);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function findFolderExpandClickTarget(node) {
|
|
659
|
+
const selectors = [
|
|
660
|
+
'.file-tree-entity-button',
|
|
661
|
+
'.item-name',
|
|
662
|
+
'.entity-name',
|
|
663
|
+
'[role="button"]',
|
|
664
|
+
'button'
|
|
665
|
+
];
|
|
666
|
+
for (const selector of selectors) {
|
|
667
|
+
let candidates = [];
|
|
668
|
+
try {
|
|
669
|
+
candidates = Array.from(node.querySelectorAll?.(selector) || []);
|
|
670
|
+
} catch (_error) {
|
|
671
|
+
candidates = [];
|
|
672
|
+
}
|
|
673
|
+
const candidate = candidates.find(item => !isFileTreeMenuControl(item));
|
|
674
|
+
if (candidate) {
|
|
675
|
+
return candidate;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return node;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function waitForFolderVisible(folderPath, timeoutMs) {
|
|
682
|
+
const deadline = Date.now() + timeoutMs;
|
|
683
|
+
while (Date.now() < deadline) {
|
|
684
|
+
const folderNode = findFolderTreeNode(folderPath);
|
|
685
|
+
if (!folderNode || isFolderExpanded(folderNode) || hasVisibleFolderDescendant(folderPath)) {
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
await delay(100);
|
|
689
|
+
}
|
|
690
|
+
return hasVisibleFolderDescendant(folderPath);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function hasVisibleFolderDescendant(folderPath) {
|
|
694
|
+
const prefix = `${folderPath}/`;
|
|
695
|
+
return collectDomProjectPaths().some(path => path.startsWith(prefix));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function projectPathExists(filePath) {
|
|
699
|
+
if (!filePath) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
703
|
+
if (!normalizedPath) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
if (getActiveFilePath() === normalizedPath) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
if (findFileTreeNode(normalizedPath)) {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
return collectDocRecords().some(record => record.path === normalizedPath);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function collectProjectTextPaths(activePath) {
|
|
716
|
+
const rawPaths = [
|
|
717
|
+
activePath,
|
|
718
|
+
...collectDocRecords().map(record => record.path),
|
|
719
|
+
...collectInternalProjectPaths(),
|
|
720
|
+
...collectDomProjectPaths()
|
|
721
|
+
];
|
|
722
|
+
return window.CodexOverleafProjectFiles.collectUniqueTextPaths(rawPaths, 80);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function collectDocRecords(options = {}) {
|
|
726
|
+
const seen = new Set();
|
|
727
|
+
const records = [];
|
|
728
|
+
for (const record of [...collectInternalDocRecords(options), ...collectDomDocRecords()]) {
|
|
729
|
+
if (!record?.id || !window.CodexOverleafProjectFiles.isTextProjectPath(record.path)) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const key = `${record.path}:${record.id}`;
|
|
733
|
+
if (seen.has(key)) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
seen.add(key);
|
|
737
|
+
records.push(record);
|
|
738
|
+
if (records.length >= 120) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return records;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function collectInternalDocRecords(options = {}) {
|
|
746
|
+
const records = [];
|
|
747
|
+
const seenObjects = new WeakSet();
|
|
748
|
+
const seenRecords = new Set();
|
|
749
|
+
const roots = collectInternalRoots(options);
|
|
750
|
+
|
|
751
|
+
for (const root of roots) {
|
|
752
|
+
walkInternalDocTree(root, '', 0);
|
|
753
|
+
if (records.length >= 120) {
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return records;
|
|
759
|
+
|
|
760
|
+
function walkInternalDocTree(value, folderPath, depth) {
|
|
761
|
+
if (!value || depth > 7 || records.length >= 120 || typeof value !== 'object') {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (isDomLikeObject(value) || seenObjects.has(value)) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
seenObjects.add(value);
|
|
768
|
+
|
|
769
|
+
if (Array.isArray(value)) {
|
|
770
|
+
for (const item of value.slice(0, 250)) {
|
|
771
|
+
walkInternalDocTree(item, folderPath, depth + 1);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const ownName = readObjectPathName(value);
|
|
777
|
+
const ownId = readObjectDocId(value);
|
|
778
|
+
const normalizedOwnName = normalizeSafeProjectPath(ownName);
|
|
779
|
+
const ownPath = normalizedOwnName && normalizedOwnName.includes('/')
|
|
780
|
+
? normalizedOwnName
|
|
781
|
+
: joinPath(folderPath, normalizedOwnName);
|
|
782
|
+
|
|
783
|
+
if (ownId && window.CodexOverleafProjectFiles.isTextProjectPath(ownPath)) {
|
|
784
|
+
const key = `${ownPath}:${ownId}`;
|
|
785
|
+
if (!seenRecords.has(key)) {
|
|
786
|
+
seenRecords.add(key);
|
|
787
|
+
records.push({
|
|
788
|
+
path: ownPath,
|
|
789
|
+
id: ownId
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const nextFolderPath = normalizedOwnName && !window.CodexOverleafProjectFiles.isTextProjectPath(normalizedOwnName)
|
|
795
|
+
? joinPath(folderPath, normalizedOwnName)
|
|
796
|
+
: folderPath;
|
|
797
|
+
|
|
798
|
+
for (const key of Object.keys(value).slice(0, 220)) {
|
|
799
|
+
if (!/project|root|folder|folders|children|entities|entity|docs|doc|file|files|tree|scope|state|data|metadata|meta/i.test(key)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
let child;
|
|
803
|
+
try {
|
|
804
|
+
child = value[key];
|
|
805
|
+
} catch (_error) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
walkInternalDocTree(child, nextFolderPath, depth + 1);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function collectInternalRoots(options = {}) {
|
|
814
|
+
const roots = [window._ide, window.Overleaf, window.overleaf, window.OL].filter(Boolean);
|
|
815
|
+
if (options.includeWindowGlobals === false) {
|
|
816
|
+
return uniqueNodes(roots);
|
|
817
|
+
}
|
|
818
|
+
for (const key of Object.keys(window).slice(0, 2500)) {
|
|
819
|
+
if (!/ide|overleaf|sharelatex|project|root|folder|doc|file|entity|state|meta|bootstrap|ol/i.test(key)) {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const value = window[key];
|
|
824
|
+
if (value && typeof value === 'object') {
|
|
825
|
+
roots.push(value);
|
|
826
|
+
}
|
|
827
|
+
} catch (_error) {
|
|
828
|
+
// Ignore guarded globals.
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return uniqueNodes(roots);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function collectInternalRootKeys() {
|
|
835
|
+
return Object.keys(window)
|
|
836
|
+
.filter(key => /ide|overleaf|sharelatex|project|root|folder|doc|file|entity|state|meta|bootstrap|ol/i.test(key))
|
|
837
|
+
.slice(0, 40);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function collectDomDocRecords() {
|
|
841
|
+
const selectors = [
|
|
842
|
+
'[role="treeitem"]',
|
|
843
|
+
'[role="row"]',
|
|
844
|
+
'.file-tree *',
|
|
845
|
+
'.project-tree *',
|
|
846
|
+
'[data-entity-id]',
|
|
847
|
+
'[data-doc-id]',
|
|
848
|
+
'[data-id]',
|
|
849
|
+
'[data-file-id]'
|
|
850
|
+
];
|
|
851
|
+
const nodes = uniqueNodes(selectors.flatMap(selector => Array.from(document.querySelectorAll(selector)).slice(0, 600)));
|
|
852
|
+
const records = [];
|
|
853
|
+
const seen = new Set();
|
|
854
|
+
|
|
855
|
+
for (const node of nodes) {
|
|
856
|
+
const path = readProjectPathFromNode(node);
|
|
857
|
+
const id = readDomDocId(node);
|
|
858
|
+
if (!path || !id) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const key = `${path}:${id}`;
|
|
862
|
+
if (seen.has(key)) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
seen.add(key);
|
|
866
|
+
records.push({
|
|
867
|
+
path,
|
|
868
|
+
id,
|
|
869
|
+
source: 'dom'
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return records;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readDomDocId(node) {
|
|
877
|
+
let current = node;
|
|
878
|
+
for (let depth = 0; current && depth < 5; depth += 1) {
|
|
879
|
+
for (const attr of ['data-entity-id', 'data-doc-id', 'data-id', 'data-file-id', 'data-ol-id', 'id']) {
|
|
880
|
+
const value = current.getAttribute?.(attr) || '';
|
|
881
|
+
const match = value.match(/[a-f0-9]{24}/i);
|
|
882
|
+
if (match) {
|
|
883
|
+
return match[0];
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
current = current.parentElement;
|
|
887
|
+
}
|
|
888
|
+
return '';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function readObjectPathName(value) {
|
|
892
|
+
for (const key of ['path', 'pathname', 'filePath', 'name', 'fileName', 'displayName']) {
|
|
893
|
+
const candidate = value?.[key];
|
|
894
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
895
|
+
return candidate;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return '';
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function readObjectDocId(value) {
|
|
902
|
+
for (const key of ['_id', 'id', 'docId', 'doc_id', 'entityId']) {
|
|
903
|
+
const candidate = value?.[key];
|
|
904
|
+
if (typeof candidate === 'string' && /^[a-f0-9]{24}$/i.test(candidate)) {
|
|
905
|
+
return candidate;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return '';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function isDomLikeObject(value) {
|
|
912
|
+
return value === window
|
|
913
|
+
|| value === document
|
|
914
|
+
|| (NodeCtor && value instanceof NodeCtor)
|
|
915
|
+
|| (EventTargetCtor && value instanceof EventTargetCtor && value.constructor?.name !== 'Object');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function joinPath(folderPath, name) {
|
|
919
|
+
const cleanName = normalizeSafeProjectPath(name);
|
|
920
|
+
const cleanFolder = normalizeSafeProjectPath(folderPath);
|
|
921
|
+
if (!cleanName) {
|
|
922
|
+
return cleanFolder;
|
|
923
|
+
}
|
|
924
|
+
return cleanFolder ? `${cleanFolder}/${cleanName}` : cleanName;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
function collectInternalProjectPaths() {
|
|
929
|
+
const paths = [];
|
|
930
|
+
const roots = [window._ide, window.Overleaf, window.overleaf, window.OL].filter(Boolean);
|
|
931
|
+
for (const root of roots) {
|
|
932
|
+
collectPathLikeValues(root, paths, 0);
|
|
933
|
+
if (paths.length >= 200) {
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return paths;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function collectPathLikeValues(value, paths, depth) {
|
|
941
|
+
if (!value || depth > 3 || paths.length >= 200) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (typeof value === 'string') {
|
|
946
|
+
if (window.CodexOverleafProjectFiles.isTextProjectPath(value)) {
|
|
947
|
+
paths.push(value);
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (typeof value !== 'object') {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (Array.isArray(value)) {
|
|
957
|
+
for (const item of value.slice(0, 80)) {
|
|
958
|
+
collectPathLikeValues(item, paths, depth + 1);
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
for (const key of Object.keys(value).slice(0, 100)) {
|
|
964
|
+
const child = value[key];
|
|
965
|
+
if (/path|name|file|doc/i.test(key)) {
|
|
966
|
+
collectPathLikeValues(child, paths, depth + 1);
|
|
967
|
+
} else if (child && typeof child === 'object' && /project|file|doc|root|entities|tree/i.test(key)) {
|
|
968
|
+
collectPathLikeValues(child, paths, depth + 1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function collectDomProjectPaths() {
|
|
974
|
+
const selectors = [
|
|
975
|
+
'[role="treeitem"]',
|
|
976
|
+
'[role="row"]',
|
|
977
|
+
'.file-tree li',
|
|
978
|
+
'.file-tree [data-entity-id]',
|
|
979
|
+
'.file-tree [data-testid]',
|
|
980
|
+
'.project-tree li',
|
|
981
|
+
'.project-tree [data-entity-id]'
|
|
982
|
+
];
|
|
983
|
+
const candidates = [];
|
|
984
|
+
for (const selector of selectors) {
|
|
985
|
+
candidates.push(...Array.from(document.querySelectorAll(selector)).slice(0, 500));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return Array.from(new Set(candidates.map(node => readProjectPathFromNode(node)).filter(Boolean)));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function readProjectPathFromNode(node) {
|
|
992
|
+
const explicitAttrPath = [
|
|
993
|
+
node.getAttribute('data-path'),
|
|
994
|
+
node.getAttribute('data-file-path')
|
|
995
|
+
].map(normalizeProjectPathCandidate).find(Boolean);
|
|
996
|
+
if (explicitAttrPath) {
|
|
997
|
+
if (explicitAttrPath.includes('/')) {
|
|
998
|
+
return explicitAttrPath;
|
|
999
|
+
}
|
|
1000
|
+
return resolveProjectPathFromDomDocId(node, explicitAttrPath)
|
|
1001
|
+
|| inferPathFromTreeAncestors(node, explicitAttrPath)
|
|
1002
|
+
|| (node.parentElement ? resolveProjectPathFromDomTree(node, explicitAttrPath) : '')
|
|
1003
|
+
|| explicitAttrPath;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const attrPath = normalizeProjectPathCandidate(node.getAttribute('data-name'));
|
|
1007
|
+
if (attrPath) {
|
|
1008
|
+
if (!attrPath.includes('/')) {
|
|
1009
|
+
return inferPathFromTreeAncestors(node, attrPath)
|
|
1010
|
+
|| resolveProjectPathFromDomDocId(node, attrPath)
|
|
1011
|
+
|| resolveProjectPathFromDomTree(node, attrPath)
|
|
1012
|
+
|| attrPath;
|
|
1013
|
+
}
|
|
1014
|
+
return attrPath;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const labelPath = [
|
|
1018
|
+
node.getAttribute('aria-label'),
|
|
1019
|
+
node.getAttribute('title')
|
|
1020
|
+
].map(normalizeProjectPathCandidate).find(Boolean);
|
|
1021
|
+
if (labelPath) {
|
|
1022
|
+
if (!labelPath.includes('/')) {
|
|
1023
|
+
return inferPathFromTreeAncestors(node, labelPath)
|
|
1024
|
+
|| resolveProjectPathFromDomDocId(node, labelPath)
|
|
1025
|
+
|| resolveProjectPathFromDomTree(node, labelPath)
|
|
1026
|
+
|| labelPath;
|
|
1027
|
+
}
|
|
1028
|
+
return labelPath;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const textPath = normalizeProjectPathCandidate(node.textContent || '');
|
|
1032
|
+
if (!textPath) {
|
|
1033
|
+
const docIdPath = resolveProjectPathFromDomDocId(node);
|
|
1034
|
+
const domTreePath = resolveProjectPathFromDomTree(node);
|
|
1035
|
+
return docIdPath || domTreePath;
|
|
1036
|
+
}
|
|
1037
|
+
if (!textPath.includes('/')) {
|
|
1038
|
+
return inferPathFromTreeAncestors(node, textPath)
|
|
1039
|
+
|| resolveProjectPathFromDomDocId(node, textPath)
|
|
1040
|
+
|| resolveProjectPathFromDomTree(node, textPath)
|
|
1041
|
+
|| textPath;
|
|
1042
|
+
}
|
|
1043
|
+
return textPath;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function isActiveFilePathCandidate(path, node) {
|
|
1047
|
+
if (!path || !window.CodexOverleafProjectFiles.isTextProjectPath(path)) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
if (hasMultipleTextPathExtensions(path)) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
if (isFolderTreeNode(node)) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
const text = normalizeDomText(node?.textContent || '');
|
|
1057
|
+
return !hasMultipleTextPathExtensions(text);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function isFolderTreeNode(node) {
|
|
1061
|
+
const expanded = node?.getAttribute?.('aria-expanded');
|
|
1062
|
+
return (expanded === 'true' || expanded === 'false') && Boolean(readFolderNameFromNode(node));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function resolveProjectPathFromDomTree(node, pathHint = '') {
|
|
1066
|
+
const cache = getDomProjectPathCache();
|
|
1067
|
+
const normalizedHint = normalizeSafeProjectPath(pathHint);
|
|
1068
|
+
let current = node || null;
|
|
1069
|
+
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
1070
|
+
const resolvedPath = cache.get(current) || '';
|
|
1071
|
+
if (resolvedPath && pathMatchesHint(resolvedPath, normalizedHint)) {
|
|
1072
|
+
return resolvedPath;
|
|
1073
|
+
}
|
|
1074
|
+
current = current.parentElement;
|
|
1075
|
+
}
|
|
1076
|
+
return '';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function pathMatchesHint(path, hint) {
|
|
1080
|
+
if (!hint) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
if (hint.includes('/')) {
|
|
1084
|
+
return path === hint;
|
|
1085
|
+
}
|
|
1086
|
+
return (path.split('/').pop() || '') === hint;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function getDomProjectPathCache() {
|
|
1090
|
+
const now = Date.now();
|
|
1091
|
+
if (domProjectPathCache && now - domProjectPathCacheTime < 1000) {
|
|
1092
|
+
return domProjectPathCache;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const pathByNode = new WeakMap();
|
|
1096
|
+
const visited = new WeakSet();
|
|
1097
|
+
const roots = uniqueNodes([
|
|
1098
|
+
...collectElements('.file-tree', 20),
|
|
1099
|
+
...collectElements('.project-tree', 20),
|
|
1100
|
+
...collectElements('.file-tree-inner', 20),
|
|
1101
|
+
...collectElements('[role="tree"]', 40)
|
|
1102
|
+
]);
|
|
1103
|
+
for (const root of roots) {
|
|
1104
|
+
walkDomProjectTree(root, '', 0, pathByNode, visited);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
domProjectPathCache = pathByNode;
|
|
1108
|
+
domProjectPathCacheTime = now;
|
|
1109
|
+
return domProjectPathCache;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function walkDomProjectTree(node, folderPath, depth, pathByNode, visited) {
|
|
1113
|
+
if (!node || depth > 60 || visited.has(node)) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
visited.add(node);
|
|
1117
|
+
|
|
1118
|
+
const filePath = readFilePathFromTreeNodeSelf(node, folderPath);
|
|
1119
|
+
const folderName = filePath ? '' : readFolderNameFromNode(node);
|
|
1120
|
+
const nextFolderPath = folderName ? joinPath(folderPath, folderName) : folderPath;
|
|
1121
|
+
if (filePath) {
|
|
1122
|
+
pathByNode.set(node, filePath);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const children = Array.from(node.children || []);
|
|
1126
|
+
let pendingSiblingFolderPath = '';
|
|
1127
|
+
for (const child of children) {
|
|
1128
|
+
const childFilePath = readFilePathFromTreeNodeSelf(child, nextFolderPath);
|
|
1129
|
+
const childFolderName = childFilePath ? '' : readFolderNameFromNode(child);
|
|
1130
|
+
const childIsNestedList = isNestedFolderListNode(child);
|
|
1131
|
+
const childFolderPath = childIsNestedList && pendingSiblingFolderPath
|
|
1132
|
+
? pendingSiblingFolderPath
|
|
1133
|
+
: nextFolderPath;
|
|
1134
|
+
walkDomProjectTree(child, childFolderPath, depth + 1, pathByNode, visited);
|
|
1135
|
+
|
|
1136
|
+
if (childFolderName) {
|
|
1137
|
+
pendingSiblingFolderPath = joinPath(nextFolderPath, childFolderName);
|
|
1138
|
+
} else if (!childIsNestedList && childFilePath) {
|
|
1139
|
+
pendingSiblingFolderPath = '';
|
|
1140
|
+
} else if (childIsNestedList && pendingSiblingFolderPath) {
|
|
1141
|
+
pendingSiblingFolderPath = '';
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function isNestedFolderListNode(node) {
|
|
1147
|
+
const className = String(node?.className || '');
|
|
1148
|
+
const role = node?.getAttribute?.('role') || '';
|
|
1149
|
+
return /file-tree-folder-list/i.test(className)
|
|
1150
|
+
|| (role === 'tree' && !/file-tree-list/i.test(className));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function readFilePathFromTreeNodeSelf(node, folderPath) {
|
|
1154
|
+
const candidates = [
|
|
1155
|
+
node.getAttribute?.('data-path'),
|
|
1156
|
+
node.getAttribute?.('data-file-path'),
|
|
1157
|
+
node.getAttribute?.('data-name'),
|
|
1158
|
+
node.getAttribute?.('aria-label'),
|
|
1159
|
+
node.getAttribute?.('title'),
|
|
1160
|
+
node.textContent || ''
|
|
1161
|
+
];
|
|
1162
|
+
for (const value of candidates) {
|
|
1163
|
+
const path = normalizeProjectPathCandidate(value);
|
|
1164
|
+
if (!path) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const resolvedPath = path.includes('/') ? path : joinPath(folderPath, path);
|
|
1168
|
+
if (window.CodexOverleafProjectFiles.isTextProjectPath(resolvedPath)) {
|
|
1169
|
+
return resolvedPath;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return '';
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function resolveProjectPathFromDomDocId(node, pathHint = '') {
|
|
1176
|
+
const id = readDomDocId(node);
|
|
1177
|
+
if (!id) {
|
|
1178
|
+
return '';
|
|
1179
|
+
}
|
|
1180
|
+
const resolvedPath = getInternalDocPathByIdMap().get(id) || '';
|
|
1181
|
+
if (!resolvedPath) {
|
|
1182
|
+
return '';
|
|
1183
|
+
}
|
|
1184
|
+
const normalizedHint = normalizeSafeProjectPath(pathHint);
|
|
1185
|
+
if (!normalizedHint) {
|
|
1186
|
+
return resolvedPath;
|
|
1187
|
+
}
|
|
1188
|
+
if (normalizedHint.includes('/')) {
|
|
1189
|
+
return resolvedPath === normalizedHint ? resolvedPath : '';
|
|
1190
|
+
}
|
|
1191
|
+
const resolvedName = resolvedPath.split('/').pop() || '';
|
|
1192
|
+
return resolvedName === normalizedHint ? resolvedPath : '';
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function getInternalDocPathByIdMap() {
|
|
1196
|
+
const now = Date.now();
|
|
1197
|
+
if (internalDocPathByIdCache && now - internalDocPathByIdCacheTime < 1000) {
|
|
1198
|
+
return internalDocPathByIdCache;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const byId = new Map();
|
|
1202
|
+
const ambiguousIds = new Set();
|
|
1203
|
+
for (const record of collectInternalDocRecords({ includeWindowGlobals: false })) {
|
|
1204
|
+
if (!record?.id || !window.CodexOverleafProjectFiles.isTextProjectPath(record.path)) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const existingPath = byId.get(record.id);
|
|
1208
|
+
if (existingPath && existingPath !== record.path) {
|
|
1209
|
+
ambiguousIds.add(record.id);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
byId.set(record.id, record.path);
|
|
1213
|
+
}
|
|
1214
|
+
for (const id of ambiguousIds) {
|
|
1215
|
+
byId.delete(id);
|
|
1216
|
+
}
|
|
1217
|
+
internalDocPathByIdCache = byId;
|
|
1218
|
+
internalDocPathByIdCacheTime = now;
|
|
1219
|
+
return internalDocPathByIdCache;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function normalizeProjectPathCandidate(value) {
|
|
1223
|
+
const text = normalizeDomText(value);
|
|
1224
|
+
if (!text || hasMultipleTextPathExtensions(text)) {
|
|
1225
|
+
return '';
|
|
1226
|
+
}
|
|
1227
|
+
const candidates = [
|
|
1228
|
+
stripOverleafIconAffixes(text),
|
|
1229
|
+
text
|
|
1230
|
+
];
|
|
1231
|
+
for (const candidate of candidates) {
|
|
1232
|
+
const normalized = normalizeSafeProjectPath(candidate);
|
|
1233
|
+
if (normalized && window.CodexOverleafProjectFiles.isTextProjectPath(normalized)) {
|
|
1234
|
+
return normalized;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return '';
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function normalizeDomText(value) {
|
|
1241
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function hasMultipleTextPathExtensions(value) {
|
|
1245
|
+
const text = normalizeDomText(value);
|
|
1246
|
+
const pattern = new RegExp(`\\.${TEXT_PATH_EXTENSION_PATTERN}`, 'gi');
|
|
1247
|
+
const matches = text.match(pattern) || [];
|
|
1248
|
+
if (matches.length > 1) {
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function stripOverleafIconAffixes(value) {
|
|
1255
|
+
let text = normalizeDomText(value);
|
|
1256
|
+
for (let index = 0; index < 4; index += 1) {
|
|
1257
|
+
const next = text
|
|
1258
|
+
.replace(/^(?:description|article|book_5|insert_drive_file|draft|text_snippet|note|chevron_right|expand_more|folder_open|folder)+/i, '')
|
|
1259
|
+
.replace(/(?:more_vert\s*Menu|more_vert|Menu)$/i, '')
|
|
1260
|
+
.trim();
|
|
1261
|
+
if (next === text) {
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
text = next;
|
|
1265
|
+
}
|
|
1266
|
+
return text;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function inferPathFromTreeAncestors(node, fileName) {
|
|
1270
|
+
const folders = [];
|
|
1271
|
+
let current = node?.parentElement || null;
|
|
1272
|
+
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
1273
|
+
const folderName = readFolderNameFromNode(current);
|
|
1274
|
+
if (folderName) {
|
|
1275
|
+
folders.unshift(folderName);
|
|
1276
|
+
}
|
|
1277
|
+
current = current.parentElement;
|
|
1278
|
+
}
|
|
1279
|
+
if (!folders.length) {
|
|
1280
|
+
return '';
|
|
1281
|
+
}
|
|
1282
|
+
const path = normalizeSafeProjectPath([...folders, fileName].join('/'));
|
|
1283
|
+
return window.CodexOverleafProjectFiles.isTextProjectPath(path) ? path : '';
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function inferFolderPathFromTreeAncestors(node, folderName) {
|
|
1287
|
+
const folders = [];
|
|
1288
|
+
let current = node?.parentElement || null;
|
|
1289
|
+
for (let depth = 0; current && depth < 8; depth += 1) {
|
|
1290
|
+
const parentFolderName = readFolderNameFromNode(current);
|
|
1291
|
+
if (parentFolderName) {
|
|
1292
|
+
folders.unshift(parentFolderName);
|
|
1293
|
+
}
|
|
1294
|
+
current = current.parentElement;
|
|
1295
|
+
}
|
|
1296
|
+
const path = normalizeSafeProjectPath([...folders, folderName].join('/'));
|
|
1297
|
+
return path && !window.CodexOverleafProjectFiles.isTextProjectPath(path) ? path : '';
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function invalidateDomProjectPathCache() {
|
|
1301
|
+
domProjectPathCache = null;
|
|
1302
|
+
domProjectPathCacheTime = 0;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function readFolderNameFromNode(node) {
|
|
1306
|
+
const candidates = [
|
|
1307
|
+
node.getAttribute?.('data-name'),
|
|
1308
|
+
node.getAttribute?.('aria-label'),
|
|
1309
|
+
node.getAttribute?.('title')
|
|
1310
|
+
];
|
|
1311
|
+
for (const value of candidates) {
|
|
1312
|
+
const folderName = normalizeFolderName(value);
|
|
1313
|
+
if (folderName) {
|
|
1314
|
+
return folderName;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return '';
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function normalizeFolderName(value) {
|
|
1321
|
+
const text = normalizeDomText(value);
|
|
1322
|
+
if (!text || /[\\/]/.test(text) || window.CodexOverleafProjectFiles.isTextProjectPath(text)) {
|
|
1323
|
+
return '';
|
|
1324
|
+
}
|
|
1325
|
+
if (new RegExp(`\\.${TEXT_PATH_EXTENSION_PATTERN}\\b`, 'i').test(text)) {
|
|
1326
|
+
return '';
|
|
1327
|
+
}
|
|
1328
|
+
if (/^(?:description|article|book_5|insert_drive_file|draft|text_snippet|note|chevron_right|expand_more|folder_open|folder|expand|collapse|open|close|menu)\b/i.test(text)) {
|
|
1329
|
+
return '';
|
|
1330
|
+
}
|
|
1331
|
+
if (/(?:more_vert|action menu|Menu)$/i.test(text)) {
|
|
1332
|
+
return '';
|
|
1333
|
+
}
|
|
1334
|
+
if (!/^[\w .@+-]+$/.test(text) || text.length > 80) {
|
|
1335
|
+
return '';
|
|
1336
|
+
}
|
|
1337
|
+
return text;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function waitForActiveFile(filePath, timeoutMs) {
|
|
1341
|
+
const deadline = Date.now() + timeoutMs;
|
|
1342
|
+
while (Date.now() < deadline) {
|
|
1343
|
+
if (getActiveFilePath() === filePath) {
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
await delay(100);
|
|
1347
|
+
}
|
|
1348
|
+
return getActiveFilePath() === filePath;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
async function waitForActiveEditorText(filePath, timeoutMs, options = {}) {
|
|
1352
|
+
const deadline = Date.now() + timeoutMs;
|
|
1353
|
+
let lastText = '';
|
|
1354
|
+
while (Date.now() < deadline) {
|
|
1355
|
+
const activeFileMatches = !filePath || getActiveFilePath() === filePath;
|
|
1356
|
+
const text = readActiveEditorText();
|
|
1357
|
+
lastText = text;
|
|
1358
|
+
const textReady = window.CodexOverleafProjectFiles.isUsableProjectFileContent(text);
|
|
1359
|
+
const signatureChanged = !options.notSignature || contentSignature(text) !== options.notSignature;
|
|
1360
|
+
if (activeFileMatches && textReady && signatureChanged) {
|
|
1361
|
+
return {
|
|
1362
|
+
ok: true,
|
|
1363
|
+
text
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
await delay(150);
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
ok: false,
|
|
1370
|
+
text: lastText,
|
|
1371
|
+
reason: `Editor content was not ready for ${filePath || 'active file'}; last length ${String(lastText || '').length}`
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function contentSignature(content) {
|
|
1376
|
+
const text = String(content || '');
|
|
1377
|
+
return `${text.length}:${text.slice(0, 120)}:${text.slice(-120)}`;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function findFileTreeManager() {
|
|
1381
|
+
const roots = [window._ide, window.Overleaf, window.overleaf, window.OL].filter(Boolean);
|
|
1382
|
+
for (const root of roots) {
|
|
1383
|
+
for (const key of Object.keys(root)) {
|
|
1384
|
+
if (/file.*tree|tree.*file|file.*manager|entity.*manager/i.test(key) && root[key] && typeof root[key] === 'object') {
|
|
1385
|
+
return root[key];
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return window._ide?.fileTreeManager || window._ide?.fileTree || null;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function findHistoryObject() {
|
|
1393
|
+
const roots = [window._ide, window.Overleaf, window.overleaf, window.OL].filter(Boolean);
|
|
1394
|
+
for (const root of roots) {
|
|
1395
|
+
for (const key of Object.keys(root)) {
|
|
1396
|
+
if (/history/i.test(key) && root[key] && typeof root[key] === 'object') {
|
|
1397
|
+
return root[key];
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function delay(ms) {
|
|
1405
|
+
return new Promise(resolve => window.setTimeout(resolve, ms));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async function navigateToFile(filePath, options = {}) {
|
|
1409
|
+
const normalizedPath = normalizeSafeProjectPath(filePath);
|
|
1410
|
+
if (!normalizedPath) {
|
|
1411
|
+
return {
|
|
1412
|
+
loaded: false,
|
|
1413
|
+
editorReady: false,
|
|
1414
|
+
reason: 'Invalid file path'
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
const opened = await openFileByPath(normalizedPath);
|
|
1418
|
+
if (!opened.ok) {
|
|
1419
|
+
return {
|
|
1420
|
+
loaded: false,
|
|
1421
|
+
editorReady: false,
|
|
1422
|
+
reason: opened.reason || 'Could not open file'
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const ready = await waitForActiveEditorText(normalizedPath, resolveTimeoutMs(options, 2500));
|
|
1426
|
+
return {
|
|
1427
|
+
loaded: true,
|
|
1428
|
+
editorReady: ready.ok === true,
|
|
1429
|
+
method: opened.method,
|
|
1430
|
+
reason: ready.ok ? '' : ready.reason
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function createFile(filePath, content, options = {}) {
|
|
1435
|
+
return callFileTreeMutation({
|
|
1436
|
+
type: 'create',
|
|
1437
|
+
path: filePath,
|
|
1438
|
+
content
|
|
1439
|
+
}, options);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
async function renameFile(fromPath, toPath, options = {}) {
|
|
1443
|
+
return callFileTreeMutation({
|
|
1444
|
+
type: 'rename',
|
|
1445
|
+
path: fromPath,
|
|
1446
|
+
to: toPath
|
|
1447
|
+
}, options);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function deleteFile(filePath, options = {}) {
|
|
1451
|
+
return callFileTreeMutation({
|
|
1452
|
+
type: 'delete',
|
|
1453
|
+
path: filePath
|
|
1454
|
+
}, options);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
async function callFileTreeMutation(rawOperation, options = {}) {
|
|
1458
|
+
const operation = {
|
|
1459
|
+
...rawOperation,
|
|
1460
|
+
path: normalizeSafeProjectPath(rawOperation.path),
|
|
1461
|
+
to: rawOperation.to ? normalizeSafeProjectPath(rawOperation.to) : rawOperation.to
|
|
1462
|
+
};
|
|
1463
|
+
if (!operation.path || ((operation.type === 'rename' || operation.type === 'move') && !operation.to)) {
|
|
1464
|
+
return buildTreeContractResult(operation, false, 'Invalid file-tree path');
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const manager = findFileTreeManager();
|
|
1468
|
+
for (const methodName of fileTreeMethodNames(operation.type)) {
|
|
1469
|
+
const method = manager?.[methodName];
|
|
1470
|
+
if (typeof method !== 'function') {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
if (operation.type === 'create') {
|
|
1475
|
+
await method.call(manager, operation.path, normalizeCreateFileContent(operation.path, operation.content));
|
|
1476
|
+
} else if (operation.type === 'rename' || operation.type === 'move') {
|
|
1477
|
+
await method.call(manager, operation.path, operation.to);
|
|
1478
|
+
} else if (operation.type === 'delete') {
|
|
1479
|
+
await method.call(manager, operation.path);
|
|
1480
|
+
}
|
|
1481
|
+
await delay(120);
|
|
1482
|
+
return buildTreeContractResult(operation, verifyTreeMutation(operation), '', 'fileTreeManager.' + methodName);
|
|
1483
|
+
} catch (_error) {
|
|
1484
|
+
// Try the next known Overleaf file-tree method.
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return buildTreeContractResult(operation, false, 'No supported Overleaf file-tree method was detected');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function normalizeCreateFileContent(filePath, content) {
|
|
1491
|
+
if (content instanceof Uint8Array) {
|
|
1492
|
+
const fileName = String(filePath || '').split('/').filter(Boolean).pop() || 'asset';
|
|
1493
|
+
if (typeof File === 'function') {
|
|
1494
|
+
return new File([content], fileName);
|
|
1495
|
+
}
|
|
1496
|
+
return new Blob([content]);
|
|
1497
|
+
}
|
|
1498
|
+
return String(content || '');
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function verifyTreeMutation(operation) {
|
|
1502
|
+
if (operation.type === 'delete') {
|
|
1503
|
+
return !projectPathExists(operation.path);
|
|
1504
|
+
}
|
|
1505
|
+
if (operation.type === 'rename' || operation.type === 'move') {
|
|
1506
|
+
return Boolean(operation.to && projectPathExists(operation.to) && !projectPathExists(operation.path));
|
|
1507
|
+
}
|
|
1508
|
+
return projectPathExists(operation.path);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function buildTreeContractResult(operation, ok, reason = '', method = '') {
|
|
1512
|
+
if (operation.type === 'delete') {
|
|
1513
|
+
return {
|
|
1514
|
+
deleted: ok,
|
|
1515
|
+
reason: ok ? '' : reason,
|
|
1516
|
+
method
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
if (operation.type === 'rename' || operation.type === 'move') {
|
|
1520
|
+
return {
|
|
1521
|
+
renamed: ok,
|
|
1522
|
+
reason: ok ? '' : reason,
|
|
1523
|
+
method
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
created: ok,
|
|
1528
|
+
reason: ok ? '' : reason,
|
|
1529
|
+
method
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function fileTreeMethodNames(type) {
|
|
1534
|
+
return {
|
|
1535
|
+
create: ['createDoc', 'createFile', 'addDoc', 'addFile'],
|
|
1536
|
+
rename: ['renameEntity', 'renameFile', 'renameDoc'],
|
|
1537
|
+
move: ['moveEntity', 'moveFile', 'moveDoc'],
|
|
1538
|
+
delete: ['deleteEntity', 'deleteFile', 'deleteDoc', 'removeEntity']
|
|
1539
|
+
}[type] || [];
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function resolveTimeoutMs(options = {}, fallback) {
|
|
1543
|
+
const value = Number(options.timeoutMs);
|
|
1544
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
installFileTreeSelectionTracking();
|
|
1548
|
+
|
|
1549
|
+
return {
|
|
1550
|
+
collectDocRecords,
|
|
1551
|
+
collectDomProjectPaths,
|
|
1552
|
+
collectInternalRootKeys,
|
|
1553
|
+
collectProjectTextPaths,
|
|
1554
|
+
contentSignature,
|
|
1555
|
+
createFile,
|
|
1556
|
+
deleteFile,
|
|
1557
|
+
fileTreeMethodNames,
|
|
1558
|
+
findFileTreeManager,
|
|
1559
|
+
findFileTreeNode,
|
|
1560
|
+
findHistoryObject,
|
|
1561
|
+
getActiveFilePath,
|
|
1562
|
+
getEditorStoreFilePath,
|
|
1563
|
+
getRecentFileTreeSelectionPath,
|
|
1564
|
+
getProjectId,
|
|
1565
|
+
navigateToFile,
|
|
1566
|
+
openFileByPath,
|
|
1567
|
+
projectPathExists,
|
|
1568
|
+
readProjectPathFromNode,
|
|
1569
|
+
renameFile,
|
|
1570
|
+
waitForActiveEditorText,
|
|
1571
|
+
waitForActiveFile
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function fallbackNormalizeSafeProjectPath(value) {
|
|
1576
|
+
const text = String(value || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '').trim();
|
|
1577
|
+
if (!text || text.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
1578
|
+
return '';
|
|
1579
|
+
}
|
|
1580
|
+
return text;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return { create };
|
|
1584
|
+
});
|