From cfe20ba21a9942684262026314414fea87bf92f0 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 3 Feb 2026 22:51:31 +0200 Subject: [PATCH 1/5] Add tests --- pkg/manifest/a11y_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/manifest/a11y_test.go b/pkg/manifest/a11y_test.go index bf4fcea..13ff152 100644 --- a/pkg/manifest/a11y_test.go +++ b/pkg/manifest/a11y_test.go @@ -177,3 +177,33 @@ func TestA11yMarshalFullJSON(t *testing.T) { []byte(`{"conformsTo":["http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a","https://profile2"],"certification":{"certifiedBy":"company1","credential":"credential1","report":"https://report1"},"summary":"Summary","accessMode":["auditory","chartOnVisual"],"accessModeSufficient":[["auditory"],["visual","tactile"],["visual"]],"feature":["readingOrder","alternativeText"],"hazard":["flashing","motionSimulation"],"exemption":["eaa-fundamental-alteration","eaa-microenterprise"]}`), ) } + +func TestA11yMarshalConformsToSingleValue(t *testing.T) { + m := A11y{ + ConformsTo: []A11yProfile{ + EPUBA11y11WCAG22AA, + }, + } + data, err := json.Marshal(m) + require.NoError(t, err) + assert.Equal( + t, + []byte(`{"conformsTo":"https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aa"}`), + data, + ) +} + +func TestA11yMarshalAccessModeSufficientSingleValue(t *testing.T) { + m := A11y{ + AccessModesSufficient: A11yPrimaryAccessModeList{ + {A11yPrimaryAccessModeTextual}, + }, + } + data, err := json.Marshal(m) + require.NoError(t, err) + assert.Equal( + t, + []byte(`{"accessModeSufficient":"textual"}`), + data, + ) +} From fb9a046c083d1c738d6e4374245c76d3ea229db4 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 3 Feb 2026 22:52:01 +0200 Subject: [PATCH 2/5] Add custom type for list of lists of `A11yPrimaryAccessMode` This is needed to have a custom `MarshalJSON` implementation to marshal e.g. `["textual"]` as `"textual"`. Addresses #281 --- pkg/manifest/a11y.go | 17 ++++++++++++++--- pkg/manifest/a11y_test.go | 8 ++++---- pkg/parser/epub/metadata.go | 4 ++-- pkg/parser/epub/metadata_test.go | 6 +++--- pkg/streamer/a11y_infer_test.go | 2 +- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pkg/manifest/a11y.go b/pkg/manifest/a11y.go index 69d00be..20bd581 100644 --- a/pkg/manifest/a11y.go +++ b/pkg/manifest/a11y.go @@ -17,7 +17,7 @@ type A11y struct { Certification *A11yCertification `json:"certification,omitempty"` // Certification of accessible publications. 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." AccessModes []A11yAccessMode `json:"accessMode,omitempty"` // The human sensory perceptual system or cognitive faculty through which a person may process or perceive information. - AccessModesSufficient [][]A11yPrimaryAccessMode `json:"accessModeSufficient,omitempty"` // A list of single or combined accessModes that are sufficient to understand all the intellectual content of a resource. + AccessModesSufficient A11yPrimaryAccessModeList `json:"accessModeSufficient,omitempty"` // A list of single or combined accessModes that are sufficient to understand all the intellectual content of a resource. Features []A11yFeature `json:"feature,omitempty"` // Content features of the resource, such as accessible media, alternatives and supported enhancements for accessibility. Hazards []A11yHazard `json:"hazard,omitempty"` // A characteristic of the described resource that is physiologically dangerous to some users. Exemptions []A11yExemption `json:"exemption,omitempty"` // Justifications for non-conformance based on exemptions in a given jurisdiction. @@ -28,7 +28,7 @@ func NewA11y() A11y { return A11y{ ConformsTo: A11yProfileList{}, AccessModes: []A11yAccessMode{}, - AccessModesSufficient: [][]A11yPrimaryAccessMode{}, + AccessModesSufficient: A11yPrimaryAccessModeList{}, Features: []A11yFeature{}, Hazards: []A11yHazard{}, Exemptions: []A11yExemption{}, @@ -109,7 +109,7 @@ func A11yFromJSON(rawJSON map[string]interface{}) (*A11y, error) { } a.AccessModes = A11yAccessModesFromStrings(accessModes) - ams := [][]A11yPrimaryAccessMode{} + ams := A11yPrimaryAccessModeList{} if amsJSON, ok := rawJSON["accessModeSufficient"].([]interface{}); ok { for _, l := range amsJSON { strings, err := parseSliceOrString(l, true) @@ -300,6 +300,17 @@ func A11yAccessModesFromStrings(strings []string) []A11yAccessMode { // A11yPrimaryAccessMode is a human primary sensory perceptual system or cognitive faculty through which a person may process or perceive information. type A11yPrimaryAccessMode string +type A11yPrimaryAccessModeList [][]A11yPrimaryAccessMode + +func (l A11yPrimaryAccessModeList) MarshalJSON() ([]byte, error) { + if len(l) == 1 && len(l[0]) == 1 { + return json.Marshal(l[0][0]) + } + + type alias A11yPrimaryAccessModeList + return json.Marshal(alias(l)) +} + const ( // Indicates that auditory perception is necessary to consume the information. A11yPrimaryAccessModeAuditory A11yPrimaryAccessMode = "auditory" diff --git a/pkg/manifest/a11y_test.go b/pkg/manifest/a11y_test.go index 13ff152..1507a9c 100644 --- a/pkg/manifest/a11y_test.go +++ b/pkg/manifest/a11y_test.go @@ -45,7 +45,7 @@ func TestA11yUnmarshalFullJSON(t *testing.T) { A11yAccessModeAuditory, A11yAccessModeChartOnVisual, }, - AccessModesSufficient: [][]A11yPrimaryAccessMode{ + AccessModesSufficient: A11yPrimaryAccessModeList{ { A11yPrimaryAccessModeVisual, A11yPrimaryAccessModeTactile, @@ -110,7 +110,7 @@ func TestA11yUnmarshalAccessModeSufficientContainingBothStringsAndArrays(t *test var m A11y require.NoError(t, json.Unmarshal([]byte(`{"accessModeSufficient": ["auditory", ["visual", "tactile"], [], "visual"]}`), &m)) var e A11y = NewA11y() - e.AccessModesSufficient = [][]A11yPrimaryAccessMode{ + e.AccessModesSufficient = A11yPrimaryAccessModeList{ {A11yPrimaryAccessModeAuditory}, {A11yPrimaryAccessModeVisual, A11yPrimaryAccessModeTactile}, {A11yPrimaryAccessModeVisual}, @@ -122,7 +122,7 @@ func TestA11yMarshalMinimalJSON(t *testing.T) { m := A11y{ ConformsTo: []A11yProfile{}, AccessModes: []A11yAccessMode{}, - AccessModesSufficient: [][]A11yPrimaryAccessMode{}, + AccessModesSufficient: A11yPrimaryAccessModeList{}, Features: []A11yFeature{}, Hazards: []A11yHazard{}, Exemptions: []A11yExemption{}, @@ -148,7 +148,7 @@ func TestA11yMarshalFullJSON(t *testing.T) { A11yAccessModeAuditory, A11yAccessModeChartOnVisual, }, - AccessModesSufficient: [][]A11yPrimaryAccessMode{ + AccessModesSufficient: A11yPrimaryAccessModeList{ {A11yPrimaryAccessModeAuditory}, { A11yPrimaryAccessModeVisual, diff --git a/pkg/parser/epub/metadata.go b/pkg/parser/epub/metadata.go index d2590fe..2960ae8 100644 --- a/pkg/parser/epub/metadata.go +++ b/pkg/parser/epub/metadata.go @@ -819,8 +819,8 @@ func valuesToAccessModes(values []string) []manifest.A11yAccessMode { return am } -func valuesToAccessModesSufficient(values []string) [][]manifest.A11yPrimaryAccessMode { - ams := make([][]manifest.A11yPrimaryAccessMode, 0, len(values)) +func valuesToAccessModesSufficient(values []string) manifest.A11yPrimaryAccessModeList { + ams := make(manifest.A11yPrimaryAccessModeList, 0, len(values)) for _, v := range values { c := a11yAccessModesSufficient(v) if len(c) > 0 { diff --git a/pkg/parser/epub/metadata_test.go b/pkg/parser/epub/metadata_test.go index fefc650..c510f93 100644 --- a/pkg/parser/epub/metadata_test.go +++ b/pkg/parser/epub/metadata_test.go @@ -319,7 +319,7 @@ func TestMetadataEPUB2Accessibility(t *testing.T) { } e.Summary = "The publication contains structural and page navigation." e.AccessModes = []manifest.A11yAccessMode{manifest.A11yAccessModeTextual, manifest.A11yAccessModeVisual} - e.AccessModesSufficient = [][]manifest.A11yPrimaryAccessMode{ + e.AccessModesSufficient = manifest.A11yPrimaryAccessModeList{ {manifest.A11yPrimaryAccessModeTextual}, {manifest.A11yPrimaryAccessModeTextual, manifest.A11yPrimaryAccessModeVisual}, } @@ -351,7 +351,7 @@ func TestMetadataEPUB3Accessibility(t *testing.T) { } e.Summary = "The publication contains structural and page navigation." e.AccessModes = []manifest.A11yAccessMode{manifest.A11yAccessModeTextual, manifest.A11yAccessModeVisual} - e.AccessModesSufficient = [][]manifest.A11yPrimaryAccessMode{ + e.AccessModesSufficient = manifest.A11yPrimaryAccessModeList{ {manifest.A11yPrimaryAccessModeTextual}, {manifest.A11yPrimaryAccessModeTextual, manifest.A11yPrimaryAccessModeVisual}, } @@ -372,7 +372,7 @@ func TestMetadataEPUB3AccessibilityRefines(t *testing.T) { CertifiedBy: "Standard Ebooks", } e.AccessModes = manifest.A11yAccessModesFromStrings([]string{"textual"}) - e.AccessModesSufficient = [][]manifest.A11yPrimaryAccessMode{ + e.AccessModesSufficient = manifest.A11yPrimaryAccessModeList{ a11yAccessModesSufficient("textual"), } e.Features = valuesToA11yFeatures([]string{"readingOrder", "structuralNavigation", "tableOfContents", "unlocked"}) diff --git a/pkg/streamer/a11y_infer_test.go b/pkg/streamer/a11y_infer_test.go index 177c4c3..f969f2a 100644 --- a/pkg/streamer/a11y_infer_test.go +++ b/pkg/streamer/a11y_infer_test.go @@ -29,7 +29,7 @@ func TestReturnsAdditionalInferredA11yMetadata(t *testing.T) { inferreddA11y := manifest.NewA11y() inferreddA11y.AccessModes = []manifest.A11yAccessMode{manifest.A11yAccessModeTextual} - inferreddA11y.AccessModesSufficient = [][]manifest.A11yPrimaryAccessMode{{manifest.A11yPrimaryAccessModeTextual}} + inferreddA11y.AccessModesSufficient = manifest.A11yPrimaryAccessModeList{{manifest.A11yPrimaryAccessModeTextual}} res, err := inferA11yMetadataInPublicationManifest(context.TODO(), pub.New(m, nil, nil), nil) require.NoError(t, err) From 5b9cc4b802a7bd4a13f55271a67becacdccdfa0e Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 3 Feb 2026 22:55:17 +0200 Subject: [PATCH 3/5] Remove leading whitespace in field comment --- pkg/manifest/a11y.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/manifest/a11y.go b/pkg/manifest/a11y.go index 20bd581..3ad52d6 100644 --- a/pkg/manifest/a11y.go +++ b/pkg/manifest/a11y.go @@ -17,7 +17,7 @@ type A11y struct { Certification *A11yCertification `json:"certification,omitempty"` // Certification of accessible publications. 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." AccessModes []A11yAccessMode `json:"accessMode,omitempty"` // The human sensory perceptual system or cognitive faculty through which a person may process or perceive information. - AccessModesSufficient A11yPrimaryAccessModeList `json:"accessModeSufficient,omitempty"` // A list of single or combined accessModes that are sufficient to understand all the intellectual content of a resource. + AccessModesSufficient A11yPrimaryAccessModeList `json:"accessModeSufficient,omitempty"` // A list of single or combined accessModes that are sufficient to understand all the intellectual content of a resource. Features []A11yFeature `json:"feature,omitempty"` // Content features of the resource, such as accessible media, alternatives and supported enhancements for accessibility. Hazards []A11yHazard `json:"hazard,omitempty"` // A characteristic of the described resource that is physiologically dangerous to some users. Exemptions []A11yExemption `json:"exemption,omitempty"` // Justifications for non-conformance based on exemptions in a given jurisdiction. From 00fe7a96dadf51cba7de06e3ce2baae2173c0152 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 3 Feb 2026 22:58:36 +0200 Subject: [PATCH 4/5] Add a custom `MarshalJSON` implementation to `A11yProfileList` This implementation allows to render a profile list of a single value as a string. Addresses #281 --- pkg/manifest/a11y.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/manifest/a11y.go b/pkg/manifest/a11y.go index 3ad52d6..278e16a 100644 --- a/pkg/manifest/a11y.go +++ b/pkg/manifest/a11y.go @@ -229,6 +229,15 @@ func (p A11yProfile) Compare(other A11yProfile) int { type A11yProfileList []A11yProfile +func (l A11yProfileList) MarshalJSON() ([]byte, error) { + if len(l) == 1 { + return json.Marshal(l[0]) + } + + type alias A11yProfileList + return json.Marshal(alias(l)) +} + func (l A11yProfileList) Sort() { if len(l) <= 1 { return From 3ed91c40f52fcc9350ecda9e952b6847b8febcf5 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 3 Feb 2026 23:03:50 +0200 Subject: [PATCH 5/5] Update expected test case result The expected result changed after adding a custom `MarshalJSON` to `A11yProfileList` to marshal a single value as a string. --- pkg/manifest/metadata_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/manifest/metadata_test.go b/pkg/manifest/metadata_test.go index 9d5525b..47c4e8f 100644 --- a/pkg/manifest/metadata_test.go +++ b/pkg/manifest/metadata_test.go @@ -297,7 +297,7 @@ func TestMetadataFullJSON(t *testing.T) { ], "title": {"en": "Title", "fr": "Titre"}, "subtitle": {"en": "Subtitle", "fr": "Sous-titre"}, - "accessibility": {"conformsTo": ["http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"]}, + "accessibility": {"conformsTo": "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"}, "tdm": {"policy": "https://provider.com/policies/policy.json", "reservation": "all"}, "modified": "2001-01-01T12:36:27.123Z", "published": "2001-01-02T12:36:27Z",