Commit 3b236bdd authored by Robert Knight's avatar Robert Knight

Typecheck PDF anchoring code

Add `src/types/pdfjs.js` module which defines the subset of the PDF.js
viewer API that the client actually uses and make use of it in
`src/annotator/anchoring/pdf.js` and `src/annotator/plugin/pdf-metadata.js`
to typecheck these files.

As well as helping to catch errors and JSDoc mistakes in these files,
this should also make future PDF.js upgrades easier because we can see
what PDF.js APIs the client actually relies on.
parent 83dfa90a
...@@ -8,10 +8,21 @@ const createNodeIterator = require('dom-node-iterator/polyfill')(); ...@@ -8,10 +8,21 @@ const createNodeIterator = require('dom-node-iterator/polyfill')();
import RenderingStates from '../pdfjs-rendering-states'; import RenderingStates from '../pdfjs-rendering-states';
// @ts-expect-error - `./range` needs to be converted to JS.
import xpathRange from './range'; import xpathRange from './range';
import { toRange as textPositionToRange } from './text-position'; import { toRange as textPositionToRange } from './text-position';
// @ts-expect-error - `./types` needs to be converted to JS.
import { TextPositionAnchor, TextQuoteAnchor } from './types'; import { TextPositionAnchor, TextQuoteAnchor } from './types';
/**
* @typedef {import('../../types/api').TextPositionSelector} TextPositionSelector
* @typedef {import('../../types/api').TextQuoteSelector} TextQuoteSelector
*
* @typedef {import('../../types/pdfjs').PDFPageView} PDFPageView
* @typedef {import('../../types/pdfjs').PDFViewer} PDFViewer
*/
// Caches for performance. // Caches for performance.
/** /**
...@@ -37,6 +48,16 @@ function getNodeTextLayer(node) { ...@@ -37,6 +48,16 @@ function getNodeTextLayer(node) {
return node.getElementsByClassName('textLayer')[0]; return node.getElementsByClassName('textLayer')[0];
} }
/**
* Get the PDF.js viewer application.
*
* @return {PDFViewer}
*/
function getPdfViewer() {
// @ts-ignore - TS doesn't know about PDFViewerApplication global.
return PDFViewerApplication.pdfViewer;
}
/** /**
* Returns the view into which a PDF page is drawn. * Returns the view into which a PDF page is drawn.
* *
...@@ -48,7 +69,7 @@ function getNodeTextLayer(node) { ...@@ -48,7 +69,7 @@ function getNodeTextLayer(node) {
* @return {Promise<PDFPageView>} * @return {Promise<PDFPageView>}
*/ */
async function getPageView(pageIndex) { async function getPageView(pageIndex) {
const pdfViewer = PDFViewerApplication.pdfViewer; const pdfViewer = getPdfViewer();
let pageView = pdfViewer.getPageView(pageIndex); let pageView = pdfViewer.getPageView(pageIndex);
if (!pageView || !pageView.pdfPage) { if (!pageView || !pageView.pdfPage) {
...@@ -68,7 +89,7 @@ async function getPageView(pageIndex) { ...@@ -68,7 +89,7 @@ async function getPageView(pageIndex) {
}); });
} }
return pageView; return /** @type {PDFPageView} */ (pageView);
} }
/** /**
...@@ -150,7 +171,7 @@ function getPageOffset(pageIndex) { ...@@ -150,7 +171,7 @@ function getPageOffset(pageIndex) {
* `offset` within the complete text of the document. * `offset` within the complete text of the document.
* *
* @param {number} offset * @param {number} offset
* @return {PageOffset} * @return {Promise<PageOffset>}
*/ */
function findPage(offset) { function findPage(offset) {
let index = 0; let index = 0;
...@@ -182,7 +203,7 @@ function findPage(offset) { ...@@ -182,7 +203,7 @@ function findPage(offset) {
// 149 | 1 // 149 | 1
// 150 | 2 // 150 | 2
const count = textContent => { const count = textContent => {
const lastPageIndex = PDFViewerApplication.pdfViewer.pagesCount - 1; const lastPageIndex = getPdfViewer().pagesCount - 1;
if (total + textContent.length > offset || index === lastPageIndex) { if (total + textContent.length > offset || index === lastPageIndex) {
// Offset is in current page. // Offset is in current page.
offset = total; offset = total;
...@@ -214,11 +235,11 @@ function findPage(offset) { ...@@ -214,11 +235,11 @@ function findPage(offset) {
async function anchorByPosition(pageIndex, anchor) { async function anchorByPosition(pageIndex, anchor) {
const page = await getPageView(pageIndex); const page = await getPageView(pageIndex);
let renderingDone = false; if (
if (page.textLayer) { page.renderingState === RenderingStates.FINISHED &&
renderingDone = page.textLayer.renderingDone; page.textLayer &&
} page.textLayer.renderingDone
if (page.renderingState === RenderingStates.FINISHED && renderingDone) { ) {
// The page has been rendered. Locate the position in the text layer. // The page has been rendered. Locate the position in the text layer.
const root = page.textLayer.textLayerDiv; const root = page.textLayer.textLayerDiv;
return textPositionToRange(root, anchor.start, anchor.end); return textPositionToRange(root, anchor.start, anchor.end);
...@@ -226,7 +247,7 @@ async function anchorByPosition(pageIndex, anchor) { ...@@ -226,7 +247,7 @@ async function anchorByPosition(pageIndex, anchor) {
// The page has not been rendered yet. Create a placeholder element and // The page has not been rendered yet. Create a placeholder element and
// anchor to that instead. // anchor to that instead.
const div = page.div || page.el; const div = /** @type {HTMLElement} */ (page.div || page.el);
let placeholder = div.getElementsByClassName('annotator-placeholder')[0]; let placeholder = div.getElementsByClassName('annotator-placeholder')[0];
if (!placeholder) { if (!placeholder) {
placeholder = document.createElement('span'); placeholder = document.createElement('span');
...@@ -304,11 +325,11 @@ function findInPages(pageIndexes, quoteSelector, positionHint) { ...@@ -304,11 +325,11 @@ function findInPages(pageIndexes, quoteSelector, positionHint) {
* When a position anchor is available, quote search can be optimized by * When a position anchor is available, quote search can be optimized by
* searching pages nearest the expected position first. * searching pages nearest the expected position first.
* *
* @param [TextPositionAnchor] position * @param {TextPositionAnchor} position
* @return {number[]} * @return {Promise<number[]>}
*/ */
function prioritizePages(position) { function prioritizePages(position) {
const pageCount = PDFViewerApplication.pdfViewer.pagesCount; const pageCount = getPdfViewer().pagesCount;
const pageIndices = Array(pageCount) const pageIndices = Array(pageCount)
.fill(0) .fill(0)
.map((_, i) => i); .map((_, i) => i);
...@@ -317,17 +338,21 @@ function prioritizePages(position) { ...@@ -317,17 +338,21 @@ function prioritizePages(position) {
return Promise.resolve(pageIndices); return Promise.resolve(pageIndices);
} }
// Sort page indexes by offset from `pageIndex`. /**
* Sort page indexes by offset from `pageIndex`.
*
* @param {number} pageIndex
*/
function sortPages(pageIndex) { function sortPages(pageIndex) {
const left = pageIndices.slice(0, pageIndex); const left = pageIndices.slice(0, pageIndex);
const right = pageIndices.slice(pageIndex); const right = pageIndices.slice(pageIndex);
const result = []; const result = [];
while (left.length > 0 || right.length > 0) { while (left.length > 0 || right.length > 0) {
if (right.length) { if (right.length) {
result.push(right.shift()); result.push(/** @type {number} */ (right.shift()));
} }
if (left.length) { if (left.length) {
result.push(left.pop()); result.push(/** @type {number} */ (left.pop()));
} }
} }
return result; return result;
...@@ -347,6 +372,7 @@ export function anchor(root, selectors) { ...@@ -347,6 +372,7 @@ export function anchor(root, selectors) {
const position = selectors.find(s => s.type === 'TextPositionSelector'); const position = selectors.find(s => s.type === 'TextPositionSelector');
const quote = selectors.find(s => s.type === 'TextQuoteSelector'); const quote = selectors.find(s => s.type === 'TextQuoteSelector');
/** @type {Promise<Range>} */
let result = Promise.reject('unable to anchor'); let result = Promise.reject('unable to anchor');
const checkQuote = range => { const checkQuote = range => {
......
import { normalizeURI } from '../util/url'; import { normalizeURI } from '../util/url';
/**
* @typedef {import('../../types/pdfjs').PDFViewerApplication} PDFViewerApplication
*/
/** /**
* @typedef Link * @typedef Link
* @prop {string} href * @prop {string} href
...@@ -30,9 +34,10 @@ export default class PDFMetadata { ...@@ -30,9 +34,10 @@ export default class PDFMetadata {
* Construct a `PDFMetadata` that returns URIs/metadata associated with a * Construct a `PDFMetadata` that returns URIs/metadata associated with a
* given PDF viewer. * given PDF viewer.
* *
* @param {Object} app - The `PDFViewerApplication` global from PDF.js * @param {PDFViewerApplication} app - The `PDFViewerApplication` global from PDF.js
*/ */
constructor(app) { constructor(app) {
/** @type {Promise<PDFViewerApplication>} */
this._loaded = new Promise(resolve => { this._loaded = new Promise(resolve => {
const finish = () => { const finish = () => {
window.removeEventListener('documentload', finish); window.removeEventListener('documentload', finish);
...@@ -89,7 +94,7 @@ export default class PDFMetadata { ...@@ -89,7 +94,7 @@ export default class PDFMetadata {
app.metadata.has('dc:title') && app.metadata.has('dc:title') &&
app.metadata.get('dc:title') !== 'Untitled' app.metadata.get('dc:title') !== 'Untitled'
) { ) {
title = app.metadata.get('dc:title'); title = /** @type {string} */ (app.metadata.get('dc:title'));
} else if (app.documentInfo && app.documentInfo.Title) { } else if (app.documentInfo && app.documentInfo.Title) {
title = app.documentInfo.Title; title = app.documentInfo.Title;
} }
......
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
// Files in `src/annotator` that still have errors to be resolved. // Files in `src/annotator` that still have errors to be resolved.
"annotator/pdf-sidebar.js", "annotator/pdf-sidebar.js",
"annotator/anchoring/pdf.js",
"annotator/plugin/document.js", "annotator/plugin/document.js",
// Enable this once the rest of `src/sidebar` is checked. // Enable this once the rest of `src/sidebar` is checked.
......
/**
* This module defines the subset of the PDF.js interface that the client relies
* on.
*
* PDF.js doesn't provide its own types. There are partial definitions available
* from DefinitelyTyped but these don't include everything we use. The source of
* truth is the pdf.js repo (https://github.com/mozilla/pdf.js/) on GitHub.
* See in particular `src/display/api.js` in that repo.
*
* Note that the definitions here are not complete, they only include properties
* that the client uses. The names of types should match the corresponding
* JSDoc types or classes in the PDF.js source where possible.
*/
/**
* @typedef Metadata
* @prop {(name: string) => string} get
* @prop {(name: string) => boolean} has
*/
/**
* @typedef PDFDocument
* @prop {string} fingerprint
*/
/**
* @typedef PDFDocumentInfo
* @prop {string} [Title]
*/
/**
* @typedef GetTextContentParameters
* @prop {boolean} normalizeWhitespace
*/
/**
* @typedef TextContentItem
* @prop {string} str
*/
/**
* @typedef TextContent
* @prop {TextContentItem[]} items
*/
/**
* @typedef PDFPageProxy
* @prop {(o?: GetTextContentParameters) => Promise<TextContent>} getTextContent
*/
/**
* @typedef PDFPageView
* @prop {HTMLElement} div - Container element for the PDF page
* @prop {HTMLElement} el -
* Obsolete alias for `div`?. TODO: Remove this and stop checking for it.
* @prop {PDFPageProxy} pdfPage
* @prop {TextLayer|null} textLayer
* @prop {number} renderingState - See `src/annotator/pdfjs-rendering-states.js`
*/
/**
* @typedef PDFViewer
*
* Defined in `web/pdf_viewer.js` in the PDF.js source.
*
* @prop {number} pagesCount
* @prop {(page: number) => PDFPageView|null} getPageView
*/
/**
* The `PDFViewerApplication` global which is the entry-point for accessing PDF.js.
*
* Defined in `web/app.js` in the PDF.js source.
*
* @typedef PDFViewerApplication
* @prop {PDFDocument} pdfDocument
* @prop {PDFViewer} pdfViewer
* @prop {boolean} downloadComplete
* @prop {PDFDocumentInfo} documentInfo
* @prop {Metadata} metadata
*/
/**
* @typedef TextLayer
* @prop {boolean} renderingDone
* @prop {HTMLElement} textLayerDiv
*/
export {};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment