Skip to main content

A script to convert a WCAG-EM report from JSON to reStructuredText for a VPAT

At work, I've had to produce VPAT documents for the software I'm responsible for.

The VPAT template asks you to list, for each of the WCAG criteria, whether you support it or not, or if it doesn't apply.

The W3C have made a WCAG-EM Report Tool which helps you to work through the WCAG criteria and make notes about whether they're satisfied.

At the end, you can download a copy of the report in either summarised HTML format, or a JSON file with all the data you entered.

The first time I did a VPAT, I mostly manually converted the information from the WCAG-EM report to a reStructuredText table, to go in our Sphinx documentation.

Now I'm doing it a second time, I know I don't want to waste my time doing that!

So I've written a Python script which takes in the JSON file from the report tool and prints out the tables for the VPAT template, in reStructuredText format.

Here's my script:

2024/06/wcag-em-report-to-rst.py (Source)

#!/usr/bin/env python
# coding: utf-8

# Convert a WCAG-EM report tool JSON file to reStructuredText
#
# This script converts the JSON file you get from the [WCAG-EM Report Tool](https://www.w3.org/WAI/eval/report-tool/) into reStructuredText markup.
#
# It needs the following files:
#
# * https://raw.githubusercontent.com/w3c/wai-wcag-em-report-tool/main/src/data/wcag.json - The data on WCAG criteria used by the report tool.
# * https://raw.githubusercontent.com/w3c/wai-wcag-em-report-tool/main/src/locales/en/WCAG.json - The English readable names for WCAG criteria. Save as ``WCAG_text.json``.

from collections import defaultdict
from itertools import groupby
import json
import sys

# Load the report data, and the WCAG criterion data

report_json_filename = sys.argv[1]

with open(report_json_filename) as f:
    data = json.load(f)

with open('wcag.json') as f:
    wcag = json.load(f)

with open('WCAG_text.json') as f:
    wcag_text = json.load(f)

wcag_ids = {v['id']: v for v in wcag['2.2'].values()}

criteria_names = {k: v['TITLE'] for k,v in wcag_text['SUCCESS_CRITERION'].items()}

criteria = defaultdict(list)

for item in data['auditSample']:
    id = item['test']['id'][len('WCAG22:'):]
    criteria[id].append(item)


# For each criterion, gather info from the report

report = []

def indent_lines(text, indent):
    lines = text.split('\n')
    return lines[0]+'\n'.join(indent+line for line in lines[1:])

for key, criterion in criteria.items():
    description = ''
    result = ''
    for item in criterion:
        if 'description' in item['result']:
            description = item['result']['description']

        if 'Website' not in item['subject']['type']:
            continue

        result = item['result']['outcome']['title']

    result_map = {
        'Passed': 'Supports',
        'Not present': 'Not Applicable',
    }
    result = result_map.get(result, result)

    info = wcag_ids[key]
    num = info['num']
    conformanceLevel = info['conformanceLevel']
    name = criteria_names[num]

    report.append({
        'key': key,
        'description': indent_lines(description, ' '*6),
        'result': result,
        'num': num,
        'conformanceLevel': conformanceLevel,
        'name': name,
    })


# Criteria not assessed
#
# The following criteria were not assessed:

not_assessed = [n for n in wcag_ids.values() if n['id'] not in criteria]
if len(not_assessed):
    print("The following criteria were not assessed:\n")
    for n in not_assessed:
        print(f'''* {n['num']} {n['id']} ({n['conformanceLevel']}) https://www.w3.org/WAI/WCAG22/quickref/#{n['id']}''')

    print('\n')

# Produce ReStructuredText of report

for level, items in groupby(sorted(report, key=lambda x: (x['conformanceLevel'], x['num'])), key=lambda x: x['conformanceLevel']):
    header = f'''Table 1: Success Criteria, Level {level}'''
    print(header)
    print('*'*len(header))
    print('''
.. list-table::
  :header-rows: 1

  -

     - Criteria
     - Conformance Level
     - Remarks and Explanations''')

    for item in items:
        print('''  -
    - .. _vpat-{key}:

      `{num}: {name} <https://www.w3.org/WAI/WCAG22/quickref/#{key}>`__ (Level {conformanceLevel})
    - {result}
    - {description}'''.format(**item))

    print('')

And it ends up looking something like this:

Table 1: Success Criteria, Level A

Criteria

Conformance Level

Remarks and Explanations

1.1.1: Non-text Content (Level A)

Supports

Text is always the primary method of conveying information.

1.2.1: Audio-only and Video-only (Prerecorded) (Level A)

Not Applicable

1.2.2: Captions (Prerecorded) (Level A)

Not Applicable

1.2.3: Audio Description or Media Alternative (Prerecorded) (Level A)

Not Applicable

1.3.1: Info and Relationships (Level A)

Supports

1.3.2: Meaningful Sequence (Level A)

Supports

1.3.3: Sensory Characteristics (Level A)

Supports

All interactive elements are clearly labelled in text.

1.4.1: Use of Color (Level A)

Supports

and so on.

As ever, I'm putting this here both so I can find it again later, and in case anyone else finds it useful.