yamlover 0.3.4 → 0.3.6

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 (137) hide show
  1. package/bin/yamlover.js +199 -112
  2. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/client/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  62. package/dist/client/assets/decoded-CFkcuQ1f.js +1 -0
  63. package/dist/client/assets/djvu-B3wuFWwI.js +1 -0
  64. package/dist/client/assets/docx-putHYXzO.js +230 -0
  65. package/dist/client/assets/heic-oIxhIbIW.js +66 -0
  66. package/dist/client/assets/imagemap-CUKUIcGe.js +1 -0
  67. package/dist/client/assets/index-CQ3TGVSx.js +617 -0
  68. package/dist/client/assets/index-wTjOzmSY.css +1 -0
  69. package/dist/client/assets/map-CZS083CS.js +1 -0
  70. package/dist/client/assets/paged-CjxSuuik.js +1 -0
  71. package/dist/client/assets/panzoom-C7OuIIda.js +4 -0
  72. package/dist/client/assets/panzoom-CIGW-MKW.css +1 -0
  73. package/dist/client/assets/pdf-Dz3gUx0B.js +12 -0
  74. package/dist/client/assets/pdf-m5e3jsV4.css +1 -0
  75. package/dist/client/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
  76. package/dist/client/assets/psd-C8yNwHFw.js +11 -0
  77. package/dist/client/assets/spreadsheet-DBv7aqoN.js +38 -0
  78. package/dist/client/assets/tiff-73RKtIux.js +1 -0
  79. package/{index.html → dist/client/index.html} +2 -1
  80. package/package.json +18 -21
  81. package/src/client/App.tsx +0 -424
  82. package/src/client/NodeView.tsx +0 -422
  83. package/src/client/TaskStrip.tsx +0 -34
  84. package/src/client/Tree.tsx +0 -97
  85. package/src/client/api.ts +0 -213
  86. package/src/client/icons.ts +0 -91
  87. package/src/client/links.tsx +0 -108
  88. package/src/client/live.ts +0 -42
  89. package/src/client/main.tsx +0 -10
  90. package/src/client/paste-html.ts +0 -228
  91. package/src/client/paste-links.ts +0 -42
  92. package/src/client/paths.ts +0 -139
  93. package/src/client/render.tsx +0 -339
  94. package/src/client/renderers/annotate.tsx +0 -641
  95. package/src/client/renderers/asciidoc.tsx +0 -36
  96. package/src/client/renderers/chapter.tsx +0 -142
  97. package/src/client/renderers/csv.tsx +0 -234
  98. package/src/client/renderers/decoded.tsx +0 -72
  99. package/src/client/renderers/djvu.tsx +0 -271
  100. package/src/client/renderers/djvuWorker.ts +0 -99
  101. package/src/client/renderers/doc.tsx +0 -40
  102. package/src/client/renderers/docx.tsx +0 -49
  103. package/src/client/renderers/epub.tsx +0 -147
  104. package/src/client/renderers/explorer.tsx +0 -294
  105. package/src/client/renderers/fb2.tsx +0 -149
  106. package/src/client/renderers/headings.ts +0 -69
  107. package/src/client/renderers/heic.tsx +0 -23
  108. package/src/client/renderers/imagemap.tsx +0 -170
  109. package/src/client/renderers/kml.ts +0 -46
  110. package/src/client/renderers/latex.tsx +0 -37
  111. package/src/client/renderers/map.tsx +0 -205
  112. package/src/client/renderers/marklower.tsx +0 -120
  113. package/src/client/renderers/markup.tsx +0 -64
  114. package/src/client/renderers/media.tsx +0 -19
  115. package/src/client/renderers/paged.ts +0 -137
  116. package/src/client/renderers/panzoom.ts +0 -101
  117. package/src/client/renderers/pdf.tsx +0 -316
  118. package/src/client/renderers/plaintext.tsx +0 -120
  119. package/src/client/renderers/plantuml.tsx +0 -83
  120. package/src/client/renderers/psd.tsx +0 -25
  121. package/src/client/renderers/registry.tsx +0 -411
  122. package/src/client/renderers/rtf.tsx +0 -210
  123. package/src/client/renderers/spreadsheet.tsx +0 -105
  124. package/src/client/renderers/tag.tsx +0 -113
  125. package/src/client/renderers/text.tsx +0 -42
  126. package/src/client/renderers/tiff.tsx +0 -33
  127. package/src/client/styles.css +0 -1212
  128. package/src/client/vendor/README.md +0 -30
  129. package/src/client/vite-env.d.ts +0 -31
  130. package/src/server/api.ts +0 -147
  131. package/src/server/embed.ts +0 -187
  132. package/src/server/engine-api.ts +0 -1563
  133. package/src/server/gitignore.ts +0 -81
  134. package/src/server/node-kind.ts +0 -61
  135. package/src/server/tasks.ts +0 -83
  136. package/src/server/yamlover.ts +0 -1133
  137. /package/{src/client/vendor/djvu.js → dist/client/assets/djvu-Ci0lFKxE.js} +0 -0
@@ -1,271 +0,0 @@
1
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { Annotation, NodeJson, blobUrl } from "../api";
3
- import { DEFAULT_COLOR, colorOf, editable, useAnnotationMenu, useMaterialAnnotations } from "./annotate";
4
- import { usePagedScroll } from "./paged";
5
- import { DecodedPage, decodeDjvuPage, openDjvu } from "./djvuWorker";
6
-
7
- const num = (v: unknown): number => Number(v) || 0;
8
- /** A rectangular annotation region on a DjVu page, in the page's NATIVE pixels (like the OCR zones),
9
- * so it's zoom-independent. `ann` is the saved annotation (→ clickable to edit). */
10
- interface DjvuRegion { page: number; x: number; y: number; w: number; h: number; title?: string; color?: string; ann?: Annotation }
11
-
12
- /** Paints worker-decoded DjVu pixels into a <canvas> at native size (CSS-scaled to the display
13
- * width by the wrapper). putImageData is cheap; no PNG encode and no main-thread decompression. */
14
- function DjvuCanvas({ image }: { image: ImageData }) {
15
- const ref = useRef<HTMLCanvasElement>(null);
16
- useLayoutEffect(() => {
17
- const c = ref.current;
18
- if (!c) return;
19
- c.width = image.width;
20
- c.height = image.height;
21
- c.getContext("2d")?.putImageData(image, 0, 0);
22
- }, [image]);
23
- return <canvas className="djvu-page" ref={ref} />;
24
- }
25
-
26
- /**
27
- * Renders an `image/vnd.djvu` document. DjVu.js decodes pages in a Web Worker (djvuWorker.ts), and
28
- * only the pages NEAR the viewport decode (windowed, like the PDF viewer) — so a long scan opens
29
- * fast and the main thread stays free to annotate while pages decode. Each decoded page is a
30
- * <canvas> (the worker returns ImageData). An OCR text layer (when present) makes text selectable →
31
- * a region annotation; pages without OCR get a drag-marquee instead. ctrl/alt-wheel zooms (a CSS
32
- * resize — no re-decode), with the reading position anchored across zoom; `?page=` tracks the page.
33
- */
34
- export function DjvuView({ node }: { node: NodeJson }) {
35
- const ref = useRef<HTMLDivElement>(null);
36
- const [count, setCount] = useState(0);
37
- const [zoom, setZoom] = useState(1);
38
- const [width, setWidth] = useState(0); // pane width (so a page caps at ~1000px like PDF, not full pane)
39
- const [error, setError] = useState<string | null>(null);
40
- const [drag, setDrag] = useState<{ page: number; x0: number; y0: number; x1: number; y1: number } | null>(null);
41
- const [near, setNear] = useState<Set<number>>(() => new Set()); // pages near the viewport
42
- const [decoded, setDecoded] = useState<Map<number, DecodedPage>>(() => new Map()); // near pages' pixels+zones
43
- const sizes = useRef(new Map<number, { w: number; h: number }>()); // remembered native sizes → stable placeholders
44
-
45
- // Annotations: a `djvu` rect region (page + native-pixel box) from a text selection on an OCR
46
- // page, or a drag-marquee on a page with no OCR. Same picker/flow as image & PDF.
47
- const material = useMaterialAnnotations(node.path);
48
- const { openCreate, openEdit, palette, preview } = useAnnotationMenu(material);
49
- const shown = preview
50
- ? [...material.annotations, { path: "(preview)", selector: preview.selector, tag: preview.tag } as Annotation]
51
- : material.annotations;
52
- const regions: DjvuRegion[] = shown
53
- .filter((a) => a.selector?.type === "djvu")
54
- .map((a) => ({ page: num(a.selector!.page) || 1, x: num(a.selector!.x), y: num(a.selector!.y), w: num(a.selector!.w), h: num(a.selector!.h), title: a.description, color: colorOf(a), ann: editable(a) ? a : undefined }));
55
- const previewColor = preview?.color ?? DEFAULT_COLOR;
56
-
57
- // WINDOWED RENDERING: every page keeps a `.djvu-page-wrap` (so the scroll height is right), but
58
- // only near pages decode + mount a canvas; far pages are estimated-height placeholders.
59
- const wraps = useRef(new Map<number, HTMLElement>());
60
- const getPageEls = () => {
61
- const out: HTMLElement[] = [];
62
- for (let i = 1; i <= count; i++) { const el = wraps.current.get(i); if (el) out.push(el); }
63
- return out;
64
- };
65
- const paged = usePagedScroll(ref, getPageEls, count > 0 && width > 0);
66
- const pagedRef = useRef(paged);
67
- pagedRef.current = paged;
68
- useLayoutEffect(() => { paged.restoreAnchor(); }, [zoom]); // eslint-disable-line react-hooks/exhaustive-deps
69
-
70
- // Focus the `.filedjvu` scroller on mount so arrows / space / PageUp-Down scroll the document
71
- // natively (it's a nested scroller the focused RHS pane can't reach). Skip when chunk-embedded.
72
- useEffect(() => {
73
- const el = ref.current;
74
- if (el && !el.closest(".chunk-body")) el.focus({ preventScroll: true });
75
- }, []);
76
-
77
- // Track the pane width so a page fits but is capped (≤1000px) like the PDF viewer.
78
- useLayoutEffect(() => {
79
- const el = ref.current;
80
- if (!el) return;
81
- const ro = new ResizeObserver(([e]) => setWidth(e.contentRect.width));
82
- ro.observe(el);
83
- return () => ro.disconnect();
84
- }, []);
85
- const dispW = Math.min(width, 1000) * zoom; // each page's displayed width in px
86
-
87
- // ctrl/alt-wheel zooms; a plain wheel scrolls. Zoom is a CSS resize (no re-decode), applied live;
88
- // the reading position is anchored at the burst start and restored after each step.
89
- useEffect(() => {
90
- const el = ref.current;
91
- if (!el) return;
92
- let bursting = false;
93
- let end = 0;
94
- const onWheel = (e: WheelEvent) => {
95
- if (!(e.ctrlKey || e.altKey || e.metaKey)) return;
96
- e.preventDefault();
97
- if (!bursting) { pagedRef.current.captureAnchor(); bursting = true; }
98
- clearTimeout(end);
99
- end = window.setTimeout(() => (bursting = false), 250);
100
- setZoom((z) => Math.min(5, Math.max(0.4, z * (e.deltaY < 0 ? 1.1 : 1 / 1.1))));
101
- };
102
- el.addEventListener("wheel", onWheel, { passive: false });
103
- return () => { el.removeEventListener("wheel", onWheel); clearTimeout(end); };
104
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
- }, []);
106
-
107
- // Open the document in the worker (off the main thread) → page count. Decoding happens lazily,
108
- // per near page, in the effect below.
109
- useEffect(() => {
110
- let cancelled = false;
111
- setCount(0); setError(null); setNear(new Set()); setDecoded(new Map());
112
- wraps.current.clear(); sizes.current.clear();
113
- (async () => {
114
- const buf = await fetch(blobUrl(node.path)).then((r) => r.arrayBuffer());
115
- const n = await openDjvu(buf, node.path);
116
- if (!cancelled) setCount(n);
117
- })().catch((e) => !cancelled && setError(String((e as Error).message || e)));
118
- return () => { cancelled = true; };
119
- }, [node.path]);
120
-
121
- // Windowed observer (mirror the PDF viewer): mark pages near the viewport.
122
- useEffect(() => {
123
- if (!count) return;
124
- const obs = new IntersectionObserver(
125
- (entries) => {
126
- setNear((prev) => {
127
- const next = new Set(prev);
128
- for (const e of entries) {
129
- const pn = Number((e.target as HTMLElement).dataset.page);
130
- if (e.isIntersecting) next.add(pn);
131
- else next.delete(pn);
132
- }
133
- return next.size === prev.size && [...next].every((p) => prev.has(p)) ? prev : next;
134
- });
135
- },
136
- { root: ref.current, rootMargin: "2000px 0px" },
137
- );
138
- for (const el of wraps.current.values()) obs.observe(el);
139
- return () => obs.disconnect();
140
- // eslint-disable-next-line react-hooks/exhaustive-deps
141
- }, [count, width > 0]);
142
-
143
- // Decode near pages (lazy, in the worker); drop decoded pixels that left the window to bound
144
- // memory (the worker keeps an LRU cache, so re-entry is fast).
145
- useEffect(() => {
146
- let alive = true;
147
- near.forEach((n) => {
148
- if (!decoded.has(n)) {
149
- decodeDjvuPage(n)
150
- .then((dp) => {
151
- sizes.current.set(n, { w: dp.w, h: dp.h });
152
- if (alive) setDecoded((m) => new Map(m).set(n, dp));
153
- })
154
- .catch(() => {});
155
- }
156
- });
157
- setDecoded((m) => {
158
- let changed = false;
159
- const x = new Map(m);
160
- for (const k of x.keys()) if (!near.has(k)) { x.delete(k); changed = true; }
161
- return changed ? x : m;
162
- });
163
- return () => { alive = false; };
164
- // eslint-disable-next-line react-hooks/exhaustive-deps
165
- }, [near]);
166
-
167
- // A finished text selection on an OCR page → a `djvu` region (its bounding box, in native px).
168
- const onMouseUp = () => {
169
- const sel = window.getSelection();
170
- if (!sel || sel.isCollapsed || !sel.anchorNode) return;
171
- const hostEl = sel.anchorNode.nodeType === 1 ? (sel.anchorNode as Element) : sel.anchorNode.parentElement;
172
- const wrap = hostEl?.closest(".djvu-page-wrap") as HTMLElement | null;
173
- if (!wrap || !ref.current?.contains(wrap)) return;
174
- const pn = Number(wrap.dataset.page);
175
- const wr = wrap.getBoundingClientRect();
176
- const s = wr.width / (decoded.get(pn)?.w || 1); // display px per native px
177
- if (!s) return;
178
- const sr = sel.getRangeAt(0).getBoundingClientRect();
179
- if (sr.width < 2 || sr.height < 2) return;
180
- openCreate(
181
- { type: "djvu", page: pn, x: Math.round((sr.left - wr.left) / s), y: Math.round((sr.top - wr.top) / s), w: Math.round(sr.width / s), h: Math.round(sr.height / s) },
182
- { x: sr.left, y: sr.bottom + 6 },
183
- );
184
- };
185
-
186
- // Marquee drag on a page with NO OCR: draw a box, convert to native px.
187
- const scaleOf = (pn: number, wrap: HTMLElement) => wrap.getBoundingClientRect().width / (decoded.get(pn)?.w || 1);
188
- const dragStart = (pn: number, e: React.MouseEvent) => {
189
- const wr = e.currentTarget.getBoundingClientRect();
190
- const s = scaleOf(pn, e.currentTarget as HTMLElement);
191
- setDrag({ page: pn, x0: (e.clientX - wr.left) / s, y0: (e.clientY - wr.top) / s, x1: (e.clientX - wr.left) / s, y1: (e.clientY - wr.top) / s });
192
- };
193
- const dragMove = (pn: number, e: React.MouseEvent) => {
194
- const wr = e.currentTarget.getBoundingClientRect();
195
- const s = scaleOf(pn, e.currentTarget as HTMLElement);
196
- setDrag((d) => (d ? { ...d, x1: (e.clientX - wr.left) / s, y1: (e.clientY - wr.top) / s } : d));
197
- };
198
- const dragEnd = (pn: number, e: React.MouseEvent) => {
199
- const d = drag;
200
- setDrag(null);
201
- if (!d || d.page !== pn) return;
202
- const s = scaleOf(pn, e.currentTarget as HTMLElement);
203
- const left = Math.min(d.x0, d.x1), top = Math.min(d.y0, d.y1), w = Math.abs(d.x1 - d.x0), h = Math.abs(d.y1 - d.y0);
204
- if (w * s < 3 || h * s < 3) return; // a click, not a drag
205
- const wr = e.currentTarget.getBoundingClientRect();
206
- openCreate(
207
- { type: "djvu", page: pn, x: Math.round(left), y: Math.round(top), w: Math.round(w), h: Math.round(h) },
208
- { x: wr.left + left * s, y: wr.top + (top + h) * s + 6 },
209
- );
210
- };
211
-
212
- if (error) return <div className="error">djvu: {error}</div>;
213
- return (
214
- <>
215
- <div className="filedjvu yo-zoomable" ref={ref} tabIndex={0} onMouseUp={onMouseUp}>
216
- {count === 0 && <div className="loading">opening djvu…</div>}
217
- {width > 0 &&
218
- Array.from({ length: count }, (_, i) => {
219
- const pn = i + 1;
220
- const dp = near.has(pn) ? decoded.get(pn) : undefined;
221
- const size = sizes.current.get(pn);
222
- const estHeight = dispW * (size ? size.h / size.w : Math.SQRT2);
223
- const s = dp ? dispW / dp.w : 0;
224
- return (
225
- <div
226
- key={i}
227
- className="djvu-page-wrap"
228
- data-page={pn}
229
- ref={(el) => { if (el) wraps.current.set(pn, el); else wraps.current.delete(pn); }}
230
- style={{ width: dispW, height: dp ? undefined : estHeight }}
231
- >
232
- {dp ? (
233
- <>
234
- <DjvuCanvas image={dp.image} />
235
- {dp.zones.length > 0 ? (
236
- <div className="djvu-textlayer">
237
- {dp.zones.map((z, j) => (
238
- <span key={j} style={{ left: z.x * s, top: z.y * s, width: z.width * s, height: z.height * s, fontSize: z.height * s }}>{z.text}</span>
239
- ))}
240
- </div>
241
- ) : (
242
- <div className="djvu-marquee" onMouseDown={(e) => dragStart(pn, e)} onMouseMove={(e) => dragMove(pn, e)} onMouseUp={(e) => dragEnd(pn, e)}>
243
- {drag?.page === pn && (
244
- <div className="djvu-region" style={{ left: Math.min(drag.x0, drag.x1) * s, top: Math.min(drag.y0, drag.y1) * s, width: Math.abs(drag.x1 - drag.x0) * s, height: Math.abs(drag.y1 - drag.y0) * s, borderColor: previewColor, background: previewColor + "2e" }} />
245
- )}
246
- </div>
247
- )}
248
- {regions.filter((r) => r.page === pn).map((r, j) => {
249
- const c = r.color || DEFAULT_COLOR;
250
- return (
251
- <div
252
- key={j}
253
- className={"djvu-region" + (r.ann ? " editable" : "")}
254
- title={r.ann ? r.title || "click to recolor or delete" : r.title}
255
- onClick={r.ann ? (e) => { e.stopPropagation(); openEdit(r.ann!, { x: e.clientX, y: e.clientY }); } : undefined}
256
- style={{ left: r.x * s, top: r.y * s, width: r.w * s, height: r.h * s, borderColor: c, background: c + "2e" }}
257
- />
258
- );
259
- })}
260
- </>
261
- ) : (
262
- <div className="djvu-placeholder">{near.has(pn) ? "decoding…" : ""}</div>
263
- )}
264
- </div>
265
- );
266
- })}
267
- </div>
268
- {palette}
269
- </>
270
- );
271
- }
@@ -1,99 +0,0 @@
1
- // Off-main-thread DjVu decoding. The vendored DjVu.js (vendor/djvu.js) doubles as its own Web
2
- // Worker script (it detects worker context internally); `new DjVu.Worker()` spins that worker from
3
- // an inline blob. We drive ONE worker + ONE open document at a time (one viewer is open at a time),
4
- // decoding pages lazily on demand so the main thread never blocks on JB2/IW44 decompression — which
5
- // is what froze annotation while a big scan decoded. The library bundle itself is injected once as a
6
- // classic <script> so the `DjVu.Worker` class is available on the main thread.
7
- import djvuScriptUrl from "../vendor/djvu.js?url";
8
-
9
- declare global {
10
- interface Window { DjVu?: any }
11
- }
12
-
13
- /** One OCR text zone — absolute pixels in the page's native space (top-left origin). */
14
- export interface Zone { x: number; y: number; width: number; height: number; text: string }
15
- /** A decoded page: pixels (paint to a canvas), OCR zones (may be empty), native pixel size. */
16
- export interface DecodedPage { image: ImageData; zones: Zone[]; w: number; h: number }
17
-
18
- let libLoading: Promise<any> | null = null;
19
- /** Inject the vendored bundle once; resolve with the global `DjVu` namespace (for `DjVu.Worker`). */
20
- function loadDjVu(): Promise<any> {
21
- if (window.DjVu) return Promise.resolve(window.DjVu);
22
- libLoading ??= new Promise((resolve, reject) => {
23
- const s = document.createElement("script");
24
- s.src = djvuScriptUrl;
25
- s.onload = () => (window.DjVu ? resolve(window.DjVu) : reject(new Error("DjVu failed to load")));
26
- s.onerror = () => reject(new Error("could not load djvu.js"));
27
- document.head.appendChild(s);
28
- });
29
- return libLoading;
30
- }
31
-
32
- let worker: any = null; // the singleton DjVu.Worker (kept alive across viewers, like pdf.js's worker)
33
- let curKey: string | null = null; // node.path of the currently open document
34
- let opening: Promise<number> | null = null; // resolves to the page count of the open document
35
- const cache = new Map<number, DecodedPage>(); // LRU (by re-insertion) of decoded pages — bounds memory
36
- const inflight = new Map<number, Promise<DecodedPage>>();
37
- const CACHE_CAP = 6; // few full pages kept; scans are huge (a native page can be ~tens of MB)
38
- const MAX_RASTER_W = 1500; // cap stored pixels: a scan's native width is overkill for a ~1000px display
39
- // (and at full native res the in-memory ImageData crashes the tab)
40
-
41
- /** Open a DjVu document in the worker (re-creating only when the file changes) and resolve its page
42
- * count. The buffer is transferred to the worker. */
43
- export async function openDjvu(buf: ArrayBuffer, key: string): Promise<number> {
44
- const DjVu = await loadDjVu();
45
- worker ??= new DjVu.Worker(); // inline-blob worker; no separate script URL needed
46
- if (key !== curKey) {
47
- curKey = key;
48
- cache.clear();
49
- inflight.clear();
50
- opening = (async () => {
51
- await worker.createDocument(buf);
52
- return Number(await worker.doc.getPagesQuantity().run()) || 0;
53
- })();
54
- }
55
- return opening!;
56
- }
57
-
58
- /** Resize a (large) ImageData to `targetW` wide, preserving aspect → a smaller ImageData. The
59
- * browser does the resampling in `createImageBitmap` (off the main thread); the full-size source is
60
- * then released. Keeps memory bounded without changing coordinates (native size is tracked apart). */
61
- async function downscale(full: ImageData, targetW: number): Promise<ImageData> {
62
- const targetH = Math.max(1, Math.round((full.height * targetW) / full.width));
63
- const bmp = await createImageBitmap(full, { resizeWidth: targetW, resizeHeight: targetH, resizeQuality: "medium" });
64
- const cnv = document.createElement("canvas");
65
- cnv.width = targetW;
66
- cnv.height = targetH;
67
- const cx = cnv.getContext("2d")!;
68
- cx.drawImage(bmp, 0, 0);
69
- bmp.close();
70
- return cx.getImageData(0, 0, targetW, targetH);
71
- }
72
-
73
- /** Decode one page (1-based) off the main thread: image + OCR zones + native size, in a single
74
- * batched worker round-trip. Memoized (LRU) so re-scroll/zoom doesn't re-decode. */
75
- export function decodeDjvuPage(n: number): Promise<DecodedPage> {
76
- const hit = cache.get(n);
77
- if (hit) { cache.delete(n); cache.set(n, hit); return Promise.resolve(hit); } // LRU touch
78
- const pending = inflight.get(n);
79
- if (pending) return pending;
80
- const p = (async () => {
81
- const [full, zones, w, h] = await worker.run(
82
- worker.doc.getPage(n).getImageData(),
83
- worker.doc.getPage(n).getNormalizedTextZones(),
84
- worker.doc.getPage(n).getWidth(),
85
- worker.doc.getPage(n).getHeight(),
86
- );
87
- const nativeW = Number(w) || full.width, nativeH = Number(h) || full.height;
88
- // Downscale to a display-adequate raster (the native scan is huge); coordinates stay in NATIVE
89
- // px (zones + region selectors), so the smaller canvas is purely a sharpness/memory trade.
90
- const image = nativeW > MAX_RASTER_W ? await downscale(full, MAX_RASTER_W) : full;
91
- const dp: DecodedPage = { image, zones: Array.isArray(zones) ? zones : [], w: nativeW, h: nativeH };
92
- inflight.delete(n);
93
- cache.set(n, dp);
94
- while (cache.size > CACHE_CAP) cache.delete(cache.keys().next().value as number); // evict oldest
95
- return dp;
96
- })();
97
- inflight.set(n, p);
98
- return p;
99
- }
@@ -1,40 +0,0 @@
1
- import { NodeJson, blobUrl } from "../api";
2
- import { Chunk } from "./registry";
3
-
4
- /**
5
- * Renderer for a legacy `.doc` (Word 97–2003 binary / OLE compound file). Unlike
6
- * `.docx`, the old binary format has no reliable pure-browser parser, so rather
7
- * than show a broken conversion we present it honestly: a note and a download link
8
- * to the raw bytes (served by `/api/blob`). Faithful in-browser rendering would
9
- * need a server-side conversion step (e.g. LibreOffice), which is out of scope here.
10
- */
11
- function DocNote({ path }: { path: string }) {
12
- return (
13
- <div className="office-fallback">
14
- <p>
15
- Legacy <code>.doc</code> (Word 97–2003) isn’t rendered in the browser — the old binary format has no
16
- reliable client-side parser. Newer <code>.docx</code> files render in full.
17
- </p>
18
- <p>
19
- <a className="descend" href={blobUrl(path)} download>
20
- ⤓ Download the document
21
- </a>{" "}
22
- to open it in an office application.
23
- </p>
24
- </div>
25
- );
26
- }
27
-
28
- export function DocView({ node }: { node: NodeJson }) {
29
- return (
30
- <div className="text">
31
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
32
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
33
- <DocNote path={node.path} />
34
- </div>
35
- );
36
- }
37
-
38
- export function DocChunk({ chunk }: { chunk: Chunk }) {
39
- return <DocNote path={chunk.path} />;
40
- }
@@ -1,49 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import mammoth from "mammoth/mammoth.browser";
3
- import { NodeJson, blobUrl } from "../api";
4
- import { Chunk } from "./registry";
5
-
6
- /**
7
- * Renderer for a `.docx` (Office Open XML word document). The file is served as
8
- * bytes; mammoth converts the document body to clean semantic HTML (headings,
9
- * lists, bold/italic, tables, …), shown in the shared `.markup` body. mammoth is
10
- * heavy and browser-only, so the registry loads this module lazily.
11
- */
12
- function useDocxHtml(path: string): { html: string | null; error: string | null } {
13
- const [html, setHtml] = useState<string | null>(null);
14
- const [error, setError] = useState<string | null>(null);
15
- useEffect(() => {
16
- let cancelled = false;
17
- setHtml(null);
18
- setError(null);
19
- fetch(blobUrl(path))
20
- .then((r) => r.arrayBuffer())
21
- .then((buf) => mammoth.convertToHtml({ arrayBuffer: buf }))
22
- .then((res) => !cancelled && setHtml(res.value))
23
- .catch((e) => !cancelled && setError(String((e as Error).message || e)));
24
- return () => {
25
- cancelled = true;
26
- };
27
- }, [path]);
28
- return { html, error };
29
- }
30
-
31
- export function DocxView({ node }: { node: NodeJson }) {
32
- const { html, error } = useDocxHtml(node.path);
33
- if (error) return <div className="error">docx: {error}</div>;
34
- if (html == null) return <div className="loading">converting document…</div>;
35
- return (
36
- <div className="text">
37
- {node.title && <h1 className="chapter-title">{node.title}</h1>}
38
- {node.description && <p className="chapter-subtitle">{node.description}</p>}
39
- <div className="markup" dangerouslySetInnerHTML={{ __html: html }} />
40
- </div>
41
- );
42
- }
43
-
44
- export function DocxChunk({ chunk }: { chunk: Chunk }) {
45
- const { html, error } = useDocxHtml(chunk.path);
46
- if (error) return <div className="error">docx: {error}</div>;
47
- if (html == null) return <div className="loading">converting document…</div>;
48
- return <div className="markup" dangerouslySetInnerHTML={{ __html: html }} />;
49
- }
@@ -1,147 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { unzipSync, strFromU8 } from "fflate";
3
- import { NodeJson, blobUrl } from "../api";
4
-
5
- const XLINK = "http://www.w3.org/1999/xlink";
6
-
7
- /** Normalize a zip path: resolve `rel` (which may contain `..`/`.`) against the
8
- * directory `baseDir`, dropping any fragment/query. */
9
- function resolvePath(baseDir: string, rel: string): string {
10
- rel = decodeURIComponent(rel.split("#")[0].split("?")[0]);
11
- const parts = (baseDir ? baseDir.split("/") : []).concat(rel.split("/"));
12
- const out: string[] = [];
13
- for (const p of parts) {
14
- if (p === "" || p === ".") continue;
15
- if (p === "..") out.pop();
16
- else out.push(p);
17
- }
18
- return out.join("/");
19
- }
20
-
21
- function dirOf(p: string): string {
22
- const i = p.lastIndexOf("/");
23
- return i < 0 ? "" : p.slice(0, i);
24
- }
25
-
26
- interface Book {
27
- title?: string;
28
- author?: string;
29
- cover?: string;
30
- html: string;
31
- }
32
-
33
- /**
34
- * Renderer for an `application/epub+zip` (`.epub`) ebook. The file is served as
35
- * bytes; here we unzip it, read the package document (OPF) for its metadata,
36
- * manifest and spine, then render the spine's XHTML documents in order — with
37
- * internal images rewired to object URLs and scripts/styles stripped.
38
- */
39
- export function EpubView({ node }: { node: NodeJson }) {
40
- const [book, setBook] = useState<Book | null>(null);
41
- const [error, setError] = useState<string | null>(null);
42
-
43
- useEffect(() => {
44
- let cancelled = false;
45
- const urls: string[] = [];
46
- setBook(null);
47
- setError(null);
48
-
49
- fetch(blobUrl(node.path))
50
- .then((r) => r.arrayBuffer())
51
- .then((buf) => {
52
- if (cancelled) return;
53
- const files = unzipSync(new Uint8Array(buf));
54
- const text = (p: string) => (files[p] ? strFromU8(files[p]) : "");
55
- const parseXml = (p: string) => new DOMParser().parseFromString(text(p), "application/xml");
56
-
57
- // container.xml → the OPF package document
58
- const opfPath = parseXml("META-INF/container.xml")
59
- .getElementsByTagNameNS("*", "rootfile")[0]
60
- ?.getAttribute("full-path");
61
- if (!opfPath || !files[opfPath]) throw new Error("no OPF package document");
62
- const opf = parseXml(opfPath);
63
- const opfDir = dirOf(opfPath);
64
-
65
- // object URL for a zip entry, memoized (and tracked for revocation)
66
- const cache = new Map<string, string>();
67
- const objUrl = (path: string, type = ""): string => {
68
- if (!files[path]) return "";
69
- const hit = cache.get(path);
70
- if (hit) return hit;
71
- const u = URL.createObjectURL(new Blob([files[path]], { type }));
72
- cache.set(path, u);
73
- urls.push(u);
74
- return u;
75
- };
76
-
77
- // metadata
78
- const title = opf.getElementsByTagNameNS("*", "title")[0]?.textContent?.trim();
79
- const author = Array.from(opf.getElementsByTagNameNS("*", "creator"))
80
- .map((c) => c.textContent?.trim())
81
- .filter(Boolean)
82
- .join(", ");
83
-
84
- // manifest: id → { href (zip path), type }
85
- const manifest = new Map<string, { path: string; type: string }>();
86
- for (const it of Array.from(opf.getElementsByTagNameNS("*", "item"))) {
87
- const id = it.getAttribute("id");
88
- const href = it.getAttribute("href");
89
- if (id && href) manifest.set(id, { path: resolvePath(opfDir, href), type: it.getAttribute("media-type") || "" });
90
- }
91
-
92
- // cover image (EPUB2 `<meta name="cover">`)
93
- const coverId = Array.from(opf.getElementsByTagNameNS("*", "meta")).find(
94
- (m) => m.getAttribute("name") === "cover",
95
- )?.getAttribute("content");
96
- const coverItem = coverId ? manifest.get(coverId) : undefined;
97
- const cover = coverItem ? objUrl(coverItem.path, coverItem.type) : undefined;
98
-
99
- // spine: render each XHTML document in reading order
100
- const out: string[] = [];
101
- for (const ref of Array.from(opf.getElementsByTagNameNS("*", "itemref"))) {
102
- const item = manifest.get(ref.getAttribute("idref") || "");
103
- if (!item || !/html/.test(item.type) || !files[item.path]) continue;
104
- const docDir = dirOf(item.path);
105
- const dom = new DOMParser().parseFromString(strFromU8(files[item.path]), "text/html");
106
- const body = dom.body;
107
- if (!body) continue;
108
- body.querySelectorAll("script, style, link").forEach((e) => e.remove());
109
- for (const img of Array.from(body.querySelectorAll("img"))) {
110
- const src = img.getAttribute("src");
111
- const u = src ? objUrl(resolvePath(docDir, src)) : "";
112
- if (u) img.setAttribute("src", u);
113
- else img.removeAttribute("src");
114
- }
115
- for (const im of Array.from(body.querySelectorAll("image"))) {
116
- const href = im.getAttributeNS(XLINK, "href") || im.getAttribute("href") || im.getAttribute("xlink:href");
117
- const u = href ? objUrl(resolvePath(docDir, href)) : "";
118
- if (u) im.setAttributeNS(XLINK, "xlink:href", u);
119
- }
120
- out.push(`<section class="epub-doc">${body.innerHTML}</section>`);
121
- }
122
-
123
- if (cancelled) {
124
- urls.forEach(URL.revokeObjectURL);
125
- return;
126
- }
127
- setBook({ title, author: author || undefined, cover, html: out.join("\n") });
128
- })
129
- .catch((e) => !cancelled && setError(String((e as Error).message || e)));
130
-
131
- return () => {
132
- cancelled = true;
133
- urls.forEach(URL.revokeObjectURL);
134
- };
135
- }, [node.path]);
136
-
137
- if (error) return <div className="error">epub: {error}</div>;
138
- if (!book) return <div className="loading">unpacking EPUB…</div>;
139
- return (
140
- <div className="text epub">
141
- {book.cover && <img className="fb2-cover" src={book.cover} alt="" />}
142
- {book.title && <h1 className="chapter-title">{book.title}</h1>}
143
- {book.author && <p className="chapter-subtitle">{book.author}</p>}
144
- <div className="markup epub-body" dangerouslySetInnerHTML={{ __html: book.html }} />
145
- </div>
146
- );
147
- }