@peaske7/readit 0.1.8 → 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.
- package/README.md +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +124 -172
- package/src/{cli/index.ts → cli.ts} +37 -53
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +77 -106
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +74 -74
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/margin-layout.bench.ts +0 -28
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- package/src/types/index.ts +0 -127
package/src/lib/highlight/dom.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HighlightStyle, TextNodeInfo } from "./types";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Block-level elements that should have newlines between them.
|
|
5
|
-
* Used to normalize whitespace in text extraction.
|
|
6
|
-
*/
|
|
7
3
|
const BLOCK_ELEMENTS = new Set([
|
|
8
4
|
"P",
|
|
9
5
|
"DIV",
|
|
@@ -20,9 +16,6 @@ const BLOCK_ELEMENTS = new Set([
|
|
|
20
16
|
"BR",
|
|
21
17
|
]);
|
|
22
18
|
|
|
23
|
-
/**
|
|
24
|
-
* Find the closest block-level ancestor of a node.
|
|
25
|
-
*/
|
|
26
19
|
function findBlockParent(node: Node): Element | null {
|
|
27
20
|
let parent = node.parentElement;
|
|
28
21
|
while (parent && !BLOCK_ELEMENTS.has(parent.tagName)) {
|
|
@@ -31,10 +24,7 @@ function findBlockParent(node: Node): Element | null {
|
|
|
31
24
|
return parent;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
/**
|
|
35
|
-
* Calculate text offset from root to a specific node position.
|
|
36
|
-
* Accounts for newlines between block elements to match getDOMTextContent.
|
|
37
|
-
*/
|
|
27
|
+
/** Accounts for newlines between block elements to match getDOMTextContent. */
|
|
38
28
|
export function getTextOffset(
|
|
39
29
|
root: Node,
|
|
40
30
|
targetNode: Node,
|
|
@@ -48,13 +38,12 @@ export function getTextOffset(
|
|
|
48
38
|
while (node) {
|
|
49
39
|
const blockParent = findBlockParent(node);
|
|
50
40
|
|
|
51
|
-
// Add newline when transitioning between different block parents
|
|
52
41
|
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
53
42
|
if (
|
|
54
43
|
!lastBlockParent.contains(blockParent) &&
|
|
55
44
|
!blockParent.contains(lastBlockParent)
|
|
56
45
|
) {
|
|
57
|
-
offset += 1;
|
|
46
|
+
offset += 1;
|
|
58
47
|
}
|
|
59
48
|
}
|
|
60
49
|
|
|
@@ -69,10 +58,7 @@ export function getTextOffset(
|
|
|
69
58
|
return offset;
|
|
70
59
|
}
|
|
71
60
|
|
|
72
|
-
/**
|
|
73
|
-
* Extract all text content from a DOM tree.
|
|
74
|
-
* Inserts newlines between block-level elements to match browser selection behavior.
|
|
75
|
-
*/
|
|
61
|
+
/** Inserts newlines between block-level elements to match browser selection behavior. */
|
|
76
62
|
export function getDOMTextContent(root: Node): string {
|
|
77
63
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
78
64
|
let text = "";
|
|
@@ -82,7 +68,6 @@ export function getDOMTextContent(root: Node): string {
|
|
|
82
68
|
while (node) {
|
|
83
69
|
const blockParent = findBlockParent(node);
|
|
84
70
|
|
|
85
|
-
// Insert newline when transitioning between different block parents
|
|
86
71
|
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
87
72
|
// Only add newline if blocks are siblings (not nested)
|
|
88
73
|
if (
|
|
@@ -101,10 +86,6 @@ export function getDOMTextContent(root: Node): string {
|
|
|
101
86
|
return text;
|
|
102
87
|
}
|
|
103
88
|
|
|
104
|
-
/**
|
|
105
|
-
* Collect all text nodes with their cumulative offset ranges.
|
|
106
|
-
* Accounts for newlines between block elements to match getDOMTextContent.
|
|
107
|
-
*/
|
|
108
89
|
export function collectTextNodes(root: Node): TextNodeInfo[] {
|
|
109
90
|
const textNodes: TextNodeInfo[] = [];
|
|
110
91
|
let currentOffset = 0;
|
|
@@ -116,14 +97,12 @@ export function collectTextNodes(root: Node): TextNodeInfo[] {
|
|
|
116
97
|
while (node) {
|
|
117
98
|
const blockParent = findBlockParent(node);
|
|
118
99
|
|
|
119
|
-
// Account for newline when transitioning between different block parents
|
|
120
|
-
// (same logic as getDOMTextContent)
|
|
121
100
|
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
122
101
|
if (
|
|
123
102
|
!lastBlockParent.contains(blockParent) &&
|
|
124
103
|
!blockParent.contains(lastBlockParent)
|
|
125
104
|
) {
|
|
126
|
-
currentOffset += 1;
|
|
105
|
+
currentOffset += 1;
|
|
127
106
|
}
|
|
128
107
|
}
|
|
129
108
|
|
|
@@ -141,9 +120,6 @@ export function collectTextNodes(root: Node): TextNodeInfo[] {
|
|
|
141
120
|
return textNodes;
|
|
142
121
|
}
|
|
143
122
|
|
|
144
|
-
/**
|
|
145
|
-
* Extended style configuration for highlight marks with color and bracket mode support.
|
|
146
|
-
*/
|
|
147
123
|
export interface ExtendedHighlightStyle extends HighlightStyle {
|
|
148
124
|
colorIndex?: number;
|
|
149
125
|
isBracketMode?: boolean;
|
|
@@ -164,9 +140,6 @@ interface NodeSegment {
|
|
|
164
140
|
order: number;
|
|
165
141
|
}
|
|
166
142
|
|
|
167
|
-
/**
|
|
168
|
-
* Line threshold for bracket mode (selections spanning this many lines or more)
|
|
169
|
-
*/
|
|
170
143
|
const BRACKET_MODE_LINE_THRESHOLD = 5;
|
|
171
144
|
|
|
172
145
|
export function countLinesInRange(
|
|
@@ -183,9 +156,6 @@ export function countLinesInRange(
|
|
|
183
156
|
// applyHighlightWithStyle adds color indices and bracket mode for saved comments.
|
|
184
157
|
// Keeping them separate avoids unnecessary complexity in a shared abstraction.
|
|
185
158
|
|
|
186
|
-
/**
|
|
187
|
-
* Apply highlight mark elements to a text range (for pending selections).
|
|
188
|
-
*/
|
|
189
159
|
export function applyHighlightToRange(
|
|
190
160
|
root: HTMLElement,
|
|
191
161
|
startOffset: number,
|
|
@@ -225,7 +195,6 @@ export function applyHighlightToRange(
|
|
|
225
195
|
mark.appendChild(fragment);
|
|
226
196
|
range.insertNode(mark);
|
|
227
197
|
} catch (err) {
|
|
228
|
-
// Skip if fallback also fails, but log for debugging
|
|
229
198
|
console.warn("[highlight] Failed to apply highlight to range:", err);
|
|
230
199
|
}
|
|
231
200
|
}
|
|
@@ -380,35 +349,3 @@ export function clearHighlights(
|
|
|
380
349
|
}
|
|
381
350
|
}
|
|
382
351
|
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Collect highlight positions relative to a container and document.
|
|
386
|
-
*/
|
|
387
|
-
export function collectHighlightPositions(
|
|
388
|
-
root: HTMLElement,
|
|
389
|
-
containerRect: DOMRect,
|
|
390
|
-
scrollY = 0,
|
|
391
|
-
): HighlightPositions {
|
|
392
|
-
const positions: Record<string, number> = {};
|
|
393
|
-
const documentPositions: Record<string, number> = {};
|
|
394
|
-
|
|
395
|
-
// Collect comment highlight positions
|
|
396
|
-
const marks = root.querySelectorAll("mark[data-comment-id]");
|
|
397
|
-
for (const mark of marks) {
|
|
398
|
-
const commentId = mark.getAttribute("data-comment-id");
|
|
399
|
-
if (!commentId) continue;
|
|
400
|
-
|
|
401
|
-
// Get position relative to container (for margin notes)
|
|
402
|
-
const markRect = mark.getBoundingClientRect();
|
|
403
|
-
const relativeTop = markRect.top - containerRect.top;
|
|
404
|
-
|
|
405
|
-
// Use first occurrence of each comment id
|
|
406
|
-
if (!(commentId in positions)) {
|
|
407
|
-
positions[commentId] = relativeTop;
|
|
408
|
-
// Document-absolute position (for minimap)
|
|
409
|
-
documentPositions[commentId] = markRect.top + scrollY;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return { positions, documentPositions };
|
|
414
|
-
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { findTextPosition } from "./core";
|
|
2
1
|
import {
|
|
3
2
|
applyHighlightBatch,
|
|
4
3
|
applyHighlightToRange,
|
|
5
4
|
clearHighlights,
|
|
6
|
-
collectHighlightPositions,
|
|
7
5
|
getDOMTextContent,
|
|
8
6
|
getTextOffset,
|
|
9
7
|
} from "./dom";
|
|
10
|
-
import
|
|
8
|
+
import { Resolver } from "./resolver";
|
|
9
|
+
import type { HighlightComment } from "./types";
|
|
11
10
|
|
|
12
11
|
export type SelectionHandler = (
|
|
13
12
|
text: string,
|
|
@@ -15,53 +14,35 @@ export type SelectionHandler = (
|
|
|
15
14
|
endOffset: number,
|
|
16
15
|
selectionTop: number,
|
|
17
16
|
) => void;
|
|
18
|
-
export type PositionChangeHandler = (positions: HighlightPositions) => void;
|
|
19
17
|
export type HoverHandler = (commentId: string | undefined) => void;
|
|
20
18
|
export type ClickHandler = (commentId: string) => void;
|
|
21
|
-
export type ContentHeightHandler = (height: number) => void;
|
|
22
|
-
|
|
23
19
|
export interface Highlighter {
|
|
24
|
-
applyHighlights(
|
|
25
|
-
comments: HighlightComment[],
|
|
26
|
-
pendingSelection?: TextRange,
|
|
27
|
-
): void;
|
|
20
|
+
applyHighlights(comments: HighlightComment[]): void;
|
|
28
21
|
clearHighlights(): void;
|
|
29
|
-
getPositions(): HighlightPositions;
|
|
30
|
-
onPositionsChange(callback: PositionChangeHandler): () => void;
|
|
31
22
|
onHighlightHover(callback: HoverHandler): () => void;
|
|
32
23
|
onHighlightClick(callback: ClickHandler): () => void;
|
|
33
|
-
onContentHeightChange?(callback: ContentHeightHandler): () => void;
|
|
34
24
|
dispose(): void;
|
|
35
25
|
}
|
|
36
26
|
|
|
37
|
-
interface
|
|
38
|
-
type: "markdown";
|
|
27
|
+
export interface HighlighterOptions {
|
|
39
28
|
root: HTMLElement;
|
|
40
29
|
container: HTMLElement;
|
|
41
30
|
onSelect: SelectionHandler;
|
|
42
31
|
}
|
|
43
32
|
|
|
44
|
-
interface IframeOptions {
|
|
45
|
-
type: "iframe";
|
|
46
|
-
getIframe: () => HTMLIFrameElement | null;
|
|
47
|
-
onSelect: SelectionHandler;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type HighlighterOptions = MarkdownOptions | IframeOptions;
|
|
51
|
-
|
|
52
33
|
export function createHighlighter(options: HighlighterOptions): Highlighter {
|
|
53
|
-
return options.type === "markdown"
|
|
54
|
-
? createMarkdownHighlighter(options)
|
|
55
|
-
: createIframeHighlighter(options);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function createMarkdownHighlighter(options: MarkdownOptions): Highlighter {
|
|
59
34
|
const { root, container, onSelect } = options;
|
|
60
35
|
|
|
61
|
-
let positionCallback: PositionChangeHandler | undefined;
|
|
62
36
|
let hoverCallback: HoverHandler | undefined;
|
|
63
37
|
let clickCallback: ClickHandler | undefined;
|
|
64
|
-
|
|
38
|
+
|
|
39
|
+
// Incremental diffing state — avoids full clear+rebuild on every comment change
|
|
40
|
+
const activeHighlights = new Map<string, { start: number; end: number }>();
|
|
41
|
+
let lastTextContent = "";
|
|
42
|
+
|
|
43
|
+
// Web Worker for anchor resolution (offloads indexOf from main thread)
|
|
44
|
+
const resolver = new Resolver();
|
|
45
|
+
let resolveGeneration = 0;
|
|
65
46
|
|
|
66
47
|
const handleMouseUp = () => {
|
|
67
48
|
const selection = window.getSelection();
|
|
@@ -145,252 +126,117 @@ function createMarkdownHighlighter(options: MarkdownOptions): Highlighter {
|
|
|
145
126
|
}
|
|
146
127
|
};
|
|
147
128
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
129
|
+
const applyDiff = (
|
|
130
|
+
textContent: string,
|
|
131
|
+
resolved: Map<
|
|
132
|
+
string,
|
|
133
|
+
{ start: number; end: number; comment: HighlightComment }
|
|
134
|
+
>,
|
|
135
|
+
) => {
|
|
136
|
+
const toRemove: string[] = [];
|
|
137
|
+
const toAdd: string[] = [];
|
|
138
|
+
|
|
139
|
+
for (const [id, prev] of activeHighlights) {
|
|
140
|
+
const next = resolved.get(id);
|
|
141
|
+
if (!next) {
|
|
142
|
+
toRemove.push(id);
|
|
143
|
+
} else if (prev.start !== next.start || prev.end !== next.end) {
|
|
144
|
+
toRemove.push(id);
|
|
145
|
+
toAdd.push(id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
166
148
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
window.addEventListener("resize", updatePositions);
|
|
149
|
+
for (const id of resolved.keys()) {
|
|
150
|
+
if (!activeHighlights.has(id)) {
|
|
151
|
+
toAdd.push(id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
173
154
|
|
|
174
|
-
|
|
175
|
-
applyHighlights(
|
|
176
|
-
comments: HighlightComment[],
|
|
177
|
-
_pendingSelection?: TextRange,
|
|
178
|
-
) {
|
|
179
|
-
clearHighlights(root);
|
|
155
|
+
if (toRemove.length === 0 && toAdd.length === 0) return;
|
|
180
156
|
|
|
181
|
-
|
|
157
|
+
for (const id of toRemove) {
|
|
158
|
+
clearHighlights(root, `mark[data-comment-id="${id}"]`);
|
|
159
|
+
activeHighlights.delete(id);
|
|
160
|
+
}
|
|
182
161
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
c.selectedText,
|
|
188
|
-
c.startOffset,
|
|
189
|
-
);
|
|
190
|
-
if (anchor) {
|
|
191
|
-
return { ...c, startOffset: anchor.start, endOffset: anchor.end };
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
})
|
|
195
|
-
.filter((c): c is HighlightComment => c !== null)
|
|
196
|
-
.sort((a, b) => a.startOffset - b.startOffset);
|
|
162
|
+
if (toAdd.length > 0) {
|
|
163
|
+
const newHighlights = toAdd
|
|
164
|
+
.map((id) => resolved.get(id)!)
|
|
165
|
+
.sort((a, b) => a.start - b.start);
|
|
197
166
|
|
|
198
167
|
applyHighlightBatch(
|
|
199
168
|
root,
|
|
200
169
|
textContent,
|
|
201
|
-
|
|
202
|
-
startOffset:
|
|
203
|
-
endOffset:
|
|
170
|
+
newHighlights.map((h) => ({
|
|
171
|
+
startOffset: h.start,
|
|
172
|
+
endOffset: h.end,
|
|
204
173
|
style: {
|
|
205
174
|
attribute: "data-comment-id",
|
|
206
|
-
attributeValue: comment.id,
|
|
175
|
+
attributeValue: h.comment.id,
|
|
207
176
|
colorIndex: 0,
|
|
208
177
|
},
|
|
209
178
|
})),
|
|
210
179
|
);
|
|
211
180
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
},
|
|
216
|
-
|
|
217
|
-
clearHighlights() {
|
|
218
|
-
clearHighlights(root);
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
getPositions(): HighlightPositions {
|
|
222
|
-
const containerRect = container.getBoundingClientRect();
|
|
223
|
-
return collectHighlightPositions(root, containerRect, window.scrollY);
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
onPositionsChange(callback: PositionChangeHandler) {
|
|
227
|
-
positionCallback = callback;
|
|
228
|
-
return () => {
|
|
229
|
-
positionCallback = undefined;
|
|
230
|
-
};
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
onHighlightHover(callback: HoverHandler) {
|
|
234
|
-
hoverCallback = callback;
|
|
235
|
-
return () => {
|
|
236
|
-
hoverCallback = undefined;
|
|
237
|
-
};
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
onHighlightClick(callback: ClickHandler) {
|
|
241
|
-
clickCallback = callback;
|
|
242
|
-
return () => {
|
|
243
|
-
clickCallback = undefined;
|
|
244
|
-
};
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
dispose() {
|
|
248
|
-
root.removeEventListener("mouseup", handleMouseUp);
|
|
249
|
-
root.removeEventListener("mouseover", handleMouseOver);
|
|
250
|
-
root.removeEventListener("mouseout", handleMouseOut);
|
|
251
|
-
root.removeEventListener("click", handleClick);
|
|
252
|
-
window.removeEventListener("scroll", handleScroll);
|
|
253
|
-
window.removeEventListener("resize", updatePositions);
|
|
254
|
-
if (scrollRafId !== null) {
|
|
255
|
-
cancelAnimationFrame(scrollRafId);
|
|
181
|
+
for (const id of toAdd) {
|
|
182
|
+
const range = resolved.get(id)!;
|
|
183
|
+
activeHighlights.set(id, { start: range.start, end: range.end });
|
|
256
184
|
}
|
|
257
|
-
|
|
258
|
-
hoverCallback = undefined;
|
|
259
|
-
clickCallback = undefined;
|
|
260
|
-
},
|
|
185
|
+
}
|
|
261
186
|
};
|
|
262
|
-
}
|
|
263
187
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
let positionCallback: PositionChangeHandler | undefined;
|
|
269
|
-
let hoverCallback: HoverHandler | undefined;
|
|
270
|
-
let clickCallback: ClickHandler | undefined;
|
|
271
|
-
let contentHeightCallback: ContentHeightHandler | undefined;
|
|
272
|
-
let pendingHighlights:
|
|
273
|
-
| { comments: HighlightComment[]; pending?: TextRange }
|
|
274
|
-
| undefined;
|
|
275
|
-
|
|
276
|
-
const handleMessage = (event: MessageEvent) => {
|
|
277
|
-
const iframe = getIframe();
|
|
278
|
-
if (!iframe || iframe.contentWindow !== event.source) return;
|
|
279
|
-
|
|
280
|
-
switch (event.data.type) {
|
|
281
|
-
case "iframeReady":
|
|
282
|
-
isReady = true;
|
|
283
|
-
if (pendingHighlights) {
|
|
284
|
-
sendHighlights(pendingHighlights.comments, pendingHighlights.pending);
|
|
285
|
-
pendingHighlights = undefined;
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
288
|
-
|
|
289
|
-
case "textSelection":
|
|
290
|
-
onSelect(
|
|
291
|
-
event.data.text,
|
|
292
|
-
event.data.startOffset,
|
|
293
|
-
event.data.endOffset,
|
|
294
|
-
event.data.selectionTop ?? 0,
|
|
295
|
-
);
|
|
296
|
-
break;
|
|
297
|
-
|
|
298
|
-
case "highlightPositions":
|
|
299
|
-
if (positionCallback) {
|
|
300
|
-
const positions: Record<string, number> = {};
|
|
301
|
-
const documentPositions: Record<string, number> = {};
|
|
302
|
-
for (const [id, top] of Object.entries(event.data.positions)) {
|
|
303
|
-
if (typeof top === "number") {
|
|
304
|
-
positions[id] = top;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
for (const [id, top] of Object.entries(
|
|
308
|
-
event.data.documentPositions || {},
|
|
309
|
-
)) {
|
|
310
|
-
if (typeof top === "number") {
|
|
311
|
-
documentPositions[id] = top;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
positionCallback({
|
|
315
|
-
positions,
|
|
316
|
-
documentPositions,
|
|
317
|
-
pendingTop:
|
|
318
|
-
typeof event.data.pendingTop === "number"
|
|
319
|
-
? event.data.pendingTop
|
|
320
|
-
: undefined,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
break;
|
|
188
|
+
root.addEventListener("mouseup", handleMouseUp);
|
|
189
|
+
root.addEventListener("mouseover", handleMouseOver);
|
|
190
|
+
root.addEventListener("mouseout", handleMouseOut);
|
|
191
|
+
root.addEventListener("click", handleClick);
|
|
324
192
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
break;
|
|
193
|
+
return {
|
|
194
|
+
applyHighlights(comments: HighlightComment[]) {
|
|
195
|
+
const textContent = getDOMTextContent(root);
|
|
330
196
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
197
|
+
// If DOM content changed (e.g. document reload), full rebuild is required
|
|
198
|
+
const contentChanged = textContent !== lastTextContent;
|
|
199
|
+
if (contentChanged) {
|
|
200
|
+
clearHighlights(root);
|
|
201
|
+
activeHighlights.clear();
|
|
202
|
+
lastTextContent = textContent;
|
|
203
|
+
}
|
|
336
204
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
contentHeightCallback(event.data.height);
|
|
340
|
-
}
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
};
|
|
205
|
+
// Bump generation so stale Worker responses are discarded
|
|
206
|
+
const generation = ++resolveGeneration;
|
|
344
207
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const iframe = getIframe();
|
|
350
|
-
iframe?.contentWindow?.postMessage(
|
|
351
|
-
{
|
|
352
|
-
type: "applyHighlights",
|
|
353
|
-
comments: comments.map((c) => ({
|
|
354
|
-
id: c.id,
|
|
355
|
-
selectedText: c.selectedText,
|
|
356
|
-
startOffset: c.startOffset,
|
|
357
|
-
endOffset: c.endOffset,
|
|
358
|
-
})),
|
|
359
|
-
pendingSelection: pending ?? null,
|
|
360
|
-
},
|
|
361
|
-
"*",
|
|
362
|
-
);
|
|
363
|
-
};
|
|
208
|
+
// Resolve anchors off the main thread, then diff and apply
|
|
209
|
+
resolver.resolve(textContent, comments).then((anchorMap) => {
|
|
210
|
+
// Discard if a newer applyHighlights call has started
|
|
211
|
+
if (generation !== resolveGeneration) return;
|
|
364
212
|
|
|
365
|
-
|
|
213
|
+
const resolved = new Map<
|
|
214
|
+
string,
|
|
215
|
+
{ start: number; end: number; comment: HighlightComment }
|
|
216
|
+
>();
|
|
217
|
+
for (const c of comments) {
|
|
218
|
+
const anchor = anchorMap.get(c.id);
|
|
219
|
+
if (anchor) {
|
|
220
|
+
resolved.set(c.id, {
|
|
221
|
+
start: anchor.start,
|
|
222
|
+
end: anchor.end,
|
|
223
|
+
comment: {
|
|
224
|
+
...c,
|
|
225
|
+
startOffset: anchor.start,
|
|
226
|
+
endOffset: anchor.end,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
366
231
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
comments: HighlightComment[],
|
|
370
|
-
pendingSelection?: TextRange,
|
|
371
|
-
) {
|
|
372
|
-
if (isReady) {
|
|
373
|
-
sendHighlights(comments, pendingSelection);
|
|
374
|
-
} else {
|
|
375
|
-
pendingHighlights = { comments, pending: pendingSelection };
|
|
376
|
-
}
|
|
232
|
+
applyDiff(textContent, resolved);
|
|
233
|
+
});
|
|
377
234
|
},
|
|
378
235
|
|
|
379
236
|
clearHighlights() {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
getPositions(): HighlightPositions {
|
|
386
|
-
return { positions: {}, documentPositions: {} };
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
onPositionsChange(callback: PositionChangeHandler) {
|
|
390
|
-
positionCallback = callback;
|
|
391
|
-
return () => {
|
|
392
|
-
positionCallback = undefined;
|
|
393
|
-
};
|
|
237
|
+
clearHighlights(root);
|
|
238
|
+
activeHighlights.clear();
|
|
239
|
+
lastTextContent = "";
|
|
394
240
|
},
|
|
395
241
|
|
|
396
242
|
onHighlightHover(callback: HoverHandler) {
|
|
@@ -407,21 +253,15 @@ function createIframeHighlighter(options: IframeOptions): Highlighter {
|
|
|
407
253
|
};
|
|
408
254
|
},
|
|
409
255
|
|
|
410
|
-
onContentHeightChange(callback: ContentHeightHandler) {
|
|
411
|
-
contentHeightCallback = callback;
|
|
412
|
-
return () => {
|
|
413
|
-
contentHeightCallback = undefined;
|
|
414
|
-
};
|
|
415
|
-
},
|
|
416
|
-
|
|
417
256
|
dispose() {
|
|
418
|
-
|
|
419
|
-
|
|
257
|
+
resolveGeneration++;
|
|
258
|
+
resolver.dispose();
|
|
259
|
+
root.removeEventListener("mouseup", handleMouseUp);
|
|
260
|
+
root.removeEventListener("mouseover", handleMouseOver);
|
|
261
|
+
root.removeEventListener("mouseout", handleMouseOut);
|
|
262
|
+
root.removeEventListener("click", handleClick);
|
|
420
263
|
hoverCallback = undefined;
|
|
421
264
|
clickCallback = undefined;
|
|
422
|
-
contentHeightCallback = undefined;
|
|
423
|
-
isReady = false;
|
|
424
|
-
pendingHighlights = undefined;
|
|
425
265
|
},
|
|
426
266
|
};
|
|
427
267
|
}
|