UNPKG

@readium/shared

Version:

Shared models to be used across other Readium projects and implementations in Typescript

341 lines (296 loc) 11 kB
/* 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 { MediaType } from '../util/mediatype/MediaType'; import { Properties } from './Properties'; import { URITemplate } from '../util/URITemplate'; import { arrayfromJSONorString, positiveNumberfromJSON, setToArray, } from '../util/JSONParse'; import { LocatorLocations, Locator } from './Locator'; /** * Link Object for the Readium Web Publication Manifest. * https://readium.org/webpub-manifest/schema/link.schema.json */ export class Link { /** URI or URI template of the linked resource. */ public readonly href: string; /** Indicates that a URI template is used in href. */ public readonly templated?: boolean; /** MIME type of the linked resource. */ public readonly type?: string; /** Title of the linked resource. */ public readonly title?: string; /** Relation between the linked resource and its containing collection. */ public readonly rels?: Set<string>; /** Properties associated to the linked resource. */ public properties?: Properties; /** Height of the linked resource in pixels. */ public readonly height?: number; /** Width of the linked resource in pixels. */ public readonly width?: number; /** Size of the linked resource in bytes. */ public readonly size?: number; /** Length of the linked resource in seconds. */ public readonly duration?: number; /** Bitrate of the linked resource in kbps. */ public readonly bitrate?: number; /** Expected language of the linked resource. */ public readonly languages?: Array<string>; /** Alternate resources for the linked resource. */ public readonly alternates?: Links; /** Resources that are children of the linked resource, in the context of a given collection role. */ public readonly children?: Links; /** * Creates a [Link]. */ constructor(values: { href: string; templated?: boolean; type?: string; title?: string; rels?: Set<string>; properties?: Properties; height?: number; width?: number; size?: number; duration?: number; bitrate?: number; languages?: Array<string>; alternates?: Links; children?: Links; }) { this.href = values.href; this.templated = values.templated; this.type = values.type; this.title = values.title; this.rels = values.rels; this.properties = values.properties; this.height = values.height; this.width = values.width; this.size = values.size; this.duration = values.duration; this.bitrate = values.bitrate; this.languages = values.languages; this.alternates = values.alternates; this.children = values.children; } /** * Parses a [Link] from its RWPM JSON representation. */ public static deserialize(json: any): Link | undefined { if (!json || typeof json.href !== 'string') return; return new Link({ href: json.href, templated: json.templated, type: json.type, title: json.title, rels: json.rel ? json.rel instanceof Array ? new Set<string>(json.rel as Array<string>) : new Set<string>([json.rel as string]) : undefined, properties: Properties.deserialize(json.properties), height: positiveNumberfromJSON(json.height), width: positiveNumberfromJSON(json.width), size: positiveNumberfromJSON(json.size), duration: positiveNumberfromJSON(json.duration), bitrate: positiveNumberfromJSON(json.bitrate), languages: arrayfromJSONorString(json.language), alternates: Links.deserialize(json.alternate), children: Links.deserialize(json.children), }); } /** * Serializes a [Link] to its RWPM JSON representation. */ public serialize(): any { const json: any = { href: this.href }; if (this.templated !== undefined) json.templated = this.templated; if (this.type !== undefined) json.type = this.type; if (this.title !== undefined) json.title = this.title; if (this.rels) json.rel = setToArray(this.rels); if (this.properties) json.properties = this.properties.serialize(); if (this.height !== undefined) json.height = this.height; if (this.width !== undefined) json.width = this.width; if (this.size !== undefined) json.size = this.size; if (this.duration !== undefined) json.duration = this.duration; if (this.bitrate !== undefined) json.bitrate = this.bitrate; if (this.languages) json.language = this.languages; if (this.alternates) json.alternate = this.alternates.serialize(); if (this.children) json.children = this.children.serialize(); return json; } /** MediaType of the linked resource. */ public get mediaType(): MediaType { return this.type !== undefined ? MediaType.parse({ mediaType: this.type }) : MediaType.BINARY; } /** Computes an absolute URL to the link, relative to the given `baseURL`. * If the link's `href` is already absolute, the `baseURL` is ignored. */ public toURL(baseUrl?: string): string | undefined { const href = this.href.replace(/^(\/)/, ''); if (href.length === 0) return; let _baseUrl = baseUrl ? baseUrl : '/'; if (_baseUrl.startsWith('/')) { _baseUrl = 'file://' + _baseUrl; } return new URL(href, _baseUrl).href.replace(/^(file:\/\/)/, ''); } /** List of URI template parameter keys, if the `Link` is templated. */ public get templateParameters(): Set<string> { return this.templated ? new URITemplate(this.href).parameters : new Set(); } /** Expands the `Link`'s HREF by replacing URI template variables by the given parameters. * See RFC 6570 on URI template: https://tools.ietf.org/html/rfc6570 */ public expandTemplate(parameters: { [param: string]: string }): Link { // Probably make copy instead of a new one return new Link({ href: new URITemplate(this.href).expand(parameters), templated: false, }); } /** * Makes a copy of this [Link] after merging in the given additional other [properties]. */ public addProperties(properties: { [key: string]: any }): Link { const link = Link.deserialize(this.serialize()) as Link; link.properties = link.properties ? link.properties?.add(properties) : new Properties(properties); return link; } /** * Creates a [Locator] from a reading order [Link]. */ public get locator(): Locator { let parts = this.href.split('#'); return new Locator({ href: parts.length > 0 && parts[0] !== undefined ? parts[0] : this.href, type: this.type ?? '', title: this.title, locations: new LocatorLocations({ fragments: parts.length > 1 && parts[1] !== undefined ? [parts[1]] : [], }), }); } } /** * Parses multiple JSON links into an array of Link. */ export class Links { public items: Array<Link>; /** * Creates a [Links]. */ constructor(items: Array<Link>) { this.items = items; } /** * Creates a list of [Link] from its RWPM JSON representation. */ public static deserialize(json: any): Links | undefined { if (!(json && json instanceof Array)) return; return new Links( json .map<Link>(item => Link.deserialize(item) as Link) .filter(x => x !== undefined) ); } /** * Serializes an array of [Link] to its RWPM JSON representation. */ public serialize(): any { return this.items.map(x => x.serialize()); } /** Finds the first link with the given relation. */ public findWithRel(rel: string): Link | undefined { const predicate = (el: Link) => el.rels && el.rels.has(rel); return this.items.find(predicate); } /** Finds all the links with the given relation. */ public filterByRel(rel: string): Array<Link> { const predicate = (el: Link) => el.rels && el.rels.has(rel); return this.items.filter(predicate); } /** Finds the first link matching the given HREF. */ public findWithHref(href: string): Link | undefined { const predicate = (el: Link) => el.href === href; return this.items.find(predicate); } /** Finds the index of the first link matching the given HREF. */ public findIndexWithHref(href: string): number { const predicate = (el: Link) => el.href === href; return this.items.findIndex(predicate); } /** Finds the first link matching the given media type. */ public findWithMediaType(mediaType: string): Link | undefined { const predicate = (el: Link) => el.mediaType.matches(mediaType); return this.items.find(predicate); } /** Finds all the links matching the given media type. */ public filterByMediaType(mediaType: string): Array<Link> { const predicate = (el: Link) => el.mediaType.matches(mediaType); return this.items.filter(predicate); } /** Finds all the links matching any of the given media types. */ public filterByMediaTypes(mediaTypes: Array<string>): Array<Link> { const predicate = (el: Link) => { for (const mediaType of mediaTypes) { if (el.mediaType.matches(mediaType)) { return true; } } return false; }; return this.items.filter(predicate); } /** Returns whether all the resources in the collection are audio clips. */ public everyIsAudio(): boolean { const predicate = (el: Link) => el.mediaType.isAudio; return this.items.length > 0 && this.items.every(predicate); } /** Returns whether all the resources in the collection are bitmaps. */ public everyIsBitmap(): boolean { const predicate = (el: Link) => el.mediaType.isBitmap; return this.items.length > 0 && this.items.every(predicate); } /** Returns whether all the resources in the collection are HTML documents. */ public everyIsHTML(): boolean { const predicate = (el: Link) => el.mediaType.isHTML; return this.items.length > 0 && this.items.every(predicate); } /** Returns whether all the resources in the collection are video clips. */ public everyIsVideo(): boolean { const predicate = (el: Link) => el.mediaType.isVideo; return this.items.length > 0 && this.items.every(predicate); } /** Returns whether all the resources in the collection are matching any of the given media types. */ public everyMatchesMediaType(mediaTypes: string | Array<string>): boolean { if (Array.isArray(mediaTypes)) { return ( this.items.length > 0 && this.items.every((el: Link) => { for (const mediaType of mediaTypes) { return el.mediaType.matches(mediaType); } return false; }) ); } else { return ( this.items.length > 0 && this.items.every((el: Link) => el.mediaType.matches(mediaTypes)) ); } } public filterLinksHasType(): Array<Link> { return this.items.filter(x => x.type); } }