diff --git a/decorator/CHANGELOG.MD b/decorator/CHANGELOG.MD index 0cd0f137..cc71622d 100644 --- a/decorator/CHANGELOG.MD +++ b/decorator/CHANGELOG.MD @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.1] – 2026-09-15 + +### Fixed + +- Republished against `@readium/navigator-html-injectables@2.8.1`/`@readium/shared@2.5.1` ([#254](https://github.com/readium/ts-toolkit/pull/254)) + ## [1.2.0] – 2026-09-10 ### Added diff --git a/decorator/package.json b/decorator/package.json index eac5a57e..18e849e3 100644 --- a/decorator/package.json +++ b/decorator/package.json @@ -1,6 +1,6 @@ { "name": "@readium/decorator", - "version": "1.2.0", + "version": "1.2.1", "type": "module", "description": "Standalone decoration controller and renderer for HTML content", "author": "readium", diff --git a/navigator-html-injectables/CHANGELOG.MD b/navigator-html-injectables/CHANGELOG.MD index 6e501eed..afcd2367 100644 --- a/navigator-html-injectables/CHANGELOG.MD +++ b/navigator-html-injectables/CHANGELOG.MD @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.8.1] – 2026-09-15 + +### Fixed + +- Republished against `@readium/shared@2.5.1` ([#254](https://github.com/readium/ts-toolkit/pull/254)) + ## [2.8.0] – 2026-09-10 ### Added diff --git a/navigator-html-injectables/package.json b/navigator-html-injectables/package.json index 45940e64..9c89e1aa 100644 --- a/navigator-html-injectables/package.json +++ b/navigator-html-injectables/package.json @@ -1,6 +1,6 @@ { "name": "@readium/navigator-html-injectables", - "version": "2.8.0", + "version": "2.8.1", "type": "module", "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator", "author": "readium", diff --git a/navigator/CHANGELOG.MD b/navigator/CHANGELOG.MD index d4e07073..0ba629e6 100644 --- a/navigator/CHANGELOG.MD +++ b/navigator/CHANGELOG.MD @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.10.1] – 2026-09-15 + +### Fixed + +- Republished against `@readium/decorator@1.2.1`/`@readium/navigator-html-injectables@2.8.1`/`@readium/shared@2.5.1` ([#254](https://github.com/readium/ts-toolkit/pull/254)) + ## [2.10.0] – 2026-09-10 ### Added diff --git a/navigator/package.json b/navigator/package.json index bb939fc1..45866535 100644 --- a/navigator/package.json +++ b/navigator/package.json @@ -1,6 +1,6 @@ { "name": "@readium/navigator", - "version": "2.10.0", + "version": "2.10.1", "type": "module", "description": "Next generation SDK for publications in Web Apps", "author": "readium", diff --git a/shared/CHANGELOG.MD b/shared/CHANGELOG.MD index fad0483d..37a254d2 100644 --- a/shared/CHANGELOG.MD +++ b/shared/CHANGELOG.MD @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.5.1] – 2026-09-15 + +### Fixed + +- `Accessibility.deserialize` no longer throws when `conformsTo`, `accessMode`, `feature`, `hazard`, `exemption`, or `accessModeSufficient` are serialized as a bare string or another non-array value instead of an array ([#254](https://github.com/readium/ts-toolkit/pull/254)) + ## [2.5.0] – 2026-09-08 ### Added diff --git a/shared/package.json b/shared/package.json index de153056..4ca49b9a 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,6 +1,6 @@ { "name": "@readium/shared", - "version": "2.5.0", + "version": "2.5.1", "type": "module", "description": "Shared models to be used across other Readium projects and implementations in Typescript", "author": "readium", diff --git a/shared/src/publication/accessibility/Accessibility.ts b/shared/src/publication/accessibility/Accessibility.ts index 1482c088..f1f3890d 100644 --- a/shared/src/publication/accessibility/Accessibility.ts +++ b/shared/src/publication/accessibility/Accessibility.ts @@ -3,6 +3,8 @@ * available in the LICENSE file present in the Github repository of the project. */ +import { arrayfromJSON, arrayfromJSONorString } from '../../util/JSONParse.ts'; + /** * Holds the accessibility metadata of a Publication. * @@ -77,49 +79,57 @@ export class Accessibility { if (!json || typeof json !== 'object') return; type AccessibilityJson = { - conformsTo?: string[]; + conformsTo?: string | string[]; certification?: { certifiedBy: string; credential: string; report: string; }; summary?: string; - accessMode?: string[]; - accessModeSufficient?: string[][]; - feature?: string[]; - hazard?: string[]; - exemption?: string[]; + accessMode?: string | string[]; + accessModeSufficient?: (string | string[])[]; + feature?: string | string[]; + hazard?: string | string[]; + exemption?: string | string[]; }; const accessibilityJson = json as AccessibilityJson; + // Per the RWPM a11y schema, these fields may be a bare string or an array of strings. + const conformsTo = arrayfromJSONorString(accessibilityJson.conformsTo); + const accessMode = arrayfromJSONorString(accessibilityJson.accessMode); + const accessModeSufficient = arrayfromJSON(accessibilityJson.accessModeSufficient); + const feature = arrayfromJSONorString(accessibilityJson.feature); + const hazard = arrayfromJSONorString(accessibilityJson.hazard); + const exemption = arrayfromJSONorString(accessibilityJson.exemption); + return new Accessibility({ - conformsTo: accessibilityJson.conformsTo - ? accessibilityJson.conformsTo.map(uri => AccessibilityProfile.deserialize(uri)) + conformsTo: conformsTo + ? conformsTo.map(uri => AccessibilityProfile.deserialize(uri)) .filter((profile): profile is AccessibilityProfile => profile !== undefined) : undefined, - certification: accessibilityJson.certification + certification: accessibilityJson.certification ? Certification.deserialize(accessibilityJson.certification) : undefined, summary: accessibilityJson.summary, - accessMode: accessibilityJson.accessMode - ? accessibilityJson.accessMode.map(value => AccessMode.deserialize(value)) + accessMode: accessMode + ? accessMode.map(value => AccessMode.deserialize(value)) .filter((mode): mode is AccessMode => mode !== undefined) : undefined, - accessModeSufficient: accessibilityJson.accessModeSufficient - ? accessibilityJson.accessModeSufficient.map(modes => PrimaryAccessMode.deserialize(modes)) - .filter((mode): mode is PrimaryAccessMode => mode !== undefined) + accessModeSufficient: accessModeSufficient + ? accessModeSufficient.map(modes => PrimaryAccessMode.deserialize(modes)) + .filter((mode): mode is PrimaryAccessMode => mode !== undefined) : undefined, - feature: accessibilityJson.feature - ? accessibilityJson.feature.map(value => Feature.deserialize(value)) + feature: feature + ? feature.map(value => Feature.deserialize(value)) .filter((feature): feature is Feature => feature !== undefined) : undefined, - hazard: accessibilityJson.hazard - ? accessibilityJson.hazard.map(value => Hazard.deserialize(value)) + hazard: hazard + ? hazard.map(value => Hazard.deserialize(value)) .filter((hazard): hazard is Hazard => hazard !== undefined) : undefined, - exemption: accessibilityJson.exemption - ? accessibilityJson.exemption.map(value => Exemption.deserialize(value)) + exemption: exemption + ? exemption.map(value => Exemption.deserialize(value)) .filter((exemption): exemption is Exemption => exemption !== undefined) : undefined }); @@ -427,6 +437,9 @@ export class PrimaryAccessMode { if (!json) return; if (typeof json === 'string') { + if (!PrimaryAccessMode.VALID_MODES.has(json.toLowerCase())) { + return undefined; + } return new PrimaryAccessMode(json); } diff --git a/shared/test/accessibility/Accessibility.test.ts b/shared/test/accessibility/Accessibility.test.ts index e5ec3cf3..dc318a56 100644 --- a/shared/test/accessibility/Accessibility.test.ts +++ b/shared/test/accessibility/Accessibility.test.ts @@ -222,6 +222,18 @@ describe('Accessibility Tests', () => { expect(Accessibility.deserialize(json2)).toEqual(expected2); }); + it('parse drops invalid bare-string PrimaryAccessMode entries', () => { + const json = { + accessModeSufficient: ['invalid', 'textual'] + }; + + const expected = new Accessibility({ + accessModeSufficient: [new PrimaryAccessMode('textual')] + }); + + expect(Accessibility.deserialize(json)).toEqual(expected); + }); + it('parse PrimaryAccessMode with multiple arrays', () => { const json = { accessModeSufficient: [['textual', 'visual'], ['auditory']] @@ -256,6 +268,72 @@ describe('Accessibility Tests', () => { expect(Accessibility.deserialize(json2)).toEqual(expected2); }); + it('parse conformsTo serialized as a bare string', () => { + const json = { + conformsTo: 'https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a' + }; + + const expected = new Accessibility({ + conformsTo: [new AccessibilityProfile('https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a')] + }); + + expect(Accessibility.deserialize(json)).toEqual(expected); + }); + + it('parses accessMode/feature/hazard/exemption serialized as a bare string', () => { + const json = { + accessMode: 'textual', + feature: 'alternativeText', + hazard: 'none', + exemption: 'eaa-microenterprise' + }; + + const expected = new Accessibility({ + accessMode: [new AccessMode('textual')], + feature: [new Feature('alternativeText')], + hazard: [new Hazard('none')], + exemption: [new Exemption('eaa-microenterprise')] + }); + + expect(Accessibility.deserialize(json)).toEqual(expected); + }); + + it('does not throw when accessModeSufficient itself is serialized as a bare string', () => { + const json = { + accessModeSufficient: 'textual' + }; + + expect(Accessibility.deserialize(json)).toEqual(new Accessibility()); + }); + + it('does not throw and ignores malformed non-string, non-array values', () => { + const json = { + conformsTo: 42, + accessMode: null, + accessModeSufficient: { foo: 'bar' }, + feature: true, + hazard: 42, + exemption: {} + }; + + expect(Accessibility.deserialize(json)).toEqual(new Accessibility()); + }); + + it('still parses accessModeSufficient items as string or array per the spec oneOf', () => { + const json = { + accessModeSufficient: ['textual', ['visual', 'auditory']] + }; + + const expected = new Accessibility({ + accessModeSufficient: [ + new PrimaryAccessMode('textual'), + new PrimaryAccessMode(['visual', 'auditory']) + ] + }); + + expect(Accessibility.deserialize(json)).toEqual(expected); + }); + it('serialize Profile', () => { const accessibility = new Accessibility({ conformsTo: [ diff --git a/shared/test/accessibility/Localization.test.ts b/shared/test/accessibility/Localization.test.ts index f132d56f..f971b9cf 100644 --- a/shared/test/accessibility/Localization.test.ts +++ b/shared/test/accessibility/Localization.test.ts @@ -2,10 +2,17 @@ import { SUPPORTED_LANGUAGES } from '../../src/publication/accessibility/Support import { Localization } from '../../src/publication/accessibility/Localization'; describe('Localization', () => { + let warnSpy: jest.SpyInstance; + // Reset the singleton instance before each test beforeEach(() => { // @ts-ignore - Accessing private member for testing Localization.instance = undefined; + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); }); it('should be a singleton', () => {