Commit 91a563e1 authored by Robert Knight's avatar Robert Knight

Generate absolute URLs in EPUBContentSelector selectors

Change `EPUBContentSelector` selectors captured in VS books to include
the resolved URL of the content document (eg.
"https://jigsaw.vitalsource.com/books/{book_id}/...") rather than just
the path ("/books/{book_id}/...").

This may make it easier to work with these URLs outside of the VitalSource
viewer, and should also make these URLs more consistent with
`EPUBContentSelector` selectors captured in other ebook readers in future.
parent 265e59fc
...@@ -54,6 +54,10 @@ class FakeVitalSourceViewer { ...@@ -54,6 +54,10 @@ class FakeVitalSourceViewer {
} }
} }
function resolveURL(relativeURL) {
return new URL(relativeURL, document.baseURI).toString();
}
describe('annotator/integrations/vitalsource', () => { describe('annotator/integrations/vitalsource', () => {
let featureFlags; let featureFlags;
let fakeViewer; let fakeViewer;
...@@ -376,7 +380,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -376,7 +380,7 @@ describe('annotator/integrations/vitalsource', () => {
assert.ok(cfiSelector); assert.ok(cfiSelector);
assert.deepEqual(cfiSelector, { assert.deepEqual(cfiSelector, {
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
url: '/pages/chapter_02.xhtml', url: resolveURL('/pages/chapter_02.xhtml'),
cfi: '/2', cfi: '/2',
title: 'Chapter two (from TOC)', title: 'Chapter two (from TOC)',
}); });
...@@ -401,7 +405,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -401,7 +405,7 @@ describe('annotator/integrations/vitalsource', () => {
assert.ok(cfiSelector); assert.ok(cfiSelector);
assert.deepEqual(cfiSelector, { assert.deepEqual(cfiSelector, {
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
url: '/pages/2', url: resolveURL('/pages/2'),
cfi: '/1', cfi: '/1',
title: 'First chapter', title: 'First chapter',
}); });
...@@ -479,7 +483,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -479,7 +483,7 @@ describe('annotator/integrations/vitalsource', () => {
const ann = createAnnotationWithSelector({ const ann = createAnnotationWithSelector({
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
cfi: '/2/4', cfi: '/2/4',
url: '/chapters/02.xhtml', url: resolveURL('/chapters/02.xhtml'),
}); });
integration.navigateToSegment(ann); integration.navigateToSegment(ann);
...@@ -491,7 +495,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -491,7 +495,7 @@ describe('annotator/integrations/vitalsource', () => {
const integration = createIntegration(); const integration = createIntegration();
const ann = createAnnotationWithSelector({ const ann = createAnnotationWithSelector({
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
url: '/chapters/02.xhtml', url: resolveURL('/chapters/02.xhtml'),
}); });
integration.navigateToSegment(ann); integration.navigateToSegment(ann);
...@@ -506,7 +510,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -506,7 +510,7 @@ describe('annotator/integrations/vitalsource', () => {
const segment = await integration.segmentInfo(); const segment = await integration.segmentInfo();
assert.deepEqual(segment, { assert.deepEqual(segment, {
cfi: '/2', cfi: '/2',
url: '/pages/chapter_02.xhtml', url: resolveURL('/pages/chapter_02.xhtml'),
}); });
}); });
}); });
......
...@@ -175,6 +175,18 @@ function makeContentFrameScrollable(frame: HTMLIFrameElement) { ...@@ -175,6 +175,18 @@ function makeContentFrameScrollable(frame: HTMLIFrameElement) {
attrObserver.observe(frame, { attributes: true }); attrObserver.observe(frame, { attributes: true });
} }
/**
* Return a copy of URL with the origin removed.
*
* eg. "https://jigsaw.vitalsource.com/books/123/chapter.html?foo" =>
* "/books/123/chapter.html"
*/
function stripOrigin(url: string) {
// Resolve input URL in case it doesn't already have an origin.
const parsed = new URL(url, document.baseURI);
return parsed.pathname + parsed.search;
}
/** /**
* Integration for the content frame in VitalSource's Bookshelf ebook reader. * Integration for the content frame in VitalSource's Bookshelf ebook reader.
* *
...@@ -312,23 +324,13 @@ export class VitalSourceContentIntegration ...@@ -312,23 +324,13 @@ export class VitalSourceContentIntegration
return selectors; return selectors;
} }
const [pageInfo, toc] = await Promise.all([ const {
this._bookElement.getCurrentPage(), cfi,
this._bookElement.getTOC(), index: pageIndex,
]); page: pageLabel,
title,
let title = pageInfo.chapterTitle; url,
} = await this._getPageInfo(true /* includeTitle */);
// Find the first table of contents entry that corresponds to the current page,
// and use its title instead of `pageInfo.chapterTitle`. This works around
// https://github.com/hypothesis/client/issues/4986.
const pageCFI = documentCFI(pageInfo.cfi);
const tocEntry = toc.data?.find(
entry => documentCFI(entry.cfi) === pageCFI
);
if (tocEntry) {
title = tocEntry.title;
}
// 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
...@@ -336,8 +338,8 @@ export class VitalSourceContentIntegration ...@@ -336,8 +338,8 @@ export class VitalSourceContentIntegration
const extraSelectors: Selector[] = [ const extraSelectors: Selector[] = [
{ {
type: 'EPUBContentSelector', type: 'EPUBContentSelector',
cfi: pageInfo.cfi, cfi,
url: pageInfo.absoluteURL, url,
title, title,
}, },
]; ];
...@@ -351,8 +353,8 @@ export class VitalSourceContentIntegration ...@@ -351,8 +353,8 @@ export class VitalSourceContentIntegration
if (bookInfo.format === 'pbk') { if (bookInfo.format === 'pbk') {
extraSelectors.push({ extraSelectors.push({
type: 'PageSelector', type: 'PageSelector',
index: pageInfo.index, index: pageIndex,
label: pageInfo.page, label: pageLabel,
}); });
} }
...@@ -425,7 +427,7 @@ export class VitalSourceContentIntegration ...@@ -425,7 +427,7 @@ export class VitalSourceContentIntegration
if (selector?.cfi) { if (selector?.cfi) {
this._bookElement.goToCfi(selector.cfi); this._bookElement.goToCfi(selector.cfi);
} else if (selector?.url) { } else if (selector?.url) {
this._bookElement.goToURL(selector.url); this._bookElement.goToURL(stripOrigin(selector.url));
} else { } else {
throw new Error('No segment information available'); throw new Error('No segment information available');
} }
...@@ -437,14 +439,53 @@ export class VitalSourceContentIntegration ...@@ -437,14 +439,53 @@ export class VitalSourceContentIntegration
return true; return true;
} }
async segmentInfo(): Promise<SegmentInfo> { /**
const pageInfo = await this._bookElement.getCurrentPage(); * Retrieve information about the currently displayed content document or
* page.
*
* @param includeTitle - Whether to fetch the title. This involves some extra
* work so should be skipped when not required.
*/
async _getPageInfo(includeTitle: boolean) {
const [pageInfo, toc] = await Promise.all([
this._bookElement.getCurrentPage(),
includeTitle ? this._bookElement.getTOC() : undefined,
]);
let title;
if (toc) {
title = pageInfo.chapterTitle;
// Find the first table of contents entry that corresponds to the current page,
// and use its title instead of `pageInfo.chapterTitle`. This works around
// https://github.com/hypothesis/client/issues/4986.
const pageCFI = documentCFI(pageInfo.cfi);
const tocEntry = toc.data?.find(
entry => documentCFI(entry.cfi) === pageCFI
);
if (tocEntry) {
title = tocEntry.title;
}
}
return { return {
cfi: pageInfo.cfi, cfi: pageInfo.cfi,
url: pageInfo.absoluteURL, index: pageInfo.index,
page: pageInfo.page,
title,
// The `pageInfo.absoluteURL` URL is an absolute path that does not
// include the origin of VitalSource's CDN.
url: new URL(pageInfo.absoluteURL, document.baseURI).toString(),
}; };
} }
async segmentInfo(): Promise<SegmentInfo> {
const { cfi, url } = await this._getPageInfo(false /* includeTitle */);
return { cfi, url };
}
async uri() { async uri() {
if (this._bookIsSingleDocument()) { if (this._bookIsSingleDocument()) {
const bookInfo = this._bookElement.getBookInfo(); const bookInfo = this._bookElement.getBookInfo();
......
...@@ -76,8 +76,8 @@ export type EPUBContentSelector = { ...@@ -76,8 +76,8 @@ export type EPUBContentSelector = {
type: 'EPUBContentSelector'; type: 'EPUBContentSelector';
/** /**
* URL of the content document. This can either be an absolute HTTP URL, or * URL of the content document. This should be an absolute HTTPS URL if
* a URL that is relative to the root of the EPUB. * available, but may be relative to the root of the EPUB.
*/ */
url: string; url: string;
......
...@@ -174,6 +174,8 @@ export type MosaicBookElement = HTMLElement & { ...@@ -174,6 +174,8 @@ export type MosaicBookElement = HTMLElement & {
/** /**
* Navigate the book to the page or content document whose URL matches `url`. * Navigate the book to the page or content document whose URL matches `url`.
*
* `url` must be a relative URL with an absolute path (eg. "/books/123/chapter01.xhtml").
*/ */
goToURL(url: string): void; goToURL(url: string): void;
}; };
......
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