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,948 @@
|
|
|
1
|
+
(function initCodexOverleafSnapshotRouter(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafSnapshotRouter = factory();
|
|
6
|
+
}
|
|
7
|
+
})(typeof window !== 'undefined' ? window : globalThis, function snapshotRouterFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const ZIP_FETCH_TIMEOUT_MS = 30000;
|
|
11
|
+
const MAX_BINARY_FILE_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
const MAX_BINARY_TOTAL_BYTES = 80 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
function create(deps = {}) {
|
|
15
|
+
const window = deps.window || (typeof globalThis !== 'undefined' ? globalThis : {});
|
|
16
|
+
const treeOperations = deps.treeOperations || {};
|
|
17
|
+
const normalizeSafeProjectPath = typeof deps.normalizePath === 'function'
|
|
18
|
+
? deps.normalizePath
|
|
19
|
+
: fallbackNormalizeSafeProjectPath;
|
|
20
|
+
const readActiveEditorText = typeof deps.readActiveEditorText === 'function'
|
|
21
|
+
? deps.readActiveEditorText
|
|
22
|
+
: () => '';
|
|
23
|
+
let invalidatedAt = 0;
|
|
24
|
+
|
|
25
|
+
function getProjectId() {
|
|
26
|
+
return treeOperations.getProjectId?.() || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getActiveFilePath() {
|
|
30
|
+
return treeOperations.getActiveFilePath?.() || '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPassiveActiveFilePath() {
|
|
34
|
+
const path = treeOperations.getEditorStoreFilePath?.({ passive: true }) || '';
|
|
35
|
+
return normalizeSafeProjectPath(path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectDocRecords(options = {}) {
|
|
39
|
+
return treeOperations.collectDocRecords?.(options) || [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectDomProjectPaths() {
|
|
43
|
+
return treeOperations.collectDomProjectPaths?.() || [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectProjectTextPaths(activePath) {
|
|
47
|
+
return treeOperations.collectProjectTextPaths?.(activePath) || [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function openFileByPath(filePath) {
|
|
51
|
+
return treeOperations.openFileByPath?.(filePath) || Promise.resolve({ ok: false, reason: 'Tree operations are unavailable' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function waitForActiveFile(filePath, timeoutMs) {
|
|
55
|
+
return treeOperations.waitForActiveFile?.(filePath, timeoutMs) || Promise.resolve(false);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function waitForActiveEditorText(filePath, timeoutMs, options = {}) {
|
|
59
|
+
return treeOperations.waitForActiveEditorText?.(filePath, timeoutMs, options)
|
|
60
|
+
|| Promise.resolve({ ok: false, text: '', reason: 'Tree operations are unavailable' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function contentSignature(content) {
|
|
64
|
+
const fallback = String(content || '');
|
|
65
|
+
return treeOperations.contentSignature?.(content) || (fallback.length + ':' + fallback.slice(0, 120));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function buildProjectFileList(params = {}) {
|
|
69
|
+
const activePath = getActiveFilePath();
|
|
70
|
+
const internalDocRecords = collectDocRecords({ includeWindowGlobals: false });
|
|
71
|
+
const docRecordByPath = new Map(internalDocRecords.map(record => [record.path, record]));
|
|
72
|
+
if (params.preferExact !== false) {
|
|
73
|
+
const zipSnapshot = await fetchProjectZipSnapshot({
|
|
74
|
+
...params,
|
|
75
|
+
includeBinaryFiles: true,
|
|
76
|
+
includeContent: false
|
|
77
|
+
});
|
|
78
|
+
if (zipSnapshot.ok && zipSnapshot.files.length) {
|
|
79
|
+
const paths = collectUniqueProjectPaths(zipSnapshot.files.map(file => file.path), 1000);
|
|
80
|
+
const zipFileByPath = buildProjectFileEntryLookup(zipSnapshot.files);
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
id: getProjectId(),
|
|
84
|
+
url: window.location.href,
|
|
85
|
+
activePath,
|
|
86
|
+
files: paths.map(path => {
|
|
87
|
+
const isText = window.CodexOverleafProjectFiles.isTextProjectPath(path);
|
|
88
|
+
return {
|
|
89
|
+
path,
|
|
90
|
+
active: path === activePath,
|
|
91
|
+
source: 'overleaf-zip',
|
|
92
|
+
kind: isText ? 'text' : 'binary',
|
|
93
|
+
selectable: isText,
|
|
94
|
+
...buildFileSizeMetadata(zipFileByPath.get(path))
|
|
95
|
+
};
|
|
96
|
+
}),
|
|
97
|
+
capabilities: {
|
|
98
|
+
fullProjectSnapshot: true,
|
|
99
|
+
method: 'overleaf-zip-file-list',
|
|
100
|
+
skipped: [],
|
|
101
|
+
diagnostics: {
|
|
102
|
+
zipEndpoint: zipSnapshot.endpoint,
|
|
103
|
+
docRecordCount: internalDocRecords.length,
|
|
104
|
+
docRecords: internalDocRecords.slice(0, 8)
|
|
105
|
+
},
|
|
106
|
+
note: 'Listed Overleaf project files from the exact source ZIP file tree.'
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (params.exactOnly === true || params.requireExact === true) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
id: getProjectId(),
|
|
114
|
+
url: window.location.href,
|
|
115
|
+
activePath,
|
|
116
|
+
files: [],
|
|
117
|
+
reason: zipSnapshot.reason || 'Overleaf source ZIP was unavailable',
|
|
118
|
+
capabilities: {
|
|
119
|
+
fullProjectSnapshot: false,
|
|
120
|
+
method: 'overleaf-zip-file-list-unavailable',
|
|
121
|
+
skipped: [{
|
|
122
|
+
path: 'project.zip',
|
|
123
|
+
reason: zipSnapshot.reason || 'Overleaf source ZIP was unavailable'
|
|
124
|
+
}],
|
|
125
|
+
diagnostics: {
|
|
126
|
+
docRecordCount: internalDocRecords.length,
|
|
127
|
+
docRecords: internalDocRecords.slice(0, 8)
|
|
128
|
+
},
|
|
129
|
+
note: 'The exact Overleaf source ZIP file tree was required, so the page file tree fallback was not used.'
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const paths = collectProjectFileListPaths(activePath, internalDocRecords);
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
id: getProjectId(),
|
|
139
|
+
url: window.location.href,
|
|
140
|
+
activePath,
|
|
141
|
+
files: paths.map(path => ({
|
|
142
|
+
path,
|
|
143
|
+
active: path === activePath,
|
|
144
|
+
source: docRecordByPath.has(path) ? 'overleaf-doc-record' : path === activePath ? 'active-file' : 'page-file-list'
|
|
145
|
+
})),
|
|
146
|
+
capabilities: {
|
|
147
|
+
fullProjectSnapshot: false,
|
|
148
|
+
method: 'overleaf-file-tree',
|
|
149
|
+
skipped: [],
|
|
150
|
+
diagnostics: {
|
|
151
|
+
docRecordCount: internalDocRecords.length,
|
|
152
|
+
docRecords: internalDocRecords.slice(0, 8)
|
|
153
|
+
},
|
|
154
|
+
note: 'Listed Overleaf project text files from page state without downloading the source ZIP.'
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function collectProjectFileListPaths(activePath, docRecords = []) {
|
|
160
|
+
const domPaths = collectDomProjectPaths();
|
|
161
|
+
const domPathSet = new Set(domPaths);
|
|
162
|
+
const docPaths = docRecords
|
|
163
|
+
.map(record => record.path)
|
|
164
|
+
.filter(path => window.CodexOverleafProjectFiles.isTextProjectPath(path));
|
|
165
|
+
const activePaths = domPathSet.has(activePath) ? [activePath] : [];
|
|
166
|
+
return window.CodexOverleafProjectFiles.collectUniqueTextPaths([
|
|
167
|
+
...activePaths,
|
|
168
|
+
...domPaths,
|
|
169
|
+
...docPaths
|
|
170
|
+
], 160);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function collectUniqueProjectPaths(paths, limit = 1000) {
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const result = [];
|
|
176
|
+
for (const rawPath of paths || []) {
|
|
177
|
+
const path = normalizeSafeProjectPath(rawPath);
|
|
178
|
+
if (!isSafeProjectPath(path) || seen.has(path)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
seen.add(path);
|
|
182
|
+
result.push(path);
|
|
183
|
+
if (result.length >= limit) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildProjectFileEntryLookup(files = []) {
|
|
191
|
+
const entries = new Map();
|
|
192
|
+
for (const file of files || []) {
|
|
193
|
+
const path = normalizeSafeProjectPath(file?.path);
|
|
194
|
+
if (!isSafeProjectPath(path) || entries.has(path)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
entries.set(path, file);
|
|
198
|
+
}
|
|
199
|
+
return entries;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildFileSizeMetadata(file = {}) {
|
|
203
|
+
const size = Number(file?.size ?? file?.byteLength);
|
|
204
|
+
return Number.isFinite(size) && size >= 0 ? { size } : {};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isSafeProjectPath(path) {
|
|
208
|
+
return Boolean(path && normalizeSafeProjectPath(path) === path && !path.endsWith('/'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function buildProjectSnapshot(params = {}) {
|
|
212
|
+
if (params.zipOnly) {
|
|
213
|
+
return buildZipOnlyProjectSnapshot(params);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const activePath = getActiveFilePath();
|
|
217
|
+
const internalDocRecords = collectDocRecords();
|
|
218
|
+
const docRecordByPath = new Map(internalDocRecords.map(record => [record.path, record]));
|
|
219
|
+
let lightweightSnapshot = null;
|
|
220
|
+
|
|
221
|
+
if (params.preferLightweight) {
|
|
222
|
+
lightweightSnapshot = await buildPageProjectSnapshot({
|
|
223
|
+
params,
|
|
224
|
+
activePath,
|
|
225
|
+
internalDocRecords,
|
|
226
|
+
docRecordByPath,
|
|
227
|
+
zipSnapshot: null,
|
|
228
|
+
lightweightOnly: true
|
|
229
|
+
});
|
|
230
|
+
if (
|
|
231
|
+
params.allowZipFallback === false
|
|
232
|
+
|| lightweightSnapshot.files.length && (!params.requireFullProject || lightweightSnapshot.capabilities.fullProjectSnapshot)
|
|
233
|
+
) {
|
|
234
|
+
return lightweightSnapshot;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const zipSnapshot = await fetchProjectZipSnapshot(params);
|
|
239
|
+
if (zipSnapshot.ok && zipSnapshot.files.length) {
|
|
240
|
+
const mergedFiles = mergeSnapshotFiles(
|
|
241
|
+
zipSnapshot.files,
|
|
242
|
+
filterZipSnapshotOverlayFiles(zipSnapshot.files, lightweightSnapshot?.files || [])
|
|
243
|
+
);
|
|
244
|
+
const files = filterRequestedSnapshotFiles(mergedFiles, params, activePath);
|
|
245
|
+
const requestedPathsComplete = params.restrictToRequestedPathsOnly === true
|
|
246
|
+
? isCompleteProjectSnapshot(files, getRequestedSnapshotPaths(params, activePath))
|
|
247
|
+
: true;
|
|
248
|
+
return {
|
|
249
|
+
id: getProjectId(),
|
|
250
|
+
url: window.location.href,
|
|
251
|
+
activePath,
|
|
252
|
+
files,
|
|
253
|
+
capabilities: {
|
|
254
|
+
fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : true,
|
|
255
|
+
requestedPathsComplete,
|
|
256
|
+
method: 'overleaf-zip',
|
|
257
|
+
skipped: zipSnapshot.skipped || [],
|
|
258
|
+
diagnostics: {
|
|
259
|
+
zipEndpoint: zipSnapshot.endpoint,
|
|
260
|
+
docRecordCount: internalDocRecords.length,
|
|
261
|
+
docRecords: internalDocRecords.slice(0, 8)
|
|
262
|
+
},
|
|
263
|
+
note: 'Captured project text files from Overleaf source ZIP using the current browser session.'
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (params.allowEditorNavigation === false) {
|
|
269
|
+
return buildPageProjectSnapshot({
|
|
270
|
+
params,
|
|
271
|
+
activePath,
|
|
272
|
+
internalDocRecords,
|
|
273
|
+
docRecordByPath,
|
|
274
|
+
zipSnapshot,
|
|
275
|
+
lightweightOnly: true
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return buildPageProjectSnapshot({
|
|
280
|
+
params,
|
|
281
|
+
activePath,
|
|
282
|
+
internalDocRecords,
|
|
283
|
+
docRecordByPath,
|
|
284
|
+
zipSnapshot,
|
|
285
|
+
lightweightOnly: false
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function buildPageProjectSnapshot({
|
|
290
|
+
params = {},
|
|
291
|
+
activePath,
|
|
292
|
+
internalDocRecords,
|
|
293
|
+
docRecordByPath,
|
|
294
|
+
zipSnapshot,
|
|
295
|
+
lightweightOnly
|
|
296
|
+
}) {
|
|
297
|
+
const snapshotActivePath = params.allowEditorNavigation === false
|
|
298
|
+
? getActiveFilePath()
|
|
299
|
+
: activePath;
|
|
300
|
+
const requestedPaths = [
|
|
301
|
+
snapshotActivePath,
|
|
302
|
+
...(Array.isArray(params.focusFiles) ? params.focusFiles : [])
|
|
303
|
+
];
|
|
304
|
+
const projectPaths = window.CodexOverleafProjectFiles.collectUniqueTextPaths(
|
|
305
|
+
params.restrictToRequestedPathsOnly === true
|
|
306
|
+
? requestedPaths
|
|
307
|
+
: [
|
|
308
|
+
...requestedPaths,
|
|
309
|
+
...collectProjectTextPaths(snapshotActivePath)
|
|
310
|
+
],
|
|
311
|
+
80
|
|
312
|
+
);
|
|
313
|
+
const files = [];
|
|
314
|
+
const skipped = [];
|
|
315
|
+
if (zipSnapshot && !zipSnapshot.ok) {
|
|
316
|
+
skipped.push({
|
|
317
|
+
path: 'project.zip',
|
|
318
|
+
reason: zipSnapshot.reason
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
let previousSignature = '';
|
|
322
|
+
|
|
323
|
+
for (const filePath of projectPaths) {
|
|
324
|
+
if (filePath === snapshotActivePath) {
|
|
325
|
+
const ready = await waitForActiveEditorText(filePath, 5000);
|
|
326
|
+
if (!ready.ok) {
|
|
327
|
+
skipped.push({
|
|
328
|
+
path: filePath,
|
|
329
|
+
reason: ready.reason
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
files.push({
|
|
334
|
+
path: filePath,
|
|
335
|
+
content: ready.text,
|
|
336
|
+
source: 'active-editor'
|
|
337
|
+
});
|
|
338
|
+
previousSignature = contentSignature(ready.text);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const docRecord = docRecordByPath.get(filePath);
|
|
343
|
+
if (docRecord) {
|
|
344
|
+
const fetched = await fetchOverleafDocContent(docRecord);
|
|
345
|
+
if (fetched.ok) {
|
|
346
|
+
files.push({
|
|
347
|
+
path: filePath,
|
|
348
|
+
content: fetched.content,
|
|
349
|
+
source: fetched.method
|
|
350
|
+
});
|
|
351
|
+
previousSignature = contentSignature(fetched.content);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
skipped.push({
|
|
355
|
+
path: filePath,
|
|
356
|
+
reason: fetched.reason
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!canOpenInactiveFileForSnapshot(filePath, params, lightweightOnly, docRecord)) {
|
|
361
|
+
skipped.push({
|
|
362
|
+
path: filePath,
|
|
363
|
+
reason: 'Lightweight snapshot skipped opening inactive files'
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const opened = await openFileByPath(filePath);
|
|
369
|
+
if (!opened.ok) {
|
|
370
|
+
skipped.push({
|
|
371
|
+
path: filePath,
|
|
372
|
+
reason: opened.reason || 'open failed'
|
|
373
|
+
});
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await waitForActiveFile(filePath, 5000);
|
|
378
|
+
const ready = await waitForActiveEditorText(filePath, 7000, {
|
|
379
|
+
notSignature: previousSignature
|
|
380
|
+
});
|
|
381
|
+
if (!ready.ok) {
|
|
382
|
+
skipped.push({
|
|
383
|
+
path: filePath,
|
|
384
|
+
reason: ready.reason
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
files.push({
|
|
389
|
+
path: filePath,
|
|
390
|
+
content: ready.text,
|
|
391
|
+
source: opened.method || 'file-tree-open'
|
|
392
|
+
});
|
|
393
|
+
previousSignature = contentSignature(ready.text);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (params.allowEditorNavigation !== false && activePath && getActiveFilePath() !== activePath) {
|
|
397
|
+
await openFileByPath(activePath);
|
|
398
|
+
await waitForActiveFile(activePath, 5000);
|
|
399
|
+
await waitForActiveEditorText(activePath, 5000);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (snapshotActivePath && !files.length && (!lightweightOnly || params.allowZipFallback === false)) {
|
|
403
|
+
files.push({
|
|
404
|
+
path: snapshotActivePath,
|
|
405
|
+
content: readActiveEditorText(),
|
|
406
|
+
source: 'active-editor'
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const requestedPathsComplete = isCompleteProjectSnapshot(files, projectPaths);
|
|
411
|
+
return {
|
|
412
|
+
id: getProjectId(),
|
|
413
|
+
url: window.location.href,
|
|
414
|
+
activePath: snapshotActivePath,
|
|
415
|
+
files,
|
|
416
|
+
capabilities: {
|
|
417
|
+
fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : requestedPathsComplete,
|
|
418
|
+
requestedPathsComplete,
|
|
419
|
+
method: resolvePageSnapshotMethod(files, lightweightOnly),
|
|
420
|
+
skipped,
|
|
421
|
+
diagnostics: {
|
|
422
|
+
docRecordCount: internalDocRecords.length,
|
|
423
|
+
docRecords: internalDocRecords.slice(0, 8)
|
|
424
|
+
},
|
|
425
|
+
note: lightweightOnly
|
|
426
|
+
? 'Captured current Overleaf text from page/editor state without downloading the source ZIP.'
|
|
427
|
+
: (files.length > 1
|
|
428
|
+
? 'Captured text project files from Overleaf document data, with editor fallback when needed.'
|
|
429
|
+
: 'Only the active editor was captured; no additional visible text files were detected.')
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function buildZipOnlyProjectSnapshot(params = {}) {
|
|
435
|
+
const activePath = getPassiveActiveFilePath();
|
|
436
|
+
const activeText = activePath ? readActiveEditorText() : '';
|
|
437
|
+
|
|
438
|
+
const zipSnapshot = await fetchProjectZipSnapshot(params);
|
|
439
|
+
if (zipSnapshot.ok && zipSnapshot.files.length) {
|
|
440
|
+
const activeOverlay = buildActiveEditorOverlay(activePath, activeText, {
|
|
441
|
+
allowedPaths: zipSnapshot.files.map(file => file.path)
|
|
442
|
+
});
|
|
443
|
+
const files = filterRequestedSnapshotFiles(
|
|
444
|
+
mergeSnapshotFiles(zipSnapshot.files, activeOverlay),
|
|
445
|
+
params,
|
|
446
|
+
activePath
|
|
447
|
+
);
|
|
448
|
+
const requestedPathsComplete = params.restrictToRequestedPathsOnly === true
|
|
449
|
+
? isCompleteProjectSnapshot(files, getRequestedSnapshotPaths(params, activePath))
|
|
450
|
+
: true;
|
|
451
|
+
return {
|
|
452
|
+
id: getProjectId(),
|
|
453
|
+
url: window.location.href,
|
|
454
|
+
activePath,
|
|
455
|
+
files,
|
|
456
|
+
capabilities: {
|
|
457
|
+
fullProjectSnapshot: params.restrictToRequestedPathsOnly === true ? false : true,
|
|
458
|
+
requestedPathsComplete,
|
|
459
|
+
method: 'overleaf-zip',
|
|
460
|
+
skipped: zipSnapshot.skipped || [],
|
|
461
|
+
diagnostics: {
|
|
462
|
+
zipEndpoint: zipSnapshot.endpoint,
|
|
463
|
+
docRecordCount: 0,
|
|
464
|
+
docRecords: []
|
|
465
|
+
},
|
|
466
|
+
note: 'Captured project files from Overleaf source ZIP without inspecting or opening the file tree.'
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
id: getProjectId(),
|
|
473
|
+
url: window.location.href,
|
|
474
|
+
activePath,
|
|
475
|
+
files: buildActiveEditorOverlay(activePath, activeText),
|
|
476
|
+
capabilities: {
|
|
477
|
+
fullProjectSnapshot: false,
|
|
478
|
+
method: activePath && window.CodexOverleafProjectFiles.isUsableProjectFileContent(activeText)
|
|
479
|
+
? 'active-editor-zip-only-fallback'
|
|
480
|
+
: 'zip-only-fallback-empty',
|
|
481
|
+
skipped: [{
|
|
482
|
+
path: 'project.zip',
|
|
483
|
+
reason: zipSnapshot.reason
|
|
484
|
+
}],
|
|
485
|
+
diagnostics: {
|
|
486
|
+
docRecordCount: 0,
|
|
487
|
+
docRecords: []
|
|
488
|
+
},
|
|
489
|
+
note: 'Only the current editor was read because the Overleaf source ZIP was unavailable; no file tree navigation was attempted.'
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function filterZipSnapshotOverlayFiles(zipFiles = [], overlayFiles = []) {
|
|
495
|
+
const zipPaths = new Set((zipFiles || []).map(file => file.path).filter(Boolean));
|
|
496
|
+
return (overlayFiles || []).filter(file => file?.source !== 'active-editor' || zipPaths.has(file.path));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getRequestedSnapshotPaths(params = {}, activePath = '') {
|
|
500
|
+
return window.CodexOverleafProjectFiles.collectUniqueTextPaths([
|
|
501
|
+
activePath,
|
|
502
|
+
...(Array.isArray(params.focusFiles) ? params.focusFiles : [])
|
|
503
|
+
], 80);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function filterRequestedSnapshotFiles(files = [], params = {}, activePath = '') {
|
|
507
|
+
if (params.restrictToRequestedPathsOnly !== true) {
|
|
508
|
+
return files;
|
|
509
|
+
}
|
|
510
|
+
const requestedSet = new Set(getRequestedSnapshotPaths(params, activePath));
|
|
511
|
+
return (files || []).filter(file => requestedSet.has(file?.path));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function buildActiveEditorOverlay(activePath, activeText, options = {}) {
|
|
515
|
+
if (!activePath || !window.CodexOverleafProjectFiles.isUsableProjectFileContent(activeText)) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
if (Array.isArray(options.allowedPaths) && !options.allowedPaths.includes(activePath)) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
return [{
|
|
522
|
+
path: activePath,
|
|
523
|
+
content: activeText,
|
|
524
|
+
source: 'active-editor',
|
|
525
|
+
kind: 'text'
|
|
526
|
+
}];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function mergeSnapshotFiles(primaryFiles = [], overrideFiles = []) {
|
|
530
|
+
const filesByPath = new Map((primaryFiles || []).map(file => [file.path, { ...file }]));
|
|
531
|
+
for (const file of overrideFiles || []) {
|
|
532
|
+
if (!file?.path || file.kind === 'binary' || typeof file.content !== 'string') {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(file.content)) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
filesByPath.set(file.path, {
|
|
539
|
+
...(filesByPath.get(file.path) || {}),
|
|
540
|
+
...file,
|
|
541
|
+
kind: 'text'
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return Array.from(filesByPath.values());
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function resolvePageSnapshotMethod(files, lightweightOnly) {
|
|
548
|
+
if (files.some(file => /^overleaf-doc-fetch/.test(file.source))) {
|
|
549
|
+
return lightweightOnly ? 'overleaf-doc-fetch-lightweight' : 'overleaf-doc-fetch';
|
|
550
|
+
}
|
|
551
|
+
if (files.length > 1) {
|
|
552
|
+
return lightweightOnly ? 'page-text-files-lightweight' : 'open-file-tree-text-files';
|
|
553
|
+
}
|
|
554
|
+
return lightweightOnly ? 'active-editor-lightweight' : 'active-editor';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isFocusSnapshotPath(filePath, params = {}) {
|
|
558
|
+
return Array.isArray(params.focusFiles) && params.focusFiles.includes(filePath);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function canOpenInactiveFileForSnapshot(filePath, params = {}, lightweightOnly, docRecord) {
|
|
562
|
+
if (!lightweightOnly) {
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
if (docRecord) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
return params.allowEditorNavigation === true && isFocusSnapshotPath(filePath, params);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isCompleteProjectSnapshot(files, projectPaths) {
|
|
572
|
+
if (!Array.isArray(projectPaths) || !projectPaths.length) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
const capturedPaths = new Set((files || []).map(file => file.path));
|
|
576
|
+
return projectPaths.every(filePath => capturedPaths.has(filePath));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function fetchOverleafDocContent(record) {
|
|
580
|
+
if (!record?.id || !record.path) {
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
reason: 'Missing Overleaf doc id'
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const projectId = getProjectId();
|
|
588
|
+
if (!projectId) {
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
reason: 'Missing Overleaf project id'
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const response = await fetch(`${window.location.origin}/project/${encodeURIComponent(projectId)}/doc/${encodeURIComponent(record.id)}`, {
|
|
597
|
+
credentials: 'include',
|
|
598
|
+
headers: {
|
|
599
|
+
accept: 'application/json'
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
if (!response.ok) {
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
reason: `Overleaf doc fetch returned ${response.status}`
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const payload = await response.json();
|
|
609
|
+
const content = readDocPayloadContent(payload);
|
|
610
|
+
if (!window.CodexOverleafProjectFiles.isUsableProjectFileContent(content)) {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
reason: 'Overleaf doc fetch returned empty/loading content'
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
ok: true,
|
|
618
|
+
content,
|
|
619
|
+
method: 'overleaf-doc-fetch'
|
|
620
|
+
};
|
|
621
|
+
} catch (error) {
|
|
622
|
+
return {
|
|
623
|
+
ok: false,
|
|
624
|
+
reason: `Overleaf doc fetch failed: ${error.message}`
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function fetchProjectZipSnapshot(params = {}) {
|
|
630
|
+
const projectId = getProjectId();
|
|
631
|
+
if (!projectId) {
|
|
632
|
+
return {
|
|
633
|
+
ok: false,
|
|
634
|
+
reason: 'Missing Overleaf project id'
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
const timeoutMs = normalizeZipFetchTimeout(params.zipTimeoutMs);
|
|
638
|
+
|
|
639
|
+
const endpoints = [
|
|
640
|
+
`${window.location.origin}/project/${encodeURIComponent(projectId)}/download/zip`,
|
|
641
|
+
`${window.location.origin}/download/project/${encodeURIComponent(projectId)}`
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
const errors = [];
|
|
645
|
+
for (const endpoint of endpoints) {
|
|
646
|
+
try {
|
|
647
|
+
const { response, contentType, buffer } = await fetchZipEndpoint(endpoint, timeoutMs);
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
errors.push(`${endpoint} returned ${response.status}`);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const extracted = await extractFilesFromZip(buffer, {
|
|
653
|
+
includeBinaryFiles: Boolean(params.includeBinaryFiles),
|
|
654
|
+
includeContent: params.includeContent !== false
|
|
655
|
+
});
|
|
656
|
+
const files = extracted.files || [];
|
|
657
|
+
if (!files.length) {
|
|
658
|
+
errors.push(`${endpoint} returned no text files (${contentType || 'unknown content type'})`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
ok: true,
|
|
663
|
+
endpoint,
|
|
664
|
+
skipped: extracted.skipped || [],
|
|
665
|
+
files: files.map(file => ({
|
|
666
|
+
...file,
|
|
667
|
+
source: 'overleaf-zip'
|
|
668
|
+
}))
|
|
669
|
+
};
|
|
670
|
+
} catch (error) {
|
|
671
|
+
errors.push(`${endpoint} failed: ${error.message}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
reason: errors.join('; ') || 'Overleaf source ZIP was unavailable'
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function fetchZipEndpoint(endpoint, timeoutMs) {
|
|
682
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
683
|
+
const request = (async () => {
|
|
684
|
+
const response = await fetch(endpoint, {
|
|
685
|
+
credentials: 'include',
|
|
686
|
+
headers: {
|
|
687
|
+
accept: 'application/zip,application/octet-stream,*/*'
|
|
688
|
+
},
|
|
689
|
+
...(controller ? { signal: controller.signal } : {})
|
|
690
|
+
});
|
|
691
|
+
const contentType = response.headers.get('content-type') || '';
|
|
692
|
+
const buffer = response.ok ? await response.arrayBuffer() : null;
|
|
693
|
+
return {
|
|
694
|
+
response,
|
|
695
|
+
contentType,
|
|
696
|
+
buffer
|
|
697
|
+
};
|
|
698
|
+
})();
|
|
699
|
+
|
|
700
|
+
return withTimeout(request, timeoutMs, () => {
|
|
701
|
+
controller?.abort();
|
|
702
|
+
}, `Overleaf ZIP download timed out after ${timeoutMs}ms`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function withTimeout(promise, timeoutMs, onTimeout, message) {
|
|
706
|
+
let timeoutId = null;
|
|
707
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
708
|
+
timeoutId = window.setTimeout(() => {
|
|
709
|
+
try {
|
|
710
|
+
onTimeout?.();
|
|
711
|
+
} finally {
|
|
712
|
+
reject(new Error(message));
|
|
713
|
+
}
|
|
714
|
+
}, timeoutMs);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
return Promise.race([promise, timeout])
|
|
718
|
+
.finally(() => {
|
|
719
|
+
if (timeoutId) {
|
|
720
|
+
window.clearTimeout(timeoutId);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function normalizeZipFetchTimeout(value) {
|
|
726
|
+
const parsed = Number(value);
|
|
727
|
+
if (Number.isFinite(parsed) && parsed >= 100) {
|
|
728
|
+
return Math.min(parsed, 30000);
|
|
729
|
+
}
|
|
730
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
731
|
+
return parsed;
|
|
732
|
+
}
|
|
733
|
+
return ZIP_FETCH_TIMEOUT_MS;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function extractFilesFromZip(buffer, options = {}) {
|
|
737
|
+
const view = new DataView(buffer);
|
|
738
|
+
const bytes = new Uint8Array(buffer);
|
|
739
|
+
const eocdOffset = findEndOfCentralDirectory(view);
|
|
740
|
+
if (eocdOffset < 0) {
|
|
741
|
+
throw new Error('ZIP end-of-central-directory record was not found');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const entryCount = view.getUint16(eocdOffset + 10, true);
|
|
745
|
+
const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);
|
|
746
|
+
const decoder = new TextDecoder('utf-8');
|
|
747
|
+
const files = [];
|
|
748
|
+
const skipped = [];
|
|
749
|
+
let binaryBytes = 0;
|
|
750
|
+
let offset = centralDirectoryOffset;
|
|
751
|
+
|
|
752
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
753
|
+
if (view.getUint32(offset, true) !== 0x02014b50) {
|
|
754
|
+
throw new Error('Invalid ZIP central-directory header');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const compressionMethod = view.getUint16(offset + 10, true);
|
|
758
|
+
const compressedSize = view.getUint32(offset + 20, true);
|
|
759
|
+
const uncompressedSize = view.getUint32(offset + 24, true);
|
|
760
|
+
const fileNameLength = view.getUint16(offset + 28, true);
|
|
761
|
+
const extraLength = view.getUint16(offset + 30, true);
|
|
762
|
+
const commentLength = view.getUint16(offset + 32, true);
|
|
763
|
+
const localHeaderOffset = view.getUint32(offset + 42, true);
|
|
764
|
+
const rawName = bytes.slice(offset + 46, offset + 46 + fileNameLength);
|
|
765
|
+
const path = normalizeSafeProjectPath(decoder.decode(rawName));
|
|
766
|
+
const isText = window.CodexOverleafProjectFiles.isTextProjectPath(path);
|
|
767
|
+
const shouldInclude = path && !path.endsWith('/') && (isText || options.includeBinaryFiles);
|
|
768
|
+
|
|
769
|
+
if (shouldInclude) {
|
|
770
|
+
const file = { path, size: uncompressedSize };
|
|
771
|
+
if (options.includeContent !== false && (isText || options.includeBinaryFiles)) {
|
|
772
|
+
if (!isText) {
|
|
773
|
+
if (uncompressedSize > MAX_BINARY_FILE_BYTES) {
|
|
774
|
+
skipped.push({
|
|
775
|
+
path,
|
|
776
|
+
reason: 'binary_file_too_large',
|
|
777
|
+
size: uncompressedSize
|
|
778
|
+
});
|
|
779
|
+
offset += 46 + fileNameLength + extraLength + commentLength;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (binaryBytes + uncompressedSize > MAX_BINARY_TOTAL_BYTES) {
|
|
783
|
+
skipped.push({
|
|
784
|
+
path,
|
|
785
|
+
reason: 'binary_project_too_large',
|
|
786
|
+
size: uncompressedSize
|
|
787
|
+
});
|
|
788
|
+
offset += 46 + fileNameLength + extraLength + commentLength;
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
binaryBytes += uncompressedSize;
|
|
792
|
+
}
|
|
793
|
+
const contentBytes = await readZipEntryBytes(view, bytes, localHeaderOffset, compressedSize, compressionMethod);
|
|
794
|
+
if (isText) {
|
|
795
|
+
file.content = decoder.decode(contentBytes);
|
|
796
|
+
} else {
|
|
797
|
+
file.contentBase64 = uint8ArrayToBase64(contentBytes);
|
|
798
|
+
file.size = contentBytes.byteLength;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
files.push({
|
|
802
|
+
...file,
|
|
803
|
+
kind: isText ? 'text' : 'binary'
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
offset += 46 + fileNameLength + extraLength + commentLength;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return { files, skipped };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function uint8ArrayToBase64(bytes) {
|
|
814
|
+
let binary = '';
|
|
815
|
+
const chunkSize = 0x8000;
|
|
816
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
817
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
|
818
|
+
}
|
|
819
|
+
return window.btoa(binary);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function findEndOfCentralDirectory(view) {
|
|
823
|
+
const minOffset = Math.max(0, view.byteLength - 0xffff - 22);
|
|
824
|
+
for (let offset = view.byteLength - 22; offset >= minOffset; offset -= 1) {
|
|
825
|
+
if (view.getUint32(offset, true) === 0x06054b50) {
|
|
826
|
+
return offset;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return -1;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function readZipEntryBytes(view, bytes, localHeaderOffset, compressedSize, compressionMethod) {
|
|
833
|
+
if (view.getUint32(localHeaderOffset, true) !== 0x04034b50) {
|
|
834
|
+
throw new Error('Invalid ZIP local-file header');
|
|
835
|
+
}
|
|
836
|
+
const localFileNameLength = view.getUint16(localHeaderOffset + 26, true);
|
|
837
|
+
const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
|
|
838
|
+
const dataOffset = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
|
|
839
|
+
const compressed = bytes.slice(dataOffset, dataOffset + compressedSize);
|
|
840
|
+
|
|
841
|
+
if (compressionMethod === 0) {
|
|
842
|
+
return compressed;
|
|
843
|
+
}
|
|
844
|
+
if (compressionMethod === 8) {
|
|
845
|
+
return inflateRaw(compressed);
|
|
846
|
+
}
|
|
847
|
+
throw new Error(`Unsupported ZIP compression method ${compressionMethod}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function inflateRaw(bytes) {
|
|
851
|
+
if (typeof DecompressionStream !== 'function') {
|
|
852
|
+
throw new Error('Browser does not support DecompressionStream');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const errors = [];
|
|
856
|
+
for (const format of ['deflate-raw', 'deflate']) {
|
|
857
|
+
try {
|
|
858
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format));
|
|
859
|
+
const response = new Response(stream);
|
|
860
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
861
|
+
} catch (error) {
|
|
862
|
+
errors.push(`${format}: ${error.message}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
throw new Error(`Could not inflate ZIP entry (${errors.join('; ')})`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function readDocPayloadContent(payload) {
|
|
869
|
+
const candidates = [
|
|
870
|
+
payload?.lines,
|
|
871
|
+
payload?.doc?.lines,
|
|
872
|
+
payload?.document?.lines,
|
|
873
|
+
payload?.content,
|
|
874
|
+
payload?.doc?.content,
|
|
875
|
+
payload?.document?.content
|
|
876
|
+
];
|
|
877
|
+
|
|
878
|
+
for (const candidate of candidates) {
|
|
879
|
+
if (Array.isArray(candidate) && candidate.every(line => typeof line === 'string')) {
|
|
880
|
+
return candidate.join('\n');
|
|
881
|
+
}
|
|
882
|
+
if (typeof candidate === 'string') {
|
|
883
|
+
return candidate;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return '';
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function assembleSnapshot(options = {}) {
|
|
891
|
+
const timeoutMs = Number(options.timeoutMs);
|
|
892
|
+
const params = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
893
|
+
? { ...options, zipTimeoutMs: timeoutMs }
|
|
894
|
+
: options;
|
|
895
|
+
const snapshot = await buildProjectSnapshot(params);
|
|
896
|
+
return {
|
|
897
|
+
...snapshot,
|
|
898
|
+
source: snapshot?.capabilities?.method || '',
|
|
899
|
+
timestamp: Date.now()
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async function readFileTree(options = {}) {
|
|
904
|
+
const timeoutMs = Number(options.timeoutMs);
|
|
905
|
+
const result = await buildProjectFileList({
|
|
906
|
+
...options,
|
|
907
|
+
...(Number.isFinite(timeoutMs) && timeoutMs >= 0 ? { zipTimeoutMs: timeoutMs } : {})
|
|
908
|
+
});
|
|
909
|
+
if (!result.ok) {
|
|
910
|
+
throw new Error(result.reason || 'Could not read Overleaf project file tree');
|
|
911
|
+
}
|
|
912
|
+
return (result.files || []).map(file => ({
|
|
913
|
+
path: file.path,
|
|
914
|
+
type: file.kind || (file.selectable === false ? 'binary' : 'text'),
|
|
915
|
+
size: file.size
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function invalidateCache() {
|
|
920
|
+
invalidatedAt = Date.now();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function getInvalidatedAt() {
|
|
924
|
+
return invalidatedAt;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return {
|
|
928
|
+
assembleSnapshot,
|
|
929
|
+
buildProjectFileList,
|
|
930
|
+
buildProjectSnapshot,
|
|
931
|
+
fetchProjectZipSnapshot,
|
|
932
|
+
getInvalidatedAt,
|
|
933
|
+
getRequestedSnapshotPaths,
|
|
934
|
+
invalidateCache,
|
|
935
|
+
readFileTree
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function fallbackNormalizeSafeProjectPath(value) {
|
|
940
|
+
const text = String(value || '').replace(/\\/g, '/').replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '').trim();
|
|
941
|
+
if (!text || text.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
942
|
+
return '';
|
|
943
|
+
}
|
|
944
|
+
return text;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return { create };
|
|
948
|
+
});
|