Skip to content

Commit da4f541

Browse files
committed
fix: allow ARIA headings in EDUPUB (legacy profile)
This commit fixes the legacy EDUPUB checks by allowing ARIA headings along with native h1-h6 headings. Note: EDUPUB support is a legacy feature of EPUBCheck, since the standard is not actively maintained. Fix #1483
1 parent 199da0b commit da4f541

File tree

4 files changed

+154
-67
lines changed

4 files changed

+154
-67
lines changed

src/main/resources/com/adobe/epubcheck/schema/30/edupub/edu-structure.sch

Lines changed: 111 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,148 @@
22
<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
33
<ns uri="http://www.idpf.org/2007/ops" prefix="epub"/>
44
<ns uri="http://www.w3.org/1999/xhtml" prefix="html"/>
5-
5+
66
<!-- following variable declarations are used to test h# nesting depth -->
77
<!-- checks if the body contains anything other than a single section or article - i.e., is it an implied section
88
- previously tested if >1 article/section children with : or count(//html:body/child::html:*[self::html:article or self::html:section]) &gt; 1
99
but ambiguous whether multiple section elements is an implied body or just a weird breakup of the file
1010
-->
11-
<let name="body-is-section" value="exists(//html:body/(html:* except (html:article|html:section)))"/>
12-
11+
<let name="body-is-section"
12+
value="exists(//html:body/(html:* except (html:article | html:section)))"/>
13+
1314
<!-- check if implied heading -->
1415
<let name="body-label-len" value="string-length(normalize-space(//html:body/@aria-label))"/>
15-
16+
1617
<!-- finds the topmost heading in the file that is the descendant of the body (not sectioning element ancestors) or the first descendant of a section or article -->
17-
<let name="topmost-heading" value="//html:body//(html:h1|html:h2|html:h3|html:h4|html:h5|html:h6)[not(ancestor::html:aside|ancestor::html:nav) and count(ancestor::html:section|ancestor::html:article) le 1]"/>
18-
18+
<let name="topmost-heading"
19+
value="//html:body//(html:h1 | html:h2 | html:h3 | html:h4 | html:h5 | html:h6 | html:*[@role = 'heading'])[not(ancestor::html:aside | ancestor::html:nav) and count(ancestor::html:section | ancestor::html:article) le 1]"/>
20+
1921
<!-- extract the starting rank from the topmost-heading -->
20-
<let name="topmost-heading-rank" value="if ($body-label-len &gt; 0) then 1 else if (exists($topmost-heading)) then number(substring(name($topmost-heading[1]),2)) else 1"/>
21-
22+
<let name="topmost-heading-rank" value="
23+
if ($body-label-len &gt; 0) then
24+
1
25+
else
26+
if (exists($topmost-heading)) then
27+
if ($topmost-heading[1][@role = 'heading']) then
28+
if ($topmost-heading[1]/@aria-level) then
29+
number($topmost-heading[1]/@aria-level)
30+
else
31+
2
32+
else
33+
number(substring(name($topmost-heading[1]), 2))
34+
else
35+
1"/>
36+
2237
<!-- find the nesting depth of the topmost heading (0 if body, 1 if a section or article around it) -->
23-
<let name="topmost-heading-nest" value="if ($body-label-len &gt; 0) then 0 else if (empty($topmost-heading[1]/(ancestor::html:section|ancestor::html:article|ancestor::html:nav))) then 0 else 1"/>
24-
25-
38+
<let name="topmost-heading-nest" value="
39+
if ($body-label-len &gt; 0) then
40+
0
41+
else
42+
if (empty($topmost-heading[1]/(ancestor::html:section | ancestor::html:article | ancestor::html:nav))) then
43+
0
44+
else
45+
1"/>
46+
47+
2648
<pattern id="edupub.headings">
27-
<rule context="html:body[html:* except (html:article|html:section|html:aside|html:nav)]">
28-
<let name="headings" value=".//(html:h1|html:h2|html:h3|html:h4|html:h5|html:h6)[empty(ancestor::html:section|ancestor::html:aside|ancestor::html:article|ancestor::html:nav)]"/>
29-
49+
<rule context="html:body[html:* except (html:article | html:section | html:aside | html:nav)]">
50+
<let name="headings"
51+
value=".//(html:h1 | html:h2 | html:h3 | html:h4 | html:h5 | html:h6 | html:*[@role = 'heading'])[empty(ancestor::html:section | ancestor::html:aside | ancestor::html:article | ancestor::html:nav)]"/>
52+
3053
<report test="@aria-label and $body-label-len = 0">Empty aria-label attribute found.</report>
31-
32-
<assert test="$body-label-len &gt; 0 or count($headings) &gt; 0">The body element requires a heading when it is used as an implied section.</assert>
33-
54+
55+
<assert test="$body-label-len &gt; 0 or count($headings) &gt; 0">The body element requires a
56+
heading when it is used as an implied section.</assert>
57+
3458
<!-- <report test="$arialabel-len &gt; 0 and count($headings) &gt; 0">The aria-label attribute must not be mixed with ranked headings.</report> -->
35-
36-
<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant of body.</report>
37-
38-
<report test="count($headings) = 1 and string-length(normalize-space(string-join($headings|$headings/html:img/@alt|$headings//@aria-label))) = 0">Empty ranked heading detected.</report>
39-
40-
<report test="@aria-label and (normalize-space($headings) = normalize-space(@aria-label))">The value of the "aria-label" attribute must not be the same as the content of the heading.</report>
59+
60+
<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant
61+
of body.</report>
62+
63+
<report
64+
test="count($headings) = 1 and string-length(normalize-space(string-join($headings | $headings/html:img/@alt | $headings//@aria-label))) = 0"
65+
>Empty ranked heading detected.</report>
66+
67+
<report test="@aria-label and (normalize-space($headings) = normalize-space(@aria-label))">The
68+
value of the "aria-label" attribute must not be the same as the content of the
69+
heading.</report>
4170
</rule>
42-
43-
44-
<rule context="html:section|html:article">
71+
72+
73+
<rule context="html:section | html:article">
4574
<let name="arialabel-len" value="string-length(normalize-space(@aria-label))"/>
46-
<let name="headings" value=".//(html:h1|html:h2|html:h3|html:h4|html:h5|html:h6)[(ancestor::html:section|ancestor::html:article|ancestor::html:aside|ancestor::html:nav)[last()] = current()]"/>
47-
75+
<let name="headings"
76+
value=".//(html:h1 | html:h2 | html:h3 | html:h4 | html:h5 | html:h6 | html:*[@role = 'heading'])[(ancestor::html:section | ancestor::html:article | ancestor::html:aside | ancestor::html:nav)[last()] = current()]"/>
77+
4878
<report test="@aria-label and $arialabel-len = 0">Empty aria-label attribute found.</report>
49-
50-
<assert test="$arialabel-len &gt; 0 or count($headings) &gt; 0"><value-of select="name()"/> does not have a heading.</assert>
51-
79+
80+
<assert test="$arialabel-len &gt; 0 or count($headings) &gt; 0"><value-of select="name()"/>
81+
does not have a heading.</assert>
82+
5283
<!-- <report test="$arialabel-len &gt; 0 and count($headings) &gt; 0">The aria-label attribute must not be mixed with ranked headings.</report> -->
53-
54-
<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant of <value-of select="name()"/>.</report>
55-
56-
<report test="count($headings) = 1 and string-length(normalize-space(string-join($headings|$headings/html:img/@alt|$headings//@aria-label))) = 0">Empty ranked heading detected.</report>
57-
58-
<report test="@aria-label and (normalize-space($headings) = normalize-space(@aria-label))">The value of the "aria-label" attribute must not be the same as the content of the heading.</report>
84+
85+
<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant
86+
of <value-of select="name()"/>.</report>
87+
88+
<report
89+
test="count($headings) = 1 and string-length(normalize-space(string-join($headings | $headings/html:img/@alt | $headings//@aria-label))) = 0"
90+
>Empty ranked heading detected.</report>
91+
92+
<report test="@aria-label and (normalize-space($headings) = normalize-space(@aria-label))">The
93+
value of the "aria-label" attribute must not be the same as the content of the
94+
heading.</report>
5995
</rule>
60-
61-
<rule context="html:h1|html:h2|html:h3|html:h4|html:h5|html:h6">
96+
97+
<rule
98+
context="html:h1 | html:h2 | html:h3 | html:h4 | html:h5 | html:h6 | html:*[@role = 'heading']">
6299
<!-- get the # from the h# tag found -->
63-
<let name="current-rank" value="number(substring(name(current()),2))"/>
64-
100+
<let name="current-rank" value="
101+
if (current()[@role = 'heading']) then
102+
if (current()/@aria-level) then
103+
number(current()/@aria-level)
104+
else
105+
2
106+
else
107+
number(substring(name(current()), 2))"/>
108+
65109
<!-- find nesting depth -->
66-
<let name="current-nesting" value="count(ancestor::html:section|ancestor::html:article|ancestor::html:aside|ancestor::html:nav)"/>
67-
110+
<let name="current-nesting"
111+
value="count(ancestor::html:section | ancestor::html:article | ancestor::html:aside | ancestor::html:nav)"/>
112+
68113
<!-- derive the expected rank of this heading from the implied body or sectioning -->
69-
<let name="expected-rank" value="if ($body-is-section) then $topmost-heading-rank - $topmost-heading-nest + $current-nesting else $topmost-heading-rank + $current-nesting - 1"/>
70-
114+
<let name="expected-rank" value="
115+
if ($body-is-section) then
116+
$topmost-heading-rank - $topmost-heading-nest + $current-nesting
117+
else
118+
$topmost-heading-rank + $current-nesting - 1"/>
119+
71120
<!-- report ranked headings in sectioning roots -->
72-
<report test="ancestor::html:figure or ancestor::html:blockquote">Ranked headings are not valid in figure or blockquote</report>
73-
121+
<report test="ancestor::html:figure or ancestor::html:blockquote">Ranked headings are not
122+
valid in figure or blockquote</report>
123+
74124
<!-- if the expected rank is below 6, check that it matches what is expected -->
75-
<report test="$expected-rank &lt; 6 and not($current-rank = $expected-rank)">The heading rank h<value-of select="$current-rank"/> does not match the current nesting level (<value-of select="$expected-rank"/>).</report>
76-
125+
<report test="$expected-rank &lt; 6 and not($current-rank = $expected-rank)">The heading rank
126+
h<value-of select="$current-rank"/> does not match the current nesting level (<value-of
127+
select="$expected-rank"/>).</report>
128+
77129
<!-- otherwise, just stop testing after 5 and report any headings that aren't six, since no higher exist -->
78-
<report test="$expected-rank &gt; 5 and $current-rank &lt; 6">The current heading rank should be h6.</report>
130+
<report test="$expected-rank &gt; 5 and $current-rank &lt; 6">The current heading rank should
131+
be h6.</report>
79132
</rule>
80133
</pattern>
81-
134+
82135
<pattern id="edupub.sectioning">
83136
<rule context="*[parent::html:body or parent::html:section][not(self::html:section)]">
84-
<report test="preceding-sibling::html:section">Non-section elements not allowed between or after section elements.</report>
137+
<report test="preceding-sibling::html:section">Non-section elements not allowed between or
138+
after section elements.</report>
85139
</rule>
86140
</pattern>
87-
141+
88142
<pattern id="edupub.subtitles">
89-
<rule context="html:p[@epub:type='subtitle'][preceding-sibling::*[self::html:h1|self::html:h2|self::html:h3|self::html:h4|self::html:h5|self::html:h6]]">
90-
<assert test="ancestor::html:header">Section subtitles must be wrapped in a header element.</assert>
143+
<rule
144+
context="html:p[@epub:type = 'subtitle'][preceding-sibling::*[self::html:h1 | self::html:h2 | self::html:h3 | self::html:h4 | self::html:h5 | self::html:h6]]">
145+
<assert test="ancestor::html:header">Section subtitles must be wrapped in a header
146+
element.</assert>
91147
</rule>
92148
</pattern>
93149
</schema>

src/test/resources/epub-edupub/edupub-content-document-xhtml.feature

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ Feature: EPUB for Education ▸ XHTML Content Document Checks
4141
And the message contains 'The body element requires a heading when it is used as an implied section'
4242
And no other errors or warnings are reported
4343

44+
Scenario: Report a missing section heading
45+
When checking document 'edupub-heading-missing-error.xhtml'
46+
Then error RSC-005 is reported
47+
And the message contains 'section does not have a heading'
48+
But no other errors or warnings are reported
49+
50+
Scenario: Allow a section heading specified as ARIA heading role
51+
When checking document 'edupub-heading-aria-role-valid.xhtml'
52+
Then no errors or warnings are reported
53+
54+
Scenario: Verify a heading with only an `img` that has alternative text
55+
When checking document 'edupub-heading-img-alt-valid.xhtml'
56+
Then no errors or warnings are reported
57+
58+
Scenario: Report a heading with only an `img` without alternative text
59+
When checking document 'edupub-heading-img-no-alt-error.xhtml'
60+
Then error RSC-005 is reported
61+
And the message contains 'Empty ranked heading detected'
62+
And no other errors or warnings are reported
4463

4564
## 4.3 Titles and Headings
4665

@@ -84,15 +103,3 @@ Feature: EPUB for Education ▸ XHTML Content Document Checks
84103
And no other errors or warnings are reported
85104

86105

87-
# No matching section
88-
89-
Scenario: Verify a heading with only an `img` that has alternative text
90-
When checking document 'edupub-heading-img-alt-valid.xhtml'
91-
Then no errors or warnings are reported
92-
93-
Scenario: Report a heading with only an `img` without alternative text
94-
When checking document 'edupub-heading-img-no-alt-error.xhtml'
95-
Then error RSC-005 is reported
96-
And the message contains 'Empty ranked heading detected'
97-
And no other errors or warnings are reported
98-
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3+
<head>
4+
<title>Test</title>
5+
<meta charset="UTF-8" />
6+
</head>
7+
<body>
8+
<section>
9+
<span aria-level="1" role="heading">Heading</span>
10+
</section>
11+
</body>
12+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3+
<head>
4+
<title>Test</title>
5+
<meta charset="UTF-8" />
6+
</head>
7+
<body>
8+
<section>
9+
<!--<h1>Test</h1>-->
10+
</section>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)