From 8381020842b46a313788f750e8acc19e34a5ec1d Mon Sep 17 00:00:00 2001 From: Ralph Amissah Date: Sat, 12 Sep 2026 22:15:56 -0400 Subject: 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) --- src/sisudoc/outputs/io_out/epub3.d | 103 +++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 57 deletions(-) (limited to 'src') 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"┃ - + + xmlns:dc="http://purl.org/dc/elements/1.1/"> + urn:uuid:%s %s main %s subtitle - %s + %s + %s %s %s Copyright: %s - %s - urn:uuid:%s + %s @@ -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 ~= " " ~ "" ~ "\n "; content ~= parts["spine"]; content ~= " " ~ "" ~ "\n "; - content ~= " " ~ "" ~ "\n "; - content ~= parts["guide"]; - content ~= " " ~ "" ~ "\n "; content ~= "" ~ ""; 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"┃ - ┃", - obj.tags.segment_anchor_tag_epub, - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["spine"] ~= - format(q"┃ - ┃", - obj.tags.segment_anchor_tag_epub, - ); - oepbs_content_parts["guide"] ~= - format(q"┃ - ┃", - 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"┃ - ┃", - 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"┃ - ┃", - obj.tags.segment_anchor_tag_epub, - obj.metainfo.object_number, - ); - oepbs_content_parts["guide"] ~= - format(q"┃ - ┃", - 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"┃ + ┃", + obj.tags.segment_anchor_tag_epub, + obj.tags.segment_anchor_tag_epub, + ); + oepbs_content_parts["spine"] ~= + format(q"┃ + ┃", + obj.tags.segment_anchor_tag_epub, + ); } } } -- cgit v1.2.3