Skip to content

Commit 500aa9f

Browse files
authoredFeb 21, 2025··
Add EPUB a11y 1.1 Conformance Support (#195)
1 parent e903edc commit 500aa9f

File tree

7 files changed

+109
-13
lines changed

7 files changed

+109
-13
lines changed
 

‎CHANGELOG.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ All notable changes to this project will be documented in this file.
66

77
## [Unreleased]
88

9-
None
9+
### Added
10+
11+
- Support for [EPUB Accessibility 1.1](https://www.w3.org/TR/epub-a11y-11/) conformance values
12+
13+
### Changed
14+
15+
- A11y `conformsTo` values are now sorted from highest to lowest level
1016

1117
## [0.7.1] - 2025-02-07
1218

‎pkg/manifest/a11y.go

+61-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package manifest
22

33
import (
44
"encoding/json"
5+
"slices"
56

67
"github.com/pkg/errors"
78
"github.com/readium/go-toolkit/pkg/internal/extensions"
@@ -12,7 +13,7 @@ import (
1213
// https://www.w3.org/2021/a11y-discov-vocab/latest/
1314
// https://readium.org/webpub-manifest/schema/a11y.schema.json
1415
type A11y struct {
15-
ConformsTo []A11yProfile `json:"conformsTo,omitempty"` // An established standard to which the described resource conforms.
16+
ConformsTo A11yProfileList `json:"conformsTo,omitempty"` // An established standard to which the described resource conforms.
1617
Certification *A11yCertification `json:"certification,omitempty"` // Certification of accessible publications.
1718
Summary string `json:"summary,omitempty"` // A human-readable summary of specific accessibility features or deficiencies, consistent with the other accessibility metadata but expressing subtleties such as "short descriptions are present but long descriptions will be needed for non-visual users" or "short descriptions are present and no long descriptions are needed."
1819
AccessModes []A11yAccessMode `json:"accessMode,omitempty"` // The human sensory perceptual system or cognitive faculty through which a person may process or perceive information.
@@ -25,7 +26,7 @@ type A11y struct {
2526
// NewA11y creates a new empty A11y.
2627
func NewA11y() A11y {
2728
return A11y{
28-
ConformsTo: []A11yProfile{},
29+
ConformsTo: A11yProfileList{},
2930
AccessModes: []A11yAccessMode{},
3031
AccessModesSufficient: [][]A11yPrimaryAccessMode{},
3132
Features: []A11yFeature{},
@@ -47,6 +48,7 @@ func (a *A11y) Merge(other *A11y) {
4748
}
4849

4950
a.ConformsTo = extensions.AppendIfMissing(a.ConformsTo, other.ConformsTo...)
51+
a.ConformsTo.Sort()
5052

5153
if other.Certification != nil {
5254
a.Certification = other.Certification
@@ -86,6 +88,7 @@ func A11yFromJSON(rawJSON map[string]interface{}) (*A11y, error) {
8688
return nil, errors.Wrap(err, "failed unmarshalling 'conformsTo'")
8789
}
8890
a.ConformsTo = A11yProfilesFromStrings(conformsTo)
91+
a.ConformsTo.Sort()
8992

9093
if certJSON, ok := rawJSON["certification"].(map[string]interface{}); ok {
9194
c := A11yCertification{
@@ -171,14 +174,70 @@ const (
171174
EPUBA11y10WCAG20AA A11yProfile = "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"
172175
// EPUB Accessibility 1.0 - WCAG 2.0 Level AAA
173176
EPUBA11y10WCAG20AAA A11yProfile = "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa"
177+
// EPUB Accessibility 1.1 - WCAG 2.0 Level A
178+
EPUBA11y11WCAG20A A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a"
179+
// EPUB Accessibility 1.1 - WCAG 2.0 Level AA
180+
EPUBA11y11WCAG20AA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aa"
181+
// EPUB Accessibility 1.1 - WCAG 2.0 Level AAA
182+
EPUBA11y11WCAG20AAA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aaa"
183+
// EPUB Accessibility 1.1 - WCAG 2.1 Level A
184+
EPUBA11y11WCAG21A A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.1-a"
185+
// EPUB Accessibility 1.1 - WCAG 2.1 Level AA
186+
EPUBA11y11WCAG21AA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aa"
187+
// EPUB Accessibility 1.1 - WCAG 2.1 Level AAA
188+
EPUBA11y11WCAG21AAA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aaa"
189+
// EPUB Accessibility 1.1 - WCAG 2.2 Level A
190+
EPUBA11y11WCAG22A A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.2-a"
191+
// EPUB Accessibility 1.1 - WCAG 2.2 Level AA
192+
EPUBA11y11WCAG22AA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aa"
193+
// EPUB Accessibility 1.1 - WCAG 2.2 Level AAA
194+
EPUBA11y11WCAG22AAA A11yProfile = "https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aaa"
174195
)
175196

197+
// Used for sorting. Make sure to keep it up-to-date with the consts
198+
var a11yProfileRanking = map[A11yProfile]int{
199+
EPUBA11y10WCAG20A: 1,
200+
EPUBA11y10WCAG20AA: 2,
201+
EPUBA11y10WCAG20AAA: 3,
202+
EPUBA11y11WCAG20A: 4,
203+
EPUBA11y11WCAG20AA: 5,
204+
EPUBA11y11WCAG20AAA: 6,
205+
EPUBA11y11WCAG21A: 7,
206+
EPUBA11y11WCAG21AA: 8,
207+
EPUBA11y11WCAG21AAA: 9,
208+
EPUBA11y11WCAG22A: 10,
209+
EPUBA11y11WCAG22AA: 11,
210+
EPUBA11y11WCAG22AAA: 12,
211+
}
212+
176213
func A11yProfilesFromStrings(strings []string) []A11yProfile {
177214
return fromStrings(strings, func(str string) A11yProfile {
178215
return A11yProfile(str)
179216
})
180217
}
181218

219+
func (p A11yProfile) Compare(other A11yProfile) int {
220+
// Compare based on the compatibility level
221+
if p == other {
222+
return 0
223+
}
224+
225+
pRank := a11yProfileRanking[p]
226+
oRank := a11yProfileRanking[other]
227+
return oRank - pRank
228+
}
229+
230+
type A11yProfileList []A11yProfile
231+
232+
func (l A11yProfileList) Sort() {
233+
if len(l) <= 1 {
234+
return
235+
}
236+
slices.SortFunc(l, func(a, b A11yProfile) int {
237+
return a.Compare(b)
238+
})
239+
}
240+
182241
// A11yCertification represents a certification for an accessible publication.
183242
type A11yCertification struct {
184243
CertifiedBy string `json:"certifiedBy,omitempty"` // Identifies a party responsible for the testing and certification of the accessibility of a Publication.

‎pkg/manifest/a11y_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ func TestA11yUnmarshalConformsToArray(t *testing.T) {
8787
assert.Equal(t, e, m, "unmarshalled JSON object should be equal to A11y object")
8888
}
8989

90+
func TestA11ySortConformsTo(t *testing.T) {
91+
a := A11yProfileList{
92+
EPUBA11y10WCAG20A,
93+
EPUBA11y11WCAG22A,
94+
EPUBA11y11WCAG20AA,
95+
EPUBA11y11WCAG21AA,
96+
EPUBA11y11WCAG22AAA,
97+
}
98+
a.Sort()
99+
assert.Equal(t, A11yProfileList{
100+
EPUBA11y11WCAG22AAA,
101+
EPUBA11y11WCAG22A,
102+
EPUBA11y11WCAG21AA,
103+
EPUBA11y11WCAG20AA,
104+
EPUBA11y10WCAG20A,
105+
}, a)
106+
}
107+
90108
func TestA11yUnmarshalAccessModeSufficientContainingBothStringsAndArrays(t *testing.T) {
91109
var m A11y
92110
assert.NoError(t, json.Unmarshal([]byte(`{"accessModeSufficient": ["auditory", ["visual", "tactile"], [], "visual"]}`), &m))

‎pkg/parser/epub/metadata.go

+17-8
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ func (m PubMetadataAdapter) TDM() *manifest.TDM {
675675
}
676676

677677
func (m PubMetadataAdapter) a11yConformsTo() []manifest.A11yProfile {
678-
profiles := []manifest.A11yProfile{}
678+
profiles := manifest.A11yProfileList{}
679679

680680
if items, ok := m.items[VocabularyDCTerms+"conformsto"]; ok {
681681
for _, item := range items {
@@ -691,32 +691,41 @@ func (m PubMetadataAdapter) a11yConformsTo() []manifest.A11yProfile {
691691
}
692692
}
693693

694+
profiles.Sort()
694695
return profiles
695696
}
696697

697698
func a11yProfile(value string) manifest.A11yProfile {
698699
switch value {
699-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level A",
700-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
700+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
701701
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
702702
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
703703
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a":
704704
return manifest.EPUBA11y10WCAG20A
705705

706-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AA",
707-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
706+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
708707
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
709708
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
710709
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa":
711710
return manifest.EPUBA11y10WCAG20AA
712711

713-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA",
714-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
712+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
715713
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
716714
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
717715
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa":
718716
return manifest.EPUBA11y10WCAG20AAA
719-
717+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level A":
718+
return manifest.EPUBA11y11WCAG20A
719+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AA":
720+
return manifest.EPUBA11y11WCAG20AA
721+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA":
722+
return manifest.EPUBA11y11WCAG20AAA
723+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level A":
724+
return manifest.EPUBA11y11WCAG21A
725+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level AA":
726+
return manifest.EPUBA11y11WCAG21AA
727+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level AAA":
728+
return manifest.EPUBA11y11WCAG21AAA
720729
default:
721730
return ""
722731
}

‎pkg/parser/epub/metadata_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func TestMetadataEPUB2Accessibility(t *testing.T) {
309309
m, err := loadMetadata("accessibility-epub2")
310310
assert.NoError(t, err)
311311
e := manifest.NewA11y()
312-
e.ConformsTo = []manifest.A11yProfile{manifest.EPUBA11y10WCAG20A}
312+
e.ConformsTo = []manifest.A11yProfile{manifest.EPUBA11y11WCAG21AA, manifest.EPUBA11y11WCAG20AAA, manifest.EPUBA11y10WCAG20A}
313313
e.Certification = &manifest.A11yCertification{
314314
CertifiedBy: "Accessibility Testers Group",
315315
Credential: "DAISY OK",
@@ -341,7 +341,7 @@ func TestMetadataEPUB3Accessibility(t *testing.T) {
341341
m, err := loadMetadata("accessibility-epub3")
342342
assert.NoError(t, err)
343343
e := manifest.NewA11y()
344-
e.ConformsTo = []manifest.A11yProfile{manifest.EPUBA11y10WCAG20A}
344+
e.ConformsTo = []manifest.A11yProfile{manifest.EPUBA11y11WCAG21AA, manifest.EPUBA11y11WCAG20AAA, manifest.EPUBA11y10WCAG20A}
345345
e.Certification = &manifest.A11yCertification{
346346
CertifiedBy: "Accessibility Testers Group",
347347
Credential: "DAISY OK",

‎pkg/parser/epub/testdata/package/accessibility-epub2.opf

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
<dc:conformsTo>any profile</dc:conformsTo>
77
<dc:conformsTo>http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a</dc:conformsTo>
8+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.0 Level AAA</dc:conformsTo>
9+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.1 Level AA</dc:conformsTo>
810
<meta name="schema:accessibilitySummary" content="The publication contains structural and page navigation."/>
911

1012
<meta name="schema:accessMode" content="textual"/>

‎pkg/parser/epub/testdata/package/accessibility-epub3.opf

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
<dc:conformsTo>any profile</dc:conformsTo>
77
<link href="http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a" rel="dcterms:conformsTo"/>
8+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.0 Level AAA</dc:conformsTo>
9+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.1 Level AA</dc:conformsTo>
810
<meta property="schema:accessibilitySummary">The publication contains structural and page navigation.</meta>
911

1012
<meta property="schema:accessMode">textual</meta>

0 commit comments

Comments
 (0)
Please sign in to comment.