Skip to content

Commit

Permalink
improvements to styles and implementations (#41)
Browse files Browse the repository at this point in the history
* reconfigure post processing

* tighten css display

* toc, summary template updates

* rm redundant toc entries

* header and toc resources can be defined ahead fo time

* improve toc creaTION

* add activity log label to dom

* change the line height so pre respects line height

* change include logic statements so they are slightly less confusing

* change how anchor is exposed

* tweaks to overflow

* add description to meta

* triple A text

* comment

* rm bad css

* per request add more font size options to the settings

* rm aria describedby

* add the ability to suppress scrolling for better digital magnifier experience

* configure visibility switch

* add fullscreen button

* better dark mode handling

* name horizontal scrolling and change impl accordingly

* wrap pre for easier navigation without horizontal scroling

* default to showing visibility widget

* always include slide type

* wrap textarea

* fullscreen button

* textarea resize

* with resize fix

* <fit cell width when horizotnally expanded

* reorder options and leave note that this should be configurable

* more scroll handling. what a suck of a day

* tighten up execution counts

* fix bad label, tests run local
  • Loading branch information
tonyfast authored Mar 4, 2024
1 parent 4a85788 commit 1c7b051
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 106 deletions.
81 changes: 38 additions & 43 deletions nbconvert_a11y/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ class A11yExporter(PostProcess, HTMLExporter):
include_toc = Bool(
True, help="collect a table of contents of the headings in the document"
).tag(config=True)
include_summary = Bool(
True, help="collect notebook properties into a summary"
).tag(config=True)
include_summary = Bool(True, help="collect notebook properties into a summary").tag(config=True)
wcag_priority = Enum(
["AAA", "AA", "A"], "AA", help="the default inital wcag priority to start with"
).tag(config=True)
Expand All @@ -124,11 +122,11 @@ class A11yExporter(PostProcess, HTMLExporter):
include_cell_index = Bool(
True, help="show the ordinal cell index, typically this is ignored from notebooks."
).tag(config=True)
include_visibility = Bool(False, help="include visibility toggle").tag(config=True)
include_visibility = Bool(True, help="include visibility toggle").tag(config=True)
include_upload = Bool(False, help="include template for uploading new content").tag(config=True)
allow_run_mode = Bool(False, help="enable buttons for a run mode").tag(config=True)
hide_anchor_links = Bool(False).tag(config=True)
exclude_anchor_links = Bool(True).tag(config=True)
exclude_anchor_links = Bool(False).tag(config=True)
code_theme = Enum(list(THEMES), "gh-high", help="an accessible pygments dark/light theme").tag(
config=True
)
Expand Down Expand Up @@ -187,7 +185,7 @@ def init_resources(self, resources=None):
resources["include_help"] = self.include_help
resources["include_toc"] = self.include_toc
resources["include_summary"] = self.include_summary
resources["include_visibility"] = self.include_upload
resources["include_visibility"] = self.include_visibility
resources["include_upload"] = self.include_upload
resources["wcag_priority"] = self.wcag_priority
resources["accesskey_navigation"] = self.accesskey_navigation
Expand All @@ -212,14 +210,12 @@ def from_notebook_node(self, nb, resources=None, **kw):
def post_process_html(self, body):
"""A final pass at the exported html to add table of contents, heading links, and other a11y affordances."""
soup = soupify(body)
describe_main(soup)
heading_links(soup)
details = soup.select_one("""[aria-labelledby="nb-toc"] details""")
if details:
details.extend(soupify(toc(soup)).body.children)
for x in details.select("ul"):
x.name = "ol"
details.select_one("ol").attrs["aria-labelledby"] = "nb-toc"
if self.include_toc:
details = soup.select_one("""[aria-labelledby="nb-toc"] details""")
if details:
if not details.select_one("nav"):
details.append(toc(soup))
return soup.prettify(formatter="html5")


Expand Down Expand Up @@ -270,7 +266,7 @@ def highlight(code, lang="python", attrs=None, experimental=True):
lang = lang or pygments.lexers.get_lexer_by_name(lang or "python")

formatter = pygments.formatters.get_formatter_by_name(
"html", debug_token_types=True, title=f"{lang} code", wrapcode=True
"html", debug_token_types=False, title=f"{lang} code", wrapcode=True
)
try:
return pygments.highlight(
Expand All @@ -287,31 +283,39 @@ def soupify(body: str) -> BeautifulSoup:
return BeautifulSoup(body, features="html5lib")


def mdtoc(html):
def toc(html):
"""Create a table of contents in markdown that will be converted to html"""
import io

toc = io.StringIO()
toc = BeautifulSoup(features="html.parser")
toc.append(nav := toc.new_tag("nav"))
nav.append(ol := toc.new_tag("ol"))
last_level = 1
headers = set()
for header in html.select(".cell :is(h1,h2,h3,h4,h5,h6)"):
if header in headers:
continue
headers.add(header)
id = header.attrs.get("id")
if not id:
from slugify import slugify

if header.string:
id = slugify(header.string)
else:
continue

continue
# there is missing logistics for managely role=heading
# adding code group semantics will motivate this addition
level = int(header.name[-1])
toc.write(" " * (level - 1) + f"* [{header.string}](#{id})\n")
return toc.getvalue()


def toc(html):
"""Create an html table of contents"""
return get_markdown(mdtoc(html))
if last_level > level:
for l in range(level, last_level):
last_level -= 1
ol = ol.parent.parent
elif last_level < level:
for l in range(last_level, level):
last_level += 1
ol.append(li := toc.new_tag("li"))
li.append(ol := toc.new_tag("ol"))
ol.append(li := toc.new_tag("li"))
li.append(a := toc.new_tag("a"))
a.append(header.text)
a.attrs.update(href=f"#{id}")

return toc


def heading_links(html):
Expand All @@ -321,12 +325,12 @@ def heading_links(html):
if not id:
from slugify import slugify

if header.string:
id = slugify(header.string)
if header.text:
id = slugify(header.text)
else:
continue

link = soupify(f"""<a href="#{id}">{header.string}</a>""").body.a
link = soupify(f"""<a href="#{id}">{header.text}</a>""").body.a
header.clear()
header.append(link)

Expand Down Expand Up @@ -362,15 +366,6 @@ def count_code_cells(nb):
return len([None for x in nb.cells if x["cell_type"] == "code"])


def describe_main(soup):
"""Add REFIDs to aria-describedby"""
x = soup.select_one("#toc > details > summary")
if x:
x.attrs["aria-describedby"] = soup.select_one("main").attrs["aria-describedby"] = (
"nb-cells-count-label nb-cells-label nb-code-cells nb-code-cells-label nb-ordered nb-loc nb-loc-label"
)


def is_ordered(nb) -> str:
"""Measure if the notebook is ordered"""
start = 0
Expand Down
26 changes: 17 additions & 9 deletions nbconvert_a11y/templates/a11y/base.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ the notebook experiennce from browse to edit/focus mode.
{% set title = nb.metadata.get('title', resources['metadata']['name']) | escape_html_keep_quotes %}

{%- block html_head_js -%}
{% if nb.metadata.description %}
<meta name="description" content="{{nb.metadata.description}}">{% endif %}
<meta name="color-scheme" content="light">
{# sa11y needs to be loaded before requirejs #}
{% if resources.include_sa11y %}{% include "a11y/components/sa11y.html.j2"%}{% endif %}
Expand All @@ -24,7 +26,7 @@ the notebook experiennce from browse to edit/focus mode.
{% include "a11y/static/style.css" %}
</style>
{% for theme in ["light", "dark"] %}
<style type="text/css" id="nb-{{theme}}-theme" media="{% if loop.index - 1%}not {% endif %}screen">
<style type="text/css" id="nb-{{theme}}-highlight" media="{% if loop.index - 1%}not {% endif %}screen">
{% include "a11y/static/theme/{}.css" .format(resources.code_theme.format(theme)) %}
</style>
{% endfor %}
Expand All @@ -33,12 +35,21 @@ the notebook experiennce from browse to edit/focus mode.
{% block extra_css %}{% endblock %}

{% block body_header %}
{%- set title = nb.metadata.get("title") -%}
{%- set description = nb.metadata.get("description") -%}
{% set describedby = "nb-ct-total nb-cells-label nb-ct-code nb-state nb-ct-loc nb-loc-label
nb-ct-outputs nb-outputs-label"%}

<body class="wcag-{{resources.wcag_priority.lower()}}">
<header>
<a href="#1" id="TOP">skip to main content</a>
{{site_navigation | default("")}}
{%- if resources.include_summary -%}{% include "a11y/components/nb-summary.html.j2"%}{%- endif -%}
<a href="#1" id="TOP">skip to main content</a><br />
{% if resources.include_settings %}
<button onclick="openDialog()" aria-controls="nb-settings" accesskey=",">settings</button>
{% endif %}
<button onclick="fullScreen()" id="nb-fullscreen">fullscreen</button>

{{resources.header | default("")}}
{%- if resources.include_summary -%}{% include "a11y/components/nb-summary.html.j2" %}{%- endif -%}
{%- if resources.include_toc -%}{% include "a11y/components/toc.html.j2" %}{%- endif -%}
<dialog id="nb-metadata">
<form method="dialog">
Expand All @@ -58,12 +69,10 @@ the notebook experiennce from browse to edit/focus mode.
{# skip to top is needed for long notebooks.
it is difficult to access for keyboard users. #}
<footer>
{{resources.footer}}
{% include "a11y/components/nb-toolbar.html.j2" %}
{# make the individual settings discoverable before all the settings
when using shift+tab #}
{% if resources.include_settings %}
<button onclick="openDialog()" aria-controls="nb-settings" accesskey=",">settings</button>
{% endif %}
{% if resources.include_visibility %}<button aria-controls="nb-visibility-dialog" onclick="openDialog()"
accesskey="-">show/hide</button>
{% endif %}
Expand All @@ -75,8 +84,7 @@ the notebook experiennce from browse to edit/focus mode.
{% if resources.include_visibility %}{% include "a11y/components/visibility.html.j2"%}{% endif %}
{% if resources.include_help %}{% include "a11y/components/help.html.j2" %}{% endif %}
{% if resources.include_axe %}{% include "a11y/components/audit.html.j2" %}{% endif %}
{{footer}}
{{activity_log()}}
{{activity_log(id=True)}}
<a href="#TOP" accesskey="0">skip to top</a>
</footer>
{% if resources.include_upload %}{% block template_element %}{% endblock %}{% endif %}
Expand Down
34 changes: 20 additions & 14 deletions nbconvert_a11y/templates/a11y/components/cell.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@
{% from "a11y/components/displays.html.j2" import cell_display_priority with context %}

{% macro cell_anchor(i, cell_type, execution_count=None, outputs=None, hidden=False)%}
{% if not resources.exclude_anchors_links %}
<a class="nb-anchor{% if resources.hide_anchor_links%} visually-hidden{% endif %}" href="#{{i}}" id="{{i}}"
aria-labelledby="nb-cell-label {{i}}" {% if resources.accesskey_navigation and isinstance(i, int) and (i < 10)
%}accesskey="{{i}}" {% endif %} aria-describedby="nb-{{cell_type}}-label nb-cell-label {% if cell_type != "
markdown".strip() %}cell-{{i}}-loc nb-loc-label{% endif %}{% if cell_type==" code" .strip() and execution_count %}
cell-{{i}}-outputs-len{% endif %}" {{hide(hidden)}}>{{i}}</a>
{% endif %}
cell-{{i}}-outputs-len{% endif %}" {{hide(hidden or resources.exclude_anchor_links)}}>{{i}}</a>
{% endmacro %}

{% macro cell_form(i, cell_type, hidden=True) %}
{# the cell form acts a formal reference for each cell. as a form, each cell can handle a submission process
that would include talking to the kernel. #}
<form class="nb-toolbar" id="cell-{{i}}" name="cell-{{i}}" aria-labelledby="cell-{{i}}-source-label" {{hide(hidden)}}>
<form class="nb-toolbar" id="cell-{{i}}" name="cell-{{i}}" aria-labelledby="cell-{{i}}-exec-label" {{hide(hidden)}}>
<fieldset>
<legend>actions</legend>
<button type="submit">Run Cell</button>
Expand All @@ -38,20 +36,21 @@ that would include talking to the kernel. #}
{% endmacro %}

{% macro cell_execution_count(i, execution_count, hidden=False) %}
<label id="cell-{{i}}-source-label" class="nb-execution_count" {{hide(hidden)}}>
<span>{{resources.prompt_in}}</span>
<label class="nb-execution_count" {{hide(hidden)}}>
<span id="cell-{{i}}-in-label">{{resources.prompt_in}}</span>
<span aria-hidden="true">{{resources.prompt_left}}</span>
<span>{{execution_count}}</span>
<span id="cell-{{i}}-exec-label">{{execution_count}}</span>
<span aria-hidden="true">{{resources.prompt_right}}</span>
</label>
{% endmacro %}


{% macro cell_source(i, source, cell_type, execution_count, hidden=False) %}
<textarea class="nb-source" form="cell-{{i}}" id="cell-{{i}}-source" name="source"
rows="{{source.splitlines().__len__()}}" aria-labelledby="cell-{{i}}-source-label nb-source-label"
rows="{{source.splitlines().__len__()}}" aria-labelledby="cell-{{i}}-in-label cell-{{i}}-exec-label nb-source-label"
readonly>{{source}}</textarea>
<div role="group" aria-labelledby="nb-source-label" aria-roledescription="highlighted">
{# should say something like highlight source #}
<div role="group" aria-labelledby="cell-{{i}}-in-label cell-{{i}}-exec-label" aria-roledescription="highlighted">
{{highlight(source, cell_type)}}
</div>
{% endmacro %}
Expand All @@ -69,15 +68,15 @@ that would include talking to the kernel. #}
</dialog>
{% endmacro %}

{%- macro cell_output(i, cell, source, outputs, cell_type, execution_count, hidden=False) -%}
{%- macro cell_output(i, cell, source, outputs, cell_type, metadata, execution_count, hidden=False) -%}
{% set CODE = cell_type == "code" %}
{% if CODE %}
{% if execution_count %}
<span class="nb-outputs visually-hidden" id="cell-{{i}}-outputs-len">{{outputs.__len__()}} outputs.</span>
{% endif %}
{# the following span should get its own column in the table #}
<fieldset
class="nb-outputs{% if cell.metadata.get('scrolled')%} nb-scrolled{% endif %}{% if cell.metadata.get('collapsed')%} nb-collapsed{% endif %}"
class="nb-outputs{% if metadata.get('scrolled')%} nb-scrolled{% endif %}{% if metadata.get('collapsed')%} nb-collapsed{% endif %}"
{{hide(hidden)}} data-outputs="{{outputs.__len__()}}">
<legend id="cell-{{i}}-outputs-label" aria-describedby="nb-outputs-desc" {{hide(not
resources.global_content_filter.include_output_prompt)}}>
Expand All @@ -92,9 +91,16 @@ that would include talking to the kernel. #}
{{cell_display_priority(i, outputs, cell)}}
{% endif %}
</fieldset>
{% else %}
{%- else %}
{%- set html = metadata.get("data", {}).get("text/html") %}
{%- if html -%}
{# if there is a data bundle in the metadata then we can use that
instead of computing the markup #}
{{html}}
{%- else -%}
{{ markdown(source) | strip_files_prefix }}
{% endif %}
{%- endif -%}
{%- endif -%}
{%- endmacro -%}

{% macro cell_loc(i, cell, tag="span") %}
Expand All @@ -112,7 +118,7 @@ that would include talking to the kernel. #}
{{cell_cell_type(loop.index, cell.cell_type, hidden=True)}}
{{cell_source(loop.index, cell.source, cell.cell_type, cell.execution_count, hidden=cell.cell_type != "code")}}
{{cell_loc(loop.index, cell)}}
{{cell_output(loop.index, cell, cell.source, cell.outputs, cell.cell_type, cell.execution_count)}}
{{cell_output(loop.index, cell, cell.source, cell.outputs, cell.cell_type, cell.metadata, cell.execution_count)}}
{{cell_metadata(loop.index, cell.metadata, hidden=True)}}
</{{tag}}>
{% endmacro%}
2 changes: 1 addition & 1 deletion nbconvert_a11y/templates/a11y/components/core.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ at some point effort should be put into separating context dependent and indepen
<details class="log">
<summary><span {% if id %}id="nb-activity-log-label" {% endif %}>activity log</span></summary>
</details>
<table aria-live="polite">
<table aria-live="polite" aria-labelledby="nb-activity-log-label">
<tr>
<th hidden>time</th>
<td>message</td>
Expand Down
4 changes: 0 additions & 4 deletions nbconvert_a11y/templates/a11y/components/nb-summary.html.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
{%- set title = nb.metadata.get("title") -%}
{%- set description = nb.metadata.get("description") -%}
{% set describedby = "nb-ct-total nb-cells-label nb-ct-code nb-state nb-ct-loc nb-loc-label
nb-ct-outputs nb-outputs-label"%}
<section aria-labelledby="nb-summary-label">
<details>
<summary id="nb-summary-label" aria-describedby="{{describedby}}">notebook summary</summary>
Expand Down
19 changes: 17 additions & 2 deletions nbconvert_a11y/templates/a11y/components/settings.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ settings provides multiple screen reader navigation techniques when the dialog i

{% from "a11y/components/core.html.j2" import h, radiogroup, select, activity_log, input_number, checkbox, dialog_close
%}
{% set font_sizes = {"xx-small": "xx-small",
"x-small": "x-small",
"small": "small",
"medium": "medium",
"large": "120%",
"x-large": "150%",
"xx-large": "200%",
"xxx-large": "300%",
"xxxx-large": "400%",
"xxxxx-large": "500%"}
%}
<dialog id="nb-settings">
<form name="settings">
{{h(1, "accessibility settings")}}
Expand All @@ -13,22 +24,26 @@ settings provides multiple screen reader navigation techniques when the dialog i
<li>
{{h(2, "notebook layout")}}
<ul aria-labelledby="nb-notebook-layout-label">
{# the best implementation would make these configurable for folks who change them often. #}
<li>{{select("color scheme", {"light mode": "light", "dark mode": "dark"})}}</li>
<li>{{input_number("margin", 5, min=0, max=40, step=5, extra_title="%")}}</li>
<li>{{checkbox("horizontal scrolling", False)}}</li>
<li>{{input_number("line height", 1.5, min=0.5, max=3, step=0.1)}}</li>
{# screen overflow exists for digital magnifier users to follow long horizontal lines of text. #}
<li>{{select("cell navigation", default=resources.table_pattern.__name__.lower(), values="list table
region presentation".split(), disabled="grid
treegrid
tree".split())}}</li>
{% set priority = {"Triple A": "AAA","Double A": "AA", "Single A": "A"}%}
<li>{{select("accessibility priority", priority, resources.wcag_priority)}}</li>
<li>{{input_number("margin", 5, min=0, max=40, step=5, extra_title="%")}}</li>
<li>{{checkbox("accesskey navigation", True)}}</li>
</ul>
</li>
<li>

{{h(2, "font settings")}}
<ul aria-labelledby="nb-font-settings-label">
<li>{{select("font size", "xx-small x-small small medium large x-large xx-large".split(),
<li>{{select("font size", font_sizes,
"medium")}}</li>
<li>
{{select("font family", "serif sans-serif".split())}}
Expand Down
2 changes: 1 addition & 1 deletion nbconvert_a11y/templates/a11y/components/toc.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
<summary id="toc"><span id="nb-toc">table of contents</span></summary>
{# the table of contents is populated in python. #}
<a id="nb-toc-start"></a>
<ol></ol>
{{resources.toc|default("")}}
</details>
</section>
Loading

0 comments on commit 1c7b051

Please sign in to comment.