-*- mode: org -*- #+TITLE: sisudoc spine (doc_reform) output xmls #+DESCRIPTION: documents - structuring, publishing in multiple formats & search #+FILETAGS: :spine:output:xml: #+AUTHOR: Ralph Amissah #+EMAIL: [[mailto:ralph.amissah@gmail.com][ralph.amissah@gmail.com]] #+COPYRIGHT: Copyright (C) 2015 (continuously updated, current 2026) Ralph Amissah #+LANGUAGE: en #+STARTUP: content hideblocks hidestars noindent entitiespretty #+PROPERTY: header-args+ :eval never-export :exports code #+PROPERTY: header-args+ :noweb yes :padline no #+PROPERTY: header-args+ :results silent :cache no #+PROPERTY: header-args+ :mkdirp yes #+OPTIONS: H:3 num:nil toc:t \n:t ::t |:t ^:nil -:t f:t *:t - magic single double-quote → " ← FIX changes hilighting behavior (occuring after it) in org document. INVESTIGATE (org-mode CONFIG?) FIND & FIX - [[./doc-reform.org][doc-reform.org]] [[./][org/]] - [[./output_hub.org][output_hub]] * xml offspring (xhtml html epub) ** format xhtml objects :format: *** _module template_ :module: #+HEADER: :tangle "../src/sisudoc/outputs/io_out/xmls.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> module sisudoc.outputs.io_out.xmls; @safe: template outputXHTMLs() { <> mixin spineRgxOut; mixin spineRgxXHTML; struct outputXHTMLs { static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> } } #+END_SRC *** output imports **** xml html #+NAME: output_imports_xml #+BEGIN_SRC d import std.file; import std.outbuffer; import std.uri; import std.conv : to; import sisudoc.outputs.io_out; import sisudoc.outputs.io_out.rgx; import sisudoc.ocda.meta.rgx_files; import sisudoc.outputs.io_out.rgx_xhtml; import sisudoc.outputs.io_out.zip_pod; import sisudoc.outputs.io_out.xmls; import sisudoc.outputs.io_out.xmls_css; #+END_SRC **** epub3 #+NAME: output_imports_epub #+BEGIN_SRC d import std.file; import std.outbuffer; import std.uri; import std.zip; import std.conv : to; import sisudoc.outputs.io_out; import sisudoc.outputs.io_out.rgx; import sisudoc.outputs.io_out.rgx_xhtml; import sisudoc.outputs.io_out.zip_pod; import sisudoc.outputs.io_out.xmls; import sisudoc.outputs.io_out.xmls_css; #+END_SRC *** misc **** div delimiter #+NAME: xhtml_format_objects_div_delimit #+BEGIN_SRC d string div_delimit( string section, return ref string previous_section ) { string delimit = ""; string delimit_ = ""; if (section != previous_section) { switch (section) { case "head": delimit_ ~= "\n
\n" ; break; case "toc": delimit_ ~= "\n
\n" ; break; case "bookindex": delimit_ ~= "\n
\n" ; break; default: delimit_ ~= "\n
\n" ; break; } if (previous_section.length > 0) { delimit ~= "\n
"; } previous_section = section; delimit ~= delimit_; } // you also need to close the last div, introduce a footer? return 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 #+BEGIN_SRC d string special_characters_text(string _txt) { _txt = _txt .replaceAll(rgx_xhtml.ampersand, "&") // "&" .replaceAll(rgx_xhtml.quotation, """) // """ .replaceAll(rgx_xhtml.less_than, "<") // "<" .replaceAll(rgx_xhtml.greater_than, ">") // ">" .replaceAll(rgx.br_line, "
") .replaceAll(rgx.br_line_inline, "
") .replaceAll(rgx.br_line_spaced, "
\n
") .replaceAll(rgx.nbsp_char, " "); return _txt; } #+END_SRC **** special characters plain special_characters_text escapes the markup characters and then puts
back in for a line break, which is right for element content and wrong for an attribute value: a "<" in an attribute is not well-formed XML and parsing stops there. A line break has no meaning in a one line value anyway, so it becomes a space. #+NAME: xhtml_format_objects_special_characters_plain #+BEGIN_SRC d /+ the same escaping for where markup cannot go. +/ string special_characters_plain(string _txt) { _txt = _txt .replaceAll(rgx.br_line, " ") .replaceAll(rgx.br_line_inline, " ") .replaceAll(rgx.br_line_spaced, " ") .replaceAll(rgx_xhtml.ampersand, "&") .replaceAll(rgx_xhtml.quotation, """) .replaceAll(rgx_xhtml.less_than, "<") .replaceAll(rgx_xhtml.greater_than, ">") .replaceAll(rgx.nbsp_char, " "); return _txt; } /+ ↓ a date for an attribute value: the same trimming, no markup +/ string special_characters_plain_date(string _txt) { _txt = _txt .replaceAll(regex(r"(?:-00)+"), "") .replaceAll(rgx.br_line, " ") .replaceAll(rgx.br_line_inline, " ") .replaceAll(rgx.br_line_spaced, " ") .replaceAll(rgx.nbsp_char, " "); return _txt; } #+END_SRC **** special characters date #+NAME: xhtml_format_objects_special_characters_date #+BEGIN_SRC d string special_characters_date(string _txt) { _txt = _txt .replaceAll(regex(r"(?:-00)+"), "") .replaceAll(rgx.br_line, "
") .replaceAll(rgx.br_line_inline, "
") .replaceAll(rgx.br_line_spaced, "
\n
") .replaceAll(rgx.nbsp_char, " "); return _txt; } #+END_SRC **** breaks indents bullets #+NAME: xhtml_format_objects_breaks_indents_bullets #+BEGIN_SRC d /+ ↓ a group's line-leading indent and bullet markers ("_1 ", "_* ", "_2* ", "_0_1 "), then the escaping and the line breaks. . The markers are anchored to the start of a line, so they must be read while the text still has lines: special_characters_text turns the line separator into
and takes the newlines with it, after which "^" matches only at the start of the object and every later marker is printed as text. So the markers go first, into a private-use codepoint that the escaping leaves alone, and that becomes a no-break space after it. U+E003, beside the U+E000 to U+E002 the markdown and typst writers use for their own protection. Written   rather than  , because the epub is xml, and xml defines no   entity: in an epub it is a fatal error. +/ string special_characters_breaks_indents_bullets(O)( const O obj, ) { import std.array : replace, replicate; import std.regex : Captures, replaceAll; enum _sp = ""; string _txt = obj.text; if (obj.metainfo.is_a == "group") { _txt = (_txt) .replaceAll(rgx.grouped_para_indent_hang, _sp.replicate(2)) .replaceAll!((Captures!string m) => _sp.replicate(2 * m["indent"].to!int) ~ "●" ~ _sp.replicate(2)) (rgx.grouped_para_bullet_indent) .replaceAll(rgx.grouped_para_bullet, "●" ~ _sp.replicate(2)) .replaceAll!((Captures!string m) => _sp.replicate(2 * m["indent"].to!int)) (rgx.grouped_para_indent); } _txt = special_characters_text(_txt).replace(_sp, " "); if (!(obj.metainfo.is_a == "code")) { _txt = (_txt) .replaceAll(rgx_xhtml.line_break, "
"); } return _txt; } #+END_SRC **** font_face #+NAME: xhtml_format_objects_font_face #+BEGIN_SRC d string font_face(string _txt) { _txt = _txt .replaceAll(rgx.inline_emphasis, ("$1")) .replaceAll(rgx.inline_bold, ("$1")) .replaceAll(rgx.inline_underscore, ("$1")) .replaceAll(rgx.inline_italics, ("$1")) .replaceAll(rgx.inline_superscript, ("$1")) .replaceAll(rgx.inline_subscript, ("$1")) .replaceAll(rgx.inline_strike, ("$1")) .replaceAll(rgx.inline_insert, ("$1")) .replaceAll(rgx.inline_mono, ("$1")) .replaceAll(rgx.inline_cite, ("$1")); return _txt; } #+END_SRC **** heading element #+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 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, in html as in epub: there is no element. +/ int _heading_element_level(O)(O obj, string _xml_type) { int _lev = obj.metainfo.heading_lev_markup.to!int; if (_lev < 1) { return 1; } return (_lev > 6) ? 6 : _lev; } /+ ↓ the class a heading carries. The title (level A) shares

with level B, having no element of its own since bespoke is not recognized and no longer used, so it is told apart by class: "heading title", which the stylesheets size a step above h1, as h0 was. Shifting every level down one instead would give the title alone h1, and would further compress the limited html heading range (h1 - h6). +/ string _heading_class(O)(O obj) { return (obj.metainfo.heading_lev_markup == 0) ? obj.metainfo.is_a ~ " title" : obj.metainfo.is_a; } #+END_SRC **** anchor tags #+NAME: xhtml_format_objects_xml_anchor_tags #+BEGIN_SRC d /+ ↓ the anchors an object carries. "name" on an 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_", 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 ~= (_is_a_unique_target) ? "" : ""; } } } return tags; } #+END_SRC **** doc head & tails ***** dublin core #+NAME: xhtml_format_objects_header_dc_metadata #+BEGIN_SRC d /+ ↓ Dublin Core metas, shared by the html and epub head. A meta is written only where there is a value for it. +/ string dc_metadata(M)( M doc_matters, string _indent, bool _generator, ) { auto _meta = doc_matters.conf_make_meta.meta; string o; void _dc(string _name, string _content) { if (!(_content.empty)) { o ~= _indent ~ "\n"; } } _dc("dc.title", special_characters_plain(_meta.title_full)); _dc("dc.author", special_characters_plain(_meta.creator_author)); _dc("dc.publisher", special_characters_plain(_meta.publisher)); _dc("dc.date", special_characters_plain_date(_meta.date_published)); _dc("dc.date.created", special_characters_plain_date(_meta.date_created)); _dc("dc.date.issued", special_characters_plain_date(_meta.date_issued)); _dc("dc.date.available", special_characters_plain_date(_meta.date_available)); _dc("dc.date.valid", special_characters_plain_date(_meta.date_valid)); _dc("dc.date.modified", special_characters_plain_date(_meta.date_modified)); _dc("dc.language", doc_matters.src.language_tag); _dc("dc.rights", special_characters_plain(_meta.rights_copyright)); if (_generator) { _dc("generator", special_characters_plain(doc_matters.generator_program.name_and_version)); } o ~= _indent ~ ""; return o; } #+END_SRC ***** metadata #+NAME: xhtml_format_objects_header_metadata #+BEGIN_SRC d string header_metadata(M)( M doc_matters, ) { return "\n" ~ " \n" ~ dc_metadata(doc_matters, " ", doc_matters.opt.action.generated_by) ~ "\n" ~ " "; } #+END_SRC ***** site info button #+NAME: xhtml_format_objects_site_info_button #+BEGIN_SRC d string site_info_button(M)( M doc_matters, ) { string _locations; if (doc_matters.conf_make_meta.make.home_button_text.length > 0) { _locations = (doc_matters.conf_make_meta.make.home_button_text) .replaceAll( rgx.inline_link, ("

$1

")) .replaceAll( rgx.br_line, "") .replaceAll( rgx.br_line_inline, ""); } else { _locations = "

spine

\n

sources / git

\n

www.sisudoc.org

"; } string o; o = format(q"┃
%s
┃", _locations, ); return o; } #+END_SRC ***** search form #+NAME: xhtml_format_objects_inline_search_form #+BEGIN_SRC d string inline_search_form(M)( M doc_matters, ) { string o; string _form; if (doc_matters.opt.action.html_link_search) { o = format(q"┃
  %s %s
   %s
┃", doc_matters.conf_make_meta.conf.w_srv_cgi_action, (doc_matters.conf_make_meta.conf.w_srv_db_sqlite_filename.empty) ? "" : "\n 🔎 ", (doc_matters.conf_make_meta.conf.w_srv_db_sqlite_filename.empty) ? "" : "\n ", doc_matters.src.filename_base, doc_matters.conf_make_meta.conf.w_srv_cgi_action, (doc_matters.conf_make_meta.conf.w_srv_db_sqlite_filename.empty) ? "" : "\n ", doc_matters.src.filename_base, ); } else { o = ""; } return o; } #+END_SRC ***** html head & head banner ▣ ▤ ⁜ ☀ ፨ ፠ ※ ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ 〓 ⚏ ⚏ ⌘ 🫛 🗎 ⌥ ⟰ #+NAME: xhtml_format_objects_html_head #+BEGIN_SRC d string html_head(M)( M doc_matters, string type, ) { string o; string metadata_links = ((doc_matters.opt.action.html_link_curate) ? format(q"┃

⟰  [ %s  ◆ %s%s ℹ ]   ≅ | 🖋 | ⌘ |

┃", (doc_matters.opt.action.webserver_url_doc_root.length > 0) ? doc_matters.opt.action.webserver_url_doc_root : doc_matters.conf_make_meta.conf.w_srv_data_root_url , // HOME index.html equivalent _cfg.www_url_doc_root, (type == "seg") ? "../" ~ doc_matters.src.filename_base ~ ".html" : "./" ~ doc_matters.src.filename_base ~ "/toc.html", (type == "seg") ? "▤" : "※", (type == "seg") ? "../../" : "../", doc_matters.src.filename_base, doc_matters.src.lng, (doc_matters.opt.action.html_link_pdf || doc_matters.opt.action.html_link_pdf_a4) ? (" □ ") : "", (doc_matters.opt.action.html_link_pdf || doc_matters.opt.action.html_link_pdf_letter) ? (" □ ") : "", (type == "seg") ? "../" : "", doc_matters.src.filename_base, (type == "seg") ? "../" : "", (type == "seg") ? "../" : "", (type == "seg") ? "../" : "", ) : ""); o = format(q"┃ %s%s %s
%s
%s
%s
┃", special_characters_text(doc_matters.conf_make_meta.meta.title_full), (doc_matters.conf_make_meta.meta.creator_author.empty) ? "" : ", " ~ special_characters_text(doc_matters.conf_make_meta.meta.creator_author), header_metadata(doc_matters), ((type == "seg") ? "../" : ""), ((type == "seg") ? "../../../css/html_seg.css" : "../../css/html_scroll.css"), doc_matters.src.language_tag, site_info_button(doc_matters), metadata_links, inline_search_form(doc_matters), ); return o; } #+END_SRC ***** epub seg head #+NAME: xhtml_format_objects_epub3_seg_head #+BEGIN_SRC d string epub3_seg_head(M)( M doc_matters, ) { string html_simple = format(q"┃ ┃", doc_matters.src.language_tag, doc_matters.src.language_tag, ); string o; o = format(q"┃%s %s%s %s ┃", html_simple, special_characters_plain(doc_matters.conf_make_meta.meta.title_full), (doc_matters.conf_make_meta.meta.creator_author.empty) ? "" : ", " ~ special_characters_plain(doc_matters.conf_make_meta.meta.creator_author), dc_metadata(doc_matters, " ", true), doc_matters.src.language_tag, ); return o; } #+END_SRC ***** xhtml dom tail #+NAME: xhtml_format_objects_dom_close #+BEGIN_SRC d string dom_close() { string o; o = format(q"┃

┃"); return o; } #+END_SRC ***** xhtml tail #+NAME: xhtml_format_objects_tail #+HEADER: :noweb yes #+BEGIN_SRC d string tail(M)(M doc_matters) { string o; o = format(q"┃
%s

≅ SiSU Spine ፨ (object numbering & object search)

(web 1993, object numbering 1997, object search 2002 ...) <>

┃", special_characters_text(doc_matters.conf_make_meta.meta.rights_copyright), ((doc_matters.conf_make_meta.meta.rights_license).empty) ? "" : "
", ((doc_matters.conf_make_meta.meta.rights_license).empty) ? "" : "License: " ~ special_characters_text(doc_matters.conf_make_meta.meta.rights_license) ); return o; } #+END_SRC *** inline markup **** images #+NAME: xhtml_format_objects_inline_images #+BEGIN_SRC d string inline_images(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "seg", ) { string _img_pth; switch (_xml_type) { case "epub": _img_pth = "image/"; break; case "scroll": _img_pth = format(q"┃%s/image/┃", "../.."); break; case "seg": _img_pth = format(q"┃%s/image/┃", "../../.."); break; default: break; } if (_txt.match(rgx.inline_image)) { _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 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 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 "\"""; } #+END_SRC **** links ***** scroll, seg, epub #+NAME: xhtml_format_objects_inline_links #+BEGIN_SRC d string inline_links(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "seg", ) { string seg_lvs; if (obj.has.inline_links) { if (obj.metainfo.is_a != "code") { _txt = replaceAll!(m => m[1] ~ "┤" ~ (replaceAll!(n => n["type"] ~ n["path"] ~ (n["file"].encodeComponent) )((obj.stow.link[m["num"].to!ulong]).to!string, rgx.uri_identify_components)) ~ "├" )(_txt, rgx.inline_link_number_only); } if ((_txt.match(rgx.mark_internal_site_lnk)) && (_xml_type == "scroll")) { // conditions reversed to avoid: gdc compiled program run segfault _txt = _txt.replaceAll( rgx.inline_seg_link, "$1"); } 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) && (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] ) { _txt = _txt.replaceFirst( rgx.inline_link_hash, "┥$1┝┤$3" ~ _suffix ~ "├" ); } else { _txt = _txt.replaceFirst( rgx.inline_link_hash, "┥$1┝┤" ~ doc_matters.has.tag_associations[(m.captures["hash"])][seg_lvs] ~ _suffix ~ "#" ~ "$3" ~ "├" ); } } else { if (doc_matters.opt.action.vox_gt_1) { writeln( "WARNING on internal document links, anchor to link <<" ~ m.captures["hash"] ~ ">> not found in document, " ~ "anchor: " ~ m.captures["hash"] ~ " document: " ~ doc_matters.src.filename ); } } } } _txt = _txt .replaceAll( rgx.inline_link_fn_suffix, ("$1" ~ _suffix)) .replaceAll( rgx.inline_link, ("$1")) .replaceAll( rgx.mark_internal_site_lnk, ""); } debug(markup_links) { if (_txt.match(rgx.inline_link)) { writeln(__LINE__, " (missed) markup link identified (", obj.has.inline_links, "): ", obj.metainfo.is_a, ": ", obj.text ); } } debug(markup) { if (_txt.match(rgx.inline_link)) { writeln(__LINE__, " (missed) markup link identified (", obj.has.inline_links, "): ", obj.metainfo.is_a, ": ", obj.text ); } } return _txt; } #+END_SRC **** notes ***** scroll #+NAME: xhtml_format_objects_inline_notes_scroll #+BEGIN_SRC d string inline_notes_scroll(O,M)( string _txt, const O obj, M doc_matters, ) { if (obj.has.inline_notes_reg) { _txt = font_face(_txt); _txt = _txt.replaceAll( rgx.inline_notes_al_regular_number_note, (" $1 ") ); } if (obj.has.inline_notes_star) { _txt = font_face(_txt); _txt = _txt.replaceAll( rgx.inline_notes_al_special_char_note, (" $1 ") ); } debug(markup_endnotes) { if (_txt.match(rgx.inline_notes_al_regular_number_note)) { writeln(__LINE__, " (missed) markup endnote: ", obj.metainfo.is_a, ": ", obj.text); } } debug(markup) { if (_txt.match(rgx.inline_notes_al_regular_number_note)) { writeln(__LINE__, " (missed) markup endnote: ", obj.metainfo.is_a, ": ", obj.text); } } return _txt; } #+END_SRC ***** seg #+NAME: xhtml_format_objects_inline_notes_seg #+BEGIN_SRC d Tuple!(string, string[]) inline_notes_seg(O,M)( string _txt, const O obj, M doc_matters, ) { string[] _endnotes; if (obj.has.inline_notes_star) { _txt = font_face(_txt); /+ need markup for text, and separated footnote +/ 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", "

", "", " ", m.captures[1], ".", m.captures[2], "

" ); } _txt = _txt.replaceAll( rgx.inline_notes_al_special_char_note, (" $1 ") ); } if (obj.has.inline_notes_reg) { _txt = font_face(_txt); /+ need markup for text, and separated footnote +/ 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", "

", "", " ", m.captures[1], ".", m.captures[2], "

" ); } _txt = _txt.replaceAll( rgx.inline_notes_al_regular_number_note, (" $1 ") ); } else if (_txt.match(rgx.inline_notes_al_regular_number_note)) { debug(markup) { writeln(__LINE__, " endnote: ", obj.metainfo.is_a, ": ", obj.text); } } Tuple!(string, string[]) t = tuple( _txt, _endnotes, ); return t; } #+END_SRC **** inline markup ***** scroll #+NAME: xhtml_format_objects_inline_markup_scroll #+BEGIN_SRC d string inline_markup_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", ) { if (obj.metainfo.dummy_heading && (obj.metainfo.is_a == "toc" || obj.metainfo.is_a == "heading")) { _txt = ""; } else { _txt = inline_images(_txt, obj, doc_matters, _suffix, "scroll"); _txt = inline_links(_txt, obj, doc_matters, _suffix, "scroll"); _txt = inline_notes_scroll(_txt, obj, doc_matters); } return _txt; } #+END_SRC ***** seg #+NAME: xhtml_format_objects_inline_markup_seg #+BEGIN_SRC d Tuple!(string, string[]) inline_markup_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "seg", ) { if (obj.metainfo.dummy_heading && ((_xml_type == "epub" && (obj.metainfo.is_a == "toc" || obj.metainfo.is_a == "heading")) || obj.metainfo.is_a == "heading") ) { _txt = ""; } else { _txt = inline_images(_txt, obj, doc_matters, _suffix, _xml_type); // TODO _txt = inline_links(_txt, obj, doc_matters, _suffix, _xml_type); // TODO } Tuple!(string, string[]) t = inline_notes_seg(_txt, obj, doc_matters); return t; } #+END_SRC *** toc **** subtoc #+NAME: xhtml_format_objects_lev4_heading_subtoc #+BEGIN_SRC d string lev4_heading_subtoc(O,M)( const O obj, M doc_matters, ) { char[] lev4_subtoc; lev4_subtoc ~= "
\n"; foreach (subtoc; obj.tags.lev4_subtoc) { if (auto m = subtoc.match(rgx.inline_link_subtoc)) { auto indent = (m.captures[1].to!int - 3).to!string; // css assumptions based on use of em for left margin & indent auto text = m.captures[2].to!string; text = font_face(text); auto link = m.captures[3].to!string; lev4_subtoc ~= subtoc.replaceFirst(rgx.inline_link_subtoc, format(q"┃

۰ %s

┃", indent, indent, link, text, )); } } lev4_subtoc ~= "
\n"; return lev4_subtoc.to!string; } #+END_SRC **** navigation pre next svg #+NAME: xhtml_format_objects_nav_pre_next_svg #+BEGIN_SRC d auto nav_pre_next_svg(O,M)( const O obj, M doc_matters, ) { string prev, next, toc; if (obj.tags.segment_anchor_tag_epub == "toc") { toc = ""; prev = ""; } else { toc = format(q"┃ ┃", ); } if (obj.tags.segname_prev == "") { prev = ""; } else { prev = format(q"┃ ┃", obj.tags.segname_prev, ); } if (obj.tags.segname_next == "") { next = ""; } else { next = format(q"┃ ┃", obj.tags.segname_next, ); } string _toc_pre_next = format(q"┃ ┃", toc, prev, next, ); string _pre_next = format(q"┃ ┃", prev, next, ); struct bar { string toc_pre_next() { return _toc_pre_next; } string pre_next() { return _pre_next; } } return bar(); } #+END_SRC *** heading **** heading #+NAME: xhtml_format_objects_heading #+BEGIN_SRC d string heading(O,M)( string _txt, const O obj, M doc_matters, string _xml_type = "html", ) { assert(obj.metainfo.is_of_part == "body" || "frontmatter" || "backmatter"); assert(obj.metainfo.is_of_section == "body" || "toc" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "para"); assert(obj.metainfo.is_a == "heading"); string tags = _xhtml_anchor_tags(obj); string heading_lev_anchor_tag; string _horizontal_rule = "
"; if ((_xml_type != "html") || (obj.metainfo.heading_lev_markup == 0 || obj.metainfo.heading_lev_markup > 4)) { _horizontal_rule = ""; } _txt = font_face(_txt); string o; heading_lev_anchor_tag = (obj.tags.heading_lev_anchor_tag.empty) ? "" : ""; if (_txt.empty) { o = format(q"┃%s ┃", _horizontal_rule, ); } else { o = ""; if (obj.metainfo.is_of_section == "toc") { o ~= format(q"┃
┃", special_characters_text(doc_matters.conf_make_meta.meta.rights_copyright), special_characters_text(doc_matters.conf_make_meta.meta.rights_license) ); } } if (!(obj.metainfo.identifier.empty)) { o ~= format(q"┃%s
%s%s %s
┃", _horizontal_rule, obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), _heading_element_level(obj, _xml_type), _heading_class(obj), obj.metainfo.identifier, tags, heading_lev_anchor_tag, _txt, _heading_element_level(obj, _xml_type), ); } else { o ~= format(q"┃%s
%s%s %s
┃", _horizontal_rule, _heading_element_level(obj, _xml_type), _heading_class(obj), tags, heading_lev_anchor_tag, _txt, _heading_element_level(obj, _xml_type), ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_heading_scroll #+BEGIN_SRC d string heading_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", ) { _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = heading(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_heading_seg #+BEGIN_SRC d Tuple!(string, string[]) heading_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0]; string[] _endnotes = t[1]; string o = heading(_txt, obj, doc_matters, _xml_type); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** para **** para #+NAME: xhtml_format_objects_para #+BEGIN_SRC d string para(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body" || "frontmatter" || "backmatter"); assert(obj.metainfo.is_of_section == "body" || "toc" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "para"); assert(obj.metainfo.is_a == "para" || "toc" || "endnote" || "glossary" || "bibliography" || "bookindex" || "blurb"); string tags = _xhtml_anchor_tags(obj); _txt = font_face(_txt); string o; _txt = (obj.attrib.bullet) ? ("●  " ~ _txt) : _txt; _txt = _txt.replaceFirst(rgx.inline_link_anchor, ""); if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s %s

┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.attrib.indent_hang, obj.attrib.indent_base, obj.metainfo.identifier, _para_role(obj), tags, _txt ); } else { o = format(q"┃

%s %s

┃", obj.metainfo.is_a, obj.attrib.indent_hang, obj.attrib.indent_base, _para_role(obj), tags, _txt ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_para_scroll #+BEGIN_SRC d string para_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", ) { if (obj.metainfo.is_a == "toc" && _txt.match(rgx.inline_link_toc_to_backmatter)) { _txt = _txt.replaceAll(rgx.inline_link_toc_to_backmatter, "┤#section_$1├"); } _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = para(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_para_seg #+BEGIN_SRC d Tuple!(string, string[]) para_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0].to!string; string[] _endnotes = t[1]; string o = para(_txt, obj, doc_matters); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** quote **** quote #+NAME: xhtml_format_objects_quote #+BEGIN_SRC d string quote(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "quote"); _txt = font_face(_txt); string o; if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s

┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.identifier, _txt ); } else { o = format(q"┃

%s

┃", obj.metainfo.is_a, _txt ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_quote_scroll #+BEGIN_SRC d string quote_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", ) { _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = quote(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_quote_seg #+BEGIN_SRC d Tuple!(string, string[]) quote_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0].to!string; string[] _endnotes = t[1]; string o = quote(_txt, obj, doc_matters); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** group **** group #+NAME: xhtml_format_objects_group #+BEGIN_SRC d string group(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "group"); _txt = font_face(_txt); string o; if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s

┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.identifier, _txt ); } else { o = format(q"┃

%s

┃", obj.metainfo.is_a, _txt ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_group_scroll #+BEGIN_SRC d string group_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = group(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_group_seg #+BEGIN_SRC d Tuple!(string, string[]) group_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0].to!string; string[] _endnotes = t[1]; string o = group(_txt, obj, doc_matters); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** block **** block #+NAME: xhtml_format_objects_block #+BEGIN_SRC d string block(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "block"); _txt = font_face(_txt); string o; if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s

┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.identifier, _txt.stripRight ); } else { o = format(q"┃

%s

┃", obj.metainfo.is_a, _txt.stripRight ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_block_scroll #+BEGIN_SRC d string block_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = block(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_block_seg #+BEGIN_SRC d Tuple!(string, string[]) block_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0].to!string; string[] _endnotes = t[1]; string o = block(_txt, obj, doc_matters); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** poem verse **** verse #+NAME: xhtml_format_objects_verse #+BEGIN_SRC d string verse(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body" || "glossary" || "bibliography" || "bookindex" || "blurb"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "verse"); _txt = font_face(_txt); string o; if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s

┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.identifier, _txt ); } else { o = format(q"┃

%s

┃", obj.metainfo.is_a, _txt ); } return o; } #+END_SRC **** scroll #+NAME: xhtml_format_objects_verse_scroll #+BEGIN_SRC d string verse_scroll(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { _txt = inline_markup_scroll(_txt, obj, doc_matters, _suffix); string o = verse(_txt, obj, doc_matters); return o; } #+END_SRC **** seg #+NAME: xhtml_format_objects_verse_seg #+BEGIN_SRC d Tuple!(string, string[]) verse_seg(O,M)( string _txt, const O obj, M doc_matters, string _suffix = ".html", string _xml_type = "html", ) { Tuple!(string, string[]) t = inline_markup_seg(_txt, obj, doc_matters, _suffix, _xml_type); _txt = t[0].to!string; string[] _endnotes = t[1]; string o = verse(_txt, obj, doc_matters); Tuple!(string, string[]) u = tuple( o, _endnotes, ); return u; } #+END_SRC *** code #+NAME: xhtml_format_objects_code #+BEGIN_SRC d string code(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "code"); string o; string codelines(string _txt) { string _codelines; if (obj.code_block.linenumbers) { string[] _block_lines = (_txt).split(rgx.br_linebreaks_newlines); _codelines = "
\n";
      foreach (k, _line; _block_lines) {
        if (k == 1) {
          _codelines ~= format(q"┃    %s
┃",
            _line,
          );
        } else {
          _codelines ~= format(q"┃    %s
┃",
            _line,
          );
        }
      }
      _codelines ~= "  
"; } else { _codelines = "
\n";
      _codelines ~= _txt;
      _codelines ~= "  
"; } return _codelines; } /+ ↓ a code block is wrapped in a
, not a

+/ if (!(obj.metainfo.identifier.empty)) { o = format(q"┃

%s
┃", obj.metainfo.identifier, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.identifier, codelines(_txt) ); } else { o = format(q"┃
%s
┃", obj.metainfo.is_a, codelines(_txt) ); } return o; } #+END_SRC *** table **** TODO tablarize align="left|right|center" $100 "style=\"text-align:right\"" "style=\"text-align:left\"" "style=\"text-align:" ~ "right\"" #+NAME: xhtml_format_objects_tablarize #+BEGIN_SRC d /+ ↓ column widths as a , 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) ? "" : ""; } return "" ~ _cols ~ ""; } Tuple!(string, string) tablarize(O)( string _txt, const O obj, ) { string[] _table_rows = (_txt).split(rgx.table_delimiter_row); 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 , the rest in +/ if (row_idx == 0 && obj.table.heading) { _table ~= ""; _in_head = true; } else if (!_in_body) { if (_in_head) { _table ~= ""; _in_head = false; } _table ~= ""; _in_body = true; } _table ~= ""; 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 ~= ""; } _table ~= ""; } if (_in_head) { _table ~= ""; } else if (_in_body) { _table ~= ""; } Tuple!(string, string) t = tuple( (_table_colgroup(obj) ~ _table), _tablenote, ); return t; } #+END_SRC **** table #+NAME: xhtml_format_objects_table #+BEGIN_SRC d string table(O,M)( string _txt, const O obj, M doc_matters, ) { assert(obj.metainfo.is_of_part == "body"); assert(obj.metainfo.is_of_section == "body"); assert(obj.metainfo.is_of_type == "block"); assert(obj.metainfo.is_a == "table"); string tags = _xhtml_anchor_tags(obj); _txt = font_face(_txt); auto t = tablarize(_txt, obj); _txt = t[0]; string _note = t[1]; string o; o = format(q"┃
%s %s
%s
┃", obj.metainfo.object_number, (doc_matters.opt.action.ocn_off) ? "" : ((obj.metainfo.object_number.empty) ? "" : obj.metainfo.identifier), obj.metainfo.is_a, obj.metainfo.object_number, tags, _txt, (_note.strip.empty) ? "" : ("

" ~ _note.strip ~ "

") ); return o; } #+END_SRC * _html_ [#A] :html: ** _module template_ :module: #+HEADER: :tangle "../src/sisudoc/outputs/io_out/html.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> module sisudoc.outputs.io_out.html; @safe: template outputHTML() { <> mixin outputXHTMLs; void scroll(D)(D doc) { <> <> <> <> <> <> } <> void seg(D)(D doc) { <> <> <> <> <> <> <> <> } <> <> <> } #+END_SRC ** scroll :scroll: *** ↻ loop & switch (sections & objects) format html output #+NAME: output_html_scroll_0 #+BEGIN_SRC d mixin spineRgxOut; mixin spineRgxXHTML; auto xhtml_format = outputXHTMLs(); static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); string[] doc_html; string[] doc_out; string suffix = ".html"; string previous_section = ""; string delimit = ""; #+END_SRC **** ↻ the loops & outer switch (sections & objects) format output #+NAME: output_html_scroll_loop_parts #+BEGIN_SRC d foreach (section; doc.matters.has.keys_seq.scroll) { foreach (obj; doc.abstraction[section]) { delimit = xhtml_format.div_delimit(section, previous_section); string _txt = xhtml_format.special_characters_breaks_indents_bullets(obj); switch (obj.metainfo.is_of_part) { #+END_SRC ***** frontmatter #+NAME: output_html_scroll_is_frontmatter #+BEGIN_SRC d case "frontmatter": assert(section == "head" || "toc"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "heading": doc_html ~= delimit ~ xhtml_format.heading_scroll(_txt, obj, doc.matters, suffix); break; case "toc": doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ***** body #+NAME: output_html_scroll_is_body #+BEGIN_SRC d case "body": assert(section == "body" || "head"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "heading": doc_html ~= delimit ~ xhtml_format.heading_scroll(_txt, obj, doc.matters, suffix); break; case "para": doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; case "block": switch (obj.metainfo.is_a) { case "quote": doc_html ~= xhtml_format.quote_scroll(_txt, obj, doc.matters); break; case "group": doc_html ~= xhtml_format.group_scroll(_txt, obj, doc.matters); break; case "block": doc_html ~= xhtml_format.block_scroll(_txt, obj, doc.matters); break; case "poem": break; case "verse": doc_html ~= xhtml_format.verse_scroll(_txt, obj, doc.matters, suffix); break; case "code": doc_html ~= xhtml_format.code(_txt, obj, doc.matters); break; case "table": doc_html ~= xhtml_format.table(_txt, obj, doc.matters); break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ***** backmatter #+NAME: output_html_scroll_is_backmatter #+BEGIN_SRC d case "backmatter": assert(section == "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "heading": doc_html ~= delimit ~ xhtml_format.heading_scroll(_txt, obj, doc.matters, suffix); break; case "endnote": assert(section == "endnotes"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; case "glossary": assert(section == "glossary"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; case "bibliography": assert(section == "bibliography"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; case "bookindex": assert(section == "bookindex"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; case "blurb": assert(section == "blurb"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; case "tail": assert(section == "tail"); doc_html ~= xhtml_format.para_scroll(_txt, obj, doc.matters, suffix); break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; case "comment": break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_part); writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); writeln(__FILE__, ":", __LINE__, ": ", obj.text); } } break; #+END_SRC ***** closings & post loop #+NAME: output_html_scroll_close_is_case #+BEGIN_SRC d } } } doc_out = xhtml_format.html_head(doc.matters, "scroll") ~ doc_html ~ xhtml_format.dom_close ~ xhtml_format.tail(doc.matters); scroll_write_output(doc_out, doc.matters); #+END_SRC *** write output file #+NAME: output_html_scroll_scroll_write_output #+BEGIN_SRC d @trusted void scroll_write_output(O,M)( O doc_out, M doc_matters, ) { debug(asserts) { static assert(is(typeof(doc_out) == string[])); } auto pth_html = spinePathsHTML!()(doc_matters.output_path, doc_matters.src.language); try { if (!exists(pth_html.base)) { pth_html.base.mkdirRecurse; } { auto f = File(pth_html.fn_scroll(doc_matters.src.filename), "w"); foreach (o; doc_out) { f.writeln(o); } } if (!exists(pth_html.base ~ "/index.html")) { import sisudoc.outputs.io_out.html_snippet; mixin htmlSnippet; auto f = File(pth_html.base ~"/index.html", "w"); f.writeln(format_html_blank_page_guide_home( "../../css/html_scroll.css", (doc_matters.opt.action.webserver_url_doc_root.length > 0) ? doc_matters.opt.action.webserver_url_doc_root : doc_matters.conf_make_meta.conf.w_srv_data_root_url, "../../index.html", )); } } catch (ErrnoException ex) { // Handle error } if (doc_matters.opt.action.vox_gt_1) { writeln(" ", pth_html.fn_scroll(doc_matters.src.filename)); } } #+END_SRC ** seg :seg: *** ↻ loop & switch (sections & objects) format html output #+NAME: output_html_seg_0 #+BEGIN_SRC d mixin spineRgxOut; mixin spineRgxXHTML; static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); auto xhtml_format = outputXHTMLs(); string[][string] doc_html; string[][string] doc_html_endnotes; string segment_filename; string[] top_level_headings = ["","","",""]; string previous_seg_filename = ""; string suffix = ".html"; string previous_section = ""; string delimit = ""; #+END_SRC **** ↻ the loop (sections & objects) format output #+NAME: output_html_seg_loop_parts #+BEGIN_SRC d foreach (section; doc.matters.has.keys_seq.seg) { foreach (obj; doc.abstraction[section]) { delimit = xhtml_format.div_delimit(section, previous_section); string _txt = xhtml_format.special_characters_breaks_indents_bullets(obj); #+END_SRC ***** all headings #+NAME: output_html_seg_is_heading #+BEGIN_SRC d if (obj.metainfo.is_a == "heading") { assert(section == "head" || "toc" || "body" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); switch (obj.metainfo.heading_lev_markup) { case 0: .. case 3: /+ fill buffer, and replace with new levels from 1 to 3 +/ switch (obj.metainfo.heading_lev_markup) { case 0: top_level_headings[0] = ""; top_level_headings[1] = ""; top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 1: top_level_headings[1] = ""; top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 2: top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 3: top_level_headings[3] = ""; goto default; default: Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "seg"); top_level_headings[obj.metainfo.heading_lev_markup] = t[0]; break; } break; case 4: segment_filename = obj.tags.segment_anchor_tag_epub; doc_html[segment_filename] ~= xhtml_format.html_head(doc.matters, "seg"); auto navigation_bar = xhtml_format.nav_pre_next_svg(obj, doc.matters); doc_html[segment_filename] ~= navigation_bar.toc_pre_next; previous_seg_filename = segment_filename; foreach (top_level_heading; top_level_headings) { doc_html[segment_filename] ~= top_level_heading; } Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0].to!string; doc_html[segment_filename] ~= xhtml_format.lev4_heading_subtoc(obj, doc.matters); doc_html_endnotes[segment_filename] ~= t[1]; break; case 5: .. case 7: Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0].to!string; doc_html_endnotes[segment_filename] ~= t[1]; break; case 8: .. case 9: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a, ": ", obj.metainfo.heading_lev_markup); writeln(__FILE__, ":", __LINE__, ": ", obj.text); } } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a, ": ", obj.metainfo.heading_lev_markup); } } break; } #+END_SRC ***** non-heading #+NAME: output_html_seg_is_not_heading_switch #+BEGIN_SRC d } else { assert(section == "head" || "toc" || "body" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); Tuple!(string, string[]) t; switch (obj.metainfo.is_of_part) { #+END_SRC ****** frontmatter #+NAME: output_html_seg_is_frontmatter #+BEGIN_SRC d case "frontmatter": assert(section == "head" || "toc"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "toc": t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0].to!string; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; #+END_SRC ****** body #+NAME: output_html_seg_is_body #+BEGIN_SRC d case "body": assert(section == "body"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "para": t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0].to!string; doc_html_endnotes[segment_filename] ~= t[1]; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; case "block": switch (obj.metainfo.is_a) { case "quote": t = xhtml_format.quote_seg(_txt, obj, doc.matters, suffix, "seg"); goto default; case "group": t = xhtml_format.group_seg(_txt, obj, doc.matters, suffix, "seg"); goto default; case "block": t = xhtml_format.block_seg(_txt, obj, doc.matters, suffix, "seg"); goto default; case "poem": break; case "verse": t = xhtml_format.verse_seg(_txt, obj, doc.matters, suffix, "seg"); goto default; case "code": doc_html[segment_filename] ~= xhtml_format.code(_txt, obj, doc.matters); break; case "table": doc_html[segment_filename] ~= xhtml_format.table(_txt, obj, doc.matters); doc_html_endnotes[segment_filename] ~= ""; break; default: if ((obj.metainfo.is_a == "quote" || obj.metainfo.is_a == "group" || obj.metainfo.is_a == "block" || obj.metainfo.is_a == "verse" )) { doc_html[segment_filename] ~= t[0].to!string; doc_html_endnotes[segment_filename] ~= t[1]; } else { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ****** backmatter #+NAME: output_html_seg_is_backmatter #+BEGIN_SRC d case "backmatter": assert(section == "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "endnote": assert(section == "endnotes"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; break; case "glossary": assert(section == "glossary"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; doc_html_endnotes[segment_filename] ~= t[1]; break; case "bibliography": assert(section == "bibliography"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; doc_html_endnotes[segment_filename] ~= t[1]; break; case "bookindex": assert(section == "bookindex"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; doc_html_endnotes[segment_filename] ~= t[1]; break; case "blurb": assert(section == "blurb"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; doc_html_endnotes[segment_filename] ~= t[1]; break; case "tail": assert(section == "tail"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "seg"); doc_html[segment_filename] ~= t[0]; doc_html_endnotes[segment_filename] ~= t[1]; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; case "comment": break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_html) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_part); } } break; } #+END_SRC ***** closings & post loop #+NAME: output_html_seg_close_is_case #+BEGIN_SRC d } } } seg_write_output(doc_html, doc_html_endnotes, doc.matters); #+END_SRC *** write output files #+NAME: output_html_seg_write_output #+BEGIN_SRC d @trusted void seg_write_output(D,E,M)( // @system? D doc_html, E doc_html_endnotes, M doc_matters, ) { debug(asserts) { static assert(is(typeof(doc_html) == string[][string])); } mixin spineRgxFiles; static auto rgx_files = RgxFiles(); auto pth_html = spinePathsHTML!()(doc_matters.output_path, doc_matters.src.language); auto xhtml_format = outputXHTMLs(); auto m = doc_matters.src.filename.matchFirst(rgx_files.src_fn); try { if (!exists(pth_html.seg(doc_matters.src.filename))) { pth_html.seg(doc_matters.src.filename).mkdirRecurse; } foreach (seg_filename; doc_matters.has.segnames_lv4) { auto f = File(pth_html.fn_seg(doc_matters.src.filename, seg_filename), "w"); foreach (docseg; doc_html[seg_filename]) { f.writeln(docseg); } foreach (docseg; doc_html_endnotes[seg_filename]) { f.writeln(docseg); } f.writeln(xhtml_format.tail(doc_matters)); } if (!exists(pth_html.fn_seg(doc_matters.src.filename, "index"))) { symlink("./toc.html", (pth_html.fn_seg(doc_matters.src.filename, "index"))); } } catch (ErrnoException ex) { // handle error } if (doc_matters.opt.action.vox_gt_1) { writeln(" ", pth_html.fn_seg(doc_matters.src.filename, "toc")); } } #+END_SRC ** css :css: #+NAME: output_html_css #+BEGIN_SRC d void css(M)(M doc_matters) { auto css = spineCss(doc_matters); auto pth_html = spinePathsHTML!()(doc_matters.output_path, doc_matters.src.language); try { if (!exists(pth_html.css)) { (pth_html.css).mkdirRecurse; } { auto f = File(pth_html.fn_seg_css, "w"); f.writeln(css.html_seg); f = File(pth_html.fn_scroll_css, "w"); f.writeln(css.html_scroll); } if (!exists(pth_html.css ~ "/index.html")) { import sisudoc.outputs.io_out.html_snippet; mixin htmlSnippet; auto f = File(pth_html.css ~"/index.html", "w"); f.writeln(format_html_blank_page_guide_home( "./css/html_scroll.css", (doc_matters.opt.action.webserver_url_doc_root.length > 0) ? doc_matters.opt.action.webserver_url_doc_root : doc_matters.conf_make_meta.conf.w_srv_data_root_url, "../index.html", )); } } catch (ErrnoException ex) { // Handle error } } #+END_SRC ** images :images: #+NAME: copy_html_images #+BEGIN_SRC d @trusted void images_cp(M)(M doc_matters) { // @system { /+ (copy html images) +/ auto pth_html = spinePathsHTML!()(doc_matters.output_path, doc_matters.src.language); if (!exists(pth_html.image)) { pth_html.image.mkdirRecurse; } foreach (image; doc_matters.srcs.image_list) { auto fn_src_in = doc_matters.src.image_dir_path ~ "/" ~ image; auto fn_src_out = pth_html.image ~ "/" ~ image; debug(images_html) { writeln(fn_src_in, " -> ", fn_src_out); } if (exists(fn_src_in)) { fn_src_in.copy(fn_src_out); } else { if (doc_matters.opt.action.vox_gt_1) { writeln("WARNING image not found: ", fn_src_in); } } } if (!exists(pth_html.image ~ "/index.html")) { import sisudoc.outputs.io_out.html_snippet; mixin htmlSnippet; auto f = File(pth_html.image ~"/index.html", "w"); f.writeln(format_html_blank_page_guide_home( "../css/html_scroll.css", (doc_matters.opt.action.webserver_url_doc_root.length > 0) ? doc_matters.opt.action.webserver_url_doc_root : doc_matters.conf_make_meta.conf.w_srv_data_root_url , "../index.html", )); } } } #+END_SRC * _epub_ [#B] :epub: |-----------------------+--------------------------+---------------------------+----------------------------------| | function | filename | module | variable | |-----------------------+--------------------------+---------------------------+----------------------------------| | identify doc filetype | mimetype | epub3_mimetypes | mimetypes | |-----------------------+--------------------------+---------------------------+----------------------------------| | identify doc root | META-INF/container.xml | epub3_container_xml | meta_inf_container_xml | |-----------------------+--------------------------+---------------------------+----------------------------------| | doc manifest | OEBPS/content.opf | epub3_oebps_content | oebps_content_opf | |-----------------------+--------------------------+---------------------------+----------------------------------| | doc navigation | OEBPS/toc_nav.xhtml | epub3_oebps_toc_nav_xhtml | oebps_toc_nav_xhtml | | | OEBPS/toc.ncx | epub2_oebps_toc_ncx | oebps_toc_ncx | |-----------------------+--------------------------+---------------------------+----------------------------------| | doc contents | OEBPS/[files ... ].xhtml | outputEPub3 | doc_epub3[seg_filename] | | | | | doc_epub3_endnotes[seg_filename] | |-----------------------+--------------------------+---------------------------+----------------------------------| ** _module template_ :module: #+HEADER: :tangle "../src/sisudoc/outputs/io_out/epub3.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> module sisudoc.outputs.io_out.epub3; @safe: template outputEPub3() { <> mixin InternalMarkup; mixin outputXHTMLs; static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> } #+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 landmarks navigation. This is what EPUB3 offers in place of the EPUB2 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( "
  • %s
  • \n", _w[1], _file, _w[2]); } if (_items.empty) { return ""; } return " \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(" %s\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, 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 #+BEGIN_SRC d /+ ↓ the last modified time EPUB3 requires, exactly one dcterms:modified on the package in UTC to the second, comes from doc_matters.modified_utc. Set in spine rather than here because an odt's office:meta needs the same instant, and a single definition cannot drift apart. +/ #+END_SRC **** time unused #+NAME: output_epub3_last_modified_time_UNUSED #+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 . Taken from the document rather than from the clock, so that an epub is a function of its source and not of the moment it was built. Read in order: SOURCE_DATE_EPOCH where the environment sets it, which is the reproducible-builds convention and lets a packager pin every artefact of one run; then the document's own date.modified; then date.published. The clock is the last resort, for a document that says nothing about when it is from. . Before this, two runs over unchanged markup produced different epub bytes, so no comparison of epub output could tell a real change from the seconds between two builds. . A document date is loose markup: "1991", "2006-03" and "2021-12-00" all occur in the sample set, and a month or day of 00 is not a date. A missing or zero part becomes 01, and a day out of range for its month falls back to the first, so what is written is always a valid timestamp. +/ string _epub_modified_utc(M)(M _meta) { import std.datetime : Clock, UTC, SysTime, Date, unixTimeToStdTime; import std.process : environment; import std.regex : ctRegex, matchFirst; string _stamp(int _y, int _mo, int _d) { return format("%04d-%02d-%02dT00:00:00Z", _y, _mo, _d); } /+ ↓ a build may pin every artefact of the run +/ string _sde = environment.get("SOURCE_DATE_EPOCH", ""); if (_sde.length > 0) { try { auto _t = SysTime(unixTimeToStdTime(_sde.to!long), UTC()); return format("%04d-%02d-%02dT%02d:%02d:%02dZ", _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); } catch (Exception) { } } /+ ↓ what the document itself says +/ static auto _rgx_date = ctRegex!(`^\s*(?P\d{4})(?:[-/](?P\d{1,2}))?(?:[-/](?P\d{1,2}))?`); foreach (_said; [_meta.date_modified, _meta.date_published]) { if (_said.length == 0) { continue; } if (auto _m = _said.matchFirst(_rgx_date)) { int _y = _m["y"].to!int; int _mo = (_m["m"].length > 0) ? _m["m"].to!int : 1; int _d = (_m["d"].length > 0) ? _m["d"].to!int : 1; if (_mo < 1 || _mo > 12) { _mo = 1; } if (_d < 1) { _d = 1; } try { cast(void) Date(_y, _mo, _d); } catch (Exception) { _d = 1; } return _stamp(_y, _mo, _d); } } /+ ↓ the document says nothing: the run's own time, as before +/ 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] - mimetype file indicating that zip file contains an EPUB #+NAME: output_epub3_fixed_mimetypes #+BEGIN_SRC d string epub3_mimetypes() { string o; o = format(q"┃application/epub+zip┃") ~ "\n"; return o; } #+END_SRC **** _identify doc root_ (META-INF/container.xml) [static] rootfile: contains document root path - identifies the root package document (so systems can find it), [unchanged from epub2] #+NAME: output_epub3_fixed_container_xml #+BEGIN_SRC d string epub3_container_xml() { string o; o = format(q"┃┃") ~ "\n"; o ~= format(q"┃ ┃") ~ "\n\n"; return o; } #+END_SRC *** constructs (in dir: OEBPS) **** TODO _doc manifest_ (OEBPS/content.opf) manifest, register content: files, images etc. - manifest, listing all resources - provides the default reading order - identifies the navigation document #+HEADER: :noweb yes #+BEGIN_SRC d #+END_SRC #+NAME: epub3_package_version #+BEGIN_SRC d #+END_SRC #+NAME: output_epub3_constructs_oebps_content #+HEADER: :noweb yes #+BEGIN_SRC d 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 = _epub_publication_uuid(doc.matters.src.doc_uid_out); string content = format(q"┃ <> urn:uuid:%s %s main %s %s %s %s %s Copyright: %s %s %s ┃", doc.matters.src.language_tag, _uuid, xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.title_main), (doc.matters.conf_make_meta.meta.title_sub.empty) ? "" : format( " %s\n" ~ " subtitle\n", 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_surname_fn.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.creator_author_surname_fn), doc.matters.src.language_tag, // 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), doc.matters.modified_utc, _epub_a11y_metadata(doc), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); content ~= parts["manifest_documents"]; foreach (image; doc.matters.srcs.image_list) { content ~= format(q"┃ ┃", _manifest_id("img", image.baseName.stripExtension), (pth_epub3.doc_oebps_image).chompPrefix("OEBPS/"), image, _image_media_type(image.extension.chompPrefix(".")), ); } content ~= " " ~ "" ~ "\n "; content ~= " " ~ "" ~ "\n "; content ~= parts["spine"]; content ~= " " ~ "" ~ "\n "; content ~= "" ~ ""; debug(epubmanifest) { foreach (section; doc.matters.has.keys_seq.seg) { // TODO foreach (obj; doc.abstraction[section]) { if (obj.metainfo.is_a == "heading") { if (obj.metainfo.heading_lev_markup == 4) { writefln( "%s~ [%s.xhtml] %s", obj.marked_up_level, obj.tags.segment_anchor_tag_epub, obj.text ); } else if (obj.metainfo.heading_lev_markup > 4) { writefln( "%s~ [%s.xhtml#%s] %s", obj.marked_up_level, obj.tags.segment_anchor_tag_epub, obj.metainfo.object_number, obj.text ); } } } } } return content; } #+END_SRC **** _doc navigation epub3_ (OEBPS/toc_nav.xhtml) epub3 navigable toc using Dom structure - toc_nav.xhtml declared as nav file in content.opf (epub3 navigation document) #+NAME: output_epub3_constructs_oebps_toc_nav_xhtml #+BEGIN_SRC d string epub3_oebps_toc_nav_xhtml(D)(D doc) { enum DomTags { none, open, close, close_and_open, open_still, } auto markup = InlineMarkup(); static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); string toc; bool _new_title_set = false; string toc_head = format(q"┃ %s

    Contents

    "; _toc_nav_tail ~= _epub_landmarks(doc); _toc_nav_tail ~=" \n"; } } } } } } toc ~= _toc_nav_tail; return toc; } #+END_SRC ** the document contents :seg: *** ↻ loop & switch (sections & objects) format epub3 xhtml output #+NAME: output_epub3_xhtml_seg_output #+BEGIN_SRC d @system void outputEPub3(D)(D doc) { mixin spineRgxOut; mixin spineRgxXHTML; auto xhtml_format = outputXHTMLs(); static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); string segment_filename; /+ ↓ a segment file gets one document head, not one per heading. +/ bool[string] _seg_head_written; string _segHead(M)(M _m, string _seg) { if (_seg in _seg_head_written) { return ""; } _seg_head_written[_seg] = true; return xhtml_format.epub3_seg_head(_m); } 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 +/ string mimetypes; string meta_inf_container_xml; string oebps_toc_nav_xhtml; /+ variable output +/ string oebps_content_opf; string[][string] doc_epub3; string[][string] doc_epub3_endnotes; string[] doc_parts; } auto epubWrite = writeOut(); #+END_SRC **** ↻ the loop (sections & objects) format output #+NAME: output_epub3_xhtml_seg_output_loop #+BEGIN_SRC d foreach (section; doc.matters.has.keys_seq.seg) { foreach (obj; doc.abstraction[section]) { string _txt = xhtml_format.special_characters_breaks_indents_bullets(obj); #+END_SRC ***** all headings #+NAME: output_epub3_xhtml_seg_object_is_heading #+BEGIN_SRC d if (obj.metainfo.is_a == "heading") { assert(section == "head" || "toc" || "body" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); switch (obj.metainfo.heading_lev_markup) { case 0: .. case 3: /+ fill buffer, and replace with new levels from 1 to 3 +/ switch (obj.metainfo.heading_lev_markup) { case 0: top_level_headings[0] = ""; top_level_headings[1] = ""; top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 1: top_level_headings[1] = ""; top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 2: top_level_headings[2] = ""; top_level_headings[3] = ""; goto default; case 3: top_level_headings[3] = ""; goto default; default: epubWrite.doc_parts ~= obj.tags.segment_anchor_tag_epub; epubWrite.doc_epub3[obj.tags.segment_anchor_tag_epub] ~= _segHead(doc.matters, obj.tags.segment_anchor_tag_epub); Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[obj.tags.segment_anchor_tag_epub] ~= t[0]; epubWrite.doc_epub3_endnotes[obj.tags.segment_anchor_tag_epub] ~= t[1]; break; } break; case 4: segment_filename = obj.tags.segment_anchor_tag_epub; epubWrite.doc_epub3[segment_filename] ~= _segHead(doc.matters, segment_filename); Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case 5: .. case 7: Tuple!(string, string[]) t = xhtml_format.heading_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case 8: .. case 9: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a, ": ", obj.metainfo.heading_lev_markup); writeln(__FILE__, ":", __LINE__, ": ", obj.text); } } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a, ": ", obj.metainfo.heading_lev_markup); } } break; } #+END_SRC ***** non-heading #+NAME: output_epub3_xhtml_seg_object_is_not_heading #+BEGIN_SRC d } else { assert(section == "head" || "toc" || "body" || "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); Tuple!(string, string[]) t; switch (obj.metainfo.is_of_part) { #+END_SRC ****** frontmatter #+NAME: output_epub3_xhtml_seg_object_is_frontmatter #+BEGIN_SRC d case "frontmatter": assert(section == "head" || "toc"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "toc": t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ****** body #+NAME: output_epub3_xhtml_seg_object_is_body #+BEGIN_SRC d case "body": assert(section == "body"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "para": t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; case "block": switch (obj.metainfo.is_a) { case "quote": t = xhtml_format.quote_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0].to!string; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "group": t = xhtml_format.group_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0].to!string; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "block": t = xhtml_format.block_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0].to!string; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "poem": break; case "verse": t = xhtml_format.verse_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0].to!string; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "code": epubWrite.doc_epub3[segment_filename] ~= xhtml_format.code(_txt, obj, doc.matters); break; case "table": epubWrite.doc_epub3[segment_filename] ~= xhtml_format.table(_txt, obj, doc.matters); epubWrite.doc_epub3_endnotes[segment_filename] ~= ""; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ****** backmatter #+NAME: output_epub3_xhtml_seg_object_is_backmatter #+BEGIN_SRC d case "backmatter": assert(section == "endnotes" || "glossary" || "bibliography" || "bookindex" || "blurb" || "tail"); switch (obj.metainfo.is_of_type) { case "para": switch (obj.metainfo.is_a) { case "endnote": assert(section == "endnotes"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; break; case "glossary": assert(section == "glossary"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "bibliography": assert(section == "bibliography"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "bookindex": assert(section == "bookindex"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "blurb": assert(section == "blurb"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; case "tail": assert(section == "tail"); t = xhtml_format.para_seg(_txt, obj, doc.matters, suffix, "epub"); epubWrite.doc_epub3[segment_filename] ~= t[0]; epubWrite.doc_epub3_endnotes[segment_filename] ~= t[1]; break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_a); } } break; } break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_type); } } break; } break; #+END_SRC ****** comment #+NAME: output_epub3_xhtml_seg_object_is_comment_and_close #+BEGIN_SRC d case "comment": break; default: { /+ debug +/ if (doc.matters.opt.action.debug_do_epub) { writeln(__FILE__, ":", __LINE__, ": ", obj.metainfo.is_of_part); } } break; } } #+END_SRC ****** heading #+NAME: output_epub3_xhtml_seg_object_is_heading_set #+BEGIN_SRC d /+ ↓ 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" /+ ↓ "tail" is the end of document marker, and no file is written for it (the writing loop skips the section), so it is not in the manifest either. +/ && section != "tail" && 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"┃ ┃", _manifest_id("seg", obj.tags.segment_anchor_tag_epub), obj.tags.segment_anchor_tag_epub, ); oepbs_content_parts["spine"] ~= format(q"┃ ┃", _manifest_id("seg", obj.tags.segment_anchor_tag_epub), ); #+END_SRC ***** closings & post loop #+NAME: output_epub3_xhtml_seg_close_ifs_and_write #+BEGIN_SRC d } } } /+ epub specific documents +/ epubWrite.mimetypes = epub3_mimetypes; epubWrite.meta_inf_container_xml = epub3_container_xml; epubWrite.oebps_toc_nav_xhtml = doc.epub3_oebps_toc_nav_xhtml; epubWrite.oebps_content_opf = doc.epub3_oebps_content(oepbs_content_parts); epubWrite.epub3_write_output_files(doc.matters); } #+END_SRC ** write output files :write:zip: zip -0 file.epub mimetype && zip -r -9 file.epub META-INF OEBPS (mimetypes should be included but should not be zipped) #+NAME: output_epub3_xhtml_seg_write_output_files #+BEGIN_SRC d @system void epub3_write_output_files(W,M)( W epub_write, M doc_matters, ) { debug(asserts) { static assert(is(typeof(epub_write.doc_epub3) == string[][string])); static assert(is(typeof(epub_write.mimetypes) == string)); static assert(is(typeof(epub_write.meta_inf_container_xml) == string)); static assert(is(typeof(epub_write.oebps_toc_nav_xhtml) == string)); static assert(is(typeof(epub_write.oebps_content_opf) == string)); } static auto rgx = RgxO(); static auto rgx_xhtml = RgxXHTML(); auto pth_epub3 = spinePathsEPUB!()(doc_matters.output_path, doc_matters.src.language); auto xhtml_format = outputXHTMLs(); /+ zip file +/ auto fn_epub = pth_epub3.epub_file(doc_matters.src.filename); auto zip = new ZipArchive(); // ZipArchive zip = new ZipArchive(); /+ zip archive member files. ZipArchive.build() emits members sorted by ArchiveMember.index, and addMember does not set one. An epub is not free to choose: OCF requires the mimetype entry to be the first in the archive, a reader being entitled to look only there to tell what the file is. The index is therefore set deliberately, 0 for the mimetype and 1 upward for the rest as they are added. . Compression is already done for every member (ArchiveMember defaults to it), which is the other half of what OCF asks of the mimetype. +/ uint _zip_seq = 1; void EPUBzip()(string contents, string fn, bool _is_mimetype = false) { auto zip_arc_member_file = new ArchiveMember(); zip_arc_member_file.name = fn; zip_arc_member_file.index = _is_mimetype ? 0 : _zip_seq++; if (_is_mimetype) { zip_arc_member_file.compressionMethod = CompressionMethod.none; } auto zip_data = new OutBuffer(); (doc_matters.opt.action.debug_do_epub) ? zip_data.write(contents.dup) : zip_data.write(contents.dup .replaceAll(rgx.spaces_line_start, "") .replaceAll(rgx.newline, " ") .strip ); zip_arc_member_file.expandedData = zip_data.toBytes(); zip.addMember(zip_arc_member_file); createZipFile!()(fn_epub, zip.build()); } try { if (!exists(pth_epub3.base)) { pth_epub3.base.mkdirRecurse; } if (!exists(pth_epub3.base ~ "/index.html")) { import sisudoc.outputs.io_out.html_snippet; mixin htmlSnippet; auto f = File(pth_epub3.base ~"/index.html", "w"); f.writeln(format_html_blank_page_guide_home( "../../css/html_scroll.css", (doc_matters.opt.action.webserver_url_doc_root.length > 0) ? doc_matters.opt.action.webserver_url_doc_root : doc_matters.conf_make_meta.conf.w_srv_data_root_url, "../../index.html", )); } { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { if (!exists(pth_epub3.dbg_doc_meta_inf(doc_matters.src.filename))) { pth_epub3.dbg_doc_meta_inf(doc_matters.src.filename).mkdirRecurse; } if (!exists(pth_epub3.dbg_doc_oebps_css(doc_matters.src.filename))) { pth_epub3.dbg_doc_oebps_css(doc_matters.src.filename).mkdirRecurse; } if (!exists(pth_epub3.dbg_doc_oebps_image(doc_matters.src.filename))) { pth_epub3.dbg_doc_oebps_image(doc_matters.src.filename).mkdirRecurse; } } } { /+ OEBPS/[segments].xhtml (the document contents) +/ foreach (seg_filename; doc_matters.has.segnames_lv_0_to_4) { string fn = pth_epub3.fn_oebps_content_xhtml(seg_filename); auto zip_arc_member_file = new ArchiveMember(); zip_arc_member_file.name = fn; auto zip_data = new OutBuffer(); { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { string fn_dbg = pth_epub3.dbg_fn_oebps_content_xhtml(doc_matters.src.filename, seg_filename); auto f = File(fn_dbg, "w"); foreach (docseg; epub_write.doc_epub3[seg_filename]) { f.writeln(docseg); } foreach (docseg; epub_write.doc_epub3_endnotes[seg_filename]) { f.writeln(docseg); } f.writeln(xhtml_format.tail(doc_matters)); } } foreach (docseg; epub_write.doc_epub3[seg_filename]) { zip_data.write(docseg.dup); } foreach (docseg; epub_write.doc_epub3_endnotes[seg_filename]) { zip_data.write(docseg.dup); } zip_data.write(xhtml_format.tail(doc_matters).dup); zip_arc_member_file.expandedData = zip_data.toBytes(); zip.addMember(zip_arc_member_file); /+ create the zip file +/ createZipFile!()(fn_epub, zip.build()); } } string fn; string fn_dbg; File f; { /+ mimetypes (identify zip file type) +/ { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { fn_dbg = pth_epub3.dbg_fn_mimetypes(doc_matters.src.filename); File(fn_dbg, "w").writeln(epub_write.mimetypes); } } fn = pth_epub3.fn_mimetypes; EPUBzip(epub_write.mimetypes, fn, true); /+ first in the archive +/ } { /+ META-INF/container.xml (identify doc root) +/ { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { fn_dbg = pth_epub3.dbg_fn_dmi_container_xml(doc_matters.src.filename); File(fn_dbg, "w").writeln(epub_write.meta_inf_container_xml); } } fn = pth_epub3.fn_dmi_container_xml; EPUBzip(epub_write.meta_inf_container_xml, fn); } { /+ OEBPS/toc_nav.xhtml (navigation toc epub3) +/ { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { fn_dbg = pth_epub3.dbg_fn_oebps_toc_nav_xhtml(doc_matters.src.filename); File(fn_dbg, "w").writeln(epub_write.oebps_toc_nav_xhtml); } } fn = pth_epub3.fn_oebps_toc_nav_xhtml; EPUBzip(epub_write.oebps_toc_nav_xhtml, fn); } { /+ OEBPS/content.opf (doc manifest) +/ { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { fn_dbg = pth_epub3.dbg_fn_oebps_content_opf(doc_matters.src.filename); File(fn_dbg, "w").writeln(epub_write.oebps_content_opf); } } fn = pth_epub3.fn_oebps_content_opf; EPUBzip(epub_write.oebps_content_opf, fn); } { /+ OEBPS/_dr/image (images) +/ foreach (image; doc_matters.srcs.image_list) { { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { if (doc_matters.opt.action.vox_gt_3) { writeln( doc_matters.src.image_dir_path, "/", image, " -> ", pth_epub3.dbg_doc_oebps_image(doc_matters.src.filename), "/", image ); } if (exists(doc_matters.src.image_dir_path ~ "/" ~ image)) { (doc_matters.src.image_dir_path ~ "/" ~ image) .copy((pth_epub3.dbg_doc_oebps_image(doc_matters.src.filename)) ~ "/" ~ image); } } } auto fn_src = doc_matters.src.image_dir_path ~ "/" ~ image; auto fn_out = pth_epub3.doc_oebps_image ~ "/" ~ image; if (exists(fn_src)) { { auto zip_arc_member_file = new ArchiveMember(); zip_arc_member_file.name = fn_out; auto zip_data = new OutBuffer(); zip_data.write(cast(char[]) ((fn_src).read)); zip_arc_member_file.expandedData = zip_data.toBytes(); zip.addMember(zip_arc_member_file); createZipFile!()(fn_epub, zip.build()); } } } } { /+ OEBPS/epub.css +/ auto css = spineCss(doc_matters); { /+ debug +/ if (doc_matters.opt.action.debug_do_epub) { fn_dbg = pth_epub3.dbg_fn_oebps_css(doc_matters.src.filename); File(fn_dbg, "w").writeln(css.epub); } } fn = pth_epub3.fn_oebps_css; auto zip_arc_member_file = new ArchiveMember(); zip_arc_member_file.name = fn; auto zip_data = new OutBuffer(); zip_data.write(css.epub.dup); zip_arc_member_file.expandedData = zip_data.toBytes(); zip.addMember(zip_arc_member_file); createZipFile!()(fn_epub, zip.build()); } } catch (ErrnoException ex) { // Handle error } if (doc_matters.opt.action.vox_gt_1) { writeln(" ", fn_epub); } #+END_SRC ** zip debug, read zip archive :zip: #+NAME: output_epub3_xhtml_seg_debug_extract_zip_dir #+BEGIN_SRC d debug(epub_archive) { if (exists(fn_epub)) { try { auto zipped = new ZipArchive((fn_epub).read); foreach (filename, member; zipped.directory) { auto data = zipped.expand(member); writeln(filename, " length ", data.length); } } catch (ZipException ex) { // Handle errors } } } } #+END_SRC * org includes ** project version #+NAME: spine_version #+HEADER: :noweb yes #+BEGIN_SRC emacs-lisp <<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:spine_project_version()>> #+END_SRC ** year #+NAME: year #+HEADER: :noweb yes #+BEGIN_SRC emacs-lisp <<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:year()>> #+END_SRC ** document header including copyright & license #+NAME: doc_header_including_copyright_and_license #+HEADER: :noweb yes #+BEGIN_SRC emacs-lisp <<./sisudoc_spine_version_info_and_doc_header_including_copyright_and_license.org:spine_doc_header_including_copyright_and_license()>> #+END_SRC * __END__