@readium/shared 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1055 @@
1
+ import { Accessibility, Feature, AccessMode, PrimaryAccessMode, Hazard } from './Accessibility';
2
+ import { Publication } from '../Publication';
3
+ import { AccessibilityProfile } from './Accessibility';
4
+
5
+ import { Localization } from './Localization';
6
+ import { Layout } from '../Layout';
7
+
8
+ /**
9
+ * Represents a single accessibility claim
10
+ */
11
+ export interface AccessibilityDisplayStatement {
12
+ id: string;
13
+ compactString?: string;
14
+ descriptiveString?: string;
15
+ }
16
+
17
+ /**
18
+ * Represents a collection of related accessibility claims which should be
19
+ * displayed together in a section
20
+ */
21
+ export interface AccessibilityDisplayField {
22
+ // Unique identifier for this display field
23
+ id: string;
24
+
25
+ // Title for this display field
26
+ title: string;
27
+
28
+ // List of accessibility claims to display for this field
29
+ statements: AccessibilityDisplayStatement[];
30
+
31
+ // Indicates whether this display field should be rendered
32
+ shouldDisplay: boolean;
33
+ }
34
+
35
+ /**
36
+ * Represents the different ways visual adjustments can be made
37
+ */
38
+ export enum VisualAdjustments {
39
+ Unknown = 'unknown',
40
+ Modifiable = 'modifiable',
41
+ Unmodifiable = 'unmodifiable'
42
+ }
43
+
44
+ /**
45
+ * Represents the different types of non-visual reading
46
+ */
47
+ export enum NonvisualReading {
48
+ NoMetadata = 'noMetadata',
49
+ Readable = 'readable',
50
+ NotFully = 'notFully',
51
+ Unreadable = 'unreadable'
52
+ }
53
+
54
+ /**
55
+ * Represents the different types of prerecorded audio
56
+ */
57
+ export enum PrerecordedAudio {
58
+ NoMetadata = 'noMetadata',
59
+ Synchronized = 'synchronized',
60
+ AudioOnly = 'audioOnly',
61
+ AudioComplementary = 'audioComplementary'
62
+ }
63
+
64
+ /**
65
+ * The ways of reading display field is a banner heading that groups
66
+ * together the following information about how the content facilitates
67
+ * access.
68
+ */
69
+ export class WaysOfReading implements AccessibilityDisplayField {
70
+ public readonly id = "ways-of-reading.title";
71
+ public readonly title: string;
72
+ public readonly shouldDisplay: boolean;
73
+
74
+ public readonly visualAdjustments: VisualAdjustments;
75
+ public readonly nonvisualReading: NonvisualReading;
76
+ public readonly nonvisualReadingAltText: boolean;
77
+ public readonly prerecordedAudio: PrerecordedAudio;
78
+ public readonly statements: AccessibilityDisplayStatement[];
79
+
80
+ private constructor(
81
+ visualAdjustments: VisualAdjustments = VisualAdjustments.Unknown,
82
+ nonvisualReading: NonvisualReading = NonvisualReading.NoMetadata,
83
+ nonvisualReadingAltText: boolean = false,
84
+ prerecordedAudio: PrerecordedAudio = PrerecordedAudio.NoMetadata
85
+ ) {
86
+ this.visualAdjustments = visualAdjustments;
87
+ this.nonvisualReading = nonvisualReading;
88
+ this.nonvisualReadingAltText = nonvisualReadingAltText;
89
+ this.prerecordedAudio = prerecordedAudio;
90
+
91
+ // This should be displayed even if there is no metadata
92
+ this.shouldDisplay = true;
93
+
94
+ const titleLocale = Localization.getString(this.id);
95
+ this.title = titleLocale.compact;
96
+
97
+ this.statements = [];
98
+
99
+ // Visual Adjustments
100
+ const visualAdjustmentsKey = visualAdjustments === VisualAdjustments.Modifiable
101
+ ? "ways-of-reading.visual-adjustments.modifiable"
102
+ : visualAdjustments === VisualAdjustments.Unmodifiable
103
+ ? "ways-of-reading.visual-adjustments.unmodifiable"
104
+ : "ways-of-reading.visual-adjustments.unknown";
105
+
106
+ const visualAdjustmentsLocale = Localization.getString(visualAdjustmentsKey);
107
+ this.statements.push({
108
+ id: visualAdjustmentsKey,
109
+ compactString: visualAdjustmentsLocale.compact,
110
+ descriptiveString: visualAdjustmentsLocale.descriptive
111
+ });
112
+
113
+ // Non-visual Reading
114
+ let nonvisualReadingKey = "";
115
+ if (nonvisualReading === NonvisualReading.Readable) {
116
+ nonvisualReadingKey = "ways-of-reading.nonvisual-reading.readable";
117
+ } else if (nonvisualReading === NonvisualReading.NotFully) {
118
+ nonvisualReadingKey = "ways-of-reading.nonvisual-reading.not-fully";
119
+ } else if (nonvisualReading === NonvisualReading.Unreadable) {
120
+ nonvisualReadingKey = "ways-of-reading.nonvisual-reading.none";
121
+ } else if (nonvisualReading === NonvisualReading.NoMetadata) {
122
+ nonvisualReadingKey = "ways-of-reading.nonvisual-reading.no-metadata";
123
+ }
124
+
125
+ const nonvisualReadingLocale = Localization.getString(nonvisualReadingKey);
126
+ this.statements.push({
127
+ id: nonvisualReadingKey,
128
+ compactString: nonvisualReadingLocale.compact,
129
+ descriptiveString: nonvisualReadingLocale.descriptive
130
+ });
131
+
132
+ // Non-visual Reading Alt Text
133
+ if (nonvisualReadingAltText) {
134
+ const altTextLocale = Localization.getString("ways-of-reading.nonvisual-reading.alt-text");
135
+ this.statements.push({
136
+ id: "ways-of-reading.nonvisual-reading.alt-text",
137
+ compactString: altTextLocale.compact,
138
+ descriptiveString: altTextLocale.descriptive
139
+ });
140
+ }
141
+
142
+ // Prerecorded Audio
143
+ let prerecordedAudioKey = "";
144
+ if (prerecordedAudio === PrerecordedAudio.Synchronized) {
145
+ prerecordedAudioKey = "ways-of-reading.prerecorded-audio.synchronized";
146
+ } else if (prerecordedAudio === PrerecordedAudio.AudioOnly) {
147
+ prerecordedAudioKey = "ways-of-reading.prerecorded-audio.only";
148
+ } else if (prerecordedAudio === PrerecordedAudio.AudioComplementary) {
149
+ prerecordedAudioKey = "ways-of-reading.prerecorded-audio.complementary";
150
+ } else if (prerecordedAudio === PrerecordedAudio.NoMetadata) {
151
+ prerecordedAudioKey = "ways-of-reading.prerecorded-audio.no-metadata";
152
+ }
153
+
154
+ const prerecordedAudioLocale = Localization.getString(prerecordedAudioKey);
155
+ this.statements.push({
156
+ id: prerecordedAudioKey,
157
+ compactString: prerecordedAudioLocale.compact,
158
+ descriptiveString: prerecordedAudioLocale.descriptive
159
+ });
160
+ }
161
+
162
+ public static fromPublication(publication: Publication): WaysOfReading {
163
+ const a11y = publication.metadata.accessibility ?? new Accessibility();
164
+ const features = a11y.feature ?? [];
165
+ const isFXL = publication.metadata.layout === Layout.fixed;
166
+
167
+ const visualAdjustments = features.some(f => f.value === Feature.DISPLAY_TRANSFORMABILITY.value)
168
+ ? VisualAdjustments.Modifiable
169
+ : isFXL
170
+ ? VisualAdjustments.Unmodifiable
171
+ : VisualAdjustments.Unknown;
172
+
173
+ const accessModes = a11y.accessMode ?? [];
174
+ const accessModeSufficient = a11y.accessModeSufficient ?? [];
175
+
176
+ const allText = accessModes.length > 0 && accessModes.every((m: AccessMode) => m.value === AccessMode.TEXTUAL.value) ||
177
+ accessModeSufficient.some((modes: PrimaryAccessMode) => {
178
+ const modeValue = modes.value;
179
+ if (Array.isArray(modeValue)) {
180
+ return modeValue.every(mode => mode === AccessMode.TEXTUAL.value);
181
+ }
182
+ return modeValue === AccessMode.TEXTUAL.value;
183
+ });
184
+
185
+ const someText = accessModes.some((m: AccessMode) => m.value === AccessMode.TEXTUAL.value) ||
186
+ accessModeSufficient.some((modes: PrimaryAccessMode) => {
187
+ const modeValue = modes.value;
188
+ if (Array.isArray(modeValue)) {
189
+ return modeValue.some(mode => mode === AccessMode.TEXTUAL.value);
190
+ }
191
+ return modeValue === AccessMode.TEXTUAL.value;
192
+ });
193
+
194
+ const noText = !(accessModes.length === 0 && accessModeSufficient.length === 0) &&
195
+ !accessModes.some((m: AccessMode) => m.value === AccessMode.TEXTUAL.value) &&
196
+ !accessModeSufficient.some((modes: PrimaryAccessMode) => {
197
+ const modeValue = modes.value;
198
+ if (Array.isArray(modeValue)) {
199
+ return modeValue.some(mode => mode === AccessMode.TEXTUAL.value);
200
+ }
201
+ return modeValue === AccessMode.TEXTUAL.value;
202
+ });
203
+
204
+ const hasTextAlt = features.some(f => [
205
+ Feature.LONG_DESCRIPTION.value,
206
+ Feature.ALTERNATIVE_TEXT.value,
207
+ Feature.DESCRIBED_MATH.value,
208
+ Feature.TRANSCRIPT.value
209
+ ].includes(f.value));
210
+
211
+ const nonvisualReading = allText
212
+ ? NonvisualReading.Readable
213
+ : (someText || hasTextAlt)
214
+ ? NonvisualReading.NotFully
215
+ : noText
216
+ ? NonvisualReading.Unreadable
217
+ : NonvisualReading.NoMetadata;
218
+
219
+ const nonvisualReadingAltText = hasTextAlt;
220
+
221
+ const prerecordedAudio = features.some(f => f.value === Feature.SYNCHRONIZED_AUDIO_TEXT.value)
222
+ ? PrerecordedAudio.Synchronized
223
+ : accessModeSufficient.some((modes: PrimaryAccessMode) => {
224
+ const modeValue = modes.value;
225
+ if (Array.isArray(modeValue)) {
226
+ return modeValue.some(mode => mode === AccessMode.AUDITORY.value);
227
+ }
228
+ return modeValue === AccessMode.AUDITORY.value;
229
+ })
230
+ ? PrerecordedAudio.AudioOnly
231
+ : accessModes.some((m: AccessMode) => m.value === AccessMode.AUDITORY.value)
232
+ ? PrerecordedAudio.AudioComplementary
233
+ : PrerecordedAudio.NoMetadata;
234
+
235
+ return new WaysOfReading(
236
+ visualAdjustments,
237
+ nonvisualReading,
238
+ nonvisualReadingAltText,
239
+ prerecordedAudio
240
+ );
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Navigation features of the content
246
+ */
247
+ export class Navigation implements AccessibilityDisplayField {
248
+ public readonly id = "navigation.title";
249
+ public readonly title: string;
250
+ public readonly shouldDisplay: boolean;
251
+
252
+ public readonly noMetadata: boolean;
253
+ public readonly tableOfContents: boolean;
254
+ public readonly index: boolean;
255
+ public readonly headings: boolean;
256
+ public readonly page: boolean;
257
+ public readonly statements: AccessibilityDisplayStatement[];
258
+
259
+ private constructor(
260
+ tableOfContents: boolean = false,
261
+ index: boolean = false,
262
+ headings: boolean = false,
263
+ page: boolean = false
264
+ ) {
265
+ this.tableOfContents = tableOfContents;
266
+ this.index = index;
267
+ this.headings = headings;
268
+ this.page = page;
269
+ this.noMetadata = !tableOfContents && !index && !headings && !page;
270
+
271
+ const titleLocale = Localization.getString(this.id);
272
+ this.title = titleLocale.compact;
273
+
274
+ this.shouldDisplay = !this.noMetadata;
275
+
276
+ this.statements = [];
277
+ if (tableOfContents) {
278
+ const tocLocale = Localization.getString("navigation.toc");
279
+ this.statements.push({
280
+ id: "navigation.toc",
281
+ compactString: tocLocale.compact,
282
+ descriptiveString: tocLocale.descriptive
283
+ });
284
+ }
285
+ if (index) {
286
+ const indexLocale = Localization.getString("navigation.index");
287
+ this.statements.push({
288
+ id: "navigation.index",
289
+ compactString: indexLocale.compact,
290
+ descriptiveString: indexLocale.descriptive
291
+ });
292
+ }
293
+ if (headings) {
294
+ const headingsLocale = Localization.getString("navigation.structural");
295
+ this.statements.push({
296
+ id: "navigation.structural",
297
+ compactString: headingsLocale.compact,
298
+ descriptiveString: headingsLocale.descriptive
299
+ });
300
+ }
301
+ if (page) {
302
+ const pageNavLocale = Localization.getString("navigation.page-navigation");
303
+ this.statements.push({
304
+ id: "navigation.page-navigation",
305
+ compactString: pageNavLocale.compact,
306
+ descriptiveString: pageNavLocale.descriptive
307
+ });
308
+ }
309
+
310
+ if (this.statements.length === 0) {
311
+ const noNavLocale = Localization.getString("navigation.no-metadata");
312
+ this.statements.push({
313
+ id: "navigation.no-metadata",
314
+ compactString: noNavLocale.compact,
315
+ descriptiveString: noNavLocale.descriptive
316
+ });
317
+ }
318
+ }
319
+
320
+ public static fromPublication(publication: Publication): Navigation {
321
+ const a11y = publication.metadata.accessibility ?? new Accessibility();
322
+ const features = a11y.feature ?? [];
323
+
324
+ return new Navigation(
325
+ features.some(f => f.value === Feature.TABLE_OF_CONTENTS.value),
326
+ features.some(f => f.value === Feature.INDEX.value),
327
+ features.some(f => f.value === Feature.STRUCTURAL_NAVIGATION.value),
328
+ features.some(f => f.value === Feature.PAGE_NAVIGATION.value)
329
+ );
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Represents the different types of rich content
335
+ */
336
+ export enum RichContentType {
337
+ Math = 'math',
338
+ Chemistry = 'chemistry',
339
+ Music = 'music',
340
+ Diagram = 'diagram',
341
+ Chart = 'chart',
342
+ Graph = 'graph',
343
+ Table = 'table',
344
+ Image = 'image'
345
+ }
346
+
347
+ /**
348
+ * Rich content features of the content
349
+ */
350
+ export class RichContent implements AccessibilityDisplayField {
351
+ public readonly id = "rich-content.title";
352
+ public readonly title: string;
353
+ public readonly shouldDisplay: boolean;
354
+
355
+ public readonly noMetadata: boolean;
356
+ public readonly extendedAltTextDescriptions: boolean;
357
+ public readonly mathFormula: boolean;
358
+ public readonly mathFormulaAsMathML: boolean;
359
+ public readonly mathFormulaAsLaTeX: boolean;
360
+ public readonly chemicalFormulaAsMathML: boolean;
361
+ public readonly chemicalFormulaAsLaTeX: boolean;
362
+ public readonly closedCaptions: boolean;
363
+ public readonly openCaptions: boolean;
364
+ public readonly transcript: boolean;
365
+ public readonly statements: AccessibilityDisplayStatement[];
366
+
367
+ private constructor(
368
+ extendedAltTextDescriptions: boolean = false,
369
+ mathFormula: boolean = false,
370
+ mathFormulaAsMathML: boolean = false,
371
+ mathFormulaAsLaTeX: boolean = false,
372
+ chemicalFormulaAsMathML: boolean = false,
373
+ chemicalFormulaAsLaTeX: boolean = false,
374
+ closedCaptions: boolean = false,
375
+ openCaptions: boolean = false,
376
+ transcript: boolean = false
377
+ ) {
378
+ this.extendedAltTextDescriptions = extendedAltTextDescriptions;
379
+ this.mathFormula = mathFormula;
380
+ this.mathFormulaAsMathML = mathFormulaAsMathML;
381
+ this.mathFormulaAsLaTeX = mathFormulaAsLaTeX;
382
+ this.chemicalFormulaAsMathML = chemicalFormulaAsMathML;
383
+ this.chemicalFormulaAsLaTeX = chemicalFormulaAsLaTeX;
384
+ this.closedCaptions = closedCaptions;
385
+ this.openCaptions = openCaptions;
386
+ this.transcript = transcript;
387
+ this.noMetadata = !extendedAltTextDescriptions && !mathFormula && !mathFormulaAsMathML &&
388
+ !mathFormulaAsLaTeX && !chemicalFormulaAsMathML && !chemicalFormulaAsLaTeX &&
389
+ !closedCaptions && !openCaptions && !transcript;
390
+
391
+ this.shouldDisplay = !this.noMetadata;
392
+
393
+ const titleLocale = Localization.getString(this.id);
394
+ this.title = titleLocale.compact;
395
+
396
+ this.statements = [];
397
+ if (extendedAltTextDescriptions) {
398
+ const extendedLocale = Localization.getString("rich-content.extended-descriptions");
399
+ this.statements.push({
400
+ id: "rich-content.extended-descriptions",
401
+ compactString: extendedLocale.compact,
402
+ descriptiveString: extendedLocale.descriptive
403
+ });
404
+ }
405
+ if (mathFormula) {
406
+ const mathLocale = Localization.getString("rich-content.accessible-math-described");
407
+ this.statements.push({
408
+ id: "rich-content.accessible-math-described",
409
+ compactString: mathLocale.compact,
410
+ descriptiveString: mathLocale.descriptive
411
+ });
412
+ }
413
+ if (mathFormulaAsMathML) {
414
+ const mathMLLocale = Localization.getString("rich-content.math-as-mathml");
415
+ this.statements.push({
416
+ id: "rich-content.math-as-mathml",
417
+ compactString: mathMLLocale.compact,
418
+ descriptiveString: mathMLLocale.descriptive
419
+ });
420
+ }
421
+ if (mathFormulaAsLaTeX) {
422
+ const latexLocale = Localization.getString("rich-content.accessible-math-as-latex");
423
+ this.statements.push({
424
+ id: "rich-content.accessible-math-as-latex",
425
+ compactString: latexLocale.compact,
426
+ descriptiveString: latexLocale.descriptive
427
+ });
428
+ }
429
+ if (chemicalFormulaAsMathML) {
430
+ const chemMLLocale = Localization.getString("rich-content.accessible-chemistry-as-mathml");
431
+ this.statements.push({
432
+ id: "rich-content.accessible-chemistry-as-mathml",
433
+ compactString: chemMLLocale.compact,
434
+ descriptiveString: chemMLLocale.descriptive
435
+ });
436
+ }
437
+ if (chemicalFormulaAsLaTeX) {
438
+ const chemLatexLocale = Localization.getString("rich-content.accessible-chemistry-as-latex");
439
+ this.statements.push({
440
+ id: "rich-content.accessible-chemistry-as-latex",
441
+ compactString: chemLatexLocale.compact,
442
+ descriptiveString: chemLatexLocale.descriptive
443
+ });
444
+ }
445
+ if (closedCaptions) {
446
+ const ccLocale = Localization.getString("rich-content.closed-captions");
447
+ this.statements.push({
448
+ id: "rich-content.closed-captions",
449
+ compactString: ccLocale.compact,
450
+ descriptiveString: ccLocale.descriptive
451
+ });
452
+ }
453
+ if (openCaptions) {
454
+ const ocLocale = Localization.getString("rich-content.open-captions");
455
+ this.statements.push({
456
+ id: "rich-content.open-captions",
457
+ compactString: ocLocale.compact,
458
+ descriptiveString: ocLocale.descriptive
459
+ });
460
+ }
461
+ if (transcript) {
462
+ const transcriptLocale = Localization.getString("rich-content.transcript");
463
+ this.statements.push({
464
+ id: "rich-content.transcript",
465
+ compactString: transcriptLocale.compact,
466
+ descriptiveString: transcriptLocale.descriptive
467
+ });
468
+ }
469
+
470
+ if (this.statements.length === 0) {
471
+ const unknownLocale = Localization.getString("rich-content.unknown");
472
+ this.statements.push({
473
+ id: "rich-content.unknown",
474
+ compactString: unknownLocale.compact,
475
+ descriptiveString: unknownLocale.descriptive
476
+ });
477
+ }
478
+ }
479
+
480
+ public static fromPublication(publication: Publication): RichContent {
481
+ const a11y = publication.metadata.accessibility ?? new Accessibility();
482
+ const features = a11y.feature ?? [];
483
+
484
+ return new RichContent(
485
+ features.some(f => f.value === Feature.LONG_DESCRIPTION.value),
486
+ features.some(f => f.value === Feature.DESCRIBED_MATH.value),
487
+ features.some(f => f.value === Feature.MATH_ML.value),
488
+ features.some(f => f.value === Feature.LATEX.value),
489
+ features.some(f => f.value === Feature.MATH_ML_CHEMISTRY.value),
490
+ features.some(f => f.value === Feature.LATEX_CHEMISTRY.value),
491
+ features.some(f => f.value === Feature.CLOSED_CAPTIONS.value),
492
+ features.some(f => f.value === Feature.OPEN_CAPTIONS.value),
493
+ features.some(f => f.value === Feature.TRANSCRIPT.value)
494
+ );
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Represents additional accessibility information
500
+ */
501
+ export class AdditionalInformation implements AccessibilityDisplayField {
502
+ public readonly id = "additional-accessibility-information.title";
503
+ public readonly title: string;
504
+ public readonly shouldDisplay: boolean;
505
+
506
+ public readonly noMetadata: boolean;
507
+ public readonly pageBreakMarkers: boolean;
508
+ public readonly aria: boolean;
509
+ public readonly audioDescriptions: boolean;
510
+ public readonly braille: boolean;
511
+ public readonly rubyAnnotations: boolean;
512
+ public readonly fullRubyAnnotations: boolean;
513
+ public readonly highAudioContrast: boolean;
514
+ public readonly highDisplayContrast: boolean;
515
+ public readonly largePrint: boolean;
516
+ public readonly signLanguage: boolean;
517
+ public readonly tactileGraphics: boolean;
518
+ public readonly tactileObjects: boolean;
519
+ public readonly textToSpeechHinting: boolean;
520
+ public readonly statements: AccessibilityDisplayStatement[];
521
+
522
+ private constructor(
523
+ pageBreakMarkers: boolean = false,
524
+ aria: boolean = false,
525
+ audioDescriptions: boolean = false,
526
+ braille: boolean = false,
527
+ rubyAnnotations: boolean = false,
528
+ fullRubyAnnotations: boolean = false,
529
+ highAudioContrast: boolean = false,
530
+ highDisplayContrast: boolean = false,
531
+ largePrint: boolean = false,
532
+ signLanguage: boolean = false,
533
+ tactileGraphics: boolean = false,
534
+ tactileObjects: boolean = false,
535
+ textToSpeechHinting: boolean = false
536
+ ) {
537
+ this.pageBreakMarkers = pageBreakMarkers;
538
+ this.aria = aria;
539
+ this.audioDescriptions = audioDescriptions;
540
+ this.braille = braille;
541
+ this.rubyAnnotations = rubyAnnotations;
542
+ this.fullRubyAnnotations = fullRubyAnnotations;
543
+ this.highAudioContrast = highAudioContrast;
544
+ this.highDisplayContrast = highDisplayContrast;
545
+ this.largePrint = largePrint;
546
+ this.signLanguage = signLanguage;
547
+ this.tactileGraphics = tactileGraphics;
548
+ this.tactileObjects = tactileObjects;
549
+ this.textToSpeechHinting = textToSpeechHinting;
550
+ this.noMetadata = !pageBreakMarkers && !aria && !audioDescriptions &&
551
+ !braille && !rubyAnnotations && !fullRubyAnnotations &&
552
+ !highAudioContrast && !highDisplayContrast && !largePrint &&
553
+ !signLanguage && !tactileGraphics && !tactileObjects && !textToSpeechHinting;
554
+
555
+ this.shouldDisplay = !this.noMetadata;
556
+
557
+ const titleLocale = Localization.getString(this.id);
558
+ this.title = titleLocale.compact;
559
+
560
+ this.statements = [];
561
+ if (pageBreakMarkers) {
562
+ const pageBreaksLocale = Localization.getString("additional-accessibility-information.page-breaks");
563
+ this.statements.push({
564
+ id: "additional-accessibility-information.page-breaks",
565
+ compactString: pageBreaksLocale.compact,
566
+ descriptiveString: pageBreaksLocale.descriptive
567
+ });
568
+ }
569
+ if (aria) {
570
+ const ariaLocale = Localization.getString("additional-accessibility-information.aria");
571
+ this.statements.push({
572
+ id: "additional-accessibility-information.aria",
573
+ compactString: ariaLocale.compact,
574
+ descriptiveString: ariaLocale.descriptive
575
+ });
576
+ }
577
+ if (audioDescriptions) {
578
+ const audioDescLocale = Localization.getString("additional-accessibility-information.audio-descriptions");
579
+ this.statements.push({
580
+ id: "additional-accessibility-information.audio-descriptions",
581
+ compactString: audioDescLocale.compact,
582
+ descriptiveString: audioDescLocale.descriptive
583
+ });
584
+ }
585
+ if (braille) {
586
+ const brailleLocale = Localization.getString("additional-accessibility-information.braille");
587
+ this.statements.push({
588
+ id: "additional-accessibility-information.braille",
589
+ compactString: brailleLocale.compact,
590
+ descriptiveString: brailleLocale.descriptive
591
+ });
592
+ }
593
+ if (rubyAnnotations) {
594
+ const rubyLocale = Localization.getString("additional-accessibility-information.ruby-annotations");
595
+ this.statements.push({
596
+ id: "additional-accessibility-information.ruby-annotations",
597
+ compactString: rubyLocale.compact,
598
+ descriptiveString: rubyLocale.descriptive
599
+ });
600
+ }
601
+ if (fullRubyAnnotations) {
602
+ const fullRubyLocale = Localization.getString("additional-accessibility-information.full-ruby-annotations");
603
+ this.statements.push({
604
+ id: "additional-accessibility-information.full-ruby-annotations",
605
+ compactString: fullRubyLocale.compact,
606
+ descriptiveString: fullRubyLocale.descriptive
607
+ });
608
+ }
609
+ if (highAudioContrast) {
610
+ const audioContrastLocale = Localization.getString("additional-accessibility-information.high-contrast-between-foreground-and-background-audio");
611
+ this.statements.push({
612
+ id: "additional-accessibility-information.high-contrast-between-foreground-and-background-audio",
613
+ compactString: audioContrastLocale.compact,
614
+ descriptiveString: audioContrastLocale.descriptive
615
+ });
616
+ }
617
+ if (highDisplayContrast) {
618
+ const displayContrastLocale = Localization.getString("additional-accessibility-information.high-contrast-between-text-and-background");
619
+ this.statements.push({
620
+ id: "additional-accessibility-information.high-contrast-between-text-and-background",
621
+ compactString: displayContrastLocale.compact,
622
+ descriptiveString: displayContrastLocale.descriptive
623
+ });
624
+ }
625
+ if (largePrint) {
626
+ const largePrintLocale = Localization.getString("additional-accessibility-information.large-print");
627
+ this.statements.push({
628
+ id: "additional-accessibility-information.large-print",
629
+ compactString: largePrintLocale.compact,
630
+ descriptiveString: largePrintLocale.descriptive
631
+ });
632
+ }
633
+ if (signLanguage) {
634
+ const signLanguageLocale = Localization.getString("additional-accessibility-information.sign-language");
635
+ this.statements.push({
636
+ id: "additional-accessibility-information.sign-language",
637
+ compactString: signLanguageLocale.compact,
638
+ descriptiveString: signLanguageLocale.descriptive
639
+ });
640
+ }
641
+ if (tactileGraphics) {
642
+ const tactileGraphicsLocale = Localization.getString("additional-accessibility-information.tactile-graphics");
643
+ this.statements.push({
644
+ id: "additional-accessibility-information.tactile-graphics",
645
+ compactString: tactileGraphicsLocale.compact,
646
+ descriptiveString: tactileGraphicsLocale.descriptive
647
+ });
648
+ }
649
+ if (tactileObjects) {
650
+ const tactileObjectsLocale = Localization.getString("additional-accessibility-information.tactile-objects");
651
+ this.statements.push({
652
+ id: "additional-accessibility-information.tactile-objects",
653
+ compactString: tactileObjectsLocale.compact,
654
+ descriptiveString: tactileObjectsLocale.descriptive
655
+ });
656
+ }
657
+ if (textToSpeechHinting) {
658
+ const ttsHintingLocale = Localization.getString("additional-accessibility-information.text-to-speech-hinting");
659
+ this.statements.push({
660
+ id: "additional-accessibility-information.text-to-speech-hinting",
661
+ compactString: ttsHintingLocale.compact,
662
+ descriptiveString: ttsHintingLocale.descriptive
663
+ });
664
+ }
665
+ }
666
+
667
+ public static fromPublication(publication: Publication): AdditionalInformation {
668
+ const a11y = publication.metadata.accessibility ?? new Accessibility();
669
+ const features = a11y.feature ?? [];
670
+
671
+ return new AdditionalInformation(
672
+ features.some(f => f.value === Feature.PAGE_BREAK_MARKERS.value || f.value === Feature.PRINT_PAGE_NUMBERS.value),
673
+ features.some(f => f.value === Feature.ARIA.value),
674
+ features.some(f => f.value === Feature.AUDIO_DESCRIPTION.value),
675
+ features.some(f => f.value === Feature.BRAILLE.value),
676
+ features.some(f => f.value === Feature.RUBY_ANNOTATIONS.value),
677
+ features.some(f => f.value === Feature.FULL_RUBY_ANNOTATIONS.value),
678
+ features.some(f => f.value === Feature.HIGH_CONTRAST_AUDIO.value),
679
+ features.some(f => f.value === Feature.HIGH_CONTRAST_DISPLAY.value),
680
+ features.some(f => f.value === Feature.LARGE_PRINT.value),
681
+ features.some(f => f.value === Feature.SIGN_LANGUAGE.value),
682
+ features.some(f => f.value === Feature.TACTILE_GRAPHIC.value),
683
+ features.some(f => f.value === Feature.TACTILE_OBJECT.value),
684
+ features.some(f => f.value === Feature.TTS_MARKUP.value)
685
+ );
686
+ }
687
+ }
688
+
689
+ export enum HazardType {
690
+ yes = 'yes',
691
+ no = 'no',
692
+ unknown = 'unknown',
693
+ noMetadata = 'noMetadata'
694
+ }
695
+
696
+ /**
697
+ * Represents potential hazards in the content
698
+ */
699
+ export class Hazards implements AccessibilityDisplayField {
700
+ public readonly id = "hazards.title";
701
+ public readonly title: string;
702
+ public readonly shouldDisplay: boolean;
703
+ public readonly noMetadata: boolean;
704
+ public readonly noHazards: boolean;
705
+ public readonly unknown: boolean;
706
+ public readonly flashing: HazardType;
707
+ public readonly motion: HazardType;
708
+ public readonly sound: HazardType;
709
+ public readonly statements: AccessibilityDisplayStatement[];
710
+
711
+ private constructor(
712
+ flashing: HazardType = HazardType.unknown,
713
+ motion: HazardType = HazardType.unknown,
714
+ sound: HazardType = HazardType.unknown
715
+ ) {
716
+ this.flashing = flashing;
717
+ this.motion = motion;
718
+ this.sound = sound;
719
+
720
+ const titleLocale = Localization.getString(this.id);
721
+ this.title = titleLocale.compact;
722
+
723
+ this.noMetadata = flashing === HazardType.noMetadata && motion === HazardType.noMetadata && sound === HazardType.noMetadata;
724
+ this.noHazards = flashing === HazardType.no && motion === HazardType.no && sound === HazardType.no;
725
+ this.unknown = flashing === HazardType.unknown && motion === HazardType.unknown && sound === HazardType.unknown;
726
+
727
+ this.shouldDisplay = !this.noMetadata;
728
+
729
+ this.statements = [];
730
+ if (this.noHazards) {
731
+ const noneLocale = Localization.getString("hazards.none");
732
+ this.statements.push({
733
+ id: "hazards.none",
734
+ compactString: noneLocale.compact,
735
+ descriptiveString: noneLocale.descriptive
736
+ });
737
+ } else if (this.unknown) {
738
+ const unknownLocale = Localization.getString("hazards.unknown");
739
+ this.statements.push({
740
+ id: "hazards.unknown",
741
+ compactString: unknownLocale.compact,
742
+ descriptiveString: unknownLocale.descriptive
743
+ });
744
+ } else if (this.noMetadata) {
745
+ const noMetadataLocale = Localization.getString("hazards.no-metadata");
746
+ this.statements.push({
747
+ id: "hazards.no-metadata",
748
+ compactString: noMetadataLocale.compact,
749
+ descriptiveString: noMetadataLocale.descriptive
750
+ });
751
+ } else {
752
+ if (flashing === HazardType.yes) {
753
+ const flashingLocale = Localization.getString("hazards.flashing");
754
+ this.statements.push({
755
+ id: "hazards.flashing",
756
+ compactString: flashingLocale.compact,
757
+ descriptiveString: flashingLocale.descriptive
758
+ });
759
+ } else if (flashing === HazardType.unknown) {
760
+ const flashingUnknownLocale = Localization.getString("hazards.flashing-unknown");
761
+ this.statements.push({
762
+ id: "hazards.flashing-unknown",
763
+ compactString: flashingUnknownLocale.compact,
764
+ descriptiveString: flashingUnknownLocale.descriptive
765
+ });
766
+ } else if (flashing === HazardType.no) {
767
+ const flashingNoneLocale = Localization.getString("hazards.flashing-none");
768
+ this.statements.push({
769
+ id: "hazards.flashing-none",
770
+ compactString: flashingNoneLocale.compact,
771
+ descriptiveString: flashingNoneLocale.descriptive
772
+ });
773
+ }
774
+ if (motion === HazardType.yes) {
775
+ const motionLocale = Localization.getString("hazards.motion");
776
+ this.statements.push({
777
+ id: "hazards.motion",
778
+ compactString: motionLocale.compact,
779
+ descriptiveString: motionLocale.descriptive
780
+ });
781
+ } else if (motion === HazardType.unknown) {
782
+ const motionUnknownLocale = Localization.getString("hazards.motion-unknown");
783
+ this.statements.push({
784
+ id: "hazards.motion-unknown",
785
+ compactString: motionUnknownLocale.compact,
786
+ descriptiveString: motionUnknownLocale.descriptive
787
+ });
788
+ } else if (motion === HazardType.no) {
789
+ const motionNoneLocale = Localization.getString("hazards.motion-none");
790
+ this.statements.push({
791
+ id: "hazards.motion-none",
792
+ compactString: motionNoneLocale.compact,
793
+ descriptiveString: motionNoneLocale.descriptive
794
+ });
795
+ }
796
+ if (sound === HazardType.yes) {
797
+ const soundLocale = Localization.getString("hazards.sound");
798
+ this.statements.push({
799
+ id: "hazards.sound",
800
+ compactString: soundLocale.compact,
801
+ descriptiveString: soundLocale.descriptive
802
+ });
803
+ } else if (sound === HazardType.unknown) {
804
+ const soundUnknownLocale = Localization.getString("hazards.sound-unknown");
805
+ this.statements.push({
806
+ id: "hazards.sound-unknown",
807
+ compactString: soundUnknownLocale.compact,
808
+ descriptiveString: soundUnknownLocale.descriptive
809
+ });
810
+ } else if (sound === HazardType.no) {
811
+ const soundNoneLocale = Localization.getString("hazards.sound-none");
812
+ this.statements.push({
813
+ id: "hazards.sound-none",
814
+ compactString: soundNoneLocale.compact,
815
+ descriptiveString: soundNoneLocale.descriptive
816
+ });
817
+ }
818
+ }
819
+ }
820
+
821
+ public static fromPublication(publication: Publication): Hazards {
822
+ const hazards = publication.metadata.accessibility?.hazard ?? [];
823
+
824
+ let fallback: HazardType;
825
+ if (hazards.some(h => h.value === Hazard.NONE.value)) {
826
+ fallback = HazardType.no;
827
+ } else if (hazards.some(h => h.value === Hazard.UNKNOWN.value)) {
828
+ fallback = HazardType.unknown;
829
+ } else {
830
+ fallback = HazardType.noMetadata;
831
+ }
832
+
833
+ let flashing: HazardType;
834
+ if (hazards.some(h => h.value === Hazard.FLASHING.value)) {
835
+ flashing = HazardType.yes;
836
+ } else if (hazards.some(h => h.value === Hazard.NO_FLASHING_HAZARD.value)) {
837
+ flashing = HazardType.no;
838
+ } else if (hazards.some(h => h.value === Hazard.UNKNOWN_FLASHING_HAZARD.value)) {
839
+ flashing = HazardType.unknown;
840
+ } else {
841
+ flashing = fallback;
842
+ }
843
+
844
+ let motion: HazardType;
845
+ if (hazards.some(h => h.value === Hazard.MOTION_SIMULATION.value)) {
846
+ motion = HazardType.yes;
847
+ } else if (hazards.some(h => h.value === Hazard.NO_MOTION_SIMULATION_HAZARD.value)) {
848
+ motion = HazardType.no;
849
+ } else if (hazards.some(h => h.value === Hazard.UNKNOWN_MOTION_SIMULATION_HAZARD.value)) {
850
+ motion = HazardType.unknown;
851
+ } else {
852
+ motion = fallback;
853
+ }
854
+
855
+ let sound: HazardType;
856
+ if (hazards.some(h => h.value === Hazard.SOUND.value)) {
857
+ sound = HazardType.yes;
858
+ } else if (hazards.some(h => h.value === Hazard.NO_SOUND_HAZARD.value)) {
859
+ sound = HazardType.no;
860
+ } else if (hazards.some(h => h.value === Hazard.UNKNOWN_SOUND_HAZARD.value)) {
861
+ sound = HazardType.unknown;
862
+ } else {
863
+ sound = fallback;
864
+ }
865
+
866
+ return new Hazards(flashing, motion, sound);
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Represents conformance to accessibility standards
872
+ */
873
+ export class Conformance implements AccessibilityDisplayField {
874
+ public readonly id = "conformance.title";
875
+ public readonly title: string;
876
+ public readonly shouldDisplay: boolean;
877
+ public readonly profiles: AccessibilityProfile[];
878
+ public readonly statements: AccessibilityDisplayStatement[];
879
+
880
+ private constructor(profiles: AccessibilityProfile[] = []) {
881
+ this.profiles = profiles;
882
+
883
+ // This should be displayed even if there is no metadata
884
+ this.shouldDisplay = true;
885
+
886
+ const titleLocale = Localization.getString(this.id);
887
+ this.title = titleLocale.compact;
888
+
889
+ this.statements = [];
890
+ if (profiles.length === 0) {
891
+ const noConformanceLocale = Localization.getString("conformance.no");
892
+ this.statements.push({
893
+ id: "conformance.no",
894
+ compactString: noConformanceLocale.compact,
895
+ descriptiveString: noConformanceLocale.descriptive
896
+ });
897
+ return;
898
+ }
899
+
900
+ if (profiles.some(profile => profile.isWCAGLevelAAA)) {
901
+ const aaaLocale = Localization.getString("conformance.aaa");
902
+ this.statements.push({
903
+ id: "conformance.aaa",
904
+ compactString: aaaLocale.compact,
905
+ descriptiveString: aaaLocale.descriptive
906
+ });
907
+ } else if (profiles.some(profile => profile.isWCAGLevelAA)) {
908
+ const aaLocale = Localization.getString("conformance.aa");
909
+ this.statements.push({
910
+ id: "conformance.aa",
911
+ compactString: aaLocale.compact,
912
+ descriptiveString: aaLocale.descriptive
913
+ });
914
+ } else if (profiles.some(profile => profile.isWCAGLevelA)) {
915
+ const aLocale = Localization.getString("conformance.a");
916
+ this.statements.push({
917
+ id: "conformance.a",
918
+ compactString: aLocale.compact,
919
+ descriptiveString: aLocale.descriptive
920
+ });
921
+ } else {
922
+ const unknownLocale = Localization.getString("conformance.unknown-standard");
923
+ this.statements.push({
924
+ id: "conformance.unknown-standard",
925
+ compactString: unknownLocale.compact,
926
+ descriptiveString: unknownLocale.descriptive
927
+ });
928
+ }
929
+ }
930
+
931
+ public static fromPublication(publication: Publication): Conformance {
932
+ const profiles = publication.metadata.accessibility?.conformsTo ?? [];
933
+ return new Conformance(profiles);
934
+ }
935
+ }
936
+ /**
937
+ * Represents legal exemptions
938
+ */
939
+ export class Legal implements AccessibilityDisplayField {
940
+ public readonly id = "legal-considerations.title";
941
+ public readonly title: string;
942
+ public readonly shouldDisplay: boolean;
943
+ public readonly exemption: boolean;
944
+ public readonly statements: AccessibilityDisplayStatement[];
945
+
946
+ private constructor(
947
+ exemption: boolean = false
948
+ ) {
949
+ this.exemption = exemption;
950
+ this.shouldDisplay = this.exemption;
951
+
952
+ const titleLocale = Localization.getString(this.id);
953
+ this.title = titleLocale.compact;
954
+
955
+ this.statements = [];
956
+ if (exemption) {
957
+ const exemptLocale = Localization.getString("legal-considerations.exempt");
958
+ this.statements.push({
959
+ id: "legal-considerations.exempt",
960
+ compactString: exemptLocale.compact,
961
+ descriptiveString: exemptLocale.descriptive
962
+ });
963
+ } else {
964
+ const noMetadataLocale = Localization.getString("legal-considerations.no-metadata");
965
+ this.statements.push({
966
+ id: "legal-considerations.no-metadata",
967
+ compactString: noMetadataLocale.compact,
968
+ descriptiveString: noMetadataLocale.descriptive
969
+ });
970
+ }
971
+ }
972
+
973
+ public static fromPublication(publication: Publication): Legal {
974
+ const exemptions = publication.metadata.accessibility?.exemption ?? [];
975
+ const hasExemption = exemptions.length > 0;
976
+ return new Legal(hasExemption);
977
+ }
978
+ }
979
+
980
+ /**
981
+ * Represents the accessibility summary
982
+ */
983
+ export class AccessibilitySummary implements AccessibilityDisplayField {
984
+ public readonly id = "accessibility-summary.title";
985
+ public readonly title: string;
986
+ public readonly shouldDisplay: boolean;
987
+
988
+ public readonly statements: AccessibilityDisplayStatement[];
989
+
990
+ private constructor(publication: Publication) {
991
+ this.shouldDisplay = true;
992
+
993
+ const titleLocale = Localization.getString(this.id);
994
+ this.title = titleLocale.compact;
995
+
996
+ const summary = publication.metadata.accessibility?.summary;
997
+ this.statements = [];
998
+
999
+ if (this.shouldDisplay && summary) {
1000
+ this.statements.push({
1001
+ id: "accessibility-summary.summary",
1002
+ compactString: summary,
1003
+ descriptiveString: summary
1004
+ });
1005
+ } else {
1006
+ const locale = Localization.getString("accessibility-summary.no-metadata");
1007
+ this.statements.push({
1008
+ id: "accessibility-summary.no-metadata",
1009
+ compactString: locale.compact,
1010
+ descriptiveString: locale.descriptive
1011
+ });
1012
+ }
1013
+ }
1014
+
1015
+ public static fromPublication(publication: Publication): AccessibilitySummary {
1016
+ return new AccessibilitySummary(publication);
1017
+ }
1018
+ }
1019
+
1020
+ /**
1021
+ * Main accessibility metadata display guide
1022
+ */
1023
+ export class AccessibilityMetadataDisplayGuide {
1024
+ public readonly waysOfReading: WaysOfReading;
1025
+ public readonly navigation: Navigation;
1026
+ public readonly richContent: RichContent;
1027
+ public readonly additionalInformation: AdditionalInformation;
1028
+ public readonly hazards: Hazards;
1029
+ public readonly conformance: Conformance;
1030
+ public readonly legal: Legal;
1031
+ public readonly accessibilitySummary: AccessibilitySummary;
1032
+ public readonly fields: AccessibilityDisplayField[];
1033
+
1034
+ constructor(publication: Publication) {
1035
+ this.waysOfReading = WaysOfReading.fromPublication(publication);
1036
+ this.navigation = Navigation.fromPublication(publication);
1037
+ this.richContent = RichContent.fromPublication(publication);
1038
+ this.additionalInformation = AdditionalInformation.fromPublication(publication);
1039
+ this.hazards = Hazards.fromPublication(publication);
1040
+ this.conformance = Conformance.fromPublication(publication);
1041
+ this.legal = Legal.fromPublication(publication);
1042
+ this.accessibilitySummary = AccessibilitySummary.fromPublication(publication);
1043
+
1044
+ this.fields = [
1045
+ this.waysOfReading,
1046
+ this.navigation,
1047
+ this.richContent,
1048
+ this.additionalInformation,
1049
+ this.hazards,
1050
+ this.conformance,
1051
+ this.legal,
1052
+ this.accessibilitySummary
1053
+ ];
1054
+ }
1055
+ }