@peaske7/readit 0.1.7 → 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 +133 -178
- package/src/{cli/index.ts → cli.ts} +211 -107
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
- 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} +111 -81
- 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
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color palette for comment highlights.
|
|
3
|
-
* Colors are assigned by document position (top-to-bottom).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const COMMENT_COLORS = [
|
|
7
|
-
{
|
|
8
|
-
name: "amber",
|
|
9
|
-
bg: "rgba(245, 222, 160, 0.5)",
|
|
10
|
-
bgFocused: "rgba(228, 195, 110, 0.65)",
|
|
11
|
-
border: "#c9a84a",
|
|
12
|
-
text: "#8b6914",
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
name: "blue",
|
|
16
|
-
bg: "rgba(168, 196, 228, 0.5)",
|
|
17
|
-
bgFocused: "rgba(130, 168, 210, 0.65)",
|
|
18
|
-
border: "#5b7fa8",
|
|
19
|
-
text: "#3d5f8a",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: "green",
|
|
23
|
-
bg: "rgba(170, 210, 170, 0.5)",
|
|
24
|
-
bgFocused: "rgba(130, 185, 135, 0.65)",
|
|
25
|
-
border: "#5a9a62",
|
|
26
|
-
text: "#3d6e45",
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: "rose",
|
|
30
|
-
bg: "rgba(225, 180, 185, 0.5)",
|
|
31
|
-
bgFocused: "rgba(205, 145, 155, 0.65)",
|
|
32
|
-
border: "#b86b78",
|
|
33
|
-
text: "#8a4a55",
|
|
34
|
-
},
|
|
35
|
-
] as const;
|
|
36
|
-
|
|
37
|
-
export type CommentColor = (typeof COMMENT_COLORS)[number];
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { TextPosition } from "./types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Find text position in content, handling duplicate occurrences.
|
|
5
|
-
* Returns the occurrence closest to hintOffset when multiple exist.
|
|
6
|
-
*/
|
|
7
|
-
export function findTextPosition(
|
|
8
|
-
textContent: string,
|
|
9
|
-
selectedText: string,
|
|
10
|
-
hintOffset?: number,
|
|
11
|
-
): TextPosition | undefined {
|
|
12
|
-
if (!selectedText || !textContent) {
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const occurrences: number[] = [];
|
|
17
|
-
let idx = 0;
|
|
18
|
-
|
|
19
|
-
for (;;) {
|
|
20
|
-
idx = textContent.indexOf(selectedText, idx);
|
|
21
|
-
if (idx === -1) break;
|
|
22
|
-
occurrences.push(idx);
|
|
23
|
-
idx += 1;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (occurrences.length === 0) {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (occurrences.length === 1) {
|
|
31
|
-
return {
|
|
32
|
-
start: occurrences[0],
|
|
33
|
-
end: occurrences[0] + selectedText.length,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Multiple occurrences: find closest to hint offset
|
|
38
|
-
const target = hintOffset ?? 0;
|
|
39
|
-
let closest = occurrences[0];
|
|
40
|
-
let minDist = Math.abs(closest - target);
|
|
41
|
-
|
|
42
|
-
for (const occ of occurrences) {
|
|
43
|
-
const dist = Math.abs(occ - target);
|
|
44
|
-
if (dist < minDist) {
|
|
45
|
-
minDist = dist;
|
|
46
|
-
closest = occ;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
start: closest,
|
|
52
|
-
end: closest + selectedText.length,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Highlighter (unified adapter)
|
|
2
|
-
|
|
3
|
-
export type { CommentColor } from "./colors";
|
|
4
|
-
// Colors
|
|
5
|
-
export { COMMENT_COLORS } from "./colors";
|
|
6
|
-
export type {
|
|
7
|
-
Highlighter,
|
|
8
|
-
HighlighterOptions,
|
|
9
|
-
HoverHandler,
|
|
10
|
-
PositionChangeHandler,
|
|
11
|
-
SelectionHandler,
|
|
12
|
-
} from "./highlighter";
|
|
13
|
-
export { createHighlighter } from "./highlighter";
|
|
14
|
-
|
|
15
|
-
// Script builder (needed by IframeContainer)
|
|
16
|
-
export { buildIframeScript } from "./script-builder";
|
|
17
|
-
|
|
18
|
-
// Types (public API)
|
|
19
|
-
export type {
|
|
20
|
-
HighlightComment,
|
|
21
|
-
HighlightPositions,
|
|
22
|
-
HighlightStyle,
|
|
23
|
-
} from "./types";
|
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the JavaScript script to be injected into the iframe.
|
|
3
|
-
*
|
|
4
|
-
* This script contains the core highlighting functions that run inside
|
|
5
|
-
* the sandboxed iframe, communicating with the parent via postMessage.
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: DUPLICATED FUNCTIONS
|
|
8
|
-
* ================================
|
|
9
|
-
* The following functions are duplicated from TypeScript sources.
|
|
10
|
-
* They must be kept in sync manually. When modifying any of these
|
|
11
|
-
* functions, update BOTH locations.
|
|
12
|
-
*
|
|
13
|
-
* Duplicated from core.ts:
|
|
14
|
-
* - findTextPosition()
|
|
15
|
-
*
|
|
16
|
-
* Duplicated from dom.ts:
|
|
17
|
-
* - getTextOffset()
|
|
18
|
-
* - getDOMTextContent()
|
|
19
|
-
* - collectTextNodes()
|
|
20
|
-
* - applyHighlightToRange()
|
|
21
|
-
* - clearHighlights()
|
|
22
|
-
* - collectHighlightPositions() (viewport variant)
|
|
23
|
-
*
|
|
24
|
-
* Why duplication exists:
|
|
25
|
-
* The iframe runs in a sandboxed environment and receives content
|
|
26
|
-
* via srcdoc. It cannot import TypeScript modules. The functions
|
|
27
|
-
* must be embedded as plain JavaScript strings.
|
|
28
|
-
*
|
|
29
|
-
* Keeping them in sync:
|
|
30
|
-
* The TypeScript sources (core.ts, dom.ts) are the source of truth.
|
|
31
|
-
* Tests in core.test.ts verify the behavior. If you change the
|
|
32
|
-
* TypeScript implementation, manually update the corresponding
|
|
33
|
-
* function here to match.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Build the complete iframe script with parent origin for secure postMessage.
|
|
38
|
-
*/
|
|
39
|
-
export function buildIframeScript(parentOrigin: string): string {
|
|
40
|
-
return `
|
|
41
|
-
<script>
|
|
42
|
-
(function() {
|
|
43
|
-
const parentOrigin = ${JSON.stringify(parentOrigin)};
|
|
44
|
-
const root = document.body;
|
|
45
|
-
|
|
46
|
-
// --- Core Functions (from core.ts) ---
|
|
47
|
-
|
|
48
|
-
function findTextPosition(textContent, selectedText, hintOffset) {
|
|
49
|
-
if (!selectedText || !textContent) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const occurrences = [];
|
|
54
|
-
let idx = 0;
|
|
55
|
-
|
|
56
|
-
for (;;) {
|
|
57
|
-
idx = textContent.indexOf(selectedText, idx);
|
|
58
|
-
if (idx === -1) break;
|
|
59
|
-
occurrences.push(idx);
|
|
60
|
-
idx += 1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (occurrences.length === 0) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (occurrences.length === 1) {
|
|
68
|
-
return {
|
|
69
|
-
start: occurrences[0],
|
|
70
|
-
end: occurrences[0] + selectedText.length,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Multiple occurrences: find closest to hint offset
|
|
75
|
-
const target = hintOffset ?? 0;
|
|
76
|
-
let closest = occurrences[0];
|
|
77
|
-
let minDist = Math.abs(closest - target);
|
|
78
|
-
|
|
79
|
-
for (const occ of occurrences) {
|
|
80
|
-
const dist = Math.abs(occ - target);
|
|
81
|
-
if (dist < minDist) {
|
|
82
|
-
minDist = dist;
|
|
83
|
-
closest = occ;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
start: closest,
|
|
89
|
-
end: closest + selectedText.length,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// --- DOM Functions (from dom.ts) ---
|
|
94
|
-
|
|
95
|
-
const BLOCK_ELEMENTS = new Set([
|
|
96
|
-
'P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
97
|
-
'PRE', 'BLOCKQUOTE', 'LI', 'TR', 'BR'
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
function findBlockParent(node) {
|
|
101
|
-
let parent = node.parentElement;
|
|
102
|
-
while (parent && !BLOCK_ELEMENTS.has(parent.tagName)) {
|
|
103
|
-
parent = parent.parentElement;
|
|
104
|
-
}
|
|
105
|
-
return parent;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function getTextOffset(root, targetNode, targetOffset) {
|
|
109
|
-
let offset = 0;
|
|
110
|
-
let lastBlockParent = null;
|
|
111
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
112
|
-
|
|
113
|
-
let node = walker.nextNode();
|
|
114
|
-
while (node) {
|
|
115
|
-
const blockParent = findBlockParent(node);
|
|
116
|
-
|
|
117
|
-
// Add newline when transitioning between different block parents
|
|
118
|
-
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
119
|
-
if (!lastBlockParent.contains(blockParent) && !blockParent.contains(lastBlockParent)) {
|
|
120
|
-
offset += 1; // Account for the newline
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (node === targetNode) {
|
|
125
|
-
return offset + targetOffset;
|
|
126
|
-
}
|
|
127
|
-
offset += (node.textContent?.length ?? 0);
|
|
128
|
-
lastBlockParent = blockParent;
|
|
129
|
-
node = walker.nextNode();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return offset;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function getDOMTextContent(root) {
|
|
136
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
137
|
-
let text = '';
|
|
138
|
-
let lastBlockParent = null;
|
|
139
|
-
let node = walker.nextNode();
|
|
140
|
-
|
|
141
|
-
while (node) {
|
|
142
|
-
const blockParent = findBlockParent(node);
|
|
143
|
-
|
|
144
|
-
// Insert newline when transitioning between different block parents
|
|
145
|
-
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
146
|
-
if (!lastBlockParent.contains(blockParent) && !blockParent.contains(lastBlockParent)) {
|
|
147
|
-
text += '\\n';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
text += node.textContent ?? '';
|
|
152
|
-
lastBlockParent = blockParent;
|
|
153
|
-
node = walker.nextNode();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return text;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function collectTextNodes(root) {
|
|
160
|
-
const textNodes = [];
|
|
161
|
-
let currentOffset = 0;
|
|
162
|
-
let lastBlockParent = null;
|
|
163
|
-
|
|
164
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
165
|
-
let node = walker.nextNode();
|
|
166
|
-
|
|
167
|
-
while (node) {
|
|
168
|
-
const blockParent = findBlockParent(node);
|
|
169
|
-
|
|
170
|
-
// Account for newline when transitioning between different block parents
|
|
171
|
-
// (same logic as getDOMTextContent)
|
|
172
|
-
if (lastBlockParent && blockParent && lastBlockParent !== blockParent) {
|
|
173
|
-
if (!lastBlockParent.contains(blockParent) && !blockParent.contains(lastBlockParent)) {
|
|
174
|
-
currentOffset += 1; // Account for the newline
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const length = node.textContent?.length ?? 0;
|
|
179
|
-
textNodes.push({
|
|
180
|
-
node: node,
|
|
181
|
-
start: currentOffset,
|
|
182
|
-
end: currentOffset + length,
|
|
183
|
-
});
|
|
184
|
-
currentOffset += length;
|
|
185
|
-
lastBlockParent = blockParent;
|
|
186
|
-
node = walker.nextNode();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return textNodes;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function applyHighlightToRange(root, startOffset, endOffset, style) {
|
|
193
|
-
const textNodes = collectTextNodes(root);
|
|
194
|
-
|
|
195
|
-
const overlappingNodes = textNodes.filter(
|
|
196
|
-
n => n.end > startOffset && n.start < endOffset
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
if (overlappingNodes.length === 0) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
for (const { node: textNode, start } of overlappingNodes) {
|
|
204
|
-
const nodeStart = Math.max(0, startOffset - start);
|
|
205
|
-
const nodeEnd = Math.min(textNode.length, endOffset - start);
|
|
206
|
-
|
|
207
|
-
if (nodeStart >= nodeEnd) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const range = document.createRange();
|
|
212
|
-
range.setStart(textNode, nodeStart);
|
|
213
|
-
range.setEnd(textNode, nodeEnd);
|
|
214
|
-
|
|
215
|
-
const mark = document.createElement('mark');
|
|
216
|
-
mark.setAttribute(style.attribute, style.attributeValue);
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
range.surroundContents(mark);
|
|
220
|
-
} catch (e) {
|
|
221
|
-
// Range crosses element boundaries (e.g., syntax-highlighted code blocks)
|
|
222
|
-
// Use extractContents + insertNode as fallback
|
|
223
|
-
try {
|
|
224
|
-
const fragment = range.extractContents();
|
|
225
|
-
mark.appendChild(fragment);
|
|
226
|
-
range.insertNode(mark);
|
|
227
|
-
} catch (e2) {
|
|
228
|
-
// If even extractContents fails, skip this node
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function clearHighlights(root) {
|
|
235
|
-
const marks = root.querySelectorAll('mark[data-comment-id], mark[data-pending]');
|
|
236
|
-
|
|
237
|
-
for (const mark of marks) {
|
|
238
|
-
const parent = mark.parentNode;
|
|
239
|
-
if (parent) {
|
|
240
|
-
while (mark.firstChild) {
|
|
241
|
-
parent.insertBefore(mark.firstChild, mark);
|
|
242
|
-
}
|
|
243
|
-
parent.removeChild(mark);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function collectHighlightPositions(root) {
|
|
249
|
-
const positions = {};
|
|
250
|
-
const documentPositions = {};
|
|
251
|
-
const scrollY = window.scrollY || 0;
|
|
252
|
-
|
|
253
|
-
const marks = root.querySelectorAll('mark[data-comment-id]');
|
|
254
|
-
for (const mark of marks) {
|
|
255
|
-
const commentId = mark.getAttribute('data-comment-id');
|
|
256
|
-
if (!commentId || positions[commentId] !== undefined) continue;
|
|
257
|
-
|
|
258
|
-
const rect = mark.getBoundingClientRect();
|
|
259
|
-
positions[commentId] = rect.top;
|
|
260
|
-
documentPositions[commentId] = rect.top + scrollY;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
let pendingTop = null;
|
|
264
|
-
const pendingMark = root.querySelector('mark[data-pending]');
|
|
265
|
-
if (pendingMark) {
|
|
266
|
-
const pendingRect = pendingMark.getBoundingClientRect();
|
|
267
|
-
pendingTop = pendingRect.top;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return { positions, documentPositions, pendingTop };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// --- Selection Handler ---
|
|
274
|
-
|
|
275
|
-
document.addEventListener('mouseup', function() {
|
|
276
|
-
const selection = window.getSelection();
|
|
277
|
-
if (!selection || selection.isCollapsed) return;
|
|
278
|
-
|
|
279
|
-
// Normalize whitespace: collapse any sequence of whitespace containing newlines
|
|
280
|
-
// Browser's selection.toString() includes CSS margins as extra newlines/spaces
|
|
281
|
-
const text = selection.toString().trim().replace(/\\r?\\n\\s*/g, '\\n');
|
|
282
|
-
if (text.length === 0) return;
|
|
283
|
-
|
|
284
|
-
const range = selection.getRangeAt(0);
|
|
285
|
-
const startOffset = getTextOffset(root, range.startContainer, range.startOffset);
|
|
286
|
-
const endOffset = getTextOffset(root, range.endContainer, range.endOffset);
|
|
287
|
-
|
|
288
|
-
const rangeRect = range.getBoundingClientRect();
|
|
289
|
-
const selectionTop = rangeRect.top + document.documentElement.scrollTop;
|
|
290
|
-
|
|
291
|
-
parent.postMessage({
|
|
292
|
-
type: 'textSelection',
|
|
293
|
-
text: text,
|
|
294
|
-
startOffset: startOffset,
|
|
295
|
-
endOffset: endOffset,
|
|
296
|
-
selectionTop: selectionTop
|
|
297
|
-
}, parentOrigin);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// --- Message Handler ---
|
|
301
|
-
|
|
302
|
-
window.addEventListener('message', function(event) {
|
|
303
|
-
// Handle scroll to heading request from parent
|
|
304
|
-
if (event.data.type === 'scrollToHeading') {
|
|
305
|
-
const id = event.data.id;
|
|
306
|
-
const element = document.getElementById(id);
|
|
307
|
-
if (element) {
|
|
308
|
-
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Handle scroll to highlight request from parent
|
|
314
|
-
if (event.data.type === 'scrollToHighlight') {
|
|
315
|
-
const mark = document.querySelector('mark[data-comment-id="' + event.data.commentId + '"]');
|
|
316
|
-
if (mark) {
|
|
317
|
-
mark.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (event.data.type === 'applyHighlights') {
|
|
323
|
-
clearHighlights(root);
|
|
324
|
-
|
|
325
|
-
const comments = event.data.comments || [];
|
|
326
|
-
const pending = event.data.pendingSelection;
|
|
327
|
-
|
|
328
|
-
const textContent = getDOMTextContent(root);
|
|
329
|
-
|
|
330
|
-
// Resolve anchors and apply highlights
|
|
331
|
-
const resolved = comments
|
|
332
|
-
.map(function(c) {
|
|
333
|
-
const anchor = findTextPosition(textContent, c.selectedText, c.startOffset);
|
|
334
|
-
if (anchor) {
|
|
335
|
-
return { id: c.id, startOffset: anchor.start, endOffset: anchor.end };
|
|
336
|
-
}
|
|
337
|
-
return { id: c.id, startOffset: c.startOffset, endOffset: c.endOffset };
|
|
338
|
-
})
|
|
339
|
-
.sort(function(a, b) { return a.startOffset - b.startOffset; });
|
|
340
|
-
|
|
341
|
-
for (const comment of resolved) {
|
|
342
|
-
applyHighlightToRange(root, comment.startOffset, comment.endOffset, {
|
|
343
|
-
attribute: 'data-comment-id',
|
|
344
|
-
attributeValue: comment.id
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (pending) {
|
|
349
|
-
applyHighlightToRange(root, pending.startOffset, pending.endOffset, {
|
|
350
|
-
attribute: 'data-pending',
|
|
351
|
-
attributeValue: 'true'
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
setTimeout(function() {
|
|
356
|
-
reportPositions();
|
|
357
|
-
reportContentHeight();
|
|
358
|
-
}, 50);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// --- Position Reporting ---
|
|
363
|
-
|
|
364
|
-
function reportPositions() {
|
|
365
|
-
const result = collectHighlightPositions(root);
|
|
366
|
-
parent.postMessage({
|
|
367
|
-
type: 'highlightPositions',
|
|
368
|
-
positions: result.positions,
|
|
369
|
-
documentPositions: result.documentPositions,
|
|
370
|
-
pendingTop: result.pendingTop
|
|
371
|
-
}, parentOrigin);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// --- Content Height Reporting ---
|
|
375
|
-
|
|
376
|
-
function reportContentHeight() {
|
|
377
|
-
parent.postMessage({
|
|
378
|
-
type: 'contentHeight',
|
|
379
|
-
height: document.body.scrollHeight
|
|
380
|
-
}, parentOrigin);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
window.addEventListener('scroll', reportPositions, { passive: true });
|
|
384
|
-
document.addEventListener('scroll', reportPositions, { passive: true });
|
|
385
|
-
window.addEventListener('resize', function() {
|
|
386
|
-
reportPositions();
|
|
387
|
-
reportContentHeight();
|
|
388
|
-
});
|
|
389
|
-
window.addEventListener('load', reportContentHeight);
|
|
390
|
-
|
|
391
|
-
// --- Hover Handlers ---
|
|
392
|
-
|
|
393
|
-
document.addEventListener('mouseover', function(e) {
|
|
394
|
-
const mark = e.target.closest('mark[data-comment-id]');
|
|
395
|
-
if (mark) {
|
|
396
|
-
parent.postMessage({
|
|
397
|
-
type: 'highlightHover',
|
|
398
|
-
commentId: mark.getAttribute('data-comment-id')
|
|
399
|
-
}, parentOrigin);
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
document.addEventListener('mouseout', function(e) {
|
|
404
|
-
const mark = e.target.closest('mark[data-comment-id]');
|
|
405
|
-
if (mark) {
|
|
406
|
-
const related = e.relatedTarget?.closest?.('mark[data-comment-id]');
|
|
407
|
-
if (!related || related.getAttribute('data-comment-id') !== mark.getAttribute('data-comment-id')) {
|
|
408
|
-
parent.postMessage({ type: 'highlightHover', commentId: null }, parentOrigin);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
document.addEventListener('click', function(e) {
|
|
414
|
-
const mark = e.target.closest('mark[data-comment-id]');
|
|
415
|
-
if (mark) {
|
|
416
|
-
parent.postMessage({
|
|
417
|
-
type: 'highlightClick',
|
|
418
|
-
commentId: mark.getAttribute('data-comment-id')
|
|
419
|
-
}, parentOrigin);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// --- Ready Signal ---
|
|
424
|
-
|
|
425
|
-
parent.postMessage({ type: 'iframeReady' }, parentOrigin);
|
|
426
|
-
|
|
427
|
-
// --- Ensure Heading IDs for TOC navigation ---
|
|
428
|
-
|
|
429
|
-
function ensureHeadingIds() {
|
|
430
|
-
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
431
|
-
const seenIds = {};
|
|
432
|
-
|
|
433
|
-
for (const heading of headings) {
|
|
434
|
-
if (!heading.id) {
|
|
435
|
-
let id = (heading.textContent || '')
|
|
436
|
-
.toLowerCase()
|
|
437
|
-
.trim()
|
|
438
|
-
.replace(/[^a-z0-9 -]/g, '')
|
|
439
|
-
.replace(/ +/g, '-')
|
|
440
|
-
.replace(/-+/g, '-');
|
|
441
|
-
|
|
442
|
-
// Handle duplicates
|
|
443
|
-
const baseId = id;
|
|
444
|
-
const count = seenIds[baseId] || 0;
|
|
445
|
-
if (count > 0) {
|
|
446
|
-
id = baseId + '-' + count;
|
|
447
|
-
}
|
|
448
|
-
seenIds[baseId] = count + 1;
|
|
449
|
-
|
|
450
|
-
heading.id = id;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
ensureHeadingIds();
|
|
455
|
-
|
|
456
|
-
// Height reporting delays to catch layout shifts
|
|
457
|
-
const HEIGHT_REPORT_DELAY_SHORT = 100;
|
|
458
|
-
const HEIGHT_REPORT_DELAY_LONG = 500;
|
|
459
|
-
|
|
460
|
-
// Report initial height reliably - use multiple strategies
|
|
461
|
-
function scheduleHeightReport() {
|
|
462
|
-
// Immediate report
|
|
463
|
-
reportContentHeight();
|
|
464
|
-
// Delayed report to catch layout shifts
|
|
465
|
-
setTimeout(reportContentHeight, HEIGHT_REPORT_DELAY_SHORT);
|
|
466
|
-
setTimeout(reportContentHeight, HEIGHT_REPORT_DELAY_LONG);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (document.readyState === 'complete') {
|
|
470
|
-
scheduleHeightReport();
|
|
471
|
-
} else {
|
|
472
|
-
window.addEventListener('load', scheduleHeightReport);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Watch for content size changes with ResizeObserver
|
|
476
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
477
|
-
const resizeObserver = new ResizeObserver(function() {
|
|
478
|
-
reportContentHeight();
|
|
479
|
-
});
|
|
480
|
-
resizeObserver.observe(document.body);
|
|
481
|
-
}
|
|
482
|
-
})();
|
|
483
|
-
</script>
|
|
484
|
-
`;
|
|
485
|
-
}
|