UNPKG

@readium/shared

Version:

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

399 lines (374 loc) 13.6 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 { arrayfromJSONorString, datefromJSON, positiveNumberfromJSON, } from '../util/JSONParse'; import { AltIdentifier } from './AltIdentifier'; import { BelongsTo } from './BelongsTo'; import { Contributors } from './Contributor'; import { Layout } from './Layout'; import { LocalizedString } from './LocalizedString'; import { Profile } from './Profiles'; import { ReadingProgression } from './ReadingProgression'; import { Subjects } from './Subject'; import { TDM } from './TDM'; /** * https://readium.org/webpub-manifest/schema/metadata.schema.json * * readingProgression : This contains the reading progression as declared in the * publication, so it might be [auto]. To lay out the content, use [effectiveReadingProgression] * to get the calculated reading progression from the declared direction and the language. * otherMetadata Additional metadata for extensions, as a JSON dictionary. */ export class Metadata { public title: LocalizedString; public typeUri?: string; public conformsTo?: Array<Profile>; public identifier?: string; public altIdentifier?: AltIdentifier; public subtitle?: LocalizedString; public sortAs?: LocalizedString; public artists?: Contributors; public authors?: Contributors; public colorists?: Contributors; public contributors?: Contributors; public editors?: Contributors; public illustrators?: Contributors; public inkers?: Contributors; public letterers?: Contributors; public narrators?: Contributors; public pencilers?: Contributors; public translators?: Contributors; public languages?: Array<string>; public description?: string; public publishers?: Contributors; public imprints?: Contributors; public published?: Date; public modified?: Date; public subjects?: Subjects; public belongsTo?: BelongsTo; public belongsToCollections?: Contributors; public belongsToSeries?: Contributors; public layout?: Layout; public readingProgression?: ReadingProgression; public duration?: number; public numberOfPages?: number; public tdm?: TDM; public otherMetadata?: { [key: string]: any }; /**All metadata not in otherMetadata */ private static readonly mappedProperties = [ 'title', '@type', 'conformsTo', 'identifier', 'altIdentifier', 'subtitle', 'sortAs', 'artist', 'author', 'colorist', 'contributor', 'editor', 'illustrator', 'inker', 'letterer', 'narrator', 'penciler', 'translator', 'language', 'description', 'publisher', 'imprint', 'published', 'modified', 'subject', 'belongsTo', 'layout', 'readingProgression', 'duration', 'numberOfPages', 'tdm' ]; /** Creates [Metadata] object */ constructor(values: { title: LocalizedString; typeUri?: string; conformsTo?: Array<Profile>; identifier?: string; altIdentifier?: AltIdentifier; subtitle?: LocalizedString; sortAs?: LocalizedString; artists?: Contributors; authors?: Contributors; colorists?: Contributors; contributors?: Contributors; editors?: Contributors; illustrators?: Contributors; inkers?: Contributors; letterers?: Contributors; narrators?: Contributors; pencilers?: Contributors; translators?: Contributors; languages?: Array<string>; description?: string; publishers?: Contributors; imprints?: Contributors; published?: Date; modified?: Date; subjects?: Subjects; belongsTo?: BelongsTo; belongsToCollections?: Contributors; belongsToSeries?: Contributors; layout?: Layout; readingProgression?: ReadingProgression; duration?: number; numberOfPages?: number; tdm?: TDM; otherMetadata?: { [key: string]: any }; }) { //title always required this.title = values.title as LocalizedString; this.typeUri = values.typeUri; this.conformsTo = values.conformsTo; this.identifier = values.identifier; this.altIdentifier = values.altIdentifier; this.subtitle = values.subtitle; this.sortAs = values.sortAs; this.artists = values.artists; this.authors = values.authors; this.colorists = values.colorists; this.contributors = values.contributors; this.editors = values.editors; this.illustrators = values.illustrators; this.inkers = values.inkers; this.letterers = values.letterers; this.narrators = values.narrators; this.pencilers = values.pencilers; this.translators = values.translators; this.languages = values.languages; this.description = values.description; this.publishers = values.publishers; this.imprints = values.imprints; this.published = values.published; this.modified = values.modified; this.subjects = values.subjects; this.belongsTo = values.belongsTo; this.belongsToCollections = values.belongsToCollections; this.belongsToSeries = values.belongsToSeries; if ( this.belongsToCollections && this.belongsToCollections.items.length > 0 ) { if (!this.belongsTo) { this.belongsTo = new BelongsTo(); } this.belongsTo.items.set('collection', this.belongsToCollections); } if (this.belongsToSeries && this.belongsToSeries.items.length > 0) { if (!this.belongsTo) { this.belongsTo = new BelongsTo(); } this.belongsTo.items.set('series', this.belongsToSeries); } this.layout = values.layout; this.readingProgression = values.readingProgression; this.duration = values.duration; this.numberOfPages = values.numberOfPages; this.tdm = values.tdm; this.otherMetadata = values.otherMetadata; } /** * Parses a [Metadata] from its RWPM JSON representation. * * If the metadata can't be parsed, a warning will be logged with [warnings]. */ public static deserialize(json: any): Metadata | undefined { if (!(json && json.title)) return; const title = LocalizedString.deserialize(json.title) as LocalizedString; const typeUri = json['@type']; const conformsTo = arrayfromJSONorString(json.conformsTo); const identifier = json.identifier; const altIdentifier = AltIdentifier.deserialize(json.altIdentifier); const subtitle = LocalizedString.deserialize(json.subtitle); const sortAs = LocalizedString.deserialize(json.sortAs); const artists = Contributors.deserialize(json.artist); const authors = Contributors.deserialize(json.author); const colorists = Contributors.deserialize(json.colorist); const contributors = Contributors.deserialize(json.contributor); const editors = Contributors.deserialize(json.editor); const illustrators = Contributors.deserialize(json.illustrator); const inkers = Contributors.deserialize(json.inker); const letterers = Contributors.deserialize(json.letterer); const narrators = Contributors.deserialize(json.narrator); const pencilers = Contributors.deserialize(json.penciler); const translators = Contributors.deserialize(json.translator); const languages = arrayfromJSONorString(json.language); const description = json.description; const publishers = Contributors.deserialize(json.publisher); const imprints = Contributors.deserialize(json.imprint); const published = datefromJSON(json.published); const modified = datefromJSON(json.modified); const subjects = Subjects.deserialize(json.subject); const belongsTo = BelongsTo.deserialize(json.belongsTo); const layout = json.layout; const readingProgression = json.readingProgression; const duration = positiveNumberfromJSON(json.duration); const numberOfPages = positiveNumberfromJSON(json.numberOfPages); const tdm = TDM.deserialize(json.tdm); let otherMetadata = Object.assign({}, json); Metadata.mappedProperties.forEach(x => delete otherMetadata[x]); if (Object.keys(otherMetadata).length === 0) { otherMetadata = undefined; } return new Metadata({ title, typeUri, conformsTo, identifier, altIdentifier, subtitle, sortAs, artists, authors, colorists, contributors, editors, illustrators, inkers, letterers, narrators, pencilers, translators, languages, description, publishers, imprints, published, modified, subjects, belongsTo, layout, readingProgression, duration, numberOfPages, tdm, otherMetadata }); } /** * Serializes a [Metadata] to its RWPM JSON representation. */ public serialize(): any { const json: any = { title: this.title.serialize() }; if (this.typeUri !== undefined) json['@type'] = this.typeUri; if (this.conformsTo) json.conformsTo = this.conformsTo; if (this.identifier !== undefined) json.identifier = this.identifier; if (this.altIdentifier) json.altIdentifier = this.altIdentifier.serialize(); if (this.subtitle) json.subtitle = this.subtitle.serialize(); if (this.sortAs) json.sortAs = this.sortAs.serialize(); if (this.editors) json.editor = this.editors.serialize(); if (this.artists) json.artist = this.artists.serialize(); if (this.authors) json.author = this.authors.serialize(); if (this.colorists) json.colorist = this.colorists.serialize(); if (this.contributors) json.contributor = this.contributors.serialize(); if (this.illustrators) json.illustrator = this.illustrators.serialize(); if (this.letterers) json.letterer = this.letterers.serialize(); if (this.narrators) json.narrator = this.narrators.serialize(); if (this.pencilers) json.penciler = this.pencilers.serialize(); if (this.translators) json.translator = this.translators.serialize(); if (this.inkers) json.inker = this.inkers.serialize(); if (this.languages) json.language = this.languages; if (this.description !== undefined) json.description = this.description; if (this.publishers) json.publisher = this.publishers.serialize(); if (this.imprints) json.imprint = this.imprints.serialize(); if (this.published !== undefined) json.published = this.published.toISOString(); if (this.modified !== undefined) json.modified = this.modified.toISOString(); if (this.subjects) json.subject = this.subjects.serialize(); if (this.belongsTo) json.belongsTo = this.belongsTo.serialize(); if (this.layout) json.layout = this.layout; if (this.readingProgression) json.readingProgression = this.readingProgression; if (this.duration !== undefined) json.duration = this.duration; if (this.numberOfPages !== undefined) json.numberOfPages = this.numberOfPages; if (this.tdm) json.tdm = this.tdm.serialize(); if (this.otherMetadata) { const metadata = this.otherMetadata; Object.keys(metadata).forEach(x => (json[x] = metadata[x])); } return json; } /** * Computes a [Layout] using the [conformsTo] profile and layout property. * * Special cases: * - EPUB profile defaults to reflowable if layout is not present * - Divina profile defaults to fixed if layout is not present * - Layout is ignored for audiobook and PDF profiles * - Layout is ignored if set to reflowable on a Divina profile * * Note: Stops at the first matching profile. */ public get effectiveLayout(): Layout | null { if (!this.conformsTo) { return null; // Default Web Publication behavior } // Check each profile in order, stopping at the first match for (const profile of this.conformsTo) { switch (profile) { case Profile.EPUB: // EPUB defaults to reflowable if layout is not set return this.layout || Layout.reflowable; case Profile.DIVINA: // Divina defaults to fixed if layout is not set // Layout is ignored if set to reflowable if (this.layout === Layout.reflowable) { return Layout.fixed; } return this.layout || Layout.fixed; case Profile.AUDIOBOOK: case Profile.PDF: // Layout is ignored for audiobook and PDF profiles return null; } } // If we get here, we have a conformsTo but no matching profile return null; } /** * Computes a [ReadingProgression] when the value of [readingProgression] is undefined, using the publication language. * * See this issue for more details: https://github.com/readium/architecture/issues/113 */ public get effectiveReadingProgression(): ReadingProgression { if (this.readingProgression) { return this.readingProgression; } // https://github.com/readium/readium-css/blob/develop/docs/CSS16-internationalization.md#missing-page-progression-direction if (this.languages?.length !== 1) { return ReadingProgression.ltr; } const primaryLang = this.languages[0].toLowerCase(); if (primaryLang === 'zh-hant' || primaryLang === 'zh-tw') { return ReadingProgression.rtl; } // The region is ignored for ar, fa and he. const lang = primaryLang.split('-')[0]; switch (lang) { case 'ar': return ReadingProgression.rtl; case 'fa': return ReadingProgression.rtl; case 'he': return ReadingProgression.rtl; default: return ReadingProgression.ltr; } } }