diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-12 22:15:56 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-14 00:13:41 -0400 |
| commit | 8381020842b46a313788f750e8acc19e34a5ec1d (patch) | |
| tree | 9ce886bd55b8043cc25ce50af7d625019e82fcbb | |
| parent | epub, html: metadata text clear of markup (diff) | |
epub: manifest lists files, spine orders files
several fixes
- for epub manifest is the list of files in the container, the spine is
the reading order of those files, fixed.
- The guide is dropped. It is EPUB2, optional in EPUB3
- numerous small fixes
(assisted by Claude-Code)
| -rw-r--r-- | org/out_xmls.org | 110 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/epub3.d | 103 |
2 files changed, 99 insertions, 114 deletions
diff --git a/org/out_xmls.org b/org/out_xmls.org index 186137b..b477d34 100644 --- a/org/out_xmls.org +++ b/org/out_xmls.org @@ -2507,6 +2507,7 @@ template outputEPub3() { static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); <<xhtml_format_objects_special_characters_text>> + <<output_epub3_last_modified_time>> <<output_epub3_fixed_mimetypes>> <<output_epub3_fixed_container_xml>> <<output_epub3_constructs_oebps_content>> @@ -2527,6 +2528,21 @@ template outputEPub3() { #+END_SRC ** special (epub) files :format: +*** time + +#+NAME: output_epub3_last_modified_time +#+BEGIN_SRC d +/+ ↓ 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 ++/ +string _epub_modified_utc() { + import std.datetime : Clock, UTC; + auto _t = Clock.currTime(UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); +} +#+END_SRC + *** static **** _identify doc filetype_ (mimetype) [static] @@ -2574,7 +2590,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="uid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> +<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> #+END_SRC #+NAME: output_epub3_constructs_oebps_content @@ -2589,18 +2605,18 @@ string epub3_oebps_content(D,P)(D doc, P parts) { <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/" - unique-identifier="urn:uuid:%s" version="2.0"> + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> <dc:title id="title">%s</dc:title> <meta refines="#title" property="title-type">main</meta> <dc:title id="subtitle">%s</dc:title> <meta refines="#subtitle" property="title-type">subtitle</meta> - <dc:creator file-as="%s" id="aut">%s</dc:creator> + <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> - <dc:identifier scheme="URI">%s</dc:identifier> - <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> + <meta property="dcterms:modified">%s</meta> </metadata> <manifest> <item id="css" href="%s" media-type="text/css"/> @@ -2612,15 +2628,14 @@ string epub3_oebps_content(D,P)(D doc, P parts) { ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.title_sub), (doc.matters.conf_make_meta.meta.creator_author.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author), - (doc.matters.conf_make_meta.meta.creator_author.empty) - ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author), + (doc.matters.conf_make_meta.meta.creator_author_surname_fn.empty) + ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author_surname_fn), doc.matters.src.language, // language, fix (needed in dochead metadata) (doc.matters.conf_make_meta.meta.date_published.empty) ? "" : xhtml_format.special_characters_plain_date(doc.matters.conf_make_meta.meta.date_published), (doc.matters.conf_make_meta.meta.rights_copyright.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright), - _uuid, - _uuid, + _epub_modified_utc(), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); content ~= parts["manifest_documents"]; @@ -2638,9 +2653,6 @@ string epub3_oebps_content(D,P)(D doc, P parts) { content ~= " " ~ "<spine>" ~ "\n "; content ~= parts["spine"]; content ~= " " ~ "</spine>" ~ "\n "; - content ~= " " ~ "<guide>" ~ "\n "; - content ~= parts["guide"]; - content ~= " " ~ "</guide>" ~ "\n "; content ~= "" ~ "</package>"; debug(epubmanifest) { foreach (section; doc.matters.has.keys_seq.seg) { // TODO @@ -2805,6 +2817,7 @@ string epub3_oebps_toc_nav_xhtml(D)(D doc) { string segment_filename; string[] top_level_headings = ["","","",""]; string[string] oepbs_content_parts; + bool[string] _segment_files_seen; /+ a segment file is listed once +/ string suffix = ".xhtml"; struct writeOut { /+ epub specific documents +/ /+ fixed output +/ @@ -3091,50 +3104,33 @@ case "backmatter": #+NAME: output_epub3_xhtml_seg_object_is_heading_set #+BEGIN_SRC d -if (obj.metainfo.is_a == "heading") { - // assert(obj.text.length > 0); // check assertion - if (obj.metainfo.heading_lev_markup <= 4) { - oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml" href="%s.xhtml" media-type="application/xhtml+xml" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml" linear="yes" /> - ┃", - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["guide"] ~= - format(q"┃<reference type="%s" href="%s" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.tags.segment_anchor_tag_epub, - ); - } else if (obj.metainfo.heading_lev_markup > 4) { - oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml#%s" href="%s.xhtml#%s" media-type="application/xhtml+xml" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml#%s" linear="yes" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - oepbs_content_parts["guide"] ~= - format(q"┃<reference type="%s#%s" href="%s#%s" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - } +/+ ↓ the manifest lists *files* and the spine is the reading order + of *files*, so one entry per segment document, named once. + . + A heading at level 4 or above opens a segment file; a heading + below that is an anchor inside one and needs no declaration, + an item href not being allowed a fragment and an item id + having to be an XML name, which "x.xhtml#8" is not. Several + headings can open onto the same file, so a file already seen + is not listed again. ++/ +if (obj.metainfo.is_a == "heading" + && obj.metainfo.heading_lev_markup <= 4 + && obj.tags.segment_anchor_tag_epub.length > 0 + && !(obj.tags.segment_anchor_tag_epub in _segment_files_seen) +) { + _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" /> + ┃", + obj.tags.segment_anchor_tag_epub, + obj.tags.segment_anchor_tag_epub, + ); + oepbs_content_parts["spine"] ~= + format(q"┃<itemref idref="%s.xhtml" linear="yes" /> + ┃", + obj.tags.segment_anchor_tag_epub, + ); #+END_SRC ***** closings & post loop diff --git a/src/sisudoc/outputs/io_out/epub3.d b/src/sisudoc/outputs/io_out/epub3.d index 100aff1..e07aeb8 100644 --- a/src/sisudoc/outputs/io_out/epub3.d +++ b/src/sisudoc/outputs/io_out/epub3.d @@ -77,6 +77,15 @@ template outputEPub3() { .replaceAll(rgx.nbsp_char, " "); return _txt; } + /+ ↓ 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 + +/ + string _epub_modified_utc() { + import std.datetime : Clock, UTC; + auto _t = Clock.currTime(UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); + } string epub3_mimetypes() { string o; o = format(q"┃application/epub+zip┃") ~ "\n"; @@ -98,22 +107,22 @@ template outputEPub3() { auto pth_epub3 = spinePathsEPUB!()(doc.matters.output_path, doc.matters.src.language); string _uuid = "18275d951861c77f78acd05672c9906924c59f18a2e0ba06dad95959693e9bd8"; // TODO sort uuid in doc.matters! string content = format(q"┃<?xml version="1.0" encoding="utf-8"?> - <package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> + <package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" prefix="rendition: http://www.idpf.org/vocab/rendition/#"> <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/" - unique-identifier="urn:uuid:%s" version="2.0"> + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> <dc:title id="title">%s</dc:title> <meta refines="#title" property="title-type">main</meta> <dc:title id="subtitle">%s</dc:title> <meta refines="#subtitle" property="title-type">subtitle</meta> - <dc:creator file-as="%s" id="aut">%s</dc:creator> + <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> - <dc:identifier scheme="URI">%s</dc:identifier> - <dc:identifier id="bookid">urn:uuid:%s</dc:identifier> + <meta property="dcterms:modified">%s</meta> </metadata> <manifest> <item id="css" href="%s" media-type="text/css"/> @@ -125,15 +134,14 @@ template outputEPub3() { ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.title_sub), (doc.matters.conf_make_meta.meta.creator_author.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author), - (doc.matters.conf_make_meta.meta.creator_author.empty) - ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author), + (doc.matters.conf_make_meta.meta.creator_author_surname_fn.empty) + ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author_surname_fn), doc.matters.src.language, // language, fix (needed in dochead metadata) (doc.matters.conf_make_meta.meta.date_published.empty) ? "" : xhtml_format.special_characters_plain_date(doc.matters.conf_make_meta.meta.date_published), (doc.matters.conf_make_meta.meta.rights_copyright.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright), - _uuid, - _uuid, + _epub_modified_utc(), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); content ~= parts["manifest_documents"]; @@ -151,9 +159,6 @@ template outputEPub3() { content ~= " " ~ "<spine>" ~ "\n "; content ~= parts["spine"]; content ~= " " ~ "</spine>" ~ "\n "; - content ~= " " ~ "<guide>" ~ "\n "; - content ~= parts["guide"]; - content ~= " " ~ "</guide>" ~ "\n "; content ~= "" ~ "</package>"; debug(epubmanifest) { foreach (section; doc.matters.has.keys_seq.seg) { // TODO @@ -303,6 +308,7 @@ template outputEPub3() { string segment_filename; string[] top_level_headings = ["","","",""]; string[string] oepbs_content_parts; + bool[string] _segment_files_seen; /+ a segment file is listed once +/ string suffix = ".xhtml"; struct writeOut { /+ epub specific documents +/ /+ fixed output +/ @@ -541,50 +547,33 @@ template outputEPub3() { break; } } - if (obj.metainfo.is_a == "heading") { - // assert(obj.text.length > 0); // check assertion - if (obj.metainfo.heading_lev_markup <= 4) { - oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml" href="%s.xhtml" media-type="application/xhtml+xml" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml" linear="yes" /> - ┃", - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["guide"] ~= - format(q"┃<reference type="%s" href="%s" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.tags.segment_anchor_tag_epub, - ); - } else if (obj.metainfo.heading_lev_markup > 4) { - oepbs_content_parts["manifest_documents"] ~= - format(q"┃<item id="%s.xhtml#%s" href="%s.xhtml#%s" media-type="application/xhtml+xml" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - oepbs_content_parts["spine"] ~= - format(q"┃<itemref idref="%s.xhtml#%s" linear="yes" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - oepbs_content_parts["guide"] ~= - format(q"┃<reference type="%s#%s" href="%s#%s" /> - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - } + /+ ↓ the manifest lists *files* and the spine is the reading order + of *files*, so one entry per segment document, named once. + . + A heading at level 4 or above opens a segment file; a heading + below that is an anchor inside one and needs no declaration, + an item href not being allowed a fragment and an item id + having to be an XML name, which "x.xhtml#8" is not. Several + headings can open onto the same file, so a file already seen + is not listed again. + +/ + if (obj.metainfo.is_a == "heading" + && obj.metainfo.heading_lev_markup <= 4 + && obj.tags.segment_anchor_tag_epub.length > 0 + && !(obj.tags.segment_anchor_tag_epub in _segment_files_seen) + ) { + _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" /> + ┃", + obj.tags.segment_anchor_tag_epub, + obj.tags.segment_anchor_tag_epub, + ); + oepbs_content_parts["spine"] ~= + format(q"┃<itemref idref="%s.xhtml" linear="yes" /> + ┃", + obj.tags.segment_anchor_tag_epub, + ); } } } |
