aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sisudoc/outputs/io_out/epub3.d
diff options
context:
space:
mode:
Diffstat (limited to 'src/sisudoc/outputs/io_out/epub3.d')
-rw-r--r--src/sisudoc/outputs/io_out/epub3.d145
1 files changed, 141 insertions, 4 deletions
diff --git a/src/sisudoc/outputs/io_out/epub3.d b/src/sisudoc/outputs/io_out/epub3.d
index 420c536..8a3baaf 100644
--- a/src/sisudoc/outputs/io_out/epub3.d
+++ b/src/sisudoc/outputs/io_out/epub3.d
@@ -106,6 +106,140 @@ template outputEPub3() {
default: return "image/" ~ _ext.toLower;
}
}
+ /+ ↓ the landmarks navigation.
+ This is what EPUB3 offers in place of the EPUB2 <guide> that was
+ dropped: not a list of headings, which is what the toc nav is, but a
+ handful of named places a reader offers as buttons - start reading
+ here, the contents, the index. It is a nav in the navigation document
+ rather than a section of the package, and it is hidden, being
+ machinery rather than content.
+ .
+ Every entry names a file that is in the manifest, found the same way
+ the manifest found it: the first heading of the section that opens a
+ segment of its own.
+ +/
+ string _epub_landmark_file(D)(D doc, string _section) {
+ if (!(_section in doc.abstraction)) { return ""; }
+ string _first;
+ foreach (obj; doc.abstraction[_section]) {
+ if (obj.metainfo.is_a != "heading"
+ || obj.metainfo.heading_lev_markup > 4
+ || obj.tags.segment_anchor_tag_epub.length == 0
+ ) { continue; }
+ /+ ↓ a level 4 heading is where the text of the section actually
+ starts. Above it are the part wrappers, which hold no text, so
+ a landmark that lands on one has sent the reader to a title and
+ not to the thing named. Take the level 4 where there is one. +/
+ if (obj.metainfo.heading_lev_markup == 4) {
+ return obj.tags.segment_anchor_tag_epub ~ ".xhtml";
+ }
+ if (_first.empty) { _first = obj.tags.segment_anchor_tag_epub ~ ".xhtml"; }
+ }
+ return _first;
+ }
+ string _epub_landmarks(D)(D doc) {
+ string[3][] _wanted = [
+ ["toc", "toc", "Table of Contents"],
+ ["body", "bodymatter", "Start of Content"],
+ ["bookindex", "index", "Index"],
+ ["glossary", "glossary", "Glossary"],
+ ["bibliography", "bibliography", "Bibliography"],
+ ];
+ string _items;
+ foreach (_w; _wanted) {
+ string _file = _epub_landmark_file(doc, _w[0]);
+ if (_file.empty) { continue; }
+ _items ~= format(
+ " <li><a epub:type=\"%s\" href=\"%s\">%s</a></li>\n",
+ _w[1], _file, _w[2]);
+ }
+ if (_items.empty) { return ""; }
+ return " <nav epub:type=\"landmarks\" id=\"landmarks\" hidden=\"\">\n"
+ ~ " <h2>Guide</h2>\n"
+ ~ " <ol>\n"
+ ~ _items
+ ~ " </ol>\n"
+ ~ " </nav>\n";
+ }
+ /+ ↓ the accessibility metadata EPUB Accessibility 1.1 asks for.
+ accessMode, accessibilityFeature and accessibilityHazard are required
+ of every EPUB by that specification (W3C Recommendation, 2024-10-17),
+ and accessModeSufficient and accessibilitySummary are recommended.
+ epubcheck does not test for them, which is why their absence never
+ showed in the error count; a reader looking for a book it can use
+ does test for them, and so does anyone distributing into the EU under
+ Directive 2019/882.
+ .
+ Every value here is derived from the document rather than asserted.
+ Nothing claims conformance to WCAG: that is a claim about an
+ evaluation that has not been made, and dcterms:conformsTo is
+ deliberately absent.
+ .
+ "schema" is a reserved prefix in EPUB 3, so it needs no declaring.
+ +/
+ struct ST_epubA11y {
+ bool images;
+ bool some_image_lacks_alt;
+ bool bookindex;
+ }
+ ST_epubA11y _epub_a11y(D)(D doc) {
+ auto xhtml_format = outputXHTMLs();
+ ST_epubA11y _a;
+ foreach (section; doc.matters.has.keys_seq.seg) {
+ if (section == "tail") { continue; }
+ if (!(section in doc.abstraction)) { continue; }
+ if (section == "bookindex") { _a.bookindex = true; }
+ foreach (obj; doc.abstraction[section]) {
+ foreach (m; obj.text.matchAll(rgx.inline_image)) {
+ _a.images = true;
+ if (xhtml_format._image_alt(m["post"].to!string).empty) {
+ _a.some_image_lacks_alt = true;
+ }
+ }
+ }
+ }
+ return _a;
+ }
+ string _epub_a11y_metadata(D)(D doc) {
+ auto _a = _epub_a11y(doc);
+ bool _alt_throughout = _a.images && !(_a.some_image_lacks_alt);
+ string _m;
+ string _meta(string _property, string _value) {
+ return format(" <meta property=\"%s\">%s</meta>\n", _property, _value);
+ }
+ _m ~= _meta("schema:accessMode", "textual");
+ if (_a.images) { _m ~= _meta("schema:accessMode", "visual"); }
+ /+ ↓ a sufficient set is one a reader can get the whole publication
+ through. Text alone is sufficient where there are no images, and
+ where every image says what it is; where an image says nothing,
+ claiming it would be untrue. +/
+ if (!(_a.images) || _alt_throughout) {
+ _m ~= _meta("schema:accessModeSufficient", "textual");
+ } else {
+ _m ~= _meta("schema:accessModeSufficient", "textual,visual");
+ }
+ _m ~= _meta("schema:accessibilityFeature", "structuralNavigation");
+ _m ~= _meta("schema:accessibilityFeature", "tableOfContents");
+ _m ~= _meta("schema:accessibilityFeature", "readingOrder");
+ if (_a.bookindex) { _m ~= _meta("schema:accessibilityFeature", "index"); }
+ if (_alt_throughout) { _m ~= _meta("schema:accessibilityFeature", "alternativeText"); }
+ /+ ↓ nothing spine writes flashes, moves or makes a sound +/
+ _m ~= _meta("schema:accessibilityHazard", "none");
+ string _summary =
+ "Structured text with headings at every level, a navigation document,"
+ ~ " and a reading order the publication declares."
+ ~ " Every substantive object carries a citation number that is the same"
+ ~ " in every format this document is published in.";
+ if (!(_a.images)) {
+ _summary ~= " There are no images.";
+ } else if (_alt_throughout) {
+ _summary ~= " Every image carries alternative text.";
+ } else {
+ _summary ~= " Some images carry no alternative text.";
+ }
+ _m ~= _meta("schema:accessibilitySummary", _summary);
+ return _m;
+ }
/+ ↓ the publication identifier, as a real RFC 4122 UUID.
Version 5, name based: the name is the document's own uid, so the
identifier is stable across rebuilds and distinct per document,
@@ -153,13 +287,13 @@ template outputEPub3() {
<dc:identifier id="bookid">urn:uuid:%s</dc:identifier>
<dc:title id="title">%s</dc:title>
<meta refines="#title" property="title-type">main</meta>
- %s <dc:creator id="aut">%s</dc:creator>
+ %s <dc:creator id="aut">%s</dc:creator>
<meta refines="#aut" property="file-as">%s</meta>
<dc:language>%s</dc:language>
<dc:date id="published">%s</dc:date>
<dc:rights>Copyright: %s</dc:rights>
<meta property="dcterms:modified">%s</meta>
- </metadata>
+ %s </metadata>
<manifest>
<item id="css" href="%s" media-type="text/css"/>
<item id="nav" href="toc_nav.xhtml" media-type="application/xhtml+xml" properties="nav" />
@@ -181,6 +315,7 @@ template outputEPub3() {
(doc.matters.conf_make_meta.meta.rights_copyright.empty)
? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright),
_epub_modified_utc(),
+ _epub_a11y_metadata(doc),
(pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"),
);
content ~= parts["manifest_documents"];
@@ -241,7 +376,7 @@ template outputEPub3() {
<header>
<h1>Contents</h1>
</header>
- <nav epub:type="toc" id="toc">
+ <nav epub:type="toc" id="toc" role="doc-toc">
┃",
(doc.matters.conf_make_meta.meta.title_full).special_characters_text,
);
@@ -332,7 +467,9 @@ template outputEPub3() {
if (n == 0) {
_toc_nav_tail ~=" </nav>
</section>
- </body>
+ ";
+ _toc_nav_tail ~= _epub_landmarks(doc);
+ _toc_nav_tail ~=" </body>
</html>\n";
}
}