Guides

PDF viewer: passages, anchors and quotes

The add-on @skaile/forge-editor-pdf-viewer: the source-anchor property, a pdfjs viewer with a text layer, scrolling to a passage on reveal, and „Zitat anlegen“, which creates a quote block derived from the PDF.

The add-on @skaile/forge-editor-pdf-viewer shows a PDF next to the text that cites it. A text selection in the PDF becomes an anchor: the page, the area on the page and the quoted words. The button „Zitat anlegen“ turns the anchor into a quote Typed block that is derived from the PDF. A click on a reference chip with a page locator scrolls the viewer to the passage.

PieceWhat it doesWhere
source-anchorthe Block property type of a passage of a PDFlibrary (@skaile/forge-editor/core)
page=<n>&anchor=<base64 JSON>the reference locator that names such a passagelibrary
quoteBlockTypes(locale)the Block type quote with one source-anchor propertyeditor and server
anchorOfSelection, quoteBlockOfpure: selection to anchor, anchor to quote blockanywhere
pdfViewerFeature()the viewer in the inspector of a Typed block that keeps a PDFeditor
PdfViewerthe viewer as a component for any pageeditor

The source-anchor property and the locator

A source-anchor value is { page, rects, quote: { exact, prefix?, suffix? } }. page counts from 1. rects are fractions of the page (x, y, w, h from 0 to 1, origin top left), so the passage stays right at any zoom. quote.exact is the selected text with white space cut to single blanks; prefix and suffix are a little text around it.

The property kind orders and compares by page, reads as the quoted text and groups by page (PROPERTY_KINDS['source-anchor']), so a query can filter quotes by page or by words. The properties form shows page and quote read-only. Only the viewer writes the value.

The same passage as text is the locator of a reference chip: page=12&anchor=<base64url JSON>. anchorLocatorOf(anchor) writes it and parseAnchorLocator(text) reads it back (null when the text is not an anchor or its page differs from the locator's). Chips and backlinks show it as S. 12; the long base64 stays out of sight. A plain locator (S. 12, 12, 3:07) works as before.

When the editor reveals a source at an anchor locator, it sends two DOM events to the node view of the PDF's file block: sk-reveal-page (detail is the page) and sk-reveal-anchor (detail is the SourceAnchor). The viewer listens to both.

Install and wire

import { defaultBlockTypes } from '@skaile/forge-editor/composites'
import { quoteBlockTypes } from '@skaile/forge-editor-pdf-viewer'
import { pdfViewerFeature } from '@skaile/forge-editor-pdf-viewer/vue' // or '/react'
import '@skaile/forge-editor-pdf-viewer/styles/pdf-viewer.css'

const editor = composeEditor({
  content: 'rich',
  interaction: 'full',
  // The Block types of the editor: the built-in ones and `quote`.
  blockTypes: [...defaultBlockTypes(t), ...quoteBlockTypes('de')],
  add: [pdfViewerFeature({ workerSrc: '/pdf.worker.min.mjs' })],
})

Give the Block type to the server too, so the schema, the AI chat and the query validation know it:

import { quoteBlockTypes } from '@skaile/forge-editor-pdf-viewer/server'

// installEditorServer({ blockTypes })
export const blockTypes = [...serverBlockTypes, ...quoteBlockTypes('de')]

pdfjs needs a worker. By default the viewer loads pdf.worker.min.mjs of the installed pdfjs version from jsDelivr. A host that cannot reach the CDN serves the file from pdfjs-dist/build/ itself and passes its URL as workerSrc (feature option, or prop of PdfViewer).

The viewer

pdfViewerFeature adds a section „PDF-Ansicht“ to the Typed block inspector. It shows for every block whose source (the source capability: paper, book, document) is a PDF file block. The viewer draws only the pages near the visible part, each as a canvas with a transparent text layer on top, so the text can be selected and searched with the browser.

  • The toolbar has previous and next page, a page field, zoom out and in (50 % to 300 %).
  • A reveal of the PDF's block scrolls to the page, or to the first line of the anchor, and marks the anchor's rects for a few seconds. Reveals of other blocks do not move this viewer.
  • PdfViewer takes src (the URL of the PDF), editor (to create quotes and follow reveals) and blockId (the Typed block that keeps the PDF). Without editor it is a plain viewer.

Create a quote

Select text in one page and press Zitat anlegen. The button works when the editor is editable and the PDF is a stored asset of a Document. The quote goes right behind the PDF's Typed block and is revealed.

{
  "type": "typedBlock",
  "attrs": {
    "blockType": "quote",
    "meta": {
      "anchor": { "page": 12, "rects": [{ "x": 0.15, "y": 0.1, "w": 0.5, "h": 0.025 }], "quote": { "exact": "Der Hund bellt laut." } },
      "derivation": { "from": { "asset": "<name>.pdf" }, "method": "highlight", "inputHash": "<sha256 of the PDF>", "outputHash": "<sha256 of the body>", "createdAt": "…" }
    }
  }
}

The title is the first words of the quote. The body is the quoted text as a block quote and, when the PDF belongs to a Typed block, a reference chip to it whose locator is the anchor: a click on the chip in the quote opens the PDF at the passage. Because the quote has a Derivation, the block shows its origin, the Explore trail lists it, and an edit of the body shows as „bearbeitet“.

quoteBlockOf(asset, anchor, options) builds the same JSON without a viewer: use it in an importer or a server tool.

import { anchorOfSelection, quoteBlockOf } from '@skaile/forge-editor-pdf-viewer/server'

const anchor = anchorOfSelection({ page: 3, box: pageRect, rects: selectionRects, text, pageText })
if (anchor) editor.commands.insertContent(quoteBlockOf({ name, hash }, anchor, { sourceBlockId: paperId }))
Copyright © 2026