Skip to content

Commit 2178101

Browse files
committed
fix: links in annotation pdfs
1 parent 69d3090 commit 2178101

11 files changed

Lines changed: 134 additions & 245 deletions

File tree

content/public/content/notes/ai-beat-us.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: AI has already won — what's left for us, now?
3-
date: 2026-02-15
3+
date: 1970-0im-15
44
tags:
55
- engineering/ai
66
- engineering

quartz.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ const config: QuartzConfig = {
178178
}),
179179
Plugin.GitHubFlavoredMarkdown(),
180180
Plugin.TableOfContents(),
181-
Plugin.CrawlLinks({ markdownLinkResolution: "absolute" }),
181+
Plugin.CrawlLinks({
182+
markdownLinkResolution: "absolute",
183+
openLinksInNewTab: true,
184+
}),
182185
Plugin.Description(),
183186
Plugin.Latex({ renderEngine: "katex" }),
184187
],

quartz/components/AnnotationViewer.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,6 @@ export default (() => {
145145
data-start={textPosition?.start}
146146
data-end={textPosition?.end}
147147
>
148-
<div class="annotation-quote">
149-
{textQuote?.prefix && (
150-
<span class="annotation-prefix">{textQuote.prefix}...</span>
151-
)}
152-
<span class="annotation-highlight">{textQuote?.exact}</span>
153-
{textQuote?.suffix && (
154-
<span class="annotation-suffix">...{textQuote.suffix}</span>
155-
)}
156-
</div>
157-
158148
{annotation.text && (
159149
<div class="annotation-comment">
160150
<div dangerouslySetInnerHTML={{ __html: annotation.text }} />

quartz/components/scripts/annotationViewer/adapters/scrollSync.ts

Lines changed: 38 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,7 @@ function findTextScrollPosition(startOffset: number, endOffset: number): number
6060
}
6161

6262
/**
63-
* Tracks which panel is currently being scrolled by the user
64-
*/
65-
type ScrollSource = "pdf" | "sidebar" | "programmatic" | null
66-
67-
/**
68-
* Set up bidirectional scroll synchronization between PDF and annotations
63+
* Set up click-based navigation between PDF and annotations
6964
*/
7065
export function setupScrollSync(viewer: Element): void {
7166
const pdfContainer = viewer.querySelector(".annotation-pdf-container") as HTMLElement
@@ -100,166 +95,8 @@ export function setupScrollSync(viewer: Element): void {
10095
}
10196
})
10297

103-
// State management for scroll synchronization
104-
let scrollSource: ScrollSource = null
105-
let programmaticScrollTarget: HTMLElement | null = null
106-
let debounceTimeout: NodeJS.Timeout | null = null
107-
let resetSourceTimeout: NodeJS.Timeout | null = null
108-
109-
/**
110-
* Set the scroll source and schedule reset
111-
*/
112-
function setScrollSource(source: ScrollSource, duration: number = 600): void {
113-
scrollSource = source
114-
115-
if (resetSourceTimeout) clearTimeout(resetSourceTimeout)
116-
117-
if (source !== null) {
118-
resetSourceTimeout = setTimeout(() => {
119-
scrollSource = null
120-
programmaticScrollTarget = null
121-
}, duration)
122-
}
123-
}
124-
125-
/**
126-
* Detect if a scroll event is user-initiated on a specific element
127-
*/
128-
function isUserScroll(element: HTMLElement): boolean {
129-
// If we're in programmatic mode and this is the target, it's not user scroll
130-
if (scrollSource === "programmatic" && programmaticScrollTarget === element) {
131-
return false
132-
}
133-
134-
// If scroll source is already set to the other panel, this is programmatic
135-
if (scrollSource === "pdf" && element === sidebar) return false
136-
if (scrollSource === "sidebar" && element === pdfContainer) return false
137-
138-
// Otherwise, it's a user scroll
139-
return true
140-
}
141-
142-
// Track user interaction to detect scroll intent
143-
let pdfUserInteracting = false
144-
let sidebarUserInteracting = false
145-
146-
pdfContainer.addEventListener("mouseenter", () => { pdfUserInteracting = true })
147-
pdfContainer.addEventListener("mouseleave", () => { pdfUserInteracting = false })
148-
sidebar.addEventListener("mouseenter", () => { sidebarUserInteracting = true })
149-
sidebar.addEventListener("mouseleave", () => { sidebarUserInteracting = false })
150-
151-
// Debounced scroll handlers
152-
const onPdfScroll = () => {
153-
if (!isUserScroll(pdfContainer)) return
154-
155-
// Set source if this is user-initiated
156-
if (pdfUserInteracting && scrollSource !== "pdf") {
157-
setScrollSource("pdf")
158-
}
159-
160-
// Clear existing timeout
161-
if (debounceTimeout) clearTimeout(debounceTimeout)
162-
163-
// Debounce to wait for scroll to settle
164-
debounceTimeout = setTimeout(() => {
165-
if (scrollSource !== "pdf") return
166-
167-
const scrollTop = pdfContainer.scrollTop
168-
const scrollCenter = scrollTop + pdfContainer.clientHeight / 2
169-
170-
let closestAnnotation = null
171-
let minDistance = Infinity
172-
173-
annotationItems.forEach((ann) => {
174-
const pdfY = parseFloat(ann.getAttribute("data-pdf-y") || "0")
175-
if (pdfY > 0) {
176-
const distance = Math.abs(pdfY - scrollCenter)
177-
if (distance < minDistance) {
178-
minDistance = distance
179-
closestAnnotation = ann
180-
}
181-
}
182-
})
183-
184-
if (closestAnnotation) {
185-
syncToAnnotation(closestAnnotation)
186-
}
187-
}, 150)
188-
}
189-
190-
// When annotations scroll, find closest annotation and center its highlight in PDF
191-
const onAnnotationScroll = () => {
192-
if (!isUserScroll(sidebar)) return
193-
194-
// Set source if this is user-initiated
195-
if (sidebarUserInteracting && scrollSource !== "sidebar") {
196-
setScrollSource("sidebar")
197-
}
198-
199-
// Clear existing timeout
200-
if (debounceTimeout) clearTimeout(debounceTimeout)
201-
202-
// Debounce to wait for scroll to settle
203-
debounceTimeout = setTimeout(() => {
204-
if (scrollSource !== "sidebar") return
205-
206-
const scrollTop = sidebar.scrollTop
207-
const scrollCenter = scrollTop + sidebar.clientHeight / 2
208-
209-
let closestAnnotation: HTMLElement | null = null
210-
let minDistance = Infinity
211-
212-
annotationItems.forEach((ann) => {
213-
const annotationTop = ann.offsetTop
214-
const annotationCenter = annotationTop + ann.clientHeight / 2
215-
const distance = Math.abs(annotationCenter - scrollCenter)
216-
217-
if (distance < minDistance) {
218-
minDistance = distance
219-
closestAnnotation = ann
220-
}
221-
})
222-
223-
if (closestAnnotation) {
224-
syncToPDF(closestAnnotation)
225-
}
226-
}, 150)
227-
}
228-
22998
/**
230-
* Sync sidebar to match PDF scroll position
231-
*/
232-
function syncToAnnotation(activeAnnotation: HTMLElement): void {
233-
updateActiveAnnotation(activeAnnotation)
234-
235-
// Scroll annotation into center view
236-
setScrollSource("programmatic", 600)
237-
programmaticScrollTarget = sidebar
238-
239-
const annotationTop = activeAnnotation.offsetTop
240-
const targetScroll =
241-
annotationTop - sidebar.clientHeight / 2 + activeAnnotation.clientHeight / 2
242-
sidebar.scrollTo({ top: targetScroll, behavior: "smooth" })
243-
}
244-
245-
/**
246-
* Sync PDF to match sidebar scroll position
247-
*/
248-
function syncToPDF(activeAnnotation: HTMLElement): void {
249-
updateActiveAnnotation(activeAnnotation)
250-
251-
const pdfY = parseFloat(activeAnnotation.getAttribute("data-pdf-y") || "0")
252-
if (pdfY > 0) {
253-
setScrollSource("programmatic", 600)
254-
programmaticScrollTarget = pdfContainer
255-
256-
const targetScroll = pdfY - pdfContainer.clientHeight / 2
257-
pdfContainer.scrollTo({ top: targetScroll, behavior: "smooth" })
258-
}
259-
}
260-
261-
/**
262-
* Update which annotation is marked as active and render highlights
99+
* Update which annotation is marked as active and highlight accordingly
263100
*/
264101
function updateActiveAnnotation(activeAnnotation: HTMLElement): void {
265102
// Mark as active
@@ -271,34 +108,56 @@ export function setupScrollSync(viewer: Element): void {
271108
}
272109
})
273110

274-
// Render highlight for active annotation
111+
// Update active highlights
275112
const annotationId = activeAnnotation.getAttribute("data-annotation-id")
276-
const annotationData = window.annotationsData?.find((a) => a.id === annotationId)
277-
if (annotationData) {
278-
window.renderHighlights(annotationData)
113+
if (annotationId && window.setActiveHighlight) {
114+
window.setActiveHighlight(annotationId)
279115
}
280116
}
281117

282-
pdfContainer.addEventListener("scroll", onPdfScroll)
283-
sidebar.addEventListener("scroll", onAnnotationScroll)
284-
285118
// Click annotation to scroll to its position in PDF
286119
annotationItems.forEach((annotation) => {
287120
annotation.addEventListener("click", () => {
288121
const pdfY = parseFloat(annotation.getAttribute("data-pdf-y") || "0")
289122
if (pdfY > 0) {
290-
setScrollSource("programmatic", 600)
291-
programmaticScrollTarget = pdfContainer
292-
293-
const scrollTarget = pdfY - pdfContainer.clientHeight / 2
123+
// Scroll to top of highlight with small offset for better visibility
124+
const scrollTarget = pdfY - 16
294125
pdfContainer.scrollTo({ top: scrollTarget, behavior: "smooth" })
295126

296127
updateActiveAnnotation(annotation)
297128
}
298129
})
299130
})
300131

301-
// Initial update
302-
setScrollSource("pdf", 100)
303-
setTimeout(() => onPdfScroll(), 50)
132+
// Click highlight to scroll to its annotation in sidebar
133+
// Use event delegation on the PDF container since highlights are created dynamically
134+
pdfContainer.addEventListener("click", (event) => {
135+
const target = event.target as HTMLElement
136+
if (target.classList.contains("pdf-text-highlight")) {
137+
const annotationId = target.getAttribute("data-annotation-id")
138+
const annotation = annotationItems.find(
139+
(ann) => ann.getAttribute("data-annotation-id") === annotationId,
140+
)
141+
142+
if (annotation) {
143+
const annotationTop = annotation.offsetTop
144+
// Scroll to top of annotation with small offset for better visibility
145+
const targetScroll = annotationTop - 16
146+
sidebar.scrollTo({ top: targetScroll, behavior: "smooth" })
147+
148+
updateActiveAnnotation(annotation)
149+
}
150+
}
151+
})
152+
153+
// Render all highlights initially (after text layers are ready)
154+
// Use setTimeout to ensure functions are exposed on window
155+
setTimeout(() => {
156+
if (window.renderAllHighlights) {
157+
console.log("[ScrollSync] Rendering all highlights")
158+
window.renderAllHighlights()
159+
} else {
160+
console.warn("[ScrollSync] window.renderAllHighlights not available")
161+
}
162+
}, 100)
304163
}

quartz/components/scripts/annotationViewer/core/highlighting.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Annotation } from "./types"
22

33
/**
4-
* Render highlights for an annotation's quoted text using TextLayer positioning
4+
* Render highlights for a single annotation
55
*/
6-
export function renderHighlights(annotation: Annotation): void {
7-
// Clear existing highlights
8-
document.querySelectorAll(".pdf-text-highlight").forEach((el) => el.remove())
9-
6+
function renderAnnotationHighlights(
7+
annotation: Annotation,
8+
isActive: boolean = false,
9+
): void {
1010
if (!annotation.target || annotation.target.length === 0) return
1111

1212
// Find TextPositionSelector
@@ -74,7 +74,8 @@ export function renderHighlights(annotation: Annotation): void {
7474

7575
// Position highlight relative to the text layer (which shares parent with highlight layer)
7676
const highlight = document.createElement("div")
77-
highlight.className = "pdf-text-highlight"
77+
highlight.className = isActive ? "pdf-text-highlight active" : "pdf-text-highlight"
78+
highlight.setAttribute("data-annotation-id", annotation.id)
7879
highlight.style.position = "absolute"
7980
// Use offsetLeft/offsetTop for position (relative to offsetParent)
8081
highlight.style.left = `${span.offsetLeft}px`
@@ -85,3 +86,44 @@ export function renderHighlights(annotation: Annotation): void {
8586
highlightLayer.appendChild(highlight)
8687
}
8788
}
89+
90+
/**
91+
* Render highlights for all annotations at once
92+
*/
93+
export function renderAllHighlights(activeAnnotationId?: string): void {
94+
// Clear existing highlights
95+
document.querySelectorAll(".pdf-text-highlight").forEach((el) => el.remove())
96+
97+
const annotations = window.annotationsData
98+
if (!annotations) return
99+
100+
// Render each annotation's highlights
101+
annotations.forEach((annotation) => {
102+
const isActive = annotation.id === activeAnnotationId
103+
renderAnnotationHighlights(annotation, isActive)
104+
})
105+
}
106+
107+
/**
108+
* Toggle which highlights are marked as active
109+
*/
110+
export function setActiveHighlight(annotationId: string): void {
111+
const highlights = document.querySelectorAll(".pdf-text-highlight")
112+
113+
highlights.forEach((highlight) => {
114+
const highlightAnnotationId = highlight.getAttribute("data-annotation-id")
115+
if (highlightAnnotationId === annotationId) {
116+
highlight.classList.add("active")
117+
} else {
118+
highlight.classList.remove("active")
119+
}
120+
})
121+
}
122+
123+
/**
124+
* Legacy function for backward compatibility
125+
* @deprecated Use renderAllHighlights() and setActiveHighlight() instead
126+
*/
127+
export function renderHighlights(annotation: Annotation): void {
128+
renderAllHighlights(annotation.id)
129+
}

quartz/components/scripts/annotationViewer/core/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ declare global {
5252
pdfScale: number
5353
annotationsData: Annotation[]
5454
renderHighlights: (annotation: Annotation) => void
55+
renderAllHighlights: (activeAnnotationId?: string) => void
56+
setActiveHighlight: (annotationId: string) => void
5557
}
5658
}
5759

quartz/components/scripts/annotationViewer/main.inline.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { loadPDFLib, loadAnnotationsData } from "./ui/loader"
22
import { initPDFViewer } from "./adapters/lifecycle"
3-
import { renderHighlights } from "./core/highlighting"
3+
import { renderHighlights, renderAllHighlights, setActiveHighlight } from "./core/highlighting"
44

55
// Wrap in IIFE and handle multiple initialization scenarios
66
;(async () => {
@@ -38,8 +38,10 @@ import { renderHighlights } from "./core/highlighting"
3838
await initAnnotationViewer()
3939
}
4040

41-
// Expose renderHighlights globally for interactive use
41+
// Expose highlighting functions globally for interactive use
4242
window.renderHighlights = renderHighlights
43+
window.renderAllHighlights = renderAllHighlights
44+
window.setActiveHighlight = setActiveHighlight
4345
})()
4446

4547
export default ""

0 commit comments

Comments
 (0)