Skip to content

Commit

Permalink
feat: report EPUB-specific violations
Browse files Browse the repository at this point in the history
Add EPUB-specific checks in a new `checker-epub` module,
sibling to `checker-nightmare` which handles the HTML
content.

The new checker is responsible for checking publication-level
violations. Currently:
- checks that the publication has a title
- checks the presence of a11y metadata
- checks that the source of page breaks (if any) is defined

Closes #10
  • Loading branch information
rdeltour committed Oct 4, 2017
1 parent 7b855df commit ab4d34b
Show file tree
Hide file tree
Showing 24 changed files with 310 additions and 26 deletions.
106 changes: 106 additions & 0 deletions src/checker/checker-epub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
'use strict';

const builders = require('../report/report-builders.js');
const winston = require('winston');

const ASSERTED_BY = 'Ace';
const MODE = 'automatic';
const KB_BASE = 'https://daisy.github.io/a11y-kb/';

function newViolation({ impact = 'serious', title, testDesc, resDesc, kbPath, kbTitle }) {
return new builders.AssertionBuilder()
.withAssertedBy(ASSERTED_BY)
.withMode(MODE)
.withTest(
new builders.TestBuilder()
.withImpact(impact)
.withTitle(title)
.withDescription(testDesc)
.withHelp(
KB_BASE + kbPath,
kbTitle)
.build())
.withResult(
new builders.ResultBuilder('fail')
.withDescription(resDesc)
.build())
.build();
}

function newMetadataAssertion(name, impact = 'serious') {
return newViolation({
impact,
title: `metadata-${name.toLowerCase().replace(':', '-')}`,
testDesc: `Ensures a '${name}' metadata is present`,
resDesc: `Add a '${name}' metadata property to the Package Document`,
kbPath: 'docs/metadata/schema-org.html',
kbTitle: 'Schema.org Accessibility Metadata',
});
}

function checkMetadata(assertions, epub) {
// Required metadata
[
'schema:accessMode',
'schema:accessibilityFeature',
'schema:accessibilitySummary',
].filter(meta => epub.metadata[meta] === undefined)
.forEach(meta => assertions.withAssertions(newMetadataAssertion(meta)));
// Recommended metadata
[
'schema:accessModeSufficient',
].filter(meta => epub.metadata[meta] === undefined)
.forEach(meta => assertions.withAssertions(newMetadataAssertion(meta, 'moderate')));
}

function checkTitle(assertions, epub) {
const title = epub.metadata['dc:title'];
if (title === undefined || title.trim() === '') {
assertions.withAssertions(newViolation({
title: 'epub-title',
testDesc: 'Ensures the EPUB has a title',
resDesc: 'Add a \'dc:title\' metadata property to the Package Document',
kbPath: '',
kbTitle: 'EPUB Title',
}));
}
}

function checkPageSource(assertion, epub) {
if (epub.navDoc.hasPageList
&& (epub.metadata['dc:source'] === undefined
|| epub.metadata['dc:source'].trim() === '')) {
assertion.withAssertions(newViolation({
title: 'epub-pagesource',
testDesc: 'Ensures the source of page breaks is identified',
resDesc: 'Add a \'dc:source\' metadata property to the Package Document',
kbPath: 'docs/navigation/pagelist.html',
kbTitle: 'Page Navigation',
}));
}
}

function check(epub, report) {
winston.info('Checking package...');
const assertion = new builders.AssertionBuilder()
.withSubAssertions()
.withTestSubject(
epub.packageDoc.src,
(epub.metadata['dc:title'] !== undefined) ? epub.metadata['dc:title'] : '');

// Check a11y metadata
checkMetadata(assertion, epub);

// Check presence of a title
checkTitle(assertion, epub);

// Check page list is sourced
checkPageSource(assertion, epub);

report.addAssertions(assertion.build());
// Report the Nav Doc
report.addEPUBNav(epub.navDoc);
return Promise.resolve();
}

module.exports.check = check;
1 change: 1 addition & 0 deletions src/checker/checker-nightmare.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ module.exports.check = (epub) => {
mode: 'detach',
},
});
winston.info('Checking documents...');
return epub.contentDocs.reduce((sequence, spineItem) =>
sequence.then(results =>
checkSingle(spineItem, epub, nightmare)
Expand Down
10 changes: 6 additions & 4 deletions src/checker/checker.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use strict';

const checker = require('./checker-nightmare.js');
const htmlChecker = require('./checker-nightmare.js');
const epubChecker = require('./checker-epub.js');
const winston = require('winston');

function consolidate(results, report) {
winston.info('Consolidating results...');
// Integrate checker results to the report
results.forEach((res) => {
report.addContentDocAssertion(res.assertions);
report.addAssertions(res.assertions);
report.addProperties(res.properties);
report.addData(res.data);
});
Expand All @@ -22,7 +24,7 @@ function consolidate(results, report) {
}

module.exports.check = function check(epub, report) {
winston.info('Checking documents...');
return checker.check(epub)
return epubChecker.check(epub, report)
.then(() => htmlChecker.check(epub))
.then(results => consolidate(results, report));
};
2 changes: 0 additions & 2 deletions src/core/ace.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ module.exports = function ace(epubPath, options) {
.then(() => epub.parse())
// initialize the report
.then(() => new Report(epub))
// Report the Nav Doc
.then(report => report.addEPUBNav(epub.navDoc))
// Check each Content Doc
.then(report => checker.check(epub, report))
// Process the Results
Expand Down
19 changes: 12 additions & 7 deletions src/epub/epub-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ function EpubParser() {
this.contentDocMediaType = "application/xhtml+xml";
}

function parseNavDoc(relpath, filepath) {
const content = fs.readFileSync(filepath).toString();
function parseNavDoc(fullpath, epubDir) {
const content = fs.readFileSync(fullpath).toString();
const doc = new DOMParser().parseFromString(content);

// Remove all links
Expand All @@ -52,10 +52,13 @@ function parseNavDoc(relpath, filepath) {
const toc = select('//html:nav'
+ '[@epub:type="toc"]/html:ol', doc);
const tocHTML = new XMLSerializer().serializeToString(toc[0]);
const hasPageList = select('//html:nav'
+ '[@epub:type="page-list"]', doc).length > 0;

return {
path: relpath,
src: path.relative(epubDir, fullpath),
tocHTML,
hasPageList,
};
}

Expand Down Expand Up @@ -96,18 +99,20 @@ EpubParser.prototype.parse = function(epubDir) {
winston.error('Package document not found.');
reject(new Error("Package document not found."));
}
this.parseData(packageDocPath);
this.packageDoc = {
src: path.relative(epubDir, packageDocPath),
};
this.parseData(packageDocPath, epubDir);
resolve(this);
});
}

EpubParser.prototype.parseData = function(packageDocPath) {
EpubParser.prototype.parseData = function(packageDocPath, epubDir) {
const content = fs.readFileSync(packageDocPath).toString();
const doc = new DOMParser().parseFromString(content);
const select = xpath.useNamespaces(
{ opf: 'http://www.idpf.org/2007/opf',
dc: 'http://purl.org/dc/elements/1.1/'});

this.metadata = parseMetadata(doc, select);

const spineItemIdrefs = select('//opf:itemref/@idref', doc);
Expand All @@ -129,7 +134,7 @@ EpubParser.prototype.parseData = function(packageDocPath) {
if (navDocRef.length > 0) {
const navDocPath = navDocRef[0].nodeValue;
const navDocFullPath = path.join(path.dirname(packageDocPath), navDocPath);
this.navDoc = parseNavDoc(navDocPath, navDocFullPath);
this.navDoc = parseNavDoc(navDocFullPath, epubDir);
}
};

Expand Down
1 change: 1 addition & 0 deletions src/epub/epub.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class EPUB {
return new epubParse.EpubParser().parse(this.basedir)
.then((parsed) => {
Object.assign(this, parsed);
this.parsed = true;
return resolve(this);
})
.catch((err) => {
Expand Down
2 changes: 1 addition & 1 deletion src/report/axe2ace.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function axe2ace(spineItem, axeResults) {
.withDescription(violation.description)
.withHelp(kbURL, kbTitle)
.build();
violation.nodes.forEach(node => assertion.withAssertion(
violation.nodes.forEach(node => assertion.withAssertions(
new builders.AssertionBuilder()
.withAssertedBy('aXe')
.withMode('automatic')
Expand Down
12 changes: 6 additions & 6 deletions src/report/report-builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function calculateResult(assertions) {
}

// add an assertion and recalc the top-level result
function withAssertion(obj, assertion) {
function withAssertions(obj, assertions) {
if (!('assertions' in obj)) obj.assertions = [];
obj.assertions.push(assertion);
obj.assertions.push(assertions);
obj['earl:result'] = calculateResult(obj.assertions);
return obj;
}
Expand All @@ -55,8 +55,8 @@ class AssertionBuilder {
this._json['earl:assertedBy'] = assertor;
return this;
}
withAssertion(assertion) {
withAssertion(this._json, assertion);
withAssertions(assertions) {
withAssertions(this._json, assertions);
return this;
}
withMode(mode) {
Expand Down Expand Up @@ -106,8 +106,8 @@ class ReportBuilder {
this._json['a11y-metadata'] = metadata;
return this;
}
withAssertion(assertions) {
withAssertion(this._json, assertions);
withAssertions(assertions) {
withAssertions(this._json, assertions);
return this;
}
withData(data) {
Expand Down
4 changes: 2 additions & 2 deletions src/report/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ module.exports = class Report {
return this._builder.build();
}

addContentDocAssertion(assertion) {
this._builder.withAssertion(assertion);
addAssertions(assertions) {
this._builder.withAssertions(assertions);
return this;
}
addData(data) {
Expand Down
49 changes: 49 additions & 0 deletions tests/__tests__/epub-rules.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const fs = require('fs');
const path = require('path');
const tmp = require('tmp');

const runAce = require('../runAceJS');

tmp.setGracefulCleanup();

let outdir;
let tmpdir;
let reportPath;

beforeEach(() => {
outdir = tmp.dirSync({ prefix: 'ace_out_', unsafeCleanup: true });
tmpdir = tmp.dirSync({ prefix: 'ace_tmp_', unsafeCleanup: true });
reportPath = path.join(outdir.name, 'ace.json');
});

afterEach(() => {
outdir.removeCallback();
tmpdir.removeCallback();
});


function ace(epub, options = {}) {
return runAce(path.join(__dirname, epub), Object.assign({
outdir: outdir.name,
tmp: tmpdir.name,
}, options))
.then(() => {
expect(fs.existsSync(reportPath)).toBeTruthy();
return JSON.parse(fs.readFileSync(reportPath, 'utf8'));
})
.catch(err => console.log(err));
}

test('nothing to report', async () => {
const report = await ace('../data/base-epub-30');
expect(report['earl:result']['earl:outcome']).toEqual('pass');
});

describe('page list and breaks', () => {
test.only('page list correctly sourced', async () => {
const report = await ace('../data/epubrules-pagelist');
expect(report['earl:result']['earl:outcome']).toEqual('pass');
});
});
10 changes: 6 additions & 4 deletions tests/__tests__/report_json.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ describe('check assertions', () => {
const report = await ace(path.join(__dirname, '../data/base-epub-30'));
expect(report['earl:result']).toBeDefined();
expect(report['earl:result']).toEqual({ 'earl:outcome': 'pass' });
expect(report.assertions).toMatchObject([{
'@type': 'earl:assertion',
assertions: [],
}]);
expect(Array.isArray(report.assertions)).toBe(true);
report.assertions.forEach(
assertion => expect(assertion).toMatchObject({
'@type': 'earl:assertion',
assertions: [],
}));
});
});

Expand Down
8 changes: 8 additions & 0 deletions tests/data/axerule-bypass/EPUB/package.opf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
<dc:language>en</dc:language>
<dc:identifier id="uid">NOID</dc:identifier>
<meta property="dcterms:modified">2017-01-01T00:00:01Z</meta>
<meta property="dcterms:modified">2017-01-01T00:00:01Z</meta>
<meta property="schema:accessibilityFeature">structuralNavigation</meta>
<meta property="schema:accessibilitySummary">everything OK!</meta>
<meta property="schema:accessibilityHazard">noFlashingHazard</meta>
<meta property="schema:accessibilityHazard">noSoundHazard</meta>
<meta property="schema:accessibilityHazard">noMotionSimulationHazard</meta>
<meta property="schema:accessMode">textual</meta>
<meta property="schema:accessModeSufficient">textual</meta>
</metadata>
<manifest>
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
Expand Down
7 changes: 7 additions & 0 deletions tests/data/base-epub-30/EPUB/package.opf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
<dc:language>en</dc:language>
<dc:identifier id="uid">NOID</dc:identifier>
<meta property="dcterms:modified">2017-01-01T00:00:01Z</meta>
<meta property="schema:accessibilityFeature">structuralNavigation</meta>
<meta property="schema:accessibilitySummary">everything OK!</meta>
<meta property="schema:accessibilityHazard">noFlashingHazard</meta>
<meta property="schema:accessibilityHazard">noSoundHazard</meta>
<meta property="schema:accessibilityHazard">noMotionSimulationHazard</meta>
<meta property="schema:accessMode">textual</meta>
<meta property="schema:accessModeSufficient">textual</meta>
</metadata>
<manifest>
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
Expand Down
10 changes: 10 additions & 0 deletions tests/data/epubrules-pagelist/EPUB/content_001.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
<head>
<title>Minimal EPUB</title>
</head>
<body>
<h1>Loomings</h1>
<p>Call me Ishmael.</p>
<span id="p1" epub:type="pagebreak" aria-label="p1"/>
</body>
</html>
16 changes: 16 additions & 0 deletions tests/data/epubrules-pagelist/EPUB/nav.xhtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en">
<head>
<title>Minimal Nav</title>
</head>
<body>
<nav epub:type="toc">
<ol>
<li><a href="content_001.xhtml">content 001</a></li>
</ol>
</nav>
<nav epub:type="page-list">
<ol>
<li><a href="content_001.xhtml#p1">p1</a></li>
</ol>
</nav></body>
</html>
Loading

0 comments on commit ab4d34b

Please sign in to comment.