@readium/shared
Version:
Shared models to be used across other Readium projects and implementations in Typescript
257 lines (214 loc) • 7.42 kB
text/typescript
/* Copyright 2021 Readium Foundation. All rights reserved.
* Use of this source code is governed by a BSD-style license,
* available in the LICENSE file present in the Github repository of the project.
*/
import { Metadata } from './Metadata';
import { Link, Links } from './Link';
import { arrayfromJSONorString } from '../util/JSONParse';
import { PublicationCollection } from './PublicationCollection';
import { MediaType } from '../util/mediatype/MediaType';
import { Locator, LocatorLocations } from "./Locator";
/** Holds the metadata of a Readium publication, as described in
* the Readium Web Publication Manifest.
* See. https://readium.org/webpub-manifest/
*/
export class Manifest {
public readonly context?: Array<string>;
public readonly metadata: Metadata;
public readonly links: Links;
/** Identifies a list of resources in reading order for the publication. */
public readonly readingOrder: Links;
/** Identifies resources that are necessary for rendering the publication. */
public readonly resources?: Links;
/** Identifies the collection that contains a table of contents. */
public readonly toc?: Links;
public readonly subcollections?: Map<string, Array<PublicationCollection>>;
constructor(values: {
context?: Array<string>;
metadata: Metadata;
links: Links;
readingOrder: Links;
resources?: Links;
toc?: Links;
subcollections?: Map<string, Array<PublicationCollection>>;
}) {
this.context = values.context;
this.metadata = values.metadata;
this.links = values.links;
this.readingOrder = values.readingOrder;
this.resources = values.resources;
this.toc = values.toc;
this.subcollections = values.subcollections;
}
/**
* Parses a [Publication] from its RWPM JSON representation.
*
* https://readium.org/webpub-manifest/
* https://readium.org/webpub-manifest/schema/publication.schema.json
*/
public static deserialize(json: any): Manifest | undefined {
if (!json) return;
const metadata = Metadata.deserialize(json.metadata);
if (!metadata) return;
const links = Links.deserialize(json.links);
if (!links) return;
const readingOrder = Links.deserialize(
json.readingOrder ? json.readingOrder : json.spine
);
if (!readingOrder) return;
return new Manifest({
context: arrayfromJSONorString(json['@context']),
metadata,
links,
readingOrder,
resources: Links.deserialize(json.resources),
toc: Links.deserialize(json.toc),
subcollections: PublicationCollection.deserializeCollections({
sub: json.sub,
}),
});
}
/**
* Serializes a [Publication] to its RWPM JSON representation.
*/
public serialize(): any {
const json: any = {};
if (this.context !== undefined) json['@context'] = this.context;
json.metadata = this.metadata.serialize();
json.links = this.links.serialize();
json.readingOrder = this.readingOrder.serialize();
if (this.resources) json.resources = this.resources.serialize();
if (this.toc) json.toc = this.toc.serialize();
PublicationCollection.serializeCollection(json, this.subcollections);
return json;
}
/** Finds the first link with the given relation in the manifest's links. */
public linkWithRel(rel: string): Link | undefined {
const links = new Array<Links>();
links.push(this.readingOrder);
if (this.resources) {
links.push(this.resources);
}
links.push(this.links);
let result: Link | undefined;
for (const collection of links) {
result = collection.findWithRel(rel);
if (result !== undefined) {
return result;
}
}
return result;
}
/** Finds all the links with the given relation in the manifest's links. */
public linksWithRel(rel: string): Array<Link> {
const result = [];
result.push(this.readingOrder.filterByRel(rel));
if (this.resources) {
result.push(this.resources.filterByRel(rel));
}
result.push(this.links.filterByRel(rel));
return result.reduce((acc, val) => acc.concat(val), []);
}
/**
* Creates a new [Locator] object from a [Link] to a resource of this manifest.
* Returns null if the resource is not found in this manifest.
*/
public locatorFromLink(link: Link): Locator | undefined {
const components = link.href.split("#");
const href = components.length == 2 ? components[0] : link.href;
const resourceLink = this.linkWithHref(href);
if(!resourceLink) return undefined;
const type = resourceLink.type;
if(!type) return undefined;
const fragment = components.length == 2 ? components[1] : undefined;
return new Locator({
href,
type,
title: resourceLink.title ?? link.title,
locations: new LocatorLocations({
fragments: fragment ? [fragment] : undefined,
progression: fragment ? undefined : 0.0,
})
});
}
/** Finds the first Link having the given `href` in the publication's links. */
public linkWithHref(href: string): Link | undefined {
const find = (links: Array<Links>): Link | undefined => {
let result = undefined;
for (const collection of links) {
result = collection.findWithHref(href);
if (result !== undefined) {
return result;
}
}
const children = new Array<Links>();
links.forEach(item => {
const arr = [];
for (const link of item.items) {
if (link.alternates) {
arr.push(link.alternates);
}
if (link.children) {
arr.push(link.children);
}
}
children.push(...arr);
});
if (children.length > 0) {
result = find(children);
}
return result;
};
const links: Array<Links> = [];
links.push(this.readingOrder);
if (this.resources) {
links.push(this.resources);
}
links.push(this.links);
const link = find(links);
if (link !== undefined) {
return link;
}
const parts = href.split(/[#]/);
if (parts.length < 2) return;
const shortHref = parts[0];
return this.linkWithHref(shortHref);
}
/** The URL where this publication is served, computed from the `Link` with `self` relation.
* e.g. https://provider.com/pub1293/manifest.json gives https://provider.com/pub1293/
*/
public get baseURL(): string | undefined {
const selfLink = this.links.items.find(
el => el.rels && el.rels.has('self')
);
if (selfLink) {
let href = selfLink.href;
if (href) {
const li = href.lastIndexOf('/');
const lastpart = li === -1 ? undefined : href.substring(li + 1);
href = href.replace(new RegExp('/?$query$'), '');
href = href.replace(new RegExp('//$'), '');
if (lastpart) {
href = href.replace(new RegExp(lastpart + '$'), '');
}
}
return href;
}
return undefined;
}
/**
* Sets the URL where this [Publication]'s RWPM manifest is served.
*/
public setSelfLink(href: string): void {
this.links.items = this.links.items.filter(
x => x.rels === undefined || !x.rels?.has('self')
);
this.links.items.push(
new Link({
href,
type: MediaType.READIUM_WEBPUB_MANIFEST.string,
rels: new Set<string>(['self']),
})
);
}
}