docslight 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,668 @@
1
+ import { onLanguageChange, t } from "./i18n.js";
2
+
3
+ const highlightSourceClasses = {
4
+ cloud: "source-cloud",
5
+ local: "source-local",
6
+ parse: "source-parse",
7
+ };
8
+
9
+ export function noop() {}
10
+
11
+ function textValue(value, fallback = "") {
12
+ if (value === null || value === undefined) return fallback;
13
+ if (typeof value === "string") return value;
14
+ return String(value);
15
+ }
16
+
17
+ function setPaneText(pane, text) {
18
+ if (!pane) return;
19
+ pane.replaceChildren();
20
+ const pre = document.createElement("pre");
21
+ pre.className = "result-preview";
22
+ pre.textContent = textValue(text);
23
+ pane.append(pre);
24
+ }
25
+
26
+ function actionButton(text, onClick) {
27
+ const button = document.createElement("button");
28
+ button.type = "button";
29
+ button.className = "ghost-button";
30
+ button.textContent = text;
31
+ button.addEventListener("click", onClick);
32
+ return button;
33
+ }
34
+
35
+ function pageSourceDimensions(page) {
36
+ return {
37
+ width: page?.width || page?.source_width || page?.page_width || null,
38
+ height: page?.height || page?.source_height || page?.page_height || null,
39
+ };
40
+ }
41
+
42
+ function isPositiveFinite(value) {
43
+ return Number.isFinite(value) && value > 0;
44
+ }
45
+
46
+ function applyPageGeometry(page, width, height) {
47
+ if (width) page.dataset.width = String(width);
48
+ if (height) page.dataset.height = String(height);
49
+ if (isPositiveFinite(Number(width)) && isPositiveFinite(Number(height))) {
50
+ page.style.setProperty("--preview-page-ratio", `${Number(width)} / ${Number(height)}`);
51
+ }
52
+ }
53
+
54
+ function appendPageLabel(page, pageNumber) {
55
+ const label = document.createElement("span");
56
+ label.className = "preview-page-label";
57
+ label.textContent = t("preview.pageLabel", { page: pageNumber });
58
+ page.append(label);
59
+ }
60
+
61
+ function normalizeHighlightEntries(bboxes) {
62
+ if (bboxes && typeof bboxes === "object" && !Array.isArray(bboxes) && bboxes.bboxes) {
63
+ return normalizeHighlightEntries(withInheritedDimensions(bboxes.bboxes, bboxes));
64
+ }
65
+
66
+ const entries = Array.isArray(bboxes) && bboxes.length >= 4 && bboxes.every((value) => typeof value === "number")
67
+ ? [{ page_id: 1, bbox: bboxes }]
68
+ : (Array.isArray(bboxes) ? bboxes : []);
69
+ if (bboxes && typeof bboxes === "object" && !Array.isArray(bboxes)) {
70
+ entries.push(bboxes);
71
+ }
72
+
73
+ return entries
74
+ .map((entry) => {
75
+ if (Array.isArray(entry)) return { page_id: 1, bbox: entry };
76
+ if (!entry || typeof entry !== "object") return null;
77
+ return {
78
+ page_id: entry.page_id || entry.page || 1,
79
+ page_index: entry.page_index,
80
+ bbox: entry.bbox || entry.bboxes || entry.block_bbox || entry.box,
81
+ source_width: entry.source_width || entry.width || entry.page_width,
82
+ source_height: entry.source_height || entry.height || entry.page_height,
83
+ };
84
+ })
85
+ .filter((entry) => entry && Array.isArray(entry.bbox) && entry.bbox.length >= 4);
86
+ }
87
+
88
+ function previewFallbackMessage(message) {
89
+ if (!message || message === "Office files can be processed, but preview and positioning highlight are not supported in this version.") {
90
+ return t("preview.officeUnsupported");
91
+ }
92
+ return message;
93
+ }
94
+
95
+ function findPreviewPage(canvas, entry) {
96
+ const pageId = entry.page_id ?? entry.page ?? 1;
97
+ const pageIndex = entry.page_index ?? Number(pageId) - 1;
98
+ const pageIdText = String(pageId);
99
+ const pageIndexText = String(pageIndex);
100
+
101
+ return Array.from(canvas.querySelectorAll(".preview-page")).find((page) => (
102
+ page.dataset.pageId === pageIdText || page.dataset.pageIndex === pageIndexText
103
+ )) || null;
104
+ }
105
+
106
+ export function initHealthBadge(healthStatus, endpoint = "/api/health") {
107
+ if (!healthStatus) return;
108
+
109
+ const renderHealthStatus = () => {
110
+ if (healthStatus.dataset.healthUnavailable === "true") {
111
+ healthStatus.textContent = t("health.unavailable");
112
+ return;
113
+ }
114
+ if (healthStatus.dataset.healthService && healthStatus.dataset.healthState) {
115
+ healthStatus.textContent = t("health.status", {
116
+ service: healthStatus.dataset.healthService,
117
+ status: healthStatus.dataset.healthState,
118
+ });
119
+ return;
120
+ }
121
+ healthStatus.textContent = t("health.checking");
122
+ };
123
+
124
+ onLanguageChange(renderHealthStatus);
125
+
126
+ fetch(endpoint)
127
+ .then((response) => response.json())
128
+ .then((payload) => {
129
+ healthStatus.dataset.healthService = payload.service;
130
+ healthStatus.dataset.healthState = payload.status;
131
+ delete healthStatus.dataset.healthUnavailable;
132
+ renderHealthStatus();
133
+ })
134
+ .catch(() => {
135
+ healthStatus.dataset.healthUnavailable = "true";
136
+ renderHealthStatus();
137
+ });
138
+ }
139
+
140
+ export function bindDropzone({ dropZone, fileInput, fileName, onFileChange } = {}) {
141
+ if (!dropZone || !fileInput) return;
142
+
143
+ const updateFileName = () => {
144
+ if (fileName) {
145
+ fileName.textContent = fileInput.files.length ? fileInput.files[0].name : t("drop.none");
146
+ }
147
+ };
148
+ const notifyChange = () => {
149
+ updateFileName();
150
+ if (typeof onFileChange === "function") onFileChange(fileInput.files[0] || null);
151
+ };
152
+
153
+ dropZone.addEventListener("dragover", (event) => {
154
+ event.preventDefault();
155
+ dropZone.classList.add("is-dragging");
156
+ });
157
+ dropZone.addEventListener("dragleave", () => {
158
+ dropZone.classList.remove("is-dragging");
159
+ });
160
+ dropZone.addEventListener("drop", (event) => {
161
+ event.preventDefault();
162
+ dropZone.classList.remove("is-dragging");
163
+ if (event.dataTransfer.files.length) {
164
+ fileInput.files = event.dataTransfer.files;
165
+ notifyChange();
166
+ }
167
+ });
168
+ fileInput.addEventListener("change", notifyChange);
169
+ onLanguageChange(updateFileName);
170
+ updateFileName();
171
+ }
172
+
173
+ export function loadPreview({ fileInput, previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state } = {}) {
174
+ const requestState = state || {};
175
+ const requestId = (requestState.previewRequestId || 0) + 1;
176
+ requestState.previewRequestId = requestId;
177
+
178
+ if (!fileInput?.files?.length) {
179
+ renderPreview(null, { previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state: requestState });
180
+ return Promise.resolve(null);
181
+ }
182
+
183
+ const body = new FormData();
184
+ body.set("file", fileInput.files[0]);
185
+ if (previewTitle) previewTitle.textContent = t("preview.loadingTitle");
186
+ if (previewCanvas) previewCanvas.textContent = t("preview.rendering");
187
+ if (officePreviewNotice) officePreviewNotice.hidden = true;
188
+
189
+ return fetch("/api/preview", { method: "POST", body })
190
+ .then(async (response) => {
191
+ const payload = await response.json();
192
+ if (requestId !== requestState.previewRequestId) return null;
193
+ if (!response.ok || !payload.success) {
194
+ if (previewCanvas) previewCanvas.textContent = payload.error || t("preview.failedHttp", { status: response.status });
195
+ return null;
196
+ }
197
+ renderPreview(payload.result, { previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state: requestState });
198
+ return payload.result;
199
+ })
200
+ .catch((error) => {
201
+ if (requestId !== requestState.previewRequestId) return null;
202
+ if (previewCanvas) previewCanvas.textContent = error instanceof Error ? error.message : t("preview.unexpectedError");
203
+ return null;
204
+ });
205
+ }
206
+
207
+ export function renderPreview(preview, { previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state } = {}) {
208
+ if (!previewCanvas) return;
209
+ if (state) state.preview = preview;
210
+ previewCanvas.replaceChildren();
211
+ if (officePreviewNotice) officePreviewNotice.hidden = true;
212
+ clearHighlight(previewCanvas, highlightStatus);
213
+
214
+ if (!preview) {
215
+ if (previewTitle) previewTitle.textContent = t("preview.title");
216
+ const empty = document.createElement("p");
217
+ empty.className = "preview-empty";
218
+ empty.textContent = t("preview.empty");
219
+ previewCanvas.append(empty);
220
+ return;
221
+ }
222
+
223
+ if (previewTitle) previewTitle.textContent = t("preview.title");
224
+
225
+ if (preview.kind === "unsupported") {
226
+ if (officePreviewNotice) officePreviewNotice.hidden = false;
227
+ const empty = document.createElement("p");
228
+ empty.className = "preview-empty";
229
+ empty.textContent = previewFallbackMessage(preview.message) || t("preview.unavailable");
230
+ previewCanvas.append(empty);
231
+ return;
232
+ }
233
+
234
+ if (preview.kind === "image") {
235
+ const page = document.createElement("div");
236
+ page.className = "preview-page";
237
+ page.dataset.pageId = "1";
238
+ page.dataset.pageIndex = "0";
239
+ applyPageGeometry(page, preview.width, preview.height);
240
+
241
+ const image = document.createElement("img");
242
+ image.alt = "Document preview page 1";
243
+ image.src = preview.data_url;
244
+ image.addEventListener("load", () => {
245
+ if (!page.dataset.width || !page.dataset.height) {
246
+ applyPageGeometry(page, image.naturalWidth, image.naturalHeight);
247
+ }
248
+ });
249
+ page.append(image);
250
+ appendPageLabel(page, 1);
251
+ previewCanvas.append(page);
252
+ return;
253
+ }
254
+
255
+ if (preview.kind === "pdf") {
256
+ (preview.pages || []).forEach((previewPage, index) => {
257
+ const page = document.createElement("div");
258
+ page.className = "preview-page";
259
+ page.dataset.pageId = String(previewPage.page_id || index + 1);
260
+ page.dataset.pageIndex = String(previewPage.page_index ?? index);
261
+ applyPageGeometry(page, previewPage.width, previewPage.height);
262
+
263
+ const image = document.createElement("img");
264
+ image.alt = `Document preview page ${index + 1}`;
265
+ image.src = previewPage.image;
266
+ page.append(image);
267
+ appendPageLabel(page, index + 1);
268
+ previewCanvas.append(page);
269
+ });
270
+ return;
271
+ }
272
+
273
+ const empty = document.createElement("p");
274
+ empty.className = "preview-empty";
275
+ empty.textContent = t("preview.unavailable");
276
+ previewCanvas.append(empty);
277
+ }
278
+
279
+ export function clearHighlight(previewCanvas, highlightStatus) {
280
+ previewCanvas?.querySelectorAll(".highlight-box").forEach((box) => box.remove());
281
+ if (highlightStatus) highlightStatus.textContent = t("preview.noHighlight");
282
+ }
283
+
284
+ export function highlightBboxes(bboxes, source = "parse", { previewCanvas, highlightStatus } = {}) {
285
+ if (!previewCanvas) return 0;
286
+ clearHighlight(previewCanvas, highlightStatus);
287
+ const entries = normalizeHighlightEntries(bboxes);
288
+ let rendered = 0;
289
+
290
+ entries.forEach((entry) => {
291
+ const page = findPreviewPage(previewCanvas, entry);
292
+ if (!page) return;
293
+
294
+ const pageWidth = Number(page.dataset.width);
295
+ const pageHeight = Number(page.dataset.height);
296
+ const [left, top, right, bottom] = entry.bbox.map(Number);
297
+ const sourceWidth = Number(entry.source_width || entry.width || pageWidth);
298
+ const sourceHeight = Number(entry.source_height || entry.height || pageHeight);
299
+ if (
300
+ !isPositiveFinite(pageWidth)
301
+ || !isPositiveFinite(pageHeight)
302
+ || !isPositiveFinite(sourceWidth)
303
+ || !isPositiveFinite(sourceHeight)
304
+ || ![left, top, right, bottom].every(Number.isFinite)
305
+ || right <= left
306
+ || bottom <= top
307
+ ) {
308
+ return;
309
+ }
310
+
311
+ const box = document.createElement("span");
312
+ box.className = `highlight-box ${highlightSourceClasses[source] || highlightSourceClasses.parse}`;
313
+ box.style.left = `${(left / sourceWidth) * 100}%`;
314
+ box.style.top = `${(top / sourceHeight) * 100}%`;
315
+ box.style.width = `${((right - left) / sourceWidth) * 100}%`;
316
+ box.style.height = `${((bottom - top) / sourceHeight) * 100}%`;
317
+ page.append(box);
318
+ rendered += 1;
319
+ });
320
+
321
+ if (highlightStatus) {
322
+ if (!rendered) {
323
+ highlightStatus.textContent = t("highlight.noPositioning");
324
+ } else if (source === "cloud") {
325
+ highlightStatus.textContent = t("highlight.cloud", { count: rendered });
326
+ } else if (source === "local") {
327
+ highlightStatus.textContent = t("highlight.local", { count: rendered });
328
+ } else {
329
+ highlightStatus.textContent = t("highlight.parse", { count: rendered });
330
+ }
331
+ }
332
+
333
+ return rendered;
334
+ }
335
+
336
+ export function withInheritedDimensions(bboxes, owner) {
337
+ if (!owner || typeof owner !== "object") return bboxes;
338
+
339
+ const inheritedWidth = owner.source_width || owner.width || owner.page_width;
340
+ const inheritedHeight = owner.source_height || owner.height || owner.page_height;
341
+ const inherit = (entry) => ({
342
+ page_id: entry.page_id || owner.page_id || owner.page || 1,
343
+ page_index: entry.page_index ?? owner.page_index,
344
+ bbox: entry.bbox || entry.block_bbox || entry.box || entry,
345
+ source_width: entry.source_width || entry.width || entry.page_width || inheritedWidth,
346
+ source_height: entry.source_height || entry.height || entry.page_height || inheritedHeight,
347
+ });
348
+
349
+ if (Array.isArray(bboxes)) {
350
+ if (bboxes.length >= 4 && bboxes.every((value) => typeof value === "number")) return [inherit(bboxes)];
351
+ return bboxes.map((entry) => (Array.isArray(entry) ? inherit(entry) : inherit(entry || {})));
352
+ }
353
+
354
+ if (bboxes && typeof bboxes === "object") {
355
+ if (bboxes.bboxes) return withInheritedDimensions(bboxes.bboxes, { ...owner, ...bboxes });
356
+ return [inherit(bboxes)];
357
+ }
358
+
359
+ return [];
360
+ }
361
+
362
+ export function renderBlocksView(result, panel, { onPick } = {}) {
363
+ if (!panel) return;
364
+ panel.replaceChildren();
365
+ const normalized = normalizeParsePayload(result);
366
+ const blocks = [];
367
+ (normalized.pages || []).forEach((page, pageIndex) => {
368
+ const pageDimensions = pageSourceDimensions(page);
369
+ (page.parsing_res_list || []).forEach((block) => {
370
+ blocks.push({
371
+ page_id: page.page_id || page.page || pageIndex + 1,
372
+ block,
373
+ pageDimensions,
374
+ });
375
+ });
376
+ });
377
+
378
+ if (!blocks.length) {
379
+ setPaneText(panel, normalized.markdown || JSON.stringify(result || {}, null, 2));
380
+ return;
381
+ }
382
+
383
+ blocks.forEach(({ page_id, block, pageDimensions }) => {
384
+ const button = document.createElement("button");
385
+ button.type = "button";
386
+ button.className = "result-block-card parse-block";
387
+ button.textContent = block.block_content || block.block_text || block.text || block.markdown || JSON.stringify(block);
388
+ button.addEventListener("click", () => {
389
+ if (block.block_bbox) {
390
+ onPick?.([{
391
+ page_id,
392
+ bbox: block.block_bbox,
393
+ source_width: pageDimensions.width,
394
+ source_height: pageDimensions.height,
395
+ }]);
396
+ } else {
397
+ onPick?.(null);
398
+ }
399
+ });
400
+ panel.append(button);
401
+ });
402
+ }
403
+
404
+ export function normalizeParsePayload(result) {
405
+ const payload = result && typeof result === "object" ? result : {};
406
+ const standardResult = payload.result && typeof payload.result === "object" ? payload.result : null;
407
+ if (!standardResult) {
408
+ return {
409
+ markdown: typeof payload.markdown === "string" ? payload.markdown : "",
410
+ pages: Array.isArray(payload.pages) ? payload.pages : [],
411
+ metadata: payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {},
412
+ full: payload,
413
+ };
414
+ }
415
+
416
+ return {
417
+ markdown: typeof standardResult.markdown === "string" ? standardResult.markdown : "",
418
+ pages: normalizeStandardParsePages(standardResult.pages),
419
+ metadata: normalizeStandardParseMetadata(payload),
420
+ full: payload,
421
+ };
422
+ }
423
+
424
+ function normalizeStandardParseMetadata(payload) {
425
+ const metadata = {};
426
+ ["code", "message", "file_type", "x_request_id", "metrics", "image_process"].forEach((key) => {
427
+ if (payload[key] !== undefined) metadata[key] = payload[key];
428
+ });
429
+ return metadata;
430
+ }
431
+
432
+ function normalizeStandardParsePages(pages) {
433
+ if (!Array.isArray(pages)) return [];
434
+ return pages
435
+ .filter((page) => page && typeof page === "object")
436
+ .map((page, index) => ({
437
+ page_id: page.page_id || index + 1,
438
+ page_index: index,
439
+ width: page.width,
440
+ height: page.height,
441
+ parsing_res_list: normalizeStandardParseBlocks(page.structured || page.content),
442
+ }));
443
+ }
444
+
445
+ function normalizeStandardParseBlocks(blocks) {
446
+ if (!Array.isArray(blocks)) return [];
447
+ return blocks
448
+ .filter((block) => block && typeof block === "object")
449
+ .map((block, index) => ({
450
+ block_id: block.id ?? index,
451
+ block_label: block.type,
452
+ block_content: block.text,
453
+ block_bbox: quadToBbox(block.pos),
454
+ }));
455
+ }
456
+
457
+ function quadToBbox(pos) {
458
+ if (!Array.isArray(pos) || pos.length < 4) return undefined;
459
+ const numbers = pos.map(Number).filter(Number.isFinite);
460
+ if (numbers.length < 4) return undefined;
461
+ if (numbers.length >= 8) {
462
+ const xs = numbers.filter((_, index) => index % 2 === 0);
463
+ const ys = numbers.filter((_, index) => index % 2 === 1);
464
+ return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
465
+ }
466
+ return numbers.slice(0, 4);
467
+ }
468
+
469
+ export function renderMarkdownView(markdown, panel) {
470
+ setPaneText(panel, markdown || "");
471
+ }
472
+
473
+ export function renderJsonView(value, panel) {
474
+ setPaneText(panel, JSON.stringify(value || {}, null, 2));
475
+ }
476
+
477
+ export function normalizeExtractPayload(result) {
478
+ const payload = result && typeof result === "object" ? result : {};
479
+ const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
480
+ const resultsSource = payload.results && typeof payload.results === "object"
481
+ ? payload.results
482
+ : (payload.data && typeof payload.data === "object" ? payload.data : payload);
483
+ const results = resultsSource && typeof resultsSource === "object" ? { ...resultsSource } : {};
484
+
485
+ if (results.fields && typeof results.fields === "object") {
486
+ const fields = results.fields;
487
+ delete results.fields;
488
+ Object.assign(results, fields);
489
+ }
490
+
491
+ ["source_width", "source_height"].forEach((key) => {
492
+ if (results[key] !== undefined && metadata[key] === undefined) {
493
+ metadata[key] = results[key];
494
+ delete results[key];
495
+ }
496
+ });
497
+
498
+ return {
499
+ results,
500
+ metadata,
501
+ full: {
502
+ results,
503
+ metadata,
504
+ },
505
+ };
506
+ }
507
+
508
+ export function renderPlaceholder(panel, message) {
509
+ if (!panel) return;
510
+ panel.replaceChildren();
511
+ const div = document.createElement("div");
512
+ div.className = "result-placeholder";
513
+ div.textContent = message;
514
+ panel.append(div);
515
+ }
516
+
517
+ function renderFieldCard(name, field, source, onPick) {
518
+ const button = document.createElement("button");
519
+ button.type = "button";
520
+ button.className = "result-field-card field-card";
521
+ const title = document.createElement("strong");
522
+ title.textContent = name;
523
+ const value = document.createElement("span");
524
+ value.textContent = typeof field === "object" && field !== null && "value" in field
525
+ ? textValue(field.value)
526
+ : textValue(field);
527
+ button.append(title, value);
528
+ button.addEventListener("click", () => {
529
+ const boxes = field && typeof field === "object"
530
+ ? withInheritedDimensions(field.bboxes || field.bbox || field.block_bbox || field.box, field)
531
+ : null;
532
+ onPick?.(boxes && boxes.length ? boxes : null, source);
533
+ });
534
+ return button;
535
+ }
536
+
537
+ export function renderExtractCards(result, panel, { source = "cloud", onPick } = {}) {
538
+ if (!panel) return;
539
+ panel.replaceChildren();
540
+ const data = normalizeExtractPayload(result).results;
541
+ const tableBboxes = data._table_bboxes || {};
542
+ let rendered = 0;
543
+
544
+ Object.entries(data).forEach(([key, value]) => {
545
+ if (key === "tables" || key === "_table_bboxes") return;
546
+ panel.append(renderFieldCard(key, value, source, onPick));
547
+ rendered += 1;
548
+ });
549
+
550
+ Object.entries(data.tables || {}).forEach(([tableName, rows]) => {
551
+ const section = document.createElement("section");
552
+ section.className = "result-table-card table-card";
553
+ const title = actionButton(tableName, () => {
554
+ if (tableBboxes[tableName]) {
555
+ onPick?.(withInheritedDimensions(
556
+ tableBboxes[tableName].bboxes || tableBboxes[tableName].bbox || tableBboxes[tableName],
557
+ tableBboxes[tableName],
558
+ ), source);
559
+ } else {
560
+ onPick?.(null, source);
561
+ }
562
+ });
563
+ const pre = document.createElement("pre");
564
+ pre.textContent = JSON.stringify(rows, null, 2);
565
+ section.append(title, pre);
566
+ panel.append(section);
567
+ rendered += 1;
568
+ });
569
+
570
+ if (!rendered) setPaneText(panel, JSON.stringify(data, null, 2));
571
+ }
572
+
573
+ export function bindResultTabs(tabList, onChange) {
574
+ if (!tabList) return;
575
+ const buttons = Array.from(tabList.querySelectorAll("[data-result-tab]"));
576
+ const panels = buttons
577
+ .map((button) => document.querySelector(`[data-result-panel="${button.dataset.resultTab}"]`))
578
+ .filter(Boolean);
579
+
580
+ const setActiveTab = (tabName) => {
581
+ buttons.forEach((button) => {
582
+ const isActive = button.dataset.resultTab === tabName;
583
+ button.classList.toggle("is-active", isActive);
584
+ button.setAttribute("aria-selected", String(isActive));
585
+ button.tabIndex = isActive ? 0 : -1;
586
+ });
587
+ panels.forEach((panel) => {
588
+ panel.hidden = panel.dataset.resultPanel !== tabName;
589
+ });
590
+ onChange?.(tabName);
591
+ };
592
+
593
+ const handleKeydown = (event) => {
594
+ const currentIndex = buttons.indexOf(event.currentTarget);
595
+ let nextIndex;
596
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
597
+ nextIndex = (currentIndex + 1) % buttons.length;
598
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
599
+ nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
600
+ } else if (event.key === "Home") {
601
+ nextIndex = 0;
602
+ } else if (event.key === "End") {
603
+ nextIndex = buttons.length - 1;
604
+ } else {
605
+ return;
606
+ }
607
+ event.preventDefault();
608
+ const nextButton = buttons[nextIndex];
609
+ setActiveTab(nextButton.dataset.resultTab);
610
+ nextButton.focus();
611
+ };
612
+
613
+ buttons.forEach((button) => {
614
+ button.setAttribute("role", "tab");
615
+ button.addEventListener("click", () => setActiveTab(button.dataset.resultTab));
616
+ button.addEventListener("keydown", handleKeydown);
617
+ });
618
+ panels.forEach((panel) => panel.setAttribute("role", "tabpanel"));
619
+ if (buttons.length) setActiveTab(buttons[0].dataset.resultTab);
620
+ }
621
+
622
+ export function downloadText(text, filename = "docslight-result.txt") {
623
+ if (!text) return;
624
+ const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
625
+ downloadBlob(blob, filename);
626
+ }
627
+
628
+ export function downloadBlob(blob, filename = "docslight-result.bin") {
629
+ if (!blob) return;
630
+ const url = URL.createObjectURL(blob);
631
+ const anchor = document.createElement("a");
632
+ anchor.href = url;
633
+ anchor.download = filename;
634
+ anchor.click();
635
+ URL.revokeObjectURL(url);
636
+ }
637
+
638
+ export function postForm(endpoint, body) {
639
+ return fetch(endpoint, { method: "POST", body }).then(async (response) => {
640
+ const contentType = response.headers.get("content-type") || "";
641
+ if (response.ok && contentType.includes("application/zip")) {
642
+ const disposition = response.headers.get("content-disposition") || "";
643
+ const filenameMatch = disposition.match(/filename="?([^";]+)"?/i);
644
+ return {
645
+ success: true,
646
+ blob: await response.blob(),
647
+ filename: filenameMatch?.[1] || "docslight-parse.zip",
648
+ };
649
+ }
650
+
651
+ let payload = null;
652
+ try {
653
+ payload = await response.json();
654
+ } catch {
655
+ payload = {};
656
+ }
657
+ if (!response.ok || payload.success === false) {
658
+ throw new Error(payload.error || `Request failed with HTTP ${response.status}`);
659
+ }
660
+ return payload;
661
+ });
662
+ }
663
+
664
+ export function setFormError(formError, message) {
665
+ if (!formError) return;
666
+ formError.textContent = message || "";
667
+ formError.hidden = !message;
668
+ }