Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow ARIA headings in EDUPUB (legacy profile) #1502

Merged
merged 1 commit into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,92 +2,148 @@
<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
<ns uri="http://www.idpf.org/2007/ops" prefix="epub"/>
<ns uri="http://www.w3.org/1999/xhtml" prefix="html"/>

<!-- following variable declarations are used to test h# nesting depth -->
<!-- checks if the body contains anything other than a single section or article - i.e., is it an implied section
- previously tested if >1 article/section children with : or count(//html:body/child::html:*[self::html:article or self::html:section]) &gt; 1
but ambiguous whether multiple section elements is an implied body or just a weird breakup of the file
-->
<let name="body-is-section" value="exists(//html:body/(html:* except (html:article|html:section)))"/>

<let name="body-is-section"
value="exists(//html:body/(html:* except (html:article | html:section)))"/>

<!-- check if implied heading -->
<let name="body-label-len" value="string-length(normalize-space(//html:body/@aria-label))"/>

<!-- 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 -->
<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]"/>

<let name="topmost-heading"
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]"/>

<!-- extract the starting rank from the topmost-heading -->
<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"/>

<let name="topmost-heading-rank" value="
if ($body-label-len &gt; 0) then
1
else
if (exists($topmost-heading)) then
if ($topmost-heading[1][@role = 'heading']) then
if ($topmost-heading[1]/@aria-level) then
number($topmost-heading[1]/@aria-level)
else
2
else
number(substring(name($topmost-heading[1]), 2))
else
1"/>

<!-- find the nesting depth of the topmost heading (0 if body, 1 if a section or article around it) -->
<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"/>


<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"/>


<pattern id="edupub.headings">
<rule context="html:body[html:* except (html:article|html:section|html:aside|html:nav)]">
<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)]"/>

<rule context="html:body[html:* except (html:article | html:section | html:aside | html:nav)]">
<let name="headings"
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)]"/>

<report test="@aria-label and $body-label-len = 0">Empty aria-label attribute found.</report>

<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>


<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>

<!-- <report test="$arialabel-len &gt; 0 and count($headings) &gt; 0">The aria-label attribute must not be mixed with ranked headings.</report> -->

<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant of body.</report>

<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>

<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>

<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant
of body.</report>

<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>

<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>
</rule>
<rule context="html:section|html:article">


<rule context="html:section | html:article">
<let name="arialabel-len" value="string-length(normalize-space(@aria-label))"/>
<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()]"/>

<let name="headings"
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()]"/>

<report test="@aria-label and $arialabel-len = 0">Empty aria-label attribute found.</report>

<assert test="$arialabel-len &gt; 0 or count($headings) &gt; 0"><value-of select="name()"/> does not have a heading.</assert>


<assert test="$arialabel-len &gt; 0 or count($headings) &gt; 0"><value-of select="name()"/>
does not have a heading.</assert>

<!-- <report test="$arialabel-len &gt; 0 and count($headings) &gt; 0">The aria-label attribute must not be mixed with ranked headings.</report> -->

<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant of <value-of select="name()"/>.</report>

<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>

<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>

<report test="count($headings) &gt; 1">More than one ranked heading found as direct descendant
of <value-of select="name()"/>.</report>

<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>

<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>
</rule>

<rule context="html:h1|html:h2|html:h3|html:h4|html:h5|html:h6">

<rule
context="html:h1 | html:h2 | html:h3 | html:h4 | html:h5 | html:h6 | html:*[@role = 'heading']">
<!-- get the # from the h# tag found -->
<let name="current-rank" value="number(substring(name(current()),2))"/>

<let name="current-rank" value="
if (current()[@role = 'heading']) then
if (current()/@aria-level) then
number(current()/@aria-level)
else
2
else
number(substring(name(current()), 2))"/>

<!-- find nesting depth -->
<let name="current-nesting" value="count(ancestor::html:section|ancestor::html:article|ancestor::html:aside|ancestor::html:nav)"/>

<let name="current-nesting"
value="count(ancestor::html:section | ancestor::html:article | ancestor::html:aside | ancestor::html:nav)"/>

<!-- derive the expected rank of this heading from the implied body or sectioning -->
<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"/>

<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"/>

<!-- report ranked headings in sectioning roots -->
<report test="ancestor::html:figure or ancestor::html:blockquote">Ranked headings are not valid in figure or blockquote</report>

<report test="ancestor::html:figure or ancestor::html:blockquote">Ranked headings are not
valid in figure or blockquote</report>

<!-- if the expected rank is below 6, check that it matches what is expected -->
<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>

<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>

<!-- otherwise, just stop testing after 5 and report any headings that aren't six, since no higher exist -->
<report test="$expected-rank &gt; 5 and $current-rank &lt; 6">The current heading rank should be h6.</report>
<report test="$expected-rank &gt; 5 and $current-rank &lt; 6">The current heading rank should
be h6.</report>
</rule>
</pattern>

<pattern id="edupub.sectioning">
<rule context="*[parent::html:body or parent::html:section][not(self::html:section)]">
<report test="preceding-sibling::html:section">Non-section elements not allowed between or after section elements.</report>
<report test="preceding-sibling::html:section">Non-section elements not allowed between or
after section elements.</report>
</rule>
</pattern>

<pattern id="edupub.subtitles">
<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]]">
<assert test="ancestor::html:header">Section subtitles must be wrapped in a header element.</assert>
<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]]">
<assert test="ancestor::html:header">Section subtitles must be wrapped in a header
element.</assert>
</rule>
</pattern>
</schema>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ Feature: EPUB for Education ▸ XHTML Content Document Checks
And the message contains 'The body element requires a heading when it is used as an implied section'
And no other errors or warnings are reported

Scenario: Report a missing section heading
When checking document 'edupub-heading-missing-error.xhtml'
Then error RSC-005 is reported
And the message contains 'section does not have a heading'
But no other errors or warnings are reported

Scenario: Allow a section heading specified as ARIA heading role
When checking document 'edupub-heading-aria-role-valid.xhtml'
Then no errors or warnings are reported

Scenario: Verify a heading with only an `img` that has alternative text
When checking document 'edupub-heading-img-alt-valid.xhtml'
Then no errors or warnings are reported

Scenario: Report a heading with only an `img` without alternative text
When checking document 'edupub-heading-img-no-alt-error.xhtml'
Then error RSC-005 is reported
And the message contains 'Empty ranked heading detected'
And no other errors or warnings are reported

## 4.3 Titles and Headings

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


# No matching section

Scenario: Verify a heading with only an `img` that has alternative text
When checking document 'edupub-heading-img-alt-valid.xhtml'
Then no errors or warnings are reported

Scenario: Report a heading with only an `img` without alternative text
When checking document 'edupub-heading-img-no-alt-error.xhtml'
Then error RSC-005 is reported
And the message contains 'Empty ranked heading detected'
And no other errors or warnings are reported

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Test</title>
<meta charset="UTF-8" />
</head>
<body>
<section>
<span aria-level="1" role="heading">Heading</span>
</section>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Test</title>
<meta charset="UTF-8" />
</head>
<body>
<section>
<!--<h1>Test</h1>-->
</section>
</body>
</html>