diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-13 20:53:26 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-14 12:18:51 -0400 |
| commit | 6835275408e1629f450d163e0cc5221c977e87d2 (patch) | |
| tree | ac35771a889a4766d8efc89af5f78e91fa99e3d3 | |
| parent | regex: confine url within an endnote (delimiter) (diff) | |
epub and html: no errors left over the sample set
epubcheck 5.3.0 over the 35 sample epubs went from 1302 remaining errors
to none. The six causes for those remaining errors fixed here.
most of the count was one of them:
- Tables (1123 errors, 86%) where the writer emitted obsolete
attributes, removed in 2014; table also now in div instead of a
paragraph. also fixed:
- Navigation (115). Every level 4 heading's nav entry ended "#0", the
ocn of an object that has no ocn and so no id.
- Duplicate ids (37). (from three unrelated sources).
- The publication identifier (27 warnings). dc:identifier was a
hardcoded hex string, not a UUID, and the same one in all 35 files,
so two documents in one library collided. It is now UUID v5 over the
document's own uid: a real RFC 4122 identifier, stable across builds
and distinct per document.
- Image manifest entries (15). media-type was "image/" plus the file
extension, giving "image/jpg", which is not a media type; epubcheck
reads it as a foreign resource and wants a fallback. There is now a
mapping, which also covers svg and webp. Item ids came from the file
basename, and "2bits_02_01-100.png" gave an id starting with a digit,
which is not an XML name; ids are now prefixed and sanitised.
- An ocn resolving to no segment (9). A poem block and its first verse
share one ocn; the verses are written into a segment and the block
itself is not.
Also dropped from the package document: an xmlns:xsi that nothing
used, and a prefix declaration for the rendition vocabulary, which is
a reserved prefix and needed no declaring. The package now carries
xml:lang.
test-epub-validity.sh now allows no fatal and no error. Two NAV-011
warnings remain and are described in it. Three reference .ssp files
change, all of them the anchor tag disambiguation above.
(assisted by Claude-Code)
| -rw-r--r-- | org/in_abstraction_artefacts.org | 14 | ||||
| -rw-r--r-- | org/ocda.org | 28 | ||||
| -rw-r--r-- | org/ocda_functions.org | 1 | ||||
| -rw-r--r-- | org/out_sqlite.org | 81 | ||||
| -rw-r--r-- | org/out_xmls.org | 173 | ||||
| -rw-r--r-- | org/out_xmls_css.org | 36 | ||||
| -rw-r--r-- | org/tests_for_document_abstraction_shell_scripts.org | 16 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/doc_has.d | 14 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/metadoc_from_src.d | 28 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/metadoc_from_src_functions.d | 1 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/epub3.d | 68 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/sqlite_collection_db.d | 81 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/xmls.d | 91 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/xmls_css.d | 108 | ||||
| -rw-r--r-- | test/reference/abstraction/content.cory_doctorow.en.ssp | 28 | ||||
| -rw-r--r-- | test/reference/abstraction/live-manual.es.ssp | 2 | ||||
| -rw-r--r-- | test/reference/abstraction/the_public_domain.james_boyle.en.ssp | 20 | ||||
| -rwxr-xr-x | test/test-epub-validity.sh | 16 |
18 files changed, 623 insertions, 183 deletions
diff --git a/org/in_abstraction_artefacts.org b/org/in_abstraction_artefacts.org index 863134c..2852a94 100644 --- a/org/in_abstraction_artefacts.org +++ b/org/in_abstraction_artefacts.org @@ -1436,8 +1436,18 @@ template spineDocHasFromAbstraction() { _tag_assoc[obj.metainfo.identifier]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; } - _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] - = obj.tags.epub_segment_anchor_tag_is; + /+ ↓ An object that is not written into any segment still carries an + identifier, and can share it: a poem block and its verses have one + ocn between them, the verses are written and the block itself is not. + The block comes second, so an unguarded assignment replaced a good + segment name with an empty one, and the link came out as + ".xhtml#297". seg_lv4 above is guarded already, which is why this + showed in the epub and not in the html. + +/ + if (obj.tags.epub_segment_anchor_tag_is.length > 0) { + _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } } } _out.imagelist = _images.sort.uniq.array; diff --git a/org/ocda.org b/org/ocda.org index ee490f4..ce03d32 100644 --- a/org/ocda.org +++ b/org/ocda.org @@ -185,6 +185,7 @@ scope(exit) { reset_note_numbers = true; lev_anchor_tag = ""; anchor_tag = ""; + _heading_anchor_tags_seen = _heading_anchor_tags_seen.init; } mixin spineNode; int[string] node_para_int_ = node_metadata_para_int; @@ -722,6 +723,18 @@ _loopMarkupSrcByLineStruct loopMarkupSrcByLine( = obj_im.obj_inline_markup_and_anchor_tags_and_misc(an_object, an_object_key, conf_make_meta, ((_new_doc) ? Yes._new_doc : No._new_doc)); an_object["substantive"] = substantive_object_and_anchor_tags_struct.obj_txt; anchor_tag = substantive_object_and_anchor_tags_struct.anchor_tag; + /+ ↓ an anchor tag names one heading. Markup can give the + same name to several ("2~x- 1.", "2~x- 2." ...), and + an id has to be unique within a document, so a repeat + takes the ocn as a suffix. The first keeps the name + the author wrote, so a link to it still arrives. + +/ + if (!(anchor_tag.empty)) { + if (anchor_tag in _heading_anchor_tags_seen) { + anchor_tag = anchor_tag ~ "-" ~ obj_cite_digits.object_number.to!string; + } + _heading_anchor_tags_seen[anchor_tag] = true; + } if (_new_doc) { cnt1 = 1; cnt2 = 1; @@ -1116,8 +1129,18 @@ if (the_document_body_section.length > 1) { // writeln("body"); tag_assoc[(obj.metainfo.identifier)]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; } - tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"] - = obj.tags.epub_segment_anchor_tag_is; + /+ ↓ An object that is not written into any segment still carries an + identifier, and can share it: a poem block and its verses have one + ocn between them, the verses are written and the block itself is not. + The block comes second, so an unguarded assignment replaced a good + segment name with an empty one, and the link came out as + ".xhtml#297". seg_lv4 above is guarded already, which is why this + showed in the epub and not in the html. + +/ + if (!(obj.tags.epub_segment_anchor_tag_is.empty)) { + tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } } if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } @@ -1700,6 +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; // 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 ca8a7be..33c94e4 100644 --- a/org/ocda_functions.org +++ b/org/ocda_functions.org @@ -59,6 +59,7 @@ string[] anchor_tags; string anchor_tag; string anchor_tag_; string[string] tag_in_seg; +bool[string] _heading_anchor_tags_seen; 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 2895b72..a9f1065 100644 --- a/org/out_sqlite.org +++ b/org/out_sqlite.org @@ -1268,35 +1268,58 @@ string html_table(M,O)( string[] _table_cols; string _table; string _tablenote; + bool _in_head = false; + bool _in_body = false; foreach(row_idx, row; _table_rows) { _table_cols = row.split(rgx.table_delimiter_col); - _table ~= "<tr>"; - foreach(col_idx, cell; _table_cols) { - if ((_table_cols.length == 1) - && (_table_rows.length <= row_idx+2)) { // check row_idx+2 (rather than == ++row_idx) - _tablenote ~= cell; - } else { - string _col_is = (row_idx == 0 && obj.table.heading) ? "th" : "td"; - string _align = ("style=\"text-align:" - ~ ((obj.table.column_aligns[col_idx] == "l") - ? "left\"" : "right\"")); - _table ~= "<" - ~ _col_is - ~ " width=\"" - ~ obj.table.column_widths[col_idx].to!string - ~ "%\" " - ~ _align - ~ ">"; - _table ~= cell; - _table ~= "</" - ~ _col_is - ~ ">"; - } - } - _table ~= "</tr>"; + if ((_table_cols.length == 1) + && (_table_rows.length <= row_idx+2)) { // check row_idx+2 (rather than == ++row_idx) + _tablenote ~= _table_cols[0]; + continue; + } + /+ ↓ a heading row goes in <thead>, the rest in <tbody> +/ + if (row_idx == 0 && obj.table.heading) { + _table ~= "<thead>"; + _in_head = true; + } else if (!_in_body) { + if (_in_head) { _table ~= "</thead>"; _in_head = false; } + _table ~= "<tbody>"; + _in_body = true; + } + _table ~= "<tr>"; + foreach(col_idx, cell; _table_cols) { + bool _is_th = (row_idx == 0 && obj.table.heading); + string _col_is = _is_th ? "th" : "td"; + string _class = (col_idx < obj.table.column_aligns.length + && obj.table.column_aligns[col_idx] == "l") ? "left" : "right"; + _table ~= "<" + ~ _col_is + ~ (_is_th ? " scope=\"col\"" : "") + ~ " class=\"" + ~ _class + ~ "\">"; + _table ~= cell; + _table ~= "</" + ~ _col_is + ~ ">"; } + _table ~= "</tr>"; + } + if (_in_head) { _table ~= "</thead>"; } + else if (_in_body) { _table ~= "</tbody>"; } + /+ ↓ column widths as a <colgroup>, not a width attribute on every cell +/ + string _colgroup; + if (obj.table.column_widths.length > 0) { + string _cols; + foreach(w; obj.table.column_widths) { + _cols ~= (w > 0) + ? "<col style=\"width:" ~ w.to!string ~ "%\" />" + : "<col />"; + } + _colgroup = "<colgroup>" ~ _cols ~ "</colgroup>"; + } Tuple!(string, string) t = tuple( - _table, + (_colgroup ~ _table), _tablenote, ); return t; @@ -1305,15 +1328,15 @@ string html_table(M,O)( Tuple!(string, string) t = _tablarize(obj, _txt); _txt = t[0]; string _note = t[1]; - string o = format(q"┃<p class="%s"> - <table summary="normal text css" width="95%%" border="0" cellpadding="2" align="center"> + string o = format(q"┃<div class="%s"> + <table class="table"> %s </table> %s - </p>┃", + </div>┃", obj.metainfo.is_a, _txt, - _note + (_note.strip.empty) ? "" : ("<p class=\"tablenote\">" ~ _note.strip ~ "</p>") ); return o; } diff --git a/org/out_xmls.org b/org/out_xmls.org index 33dfa5d..4d1c4a7 100644 --- a/org/out_xmls.org +++ b/org/out_xmls.org @@ -41,6 +41,7 @@ template outputXHTMLs() { <<xhtml_format_objects_special_characters_date>> <<xhtml_format_objects_breaks_indents_bullets>> <<xhtml_format_objects_font_face>> + <<xhtml_format_objects_xml_heading_element>> <<xhtml_format_objects_xml_anchor_tags>> <<xhtml_format_objects_header_metadata>> <<xhtml_format_objects_site_info_button>> @@ -301,6 +302,23 @@ string font_face(string _txt) { **** anchor tags +#+NAME: xhtml_format_objects_xml_heading_element +#+BEGIN_SRC d +/+ ↓ the heading element for a markup level. + Markup goes to level 7; HTML stops at h6, and an <h7> is simply + not an element, so the deepest three markup levels share h6 and + are told apart by the "heading" class and their ocn. Level 0 is + the document title, which is h1 in a segment of its own. ++/ +int _heading_element_level(O)(O obj, string _xml_type) { + int _lev = obj.metainfo.heading_lev_markup.to!int; + if (_xml_type == "epub" && _lev == 0) { return 1; } + return (_lev > 6) ? 6 : _lev; +} +#+END_SRC + +**** anchor tags + #+NAME: xhtml_format_objects_xml_anchor_tags #+BEGIN_SRC d string _xhtml_anchor_tags(O)(O obj) { @@ -639,13 +657,13 @@ string tail(M)(M doc_matters) { string o; o = format(q"┃<hr /> <div class="rights"> - <p class="small" id="copyright"> + <p class="small" id="doc_footer_copyright"> <b>Copyright:</b> %s </p> </div> %s <div class="rights"> - <p class="small" id="rights"> + <p class="small" id="doc_footer_rights"> %s </p> </div> @@ -737,7 +755,9 @@ string inline_links(O,M)( if (_xml_type == "seg" || _xml_type == "epub") { seg_lvs = (_xml_type == "epub") ? "seg_lv1to4" : "seg_lv4"; foreach (m; _txt.match(rgx.inline_link_hash)) { - if (m.captures["hash"] in doc_matters.has.tag_associations) { + if ((m.captures["hash"] in doc_matters.has.tag_associations) + && (seg_lvs in doc_matters.has.tag_associations[(m.captures["hash"])]) + ) { if ( m.captures["hash"] == doc_matters.has.tag_associations[(m.captures["hash"])][seg_lvs] @@ -1132,15 +1152,13 @@ string heading(O,M)( (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), obj.metainfo.is_a, obj.metainfo.identifier, tags, heading_lev_anchor_tag, _txt, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), ); } else { o ~= format(q"┃%s @@ -1150,14 +1168,12 @@ string heading(O,M)( </h%s> </div>┃", _horizontal_rule, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), obj.metainfo.is_a, tags, heading_lev_anchor_tag, _txt, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), ); } return o; @@ -1706,6 +1722,17 @@ align="left|right|center" #+NAME: xhtml_format_objects_tablarize #+BEGIN_SRC d +/+ ↓ column widths as a <colgroup>, not a width attribute on every cell +/ +string _table_colgroup(O)(const O obj) { + if (obj.table.column_widths.length == 0) { return ""; } + string _cols; + foreach(w; obj.table.column_widths) { + _cols ~= (w > 0) + ? "<col style=\"width:" ~ w.to!string ~ "%\" />" + : "<col />"; + } + return "<colgroup>" ~ _cols ~ "</colgroup>"; +} Tuple!(string, string) tablarize(O)( string _txt, const O obj, @@ -1714,27 +1741,42 @@ Tuple!(string, string) tablarize(O)( string[] _table_cols; string _table; string _tablenote; + bool _in_head = false; + bool _in_body = false; foreach(row_idx, row; _table_rows) { _table_cols = row.split(rgx.table_delimiter_col); + if ((_table_cols.length == 1) + && (_table_rows.length <= row_idx+2)) { + _tablenote ~= _table_cols[0]; + continue; + } + /+ ↓ a heading row goes in <thead>, the rest in <tbody> +/ + if (row_idx == 0 && obj.table.heading) { + _table ~= "<thead>"; + _in_head = true; + } else if (!_in_body) { + if (_in_head) { _table ~= "</thead>"; _in_head = false; } + _table ~= "<tbody>"; + _in_body = true; + } _table ~= "<tr>"; foreach(col_idx, cell; _table_cols) { - if ((_table_cols.length == 1) - && (_table_rows.length <= row_idx+2)) { - _tablenote ~= cell; - } else { - string _col_is = (row_idx == 0 && obj.table.heading) ? "th" : "td"; - string _align = ("style=\"text-align:" - ~ ((obj.table.column_aligns[col_idx] == "l") - ? "left\"" : "right\"")); - _table ~= "<" ~ _col_is ~ " width=\"" ~ obj.table.column_widths[col_idx].to!string ~ "%\" " ~ _align ~ ">"; - _table ~= cell; - _table ~= "</" ~ _col_is ~ ">"; - } + bool _is_th = (row_idx == 0 && obj.table.heading); + string _col_is = _is_th ? "th" : "td"; + string _class = (col_idx < obj.table.column_aligns.length + && obj.table.column_aligns[col_idx] == "l") ? "left" : "right"; + _table ~= "<" ~ _col_is + ~ (_is_th ? " scope=\"col\"" : "") + ~ " class=\"" ~ _class ~ "\">"; + _table ~= cell; + _table ~= "</" ~ _col_is ~ ">"; } _table ~= "</tr>"; } + if (_in_head) { _table ~= "</thead>"; } + else if (_in_body) { _table ~= "</tbody>"; } Tuple!(string, string) t = tuple( - _table, + (_table_colgroup(obj) ~ _table), _tablenote, ); return t; @@ -1762,12 +1804,12 @@ string table(O,M)( string o; o = format(q"┃ <div class="substance"> <label class="ocn"><a href="#%s" class="lnkocn">%s</a></label> - <p class="%s" id="%s">%s - <table summary="normal text css" width="95%%" border="0" cellpadding="2" align="center"> + <div class="%s" id="%s">%s + <table class="table"> %s </table> %s - </p> + </div> </div>┃", obj.metainfo.object_number, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), @@ -1775,7 +1817,7 @@ string table(O,M)( obj.metainfo.object_number, tags, _txt, - _note + (_note.strip.empty) ? "" : ("<p class=\"tablenote\">" ~ _note.strip ~ "</p>") ); return o; } @@ -2507,6 +2549,7 @@ template outputEPub3() { static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); <<xhtml_format_objects_special_characters_text>> + <<output_epub3_manifest_id>> <<output_epub3_last_modified_time>> <<output_epub3_fixed_mimetypes>> <<output_epub3_fixed_container_xml>> @@ -2528,6 +2571,51 @@ template outputEPub3() { #+END_SRC ** special (epub) files :format: + + +#+NAME: output_epub3_manifest_id +#+BEGIN_SRC d +/+ ↓ a manifest item id, which has to be an XML name. + An XML name may not begin with a digit, and a file quite happily + may: "2bits_02_01-100.png" gave id="2bits_02_01-100", which is not + a name. A prefix settles the first character, and anything else a + filename allows and a name does not becomes an underscore. +/ +string _manifest_id(string _prefix, string _name) { + string _id; + foreach (c; _name) { + _id ~= ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') + ? c : '_'; + } + return _prefix ~ "_" ~ _id; +} +/+ ↓ the media type of an image, which is not its file extension. + There is no "image/jpg"; a manifest that claims one is declaring a + foreign resource, and EPUB then demands a fallback for it. The + core image types are these, and anything else genuinely is + foreign. +/ +string _image_media_type(string _ext) { + switch (_ext.toLower) { + case "jpg": case "jpeg": return "image/jpeg"; + case "png": return "image/png"; + case "gif": return "image/gif"; + case "svg": case "svgz": return "image/svg+xml"; + case "webp": return "image/webp"; + default: return "image/" ~ _ext.toLower; + } +} +/+ ↓ 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, + which is what dc:identifier is for. A random v4 would be neither. + The namespace is v5 of "sisudoc.org" under the DNS namespace. +/ +string _epub_publication_uuid(string _doc_uid) { + import std.uuid : sha1UUID, dnsNamespace; + auto _ns = sha1UUID("sisudoc.org", dnsNamespace); + return sha1UUID(_doc_uid, _ns).toString; +} +#+END_SRC + *** time #+NAME: output_epub3_last_modified_time @@ -2590,7 +2678,7 @@ xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> #+NAME: epub3_package_version #+BEGIN_SRC d -<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> +<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" xml:lang="%s"> #+END_SRC #+NAME: output_epub3_constructs_oebps_content @@ -2599,11 +2687,10 @@ xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> string epub3_oebps_content(D,P)(D doc, P parts) { auto xhtml_format = outputXHTMLs(); auto pth_epub3 = spinePathsEPUB!()(doc.matters.output_path, doc.matters.src.language); - string _uuid = "18275d951861c77f78acd05672c9906924c59f18a2e0ba06dad95959693e9bd8"; // TODO sort uuid in doc.matters! + string _uuid = _epub_publication_uuid(doc.matters.src.doc_uid_out); string content = format(q"┃<?xml version="1.0" encoding="utf-8"?> <<epub3_package_version>> <metadata - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> @@ -2620,6 +2707,7 @@ string epub3_oebps_content(D,P)(D doc, P parts) { <item id="css" href="%s" media-type="text/css"/> <item id="nav" href="toc_nav.xhtml" media-type="application/xhtml+xml" properties="nav" /> ┃", + doc.matters.src.language, _uuid, xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.title_main), (doc.matters.conf_make_meta.meta.title_sub.empty) ? "" : format( @@ -2639,14 +2727,13 @@ string epub3_oebps_content(D,P)(D doc, P parts) { (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); content ~= parts["manifest_documents"]; - // TODO sort jpg & png foreach (image; doc.matters.srcs.image_list) { - content ~= format(q"┃ <item id="%s" href="%s/%s" media-type="image/%s" /> + content ~= format(q"┃ <item id="%s" href="%s/%s" media-type="%s" /> ┃", - image.baseName.stripExtension, + _manifest_id("img", image.baseName.stripExtension), (pth_epub3.doc_oebps_image).chompPrefix("OEBPS/"), image, - image.extension.chompPrefix("."), + _image_media_type(image.extension.chompPrefix(".")), ); } content ~= " " ~ "</manifest>" ~ "\n "; @@ -2753,8 +2840,14 @@ string epub3_oebps_toc_nav_xhtml(D)(D doc) { _new_title_set = true; } else { _new_title_set = false; + /+ ↓ a fragment only where there is one to reach. + A heading of level 4 or less is the top of its own + file, so the file is the target. A heading below + that sits inside a file and carries id="<ocn>", so + that is the fragment. An object with no ocn has no + id, and "#0" was pointing at nothing. +/ string _hashtag = ""; - if ((obj.metainfo.heading_lev_markup <= 4) && (obj.metainfo.ocn == 0)) { + if ((obj.metainfo.heading_lev_markup > 4) && (obj.metainfo.ocn > 0)) { _hashtag = "#" ~ obj.metainfo.ocn.to!string; } string _href = "<a href=\"" @@ -3132,15 +3225,15 @@ if (obj.metainfo.is_a == "heading" ) { _segment_files_seen[obj.tags.segment_anchor_tag_epub] = true; oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml" href="%s.xhtml" media-type="application/xhtml+xml" /> + format(q"┃<item id="%s" href="%s.xhtml" media-type="application/xhtml+xml" /> ┃", - obj.tags.segment_anchor_tag_epub, + _manifest_id("seg", obj.tags.segment_anchor_tag_epub), obj.tags.segment_anchor_tag_epub, ); oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml" linear="yes" /> + format(q"┃<itemref idref="%s" linear="yes" /> ┃", - obj.tags.segment_anchor_tag_epub, + _manifest_id("seg", obj.tags.segment_anchor_tag_epub), ); #+END_SRC diff --git a/org/out_xmls_css.org b/org/out_xmls_css.org index 3ae8e86..ae361c0 100644 --- a/org/out_xmls_css.org +++ b/org/out_xmls_css.org @@ -643,6 +643,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -1574,6 +1592,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; diff --git a/org/tests_for_document_abstraction_shell_scripts.org b/org/tests_for_document_abstraction_shell_scripts.org index a70a5f4..369a7f6 100644 --- a/org/tests_for_document_abstraction_shell_scripts.org +++ b/org/tests_for_document_abstraction_shell_scripts.org @@ -1278,13 +1278,17 @@ SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # markup and left an unbalanced </a> behind - was a url boundary defect in # the abstraction, and is fixed. # -# The errors that remain are, in order of how many: -# the table writer's obsolete presentational attributes, width on every -# cell and summary/border/cellpadding/align on the table; the navigation -# document linking to "#0", a fragment that is never defined; duplicate -# ids; a table inside a <p>; an opf id containing a colon. +# No error is allowed either, and none is outstanding. Both numbers being +# 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. FATALS_ALLOWED=${SpineEpubFatalsAllowed:-0} -ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-1312} +ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-0} if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 diff --git a/src/sisudoc/ocda/abstraction/doc_has.d b/src/sisudoc/ocda/abstraction/doc_has.d index ecdef25..4ecf018 100644 --- a/src/sisudoc/ocda/abstraction/doc_has.d +++ b/src/sisudoc/ocda/abstraction/doc_has.d @@ -264,8 +264,18 @@ template spineDocHasFromAbstraction() { _tag_assoc[obj.metainfo.identifier]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; } - _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] - = obj.tags.epub_segment_anchor_tag_is; + /+ ↓ An object that is not written into any segment still carries an + identifier, and can share it: a poem block and its verses have one + ocn between them, the verses are written and the block itself is not. + The block comes second, so an unguarded assignment replaced a good + segment name with an empty one, and the link came out as + ".xhtml#297". seg_lv4 above is guarded already, which is why this + showed in the epub and not in the html. + +/ + if (obj.tags.epub_segment_anchor_tag_is.length > 0) { + _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } } } _out.imagelist = _images.sort.uniq.array; diff --git a/src/sisudoc/ocda/meta/metadoc_from_src.d b/src/sisudoc/ocda/meta/metadoc_from_src.d index fb30a94..c856de3 100644 --- a/src/sisudoc/ocda/meta/metadoc_from_src.d +++ b/src/sisudoc/ocda/meta/metadoc_from_src.d @@ -95,6 +95,7 @@ template docAbstraction() { reset_note_numbers = true; lev_anchor_tag = ""; anchor_tag = ""; + _heading_anchor_tags_seen = _heading_anchor_tags_seen.init; } mixin spineNode; int[string] node_para_int_ = node_metadata_para_int; @@ -618,6 +619,18 @@ template docAbstraction() { = obj_im.obj_inline_markup_and_anchor_tags_and_misc(an_object, an_object_key, conf_make_meta, ((_new_doc) ? Yes._new_doc : No._new_doc)); an_object["substantive"] = substantive_object_and_anchor_tags_struct.obj_txt; anchor_tag = substantive_object_and_anchor_tags_struct.anchor_tag; + /+ ↓ an anchor tag names one heading. Markup can give the + same name to several ("2~x- 1.", "2~x- 2." ...), and + an id has to be unique within a document, so a repeat + takes the ocn as a suffix. The first keeps the name + the author wrote, so a link to it still arrives. + +/ + if (!(anchor_tag.empty)) { + if (anchor_tag in _heading_anchor_tags_seen) { + anchor_tag = anchor_tag ~ "-" ~ obj_cite_digits.object_number.to!string; + } + _heading_anchor_tags_seen[anchor_tag] = true; + } if (_new_doc) { cnt1 = 1; cnt2 = 1; @@ -1005,8 +1018,18 @@ template docAbstraction() { tag_assoc[(obj.metainfo.identifier)]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; } - tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"] - = obj.tags.epub_segment_anchor_tag_is; + /+ ↓ An object that is not written into any segment still carries an + identifier, and can share it: a poem block and its verses have one + ocn between them, the verses are written and the block itself is not. + The block comes second, so an unguarded assignment replaced a good + segment name with an empty one, and the link came out as + ".xhtml#297". seg_lv4 above is guarded already, which is why this + showed in the epub and not in the html. + +/ + if (!(obj.tags.epub_segment_anchor_tag_is.empty)) { + tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } } if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } @@ -1582,6 +1605,7 @@ template docAbstraction() { content_non_header = "8"; lev_anchor_tag = ""; anchor_tag = ""; + _heading_anchor_tags_seen = _heading_anchor_tags_seen.init; // 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 77770f4..08ee22c 100644 --- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d +++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d @@ -66,6 +66,7 @@ template docAbstractionFunctions() { string anchor_tag; string anchor_tag_; string[string] tag_in_seg; + bool[string] _heading_anchor_tags_seen; string lev_anchor_tag; string[string][string] tag_assoc; string[] lv0to3_tags; diff --git a/src/sisudoc/outputs/io_out/epub3.d b/src/sisudoc/outputs/io_out/epub3.d index b00165d..420c536 100644 --- a/src/sisudoc/outputs/io_out/epub3.d +++ b/src/sisudoc/outputs/io_out/epub3.d @@ -77,6 +77,45 @@ template outputEPub3() { .replaceAll(rgx.nbsp_char, " "); return _txt; } + /+ ↓ a manifest item id, which has to be an XML name. + An XML name may not begin with a digit, and a file quite happily + may: "2bits_02_01-100.png" gave id="2bits_02_01-100", which is not + a name. A prefix settles the first character, and anything else a + filename allows and a name does not becomes an underscore. +/ + string _manifest_id(string _prefix, string _name) { + string _id; + foreach (c; _name) { + _id ~= ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') + ? c : '_'; + } + return _prefix ~ "_" ~ _id; + } + /+ ↓ the media type of an image, which is not its file extension. + There is no "image/jpg"; a manifest that claims one is declaring a + foreign resource, and EPUB then demands a fallback for it. The + core image types are these, and anything else genuinely is + foreign. +/ + string _image_media_type(string _ext) { + switch (_ext.toLower) { + case "jpg": case "jpeg": return "image/jpeg"; + case "png": return "image/png"; + case "gif": return "image/gif"; + case "svg": case "svgz": return "image/svg+xml"; + case "webp": return "image/webp"; + default: return "image/" ~ _ext.toLower; + } + } + /+ ↓ 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, + which is what dc:identifier is for. A random v4 would be neither. + The namespace is v5 of "sisudoc.org" under the DNS namespace. +/ + string _epub_publication_uuid(string _doc_uid) { + import std.uuid : sha1UUID, dnsNamespace; + auto _ns = sha1UUID("sisudoc.org", dnsNamespace); + return sha1UUID(_doc_uid, _ns).toString; + } /+ ↓ the last modified time, as EPUB3 requires it: exactly one dcterms:modified on the package, in UTC to the second, CCYY-MM-DDThh:mm:ssZ +/ @@ -105,11 +144,10 @@ template outputEPub3() { string epub3_oebps_content(D,P)(D doc, P parts) { auto xhtml_format = outputXHTMLs(); auto pth_epub3 = spinePathsEPUB!()(doc.matters.output_path, doc.matters.src.language); - string _uuid = "18275d951861c77f78acd05672c9906924c59f18a2e0ba06dad95959693e9bd8"; // TODO sort uuid in doc.matters! + string _uuid = _epub_publication_uuid(doc.matters.src.doc_uid_out); string content = format(q"┃<?xml version="1.0" encoding="utf-8"?> - <package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> + <package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" xml:lang="%s"> <metadata - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> @@ -126,6 +164,7 @@ template outputEPub3() { <item id="css" href="%s" media-type="text/css"/> <item id="nav" href="toc_nav.xhtml" media-type="application/xhtml+xml" properties="nav" /> ┃", + doc.matters.src.language, _uuid, xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.title_main), (doc.matters.conf_make_meta.meta.title_sub.empty) ? "" : format( @@ -145,14 +184,13 @@ template outputEPub3() { (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); content ~= parts["manifest_documents"]; - // TODO sort jpg & png foreach (image; doc.matters.srcs.image_list) { - content ~= format(q"┃ <item id="%s" href="%s/%s" media-type="image/%s" /> + content ~= format(q"┃ <item id="%s" href="%s/%s" media-type="%s" /> ┃", - image.baseName.stripExtension, + _manifest_id("img", image.baseName.stripExtension), (pth_epub3.doc_oebps_image).chompPrefix("OEBPS/"), image, - image.extension.chompPrefix("."), + _image_media_type(image.extension.chompPrefix(".")), ); } content ~= " " ~ "</manifest>" ~ "\n "; @@ -251,8 +289,14 @@ template outputEPub3() { _new_title_set = true; } else { _new_title_set = false; + /+ ↓ a fragment only where there is one to reach. + A heading of level 4 or less is the top of its own + file, so the file is the target. A heading below + that sits inside a file and carries id="<ocn>", so + that is the fragment. An object with no ocn has no + id, and "#0" was pointing at nothing. +/ string _hashtag = ""; - if ((obj.metainfo.heading_lev_markup <= 4) && (obj.metainfo.ocn == 0)) { + if ((obj.metainfo.heading_lev_markup > 4) && (obj.metainfo.ocn > 0)) { _hashtag = "#" ~ obj.metainfo.ocn.to!string; } string _href = "<a href=\"" @@ -575,15 +619,15 @@ template outputEPub3() { ) { _segment_files_seen[obj.tags.segment_anchor_tag_epub] = true; oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml" href="%s.xhtml" media-type="application/xhtml+xml" /> + format(q"┃<item id="%s" href="%s.xhtml" media-type="application/xhtml+xml" /> ┃", - obj.tags.segment_anchor_tag_epub, + _manifest_id("seg", obj.tags.segment_anchor_tag_epub), obj.tags.segment_anchor_tag_epub, ); oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml" linear="yes" /> + format(q"┃<itemref idref="%s" linear="yes" /> ┃", - obj.tags.segment_anchor_tag_epub, + _manifest_id("seg", obj.tags.segment_anchor_tag_epub), ); } } diff --git a/src/sisudoc/outputs/io_out/sqlite_collection_db.d b/src/sisudoc/outputs/io_out/sqlite_collection_db.d index d5b2dcf..f1556c4 100644 --- a/src/sisudoc/outputs/io_out/sqlite_collection_db.d +++ b/src/sisudoc/outputs/io_out/sqlite_collection_db.d @@ -789,35 +789,58 @@ template SQLiteFormatAndLoadObject() { string[] _table_cols; string _table; string _tablenote; + bool _in_head = false; + bool _in_body = false; foreach(row_idx, row; _table_rows) { _table_cols = row.split(rgx.table_delimiter_col); - _table ~= "<tr>"; - foreach(col_idx, cell; _table_cols) { - if ((_table_cols.length == 1) - && (_table_rows.length <= row_idx+2)) { // check row_idx+2 (rather than == ++row_idx) - _tablenote ~= cell; - } else { - string _col_is = (row_idx == 0 && obj.table.heading) ? "th" : "td"; - string _align = ("style=\"text-align:" - ~ ((obj.table.column_aligns[col_idx] == "l") - ? "left\"" : "right\"")); - _table ~= "<" - ~ _col_is - ~ " width=\"" - ~ obj.table.column_widths[col_idx].to!string - ~ "%\" " - ~ _align - ~ ">"; - _table ~= cell; - _table ~= "</" - ~ _col_is - ~ ">"; - } - } - _table ~= "</tr>"; + if ((_table_cols.length == 1) + && (_table_rows.length <= row_idx+2)) { // check row_idx+2 (rather than == ++row_idx) + _tablenote ~= _table_cols[0]; + continue; + } + /+ ↓ a heading row goes in <thead>, the rest in <tbody> +/ + if (row_idx == 0 && obj.table.heading) { + _table ~= "<thead>"; + _in_head = true; + } else if (!_in_body) { + if (_in_head) { _table ~= "</thead>"; _in_head = false; } + _table ~= "<tbody>"; + _in_body = true; + } + _table ~= "<tr>"; + foreach(col_idx, cell; _table_cols) { + bool _is_th = (row_idx == 0 && obj.table.heading); + string _col_is = _is_th ? "th" : "td"; + string _class = (col_idx < obj.table.column_aligns.length + && obj.table.column_aligns[col_idx] == "l") ? "left" : "right"; + _table ~= "<" + ~ _col_is + ~ (_is_th ? " scope=\"col\"" : "") + ~ " class=\"" + ~ _class + ~ "\">"; + _table ~= cell; + _table ~= "</" + ~ _col_is + ~ ">"; } + _table ~= "</tr>"; + } + if (_in_head) { _table ~= "</thead>"; } + else if (_in_body) { _table ~= "</tbody>"; } + /+ ↓ column widths as a <colgroup>, not a width attribute on every cell +/ + string _colgroup; + if (obj.table.column_widths.length > 0) { + string _cols; + foreach(w; obj.table.column_widths) { + _cols ~= (w > 0) + ? "<col style=\"width:" ~ w.to!string ~ "%\" />" + : "<col />"; + } + _colgroup = "<colgroup>" ~ _cols ~ "</colgroup>"; + } Tuple!(string, string) t = tuple( - _table, + (_colgroup ~ _table), _tablenote, ); return t; @@ -826,15 +849,15 @@ template SQLiteFormatAndLoadObject() { Tuple!(string, string) t = _tablarize(obj, _txt); _txt = t[0]; string _note = t[1]; - string o = format(q"┃<p class="%s"> - <table summary="normal text css" width="95%%" border="0" cellpadding="2" align="center"> + string o = format(q"┃<div class="%s"> + <table class="table"> %s </table> %s - </p>┃", + </div>┃", obj.metainfo.is_a, _txt, - _note + (_note.strip.empty) ? "" : ("<p class=\"tablenote\">" ~ _note.strip ~ "</p>") ); return o; } diff --git a/src/sisudoc/outputs/io_out/xmls.d b/src/sisudoc/outputs/io_out/xmls.d index 097ce44..c1e733e 100644 --- a/src/sisudoc/outputs/io_out/xmls.d +++ b/src/sisudoc/outputs/io_out/xmls.d @@ -205,6 +205,17 @@ template outputXHTMLs() { .replaceAll(rgx.inline_cite, ("<cite>$1</cite>")); return _txt; } + /+ ↓ the heading element for a markup level. + Markup goes to level 7; HTML stops at h6, and an <h7> is simply + not an element, so the deepest three markup levels share h6 and + are told apart by the "heading" class and their ocn. Level 0 is + the document title, which is h1 in a segment of its own. + +/ + int _heading_element_level(O)(O obj, string _xml_type) { + int _lev = obj.metainfo.heading_lev_markup.to!int; + if (_xml_type == "epub" && _lev == 0) { return 1; } + return (_lev > 6) ? 6 : _lev; + } string _xhtml_anchor_tags(O)(O obj) { string tags=""; if (obj.tags.anchor_tags.length > 0) { @@ -495,13 +506,13 @@ string tail(M)(M doc_matters) { string o; o = format(q"┃<hr /> <div class="rights"> - <p class="small" id="copyright"> + <p class="small" id="doc_footer_copyright"> <b>Copyright:</b> %s </p> </div> %s <div class="rights"> - <p class="small" id="rights"> + <p class="small" id="doc_footer_rights"> %s </p> </div> @@ -579,7 +590,9 @@ string tail(M)(M doc_matters) { if (_xml_type == "seg" || _xml_type == "epub") { seg_lvs = (_xml_type == "epub") ? "seg_lv1to4" : "seg_lv4"; foreach (m; _txt.match(rgx.inline_link_hash)) { - if (m.captures["hash"] in doc_matters.has.tag_associations) { + if ((m.captures["hash"] in doc_matters.has.tag_associations) + && (seg_lvs in doc_matters.has.tag_associations[(m.captures["hash"])]) + ) { if ( m.captures["hash"] == doc_matters.has.tag_associations[(m.captures["hash"])][seg_lvs] @@ -928,15 +941,13 @@ string tail(M)(M doc_matters) { (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), obj.metainfo.is_a, obj.metainfo.identifier, tags, heading_lev_anchor_tag, _txt, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), ); } else { o ~= format(q"┃%s @@ -946,14 +957,12 @@ string tail(M)(M doc_matters) { </h%s> </div>┃", _horizontal_rule, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), obj.metainfo.is_a, tags, heading_lev_anchor_tag, _txt, - ((_xml_type == "epub" && obj.metainfo.heading_lev_markup == 0) ? 1 - : obj.metainfo.heading_lev_markup), + _heading_element_level(obj, _xml_type), ); } return o; @@ -1316,6 +1325,17 @@ string tail(M)(M doc_matters) { ); return u; } + /+ ↓ column widths as a <colgroup>, not a width attribute on every cell +/ + string _table_colgroup(O)(const O obj) { + if (obj.table.column_widths.length == 0) { return ""; } + string _cols; + foreach(w; obj.table.column_widths) { + _cols ~= (w > 0) + ? "<col style=\"width:" ~ w.to!string ~ "%\" />" + : "<col />"; + } + return "<colgroup>" ~ _cols ~ "</colgroup>"; + } Tuple!(string, string) tablarize(O)( string _txt, const O obj, @@ -1324,27 +1344,42 @@ string tail(M)(M doc_matters) { string[] _table_cols; string _table; string _tablenote; + bool _in_head = false; + bool _in_body = false; foreach(row_idx, row; _table_rows) { _table_cols = row.split(rgx.table_delimiter_col); + if ((_table_cols.length == 1) + && (_table_rows.length <= row_idx+2)) { + _tablenote ~= _table_cols[0]; + continue; + } + /+ ↓ a heading row goes in <thead>, the rest in <tbody> +/ + if (row_idx == 0 && obj.table.heading) { + _table ~= "<thead>"; + _in_head = true; + } else if (!_in_body) { + if (_in_head) { _table ~= "</thead>"; _in_head = false; } + _table ~= "<tbody>"; + _in_body = true; + } _table ~= "<tr>"; foreach(col_idx, cell; _table_cols) { - if ((_table_cols.length == 1) - && (_table_rows.length <= row_idx+2)) { - _tablenote ~= cell; - } else { - string _col_is = (row_idx == 0 && obj.table.heading) ? "th" : "td"; - string _align = ("style=\"text-align:" - ~ ((obj.table.column_aligns[col_idx] == "l") - ? "left\"" : "right\"")); - _table ~= "<" ~ _col_is ~ " width=\"" ~ obj.table.column_widths[col_idx].to!string ~ "%\" " ~ _align ~ ">"; - _table ~= cell; - _table ~= "</" ~ _col_is ~ ">"; - } + bool _is_th = (row_idx == 0 && obj.table.heading); + string _col_is = _is_th ? "th" : "td"; + string _class = (col_idx < obj.table.column_aligns.length + && obj.table.column_aligns[col_idx] == "l") ? "left" : "right"; + _table ~= "<" ~ _col_is + ~ (_is_th ? " scope=\"col\"" : "") + ~ " class=\"" ~ _class ~ "\">"; + _table ~= cell; + _table ~= "</" ~ _col_is ~ ">"; } _table ~= "</tr>"; } + if (_in_head) { _table ~= "</thead>"; } + else if (_in_body) { _table ~= "</tbody>"; } Tuple!(string, string) t = tuple( - _table, + (_table_colgroup(obj) ~ _table), _tablenote, ); return t; @@ -1366,12 +1401,12 @@ string tail(M)(M doc_matters) { string o; o = format(q"┃ <div class="substance"> <label class="ocn"><a href="#%s" class="lnkocn">%s</a></label> - <p class="%s" id="%s">%s - <table summary="normal text css" width="95%%" border="0" cellpadding="2" align="center"> + <div class="%s" id="%s">%s + <table class="table"> %s </table> %s - </p> + </div> </div>┃", obj.metainfo.object_number, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), @@ -1379,7 +1414,7 @@ string tail(M)(M doc_matters) { obj.metainfo.object_number, tags, _txt, - _note + (_note.strip.empty) ? "" : ("<p class=\"tablenote\">" ~ _note.strip ~ "</p>") ); return o; } diff --git a/src/sisudoc/outputs/io_out/xmls_css.d b/src/sisudoc/outputs/io_out/xmls_css.d index ed9f994..6cab68e 100644 --- a/src/sisudoc/outputs/io_out/xmls_css.d +++ b/src/sisudoc/outputs/io_out/xmls_css.d @@ -880,6 +880,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -1668,6 +1686,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -2467,6 +2503,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -3181,6 +3235,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -3906,6 +3978,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; @@ -4460,6 +4550,24 @@ table { margin-right : 2em; background-color : inherit; } +div.table { + margin : 1em 0; + overflow-x : auto; +} +table.table { + display : table; + width : 95%%; + margin : 0 auto; + border-collapse : collapse; + background-color : inherit; +} +table.table th, table.table td { + padding : 2px; + vertical-align : top; +} +p.tablenote { + margin : 0.5em 0 0 0; +} tr { } th,td { vertical-align : top; diff --git a/test/reference/abstraction/content.cory_doctorow.en.ssp b/test/reference/abstraction/content.cory_doctorow.en.ssp index 38a8fe1..a829af2 100644 --- a/test/reference/abstraction/content.cory_doctorow.en.ssp +++ b/test/reference/abstraction/content.cory_doctorow.en.ssp @@ -2536,7 +2536,7 @@ .is_of_type: para .sha256: E0F04412996A5887BE496D9455ADFB93769A4861D8F64A9BC867A3C57971DE88 .segment: seg_2 -.heading_lev_anchor: x- +.heading_lev_anchor: x--73 .segment_epub: seg_2 .segment_html_is: seg_2 .segment_epub_is: seg_2 @@ -2882,7 +2882,7 @@ .is_of_type: para .sha256: 12D04772E32C49C982DCFC34458FE7D1DB754A6BEFC7E321DFE8FBF7B747DC47 .segment: seg_2 -.heading_lev_anchor: x- +.heading_lev_anchor: x--100 .segment_epub: seg_2 .segment_html_is: seg_2 .segment_epub_is: seg_2 @@ -3000,7 +3000,7 @@ .is_of_type: para .sha256: C346CFE219158190D638C00CC0F7DAAC959E755F9D8CDC5EBB746806C943AB5B .segment: seg_2 -.heading_lev_anchor: x- +.heading_lev_anchor: x--108 .segment_epub: seg_2 .segment_html_is: seg_2 .segment_epub_is: seg_2 @@ -3476,7 +3476,7 @@ .is_of_type: para .sha256: AE2405DF403554C65B7631871B0F2EDD8C8E49F8318965F179ED900EB2D33A67 .segment: seg_2 -.heading_lev_anchor: x- +.heading_lev_anchor: x--145 .segment_epub: seg_2 .segment_html_is: seg_2 .segment_epub_is: seg_2 @@ -7531,7 +7531,7 @@ .is_of_type: para .sha256: 6E62EE82E3DF2A7DFCABA91E18EAB2A77C9C7F7B4A5DCEB49528346078B636D2 .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--440 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7588,7 +7588,7 @@ .is_of_type: para .sha256: 435168BDFA62B2A21AE519C224BAE9E63FAA5C3131E18403BAC6383B61A438C0 .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--444 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7620,7 +7620,7 @@ .is_of_type: para .sha256: 2B0C1510AFFB174A55A502270FCD30FC8A245B9ADEF468A2A445DF444CF5841A .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--446 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7728,7 +7728,7 @@ .is_of_type: para .sha256: 8A281794EBDA231325CB10A6F1908E1AE6DDA6560A379B7EE8C02C28CBCEBBC9 .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--454 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7796,7 +7796,7 @@ .is_of_type: para .sha256: B2C6211B1EAB3298CBBF4A47C6E4F40B897D6F1E53E98D6A2F260CFFDEC7D4DD .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--459 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7852,7 +7852,7 @@ .is_of_type: para .sha256: DB233F1FB52CB27629E4A008D361A0F9B138287A6E6B2549F625CC3C3F26F04E .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--463 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -7932,7 +7932,7 @@ .is_of_type: para .sha256: 2E2FF925CC1322BFFFA8826779F2C23D6C32C67AE4E99E6B01508BF9AD9B6BA4 .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--469 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -8100,7 +8100,7 @@ .is_of_type: para .sha256: 7C9EF24634EC4796B19AE4B1F81D4D404A896CDB1202AFC0098BA0C54CAA61BE .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--479 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -8156,7 +8156,7 @@ .is_of_type: para .sha256: 8BC8808A857028DB86C62FF2A59EF1D667EFBFB0AAECB3E9E548962DD47D450F .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--483 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 @@ -8236,7 +8236,7 @@ .is_of_type: para .sha256: 48CED3C978FDDAE04931DDFD52440553664432F7ECCD63F36F52B851C173C6C3 .segment: seg_15 -.heading_lev_anchor: x- +.heading_lev_anchor: x--489 .segment_epub: seg_15 .segment_html_is: seg_15 .segment_epub_is: seg_15 diff --git a/test/reference/abstraction/live-manual.es.ssp b/test/reference/abstraction/live-manual.es.ssp index 2fb8833..a3b3c06 100644 --- a/test/reference/abstraction/live-manual.es.ssp +++ b/test/reference/abstraction/live-manual.es.ssp @@ -4278,7 +4278,7 @@ .is_of_type: para .sha256: 2B5F530EEC4D1A1505FC965B9D2E24E9E3C23ECB5BD5DEEC94DDCD5F667F555C .segment: the-basics -.heading_lev_anchor: using-iso-hybrid +.heading_lev_anchor: using-iso-hybrid-189 .segment_epub: the-basics .segment_html_is: the-basics .segment_epub_is: the-basics diff --git a/test/reference/abstraction/the_public_domain.james_boyle.en.ssp b/test/reference/abstraction/the_public_domain.james_boyle.en.ssp index 675fb34..b7a67d2 100644 --- a/test/reference/abstraction/the_public_domain.james_boyle.en.ssp +++ b/test/reference/abstraction/the_public_domain.james_boyle.en.ssp @@ -1612,7 +1612,7 @@ .is_of_type: para .sha256: E963CE2FEE8900892828081A301481CCEF0B6D0BCD4F95A3EFC5673A4512AD00 .segment: chapter_1 -.heading_lev_anchor: chapter_1 +.heading_lev_anchor: chapter_1-81 .segment_epub: chapter_1 .segment_html_is: chapter_1 .segment_epub_is: chapter_1 @@ -2957,7 +2957,7 @@ .is_of_type: para .sha256: 9D02B0A0B1E2650828466A4FCD99FFE09873F72238DB5C4D3C7F226879FF83F9 .segment: chapter_2 -.heading_lev_anchor: chapter_2 +.heading_lev_anchor: chapter_2-184 .segment_epub: chapter_2 .segment_html_is: chapter_2 .segment_epub_is: chapter_2 @@ -3586,7 +3586,7 @@ .is_of_type: para .sha256: BE74615A73024FD20DBAAFD69D37E816AA338403293F0B85E3BDD73294B3B565 .segment: chapter_3 -.heading_lev_anchor: chapter_3 +.heading_lev_anchor: chapter_3-230 .segment_epub: chapter_3 .segment_html_is: chapter_3 .segment_epub_is: chapter_3 @@ -5043,7 +5043,7 @@ .is_of_type: para .sha256: 1AE5FFF4E1337498417EA656E69BE2134C39F6452004E8C41487D3301D0E61FF .segment: chapter_4 -.heading_lev_anchor: chapter_4 +.heading_lev_anchor: chapter_4-344 .segment_epub: chapter_4 .segment_html_is: chapter_4 .segment_epub_is: chapter_4 @@ -6750,7 +6750,7 @@ .is_of_type: para .sha256: 9DA46DA5A083E19F6842572DB65B18C985DDD9491BEE6358D8871EE22FA3F82A .segment: chapter_5 -.heading_lev_anchor: chapter_5 +.heading_lev_anchor: chapter_5-479 .segment_epub: chapter_5 .segment_html_is: chapter_5 .segment_epub_is: chapter_5 @@ -8591,7 +8591,7 @@ .is_of_type: para .sha256: E3653EBCD3DC67EF44731D2E2537AAE91545AC625FD36797828BD687F2AFA4B6 .segment: chapter_6 -.heading_lev_anchor: chapter_6 +.heading_lev_anchor: chapter_6-617 .segment_epub: chapter_6 .segment_html_is: chapter_6 .segment_epub_is: chapter_6 @@ -9671,7 +9671,7 @@ .is_of_type: para .sha256: 69A12F94F0BA03913A00400418D427CA970696E3DF3C8C2F4D48F1C74C783FAF .segment: chapter_7 -.heading_lev_anchor: chapter_7 +.heading_lev_anchor: chapter_7-699 .segment_epub: chapter_7 .segment_html_is: chapter_7 .segment_epub_is: chapter_7 @@ -10769,7 +10769,7 @@ .is_of_type: para .sha256: CB18592BF9C536CEECB58E13B2E2F18D85ED509FF62474A4B783E04C3AF07BC9 .segment: chapter_8 -.heading_lev_anchor: chapter_8 +.heading_lev_anchor: chapter_8-785 .segment_epub: chapter_8 .segment_html_is: chapter_8 .segment_epub_is: chapter_8 @@ -12083,7 +12083,7 @@ .is_of_type: para .sha256: CBC4E874A0AAC1856EAEC8E0979DABD8A211F4AC76D5ABF10A04A8C54A012D74 .segment: chapter_9 -.heading_lev_anchor: chapter_9 +.heading_lev_anchor: chapter_9-887 .segment_epub: chapter_9 .segment_html_is: chapter_9 .segment_epub_is: chapter_9 @@ -13106,7 +13106,7 @@ .is_of_type: para .sha256: 1142480E6C32D2754F1A1718AE249C69D6949428FAAF23E18F27DD3F3A82033A .segment: chapter_10 -.heading_lev_anchor: chapter_10 +.heading_lev_anchor: chapter_10-964 .segment_epub: chapter_10 .segment_html_is: chapter_10 .segment_epub_is: chapter_10 diff --git a/test/test-epub-validity.sh b/test/test-epub-validity.sh index 4bc5729..b3fde21 100755 --- a/test/test-epub-validity.sh +++ b/test/test-epub-validity.sh @@ -34,13 +34,17 @@ SPINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" # markup and left an unbalanced </a> behind - was a url boundary defect in # the abstraction, and is fixed. # -# The errors that remain are, in order of how many: -# the table writer's obsolete presentational attributes, width on every -# cell and summary/border/cellpadding/align on the table; the navigation -# document linking to "#0", a fragment that is never defined; duplicate -# ids; a table inside a <p>; an opf id containing a colon. +# No error is allowed either, and none is outstanding. Both numbers being +# 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. FATALS_ALLOWED=${SpineEpubFatalsAllowed:-0} -ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-1312} +ERRORS_ALLOWED=${SpineEpubErrorsAllowed:-0} if [ -z "$SpinePOD" ]; then echo "ERROR: \$SpinePOD is not set." >&2 |
