@gmickel/gno 1.29.5 → 1.30.1
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 +15 -5
- package/browser-extension/artifacts/{gno-browser-clipper-v1.29.5.zip → gno-browser-clipper-v1.30.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/src/core/network-boundary-inventory.ts +51 -0
- package/src/ingestion/index.ts +1 -1
- package/src/ingestion/walker.ts +45 -23
- package/src/serve/AGENTS.md +5 -1
- package/src/serve/CLAUDE.md +5 -1
- package/src/serve/fn112-routes.ts +232 -0
- package/src/serve/pdfjs-assets.ts +391 -0
- package/src/serve/public/components/pdf/PdfPageView.tsx +427 -0
- package/src/serve/public/components/pdf/PdfToolbar.tsx +384 -0
- package/src/serve/public/components/pdf/PdfViewer.tsx +539 -0
- package/src/serve/public/components/pdf/pdf-viewer-deps.tsx +94 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/globals.css +113 -0
- package/src/serve/public/hooks/use-pdf-document.ts +227 -0
- package/src/serve/public/hooks/use-pdf-pages.ts +1197 -0
- package/src/serve/public/lib/doc-asset-url.ts +57 -0
- package/src/serve/public/lib/math-sum-precise.ts +34 -0
- package/src/serve/public/lib/pdf.ts +772 -0
- package/src/serve/public/pages/DocView.tsx +295 -39
- package/src/serve/public/pages/doc-pdf-viewer.tsx +7 -0
- package/src/serve/routes/api.ts +154 -14
- package/src/serve/server.ts +190 -37
- package/src/serve/spa-bundle-source.ts +99 -0
- package/src/serve/watch-service.ts +219 -23
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.5.zip.sha256 +0 -1
|
@@ -451,3 +451,116 @@ mark {
|
|
|
451
451
|
scroll-behavior: auto !important;
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
|
+
|
|
455
|
+
/* ── Native PDF viewer (fn-112) ───────────────────────────────────────────── */
|
|
456
|
+
|
|
457
|
+
.gno-pdf-viewer {
|
|
458
|
+
isolation: isolate;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.gno-pdf-page-column {
|
|
462
|
+
scrollbar-gutter: stable;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.gno-pdf-page {
|
|
466
|
+
position: relative;
|
|
467
|
+
margin: 0 auto 1rem;
|
|
468
|
+
max-width: 100%;
|
|
469
|
+
background: hsl(var(--muted) / 0.2);
|
|
470
|
+
border: 1px solid hsl(var(--border) / 0.4);
|
|
471
|
+
border-radius: 0.25rem;
|
|
472
|
+
box-shadow:
|
|
473
|
+
0 1px 2px hsl(var(--background) / 0.4),
|
|
474
|
+
0 8px 24px -12px hsl(var(--foreground) / 0.18);
|
|
475
|
+
/* pdfjs v5 TextLayer CSS contract on the page wrapper (inherits to layers) */
|
|
476
|
+
--scale-factor: 1;
|
|
477
|
+
--user-unit: 1;
|
|
478
|
+
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit, 1));
|
|
479
|
+
--scale-round-x: 1px;
|
|
480
|
+
--scale-round-y: 1px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.gno-pdf-page-inner {
|
|
484
|
+
position: relative;
|
|
485
|
+
/* Existing Scholarly Dusk semantic surface token — no invented paper color */
|
|
486
|
+
background: hsl(var(--card));
|
|
487
|
+
overflow: hidden;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.gno-pdf-canvas {
|
|
491
|
+
display: block;
|
|
492
|
+
width: 100%;
|
|
493
|
+
height: auto;
|
|
494
|
+
vertical-align: top;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* Text layer: transparent text over canvas, Scholarly Dusk selection */
|
|
498
|
+
.gno-pdf-text-layer,
|
|
499
|
+
.textLayer {
|
|
500
|
+
position: absolute;
|
|
501
|
+
inset: 0;
|
|
502
|
+
overflow: clip;
|
|
503
|
+
opacity: 1;
|
|
504
|
+
line-height: 1;
|
|
505
|
+
text-size-adjust: none;
|
|
506
|
+
forced-color-adjust: none;
|
|
507
|
+
transform-origin: 0 0;
|
|
508
|
+
z-index: 2;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.gno-pdf-text-layer :is(span, br),
|
|
512
|
+
.textLayer :is(span, br) {
|
|
513
|
+
color: transparent;
|
|
514
|
+
position: absolute;
|
|
515
|
+
white-space: pre;
|
|
516
|
+
cursor: text;
|
|
517
|
+
transform-origin: 0% 0%;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.gno-pdf-text-layer ::selection,
|
|
521
|
+
.textLayer ::selection {
|
|
522
|
+
background: hsl(var(--primary) / 0.35);
|
|
523
|
+
color: transparent;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.gno-pdf-annotation-layer {
|
|
527
|
+
position: absolute;
|
|
528
|
+
inset: 0;
|
|
529
|
+
z-index: 3;
|
|
530
|
+
pointer-events: none;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.gno-pdf-annotation-link,
|
|
534
|
+
.gno-pdf-annotation-inert {
|
|
535
|
+
position: absolute;
|
|
536
|
+
pointer-events: auto;
|
|
537
|
+
background: transparent;
|
|
538
|
+
border: none;
|
|
539
|
+
padding: 0;
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.gno-pdf-annotation-inert {
|
|
544
|
+
cursor: default;
|
|
545
|
+
pointer-events: none;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.gno-pdf-page[data-rendered="false"] .gno-pdf-page-inner {
|
|
549
|
+
animation: gno-pdf-pulse 1.6s ease-in-out infinite;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@keyframes gno-pdf-pulse {
|
|
553
|
+
0%,
|
|
554
|
+
100% {
|
|
555
|
+
opacity: 1;
|
|
556
|
+
}
|
|
557
|
+
50% {
|
|
558
|
+
opacity: 0.85;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@media (prefers-reduced-motion: reduce) {
|
|
563
|
+
.gno-pdf-page[data-rendered="false"] .gno-pdf-page-inner {
|
|
564
|
+
animation: none;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyPdfError as defaultClassifyPdfError,
|
|
5
|
+
getDocument as defaultGetDocument,
|
|
6
|
+
getPdfMetrics as defaultGetPdfMetrics,
|
|
7
|
+
type GnoDocumentLoadingTask,
|
|
8
|
+
type PdfFallbackReason,
|
|
9
|
+
type PDFDocumentProxy,
|
|
10
|
+
} from "../lib/pdf";
|
|
11
|
+
|
|
12
|
+
export type PdfDocumentStatus = "loading" | "ready" | "error";
|
|
13
|
+
|
|
14
|
+
export type UsePdfDocumentResult = {
|
|
15
|
+
status: PdfDocumentStatus;
|
|
16
|
+
doc: PDFDocumentProxy | null;
|
|
17
|
+
numPages: number;
|
|
18
|
+
firstPageReady: boolean;
|
|
19
|
+
error: PdfFallbackReason | null;
|
|
20
|
+
errorMessage: string | null;
|
|
21
|
+
docId: string | null;
|
|
22
|
+
retry: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional test doubles only. Production call sites pass nothing and use the
|
|
27
|
+
* facade defaults. Not a public product API surface.
|
|
28
|
+
*/
|
|
29
|
+
export type UsePdfDocumentDeps = {
|
|
30
|
+
getDocument?: typeof defaultGetDocument;
|
|
31
|
+
classifyPdfError?: typeof defaultClassifyPdfError;
|
|
32
|
+
getPdfMetrics?: typeof defaultGetPdfMetrics;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type LoadOwnership = {
|
|
36
|
+
/** Minted opaque id for this load attempt. */
|
|
37
|
+
docId: string;
|
|
38
|
+
task: GnoDocumentLoadingTask;
|
|
39
|
+
/** Set only after promise resolves into viewer ownership. */
|
|
40
|
+
viewerDoc: PDFDocumentProxy | null;
|
|
41
|
+
/** True once teardown for this load has run (idempotent). */
|
|
42
|
+
tornDown: boolean;
|
|
43
|
+
/** True once documentDestroy was emitted (success path only). */
|
|
44
|
+
destroyMetricEmitted: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load a PDF document from a same-origin asset URL.
|
|
49
|
+
*
|
|
50
|
+
* Teardown ownership (I3-04): loadingTask.destroy() owns the transport for a
|
|
51
|
+
* load that never handed a proxy to the viewer. Once the proxy is viewer-owned,
|
|
52
|
+
* we destroy the proxy exactly once and do not also call loadingTask.destroy.
|
|
53
|
+
* documentDestroy is emitted exactly once per successfully loaded viewer
|
|
54
|
+
* instance, never for rejected/never-loaded attempts.
|
|
55
|
+
*/
|
|
56
|
+
export function usePdfDocument(
|
|
57
|
+
url: string | null,
|
|
58
|
+
deps: UsePdfDocumentDeps = {}
|
|
59
|
+
): UsePdfDocumentResult {
|
|
60
|
+
const getDocument = deps.getDocument ?? defaultGetDocument;
|
|
61
|
+
const classifyPdfError = deps.classifyPdfError ?? defaultClassifyPdfError;
|
|
62
|
+
const getPdfMetrics = deps.getPdfMetrics ?? defaultGetPdfMetrics;
|
|
63
|
+
|
|
64
|
+
const [status, setStatus] = useState<PdfDocumentStatus>("loading");
|
|
65
|
+
const [doc, setDoc] = useState<PDFDocumentProxy | null>(null);
|
|
66
|
+
const [numPages, setNumPages] = useState(0);
|
|
67
|
+
const [firstPageReady, setFirstPageReady] = useState(false);
|
|
68
|
+
const [error, setError] = useState<PdfFallbackReason | null>(null);
|
|
69
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
70
|
+
const [docId, setDocId] = useState<string | null>(null);
|
|
71
|
+
const [retryToken, setRetryToken] = useState(0);
|
|
72
|
+
|
|
73
|
+
const generationRef = useRef(0);
|
|
74
|
+
const ownershipRef = useRef<LoadOwnership | null>(null);
|
|
75
|
+
|
|
76
|
+
const retry = useCallback(() => {
|
|
77
|
+
setRetryToken((t) => t + 1);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!url) {
|
|
82
|
+
setStatus("error");
|
|
83
|
+
setError("network");
|
|
84
|
+
setErrorMessage("No document URL");
|
|
85
|
+
setDoc(null);
|
|
86
|
+
setNumPages(0);
|
|
87
|
+
setFirstPageReady(false);
|
|
88
|
+
setDocId(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
generationRef.current += 1;
|
|
93
|
+
const generation = generationRef.current;
|
|
94
|
+
const metrics = getPdfMetrics();
|
|
95
|
+
|
|
96
|
+
setStatus("loading");
|
|
97
|
+
setDoc(null);
|
|
98
|
+
setNumPages(0);
|
|
99
|
+
setFirstPageReady(false);
|
|
100
|
+
setError(null);
|
|
101
|
+
setErrorMessage(null);
|
|
102
|
+
|
|
103
|
+
const loadingTask = getDocument({ url });
|
|
104
|
+
const instanceDocId = loadingTask.gnoDocId;
|
|
105
|
+
setDocId(instanceDocId);
|
|
106
|
+
|
|
107
|
+
const ownership: LoadOwnership = {
|
|
108
|
+
docId: instanceDocId,
|
|
109
|
+
task: loadingTask,
|
|
110
|
+
viewerDoc: null,
|
|
111
|
+
tornDown: false,
|
|
112
|
+
destroyMetricEmitted: false,
|
|
113
|
+
};
|
|
114
|
+
ownershipRef.current = ownership;
|
|
115
|
+
|
|
116
|
+
const isStale = (): boolean => {
|
|
117
|
+
if (generation !== generationRef.current) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (ownership.tornDown) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return Boolean((loadingTask as { destroyed?: boolean }).destroyed);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Idempotent teardown for this load.
|
|
128
|
+
* - Never-loaded / rejected: destroy loading task only; no documentDestroy.
|
|
129
|
+
* - Viewer-owned success: destroy proxy once + documentDestroy once.
|
|
130
|
+
* - Stale late-resolved orphan (never viewer-owned): destroy proxy only.
|
|
131
|
+
*/
|
|
132
|
+
const teardown = (opts?: {
|
|
133
|
+
orphanProxy?: PDFDocumentProxy | null;
|
|
134
|
+
reason: "cleanup" | "stale-orphan";
|
|
135
|
+
}): void => {
|
|
136
|
+
if (ownership.tornDown && !opts?.orphanProxy) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (opts?.orphanProxy && opts.reason === "stale-orphan") {
|
|
141
|
+
// Resolved after we already tore down the load attempt — destroy the
|
|
142
|
+
// orphan proxy only; never emit documentDestroy (never viewer-owned).
|
|
143
|
+
try {
|
|
144
|
+
void opts.orphanProxy.destroy();
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (ownership.tornDown) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
ownership.tornDown = true;
|
|
155
|
+
|
|
156
|
+
const viewerDoc = ownership.viewerDoc;
|
|
157
|
+
ownership.viewerDoc = null;
|
|
158
|
+
|
|
159
|
+
if (viewerDoc) {
|
|
160
|
+
// Viewer owned the proxy: destroy it once. Do not also destroy the
|
|
161
|
+
// loading task (that would double-destroy the same transport).
|
|
162
|
+
try {
|
|
163
|
+
void viewerDoc.destroy();
|
|
164
|
+
} catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
if (!ownership.destroyMetricEmitted) {
|
|
168
|
+
ownership.destroyMetricEmitted = true;
|
|
169
|
+
metrics.recordDocumentDestroy({ docId: ownership.docId });
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Never handed a proxy to the viewer — loading task owns the transport.
|
|
175
|
+
try {
|
|
176
|
+
void ownership.task.destroy();
|
|
177
|
+
} catch {
|
|
178
|
+
// ignore
|
|
179
|
+
}
|
|
180
|
+
// No documentDestroy for never-loaded / rejected attempts.
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
loadingTask.promise
|
|
184
|
+
.then(async (pdf) => {
|
|
185
|
+
if (isStale()) {
|
|
186
|
+
teardown({ orphanProxy: pdf, reason: "stale-orphan" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
ownership.viewerDoc = pdf;
|
|
190
|
+
setDoc(pdf);
|
|
191
|
+
setNumPages(pdf.numPages);
|
|
192
|
+
setStatus("ready");
|
|
193
|
+
setFirstPageReady(pdf.numPages > 0);
|
|
194
|
+
})
|
|
195
|
+
.catch((err: unknown) => {
|
|
196
|
+
if (isStale()) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const reason = classifyPdfError(err);
|
|
200
|
+
setStatus("error");
|
|
201
|
+
setError(reason);
|
|
202
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
203
|
+
setDoc(null);
|
|
204
|
+
setNumPages(0);
|
|
205
|
+
setFirstPageReady(false);
|
|
206
|
+
teardown({ reason: "cleanup" });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
if (ownershipRef.current === ownership) {
|
|
211
|
+
ownershipRef.current = null;
|
|
212
|
+
}
|
|
213
|
+
teardown({ reason: "cleanup" });
|
|
214
|
+
};
|
|
215
|
+
}, [url, retryToken, getDocument, classifyPdfError, getPdfMetrics]);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
doc,
|
|
220
|
+
numPages,
|
|
221
|
+
firstPageReady,
|
|
222
|
+
error,
|
|
223
|
+
errorMessage,
|
|
224
|
+
docId,
|
|
225
|
+
retry,
|
|
226
|
+
};
|
|
227
|
+
}
|