@rightkit/document-core 0.1.2 → 0.2.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +31 -7
  3. package/dist/app/chrome.js +1 -1
  4. package/dist/app/documentPrinting.d.ts +0 -6
  5. package/dist/app/documentPrinting.js +0 -18
  6. package/dist/app/shell/nativeMenu.d.ts +3 -2
  7. package/dist/app/shell/nativeMenu.js +8 -5
  8. package/dist/documents/perfEventCounters.d.ts +0 -8
  9. package/dist/documents/perfEventCounters.js +2 -1
  10. package/dist/documents/preview/previewFrame.d.ts +2 -1
  11. package/dist/documents/preview/previewFrame.js +9 -7
  12. package/dist/documents/searchUtils.js +1 -1
  13. package/dist/host.d.ts +36 -0
  14. package/dist/host.js +54 -0
  15. package/dist/index.d.ts +1 -46
  16. package/dist/index.js +1 -46
  17. package/dist/settings/themes.js +1 -1
  18. package/dist/state/bridgeEvents.d.ts +1 -1
  19. package/dist/state/bridgeEvents.js +7 -4
  20. package/dist/state/recentUrls.js +4 -3
  21. package/dist/state/tauriBridge.d.ts +3 -47
  22. package/dist/state/tauriBridge.js +6 -162
  23. package/dist/styles.css +2 -2
  24. package/package.json +16 -87
  25. package/dist/app/shortcutChords.d.ts +0 -6
  26. package/dist/app/shortcutChords.js +0 -14
  27. package/dist/app/useCloseFindOnSurfaceChange.d.ts +0 -14
  28. package/dist/app/useCloseFindOnSurfaceChange.js +0 -23
  29. package/dist/app/useEditText.d.ts +0 -6
  30. package/dist/app/useEditText.js +0 -10
  31. package/dist/appName.d.ts +0 -2
  32. package/dist/appName.js +0 -8
  33. package/dist/documents/FullscreenIcon.d.ts +0 -4
  34. package/dist/documents/FullscreenIcon.js +0 -7
  35. package/dist/documents/mdx/mdxRuntimeScript.d.ts +0 -2
  36. package/dist/documents/mdx/mdxRuntimeScript.js +0 -3
  37. package/dist/documents/mdx/mdxRuntimeScript.txt +0 -584
  38. package/dist/documents/pdf/MacPdfReadPane.d.ts +0 -23
  39. package/dist/documents/pdf/MacPdfReadPane.js +0 -708
  40. package/dist/documents/pdf/NativePdfDocument.d.ts +0 -9
  41. package/dist/documents/pdf/NativePdfDocument.js +0 -41
  42. package/dist/documents/pdf/PdfInlineTextEditor.d.ts +0 -53
  43. package/dist/documents/pdf/PdfInlineTextEditor.js +0 -225
  44. package/dist/documents/pdf/PdfOcrLayer.d.ts +0 -4
  45. package/dist/documents/pdf/PdfOcrLayer.js +0 -30
  46. package/dist/documents/pdf/PdfOutline.d.ts +0 -6
  47. package/dist/documents/pdf/PdfOutline.js +0 -12
  48. package/dist/documents/pdf/PdfPasswordPrompt.d.ts +0 -5
  49. package/dist/documents/pdf/PdfPasswordPrompt.js +0 -24
  50. package/dist/documents/pdf/PdfThumbnails.d.ts +0 -8
  51. package/dist/documents/pdf/PdfThumbnails.js +0 -201
  52. package/dist/documents/pdf/PdfToolbar.d.ts +0 -47
  53. package/dist/documents/pdf/PdfToolbar.js +0 -76
  54. package/dist/documents/pdf/PdfViewer.d.ts +0 -36
  55. package/dist/documents/pdf/PdfViewer.js +0 -1522
  56. package/dist/documents/pdf/PdfViewerBody.d.ts +0 -22
  57. package/dist/documents/pdf/PdfViewerBody.js +0 -42
  58. package/dist/documents/pdf/macPdfNativeBridge.d.ts +0 -118
  59. package/dist/documents/pdf/macPdfNativeBridge.js +0 -134
  60. package/dist/documents/pdf/nearestVisiblePdfPage.d.ts +0 -2
  61. package/dist/documents/pdf/nearestVisiblePdfPage.js +0 -22
  62. package/dist/documents/pdf/pdfBench.d.ts +0 -88
  63. package/dist/documents/pdf/pdfBench.js +0 -535
  64. package/dist/documents/pdf/pdfBenchAutopilot.d.ts +0 -48
  65. package/dist/documents/pdf/pdfBenchAutopilot.js +0 -154
  66. package/dist/documents/pdf/pdfBenchTransport.d.ts +0 -21
  67. package/dist/documents/pdf/pdfBenchTransport.js +0 -233
  68. package/dist/documents/pdf/pdfEditModel.d.ts +0 -294
  69. package/dist/documents/pdf/pdfEditModel.js +0 -105
  70. package/dist/documents/pdf/pdfEditor.d.ts +0 -100
  71. package/dist/documents/pdf/pdfEditor.js +0 -68
  72. package/dist/documents/pdf/pdfOcr.d.ts +0 -36
  73. package/dist/documents/pdf/pdfOcr.js +0 -88
  74. package/dist/documents/pdf/pdfOcrEdit.d.ts +0 -32
  75. package/dist/documents/pdf/pdfOcrEdit.js +0 -138
  76. package/dist/documents/pdf/pdfPrinting.d.ts +0 -28
  77. package/dist/documents/pdf/pdfPrinting.js +0 -110
  78. package/dist/documents/pdf/pdfSave.d.ts +0 -87
  79. package/dist/documents/pdf/pdfSave.js +0 -250
  80. package/dist/documents/pdf/pdfShared.d.ts +0 -16
  81. package/dist/documents/pdf/pdfShared.js +0 -20
  82. package/dist/documents/pdf/pdfToMarkdown.d.ts +0 -11
  83. package/dist/documents/pdf/pdfToMarkdown.js +0 -67
  84. package/dist/documents/pdf/pdfjsLibGlobal.d.ts +0 -1
  85. package/dist/documents/pdf/pdfjsLibGlobal.js +0 -12
  86. package/dist/documents/pdf/toolbarIcons.d.ts +0 -18
  87. package/dist/documents/pdf/toolbarIcons.js +0 -31
  88. package/dist/documents/pdf/usePdfDocumentLoader.d.ts +0 -40
  89. package/dist/documents/pdf/usePdfDocumentLoader.js +0 -243
  90. package/dist/documents/pdf/usePdfEditSession.d.ts +0 -88
  91. package/dist/documents/pdf/usePdfEditSession.js +0 -341
  92. package/dist/documents/pdf/usePdfEditor.d.ts +0 -59
  93. package/dist/documents/pdf/usePdfEditor.js +0 -317
  94. package/dist/documents/pdf/usePdfFit.d.ts +0 -19
  95. package/dist/documents/pdf/usePdfFit.js +0 -55
  96. package/dist/documents/pdf/usePdfOcr.d.ts +0 -38
  97. package/dist/documents/pdf/usePdfOcr.js +0 -337
  98. package/dist/documents/pdf/usePdfPreviewRecompose.d.ts +0 -28
  99. package/dist/documents/pdf/usePdfPreviewRecompose.js +0 -256
  100. package/dist/documents/pdf/usePdfSavePlan.d.ts +0 -61
  101. package/dist/documents/pdf/usePdfSavePlan.js +0 -287
  102. package/dist/documents/pdf/usePdfSearch.d.ts +0 -31
  103. package/dist/documents/pdf/usePdfSearch.js +0 -367
  104. package/dist/documents/pdf/usePdfViewControls.d.ts +0 -72
  105. package/dist/documents/pdf/usePdfViewControls.js +0 -152
  106. package/dist/documents/scopedSelectAll.d.ts +0 -5
  107. package/dist/documents/scopedSelectAll.js +0 -77
  108. package/dist/documents/txt/cmScroll.d.ts +0 -11
  109. package/dist/documents/txt/cmScroll.js +0 -52
  110. package/dist/documents/useMirroredEditText.d.ts +0 -15
  111. package/dist/documents/useMirroredEditText.js +0 -29
  112. package/dist/documents/useSplitResize.d.ts +0 -7
  113. package/dist/documents/useSplitResize.js +0 -37
  114. package/dist/settings/rk-settings.css +0 -38
  115. package/dist/settings/sections/EditorSection.d.ts +0 -1
  116. package/dist/settings/sections/EditorSection.js +0 -18
  117. package/dist/settings/sections/FileAssociationsSection.d.ts +0 -1
  118. package/dist/settings/sections/FileAssociationsSection.js +0 -5
  119. package/dist/settings/sections/MacFileAssociationsBlock.d.ts +0 -33
  120. package/dist/settings/sections/MacFileAssociationsBlock.js +0 -151
  121. package/dist/settings/sections/ThemeSection.d.ts +0 -1
  122. package/dist/settings/sections/ThemeSection.js +0 -31
  123. package/dist/settings/sections/TypographySection.d.ts +0 -1
  124. package/dist/settings/sections/TypographySection.js +0 -19
  125. package/dist/settings/sections/onOff.d.ts +0 -11
  126. package/dist/settings/sections/onOff.js +0 -8
@@ -1,243 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState, } from "react";
2
- import * as pdfjs from "pdfjs-dist";
3
- import { acquireResource } from "../../app/resourceCensus.js";
4
- import { beginPdfBench, getPdfBenchLoadMode, markPdfBenchDocumentReady, markPdfBenchError, } from "./pdfBench.js";
5
- import { preparePdfLoading } from "./pdfBenchTransport.js";
6
- import { pdfWasmUrl } from "@rightkit/pdf-ui";
7
- /** Benchmark-only password injection; production & manual unlocks always use the prompt. */
8
- export function getPdfBenchPassword() {
9
- if (!import.meta.env?.DEV || import.meta.env?.VITE_PDF_BENCH_AUTOPILOT !== "1")
10
- return null;
11
- const password = import.meta.env?.VITE_PDF_BENCH_PASSWORD;
12
- return password ? password : null;
13
- }
14
- export function usePdfDocumentLoader({ documentPath, url, initialView, pdfRef, renderedRef, slotRefs, visibleRef, restoredRef, reportReadyRef, setCurrent, setScale, setRotation, setFitMode, }) {
15
- const [pdfDoc, setPdfDoc] = useState(null);
16
- const [numPages, setNumPages] = useState(0);
17
- const [base, setBase] = useState(null);
18
- const [pageSizes, setPageSizes] = useState(new Map());
19
- const [error, setError] = useState(null);
20
- const [renderErrors, setRenderErrors] = useState(new Map());
21
- const [pwPrompt, setPwPrompt] = useState(null);
22
- const [outline, setOutline] = useState(null);
23
- // The pdf.js loading task currently backing `pdfDoc`/`pdfRef.current` — either this effect's own
24
- // initial-load task, or a post-commit `replaceDocument` task (§10.3 reload after Save). Whichever
25
- // task is here must only be destroyed once React has committed the DIFFERENT `pdfDoc` that
26
- // superseded it (the promote effect below) — destroying a task while page layers still hold the
27
- // document it produced is what previously crashed the viewer to its ErrorBoundary.
28
- const activeTaskRef = useRef(null);
29
- // Set by `replaceDocument`, consumed by the promote effect once `pdfDoc` actually changes.
30
- const pendingReplacementTaskRef = useRef(null);
31
- const replaceDocument = useCallback((doc, task) => {
32
- pdfRef.current = doc;
33
- pendingReplacementTaskRef.current = task;
34
- setPdfDoc(doc);
35
- }, [pdfRef]);
36
- // Promotes a pending replacement to "active" — and disposes whatever it superseded — only after
37
- // React has committed `pdfDoc` to the new document. Also runs (as a no-op) after the ordinary
38
- // initial load's `setPdfDoc`, since `pendingReplacementTaskRef` is still null at that point.
39
- // `pdfDoc` is the intentional trigger (the effect must run after React commits the new document),
40
- // not a value read inside the effect body.
41
- // biome-ignore lint/correctness/useExhaustiveDependencies: see comment above.
42
- useEffect(() => {
43
- const pending = pendingReplacementTaskRef.current;
44
- if (!pending || pending === activeTaskRef.current)
45
- return;
46
- const stale = activeTaskRef.current;
47
- activeTaskRef.current = pending;
48
- pendingReplacementTaskRef.current = null;
49
- if (stale)
50
- void stale.destroy();
51
- }, [pdfDoc]);
52
- useEffect(() => {
53
- if (!url)
54
- return;
55
- let cancelled = false;
56
- let passwordCancelled = false;
57
- renderedRef.current.forEach((cleanup) => cleanup());
58
- renderedRef.current.clear();
59
- slotRefs.current.clear();
60
- visibleRef.current.clear();
61
- pdfRef.current = null;
62
- restoredRef.current = false;
63
- reportReadyRef.current = false;
64
- setError(null);
65
- setRenderErrors(new Map());
66
- setNumPages(0);
67
- setBase(null);
68
- setPageSizes(new Map());
69
- setCurrent(1);
70
- setPwPrompt(null);
71
- setOutline(null);
72
- setPdfDoc(null);
73
- const controller = new AbortController();
74
- let task = null;
75
- let preparedLoad = null;
76
- let loadedDoc = null;
77
- // One document proxy + its worker per open PDF. Released in the effect cleanup below so a
78
- // cold-unmounted PDF pane provably stops counting against resident cost.
79
- let releaseDocumentCensus = null;
80
- let releaseWorkerCensus = null;
81
- let finalized = false;
82
- const finalize = () => {
83
- if (finalized)
84
- return;
85
- finalized = true;
86
- controller.abort();
87
- if (preparedLoad)
88
- preparedLoad.dispose();
89
- else if (task)
90
- void task.destroy();
91
- if (activeTaskRef.current === task)
92
- activeTaskRef.current = null;
93
- if (pendingReplacementTaskRef.current === task)
94
- pendingReplacementTaskRef.current = null;
95
- releaseDocumentCensus?.();
96
- releaseDocumentCensus = null;
97
- releaseWorkerCensus?.();
98
- releaseWorkerCensus = null;
99
- if (pdfRef.current && pdfRef.current === loadedDoc)
100
- pdfRef.current = null;
101
- };
102
- const load = async () => {
103
- const mode = getPdfBenchLoadMode();
104
- await beginPdfBench(documentPath, pdfjs.version, mode);
105
- if (cancelled) {
106
- const error = new Error("PDF load cancelled");
107
- error.name = "AbortError";
108
- throw error;
109
- }
110
- preparedLoad = await preparePdfLoading({
111
- url,
112
- mode,
113
- signal: controller.signal,
114
- wasmUrl: pdfWasmUrl(),
115
- });
116
- task = preparedLoad.task;
117
- releaseWorkerCensus = acquireResource({
118
- kind: "pdf-worker",
119
- documentPath,
120
- format: "pdf",
121
- });
122
- task.onPassword = (updatePassword, reason) => {
123
- const benchPassword = getPdfBenchPassword();
124
- if (benchPassword) {
125
- updatePassword(benchPassword);
126
- return;
127
- }
128
- setPwPrompt({
129
- retry: reason === 2,
130
- onSubmit: (pw) => {
131
- setPwPrompt(null);
132
- updatePassword(pw);
133
- },
134
- onCancel: () => {
135
- passwordCancelled = true;
136
- setPwPrompt(null);
137
- setError("Password required to open this PDF.");
138
- finalize();
139
- },
140
- });
141
- };
142
- return preparedLoad.promise;
143
- };
144
- load()
145
- .then(async (doc) => {
146
- if (cancelled || finalized)
147
- return;
148
- markPdfBenchDocumentReady(documentPath);
149
- setPwPrompt(null);
150
- loadedDoc = doc;
151
- pdfRef.current = doc;
152
- activeTaskRef.current = task;
153
- releaseDocumentCensus = acquireResource({
154
- kind: "pdf-document",
155
- documentPath,
156
- format: "pdf",
157
- });
158
- setPdfDoc(doc);
159
- setNumPages(doc.numPages);
160
- doc
161
- .getOutline()
162
- .then((o) => {
163
- if (!cancelled && !finalized)
164
- setOutline(o ?? []);
165
- })
166
- .catch(() => {
167
- if (!cancelled && !finalized)
168
- setOutline([]);
169
- });
170
- const first = await doc.getPage(1);
171
- const v = first.getViewport({ scale: 1 });
172
- if (!cancelled && !finalized) {
173
- setBase({ w: v.width, h: v.height });
174
- setRotation((((initialView?.rotation ?? 0) % 360) + 360) % 360);
175
- if (initialView?.fit)
176
- setFitMode(initialView.fit);
177
- else if (initialView?.scale) {
178
- setScale(initialView.scale);
179
- setFitMode(null);
180
- }
181
- }
182
- })
183
- .catch((e) => {
184
- if (!cancelled && !passwordCancelled && e?.name !== "AbortError") {
185
- finalize();
186
- if (loadedDoc) {
187
- pdfRef.current = null;
188
- setPdfDoc(null);
189
- setNumPages(0);
190
- }
191
- markPdfBenchError(documentPath, e);
192
- setError(String(e?.message ?? e));
193
- }
194
- });
195
- return () => {
196
- cancelled = true;
197
- renderedRef.current.forEach((cleanup) => cleanup());
198
- renderedRef.current.clear();
199
- finalize();
200
- // A post-commit `replaceDocument` call may have swapped in a newer task after the initial
201
- // load above finished; dispose it too (and any not-yet-promoted pending one) so a document
202
- // switch/unmount can't leak its pdf.js worker.
203
- if (activeTaskRef.current && activeTaskRef.current !== task) {
204
- void activeTaskRef.current.destroy();
205
- }
206
- if (pendingReplacementTaskRef.current &&
207
- pendingReplacementTaskRef.current !== activeTaskRef.current) {
208
- void pendingReplacementTaskRef.current.destroy();
209
- }
210
- pdfRef.current = null;
211
- };
212
- }, [
213
- documentPath,
214
- url,
215
- slotRefs.current.clear,
216
- setScale,
217
- visibleRef.current.clear,
218
- setRotation,
219
- setFitMode,
220
- setCurrent,
221
- restoredRef,
222
- reportReadyRef,
223
- pdfRef,
224
- renderedRef.current.forEach,
225
- initialView?.scale,
226
- renderedRef.current.clear,
227
- initialView?.rotation,
228
- initialView?.fit,
229
- ]);
230
- return {
231
- pdfDoc,
232
- numPages,
233
- base,
234
- pageSizes,
235
- setPageSizes,
236
- error,
237
- renderErrors,
238
- setRenderErrors,
239
- pwPrompt,
240
- outline,
241
- replaceDocument,
242
- };
243
- }
@@ -1,88 +0,0 @@
1
- import { type InspectPdfPage, type ListPdfFonts, type PdfEditCommand, type PdfEditSession, type PdfFontCatalogEntry, type PdfPageGraph, type PdfPoint, type PdfColor, type PdfRect, type PdfSelection, type PdfTextAppearance } from "./pdfEditModel.js";
2
- export type PdfEditSessionTool = PdfEditSession["tool"];
3
- /** Bridge functions this hook needs, injected so it can be unit-tested without the real
4
- * `tauriBridge.ts` (§12: real bridge lands at Dispatch D2). */
5
- export interface PdfEditSessionBridge {
6
- inspectPdfPage: InspectPdfPage;
7
- listPdfFonts: ListPdfFonts;
8
- }
9
- export interface UsePdfEditSessionOptions {
10
- bridge: PdfEditSessionBridge;
11
- /** Authorized path passed to `inspectPdfPage` (§5.2). */
12
- sourcePath: string;
13
- }
14
- export interface PdfEditSessionController {
15
- session: PdfEditSession;
16
- /** Mutable latest session snapshot for async save callbacks; avoids stale render closures. */
17
- sessionRef: {
18
- current: PdfEditSession;
19
- };
20
- dirty: boolean;
21
- /** Enters Edit PDF mode (§3.2). Idempotent. */
22
- enterEditMode: () => void;
23
- /** §3.2 "Done": commits an active valid draft, then returns to View mode. Never saves. */
24
- exitEditMode: () => void;
25
- setTool: (tool: PdfEditSessionTool) => void;
26
- /** Selecting a new target commits any active valid draft first (§3.2 "Clicking another page
27
- * target commits active valid draft, then selects target"). */
28
- selectIsland: (selection: PdfSelection) => void;
29
- clearSelection: () => void;
30
- /** Seeds a `sourceEdit` draft from the current selection's island text/appearance. */
31
- startSourceEditDraft: (params: {
32
- id: string;
33
- rect: PdfRect;
34
- text: string;
35
- appearance: PdfTextAppearance;
36
- availableBounds: PdfRect;
37
- }) => void;
38
- /** Seeds an `addText` draft at a clicked PDF-space anchor (§6, `PdfAddText.anchor`). */
39
- startAddTextDraft: (params: {
40
- id: string;
41
- page: number;
42
- anchor: PdfPoint;
43
- rect: PdfRect;
44
- appearance: PdfTextAppearance;
45
- }) => void;
46
- /** Seeds a persisted OCR correction overlay from recognized line geometry. */
47
- startOcrEditDraft: (params: {
48
- id: string;
49
- page: number;
50
- lineId: string;
51
- rect: PdfRect;
52
- text: string;
53
- appearance: PdfTextAppearance;
54
- coverColor: PdfColor;
55
- }) => void;
56
- updateDraftText: (text: string) => void;
57
- updateDraftAppearance: (appearance: PdfTextAppearance) => void;
58
- /** External layout measurement sets this (§3.3); this hook does not measure text itself. */
59
- setDraftOverflow: (overflow: boolean) => void;
60
- /** §3.2 "Clicking outside active text commits valid draft" / Done / selecting elsewhere.
61
- * No-ops when there is no draft. A no-op (unchanged) `sourceEdit` draft commits nothing —
62
- * matches §6's dirty semantics: an identical before/after would not need a history command.
63
- * Returns the command that was applied, or null if nothing was committed. */
64
- commitDraft: () => PdfEditCommand | null;
65
- /** §3.2 Escape: "discards active draft without a history command." */
66
- cancelDraft: () => void;
67
- /** Applies `command` to `sourceCommands` and recomputes `sourceDigest`. Exposed directly (not
68
- * only via `commitDraft`) so an external pdf.js command-history wrapper
69
- * (`pushSourceCommand`, pdfEditor.ts) can drive apply/undo/redo through this same session. */
70
- apply: (command: PdfEditCommand) => void;
71
- /** Reverts the effect of `apply(command)` — removes the command by id. */
72
- revert: (command: PdfEditCommand) => void;
73
- /** Called after apply/revert reached through undo/redo (§9.4 in-memory preview recompose hook).
74
- * This hook does not call `compose_pdf_preview` itself — it only tracks a subscriber list so a
75
- * caller (PdfViewer) can react to every source-history mutation. */
76
- onPublish: (listener: () => void) => () => void;
77
- publish: () => void;
78
- /** Marks the current `sourceDigest` as saved and clears `annotationDirty` — call only after
79
- * `commit_pdf_save` succeeds (§10.3: "checkpoints sourceDigest"). */
80
- checkpoint: () => void;
81
- setAnnotationDirty: (dirty: boolean) => void;
82
- inspectPage: (page: number) => Promise<PdfPageGraph>;
83
- listFonts: () => Promise<PdfFontCatalogEntry[]>;
84
- /** Resets session state to a fresh document (e.g. after `commit_pdf_save` reloads the active
85
- * document, §10.3). Clears commands, selection, draft, and checkpoints to empty history. */
86
- resetForDocument: () => void;
87
- }
88
- export declare function usePdfEditSession(options: UsePdfEditSessionOptions): PdfEditSessionController;
@@ -1,341 +0,0 @@
1
- // Edit PDF mode session state (docs/PDF-EDITOR-NOW.md §3.2, §6, §12 "usePdfEditSession.ts").
2
- //
3
- // Owns mode/tool/selection/draft lifecycle, the ordered `sourceCommands` list, and digest/dirty
4
- // checkpoint tracking. Does not own pdf.js's `AnnotationEditorUIManager` command history (§6.2,
5
- // `pushSourceCommand` in pdfEditor.ts) or the real Tauri bridge (`tauriBridge.ts`, Dispatch D2) —
6
- // this hook accepts bridge functions as injected parameters (§12's "injected-bridge-function
7
- // pattern", typed in pdfEditModel.ts) so it is testable today against mocks. A caller that wires
8
- // pdf.js's shared history can drive it through `apply`/`revert`/`publish`, which together satisfy
9
- // pdfEditor.ts's `PdfSourceCommandSession<PdfEditCommand>` shape.
10
- //
11
- // No disk writes happen anywhere in this file (§3.2: "It never saves"). Save/checkpointing is
12
- // triggered externally via `checkpoint()` once `commit_pdf_save` (§10.3) succeeds.
13
- import { useCallback, useMemo, useRef, useState } from "react";
14
- import { computeSourceDigest, isSessionDirty, } from "./pdfEditModel.js";
15
- const INITIAL_TOOL = "selectText";
16
- function initialSession() {
17
- const digest = computeSourceDigest([]);
18
- return {
19
- mode: "view",
20
- tool: INITIAL_TOOL,
21
- selection: null,
22
- draft: null,
23
- sourceCommands: [],
24
- sourceDigest: digest,
25
- savedSourceDigest: digest,
26
- annotationDirty: false,
27
- };
28
- }
29
- function draftIsNoOp(draft) {
30
- if (draft.kind === "addText")
31
- return draft.text.trim().length === 0;
32
- const sameText = draft.text === draft.originalText;
33
- const sameAppearance = !!draft.originalAppearance &&
34
- draft.appearance.fontId === draft.originalAppearance.fontId &&
35
- draft.appearance.sizePt === draft.originalAppearance.sizePt &&
36
- draft.appearance.color.r === draft.originalAppearance.color.r &&
37
- draft.appearance.color.g === draft.originalAppearance.color.g &&
38
- draft.appearance.color.b === draft.originalAppearance.color.b &&
39
- draft.appearance.color.alpha === draft.originalAppearance.color.alpha;
40
- return sameText && sameAppearance;
41
- }
42
- function draftToCommand(draft) {
43
- if (draft.kind === "sourceEdit") {
44
- if (!draft.locator ||
45
- !draft.originalText ||
46
- !draft.originalAppearance ||
47
- !draft.availableBounds) {
48
- return null;
49
- }
50
- const command = {
51
- kind: "replaceSourceText",
52
- id: draft.id,
53
- locator: draft.locator,
54
- before: { text: draft.originalText, appearance: draft.originalAppearance },
55
- after: { text: draft.text, appearance: draft.appearance },
56
- availableBounds: draft.availableBounds,
57
- };
58
- return command;
59
- }
60
- if (draft.kind === "ocrEdit") {
61
- if (!draft.lineId || !draft.coverColor)
62
- return null;
63
- return {
64
- kind: "correctOcrText",
65
- id: draft.id,
66
- page: draft.page,
67
- lineId: draft.lineId,
68
- rect: draft.rect,
69
- text: draft.text,
70
- appearance: draft.appearance,
71
- coverColor: draft.coverColor,
72
- backgroundProof: "nearUniform",
73
- };
74
- }
75
- if (!draft.anchor)
76
- return null;
77
- const command = {
78
- kind: "addText",
79
- id: draft.id,
80
- page: draft.page,
81
- anchor: draft.anchor,
82
- text: draft.text,
83
- appearance: draft.appearance,
84
- };
85
- return command;
86
- }
87
- export function usePdfEditSession(options) {
88
- const { bridge, sourcePath } = options;
89
- const [session, setSession] = useState(initialSession);
90
- const sessionRef = useRef(session);
91
- sessionRef.current = session;
92
- const listenersRef = useRef(new Set());
93
- const fontsCacheRef = useRef(null);
94
- // Mirrors `session.draft`, refreshed every render, but ALSO nulled synchronously inside
95
- // `commitDraftInternal`/`cancelDraft` themselves (not only via their `setSession` updaters). A
96
- // caller (PdfViewer.tsx) can call `handleCommitDraft()` and then `selectIsland`/`exitEditMode`/an
97
- // annotation-mode switch in the same synchronous event handler — each of which also commits
98
- // internally — and React does not guarantee `session` (the render-closure value) reflects the
99
- // first commit's `draft: null` before the second one reads it. Reading/nulling this ref instead
100
- // means the second call sees the draft already cleared, instead of re-deriving and re-appending
101
- // the same command a second time before React ever re-renders.
102
- const draftRef = useRef(session.draft);
103
- draftRef.current = session.draft;
104
- // Mirrors `session.selection`, but is ALSO updated synchronously inside `selectIsland`/
105
- // `clearSelection` (not only via their `setSession` updaters). `startSourceEditDraft` reads it
106
- // instead of `session.selection`: PdfViewer calls `selectIsland(...)` and then
107
- // `startSourceEditDraft(...)` in the same synchronous click handler, and React has not
108
- // re-rendered between them, so the render-closure `session.selection` is still the pre-click
109
- // value (null on a first click) — which made `startSourceEditDraft` bail and no editor open on
110
- // the first click of an existing text run.
111
- const selectionRef = useRef(session.selection);
112
- selectionRef.current = session.selection;
113
- const dirty = useMemo(() => isSessionDirty(session), [session]);
114
- const publish = useCallback(() => {
115
- for (const listener of listenersRef.current)
116
- listener();
117
- }, []);
118
- const onPublish = useCallback((listener) => {
119
- listenersRef.current.add(listener);
120
- return () => {
121
- listenersRef.current.delete(listener);
122
- };
123
- }, []);
124
- const apply = useCallback((command) => {
125
- setSession((prev) => {
126
- const sourceCommands = [...prev.sourceCommands, command];
127
- return { ...prev, sourceCommands, sourceDigest: computeSourceDigest(sourceCommands) };
128
- });
129
- }, []);
130
- const revert = useCallback((command) => {
131
- setSession((prev) => {
132
- const index = prev.sourceCommands.findIndex((existing) => existing.id === command.id);
133
- if (index === -1)
134
- return prev;
135
- const sourceCommands = prev.sourceCommands.slice();
136
- sourceCommands.splice(index, 1);
137
- return { ...prev, sourceCommands, sourceDigest: computeSourceDigest(sourceCommands) };
138
- });
139
- }, []);
140
- const enterEditMode = useCallback(() => {
141
- setSession((prev) => (prev.mode === "edit" ? prev : { ...prev, mode: "edit" }));
142
- }, []);
143
- // Reads `draftRef.current` (not `session.draft` from the render-time closure, and not from
144
- // inside the `setSession` updater) so the command this returns is computed synchronously: React
145
- // does not guarantee a functional `setSession` updater runs before this function returns, so a
146
- // side effect set inside one cannot be relied on as this function's return value — and reading
147
- // `session.draft` directly would go stale across two synchronous calls in the same handler (see
148
- // `draftRef`'s doc comment above).
149
- const commitDraftInternal = useCallback(() => {
150
- const draft = draftRef.current;
151
- if (!draft)
152
- return null;
153
- draftRef.current = null;
154
- if (draftIsNoOp(draft)) {
155
- const current = sessionRef.current;
156
- if (current.draft)
157
- sessionRef.current = { ...current, draft: null };
158
- setSession((prev) => (prev.draft ? { ...prev, draft: null } : prev));
159
- return null;
160
- }
161
- const command = draftToCommand(draft);
162
- if (!command) {
163
- const current = sessionRef.current;
164
- if (current.draft)
165
- sessionRef.current = { ...current, draft: null };
166
- setSession((prev) => (prev.draft ? { ...prev, draft: null } : prev));
167
- return null;
168
- }
169
- const current = sessionRef.current;
170
- const sourceCommands = [...current.sourceCommands, command];
171
- sessionRef.current = {
172
- ...current,
173
- draft: null,
174
- sourceCommands,
175
- sourceDigest: computeSourceDigest(sourceCommands),
176
- };
177
- setSession((prev) => {
178
- const sourceCommands = [...prev.sourceCommands, command];
179
- return {
180
- ...prev,
181
- draft: null,
182
- sourceCommands,
183
- sourceDigest: computeSourceDigest(sourceCommands),
184
- };
185
- });
186
- return command;
187
- }, []);
188
- const commitDraft = useCallback(() => {
189
- const command = commitDraftInternal();
190
- if (command)
191
- publish();
192
- return command;
193
- }, [commitDraftInternal, publish]);
194
- const cancelDraft = useCallback(() => {
195
- draftRef.current = null;
196
- const current = sessionRef.current;
197
- if (current.draft)
198
- sessionRef.current = { ...current, draft: null };
199
- setSession((prev) => (prev.draft ? { ...prev, draft: null } : prev));
200
- }, []);
201
- const exitEditMode = useCallback(() => {
202
- commitDraftInternal();
203
- setSession((prev) => ({ ...prev, mode: "view", draft: null }));
204
- }, [commitDraftInternal]);
205
- const setTool = useCallback((tool) => {
206
- setSession((prev) => (prev.tool === tool ? prev : { ...prev, tool }));
207
- }, []);
208
- const selectIsland = useCallback((selection) => {
209
- commitDraftInternal();
210
- selectionRef.current = selection;
211
- setSession((prev) => ({ ...prev, selection }));
212
- }, [commitDraftInternal]);
213
- const clearSelection = useCallback(() => {
214
- selectionRef.current = null;
215
- setSession((prev) => (prev.selection ? { ...prev, selection: null } : prev));
216
- }, []);
217
- const startSourceEditDraft = useCallback((params) => {
218
- const selection = selectionRef.current;
219
- if (!selection)
220
- return;
221
- const draft = {
222
- kind: "sourceEdit",
223
- id: params.id,
224
- page: selection.page,
225
- rect: params.rect,
226
- text: params.text,
227
- appearance: params.appearance,
228
- locator: selection.locator,
229
- originalText: params.text,
230
- originalAppearance: params.appearance,
231
- availableBounds: params.availableBounds,
232
- anchor: null,
233
- overflow: false,
234
- };
235
- setSession((prev) => ({ ...prev, draft }));
236
- },
237
- // Reads `selectionRef.current` (see its doc comment), not `session.selection`, so this callback
238
- // needs no reactive dependency and stays stable across selection changes.
239
- []);
240
- const startAddTextDraft = useCallback((params) => {
241
- const draft = {
242
- kind: "addText",
243
- id: params.id,
244
- page: params.page,
245
- rect: params.rect,
246
- text: "",
247
- appearance: params.appearance,
248
- locator: null,
249
- originalText: null,
250
- originalAppearance: null,
251
- availableBounds: null,
252
- anchor: params.anchor,
253
- overflow: false,
254
- };
255
- setSession((prev) => ({ ...prev, draft }));
256
- }, []);
257
- const startOcrEditDraft = useCallback((params) => {
258
- const draft = {
259
- kind: "ocrEdit",
260
- id: params.id,
261
- page: params.page,
262
- rect: params.rect,
263
- text: params.text,
264
- appearance: params.appearance,
265
- locator: null,
266
- originalText: params.text,
267
- originalAppearance: params.appearance,
268
- availableBounds: params.rect,
269
- anchor: null,
270
- lineId: params.lineId,
271
- coverColor: params.coverColor,
272
- backgroundProof: "nearUniform",
273
- overflow: false,
274
- };
275
- setSession((prev) => ({ ...prev, draft }));
276
- }, []);
277
- const updateDraftText = useCallback((text) => {
278
- setSession((prev) => (prev.draft ? { ...prev, draft: { ...prev.draft, text } } : prev));
279
- }, []);
280
- const updateDraftAppearance = useCallback((appearance) => {
281
- setSession((prev) => (prev.draft ? { ...prev, draft: { ...prev.draft, appearance } } : prev));
282
- }, []);
283
- const setDraftOverflow = useCallback((overflow) => {
284
- setSession((prev) => prev.draft && prev.draft.overflow !== overflow
285
- ? { ...prev, draft: { ...prev.draft, overflow } }
286
- : prev);
287
- }, []);
288
- const checkpoint = useCallback(() => {
289
- setSession((prev) => ({
290
- ...prev,
291
- savedSourceDigest: prev.sourceDigest,
292
- annotationDirty: false,
293
- }));
294
- }, []);
295
- const setAnnotationDirty = useCallback((next) => {
296
- setSession((prev) => prev.annotationDirty === next ? prev : { ...prev, annotationDirty: next });
297
- }, []);
298
- const inspectPage = useCallback((page) => bridge.inspectPdfPage(sourcePath, page), [bridge, sourcePath]);
299
- const listFonts = useCallback(() => {
300
- if (!fontsCacheRef.current) {
301
- fontsCacheRef.current = bridge.listPdfFonts().catch((error) => {
302
- fontsCacheRef.current = null;
303
- throw error;
304
- });
305
- }
306
- return fontsCacheRef.current;
307
- }, [bridge]);
308
- const resetForDocument = useCallback(() => {
309
- fontsCacheRef.current = null;
310
- draftRef.current = null;
311
- selectionRef.current = null;
312
- setSession(initialSession());
313
- }, []);
314
- return {
315
- session,
316
- sessionRef,
317
- dirty,
318
- enterEditMode,
319
- exitEditMode,
320
- setTool,
321
- selectIsland,
322
- clearSelection,
323
- startSourceEditDraft,
324
- startAddTextDraft,
325
- startOcrEditDraft,
326
- updateDraftText,
327
- updateDraftAppearance,
328
- setDraftOverflow,
329
- commitDraft,
330
- cancelDraft,
331
- apply,
332
- revert,
333
- onPublish,
334
- publish,
335
- checkpoint,
336
- setAnnotationDirty,
337
- inspectPage,
338
- listFonts,
339
- resetForDocument,
340
- };
341
- }