From acacaf9003c6e39899518cba735d2aa7b0a6de1c Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 16:24:46 +0200 Subject: [PATCH 01/13] add tdm in Metadata https://readium.org/webpub-manifest/contexts/default/#text-and-data-mining --- shared/src/publication/Metadata.ts | 10 +++++- shared/src/publication/TDM.ts | 56 ++++++++++++++++++++++++++++++ shared/src/publication/index.ts | 1 + shared/test/Metadata.test.ts | 20 ++++++++++- shared/test/TDM.test.ts | 41 ++++++++++++++++++++++ 5 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 shared/src/publication/TDM.ts create mode 100644 shared/test/TDM.test.ts diff --git a/shared/src/publication/Metadata.ts b/shared/src/publication/Metadata.ts index 5e5c76d1..b7b54fa2 100644 --- a/shared/src/publication/Metadata.ts +++ b/shared/src/publication/Metadata.ts @@ -13,6 +13,7 @@ import { Contributors } from './Contributor'; import { LocalizedString } from './LocalizedString'; import { ReadingProgression } from './ReadingProgression'; import { Subjects } from './Subject'; +import { TDM } from './TDM'; /** * https://readium.org/webpub-manifest/schema/metadata.schema.json @@ -52,6 +53,7 @@ export class Metadata { public readingProgression?: ReadingProgression; public duration?: number; public numberOfPages?: number; + public tdm?: TDM; public otherMetadata?: { [key: string]: any }; /**All metadata not in otherMetadata */ @@ -83,6 +85,7 @@ export class Metadata { 'readingProgression', 'duration', 'numberOfPages', + 'tdm' ]; /** Creates [Metadata] object */ @@ -116,6 +119,7 @@ export class Metadata { readingProgression?: ReadingProgression; duration?: number; numberOfPages?: number; + tdm?: TDM; otherMetadata?: { [key: string]: any }; }) { //title always required @@ -166,6 +170,7 @@ export class Metadata { this.readingProgression = values.readingProgression; this.duration = values.duration; this.numberOfPages = values.numberOfPages; + this.tdm = values.tdm; this.otherMetadata = values.otherMetadata; } @@ -204,6 +209,7 @@ export class Metadata { 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]); @@ -239,7 +245,8 @@ export class Metadata { readingProgression, duration, numberOfPages, - otherMetadata, + tdm, + otherMetadata }); } @@ -278,6 +285,7 @@ export class Metadata { 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; diff --git a/shared/src/publication/TDM.ts b/shared/src/publication/TDM.ts new file mode 100644 index 00000000..c978ed50 --- /dev/null +++ b/shared/src/publication/TDM.ts @@ -0,0 +1,56 @@ +/* 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. + */ + +/** + * https://readium.org/webpub-manifest/contexts/default/#text-and-data-mining + */ + +export enum TDMReservation { + all = 'all', + none = 'none' +} + +export class TDM { + /** + * Indicates whether the publication allows text and data mining. + */ + public readonly reservation?: TDMReservation; + + /** + * Additional policy information about text and data mining usage. + */ + public readonly policy?: string; + + /** Creates a [TDM] object */ + constructor(values: { + reservation?: TDMReservation; + policy?: string; + }) { + this.reservation = values.reservation; + this.policy = values.policy; + } + + /** + * Parses a [TDM] from its RWPM JSON representation. + */ + public static deserialize(json: any): TDM | undefined { + if (!json) return; + + return new TDM({ + reservation: json.reservation as TDMReservation, + policy: json.policy, + }); + } + + /** + * Serializes a [TDM] to its RWPM JSON representation. + */ + public serialize(): any { + const json: any = {}; + if (this.reservation !== undefined) json.reservation = this.reservation; + if (this.policy !== undefined) json.policy = this.policy; + return json; + } +} diff --git a/shared/src/publication/index.ts b/shared/src/publication/index.ts index f0fdc66d..569b5908 100644 --- a/shared/src/publication/index.ts +++ b/shared/src/publication/index.ts @@ -17,4 +17,5 @@ export * from './Publication'; export * from './PublicationCollection'; export * from './ReadingProgression'; export * from './Subject'; +export * from './TDM'; export * from './GuidedNavigation'; diff --git a/shared/test/Metadata.test.ts b/shared/test/Metadata.test.ts index 6b7b4088..17689861 100644 --- a/shared/test/Metadata.test.ts +++ b/shared/test/Metadata.test.ts @@ -7,6 +7,8 @@ import { ReadingProgression, Subject, Subjects, + TDM, + TDMReservation, } from '../src'; describe('Metadata Tests', () => { @@ -45,6 +47,10 @@ describe('Metadata Tests', () => { description: 'Description', duration: 4.24, numberOfPages: 240, + tdm: { + reservation: 'all', + policy: 'Some policy text', + }, belongsTo: { collection: 'Collection', series: 'Series', @@ -117,6 +123,10 @@ describe('Metadata Tests', () => { description: 'Description', duration: 4.24, numberOfPages: 240, + tdm: new TDM({ + reservation: TDMReservation.all, + policy: 'Some policy text', + }), belongsTo: new BelongsTo({ items: new Map([ [ @@ -202,7 +212,7 @@ describe('Metadata Tests', () => { }); }); - it('parse full JSON', () => { + it('get full JSON', () => { expect( new Metadata({ identifier: '1234', @@ -291,6 +301,10 @@ describe('Metadata Tests', () => { ], ]), }), + tdm: new TDM({ + reservation: TDMReservation.all, + policy: 'Some policy text', + }), otherMetadata: { 'other-metadata1': 'value', 'other-metadata2': [42], @@ -331,6 +345,10 @@ describe('Metadata Tests', () => { series: [{ name: { undefined: 'Series' } }], 'schema:Periodical': [{ name: { undefined: 'Periodical' } }], }, + tdm: { + reservation: 'all', + policy: 'Some policy text', + }, 'other-metadata1': 'value', 'other-metadata2': [42], }); diff --git a/shared/test/TDM.test.ts b/shared/test/TDM.test.ts new file mode 100644 index 00000000..ba351a04 --- /dev/null +++ b/shared/test/TDM.test.ts @@ -0,0 +1,41 @@ +import { TDM, TDMReservation } from '../src'; + +describe('TDM Tests', () => { + it('parse minimal JSON', () => { + expect(TDM.deserialize({})).toEqual(new TDM({})); + }); + + it('parse full JSON', () => { + expect( + TDM.deserialize({ + reservation: 'all', + policy: 'Some policy text', + }) + ).toEqual( + new TDM({ + reservation: TDMReservation.all, + policy: 'Some policy text', + }) + ); + }); + + it('parse undefined JSON', () => { + expect(TDM.deserialize(undefined)).toBeUndefined(); + }); + + it('get minimal JSON', () => { + expect(new TDM({}).serialize()).toEqual({}); + }); + + it('get full JSON', () => { + expect( + new TDM({ + reservation: TDMReservation.all, + policy: 'Some policy text', + }).serialize() + ).toEqual({ + reservation: 'all', + policy: 'Some policy text', + }); + }); +}); From 959fc6619b6400be6f5386591177b6376e0db7fa Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 16:51:56 +0200 Subject: [PATCH 02/13] Add size property in Link --- shared/src/publication/Link.ts | 7 +++++++ shared/test/Link.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/shared/src/publication/Link.ts b/shared/src/publication/Link.ts index 22eb4a98..79230e9f 100644 --- a/shared/src/publication/Link.ts +++ b/shared/src/publication/Link.ts @@ -42,6 +42,9 @@ export class Link { /** 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; @@ -69,6 +72,7 @@ export class Link { properties?: Properties; height?: number; width?: number; + size?: number; duration?: number; bitrate?: number; languages?: Array; @@ -83,6 +87,7 @@ export class Link { 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; @@ -109,6 +114,7 @@ export class Link { 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), @@ -129,6 +135,7 @@ export class Link { 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; diff --git a/shared/test/Link.test.ts b/shared/test/Link.test.ts index 0a5cff99..e426128b 100644 --- a/shared/test/Link.test.ts +++ b/shared/test/Link.test.ts @@ -49,6 +49,7 @@ describe('Link Tests', () => { }, height: 1024, width: 768, + size: 1024, bitrate: 74.2, duration: 45.6, language: 'fr', @@ -65,6 +66,7 @@ describe('Link Tests', () => { properties: new Properties({ orientation: 'landscape' }), height: 1024, width: 768, + size: 1024, bitrate: 74.2, duration: 45.6, languages: ['fr'], @@ -129,6 +131,12 @@ describe('Link Tests', () => { expect(link?.height).toBeUndefined(); }); + it('parse JSON requires positive size', () => { + const link = Link.deserialize({ href: 'a', size: -20 }); + expect(link).toBeDefined(); + expect(link?.size).toBeUndefined(); + }); + it('parse JSON requires positive bitrate', () => { const link = Link.deserialize({ href: 'a', bitrate: -20 }); expect(link).toBeDefined(); @@ -179,6 +187,7 @@ describe('Link Tests', () => { properties: new Properties({ orientation: 'landscape' }), height: 1024, width: 768, + size: 1024, bitrate: 74.2, duration: 45.6, languages: ['fr'], @@ -202,6 +211,7 @@ describe('Link Tests', () => { }, height: 1024, width: 768, + size: 1024, bitrate: 74.2, duration: 45.6, language: ['fr'], From 7c54af2784d831ffb2b3402206286e5944f47df6 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 17:47:06 +0200 Subject: [PATCH 03/13] Add media overlay in Epub --- shared/src/publication/epub/MediaOverlay.ts | 43 ++++++++++++++ shared/src/publication/epub/Metadata.ts | 21 +++++++ shared/src/publication/epub/index.ts | 2 + shared/test/epub/MediaOverlay.test.ts | 64 +++++++++++++++++++++ shared/test/epub/Metadata.test.ts | 35 +++++++++++ 5 files changed, 165 insertions(+) create mode 100644 shared/src/publication/epub/MediaOverlay.ts create mode 100644 shared/src/publication/epub/Metadata.ts create mode 100644 shared/test/epub/MediaOverlay.test.ts create mode 100644 shared/test/epub/Metadata.test.ts diff --git a/shared/src/publication/epub/MediaOverlay.ts b/shared/src/publication/epub/MediaOverlay.ts new file mode 100644 index 00000000..6d533151 --- /dev/null +++ b/shared/src/publication/epub/MediaOverlay.ts @@ -0,0 +1,43 @@ +/* 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. + */ + +export class MediaOverlay { + /** Author-defined CSS class name to apply to the currently-playing EPUB Content Document element. */ + public activeClass?: string; + + /** Author-defined CSS class name to apply to the EPUB Content Document's document element when playback is active. */ + public playbackActiveClass?: string; + + /** Creates a MediaOverlay object */ + constructor(values: { + activeClass?: string; + playbackActiveClass?: string; + }) { + this.activeClass = values.activeClass; + this.playbackActiveClass = values.playbackActiveClass; + } + + /** + * Parses a MediaOverlay from its RWPM JSON representation. + */ + public static deserialize(json: any): MediaOverlay | undefined { + if (!json) return; + + return new MediaOverlay({ + activeClass: json.activeClass, + playbackActiveClass: json.playbackActiveClass, + }); + } + + /** + * Serializes a MediaOverlay to its RWPM JSON representation. + */ + public serialize(): any { + const json: any = {}; + if (this.activeClass) json.activeClass = this.activeClass; + if (this.playbackActiveClass) json.playbackActiveClass = this.playbackActiveClass; + return json; + } +} diff --git a/shared/src/publication/epub/Metadata.ts b/shared/src/publication/epub/Metadata.ts new file mode 100644 index 00000000..729c22da --- /dev/null +++ b/shared/src/publication/epub/Metadata.ts @@ -0,0 +1,21 @@ +/* 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 { Metadata } from "../Metadata"; +import { MediaOverlay } from "./MediaOverlay"; + +declare module '../Metadata' { + export interface Metadata { + getMediaOverlay(): MediaOverlay | undefined; + } +} + +Metadata.prototype.getMediaOverlay = function(): MediaOverlay | undefined { + const mediaOverlay = this.otherMetadata?.['mediaOverlay']; + + if (!mediaOverlay) return; + + return MediaOverlay.deserialize(mediaOverlay); +}; \ No newline at end of file diff --git a/shared/src/publication/epub/index.ts b/shared/src/publication/epub/index.ts index 2251a77e..80754e7f 100644 --- a/shared/src/publication/epub/index.ts +++ b/shared/src/publication/epub/index.ts @@ -1,4 +1,6 @@ export * from './EPUBLayout'; +export * from './MediaOverlay'; +export * from './Metadata'; export * from './Presentation'; export * from './Properties'; export * from './Publication'; diff --git a/shared/test/epub/MediaOverlay.test.ts b/shared/test/epub/MediaOverlay.test.ts new file mode 100644 index 00000000..921fd1a8 --- /dev/null +++ b/shared/test/epub/MediaOverlay.test.ts @@ -0,0 +1,64 @@ +/* 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 { MediaOverlay } from '../../src'; + +describe('MediaOverlay', () => { + it('parse JSON', () => { + const mediaOverlay = MediaOverlay.deserialize({ + activeClass: 'active', + playbackActiveClass: 'playing', + }); + + expect(mediaOverlay).toBeDefined(); + expect(mediaOverlay?.activeClass).toBe('active'); + expect(mediaOverlay?.playbackActiveClass).toBe('playing'); + }); + + it('parse JSON with undefined values', () => { + const mediaOverlay = MediaOverlay.deserialize({ + activeClass: undefined, + playbackActiveClass: undefined, + }); + + expect(mediaOverlay).toBeDefined(); + expect(mediaOverlay?.activeClass).toBeUndefined(); + expect(mediaOverlay?.playbackActiveClass).toBeUndefined(); + }); + + it('parse JSON with empty values', () => { + const mediaOverlay = MediaOverlay.deserialize({ + activeClass: '', + playbackActiveClass: '', + }); + + expect(mediaOverlay).toBeDefined(); + expect(mediaOverlay?.activeClass).toBe(''); + expect(mediaOverlay?.playbackActiveClass).toBe(''); + }); + + it('serialize', () => { + const mediaOverlay = new MediaOverlay({ + activeClass: 'active', + playbackActiveClass: 'playing', + }); + + const json = mediaOverlay.serialize(); + expect(json).toEqual({ + activeClass: 'active', + playbackActiveClass: 'playing', + }); + }); + + it('serialize with undefined values', () => { + const mediaOverlay = new MediaOverlay({ + activeClass: undefined, + playbackActiveClass: undefined, + }); + + const json = mediaOverlay.serialize(); + expect(json).toEqual({}); + }); +}); diff --git a/shared/test/epub/Metadata.test.ts b/shared/test/epub/Metadata.test.ts new file mode 100644 index 00000000..da86fd6a --- /dev/null +++ b/shared/test/epub/Metadata.test.ts @@ -0,0 +1,35 @@ +/* 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 { LocalizedString, MediaOverlay, Metadata } from '../../src'; + +describe('EPUB Metadata Tests', () => { + it('getMediaOverlay when available', () => { + expect( + new Metadata({ + title: new LocalizedString({ default: 'Test' }), + otherMetadata: { + mediaOverlay: { + activeClass: 'active', + playbackActiveClass: 'playing' + } + } + }).getMediaOverlay() + ).toEqual( + new MediaOverlay({ + activeClass: 'active', + playbackActiveClass: 'playing' + }) + ); + }); + + it('getMediaOverlay when missing', () => { + expect( + new Metadata({ + title: new LocalizedString({ default: 'Test' }) + }).getMediaOverlay() + ).toBeUndefined(); + }); +}); \ No newline at end of file From 74e56c515bc47b854bd35f6df7b141f51301382b Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 18:02:47 +0200 Subject: [PATCH 04/13] Add AltIdentifier --- shared/src/publication/AltIdentifier.ts | 58 ++++++++++++++++++++++ shared/src/publication/Contributor.ts | 10 ++++ shared/src/publication/index.ts | 1 + shared/src/util/JSONParse.ts | 4 +- shared/test/AltIdentifier.test.ts | 66 +++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 shared/src/publication/AltIdentifier.ts create mode 100644 shared/test/AltIdentifier.test.ts diff --git a/shared/src/publication/AltIdentifier.ts b/shared/src/publication/AltIdentifier.ts new file mode 100644 index 00000000..83acc5db --- /dev/null +++ b/shared/src/publication/AltIdentifier.ts @@ -0,0 +1,58 @@ +/* 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. + */ + +/** + * Represents an alternate identifier for a publication. + * https://readium.org/webpub-manifest/schema/altIdentifier.schema.json + */ +export class AltIdentifier { + /** The value of the alternate identifier. */ + public readonly value: string; + + /** The scheme of the alternate identifier (URI format). */ + public readonly scheme?: string; + + /** Creates an AltIdentifier object */ + constructor(values: { + value: string; + scheme?: string; + }) { + this.value = values.value; + this.scheme = values.scheme; + } + + /** + * Parses an AltIdentifier from its RWPM JSON representation. + */ + public static deserialize(json: string | any): AltIdentifier | undefined { + if (!json) return; + + if (typeof json === 'string') { + return new AltIdentifier({ value: json }); + } + + if (typeof json === 'object' && json.value) { + return new AltIdentifier({ + value: json.value, + scheme: json.scheme + }); + } + + return undefined; + } + + /** + * Serializes an AltIdentifier to its RWPM JSON representation. + */ + public serialize(): string | any { + if (this.scheme) { + return { + value: this.value, + scheme: this.scheme + }; + } + return this.value; + } +} diff --git a/shared/src/publication/Contributor.ts b/shared/src/publication/Contributor.ts index 9596a8dc..b6f7e1d4 100644 --- a/shared/src/publication/Contributor.ts +++ b/shared/src/publication/Contributor.ts @@ -10,6 +10,7 @@ import { setToArray, } from '../util/JSONParse'; import { LocalizedString } from './LocalizedString'; +import { AltIdentifier } from './AltIdentifier'; /** * Contributor Object for the Readium Web Publication Manifest. @@ -25,6 +26,9 @@ export class Contributor { /** An unambiguous reference to this contributor. */ public readonly identifier?: string; + /** Alternate identifiers for this contributor. */ + public readonly altIdentifiers?: Set; + /** The role of the contributor in the publication making. */ public readonly roles?: Set; @@ -41,6 +45,7 @@ export class Contributor { name: LocalizedString; sortAs?: string; identifier?: string; + altIdentifiers?: Set; roles?: Set; links?: Links; position?: number; @@ -48,6 +53,7 @@ export class Contributor { this.name = values.name; this.sortAs = values.sortAs; this.identifier = values.identifier; + this.altIdentifiers = values.altIdentifiers; this.roles = values.roles; this.links = values.links; this.position = values.position; @@ -70,6 +76,9 @@ export class Contributor { name: LocalizedString.deserialize(json.name) as LocalizedString, sortAs: json.sortAs, identifier: json.identifier, + altIdentifiers: json.altIdentifier + ? new Set(arrayfromJSONorString(json.altIdentifier)) + : undefined, roles: json.role ? new Set(arrayfromJSONorString(json.role)) : undefined, @@ -86,6 +95,7 @@ export class Contributor { const json: any = { name: this.name.serialize() }; if (this.sortAs !== undefined) json.sortAs = this.sortAs; if (this.identifier !== undefined) json.identifier = this.identifier; + if (this.altIdentifiers) json.altIdentifier = setToArray(this.altIdentifiers); if (this.roles) json.role = setToArray(this.roles); if (this.links) json.links = this.links.serialize(); if (this.position !== undefined) json.position = this.position; diff --git a/shared/src/publication/index.ts b/shared/src/publication/index.ts index 569b5908..fc4a6fae 100644 --- a/shared/src/publication/index.ts +++ b/shared/src/publication/index.ts @@ -4,6 +4,7 @@ export * from './html'; export * from './opds'; export * from './presentation'; export * from './services'; +export * from './AltIdentifier'; export * from './BelongsTo'; export * from './Contributor'; export * from './Link'; diff --git a/shared/src/util/JSONParse.ts b/shared/src/util/JSONParse.ts index 1768ccfb..d2834a93 100644 --- a/shared/src/util/JSONParse.ts +++ b/shared/src/util/JSONParse.ts @@ -24,8 +24,8 @@ export function positiveNumberfromJSON(json: any): number | undefined { } /** Converts a Set of a string to a string Array object */ -export function setToArray(obj: Set): Array { - const list = new Array(); +export function setToArray(obj: Set): Array { + const list = new Array(); obj.forEach(x => list.push(x)); return list; } diff --git a/shared/test/AltIdentifier.test.ts b/shared/test/AltIdentifier.test.ts new file mode 100644 index 00000000..2720d857 --- /dev/null +++ b/shared/test/AltIdentifier.test.ts @@ -0,0 +1,66 @@ +/* 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 { AltIdentifier } from '../src'; + +describe('AltIdentifier', () => { + it('parse JSON string', () => { + const identifier = AltIdentifier.deserialize('author:67890'); + expect(identifier).toBeDefined(); + expect(identifier?.value).toBe('author:67890'); + expect(identifier?.scheme).toBeUndefined(); + }); + + it('parse JSON object', () => { + const identifier = AltIdentifier.deserialize({ + value: 'author:67890', + scheme: 'http://example.com/schemes/author-id' + }); + + expect(identifier).toBeDefined(); + expect(identifier?.value).toBe('author:67890'); + expect(identifier?.scheme).toBe('http://example.com/schemes/author-id'); + }); + + it('parse JSON object without scheme', () => { + const identifier = AltIdentifier.deserialize({ + value: 'author:67890' + }); + + expect(identifier).toBeDefined(); + expect(identifier?.value).toBe('author:67890'); + expect(identifier?.scheme).toBeUndefined(); + }); + + it('parse JSON invalid', () => { + const identifier = AltIdentifier.deserialize({ + scheme: 'http://example.com/schemes/author-id' + }); + + expect(identifier).toBeUndefined(); + }); + + it('serialize string', () => { + const identifier = new AltIdentifier({ + value: 'author:22222' + }); + + const json = identifier.serialize(); + expect(json).toBe('author:22222'); + }); + + it('serialize object', () => { + const identifier = new AltIdentifier({ + value: 'author:22222', + scheme: 'http://example.com/schemes/author-id' + }); + + const json = identifier.serialize(); + expect(json).toEqual({ + value: 'author:22222', + scheme: 'http://example.com/schemes/author-id' + }); + }); +}); From 12a4143e6aa5772314f5fb35341fef26ce64698d Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 18:35:27 +0200 Subject: [PATCH 05/13] Correct AltIdentifier --- shared/src/publication/Contributor.ts | 4 +++- shared/src/publication/Metadata.ts | 8 ++++++++ shared/test/Contributor.test.ts | 14 ++++++++++++++ shared/test/Metadata.test.ts | 11 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/shared/src/publication/Contributor.ts b/shared/src/publication/Contributor.ts index b6f7e1d4..79f2d69d 100644 --- a/shared/src/publication/Contributor.ts +++ b/shared/src/publication/Contributor.ts @@ -77,7 +77,9 @@ export class Contributor { sortAs: json.sortAs, identifier: json.identifier, altIdentifiers: json.altIdentifier - ? new Set(arrayfromJSONorString(json.altIdentifier)) + ? json.altIdentifier instanceof Array + ? new Set(json.altIdentifier.map((x: AltIdentifier) => AltIdentifier.deserialize(x)).filter((x: AltIdentifier) => x !== undefined)) + : new Set([AltIdentifier.deserialize(json.altIdentifier)].filter(x => x !== undefined)) : undefined, roles: json.role ? new Set(arrayfromJSONorString(json.role)) diff --git a/shared/src/publication/Metadata.ts b/shared/src/publication/Metadata.ts index b7b54fa2..898c314d 100644 --- a/shared/src/publication/Metadata.ts +++ b/shared/src/publication/Metadata.ts @@ -8,6 +8,7 @@ import { datefromJSON, positiveNumberfromJSON, } from '../util/JSONParse'; +import { AltIdentifier } from './AltIdentifier'; import { BelongsTo } from './BelongsTo'; import { Contributors } from './Contributor'; import { LocalizedString } from './LocalizedString'; @@ -27,6 +28,7 @@ export class Metadata { public title: LocalizedString; public typeUri?: string; public identifier?: string; + public altIdentifier?: AltIdentifier; public subtitle?: LocalizedString; public sortAs?: LocalizedString; public artists?: Contributors; @@ -61,6 +63,7 @@ export class Metadata { 'title', '@type', 'identifier', + 'altIdentifier', 'subtitle', 'sortAs', 'artist', @@ -93,6 +96,7 @@ export class Metadata { title: LocalizedString; typeUri?: string; identifier?: string; + altIdentifier?: AltIdentifier; subtitle?: LocalizedString; sortAs?: LocalizedString; artists?: Contributors; @@ -126,6 +130,7 @@ export class Metadata { this.title = values.title as LocalizedString; this.typeUri = values.typeUri; this.identifier = values.identifier; + this.altIdentifier = values.altIdentifier; this.subtitle = values.subtitle; this.sortAs = values.sortAs; this.artists = values.artists; @@ -185,6 +190,7 @@ export class Metadata { const title = LocalizedString.deserialize(json.title) as LocalizedString; const typeUri = json['@type']; 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); @@ -221,6 +227,7 @@ export class Metadata { title, typeUri, identifier, + altIdentifier, subtitle, sortAs, artists, @@ -257,6 +264,7 @@ export class Metadata { const json: any = { title: this.title.serialize() }; if (this.typeUri !== undefined) json['@type'] = this.typeUri; 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(); diff --git a/shared/test/Contributor.test.ts b/shared/test/Contributor.test.ts index 960dd8be..00d6b3f8 100644 --- a/shared/test/Contributor.test.ts +++ b/shared/test/Contributor.test.ts @@ -1,4 +1,5 @@ import { + AltIdentifier, Contributor, Contributors, Link, @@ -28,6 +29,7 @@ describe('Contributor Tests', () => { Contributor.deserialize({ name: 'Colin Greenwood', identifier: 'colin', + altIdentifier: { scheme: 'http://example.com/author-id', value: 'author-22222' }, sortAs: 'greenwood', role: 'bassist', position: 4, @@ -38,6 +40,11 @@ describe('Contributor Tests', () => { name: new LocalizedString('Colin Greenwood'), sortAs: 'greenwood', identifier: 'colin', + altIdentifiers: new Set([ + new AltIdentifier({ + scheme: 'http://example.com/author-id', + value: 'author-22222', + })]), roles: new Set(['bassist']), position: 4.0, links: new Links([ @@ -166,6 +173,12 @@ describe('Contributor Tests', () => { name: new LocalizedString('Colin Greenwood'), sortAs: 'greenwood', identifier: 'colin', + altIdentifiers: new Set([ + new AltIdentifier({ + scheme: 'http://example.com/author-id', + value: 'author-22222', + }), + ]), roles: new Set(['bassist']), position: 4.0, links: new Links([ @@ -177,6 +190,7 @@ describe('Contributor Tests', () => { name: { undefined: 'Colin Greenwood' }, sortAs: 'greenwood', identifier: 'colin', + altIdentifier: [{ scheme: 'http://example.com/author-id', value: 'author-22222' }], role: ['bassist'], position: 4.0, links: [{ href: 'http://link1' }, { href: 'http://link2' }], diff --git a/shared/test/Metadata.test.ts b/shared/test/Metadata.test.ts index 17689861..3c3dbd16 100644 --- a/shared/test/Metadata.test.ts +++ b/shared/test/Metadata.test.ts @@ -9,6 +9,7 @@ import { Subjects, TDM, TDMReservation, + AltIdentifier, } from '../src'; describe('Metadata Tests', () => { @@ -22,6 +23,7 @@ describe('Metadata Tests', () => { expect( Metadata.deserialize({ identifier: '1234', + altIdentifier: { scheme: 'http://example.com/scheme', value: 'test-1234' }, '@type': 'epub', title: { en: 'Title', fr: 'Titre' }, subtitle: { en: 'Subtitle', fr: 'Sous-titre' }, @@ -63,6 +65,10 @@ describe('Metadata Tests', () => { ).toEqual( new Metadata({ identifier: '1234', + altIdentifier: new AltIdentifier({ + scheme: 'http://example.com/scheme', + value: 'test-1234', + }), typeUri: 'epub', title: new LocalizedString({ en: 'Title', @@ -216,6 +222,10 @@ describe('Metadata Tests', () => { expect( new Metadata({ identifier: '1234', + altIdentifier: new AltIdentifier({ + scheme: 'http://example.com/scheme', + value: 'test-1234', + }), typeUri: 'epub', title: new LocalizedString({ en: 'Title', @@ -312,6 +322,7 @@ describe('Metadata Tests', () => { }).serialize() ).toEqual({ identifier: '1234', + altIdentifier: { scheme: 'http://example.com/scheme', value: 'test-1234' }, '@type': 'epub', title: { en: 'Title', fr: 'Titre' }, subtitle: { en: 'Subtitle', fr: 'Sous-titre' }, From 40d2bae676e3625db6cb1c0b46d7ff1e5a1fc7d4 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Tue, 15 Jul 2025 18:41:21 +0200 Subject: [PATCH 06/13] Correct altId deserialization in Contributor --- shared/src/publication/Contributor.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shared/src/publication/Contributor.ts b/shared/src/publication/Contributor.ts index 79f2d69d..4146fb41 100644 --- a/shared/src/publication/Contributor.ts +++ b/shared/src/publication/Contributor.ts @@ -77,9 +77,12 @@ export class Contributor { sortAs: json.sortAs, identifier: json.identifier, altIdentifiers: json.altIdentifier - ? json.altIdentifier instanceof Array - ? new Set(json.altIdentifier.map((x: AltIdentifier) => AltIdentifier.deserialize(x)).filter((x: AltIdentifier) => x !== undefined)) - : new Set([AltIdentifier.deserialize(json.altIdentifier)].filter(x => x !== undefined)) + ? (json.altIdentifier instanceof Array + ? new Set(json.altIdentifier + .map((x: string | { value: string; scheme?: string }) => AltIdentifier.deserialize(x)) + .filter((x: AltIdentifier | undefined): x is AltIdentifier => x !== undefined)) + : new Set([AltIdentifier.deserialize(json.altIdentifier as string | { value: string; scheme?: string })] + .filter((x: AltIdentifier | undefined): x is AltIdentifier => x !== undefined))) : undefined, roles: json.role ? new Set(arrayfromJSONorString(json.role)) From 6ee87b63efd3382a1d60882ded7ba2bc54394833 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Wed, 16 Jul 2025 09:35:18 +0200 Subject: [PATCH 07/13] Correct serialization of altId in Contributor --- shared/src/publication/Contributor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/publication/Contributor.ts b/shared/src/publication/Contributor.ts index 4146fb41..041c131b 100644 --- a/shared/src/publication/Contributor.ts +++ b/shared/src/publication/Contributor.ts @@ -100,7 +100,7 @@ export class Contributor { const json: any = { name: this.name.serialize() }; if (this.sortAs !== undefined) json.sortAs = this.sortAs; if (this.identifier !== undefined) json.identifier = this.identifier; - if (this.altIdentifiers) json.altIdentifier = setToArray(this.altIdentifiers); + if (this.altIdentifiers) json.altIdentifier = setToArray(this.altIdentifiers).map(altId => altId.serialize()); if (this.roles) json.role = setToArray(this.roles); if (this.links) json.links = this.links.serialize(); if (this.position !== undefined) json.position = this.position; From 0ca5bf882e1ca899f5f46457529f1ea6217ebfe8 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Wed, 16 Jul 2025 09:44:00 +0200 Subject: [PATCH 08/13] Add Divina Properties --- shared/src/publication/divina/Properties.ts | 18 ++++++++++++++++ shared/src/publication/divina/index.ts | 1 + shared/src/publication/index.ts | 1 + shared/test/divina/Properties.test.ts | 24 +++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 shared/src/publication/divina/Properties.ts create mode 100644 shared/src/publication/divina/index.ts create mode 100644 shared/test/divina/Properties.test.ts diff --git a/shared/src/publication/divina/Properties.ts b/shared/src/publication/divina/Properties.ts new file mode 100644 index 00000000..3815425f --- /dev/null +++ b/shared/src/publication/divina/Properties.ts @@ -0,0 +1,18 @@ +import { Properties } from "../Properties"; + +// Divina extensions for link [Properties]. +// https://github.com/readium/webpub-manifest/blob/master/schema/extensions/divina/properties.schema.json + +declare module '../Properties' { + export interface Properties { + /** + * Specifies that an item in the reading order should break the current continuous scroll + * and start a new one. + */ + getBreakScrollBefore(): boolean; + } +} + +Properties.prototype.getBreakScrollBefore = function(): boolean { + return this.otherProperties['break-scroll-before'] ?? false; +}; \ No newline at end of file diff --git a/shared/src/publication/divina/index.ts b/shared/src/publication/divina/index.ts new file mode 100644 index 00000000..ef224bea --- /dev/null +++ b/shared/src/publication/divina/index.ts @@ -0,0 +1 @@ +export * from './Properties'; \ No newline at end of file diff --git a/shared/src/publication/index.ts b/shared/src/publication/index.ts index fc4a6fae..9bfe0369 100644 --- a/shared/src/publication/index.ts +++ b/shared/src/publication/index.ts @@ -1,3 +1,4 @@ +export * from './divina'; export * from './encryption'; export * from './epub'; export * from './html'; diff --git a/shared/test/divina/Properties.test.ts b/shared/test/divina/Properties.test.ts new file mode 100644 index 00000000..36cb1e40 --- /dev/null +++ b/shared/test/divina/Properties.test.ts @@ -0,0 +1,24 @@ +import { Properties } from '../../src'; + +describe('Divina Properties Tests', () => { + describe('getBreakScrollBefore', () => { + it('returns false when break-scroll-before is not set', () => { + const properties = new Properties({}); + expect(properties.getBreakScrollBefore()).toBe(false); + }); + + it('returns true when break-scroll-before is true', () => { + const properties = new Properties({ + 'break-scroll-before': true + }); + expect(properties.getBreakScrollBefore()).toBe(true); + }); + + it('returns false when break-scroll-before is false', () => { + const properties = new Properties({ + 'break-scroll-before': false + }); + expect(properties.getBreakScrollBefore()).toBe(false); + }); + }); +}); From 548aa43b061881d7e3e1e71a132e7abd531a8e42 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Wed, 16 Jul 2025 09:53:52 +0200 Subject: [PATCH 09/13] Correct setToArray Description --- shared/src/util/JSONParse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/util/JSONParse.ts b/shared/src/util/JSONParse.ts index d2834a93..f9868fa4 100644 --- a/shared/src/util/JSONParse.ts +++ b/shared/src/util/JSONParse.ts @@ -23,7 +23,7 @@ export function positiveNumberfromJSON(json: any): number | undefined { return num !== undefined && Math.sign(json) >= 0 ? json : undefined; } -/** Converts a Set of a string to a string Array object */ +/** Converts a Set to an Array object */ export function setToArray(obj: Set): Array { const list = new Array(); obj.forEach(x => list.push(x)); From 9b0f732251fa3f13400c35935f677c62d7d65263 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Wed, 16 Jul 2025 09:58:52 +0200 Subject: [PATCH 10/13] Make copyright of additions consistent --- shared/src/publication/epub/Metadata.ts | 5 ----- shared/test/AltIdentifier.test.ts | 5 ----- shared/test/epub/MediaOverlay.test.ts | 5 ----- shared/test/epub/Metadata.test.ts | 5 ----- 4 files changed, 20 deletions(-) diff --git a/shared/src/publication/epub/Metadata.ts b/shared/src/publication/epub/Metadata.ts index 729c22da..bb86fd23 100644 --- a/shared/src/publication/epub/Metadata.ts +++ b/shared/src/publication/epub/Metadata.ts @@ -1,8 +1,3 @@ -/* 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 { Metadata } from "../Metadata"; import { MediaOverlay } from "./MediaOverlay"; diff --git a/shared/test/AltIdentifier.test.ts b/shared/test/AltIdentifier.test.ts index 2720d857..2864f658 100644 --- a/shared/test/AltIdentifier.test.ts +++ b/shared/test/AltIdentifier.test.ts @@ -1,8 +1,3 @@ -/* 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 { AltIdentifier } from '../src'; describe('AltIdentifier', () => { diff --git a/shared/test/epub/MediaOverlay.test.ts b/shared/test/epub/MediaOverlay.test.ts index 921fd1a8..4cd4b638 100644 --- a/shared/test/epub/MediaOverlay.test.ts +++ b/shared/test/epub/MediaOverlay.test.ts @@ -1,8 +1,3 @@ -/* 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 { MediaOverlay } from '../../src'; describe('MediaOverlay', () => { diff --git a/shared/test/epub/Metadata.test.ts b/shared/test/epub/Metadata.test.ts index da86fd6a..8d8d7ec8 100644 --- a/shared/test/epub/Metadata.test.ts +++ b/shared/test/epub/Metadata.test.ts @@ -1,8 +1,3 @@ -/* 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 { LocalizedString, MediaOverlay, Metadata } from '../../src'; describe('EPUB Metadata Tests', () => { From 1c6c2ebd84357241d6238cfe10140aff10ce7be4 Mon Sep 17 00:00:00 2001 From: Jiminy Panoz Date: Mon, 21 Jul 2025 16:42:51 +0200 Subject: [PATCH 11/13] Update GuidedNavigation models And create tests --- shared/src/publication/GuidedNavigation.ts | 138 ++++++- shared/test/GuidedNavigation.test.ts | 439 +++++++++++++++++++++ 2 files changed, 562 insertions(+), 15 deletions(-) create mode 100644 shared/test/GuidedNavigation.test.ts diff --git a/shared/src/publication/GuidedNavigation.ts b/shared/src/publication/GuidedNavigation.ts index a70a1c78..0ec7329e 100644 --- a/shared/src/publication/GuidedNavigation.ts +++ b/shared/src/publication/GuidedNavigation.ts @@ -43,6 +43,66 @@ export class GuidedNavigationDocument { } } +/** + * Represents a text value containing plain text, SSML, and language information. + */ +export class GuidedNavigationText { + /** Plain text content */ + public readonly plain?: string; + + /** SSML (Speech Synthesis Markup Language) content */ + public readonly ssml?: string; + + /** + * BCP 47 language tag + * @pattern ^((?(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?([A-Za-z]{2,3}(-(?[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?