Commit 133ef3e7 authored by Robert Knight's avatar Robert Knight

Make page index retrieval work in EPUB-based books

In real EPUB-based VS books, the `index` field of `PageInfo` objects returned by
the `getCurrentPage` and `getPages` methods is undefined. Our mock VS book was
incorrect in this respect.

Correct the mock book to behave like real VS books here, and add a fallback
method to obtain a page index for EPUB-based books by looking up the page in the
list of pages returned by `MosaicBookElement.getPages`.
parent 7c53e17e
...@@ -61,21 +61,21 @@ export class MosaicBookElement extends HTMLElement { ...@@ -61,21 +61,21 @@ export class MosaicBookElement extends HTMLElement {
absoluteURL: '/document/little-women-1', absoluteURL: '/document/little-women-1',
chapterTitle: 'Chapter One', chapterTitle: 'Chapter One',
cfi: '/2', cfi: '/2',
index: 0, index: undefined,
page: '10', page: '10',
}, },
{ {
absoluteURL: '/document/little-women-2', absoluteURL: '/document/little-women-2',
chapterTitle: 'Chapter Two', chapterTitle: 'Chapter Two',
cfi: '/4', cfi: '/4',
index: 1, index: undefined,
page: '20', page: '20',
}, },
{ {
absoluteURL: '/document/little-women-3', absoluteURL: '/document/little-women-3',
chapterTitle: 'Chapter Three', chapterTitle: 'Chapter Three',
cfi: '/6', cfi: '/6',
index: 2, index: undefined,
page: '30', page: '30',
}, },
]; ];
...@@ -170,6 +170,10 @@ export class MosaicBookElement extends HTMLElement { ...@@ -170,6 +170,10 @@ export class MosaicBookElement extends HTMLElement {
return this.pageData[this.pageIndex]; return this.pageData[this.pageIndex];
} }
async getPages() {
return { ok: true, data: [...this.pageData], status: 200 };
}
async getTOC() { async getTOC() {
// In our VS test pages, the TOC entries are 1:1 with the page entries. // In our VS test pages, the TOC entries are 1:1 with the page entries.
// However in a real VS book there may be multiple TOC entries for different // However in a real VS book there may be multiple TOC entries for different
......
...@@ -236,7 +236,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -236,7 +236,7 @@ describe('annotator/integrations/vitalsource', () => {
absoluteURL: '/pages/chapter_02.xhtml', absoluteURL: '/pages/chapter_02.xhtml',
cfi: '/2', cfi: '/2',
chapterTitle: 'Chapter two', chapterTitle: 'Chapter two',
index: 1, index: undefined,
page: '20', page: '20',
}; };
} else if (this._format === 'pbk') { } else if (this._format === 'pbk') {
...@@ -252,6 +252,12 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -252,6 +252,12 @@ describe('annotator/integrations/vitalsource', () => {
} }
} }
async getPages() {
const pageData = await this.getCurrentPage();
const data = [pageData];
return { ok: true, data };
}
async getTOC() { async getTOC() {
if (this._format === 'epub') { if (this._format === 'epub') {
const toc = [ const toc = [
...@@ -394,7 +400,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -394,7 +400,7 @@ describe('annotator/integrations/vitalsource', () => {
const pageSelector = selectors.find(s => s.type === 'PageSelector'); const pageSelector = selectors.find(s => s.type === 'PageSelector');
assert.deepEqual(pageSelector, { assert.deepEqual(pageSelector, {
type: 'PageSelector', type: 'PageSelector',
index: 1, index: 0,
label: '20', label: '20',
}); });
}); });
......
...@@ -9,7 +9,11 @@ import type { ...@@ -9,7 +9,11 @@ import type {
SegmentInfo, SegmentInfo,
SidebarLayout, SidebarLayout,
} from '../../types/annotator'; } from '../../types/annotator';
import type { EPUBContentSelector, Selector } from '../../types/api'; import type {
EPUBContentSelector,
PageSelector,
Selector,
} from '../../types/api';
import type { import type {
ContentFrameGlobals, ContentFrameGlobals,
MosaicBookElement, MosaicBookElement,
...@@ -173,6 +177,23 @@ function makeContentFrameScrollable(frame: HTMLIFrameElement) { ...@@ -173,6 +177,23 @@ function makeContentFrameScrollable(frame: HTMLIFrameElement) {
attrObserver.observe(frame, { attributes: true }); attrObserver.observe(frame, { attributes: true });
} }
/**
* Lookup options for fetching page metadata from VitalSource.
*
* Enabling this options involves some extra work, so should be skipped if
* the data is not needed.
*/
type PageInfoOptions = {
/** Whether to fetch the title. */
includeTitle?: boolean;
/**
* Whether to use a fallback strategy to get the page index if not available
* in the {@link MosaicBookElement.getCurrentPage} response.
*/
includePageIndex?: boolean;
};
/** /**
* Return a copy of URL with the origin removed. * Return a copy of URL with the origin removed.
* *
...@@ -338,19 +359,18 @@ export class VitalSourceContentIntegration ...@@ -338,19 +359,18 @@ export class VitalSourceContentIntegration
page: pageLabel, page: pageLabel,
title, title,
url, url,
} = await this._getPageInfo(true /* includeTitle */); } = await this._getPageInfo({ includeTitle: true, includePageIndex: true });
// We generate an "EPUBContentSelector" with a CFI for all VS books, // We generate an "EPUBContentSelector" with a CFI for all VS books,
// although for PDF-based books the CFI is a string generated from the // although for PDF-based books the CFI is a string generated from the
// page number. // page number.
const extraSelectors: Selector[] = [ const cfiSelector: EPUBContentSelector = {
{
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
cfi, cfi,
url, url,
title, title,
}, };
]; const extraSelectors: Selector[] = [cfiSelector];
// Add page number if available. PDF-based books always have them. // Add page number if available. PDF-based books always have them.
// Publishers are encouraged to provide page numbers for EPUB-based books, // Publishers are encouraged to provide page numbers for EPUB-based books,
...@@ -359,11 +379,12 @@ export class VitalSourceContentIntegration ...@@ -359,11 +379,12 @@ export class VitalSourceContentIntegration
// //
// [1] https://www.vitalsource.com/en-uk/products/vitalsource-epub3-implementation-guide-vitalsource-vvstdocsepub3implementguide?term=VST-DOCS-EPUB3IMPLEMENTGUIDE // [1] https://www.vitalsource.com/en-uk/products/vitalsource-epub3-implementation-guide-vitalsource-vvstdocsepub3implementguide?term=VST-DOCS-EPUB3IMPLEMENTGUIDE
if (typeof pageIndex === 'number') { if (typeof pageIndex === 'number') {
extraSelectors.push({ const pageSelector: PageSelector = {
type: 'PageSelector', type: 'PageSelector',
index: pageIndex, index: pageIndex,
label: pageLabel, label: pageLabel,
}); };
extraSelectors.push(pageSelector);
} }
selectors.push(...extraSelectors); selectors.push(...extraSelectors);
...@@ -451,14 +472,15 @@ export class VitalSourceContentIntegration ...@@ -451,14 +472,15 @@ export class VitalSourceContentIntegration
/** /**
* Retrieve information about the currently displayed content document or * Retrieve information about the currently displayed content document or
* page. * page.
*
* @param includeTitle - Whether to fetch the title. This involves some extra
* work so should be skipped when not required.
*/ */
async _getPageInfo(includeTitle: boolean) { async _getPageInfo({
const [pageInfo, toc] = await Promise.all([ includeTitle = false,
includePageIndex = false,
}: PageInfoOptions = {}) {
const [pageInfo, toc, pages] = await Promise.all([
this._bookElement.getCurrentPage(), this._bookElement.getCurrentPage(),
includeTitle ? this._bookElement.getTOC() : undefined, includeTitle ? this._bookElement.getTOC() : undefined,
includePageIndex ? this._bookElement.getPages() : undefined,
]); ]);
// If changes in VitalSource ever mean that critical chapter/page metadata // If changes in VitalSource ever mean that critical chapter/page metadata
...@@ -474,7 +496,6 @@ export class VitalSourceContentIntegration ...@@ -474,7 +496,6 @@ export class VitalSourceContentIntegration
} }
let title; let title;
if (toc) { if (toc) {
title = pageInfo.chapterTitle; title = pageInfo.chapterTitle;
...@@ -490,9 +511,20 @@ export class VitalSourceContentIntegration ...@@ -490,9 +511,20 @@ export class VitalSourceContentIntegration
} }
} }
// For PDF-based books, the `pageInfo.index` property should be populated.
// For EPUB-based books, it may not be. In that case we try to find a
// page number in the complete page list instead.
let pageIndex = pageInfo.index;
if (pageIndex === undefined && pages) {
const index = pages.data?.findIndex(page => page.cfi === pageInfo.cfi);
if (index !== -1) {
pageIndex = index;
}
}
return { return {
cfi: pageInfo.cfi, cfi: pageInfo.cfi,
index: pageInfo.index, index: pageIndex,
page: pageInfo.page, page: pageInfo.page,
title, title,
...@@ -503,7 +535,7 @@ export class VitalSourceContentIntegration ...@@ -503,7 +535,7 @@ export class VitalSourceContentIntegration
} }
async segmentInfo(): Promise<SegmentInfo> { async segmentInfo(): Promise<SegmentInfo> {
const { cfi, url } = await this._getPageInfo(false /* includeTitle */); const { cfi, url } = await this._getPageInfo();
return { cfi, url }; return { cfi, url };
} }
......
...@@ -164,6 +164,9 @@ export type MosaicBookElement = HTMLElement & { ...@@ -164,6 +164,9 @@ export type MosaicBookElement = HTMLElement & {
*/ */
getCurrentPage(): Promise<PageInfo>; getCurrentPage(): Promise<PageInfo>;
/** Get the list of pages in the book. */
getPages(): Promise<DataResponse<PageInfo[]>>;
/** Retrieve the book's table of contents. */ /** Retrieve the book's table of contents. */
getTOC(): Promise<DataResponse<TableOfContentsEntry[]>>; getTOC(): Promise<DataResponse<TableOfContentsEntry[]>>;
......
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