@readium/shared
Version:
Shared models to be used across other Readium projects and implementations in Typescript
407 lines (382 loc) • 14 kB
text/typescript
/* Copyright 2025 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 { Accessibility } from './accessibility/Accessibility';
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 accessibility?: Accessibility;
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',
'accessibility',
'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;
accessibility?: Accessibility;
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.accessibility = values.accessibility;
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 accessibility = Accessibility.deserialize(json.accessibility);
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,
accessibility,
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.accessibility) json.accessibility = this.accessibility.serialize();
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;
}
}
}