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')();
import RenderingStates from '../pdfjs-rendering-states';
// @ts-expect-error - `./range` needs to be converted to JS.
import xpathRange from './range';
import { toRange as textPositionToRange } from './text-position';
// @ts-expect-error - `./types` needs to be converted to JS.
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.
/**
......@@ -37,6 +48,16 @@ function getNodeTextLayer(node) {
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.
*
......@@ -48,7 +69,7 @@ function getNodeTextLayer(node) {
* @return {Promise<PDFPageView>}
*/
async function getPageView(pageIndex) {
const pdfViewer = PDFViewerApplication.pdfViewer;
const pdfViewer = getPdfViewer();
let pageView = pdfViewer.getPageView(pageIndex);
if (!pageView || !pageView.pdfPage) {
......@@ -68,7 +89,7 @@ async function getPageView(pageIndex) {
});
}
return pageView;
return /** @type {PDFPageView} */ (pageView);
}
/**
......@@ -150,7 +171,7 @@ function getPageOffset(pageIndex) {
* `offset` within the complete text of the document.
*
* @param {number} offset
* @return {PageOffset}
* @return {Promise<PageOffset>}
*/
function findPage(offset) {
let index = 0;
......@@ -182,7 +203,7 @@ function findPage(offset) {
// 149 | 1
// 150 | 2
const count = textContent => {
const lastPageIndex = PDFViewerApplication.pdfViewer.pagesCount - 1;
const lastPageIndex = getPdfViewer().pagesCount - 1;
if (total + textContent.length > offset || index === lastPageIndex) {
// Offset is in current page.
offset = total;
......@@ -214,11 +235,11 @@ function findPage(offset) {
async function anchorByPosition(pageIndex, anchor) {
const page = await getPageView(pageIndex);
let renderingDone = false;
if (page.textLayer) {
renderingDone = page.textLayer.renderingDone;
}
if (page.renderingState === RenderingStates.FINISHED && renderingDone) {
if (
page.renderingState === RenderingStates.FINISHED &&
page.textLayer &&
page.textLayer.renderingDone
) {
// The page has been rendered. Locate the position in the text layer.
const root = page.textLayer.textLayerDiv;
return textPositionToRange(root, anchor.start, anchor.end);
......@@ -226,7 +247,7 @@ async function anchorByPosition(pageIndex, anchor) {
// The page has not been rendered yet. Create a placeholder element and
// 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];
if (!placeholder) {
placeholder = document.createElement('span');
......@@ -304,11 +325,11 @@ function findInPages(pageIndexes, quoteSelector, positionHint) {
* When a position anchor is available, quote search can be optimized by
* searching pages nearest the expected position first.
*
* @param [TextPositionAnchor] position
* @return {number[]}
* @param {TextPositionAnchor} position
* @return {Promise<number[]>}
*/
function prioritizePages(position) {
const pageCount = PDFViewerApplication.pdfViewer.pagesCount;
const pageCount = getPdfViewer().pagesCount;
const pageIndices = Array(pageCount)
.fill(0)
.map((_, i) => i);
......@@ -317,17 +338,21 @@ function prioritizePages(position) {
return Promise.resolve(pageIndices);
}
// Sort page indexes by offset from `pageIndex`.
/**
* Sort page indexes by offset from `pageIndex`.
*
* @param {number} pageIndex
*/
function sortPages(pageIndex) {
const left = pageIndices.slice(0, pageIndex);
const right = pageIndices.slice(pageIndex);
const result = [];
while (left.length > 0 || right.length > 0) {
if (right.length) {
result.push(right.shift());
result.push(/** @type {number} */ (right.shift()));
}
if (left.length) {
result.push(left.pop());
result.push(/** @type {number} */ (left.pop()));
}
}
return result;
......@@ -347,6 +372,7 @@ export function anchor(root, selectors) {
const position = selectors.find(s => s.type === 'TextPositionSelector');
const quote = selectors.find(s => s.type === 'TextQuoteSelector');
/** @type {Promise<Range>} */
let result = Promise.reject('unable to anchor');
const checkQuote = range => {
......
import { normalizeURI } from '../util/url';
/**
* @typedef {import('../../types/pdfjs').PDFViewerApplication} PDFViewerApplication
*/
/**
* @typedef Link
* @prop {string} href
......@@ -30,9 +34,10 @@ export default class PDFMetadata {
* Construct a `PDFMetadata` that returns URIs/metadata associated with a
* given PDF viewer.
*
* @param {Object} app - The `PDFViewerApplication` global from PDF.js
* @param {PDFViewerApplication} app - The `PDFViewerApplication` global from PDF.js
*/
constructor(app) {
/** @type {Promise<PDFViewerApplication>} */
this._loaded = new Promise(resolve => {
const finish = () => {
window.removeEventListener('documentload', finish);
......@@ -89,7 +94,7 @@ export default class PDFMetadata {
app.metadata.has('dc:title') &&
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) {
title = app.documentInfo.Title;
}
......
......@@ -34,7 +34,6 @@
// Files in `src/annotator` that still have errors to be resolved.
"annotator/pdf-sidebar.js",
"annotator/anchoring/pdf.js",
"annotator/plugin/document.js",
// 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