aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-13 21:56:43 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-14 12:18:51 -0400
commiteb5a9f3dab1d1848a1aa90c534a125c222327635 (patch)
tree92e53a0c8cf7c3d3dddef14d82730cdf99b6e4b9
parentepub and html: no errors left over the sample set (diff)
images alt text, and epub says what it is
Four things, not visible in an error count: - Images had no alt text, even where markup carried the text, and the field alt text is present, incorrectly used in code; fixed as the alt. { sm_tux.png 64x80 "Gnu/Linux - a better way" }image an image with nothing to say for itself gets alt="" - Images with no dimensions were incorrectly written width="0" height="0", fixed - Accessibility metadata, which EPUB Accessibility 1.1 requires: schema:accessMode, accessibilityFeature and accessibilityHazard, plus accessModeSufficient and accessibilitySummary, which epubcheck does not test for; used by - readers looking for a book it can use, - anyone distributing into the EU. Every value is derived from the document, not asserted: a document with no images says so, one whose images all carry alt text claims alternativeText and textual sufficiency, one where an image says nothing claims neither. Nothing claims WCAG conformance, which would be a claim about an evaluation that has not happened. - DPUB-ARIA roles beside the structure spine already knew about. doc-toc, doc-endnotes, doc-bibliography, doc-glossary and doc-index on the sections, doc-noteref on a note reference and doc-footnote on the note it points at. "doc_endnotes" as a class name means something to a stylesheet and nothing to a reading system. Not doc-biblioentry: DPUB- ARIA 1.1 deprecated it. These are plain ARIA, so the html gets them too. Also dealt with here: - Spine makes certain sections such as endnotes, bookindex, ... and a heading that has a name for itself cannot share the same name, if it does the name is now disambiguated, disambiguated. (disambiguation similar to that used for a repeated anchor tag) - Endnote anchors fixed to use "id" (being one unique tag in the document that is target of every reference to it)instead of obsolete form (they were <a name="note_1">. "name" on an <a>). Book index markers keep "name": the same marker sits on every object an entry names, 296 times in one document, and those are markers of where a term occurs rather than distinct targets. (assisted by Claude-Code)
-rw-r--r--org/default_regex.org4
-rw-r--r--org/ocda.org4
-rw-r--r--org/ocda_functions.org21
-rw-r--r--org/out_sqlite.org36
-rw-r--r--org/out_xmls.org284
-rw-r--r--org/tests_for_document_abstraction_shell_scripts.org11
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src.d4
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src_functions.d21
-rw-r--r--src/sisudoc/ocda/meta/rgx.d4
-rw-r--r--src/sisudoc/outputs/io_out/epub3.d145
-rw-r--r--src/sisudoc/outputs/io_out/rgx.d4
-rw-r--r--src/sisudoc/outputs/io_out/sqlite_collection_db.d36
-rw-r--r--src/sisudoc/outputs/io_out/xmls.d130
-rw-r--r--test/reference/abstraction/on_the_net_and_information.ralph_amissah.en.ssp26
-rw-r--r--test/reference/abstraction/the_autonomous_contract.ralph_amissah.en.ssp26
-rwxr-xr-xtest/test-epub-validity.sh11
16 files changed, 658 insertions, 109 deletions
diff --git a/org/default_regex.org b/org/default_regex.org
index b1bf3d9..1a6aac6 100644
--- a/org/default_regex.org
+++ b/org/default_regex.org
@@ -626,6 +626,10 @@ static endnote_section_note = ctRegex!(`┥\s*⑆\^┨(?P<no
static inline_image = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_without_dimensions = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>0)h(?P<height>0))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_info = ctRegex!(`☼?(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+)`, "mg");
+/+ the image's alt text: what markup writes as { tux.png 64x80 "alt" }image,
+ which by this stage is a quoted string leading whatever follows the image
++/
+static inline_image_alt = ctRegex!(`^\s*[“"](?P<alt>[^”"]*)[”"]`, "m");
static inline_link_anchor = ctRegex!(`┃(?P<anchor>\S+?)┃`, "mg"); // TODO *~text_link_anchor
static inline_link = ctRegex!(`┥(?P<text>.+?)┝┤(?P<link>#?(\S+?))├`, "mg");
static inline_link_empty = ctRegex!(`┥(?P<text>.+?)┝┤├`, "mg");
diff --git a/org/ocda.org b/org/ocda.org
index ce03d32..72549af 100644
--- a/org/ocda.org
+++ b/org/ocda.org
@@ -185,7 +185,7 @@ scope(exit) {
reset_note_numbers = true;
lev_anchor_tag = "";
anchor_tag = "";
- _heading_anchor_tags_seen = _heading_anchor_tags_seen.init;
+ _heading_anchor_tags_seen = _reserved_heading_anchor_tags();
}
mixin spineNode;
int[string] node_para_int_ = node_metadata_para_int;
@@ -1723,7 +1723,7 @@ html_segnames_ptr_cntr = 0;
content_non_header = "8";
lev_anchor_tag = "";
anchor_tag = "";
-_heading_anchor_tags_seen = _heading_anchor_tags_seen.init;
+_heading_anchor_tags_seen = _reserved_heading_anchor_tags();
// identify parts
/+ ↓ what the document has, as plain data. ST_DocHas is in
metadoc_object_setter so that it can also be built from a loaded
diff --git a/org/ocda_functions.org b/org/ocda_functions.org
index 33c94e4..db6d63b 100644
--- a/org/ocda_functions.org
+++ b/org/ocda_functions.org
@@ -60,6 +60,27 @@ string anchor_tag;
string anchor_tag_;
string[string] tag_in_seg;
bool[string] _heading_anchor_tags_seen;
+/+ ↓ the segment names spine makes for itself, which a heading in the
+ document may not also claim.
+ .
+ A document that writes "1~endnotes Endnote" asks for the name spine
+ gives the gathered endnotes. Both then wrote into endnotes.xhtml,
+ and the navigation listed that file twice, the second time behind
+ its own previous entry in reading order. Seeding the seen set with
+ these names means the document's heading is disambiguated the same
+ way a repeated anchor tag is, and the generated section keeps the
+ name that the table of contents links to.
++/
+bool[string] _reserved_heading_anchor_tags() {
+ bool[string] _reserved;
+ foreach (_name; [
+ "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb",
+ "_the_title",
+ ]) {
+ _reserved[_name] = true;
+ }
+ return _reserved;
+}
string lev_anchor_tag;
string[string][string] tag_assoc;
string[] lv0to3_tags;
diff --git a/org/out_sqlite.org b/org/out_sqlite.org
index a9f1065..418b2d3 100644
--- a/org/out_sqlite.org
+++ b/org/out_sqlite.org
@@ -875,14 +875,40 @@ string inline_images(M,O)(
_img_pth = "../../../image/";
}
if (_txt.match(rgx.inline_image)) {
- _txt = _txt.replaceAll( // TODO bug where image dimensions (w or h) not given & consequently set to 0; should not be used (calculate earlier, abstraction)
- rgx.inline_image,
- ("$1<img src=\""
- ~ _img_pth
- ~ "$3\" width=\"$4\" height=\"$5\" /> $6"));
+ _txt = replaceAll!(m =>
+ m["pre"].to!string
+ ~ _img_element(_img_pth ~ m["img"].to!string, m["width"].to!string,
+ m["height"].to!string, _image_alt(m["post"].to!string))
+ ~ " " ~ _image_rest(m["post"].to!string)
+ )(_txt, rgx.inline_image);
}
return _txt;
}
+/+ ↓ the same treatment of an image as the html and epub writers give
+ it, because this is the html the cgi search renders: the markup's
+ description is the alt text, and dimensions that are not there are
+ not written as zeroes.
++/
+string _image_alt(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m["alt"].to!string;
+ }
+ return "";
+}
+string _image_rest(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m.post.to!string;
+ }
+ return _post;
+}
+string _img_element(string _src, string _w, string _h, string _alt) {
+ string _dim = (_w == "0" && _h == "0")
+ ? ""
+ : " width=\"" ~ _w ~ "\" height=\"" ~ _h ~ "\"";
+ return "<img src=\"" ~ _src ~ "\""
+ ~ " alt=\"" ~ html_special_characters(_alt) ~ "\""
+ ~ _dim ~ " />";
+}
#+END_SRC
******* links
diff --git a/org/out_xmls.org b/org/out_xmls.org
index 4d1c4a7..75d61a0 100644
--- a/org/out_xmls.org
+++ b/org/out_xmls.org
@@ -36,6 +36,7 @@ template outputXHTMLs() {
static auto rgx = RgxO();
static auto rgx_xhtml = RgxXHTML();
<<xhtml_format_objects_div_delimit>>
+ <<xhtml_format_objects_dpub_aria>>
<<xhtml_format_objects_special_characters_text>>
<<xhtml_format_objects_special_characters_plain>>
<<xhtml_format_objects_special_characters_date>>
@@ -135,13 +136,13 @@ string div_delimit(
delimit_ ~= "\n<div class=\"doc_title\">\n" ;
break;
case "toc":
- delimit_ ~= "\n<div class=\"doc_toc\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_toc\"" ~ _section_role(section) ~ ">\n" ;
break;
case "bookindex":
- delimit_ ~= "\n<div class=\"doc_bookindex\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_bookindex\"" ~ _section_role(section) ~ ">\n" ;
break;
default:
- delimit_ ~= "\n<div class=\"doc_" ~ section ~ "\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_" ~ section ~ "\"" ~ _section_role(section) ~ ">\n" ;
break;
}
if (previous_section.length > 0) {
@@ -155,6 +156,46 @@ string div_delimit(
}
#+END_SRC
+**** dpub aria
+
+#+NAME: xhtml_format_objects_dpub_aria
+#+BEGIN_SRC d
+/+ ↓ the DPUB-ARIA role for a document section.
+ spine has always known what these sections are; the output never
+ said so, and "doc_endnotes" as a class name means something to a
+ stylesheet and nothing to a reading system or a screen reader.
+ These roles are what carry that structure across. They are plain
+ ARIA, valid in html as well as in the epub, so both get them.
+ .
+ A section with no role of its own returns nothing rather than an
+ invented one.
++/
+/+ ↓ and the role of an individual object, where it has one of its own.
+ A gathered endnote is a note body, whatever the section around it
+ says; the reference to it carries doc-noteref at the other end.
+ .
+ Not doc-biblioentry for a bibliography entry: DPUB-ARIA 1.1
+ deprecated it, and the entries sit inside doc-bibliography, which
+ is what a reader needs.
++/
+string _para_role(O)(O obj) {
+ switch (obj.metainfo.is_a) {
+ case "endnote": return " role=\"doc-footnote\"";
+ default: return "";
+ }
+}
+string _section_role(string _section) {
+ switch (_section) {
+ case "toc": return " role=\"doc-toc\"";
+ case "endnotes": return " role=\"doc-endnotes\"";
+ case "bibliography": return " role=\"doc-bibliography\"";
+ case "glossary": return " role=\"doc-glossary\"";
+ case "bookindex": return " role=\"doc-index\"";
+ default: return "";
+ }
+}
+#+END_SRC
+
**** special characters text
#+NAME: xhtml_format_objects_special_characters_text
@@ -300,7 +341,7 @@ string font_face(string _txt) {
}
#+END_SRC
-**** anchor tags
+**** heading element
#+NAME: xhtml_format_objects_xml_heading_element
#+BEGIN_SRC d
@@ -321,12 +362,29 @@ int _heading_element_level(O)(O obj, string _xml_type) {
#+NAME: xhtml_format_objects_xml_anchor_tags
#+BEGIN_SRC d
+/+ ↓ the anchors an object carries.
+ "name" on an <a> is obsolete; "id" is what a link target is. The
+ two are not interchangeable here, because these tags are not all
+ link targets. A gathered endnote carries exactly one, "note_<n>",
+ which is unique in the document and is what every reference to it
+ points at, so that one becomes an id.
+ .
+ A book index entry carries the same marker on every object the
+ entry names - "loc" appears 296 times in one document - and those
+ are markers of where a term occurs, not distinct targets. Making
+ them ids would put the same id in a document hundreds of times,
+ so they stay as they are until the book index gives them
+ something unique to be.
++/
string _xhtml_anchor_tags(O)(O obj) {
string tags="";
+ bool _is_a_unique_target = (obj.metainfo.is_a == "endnote");
if (obj.tags.anchor_tags.length > 0) {
foreach (tag; obj.tags.anchor_tags) {
if (!(tag.empty)) {
- tags ~= "<a name=\"" ~ special_characters_text(tag) ~ "\"></a>"; /+ repeated markers, not unique targets: see below +/
+ tags ~= (_is_a_unique_target)
+ ? "<a id=\"" ~ special_characters_text(tag) ~ "\"></a>"
+ : "<a name=\"" ~ special_characters_text(tag) ~ "\"></a>";
}
}
}
@@ -710,17 +768,54 @@ string inline_images(O,M)(
default: break;
}
if (_txt.match(rgx.inline_image)) {
- _txt = _txt
- .replaceAll(rgx.inline_image,
- ("$1<img src=\""
- ~ _img_pth
- ~ "$3\" width=\"$4\" height=\"$5\" /> $6"))
- .replaceAll(
- rgx.inline_link_empty,
- ("$1"));
+ _txt = replaceAll!(m =>
+ m["pre"].to!string
+ ~ _img_element(_img_pth ~ m["img"].to!string, m["width"].to!string,
+ m["height"].to!string, _image_alt(m["post"].to!string))
+ ~ " " ~ _image_rest(m["post"].to!string)
+ )(_txt, rgx.inline_image);
+ _txt = _txt.replaceAll(rgx.inline_link_empty, ("$1"));
}
return _txt;
}
+/+ ↓ an image's alt text, which markup carries and every writer was
+ throwing away: { tux.png 64x80 "a better way" }image. The project's
+ own markup reference calls it alt text, and it was being left in the
+ visible flow as a quoted string beside the image instead, so no <img>
+ spine wrote had an alt at all.
++/
+string _image_alt(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m["alt"].to!string;
+ }
+ return "";
+}
+/+ ↓ and what is left of the markup once the alt text is taken out of it +/
+string _image_rest(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m.post.to!string;
+ }
+ return _post;
+}
+/+ ↓ the <img> itself.
+ alt is required, and an image with nothing to say for itself takes
+ alt="", which is the way to say "decorative, skip me". No alt at all
+ is not that, it is an omission, and a reader has to announce the file
+ name instead.
+ .
+ width and height are dropped when there are none. They were written
+ as width="0" height="0", which is a perfectly valid pair of HTML
+ integers meaning zero by zero, so the image did not appear at all;
+ max-width in the stylesheet caps a width and cannot raise one.
++/
+string _img_element(string _src, string _w, string _h, string _alt) {
+ string _dim = (_w == "0" && _h == "0")
+ ? ""
+ : " width=\"" ~ _w ~ "\" height=\"" ~ _h ~ "\"";
+ return "<img src=\"" ~ _src ~ "\""
+ ~ " alt=\"" ~ special_characters_plain(_alt) ~ "\""
+ ~ _dim ~ " />";
+}
#+END_SRC
**** links
@@ -837,14 +932,14 @@ string inline_notes_scroll(O,M)(
_txt = font_face(_txt);
_txt = _txt.replaceAll(
rgx.inline_notes_al_regular_number_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
if (obj.has.inline_notes_star) {
_txt = font_face(_txt);
_txt = _txt.replaceAll(
rgx.inline_notes_al_special_char_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
debug(markup_endnotes) {
@@ -877,7 +972,7 @@ Tuple!(string, string[]) inline_notes_seg(O,M)(
foreach(m; _txt.matchAll(rgx.inline_notes_al_special_char_note)) {
_endnotes ~= format(
"%s%s%s%s\n %s%s%s%s%s %s\n%s",
- "<p class=\"endnote\">",
+ "<p class=\"endnote\" role=\"doc-footnote\">",
"<a href=\"#noteref_",
m.captures[1],
"\">",
@@ -892,7 +987,7 @@ Tuple!(string, string[]) inline_notes_seg(O,M)(
}
_txt = _txt.replaceAll(
rgx.inline_notes_al_special_char_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
if (obj.has.inline_notes_reg) {
@@ -901,7 +996,7 @@ Tuple!(string, string[]) inline_notes_seg(O,M)(
foreach(m; _txt.matchAll(rgx.inline_notes_al_regular_number_note)) {
_endnotes ~= format(
"%s%s%s%s\n %s%s%s%s%s %s\n%s",
- "<p class=\"endnote\">",
+ "<p class=\"endnote\" role=\"doc-footnote\">",
"<a href=\"#noteref_",
m.captures[1],
"\">",
@@ -916,7 +1011,7 @@ Tuple!(string, string[]) inline_notes_seg(O,M)(
}
_txt = _txt.replaceAll(
rgx.inline_notes_al_regular_number_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
} else if (_txt.match(rgx.inline_notes_al_regular_number_note)) {
debug(markup) {
@@ -1242,7 +1337,7 @@ string para(O,M)(
if (!(obj.metainfo.identifier.empty)) {
o = format(q"┃ <div class="substance">
<label class="ocn"><a href="#%s" class="lnkocn">%s</a></label>
- <p class="%s" data-indent="h%si%s" id="%s">%s
+ <p class="%s" data-indent="h%si%s" id="%s"%s>%s
%s
</p>
</div>┃",
@@ -1256,18 +1351,20 @@ string para(O,M)(
obj.attrib.indent_hang,
obj.attrib.indent_base,
obj.metainfo.identifier,
+ _para_role(obj),
tags,
_txt
);
} else {
o = format(q"┃ <div class="substance">
- <p class="%s" data-indent="h%si%s">%s
+ <p class="%s" data-indent="h%si%s"%s>%s
%s
</p>
</div>┃",
obj.metainfo.is_a,
obj.attrib.indent_hang,
obj.attrib.indent_base,
+ _para_role(obj),
tags,
_txt
);
@@ -2604,6 +2701,140 @@ string _image_media_type(string _ext) {
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,
@@ -2696,13 +2927,13 @@ string epub3_oebps_content(D,P)(D doc, P parts) {
<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" />
@@ -2724,6 +2955,7 @@ string epub3_oebps_content(D,P)(D doc, P parts) {
(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"];
@@ -2792,7 +3024,7 @@ string epub3_oebps_toc_nav_xhtml(D)(D doc) {
<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,
);
@@ -2883,7 +3115,9 @@ string epub3_oebps_toc_nav_xhtml(D)(D doc) {
if (n == 0) {
_toc_nav_tail ~=" </nav>
</section>
-</body>
+";
+ _toc_nav_tail ~= _epub_landmarks(doc);
+ _toc_nav_tail ~=" </body>
</html>\n";
}
}
diff --git a/org/tests_for_document_abstraction_shell_scripts.org b/org/tests_for_document_abstraction_shell_scripts.org
index 369a7f6..e321286 100644
--- a/org/tests_for_document_abstraction_shell_scripts.org
+++ b/org/tests_for_document_abstraction_shell_scripts.org
@@ -1282,11 +1282,7 @@ SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# zero is the point: anything at all that epubcheck calls an error is a
# regression, and there is no list to add to.
#
-# Two warnings remain and are not counted here. Both are NAV-011 in the
-# same shape, in two documents: the navigation document lists one file
-# twice, and the repeat is behind the previous entry in spine order. It
-# comes from how the endnotes part and section headings are ordered, not
-# from the navigation writer.
+# No warning is outstanding either, though warnings are not counted here.
FATALS_ALLOWED=${SpineEpubFatalsAllowed:-0}
ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-0}
@@ -1301,11 +1297,6 @@ if [ -d "$SAMPLES_RAW" ]; then
else
SAMPLES_DIR="$SAMPLES_RAW"
fi
-# SPINE_BIN="${1:-}"
-# if [ -z "$SPINE_BIN" ] || [ ! -x "$SPINE_BIN" ]; then
-# echo "ERROR: give the spine binary, e.g. ./bin/spine-ldc" >&2
-# exit 2
-# fi
SPINE_BIN="${1:-}"
# find spine binary
if [ -z "$SPINE_BIN" ]; then
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src.d b/src/sisudoc/ocda/meta/metadoc_from_src.d
index c856de3..2731573 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src.d
@@ -95,7 +95,7 @@ template docAbstraction() {
reset_note_numbers = true;
lev_anchor_tag = "";
anchor_tag = "";
- _heading_anchor_tags_seen = _heading_anchor_tags_seen.init;
+ _heading_anchor_tags_seen = _reserved_heading_anchor_tags();
}
mixin spineNode;
int[string] node_para_int_ = node_metadata_para_int;
@@ -1605,7 +1605,7 @@ template docAbstraction() {
content_non_header = "8";
lev_anchor_tag = "";
anchor_tag = "";
- _heading_anchor_tags_seen = _heading_anchor_tags_seen.init;
+ _heading_anchor_tags_seen = _reserved_heading_anchor_tags();
// identify parts
/+ ↓ what the document has, as plain data. ST_DocHas is in
metadoc_object_setter so that it can also be built from a loaded
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
index 08ee22c..0deeb72 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
@@ -67,6 +67,27 @@ template docAbstractionFunctions() {
string anchor_tag_;
string[string] tag_in_seg;
bool[string] _heading_anchor_tags_seen;
+ /+ ↓ the segment names spine makes for itself, which a heading in the
+ document may not also claim.
+ .
+ A document that writes "1~endnotes Endnote" asks for the name spine
+ gives the gathered endnotes. Both then wrote into endnotes.xhtml,
+ and the navigation listed that file twice, the second time behind
+ its own previous entry in reading order. Seeding the seen set with
+ these names means the document's heading is disambiguated the same
+ way a repeated anchor tag is, and the generated section keeps the
+ name that the table of contents links to.
+ +/
+ bool[string] _reserved_heading_anchor_tags() {
+ bool[string] _reserved;
+ foreach (_name; [
+ "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb",
+ "_the_title",
+ ]) {
+ _reserved[_name] = true;
+ }
+ return _reserved;
+ }
string lev_anchor_tag;
string[string][string] tag_assoc;
string[] lv0to3_tags;
diff --git a/src/sisudoc/ocda/meta/rgx.d b/src/sisudoc/ocda/meta/rgx.d
index 19b1c7c..4cc40ae 100644
--- a/src/sisudoc/ocda/meta/rgx.d
+++ b/src/sisudoc/ocda/meta/rgx.d
@@ -243,6 +243,10 @@ static template spineRgxIn() {
static inline_image = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_without_dimensions = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>0)h(?P<height>0))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_info = ctRegex!(`☼?(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+)`, "mg");
+ /+ the image's alt text: what markup writes as { tux.png 64x80 "alt" }image,
+ which by this stage is a quoted string leading whatever follows the image
+ +/
+ static inline_image_alt = ctRegex!(`^\s*[“"](?P<alt>[^”"]*)[”"]`, "m");
static inline_link_anchor = ctRegex!(`┃(?P<anchor>\S+?)┃`, "mg"); // TODO *~text_link_anchor
static inline_link = ctRegex!(`┥(?P<text>.+?)┝┤(?P<link>#?(\S+?))├`, "mg");
static inline_link_empty = ctRegex!(`┥(?P<text>.+?)┝┤├`, "mg");
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";
}
}
diff --git a/src/sisudoc/outputs/io_out/rgx.d b/src/sisudoc/outputs/io_out/rgx.d
index c915076..7ce87e0 100644
--- a/src/sisudoc/outputs/io_out/rgx.d
+++ b/src/sisudoc/outputs/io_out/rgx.d
@@ -100,6 +100,10 @@ static template spineRgxOut() {
static inline_image = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_without_dimensions = ctRegex!(`(?P<pre>┥)☼(?P<imginf>(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>0)h(?P<height>0))\s*(?P<post>.*?┝┤.*?├)`, "mg");
static inline_image_info = ctRegex!(`☼?(?P<img>[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P<width>\d+)h(?P<height>\d+)`, "mg");
+ /+ the image's alt text: what markup writes as { tux.png 64x80 "alt" }image,
+ which by this stage is a quoted string leading whatever follows the image
+ +/
+ static inline_image_alt = ctRegex!(`^\s*[“"](?P<alt>[^”"]*)[”"]`, "m");
static inline_link_anchor = ctRegex!(`┃(?P<anchor>\S+?)┃`, "mg"); // TODO *~text_link_anchor
static inline_link = ctRegex!(`┥(?P<text>.+?)┝┤(?P<link>#?(\S+?))├`, "mg");
static inline_link_empty = ctRegex!(`┥(?P<text>.+?)┝┤├`, "mg");
diff --git a/src/sisudoc/outputs/io_out/sqlite_collection_db.d b/src/sisudoc/outputs/io_out/sqlite_collection_db.d
index f1556c4..2a7fcd7 100644
--- a/src/sisudoc/outputs/io_out/sqlite_collection_db.d
+++ b/src/sisudoc/outputs/io_out/sqlite_collection_db.d
@@ -487,14 +487,40 @@ template SQLiteFormatAndLoadObject() {
_img_pth = "../../../image/";
}
if (_txt.match(rgx.inline_image)) {
- _txt = _txt.replaceAll( // TODO bug where image dimensions (w or h) not given & consequently set to 0; should not be used (calculate earlier, abstraction)
- rgx.inline_image,
- ("$1<img src=\""
- ~ _img_pth
- ~ "$3\" width=\"$4\" height=\"$5\" /> $6"));
+ _txt = replaceAll!(m =>
+ m["pre"].to!string
+ ~ _img_element(_img_pth ~ m["img"].to!string, m["width"].to!string,
+ m["height"].to!string, _image_alt(m["post"].to!string))
+ ~ " " ~ _image_rest(m["post"].to!string)
+ )(_txt, rgx.inline_image);
}
return _txt;
}
+ /+ ↓ the same treatment of an image as the html and epub writers give
+ it, because this is the html the cgi search renders: the markup's
+ description is the alt text, and dimensions that are not there are
+ not written as zeroes.
+ +/
+ string _image_alt(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m["alt"].to!string;
+ }
+ return "";
+ }
+ string _image_rest(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m.post.to!string;
+ }
+ return _post;
+ }
+ string _img_element(string _src, string _w, string _h, string _alt) {
+ string _dim = (_w == "0" && _h == "0")
+ ? ""
+ : " width=\"" ~ _w ~ "\" height=\"" ~ _h ~ "\"";
+ return "<img src=\"" ~ _src ~ "\""
+ ~ " alt=\"" ~ html_special_characters(_alt) ~ "\""
+ ~ _dim ~ " />";
+ }
string inline_links(M,O)(
M doc_matters,
const O obj,
diff --git a/src/sisudoc/outputs/io_out/xmls.d b/src/sisudoc/outputs/io_out/xmls.d
index c1e733e..4ce5434 100644
--- a/src/sisudoc/outputs/io_out/xmls.d
+++ b/src/sisudoc/outputs/io_out/xmls.d
@@ -78,13 +78,13 @@ template outputXHTMLs() {
delimit_ ~= "\n<div class=\"doc_title\">\n" ;
break;
case "toc":
- delimit_ ~= "\n<div class=\"doc_toc\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_toc\"" ~ _section_role(section) ~ ">\n" ;
break;
case "bookindex":
- delimit_ ~= "\n<div class=\"doc_bookindex\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_bookindex\"" ~ _section_role(section) ~ ">\n" ;
break;
default:
- delimit_ ~= "\n<div class=\"doc_" ~ section ~ "\">\n" ;
+ delimit_ ~= "\n<div class=\"doc_" ~ section ~ "\"" ~ _section_role(section) ~ ">\n" ;
break;
}
if (previous_section.length > 0) {
@@ -96,6 +96,40 @@ template outputXHTMLs() {
// you also need to close the last div, introduce a footer?
return delimit;
}
+ /+ ↓ the DPUB-ARIA role for a document section.
+ spine has always known what these sections are; the output never
+ said so, and "doc_endnotes" as a class name means something to a
+ stylesheet and nothing to a reading system or a screen reader.
+ These roles are what carry that structure across. They are plain
+ ARIA, valid in html as well as in the epub, so both get them.
+ .
+ A section with no role of its own returns nothing rather than an
+ invented one.
+ +/
+ /+ ↓ and the role of an individual object, where it has one of its own.
+ A gathered endnote is a note body, whatever the section around it
+ says; the reference to it carries doc-noteref at the other end.
+ .
+ Not doc-biblioentry for a bibliography entry: DPUB-ARIA 1.1
+ deprecated it, and the entries sit inside doc-bibliography, which
+ is what a reader needs.
+ +/
+ string _para_role(O)(O obj) {
+ switch (obj.metainfo.is_a) {
+ case "endnote": return " role=\"doc-footnote\"";
+ default: return "";
+ }
+ }
+ string _section_role(string _section) {
+ switch (_section) {
+ case "toc": return " role=\"doc-toc\"";
+ case "endnotes": return " role=\"doc-endnotes\"";
+ case "bibliography": return " role=\"doc-bibliography\"";
+ case "glossary": return " role=\"doc-glossary\"";
+ case "bookindex": return " role=\"doc-index\"";
+ default: return "";
+ }
+ }
string special_characters_text(string _txt) {
_txt = _txt
.replaceAll(rgx_xhtml.ampersand, "&amp;") // "&#38;"
@@ -216,12 +250,29 @@ template outputXHTMLs() {
if (_xml_type == "epub" && _lev == 0) { return 1; }
return (_lev > 6) ? 6 : _lev;
}
+ /+ ↓ the anchors an object carries.
+ "name" on an <a> is obsolete; "id" is what a link target is. The
+ two are not interchangeable here, because these tags are not all
+ link targets. A gathered endnote carries exactly one, "note_<n>",
+ which is unique in the document and is what every reference to it
+ points at, so that one becomes an id.
+ .
+ A book index entry carries the same marker on every object the
+ entry names - "loc" appears 296 times in one document - and those
+ are markers of where a term occurs, not distinct targets. Making
+ them ids would put the same id in a document hundreds of times,
+ so they stay as they are until the book index gives them
+ something unique to be.
+ +/
string _xhtml_anchor_tags(O)(O obj) {
string tags="";
+ bool _is_a_unique_target = (obj.metainfo.is_a == "endnote");
if (obj.tags.anchor_tags.length > 0) {
foreach (tag; obj.tags.anchor_tags) {
if (!(tag.empty)) {
- tags ~= "<a name=\"" ~ special_characters_text(tag) ~ "\"></a>"; /+ repeated markers, not unique targets: see below +/
+ tags ~= (_is_a_unique_target)
+ ? "<a id=\"" ~ special_characters_text(tag) ~ "\"></a>"
+ : "<a name=\"" ~ special_characters_text(tag) ~ "\"></a>";
}
}
}
@@ -552,17 +603,54 @@ string tail(M)(M doc_matters) {
default: break;
}
if (_txt.match(rgx.inline_image)) {
- _txt = _txt
- .replaceAll(rgx.inline_image,
- ("$1<img src=\""
- ~ _img_pth
- ~ "$3\" width=\"$4\" height=\"$5\" /> $6"))
- .replaceAll(
- rgx.inline_link_empty,
- ("$1"));
+ _txt = replaceAll!(m =>
+ m["pre"].to!string
+ ~ _img_element(_img_pth ~ m["img"].to!string, m["width"].to!string,
+ m["height"].to!string, _image_alt(m["post"].to!string))
+ ~ " " ~ _image_rest(m["post"].to!string)
+ )(_txt, rgx.inline_image);
+ _txt = _txt.replaceAll(rgx.inline_link_empty, ("$1"));
}
return _txt;
}
+ /+ ↓ an image's alt text, which markup carries and every writer was
+ throwing away: { tux.png 64x80 "a better way" }image. The project's
+ own markup reference calls it alt text, and it was being left in the
+ visible flow as a quoted string beside the image instead, so no <img>
+ spine wrote had an alt at all.
+ +/
+ string _image_alt(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m["alt"].to!string;
+ }
+ return "";
+ }
+ /+ ↓ and what is left of the markup once the alt text is taken out of it +/
+ string _image_rest(string _post) {
+ if (auto m = _post.matchFirst(rgx.inline_image_alt)) {
+ return m.post.to!string;
+ }
+ return _post;
+ }
+ /+ ↓ the <img> itself.
+ alt is required, and an image with nothing to say for itself takes
+ alt="", which is the way to say "decorative, skip me". No alt at all
+ is not that, it is an omission, and a reader has to announce the file
+ name instead.
+ .
+ width and height are dropped when there are none. They were written
+ as width="0" height="0", which is a perfectly valid pair of HTML
+ integers meaning zero by zero, so the image did not appear at all;
+ max-width in the stylesheet caps a width and cannot raise one.
+ +/
+ string _img_element(string _src, string _w, string _h, string _alt) {
+ string _dim = (_w == "0" && _h == "0")
+ ? ""
+ : " width=\"" ~ _w ~ "\" height=\"" ~ _h ~ "\"";
+ return "<img src=\"" ~ _src ~ "\""
+ ~ " alt=\"" ~ special_characters_plain(_alt) ~ "\""
+ ~ _dim ~ " />";
+ }
string inline_links(O,M)(
string _txt,
const O obj,
@@ -665,14 +753,14 @@ string tail(M)(M doc_matters) {
_txt = font_face(_txt);
_txt = _txt.replaceAll(
rgx.inline_notes_al_regular_number_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
if (obj.has.inline_notes_star) {
_txt = font_face(_txt);
_txt = _txt.replaceAll(
rgx.inline_notes_al_special_char_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
debug(markup_endnotes) {
@@ -699,7 +787,7 @@ string tail(M)(M doc_matters) {
foreach(m; _txt.matchAll(rgx.inline_notes_al_special_char_note)) {
_endnotes ~= format(
"%s%s%s%s\n %s%s%s%s%s %s\n%s",
- "<p class=\"endnote\">",
+ "<p class=\"endnote\" role=\"doc-footnote\">",
"<a href=\"#noteref_",
m.captures[1],
"\">",
@@ -714,7 +802,7 @@ string tail(M)(M doc_matters) {
}
_txt = _txt.replaceAll(
rgx.inline_notes_al_special_char_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
}
if (obj.has.inline_notes_reg) {
@@ -723,7 +811,7 @@ string tail(M)(M doc_matters) {
foreach(m; _txt.matchAll(rgx.inline_notes_al_regular_number_note)) {
_endnotes ~= format(
"%s%s%s%s\n %s%s%s%s%s %s\n%s",
- "<p class=\"endnote\">",
+ "<p class=\"endnote\" role=\"doc-footnote\">",
"<a href=\"#noteref_",
m.captures[1],
"\">",
@@ -738,7 +826,7 @@ string tail(M)(M doc_matters) {
}
_txt = _txt.replaceAll(
rgx.inline_notes_al_regular_number_note,
- ("<a href=\"#note_$1\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
+ ("<a href=\"#note_$1\" role=\"doc-noteref\"><span class=\"noteref\" id=\"noteref_$1\">&#160;<sup>$1</sup> </span></a>")
);
} else if (_txt.match(rgx.inline_notes_al_regular_number_note)) {
debug(markup) {
@@ -1012,7 +1100,7 @@ string tail(M)(M doc_matters) {
if (!(obj.metainfo.identifier.empty)) {
o = format(q"┃ <div class="substance">
<label class="ocn"><a href="#%s" class="lnkocn">%s</a></label>
- <p class="%s" data-indent="h%si%s" id="%s">%s
+ <p class="%s" data-indent="h%si%s" id="%s"%s>%s
%s
</p>
</div>┃",
@@ -1026,18 +1114,20 @@ string tail(M)(M doc_matters) {
obj.attrib.indent_hang,
obj.attrib.indent_base,
obj.metainfo.identifier,
+ _para_role(obj),
tags,
_txt
);
} else {
o = format(q"┃ <div class="substance">
- <p class="%s" data-indent="h%si%s">%s
+ <p class="%s" data-indent="h%si%s"%s>%s
%s
</p>
</div>┃",
obj.metainfo.is_a,
obj.attrib.indent_hang,
obj.attrib.indent_base,
+ _para_role(obj),
tags,
_txt
);
diff --git a/test/reference/abstraction/on_the_net_and_information.ralph_amissah.en.ssp b/test/reference/abstraction/on_the_net_and_information.ralph_amissah.en.ssp
index ea1b50a..e98bf9d 100644
--- a/test/reference/abstraction/on_the_net_and_information.ralph_amissah.en.ssp
+++ b/test/reference/abstraction/on_the_net_and_information.ralph_amissah.en.ssp
@@ -1424,7 +1424,7 @@
.sha256: ABAA10CA88E7D7A5A9290FB673E24262604FF49EDBC4BC8C5BCE1534AAD2E57C
.segment: s5
.segment_prev: s4
-.segment_next: endnotes
+.segment_next: endnotes-88
.heading_lev_anchor: s5
.segment_epub: s5
.segment_html_is: s5
@@ -1505,13 +1505,13 @@
.heading_lev_collapsed: 1
.is_of_type: para
.sha256: E021134E9113DC644B24B91718C4C39277F756F9E2CD68681AD7093906F43BA5
-.segment: endnotes
+.segment: endnotes-88
.segment_prev: s5
.segment_next: endnotes
-.heading_lev_anchor: endnotes
-.segment_epub: endnotes
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.heading_lev_anchor: endnotes-88
+.segment_epub: endnotes-88
+.segment_html_is: endnotes-88
+.segment_epub_is: endnotes-88
.heading_ancestors_text: On the Net* and the liberation of information that “wants” to be free** - A call for action by the United Nations, Universities, researchers, and development agencies, with reference primarily to international trade law, Ralph Amissah||||Endnotes|||
| Endnotes
@@ -1523,8 +1523,8 @@
.parent_lev: 4
.is_of_type: para
.sha256: 4A9D3865F6C549EA688678607007F09160E8B2C86ED46C12BD99FFAF0F5F9D5A
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-88
+.segment_epub_is: endnotes-88
| * The Net as used here refers primarily to The World Wide Web and The Internet as it has been since the advent of first graphical browsers, which greatly simplified its use. The Net is used to include not only the communication channels it provides but also the connected storage devices. The focus is of the paper is on writing and information repositories, and the meaning applied to “the Net” should be adjusted accordingly. Traditional commercial databases using telnet applications are excluded from our discourse, however, the Net is growing in features and flexibility to allow publication of information to be based on all kinds of more traditional database applications.
[90] para
@@ -1535,8 +1535,8 @@
.parent_lev: 4
.is_of_type: para
.sha256: B63299BCB5209BCF149381BE70FACDF1E39EF5E70B73BE1A25C895C1EA4B92BC
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-88
+.segment_epub_is: endnotes-88
| ** Work paper for the 10th Anniversary Publication of the Law Faculty of the University of Troms&oslash;. First published on the Net - 7⑆^┨th┣^ February, 1997. It is intended that further research be done into the subject matter of this paper.
[91] para
@@ -1547,8 +1547,8 @@
.parent_lev: 4
.is_of_type: para
.sha256: 7FD8B932957C6C9C10B947D6DF240A4B89CDAAE4565BFE40E7EF3DCB1B951344
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-88
+.segment_epub_is: endnotes-88
| *** Information Technology consultant and lecturer of private law aspects of international trade, at the Law Faculty of the University of Tromsø. Creator of ITL (International Trade Law) formerly called Ananse, which is described briefly within the text of this article.
}
@@ -1585,7 +1585,7 @@
.ocn_off: true
.is_of_type: para
.segment: endnotes
-.segment_prev: endnotes
+.segment_prev: endnotes-88
.segment_epub: endnotes
.heading_ancestors_text: On the Net* and the liberation of information that “wants” to be free** - A call for action by the United Nations, Universities, researchers, and development agencies, with reference primarily to international trade law, Ralph Amissah|Endnotes|||Endnotes|||
.anchor_tag: endnotes
diff --git a/test/reference/abstraction/the_autonomous_contract.ralph_amissah.en.ssp b/test/reference/abstraction/the_autonomous_contract.ralph_amissah.en.ssp
index 353b26f..f528aee 100644
--- a/test/reference/abstraction/the_autonomous_contract.ralph_amissah.en.ssp
+++ b/test/reference/abstraction/the_autonomous_contract.ralph_amissah.en.ssp
@@ -2266,7 +2266,7 @@
.sha256: EE64740DDA73D07992DB26832B492AC45117B03A8A49C72435024D9C459C42E8
.segment: seg_5
.segment_prev: seg_4
-.segment_next: endnotes
+.segment_next: endnotes-135
.heading_lev_anchor: seg_5
.segment_epub: seg_5
.segment_html_is: seg_5
@@ -2298,13 +2298,13 @@
.parent_lev: 1
.is_of_type: para
.sha256: D076DC82E70B30DCF40CC4ED6F1A9F3EA9E44DF6DD7003075BC52989DF4446F1
-.segment: endnotes
+.segment: endnotes-135
.segment_prev: seg_5
.segment_next: endnotes
-.heading_lev_anchor: endnotes
-.segment_epub: endnotes
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.heading_lev_anchor: endnotes-135
+.segment_epub: endnotes-135
+.segment_html_is: endnotes-135
+.segment_epub_is: endnotes-135
.heading_ancestors_text: The Autonomous Contract|Reflecting the borderless electronic-commercial environment in contracting|||6. Endnote|||
| 6. Endnote
@@ -2319,8 +2319,8 @@
.has: links
.stow_link: http://itl.irv.uit.no/trade_law/
.stow_link: http://tradelaw.net/
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-135
+.segment_epub_is: endnotes-135
| * Lectures on private law aspects of international trade at the Faculty of Law, University of Tromsø, Norway, and created ⑆/┨ ITL - International Trade Law Monitor ┣/ ┥http://itl.irv.uit.no/trade_law/┝┤0├ or ┥http://tradelaw.net/┝┤1├ off which a large number of the (black letter) legal texts referred to in this paper are available.
[137] para
@@ -2331,8 +2331,8 @@
.parent_lev: 4
.is_of_type: para
.sha256: BD0AA6850BB8BD657287CE8684F79815A0F9CCD97B151C972B68B36A0AF3F24F
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-135
+.segment_epub_is: endnotes-135
| Thanks are due to Albert Kritzer, the Executive Secretary, Institute of International Commercial Law, Pace University School of Law and Petri Keskitalo, Research Fellow, Faculty of Law, University of Troms&oslash;, for reading and commenting on this text in its preparatory stages. The views expressed here and any remaining errors are my sole responsibility.
[138] para
@@ -2343,8 +2343,8 @@
.parent_lev: 4
.is_of_type: para
.sha256: F27CBB05EFB0BBA932BD9918462309EF8DCDFC9CF158FF9ECA64CC78285DCEEC
-.segment_html_is: endnotes
-.segment_epub_is: endnotes
+.segment_html_is: endnotes-135
+.segment_epub_is: endnotes-135
| URLs are occasionally provided as references. These are subject to change without notice.
}
@@ -2381,7 +2381,7 @@
.ocn_off: true
.is_of_type: para
.segment: endnotes
-.segment_prev: endnotes
+.segment_prev: endnotes-135
.segment_epub: endnotes
.heading_ancestors_text: The Autonomous Contract|Endnotes|||Endnotes|||
.anchor_tag: endnotes
diff --git a/test/test-epub-validity.sh b/test/test-epub-validity.sh
index b3fde21..6eab838 100755
--- a/test/test-epub-validity.sh
+++ b/test/test-epub-validity.sh
@@ -38,11 +38,7 @@ SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# zero is the point: anything at all that epubcheck calls an error is a
# regression, and there is no list to add to.
#
-# Two warnings remain and are not counted here. Both are NAV-011 in the
-# same shape, in two documents: the navigation document lists one file
-# twice, and the repeat is behind the previous entry in spine order. It
-# comes from how the endnotes part and section headings are ordered, not
-# from the navigation writer.
+# No warning is outstanding either, though warnings are not counted here.
FATALS_ALLOWED=${SpineEpubFatalsAllowed:-0}
ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-0}
@@ -57,11 +53,6 @@ if [ -d "$SAMPLES_RAW" ]; then
else
SAMPLES_DIR="$SAMPLES_RAW"
fi
-# SPINE_BIN="${1:-}"
-# if [ -z "$SPINE_BIN" ] || [ ! -x "$SPINE_BIN" ]; then
-# echo "ERROR: give the spine binary, e.g. ./bin/spine-ldc" >&2
-# exit 2
-# fi
SPINE_BIN="${1:-}"
# find spine binary
if [ -z "$SPINE_BIN" ]; then