diff options
Diffstat (limited to 'src/sisudoc/outputs/io_out/epub3.d')
| -rw-r--r-- | src/sisudoc/outputs/io_out/epub3.d | 145 |
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"; } } |
