generated from coulomb/repo-seed
Implement CE-WP-0002 T01-T02: engine types + PDF viewer adapter spike
T01: shared engine types (Document, Selector union, Annotation, EvidenceItem, branded IDs with newId factory) per wiki/SharedContracts.md §1-§3. T02: react-pdf-highlighter-plus v1.1.4 spike behind the §5 DocumentViewerAdapter contract in src/anchor/. Pure round-trip math extracted to pdf-selector-math.ts with 11 unit tests proving lossless capture → selectors → JSON → restored-rects. ADR-0004 accepted; full user-flow Playwright verification deferred to T09. Adds Vite app shell (index.html, src/app/SpikeApp.tsx) so the spike is exercisable via pnpm dev. tsconfig --noEmit prevents tsc -b from littering src/ with stray .js outputs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,7 @@
|
||||
export {};
|
||||
export * from "./types";
|
||||
export {
|
||||
PdfSpikeViewer,
|
||||
selectorsFromPdfCapture,
|
||||
type PdfSpikeViewerProps,
|
||||
type StoredAnnotation,
|
||||
} from "./pdf-viewer-adapter-spike";
|
||||
|
||||
111
src/anchor/pdf-selector-math.test.ts
Normal file
111
src/anchor/pdf-selector-math.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Round-trip tests for the spike's pure transformation layer.
|
||||
*
|
||||
* These tests are CE-WP-0002-T02's machine-verifiable evidence that the
|
||||
* adapter's data round-trip is lossless: a captured PDF selection becomes
|
||||
* a `Selector[]`, the `Selector[]` round-trips through JSON
|
||||
* (localStorage-equivalent), and the reconstructed PDF rect + page match
|
||||
* the original. The browser-side selection-capture path is exercised in
|
||||
* T09 against production code.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findPdfRectSelector,
|
||||
findTextQuoteSelector,
|
||||
selectorsFromPdfCapture,
|
||||
unionRect,
|
||||
} from "./pdf-selector-math";
|
||||
import type { PdfSelectionCapture } from "./types";
|
||||
import type { NormalizedRect, Selector } from "@shared/selector";
|
||||
|
||||
const SAMPLE_CAPTURE: PdfSelectionCapture = {
|
||||
kind: "pdf",
|
||||
text: "Mitglied beim Lohnsteuerhilfeverein Vereinigte Lohnsteuerhilfe e.V.",
|
||||
page: 1,
|
||||
rects: [
|
||||
{ x: 0.12, y: 0.34, width: 0.55, height: 0.02 },
|
||||
{ x: 0.12, y: 0.37, width: 0.31, height: 0.02 },
|
||||
],
|
||||
boundingRect: { x: 0.12, y: 0.34, width: 0.55, height: 0.05 },
|
||||
};
|
||||
|
||||
describe("selectorsFromPdfCapture", () => {
|
||||
it("produces a TextQuoteSelector and PdfRectSelector from a normal capture", () => {
|
||||
const sels = selectorsFromPdfCapture(SAMPLE_CAPTURE);
|
||||
expect(sels.map((s) => s.type)).toEqual(["TextQuoteSelector", "PdfRectSelector"]);
|
||||
});
|
||||
|
||||
it("includes the verbatim quote on the TextQuoteSelector", () => {
|
||||
const tq = findTextQuoteSelector(selectorsFromPdfCapture(SAMPLE_CAPTURE));
|
||||
expect(tq?.exact).toBe(SAMPLE_CAPTURE.text);
|
||||
});
|
||||
|
||||
it("preserves page + rects 1:1 on the PdfRectSelector", () => {
|
||||
const rect = findPdfRectSelector(selectorsFromPdfCapture(SAMPLE_CAPTURE));
|
||||
expect(rect?.page).toBe(SAMPLE_CAPTURE.page);
|
||||
expect(rect?.rects).toEqual(SAMPLE_CAPTURE.rects);
|
||||
});
|
||||
|
||||
it("omits TextQuoteSelector when text is empty", () => {
|
||||
const sels = selectorsFromPdfCapture({ ...SAMPLE_CAPTURE, text: "" });
|
||||
expect(sels.map((s) => s.type)).toEqual(["PdfRectSelector"]);
|
||||
});
|
||||
|
||||
it("omits PdfRectSelector when no rects are present", () => {
|
||||
const sels = selectorsFromPdfCapture({ ...SAMPLE_CAPTURE, rects: [] });
|
||||
expect(sels.map((s) => s.type)).toEqual(["TextQuoteSelector"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Selector[] JSON round-trip", () => {
|
||||
it("survives JSON.stringify/parse without loss (the localStorage path)", () => {
|
||||
const original = selectorsFromPdfCapture(SAMPLE_CAPTURE);
|
||||
const blob = JSON.stringify(original);
|
||||
const restored = JSON.parse(blob) as Selector[];
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
|
||||
it("the restored PdfRectSelector still resolves to the same page and rects", () => {
|
||||
const restored = JSON.parse(JSON.stringify(selectorsFromPdfCapture(SAMPLE_CAPTURE))) as Selector[];
|
||||
const rect = findPdfRectSelector(restored);
|
||||
expect(rect).not.toBeNull();
|
||||
expect(rect?.page).toBe(SAMPLE_CAPTURE.page);
|
||||
expect(rect?.rects).toEqual(SAMPLE_CAPTURE.rects);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unionRect", () => {
|
||||
it("returns null for an empty input", () => {
|
||||
expect(unionRect([])).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the single rect when given exactly one", () => {
|
||||
const r: NormalizedRect = { x: 0.1, y: 0.2, width: 0.3, height: 0.4 };
|
||||
const u = unionRect([r]);
|
||||
expect(u).not.toBeNull();
|
||||
expect(u!.x).toBeCloseTo(r.x, 9);
|
||||
expect(u!.y).toBeCloseTo(r.y, 9);
|
||||
expect(u!.width).toBeCloseTo(r.width, 9);
|
||||
expect(u!.height).toBeCloseTo(r.height, 9);
|
||||
});
|
||||
|
||||
it("computes the bounding box of multi-line text rects", () => {
|
||||
const u = unionRect(SAMPLE_CAPTURE.rects);
|
||||
expect(u).not.toBeNull();
|
||||
expect(u!.x).toBeCloseTo(0.12, 5);
|
||||
expect(u!.y).toBeCloseTo(0.34, 5);
|
||||
expect(u!.width).toBeCloseTo(0.55, 5);
|
||||
expect(u!.height).toBeCloseTo(0.05, 5);
|
||||
});
|
||||
|
||||
it("is order-independent", () => {
|
||||
const reversed = [...SAMPLE_CAPTURE.rects].reverse();
|
||||
const forward = unionRect(SAMPLE_CAPTURE.rects)!;
|
||||
const back = unionRect(reversed)!;
|
||||
expect(back.x).toBeCloseTo(forward.x, 9);
|
||||
expect(back.y).toBeCloseTo(forward.y, 9);
|
||||
expect(back.width).toBeCloseTo(forward.width, 9);
|
||||
expect(back.height).toBeCloseTo(forward.height, 9);
|
||||
});
|
||||
});
|
||||
79
src/anchor/pdf-selector-math.ts
Normal file
79
src/anchor/pdf-selector-math.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Pure, library-free transformations between the adapter's
|
||||
* `PdfSelectionCapture` and the shared `Selector[]` shapes.
|
||||
*
|
||||
* Extracted from `pdf-viewer-adapter-spike.tsx` so the architectural
|
||||
* round-trip contract (capture → selectors → reconstructed rects) can be
|
||||
* unit-tested without pulling in `react-pdf-highlighter-plus`, React, or a
|
||||
* browser. The spike component re-exports `selectorsFromPdfCapture` from
|
||||
* here so there is one implementation, not two.
|
||||
*
|
||||
* This module is the source of truth for T02's "static evidence that the
|
||||
* round-trip is lossless" — see ADR-0004.
|
||||
*/
|
||||
|
||||
import type {
|
||||
NormalizedRect,
|
||||
PdfRectSelector,
|
||||
Selector,
|
||||
TextQuoteSelector,
|
||||
} from "@shared/selector";
|
||||
import type { PdfSelectionCapture } from "./types";
|
||||
|
||||
/** Build `Selector[]` from a captured PDF selection. */
|
||||
export function selectorsFromPdfCapture(capture: PdfSelectionCapture): Selector[] {
|
||||
const out: Selector[] = [];
|
||||
if (capture.text.length > 0) {
|
||||
const textQuote: TextQuoteSelector = {
|
||||
type: "TextQuoteSelector",
|
||||
exact: capture.text,
|
||||
};
|
||||
out.push(textQuote);
|
||||
}
|
||||
if (capture.rects.length > 0) {
|
||||
const rect: PdfRectSelector = {
|
||||
type: "PdfRectSelector",
|
||||
page: capture.page,
|
||||
rects: capture.rects,
|
||||
};
|
||||
out.push(rect);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Find the `PdfRectSelector` in a selector list, if any. */
|
||||
export function findPdfRectSelector(
|
||||
selectors: readonly Selector[],
|
||||
): PdfRectSelector | null {
|
||||
return (
|
||||
selectors.find((s): s is PdfRectSelector => s.type === "PdfRectSelector") ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** Find the `TextQuoteSelector` in a selector list, if any. */
|
||||
export function findTextQuoteSelector(
|
||||
selectors: readonly Selector[],
|
||||
): TextQuoteSelector | null {
|
||||
return (
|
||||
selectors.find((s): s is TextQuoteSelector => s.type === "TextQuoteSelector") ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Bounding rectangle of a non-empty list of normalized rects. */
|
||||
export function unionRect(rects: readonly NormalizedRect[]): NormalizedRect | null {
|
||||
if (rects.length === 0) return null;
|
||||
const first = rects[0]!;
|
||||
let minX = first.x;
|
||||
let minY = first.y;
|
||||
let maxX = first.x + first.width;
|
||||
let maxY = first.y + first.height;
|
||||
for (let i = 1; i < rects.length; i++) {
|
||||
const r = rects[i]!;
|
||||
if (r.x < minX) minX = r.x;
|
||||
if (r.y < minY) minY = r.y;
|
||||
if (r.x + r.width > maxX) maxX = r.x + r.width;
|
||||
if (r.y + r.height > maxY) maxY = r.y + r.height;
|
||||
}
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
192
src/anchor/pdf-viewer-adapter-spike.tsx
Normal file
192
src/anchor/pdf-viewer-adapter-spike.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Throwaway PDF viewer adapter spike (CE-WP-0002-T02).
|
||||
*
|
||||
* Purpose: prove that `react-pdf-highlighter-plus` can implement the §5
|
||||
* `DocumentViewerAdapter` contract end-to-end (select → save selectors →
|
||||
* reload → resolve → scroll → render highlight) without leaking PDF.js
|
||||
* types into `src/shared/` or `src/engine/`.
|
||||
*
|
||||
* This module is the only place in the codebase that imports
|
||||
* `react-pdf-highlighter-plus`. The exported React component is consumed
|
||||
* by `src/app/SpikeApp.tsx`.
|
||||
*
|
||||
* Replace before production. T03 (source ingest) + T04 (anchor resolution)
|
||||
* will build the real PDFViewerAdapter on top of this lessons-learned.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import {
|
||||
PdfHighlighter,
|
||||
PdfLoader,
|
||||
TextHighlight,
|
||||
MonitoredHighlightContainer,
|
||||
useHighlightContainerContext,
|
||||
type Highlight,
|
||||
type PdfHighlighterUtils,
|
||||
type PdfSelection,
|
||||
type ScaledPosition,
|
||||
} from "react-pdf-highlighter-plus";
|
||||
import "react-pdf-highlighter-plus/style/style.css";
|
||||
import "react-pdf-highlighter-plus/style/pdf_viewer.css";
|
||||
|
||||
import type { NormalizedRect, Selector } from "@shared/selector";
|
||||
import type { AnchorResolution, PdfSelectionCapture, ResolvedAnchorTarget } from "./types";
|
||||
import { findPdfRectSelector, selectorsFromPdfCapture, unionRect } from "./pdf-selector-math";
|
||||
|
||||
export { selectorsFromPdfCapture };
|
||||
|
||||
/**
|
||||
* Inverse of `selectorsFromPdfCapture`: build a viewer-renderable
|
||||
* `Highlight` from stored selectors. The spike's reload path leans on
|
||||
* `PdfRectSelector` since it carries page + page-relative rects directly.
|
||||
* T04 will own the production resolver and add the text-only paths.
|
||||
*/
|
||||
function highlightFromSelectors(
|
||||
id: string,
|
||||
text: string,
|
||||
selectors: readonly Selector[],
|
||||
): Highlight | null {
|
||||
const rectSel = findPdfRectSelector(selectors);
|
||||
if (!rectSel) return null;
|
||||
const boundingRect = unionRect(rectSel.rects);
|
||||
if (!boundingRect) return null;
|
||||
const scaledRects = rectSel.rects.map((r) => toScaled(r, rectSel.page));
|
||||
return {
|
||||
id,
|
||||
type: "text",
|
||||
content: { text },
|
||||
position: {
|
||||
boundingRect: toScaled(boundingRect, rectSel.page),
|
||||
rects: scaledRects,
|
||||
} satisfies ScaledPosition,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the adapter's `NormalizedRect` (page-relative 0..1) to the
|
||||
* `Scaled` shape react-pdf-highlighter-plus expects (also normalized 0..1
|
||||
* via width/height). We use a unit page-space of 1×1 — the library
|
||||
* computes pixel coords from `pageNumber` and the renderer's actual page
|
||||
* dimensions.
|
||||
*/
|
||||
function toScaled(r: NormalizedRect, page: number) {
|
||||
return {
|
||||
x1: r.x,
|
||||
y1: r.y,
|
||||
x2: r.x + r.width,
|
||||
y2: r.y + r.height,
|
||||
width: 1,
|
||||
height: 1,
|
||||
pageNumber: page,
|
||||
};
|
||||
}
|
||||
|
||||
/** PdfSelection → our domain-neutral `PdfSelectionCapture`. */
|
||||
function captureFromPdfSelection(sel: PdfSelection): PdfSelectionCapture {
|
||||
const page = sel.position.boundingRect.pageNumber;
|
||||
const rects = sel.position.rects.map<NormalizedRect>((r) => ({
|
||||
x: r.x1 / r.width,
|
||||
y: r.y1 / r.height,
|
||||
width: (r.x2 - r.x1) / r.width,
|
||||
height: (r.y2 - r.y1) / r.height,
|
||||
}));
|
||||
const br = sel.position.boundingRect;
|
||||
const boundingRect: NormalizedRect = {
|
||||
x: br.x1 / br.width,
|
||||
y: br.y1 / br.height,
|
||||
width: (br.x2 - br.x1) / br.width,
|
||||
height: (br.y2 - br.y1) / br.height,
|
||||
};
|
||||
return {
|
||||
kind: "pdf",
|
||||
text: sel.content.text ?? "",
|
||||
page,
|
||||
rects,
|
||||
boundingRect,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial container that renders every stored highlight as a TextHighlight.
|
||||
* For the spike, no editing tooling — just visual proof of "did the saved
|
||||
* coordinates land on the right passage on the right page after reload?"
|
||||
*/
|
||||
function SpikeHighlightContainer(): ReactNode {
|
||||
const { highlight, isScrolledTo } = useHighlightContainerContext();
|
||||
return (
|
||||
<MonitoredHighlightContainer>
|
||||
<TextHighlight highlight={highlight} isScrolledTo={isScrolledTo} />
|
||||
</MonitoredHighlightContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export interface PdfSpikeViewerProps {
|
||||
/** URL of the PDF to load (served by Vite dev server). */
|
||||
readonly pdfUrl: string;
|
||||
/** Previously-saved selector sets to restore on mount. */
|
||||
readonly storedAnnotations: readonly StoredAnnotation[];
|
||||
/** Called when the user produces a new selection. */
|
||||
onSelectionCaptured(capture: PdfSelectionCapture, selectors: Selector[]): void;
|
||||
/** Annotation id to scroll to and highlight on mount, if any. */
|
||||
readonly scrollToAnnotationId?: string;
|
||||
}
|
||||
|
||||
export interface StoredAnnotation {
|
||||
readonly id: string;
|
||||
readonly text: string;
|
||||
readonly selectors: readonly Selector[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The spike's React component. Renders a PDF and:
|
||||
* - emits `onSelectionCaptured(capture, selectors)` on every fresh selection
|
||||
* - reconstructs and renders `storedAnnotations` immediately on load
|
||||
* - scrolls to `scrollToAnnotationId` if its highlight can be reconstructed
|
||||
*/
|
||||
export function PdfSpikeViewer(props: PdfSpikeViewerProps) {
|
||||
const { pdfUrl, storedAnnotations, onSelectionCaptured, scrollToAnnotationId } = props;
|
||||
const utilsRef = useRef<PdfHighlighterUtils | null>(null);
|
||||
const [didScroll, setDidScroll] = useState<string | null>(null);
|
||||
|
||||
const highlights = useMemo<Highlight[]>(() => {
|
||||
const out: Highlight[] = [];
|
||||
for (const a of storedAnnotations) {
|
||||
const h = highlightFromSelectors(a.id, a.text, a.selectors);
|
||||
if (h) out.push(h);
|
||||
}
|
||||
return out;
|
||||
}, [storedAnnotations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollToAnnotationId || didScroll === scrollToAnnotationId) return;
|
||||
const utils = utilsRef.current;
|
||||
const target = highlights.find((h) => h.id === scrollToAnnotationId);
|
||||
if (!utils || !target) return;
|
||||
utils.scrollToHighlight(target);
|
||||
setDidScroll(scrollToAnnotationId);
|
||||
}, [scrollToAnnotationId, highlights, didScroll]);
|
||||
|
||||
return (
|
||||
<PdfLoader document={pdfUrl}>
|
||||
{(pdfDocument) => (
|
||||
<PdfHighlighter
|
||||
pdfDocument={pdfDocument}
|
||||
highlights={highlights}
|
||||
utilsRef={(u) => {
|
||||
utilsRef.current = u;
|
||||
}}
|
||||
onSelection={(selection) => {
|
||||
const capture = captureFromPdfSelection(selection);
|
||||
const selectors = selectorsFromPdfCapture(capture);
|
||||
onSelectionCaptured(capture, selectors);
|
||||
}}
|
||||
>
|
||||
<SpikeHighlightContainer />
|
||||
</PdfHighlighter>
|
||||
)}
|
||||
</PdfLoader>
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export the §5 contract surface so callers see anchor as one entry point.
|
||||
export type { AnchorResolution, ResolvedAnchorTarget, PdfSelectionCapture };
|
||||
97
src/anchor/types.ts
Normal file
97
src/anchor/types.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Adapter-side types owned by `evidence-anchor`.
|
||||
*
|
||||
* Implements the contract surface from `wiki/SharedContracts.md` §5 and the
|
||||
* resolution result shape from `wiki/ArchitectureOverview.md` §3.3 / §7.
|
||||
*
|
||||
* Anything that mentions a concrete viewer library (pdfjs, react-pdf-highlighter-plus)
|
||||
* lives *behind* this surface, never on it. `src/shared/` and `src/engine/`
|
||||
* must never import this file.
|
||||
*/
|
||||
|
||||
import type { Document, DocumentRepresentation } from "@shared/document";
|
||||
import type { Selector } from "@shared/selector";
|
||||
import type { AnnotationResolutionStatus } from "@shared/annotation";
|
||||
import type { NormalizedRect } from "@shared/selector";
|
||||
|
||||
/**
|
||||
* The raw selection captured from a viewer adapter — an opaque payload that
|
||||
* the adapter understands. The shape is intentionally permissive: each
|
||||
* concrete adapter narrows the `kind` discriminator and adds its own
|
||||
* payload. The shared layer never inspects the payload directly.
|
||||
*/
|
||||
export type SelectionCapture =
|
||||
| PdfSelectionCapture
|
||||
| DomSelectionCapture;
|
||||
|
||||
export interface PdfSelectionCapture {
|
||||
readonly kind: "pdf";
|
||||
/** Verbatim selected text, before canonical normalisation. */
|
||||
readonly text: string;
|
||||
/** 1-indexed physical page number the selection started on. */
|
||||
readonly page: number;
|
||||
/** Page-relative normalized rectangles covering the selection (0..1). */
|
||||
readonly rects: readonly NormalizedRect[];
|
||||
/** Optional bounding rectangle (page-relative, normalized). */
|
||||
readonly boundingRect?: NormalizedRect;
|
||||
}
|
||||
|
||||
/** Reserved for the HTML/Markdown adapter. Not implementable in MVP. */
|
||||
export type DomSelectionCapture = never;
|
||||
|
||||
/**
|
||||
* A passage located inside a representation, ready to be scrolled to and
|
||||
* highlighted.
|
||||
*/
|
||||
export interface ResolvedAnchorTarget {
|
||||
readonly representationId: string;
|
||||
/** 1-indexed page (PDF) or undefined for HTML/Markdown. */
|
||||
readonly page?: number;
|
||||
/** Page-relative normalized rectangles to highlight. */
|
||||
readonly rects?: readonly NormalizedRect[];
|
||||
/** Canonical-text offsets, when known. */
|
||||
readonly textPosition?: { readonly start: number; readonly end: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* The outcome of asking the adapter to resolve a `Selector[]`.
|
||||
* Matches `wiki/ArchitectureOverview.md` §3.3.
|
||||
*/
|
||||
export interface AnchorResolution {
|
||||
readonly status: AnnotationResolutionStatus;
|
||||
/** 0..1 confidence in the best candidate. */
|
||||
readonly confidence: number;
|
||||
readonly candidates: readonly ResolvedAnchorTarget[];
|
||||
/** Names of the selector kinds that produced a usable candidate. */
|
||||
readonly usedSelectorTypes: readonly string[];
|
||||
readonly warnings?: readonly string[];
|
||||
}
|
||||
|
||||
export interface HighlightRenderOptions {
|
||||
readonly color?: string;
|
||||
readonly opacity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format-neutral viewer adapter contract from `wiki/SharedContracts.md` §5.
|
||||
*
|
||||
* Concrete implementations live alongside the viewer they wrap (e.g. the
|
||||
* PDF spike in `src/anchor/pdf-viewer-adapter-spike.tsx`). The shared/engine
|
||||
* layers depend only on this interface.
|
||||
*/
|
||||
export interface DocumentViewerAdapter {
|
||||
readonly mediaTypes: readonly string[];
|
||||
load(document: Document, representation?: DocumentRepresentation): Promise<void>;
|
||||
getCurrentSelection(): Promise<SelectionCapture | null>;
|
||||
createSelectorsFromSelection(selection: SelectionCapture): Promise<Selector[]>;
|
||||
resolveSelectors(selectors: readonly Selector[]): Promise<AnchorResolution>;
|
||||
scrollToResolvedTarget(
|
||||
target: ResolvedAnchorTarget,
|
||||
opts?: { readonly center?: boolean; readonly behavior?: "auto" | "smooth" },
|
||||
): Promise<void>;
|
||||
renderHighlight(
|
||||
target: ResolvedAnchorTarget,
|
||||
opts?: HighlightRenderOptions,
|
||||
): Promise<void>;
|
||||
getHighlightClientRects(annotationId: string): Promise<readonly DOMRect[]>;
|
||||
}
|
||||
Reference in New Issue
Block a user