-*- mode: org -*- #+TITLE: sisudoc spine (doc_reform) object-centric document abstraction #+DESCRIPTION: documents - structuring, publishing in multiple formats & search #+SUMMARY: process markup document, create document abstraction #+FILETAGS: :spine:abstraction: #+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/]] * (Object-Centric) Document Abstraction - Process markup document, create document abstraction ** library templates *** ocda package.d #+HEADER: :tangle "../src/sisudoc/ocda/package.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> /++ sisudoc.ocda - canonical entry point of the document-abstraction library. Pipeline position: markup -> abstraction -> output. This package is the abstraction stage; the output stage lives in sisudoc.outputs.io_out and consumes the values produced here. This file re-exports sisudoc.ocda.abstraction (the documented public API surface) so that consumers can write import sisudoc.ocda; and reach the entry points (spineAbstraction!(), docAbstraction!(), spineAbstractionTxt) without depending on spine's directory layout. No logic lives here. +/ module sisudoc.ocda; @safe: public import sisudoc.ocda.abstraction; #+END_SRC *** ocda/abstraction package.d #+HEADER: :tangle "../src/sisudoc/ocda/abstraction/package.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> /++ sisudoc.ocda.abstraction - public surface of the document-abstraction library. Pipeline position: markup -> abstraction -> output. This package is the abstraction stage. The output stage lives in sisudoc.outputs.io_out and consumes the values produced here. Entry points: - spineAbstraction!() (from sisudoc.ocda.meta.metadoc) - A-layer: builds the in-memory document object model from a manifest (pod path, .sst path). Reads the document body, parses YAML headers, returns a struct with .abstraction (the object model) and .matters (the conf/meta/src wrapper). - docAbstraction!() (from sisudoc.ocda.meta.metadoc_from_src) - B-layer: builds the abstraction from already-loaded body text plus a pre-built ConfComposite. Pure, no file I/O. The A-layer is a thin wrapper over the B-layer; consumers that want a minimal-dependency entry should use docAbstraction!() directly. Serialisation: - sisudoc.ocda.abstraction.ssp - PEG-parsable text serialisation of the abstraction (the .ssp format). See specs/doc-abstraction- format/ for the format reference. This file is a re-export-only surface. No logic lives here; it exists so external consumers can `import sisudoc.ocda.abstraction;` and reach the entry points without depending on spine's directory layout. +/ module sisudoc.ocda.abstraction; @safe: public import sisudoc.ocda.meta.metadoc; // spineAbstraction (A-layer) public import sisudoc.ocda.meta.metadoc_from_src; // docAbstraction (B-layer) public import sisudoc.ocda.abstraction.ssp; // spineAbstractionTxt (.ssp) public import sisudoc.ocda.abstraction.ssp_in; // spineAbstractionRead (.ssp back in) public import sisudoc.ocda.abstraction.db_in; // spineAbstractionDbRead (.db back in) public import sisudoc.ocda.abstraction.load; // spineAbstractionLoad (one way in) public import sisudoc.ocda.abstraction.doc_has; // spineDocHasFromAbstraction (ST_DocHas from a loaded abstraction) public import sisudoc.ocda.abstraction.doc_from_artefact; // spineDocFromArtefact (a doc, from a .ssp or a .ocda.db) #+END_SRC ** _module template_ :module:metadoc_from_src: #+HEADER: :tangle "../src/sisudoc/ocda/meta/metadoc_from_src.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> // document abstraction: // abstraction of sisu markup for downstream processing // metadoc_from_src.d module sisudoc.ocda.meta.metadoc_from_src; @safe: template docAbstraction() { <> @system auto docAbstraction(CMM,Opt,Mf) ( char[][] markup_sourcefile_content, CMM conf_make_meta, Opt opt_action, Mf manifested, bool _new_doc ) { <> } // ← closed: abstract doc source } #+END_SRC ** docInitialize #+NAME: docInitialize #+HEADER: :noweb yes #+BEGIN_SRC d // ↓ abstraction imports import std.algorithm; import std.container; import std.digest.sha; import std.file; import std.json; import std.path; import sisudoc.ocda.meta; import sisudoc.ocda.meta.defaults; import sisudoc.ocda.meta.rgx; import sisudoc.ocda.meta.metadoc_object_setter; import sisudoc.ocda.meta.rgx; public import sisudoc.ocda.meta.metadoc_from_src_functions; mixin docAbstractionFunctions; #+END_SRC ** docAbstraction *** _toc_ abstraction summary #+NAME: docAbstraction #+HEADER: :noweb yes #+BEGIN_SRC d <> <> <> <> <> #+END_SRC *** abstraction init #+NAME: docAbstractionInit #+HEADER: :noweb yes #+BEGIN_SRC d static auto rgx = RgxI(); // ↓ abstraction init scope(success) { } scope(failure) { } scope(exit) { destroy(the_document_toc_section); destroy(the_document_head_section); destroy(the_document_body_section); destroy(the_document_bibliography_section); destroy(the_document_glossary_section); destroy(the_document_blurb_section); destroy(the_document_xml_dom_tail_section); destroy(an_object); destroy(processing); destroy(biblio_arr_json); previous_length = 0; reset_note_numbers = true; lev_anchor_tag = ""; anchor_tag = ""; _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); } /+ ↓ seeded here, on the way in, which is the seeding that matters. . The two assignments in scope(exit) and at the tail of this function run after a document has been abstracted, so they seed the *next* one. The first document of a run had nothing before it and began with the set as declared, empty: it alone was not held to the reserved names, and a heading of its own claiming "endnotes" kept the name of the generated section. Which document that was depended on what else was in the run, so a document's abstraction depended on its company, and a single document re-parsed to check an artefact made in a collection run disagreed with it. +/ _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); mixin spineNode; int[string] node_para_int_ = node_metadata_para_int; string[string] node_para_str_ = node_metadata_para_str; ObjGenericComposite comp_obj_; line_occur = [ "heading" : 0, "para" : 0, "glossary" : 0, "blurb" : 0, ]; uint[string] dochas = [ "inline_links" : 0, "inline_notes" : 0, "inline_notes_star" : 0, "codeblock" : 0, "table" : 0, "block" : 0, "group" : 0, "poem" : 0, "quote" : 0, "images" : 0, ]; uint[string] pith = [ "ocn" : 1, "section" : 0, "txt_is" : 0, "block_is" : 0, "block_state" : 0, "block_delim" : 0, "make_headings" : 0, "dummy_heading_status" : 0, "dummy_heading_multiple_objects" : 0, "no_ocn_multiple_objects" : 0, "verse_new" : 0, ]; string[string] object_number_poem = [ "start" : "", "end" : "" ]; string[] lv_ancestors_txt = [ "", "", "", "", "", "", "", "", ]; /+ ↓ dom structure state, per document (not shared, parallel safe) +/ int[] dom_structure_markedup_tags_status = [ 0, 0, 0, 0, 0, 0, 0, 0,]; int[] dom_structure_markedup_tags_status_buffer = [ 0, 0, 0, 0, 0, 0, 0, 0,]; int[] dom_structure_collapsed_tags_status = [ 0, 0, 0, 0, 0, 0, 0, 0,]; int[] dom_structure_collapsed_tags_status_buffer = [ 0, 0, 0, 0, 0, 0, 0, 0,]; int[string] lv = [ "lv" : eN.bi.off, "h0" : eN.bi.off, "h1" : eN.bi.off, "h2" : eN.bi.off, "h3" : eN.bi.off, "h4" : eN.bi.off, "h5" : eN.bi.off, "h6" : eN.bi.off, "h7" : eN.bi.off, "lev_int_collapsed" : 0, ]; int[string] collapsed_lev = [ "h0" : eN.bi.off, "h1" : eN.bi.off, "h2" : eN.bi.off, "h3" : eN.bi.off, "h4" : eN.bi.off, "h5" : eN.bi.off, "h6" : eN.bi.off, "h7" : eN.bi.off ]; string[string] heading_match_str = [ "h_A": "^(none)", "h_B": "^(none)", "h_C": "^(none)", "h_D": "^(none)", "h_1": "^(none)", "h_2": "^(none)", "h_3": "^(none)", "h_4": "^(none)" ]; Regex!char[string] heading_match_rgx = [ "h_A": regex(r"^(none)"), "h_B": regex(r"^(none)"), "h_C": regex(r"^(none)"), "h_D": regex(r"^(none)"), "h_1": regex(r"^(none)"), "h_2": regex(r"^(none)"), "h_3": regex(r"^(none)"), "h_4": regex(r"^(none)") ]; string _anchor_tag; string toc_txt_; an_object["glossary_nugget"] = ""; an_object["blurb_nugget"] = ""; comp_obj_ = set_object_heading("lev4", "frontmatter", "toc", "Table of Contents"); comp_obj_.metainfo.identifier = ""; comp_obj_.metainfo.dummy_heading = false; comp_obj_.metainfo.object_number_off = true; comp_obj_.metainfo.object_number_type = 0; comp_obj_.tags.segment_anchor_tag_epub = "toc"; comp_obj_.tags.anchor_tag_html = comp_obj_.tags.segment_anchor_tag_epub; comp_obj_.tags.in_segment_html = comp_obj_.tags.anchor_tag_html; comp_obj_.ptr.html_segnames = html_segnames_ptr; comp_obj_.tags.anchor_tags = ["toc"]; tag_assoc[comp_obj_.tags.anchor_tag_html]["seg_lv4"] = comp_obj_.tags.in_segment_html; tag_assoc[comp_obj_.tags.segment_anchor_tag_epub]["seg_lv1to4"] = comp_obj_.tags.segment_anchor_tag_epub; auto toc_head = comp_obj_; html_segnames_ptr_cntr++; the_document_toc_section = [toc_head]; static auto mkup = InlineMarkup(); static auto munge = ObjInlineMarkupMunge(); auto note_section = NotesSection(); auto bookindex_extract_hash = BookIndexNuggetHash(); string[][string] lev4_subtoc; string[][string] segnames = ["html": ["toc"], "epub": ["toc"]]; int cnt1 = 1; int cnt2 = 1; int cnt3 = 1; // abstraction init ↑ #+END_SRC *** debug abstraction init substitutions #+NAME: docAbstractionInitSubstitutionsDebug #+HEADER: :noweb yes #+BEGIN_SRC d debug (substitutions) { writeln(__LINE__, ":", __FILE__, ": DEBUG substitutions:"); if (!(conf_make_meta.make.headings.empty)) { writeln(conf_make_meta.make.headings); } if (conf_make_meta.make.substitute) { foreach(substitution_pair; conf_make_meta.make.substitute) { writeln("regex to match: ", substitution_pair[Substitute.match]); writeln("substitution to make: ", substitution_pair[Substitute.markup]); } } if (conf_make_meta.make.bold) { writeln("regex to match: ", conf_make_meta.make.bold[Substitute.match]); writeln("substitution to make: ", conf_make_meta.make.bold[Substitute.markup]); } if (conf_make_meta.make.emphasis) { writeln("regex to match: ", conf_make_meta.make.emphasis[Substitute.match]); writeln("substitution to make: ", conf_make_meta.make.emphasis[Substitute.markup]); } if (conf_make_meta.make.italics) { writeln("regex to match: ", conf_make_meta.make.italics[Substitute.match]); writeln("substitution to make: ", conf_make_meta.make.italics[Substitute.markup]); } } #+END_SRC *** _loop_ source by line (large block) #+NAME: docAbstractionMainLoopSrcByLine #+HEADER: :noweb yes #+BEGIN_SRC d _loopMarkupSrcByLineStruct loopMarkupSrcByLine( char[][] markup_sourcefile_content, string[string] an_object, uint[string] pith, ) { _loopMarkupSrcByLineStruct ret; srcDocLoopLineByLine_: foreach (line; markup_sourcefile_content) { // ↓ markup document/text line by line // "line" variable can be empty but should never be null // scope scope(exit) { } scope(failure) { stderr.writefln( "\n%s\n%s\n\n%s:%s\nFAILED while processing the file: ❮❮ %s ❯❯ on line with text:\n%s\n", __MODULE__, __FUNCTION__, __FILE__, __LINE__, manifested.src.filename, line, ); } debug(source) { writeln(line); } debug(srclines) { if (!line.empty) { writefln("* %s", line); } } if (!line.empty) { pith = line._check_ocn_status_(pith); } if ( pith["block_is"] == eN.blk_is.code && pith["block_state"] == eN.blk_state.on ) { // block object: code { ST_txt_by_line_block_generic _get = line.txt_by_line_block_code(an_object, pith); { an_object = _get.this_object; pith = _get.pith; } } continue; } else if (!matchFirst(line, rgx.skip_from_regular_parse)) { // object other than "code block" object // (includes regular text paragraph, headings & blocks other than code) // heading, glossary, blurb, poem, group, block, quote, table line = line.inline_markup_faces; // by text line (rather than by text object), linebreaks in para problematic if (line.matchFirst(rgx.heading_biblio) || (pith["section"] == eN.sect.bibliography && ((!(line.matchFirst(rgx.heading_glossary))) && (!(line.matchFirst(rgx.heading_blurb))) && (!(line.matchFirst(rgx.heading))) && (!(line.matchFirst(rgx.comment))))) ) { pith["section"] = eN.sect.bibliography; if (opt_action.backmatter && opt_action.section_biblio) { { ST_txt_by_line_block_biblio _get = line.txt_by_line_block_biblio(pith, bib_entry, biblio_entry_str_json, biblio_arr_json); { pith = _get.pith; bib_entry = _get.bib_entry; biblio_entry_str_json = _get.biblio_entry_str_json; biblio_arr_json = _get.biblio_arr_json; } } debug(bibliobuild) { writeln("- ", biblio_entry_str_json); writeln("-> ", biblio_arr_json.length); } } continue; } else if (line.matchFirst(rgx.heading_glossary) || (pith["section"] == eN.sect.glossary && ((!(line.matchFirst(rgx.heading_biblio))) && (!(line.matchFirst(rgx.heading_blurb))) && (!(line.matchFirst(rgx.heading))) && (!(line.matchFirst(rgx.comment))))) ) { // within section (block object): glossary debug(glossary) { writeln(__LINE__); writeln(line); } pith["section"] = eN.sect.glossary; if (opt_action.backmatter && opt_action.section_glossary) { ST_the_section add_to_glossary_sect = line.build_the_glossary_section(pith, tag_assoc); // double check, should not be necessary to pass pith the_document_glossary_section ~= add_to_glossary_sect.comp_section_obj[0]; if (add_to_glossary_sect.comp_section_obj.length > 1) { // heading the_document_glossary_section ~= add_to_glossary_sect.comp_section_obj[1]; } pith = add_to_glossary_sect.pith; tag_assoc = add_to_glossary_sect.tag_assoc; } continue; } else if (line.matchFirst(rgx.heading_blurb) || (pith["section"] == eN.sect.blurb && ((!(line.matchFirst(rgx.heading_glossary))) && (!(line.matchFirst(rgx.heading_biblio))) && (!(line.matchFirst(rgx.heading))) && (!(line.matchFirst(rgx.comment))))) ) { pith["section"] = eN.sect.blurb; debug(blurb) { writeln(__LINE__); writeln(line); } if ((opt_action.backmatter && opt_action.section_blurb) && !(line.empty)) { ST_the_section add_to_blurb_sect = line.build_the_blurb_section(pith, tag_assoc, opt_action); // double check, should not be necessary to pass pith the_document_blurb_section ~= add_to_blurb_sect.comp_section_obj[0]; if (add_to_blurb_sect.comp_section_obj.length > 1) { // heading the_document_blurb_section ~= add_to_blurb_sect.comp_section_obj[1]; } pith = add_to_blurb_sect.pith; tag_assoc = add_to_blurb_sect.tag_assoc; } continue; } else if (pith["block_state"] == eN.blk_state.on) { if (pith["block_is"] == eN.blk_is.quote) { line = line ._doc_header_and_make_substitutions_(conf_make_meta) ._doc_header_and_make_substitutions_fontface_(conf_make_meta); { ST_txt_by_line_block_generic _get = line.txt_by_line_block_quote(an_object, pith); { an_object = _get.this_object; pith = _get.pith; } } continue; } else if (pith["block_is"] == eN.blk_is.group) { line = line ._doc_header_and_make_substitutions_(conf_make_meta) ._doc_header_and_make_substitutions_fontface_(conf_make_meta) .replaceAll(rgx.para_delimiter, mkup.br_line_spaced ~ "$1"); { ST_txt_by_line_block_generic _get = line.txt_by_line_block_group(an_object, pith); { an_object = _get.this_object; pith = _get.pith; } } continue; } else if (pith["block_is"] == eN.blk_is.block) { line = line ._doc_header_and_make_substitutions_(conf_make_meta) ._doc_header_and_make_substitutions_fontface_(conf_make_meta); if (auto m = line.match(rgx.spaces_keep)) { line = line .replaceAll(rgx.spaces_keep, (m.captures[1]).translate([ ' ' : mkup.nbsp ])); } { ST_txt_by_line_block_generic _get = line.txt_by_line_block_block(an_object, pith); { an_object = _get.this_object; pith = _get.pith; } } continue; } else if (pith["block_is"] == eN.blk_is.poem) { { ST_txt_by_line_block_poem _get = line.txt_by_line_block_poem(an_object, pith, cntr, object_number_poem, conf_make_meta, tag_in_seg); { an_object = _get.this_object; pith = _get.pith; cntr = _get.cntr; } } continue; } else if (pith["block_is"] == eN.blk_is.table) { { auto _get = line.txt_by_line_block_table(an_object, pith, conf_make_meta); { an_object = _get.this_object; pith = _get.pith; conf_make_meta = _get.conf_make_meta; } } continue; } } else { // not within a block group assert( (pith["block_state"] == eN.blk_state.off) || (pith["block_state"] == eN.blk_state.closing), "block status: none or closed" ); if (line.matchFirst(rgx.block_open)) { if (line.matchFirst(rgx.block_poem_open)) { // poem to verse exceptions! object_reset(an_object); processing.remove("verse"); object_number_poem["start"] = obj_cite_digits.object_number.to!string; } { ST_txt_by_line_block_start _get = line.txt_by_line_block_start(pith, dochas, object_number_poem); { pith = _get.pith; dochas = _get.dochas; object_number_poem = _get.object_number_poem; } } continue; } else if (!line.empty) { // line not empty - non blocks (headings, paragraphs) & closed blocks assert(!line.empty, "line tested, line not empty surely:\n \"" ~ line ~ "\""); assert( (pith["block_state"] == eN.blk_state.off) || (pith["block_state"] == eN.blk_state.closing), "code block status: none or closed" ); if (pith["block_state"] == eN.blk_state.closing) { debug(check) { writeln(__LINE__); writeln(line); } assert( line.matchFirst(rgx.book_index_item) || line.matchFirst(rgx.book_index_item_open) || pith["section"] == eN.sect.book_index, "\nblocks closed, unless followed by book index, non-matching line:\n \"" ~ line ~ "\"" ); } if (line.matchFirst(rgx.book_index_item) || line.matchFirst(rgx.book_index_item_open) || pith["section"] == eN.sect.book_index) { { // book_index ST_flow_book_index _get = line.flow_book_index_(an_object, book_idx_tmp, pith, opt_action); { an_object = _get.this_object; pith = _get.pith; book_idx_tmp = _get.book_idx_tmp; } } } else { // not book_index an_object_key = "body_nugget"; if (auto m = line.matchFirst(rgx.comment)) { // matched comment debug(comment) { writeln(line); } an_object[an_object_key] ~= line ~= "\n"; comp_obj_comment = comp_obj_comment.init; comp_obj_comment.metainfo.is_of_part = "comment"; // breaks flow comp_obj_comment.metainfo.is_of_section = "comment"; // breaks flow comp_obj_comment.metainfo.is_of_type = "comment"; comp_obj_comment.metainfo.is_a = "comment"; comp_obj_comment.text = an_object[an_object_key].strip; the_document_body_section ~= comp_obj_comment; { ST_txt_by_line_common_reset _get = txt_by_line_common_reset_(line_occur, an_object, pith); { line_occur = _get.line_occur; an_object = _get.this_object; pith = _get.pith; } } processing.remove("verse"); ++cntr; } else if ((line_occur["para"] == eN.bi.off && line_occur["heading"] == eN.bi.off) && pith["txt_is"] == eN.txt_is.off ) { // heading or para but neither flag nor line exists if ((conf_make_meta.make.headings.length > 2) && (pith["make_headings"] == eN.bi.off)) { // heading found { ST_flow_heading_found _get = line.flow_heading_found_(heading_match_str, conf_make_meta.make.headings, heading_match_rgx, pith); { heading_match_str = _get.heading_match_str; heading_match_rgx = _get.heading_match_rgx; pith = _get.pith; } } } if (pith["make_headings"] == eN.bi.on && (line_occur["para"] == eN.bi.off && line_occur["heading"] == eN.bi.off) && pith["txt_is"] == eN.txt_is.off ) { // heading make set { ST_flow_heading_make_set _get = line.flow_heading_make_set_(line_occur, heading_match_rgx, pith); { line = _get.line; an_object = _get.this_object; pith = _get.pith; } } } // TODO node info: all headings identified at this point, // - extract node info here?? // - how long can it wait? // - should be incorporated in composite objects // - should happen before endnote links set (they need to be moved down?) if (line.matchFirst(rgx.headings)) { // heading match line = line._doc_header_and_make_substitutions_(conf_make_meta); { auto _get = line.flow_heading_matched_( an_object, line_occur, an_object_key, lv, collapsed_lev, pith, conf_make_meta, ); { an_object = _get.this_object; pith = _get.pith; } } } else if (line_occur["para"] == eN.bi.off) { // para match an_object_key = "body_nugget"; line = line ._doc_header_and_make_substitutions_(conf_make_meta) ._doc_header_and_make_substitutions_fontface_(conf_make_meta); { ST_flow_para_match _get = line.flow_para_match_(an_object, an_object_key, indent, bullet, pith, line_occur); { an_object = _get.this_object; an_object_key = _get.this_object_key; pith = _get.pith; indent = _get.indent; bullet = _get.bullet; line_occur = _get.line_occur; } } } } else if (line_occur["heading"] > eN.bi.off) { // heading debug(heading) { writeln(line); } an_object[an_object_key] ~= line ~= "\n"; ++line_occur["heading"]; } else if (line_occur["para"] > eN.bi.off) { // paragraph debug(para) { writeln(an_object_key, "-> ", line); } line = line ._doc_header_and_make_substitutions_(conf_make_meta) ._doc_header_and_make_substitutions_fontface_(conf_make_meta); an_object[an_object_key] ~= " " ~ line; ++line_occur["para"]; } } } else if (pith["block_state"] == eN.blk_state.closing) { // line empty, with blocks flag { ST_flow_block_flag_line_empty _get = line.flow_block_flag_line_empty_( an_object, bookindex_extract_hash, the_document_body_section, bookindex_unordered_hashes, obj_cite_digits, comp_obj_, cntr, pith, object_number_poem, conf_make_meta, tag_in_seg, ); { an_object = _get.this_object; the_document_body_section = _get.the_document_body_section; bookindex_unordered_hashes = _get.bookindex_unordered_hashes; obj_cite_digits = _get.obj_cite_digits; comp_obj_ = _get.comp_obj_; cntr = _get.cntr; pith = _get.pith; } } } else { // line.empty, post contents, empty variables: assert( line.empty, "\nline should be empty:\n \"" ~ line ~ "\"" ); assert( (pith["block_state"] == eN.blk_state.off), "code block status: none" ); if (_new_doc) { tag_assoc = tag_assoc.init; lv0to3_tags = lv0to3_tags.init; tag_in_seg = tag_in_seg.init; } if (pith["txt_is"] == eN.txt_is.heading && line_occur["heading"] > eN.bi.off ) { // heading object (current line empty) obj_cite_digits = (an_object["lev_markup_number"].to!int == 0) ? ocn_emit(eN.ocn.reset) : ocn_emit(pith["ocn"]); an_object["is"] = "heading"; an_object_key = "body_nugget"; ST_txtAndAnchorTagPlusHasFootnotesUrlsImages substantive_object_and_anchor_tags_struct = obj_im.obj_inline_markup_and_anchor_tags_and_misc(an_object, an_object_key, conf_make_meta, ((_new_doc) ? Yes._new_doc : No._new_doc)); an_object["substantive"] = substantive_object_and_anchor_tags_struct.obj_txt; anchor_tag = substantive_object_and_anchor_tags_struct.anchor_tag; /+ ↓ an anchor tag names one heading. Markup can give the same name to several ("2~x- 1.", "2~x- 2." ...), and an id has to be unique within a document, so a repeat takes the ocn as a suffix. The first keeps the name the author wrote, so a link to it still arrives. +/ if (!(anchor_tag.empty)) { if (anchor_tag in _heading_anchor_tags_seen) { anchor_tag = anchor_tag ~ "-" ~ obj_cite_digits.object_number.to!string; } _heading_anchor_tags_seen[anchor_tag] = true; } if (_new_doc) { cnt1 = 1; cnt2 = 1; cnt3 = 1; _new_doc = false; } if ( an_object["lev_markup_number"].to!int == 4 && (!(anchor_tag.empty) || (lv0to3_tags.length > 0)) ) { tag_in_seg["seg_lv4"] = anchor_tag; tag_in_seg["seg_lv1to4"] = anchor_tag; lev_anchor_tag = anchor_tag; tag_assoc[anchor_tag]["seg_lv4"] = tag_in_seg["seg_lv4"]; tag_assoc[anchor_tag]["seg_lv1to4"] = tag_in_seg["seg_lv1to4"]; if (lv0to3_tags.length > 0) { // names used for html markup segments 1 to 4 (rather than epub which has separate segments for A to D) foreach (lv0_to_lv3_html_tag; lv0to3_tags) { tag_assoc[lv0_to_lv3_html_tag]["seg_lv4"] = anchor_tag; } } anchor_tag_ = anchor_tag; lv0to3_tags = lv0to3_tags.init; } else if (an_object["lev_markup_number"].to!int > 4) { tag_in_seg["seg_lv4"] = anchor_tag_; tag_in_seg["seg_lv1to4"] = anchor_tag_; lev_anchor_tag = anchor_tag; tag_assoc[anchor_tag]["seg_lv4"] = tag_in_seg["seg_lv4"]; tag_assoc[anchor_tag]["seg_lv1to4"] = tag_in_seg["seg_lv1to4"]; } else if (an_object["lev_markup_number"].to!int < 4) { string segn; switch (an_object["lev_markup_number"].to!int) { // names used for epub markup segments A to D case 0: segn = "_the_title"; goto default; case 1: segn = "_part_" ~ cnt1.to!string; ++cnt1; goto default; case 2: segn = "_part_" ~ cnt1.to!string ~ "_" ~ cnt2.to!string; ++cnt2; goto default; case 3: segn = "_part_" ~ cnt1.to!string ~ "_" ~ cnt2.to!string ~ "_" ~ cnt3.to!string; ++cnt3; goto default; default: lv0to3_tags ~= obj_cite_digits.object_number.to!string; lv0to3_tags ~= segn; tag_in_seg["seg_lv4"] = segn; // for html segname need following lv4 not yet known tag_in_seg["seg_lv1to4"] = segn; break; } } an_object["bookindex_nugget"] = ("bookindex_nugget" in an_object) ? an_object["bookindex_nugget"] : ""; bookindex_unordered_hashes = bookindex_extract_hash.bookindex_nugget_hash(an_object["bookindex_nugget"], obj_cite_digits, tag_in_seg); _anchor_tag = obj_cite_digits.identifier; // (incrementally build toc) table of contents here! { ST_flow_table_of_contents_gather_headings _get = obj_im.flow_table_of_contents_gather_headings( an_object, conf_make_meta, tag_in_seg, _anchor_tag, lev4_subtoc, the_document_toc_section, ); { the_document_toc_section = _get.the_document_toc_section; lev4_subtoc = _get.lev4_subtoc; } } if (an_object["lev_markup_number"] == "4") { segnames["html"] ~= tag_in_seg["seg_lv4"]; html_segnames_ptr = html_segnames_ptr_cntr; html_segnames_ptr_cntr++; } if (an_object["lev_markup_number"].to!int <= 4) { segnames["epub"] ~= tag_in_seg["seg_lv1to4"]; } ObjGenericComposite comp_obj_ = node_construct.node_emitter_heading( an_object, tag_in_seg, lev_anchor_tag, tag_assoc, obj_cite_digits, // OCNset cntr, // int heading_ptr, // int lv_ancestors_txt, // string[] html_segnames_ptr, // int substantive_object_and_anchor_tags_struct, ); ++heading_ptr; debug(segments) { writeln(an_object["lev_markup_number"]); writeln(tag_in_seg["seg_lv4"]); writeln(tag_in_seg["seg_lv1to4"]); } the_document_body_section ~= comp_obj_; debug(objectrelated1) { writeln(line); } // check { ST_txt_by_line_common_reset _get = txt_by_line_common_reset_(line_occur, an_object, pith); { line_occur = _get.line_occur; an_object = _get.this_object; pith = _get.pith; } } an_object.remove("lev"); an_object.remove("lev_markup_number"); processing.remove("verse"); ++cntr; } else if (pith["txt_is"] == eN.txt_is.para && line_occur["para"] > eN.bi.off ) { // paragraph object (current line empty) - repeated character paragraph separator if ((an_object[an_object_key].to!string).matchFirst(rgx.repeated_character_line_separator)) { pith["ocn"] = eN.ocn.off; } obj_cite_digits = ocn_emit(pith["ocn"]); an_object["bookindex_nugget"] = ("bookindex_nugget" in an_object) ? an_object["bookindex_nugget"] : ""; bookindex_unordered_hashes = bookindex_extract_hash.bookindex_nugget_hash(an_object["bookindex_nugget"], obj_cite_digits, tag_in_seg); an_object["is"] = "para"; ObjGenericComposite comp_obj_ = node_construct.node_location_emitter( content_non_header, tag_in_seg, lev_anchor_tag, tag_assoc, obj_cite_digits, cntr, heading_ptr-1, an_object["is"], ); ST_txtAndAnchorTagPlusHasFootnotesUrlsImages substantive_obj_misc_struct = obj_im.obj_inline_markup_and_anchor_tags_and_misc(an_object, an_object_key, conf_make_meta, No._new_doc); an_object["substantive"] = substantive_obj_misc_struct.obj_txt; anchor_tag = substantive_obj_misc_struct.anchor_tag; comp_obj_ = set_object_generic("body", "body", "para", "para", an_object["substantive"].to!string.strip, obj_cite_digits.object_number); comp_obj_.tags.html_segment_anchor_tag_is = tag_in_seg["seg_lv4"]; comp_obj_.tags.epub_segment_anchor_tag_is = tag_in_seg["seg_lv1to4"]; comp_obj_.metainfo.identifier = obj_cite_digits.identifier; comp_obj_.metainfo.object_number_off = obj_cite_digits.off; comp_obj_.metainfo.o_n_book_index = obj_cite_digits.bkidx; comp_obj_.metainfo.object_number_type = obj_cite_digits.type; comp_obj_.attrib.indent_hang = indent["hang_position"]; comp_obj_.attrib.indent_base = indent["base_position"]; comp_obj_.attrib.bullet = bullet; comp_obj_.tags.anchor_tags = [anchor_tag]; anchor_tag=""; comp_obj_.has.inline_notes_reg = substantive_obj_misc_struct.has_notes_reg; comp_obj_.has.inline_notes_star = substantive_obj_misc_struct.has_notes_star; comp_obj_.has.inline_links = substantive_obj_misc_struct.has_links; comp_obj_.has.images = substantive_obj_misc_struct.has_images; comp_obj_.has.image_without_dimensions = substantive_obj_misc_struct.has_images_without_dimensions; the_document_body_section ~= comp_obj_; tag_assoc = an_object.inline_para_link_anchor(tag_in_seg, tag_assoc); { ST_txt_by_line_common_reset _get = txt_by_line_common_reset_(line_occur, an_object, pith); { line_occur = _get.line_occur; an_object = _get.this_object; pith = _get.pith; } } indent = [ "hang_position" : 0, "base_position" : 0, ]; bullet = false; processing.remove("verse"); ++cntr; // } else { // could be useful to test line variable should be empty and never null } } // close else for line empty } // close else for not the above } // close after non code, other blocks or regular text // unless (the_document_body_section.length == 0) ? if (the_document_body_section.length > 0) { if (((the_document_body_section[$-1].metainfo.is_a == "para") || (the_document_body_section[$-1].metainfo.is_a == "heading") || (the_document_body_section[$-1].metainfo.is_a == "quote") || (the_document_body_section[$-1].metainfo.is_a == "group") || (the_document_body_section[$-1].metainfo.is_a == "block") || (the_document_body_section[$-1].metainfo.is_a == "verse")) && (the_document_body_section.length > previous_length)) { if ((the_document_body_section[$-1].metainfo.is_a == "heading") && (the_document_body_section[$-1].metainfo.heading_lev_markup < 5)) { pith["section"] = eN.sect.unset; } if (the_document_body_section[$-1].metainfo.is_a == "verse") { // scan for endnotes for whole poem (each verse in poem) foreach (i; previous_length .. the_document_body_section.length) { if (the_document_body_section[i].metainfo.is_a == "verse") { if ((the_document_body_section[i].text).match( rgx.inline_notes_al_all_note )) { object_notes = note_section.gather_notes_for_endnote_section( the_document_body_section, tag_in_seg, (i).to!int, ); } } } } else { // scan object for endnotes previous_length = the_document_body_section.length.to!int; if ((the_document_body_section[$-1].text).match( rgx.inline_notes_al_all_note )) { previous_count = (the_document_body_section.length -1).to!int; object_notes = note_section.gather_notes_for_endnote_section( the_document_body_section, tag_in_seg, (the_document_body_section.length-1).to!int, ); } } previous_length = the_document_body_section.length.to!int; } } } ret.toc = the_document_toc_section; ret.body = the_document_body_section; ret.glossary = the_document_glossary_section; ret.blurb = the_document_blurb_section; ret.object_notes = object_notes; ret.segnames = segnames; return ret; } { // loopMarkupSrcByLine _loopMarkupSrcByLineStruct _doc_by_line = loopMarkupSrcByLine(markup_sourcefile_content, an_object, pith); the_document_toc_section = _doc_by_line.toc; the_document_body_section = _doc_by_line.body; the_document_glossary_section = _doc_by_line.glossary; the_document_blurb_section = _doc_by_line.blurb; segnames = _doc_by_line.segnames; object_notes = _doc_by_line.object_notes; // endnotes, compare, not sure is used destroy(_doc_by_line); } #+END_SRC *** _post loop_ (consider and segment/break up further in code & org) #+NAME: docAbstractionPostMainLoop #+HEADER: :noweb yes #+BEGIN_SRC d { // EOF backMatter comp_obj_ = set_object_heading("lev1", "backmatter", "tail", ""); comp_obj_.metainfo.identifier = ""; comp_obj_.metainfo.dummy_heading = false; comp_obj_.metainfo.object_number_off = false; comp_obj_.metainfo.object_number_type = 0; comp_obj_.tags.segment_anchor_tag_epub = "_part_eof"; comp_obj_.tags.anchor_tag_html = comp_obj_.tags.segment_anchor_tag_epub; comp_obj_.tags.in_segment_html = "tail"; comp_obj_.tags.anchor_tags = ["section_eof"]; comp_obj_.metainfo.dom_structure_markedup_tags_status = [ 0, 0, 0, 0, 0, 0, 0, 0]; comp_obj_.metainfo.dom_structure_collapsed_tags_status = [ 0, 0, 0, 0, 0, 0, 0, 0]; the_document_xml_dom_tail_section ~= comp_obj_; tag_assoc[comp_obj_.tags.anchor_tag_html]["seg_lv4"] = comp_obj_.tags.in_segment_html; tag_assoc[comp_obj_.tags.segment_anchor_tag_epub]["seg_lv1to4"] = comp_obj_.tags.segment_anchor_tag_epub; } // endNotes ST_endnotes en_st = note_section.backmatter_endnote_objects(obj_cite_digits, opt_action); { // endnotes the_document_endnotes_section = en_st.endnotes; obj_cite_digits = en_st.ocn; debug(endnotes) { writefln("%s %s", __LINE__, the_document_endnotes_section.length); foreach (o; the_document_endnotes_section) { writeln(o); } } } { // glossary if (an_object["glossary_nugget"].length == 0) { comp_obj_ = set_object_heading("lev1", "empty", "empty", "(skip) there is no Glossary section"); comp_obj_.metainfo.identifier = ""; comp_obj_.metainfo.dummy_heading = true; comp_obj_.metainfo.object_number_off = true; comp_obj_.metainfo.object_number_type = 0; the_document_glossary_section ~= comp_obj_; } debug(glossary) { foreach (gloss; the_document_glossary_section) { writeln(gloss.text); } } } { // bibliography string[] biblio_unsorted_incomplete = biblio_arr_json.dup; ST_biblio_section biblio_section = backmatter_make_the_bibliography_section(biblio_unsorted_incomplete, bib_arr_json); the_document_bibliography_section = biblio_section.bibliography_section; tag_assoc = biblio_section.tag_assoc; } { // bookindex BookIndexReportSection bi = BookIndexReportSection(); ST_bookindex bi_st = bi.backmatter_bookindex_build_abstraction_section(bookindex_unordered_hashes, obj_cite_digits, opt_action); destroy(bookindex_unordered_hashes); the_document_bookindex_section = bi_st.bookindex; obj_cite_digits = bi_st.ocn; debug(bookindex) { foreach (bi_entry; the_document_bookindex_section) { writeln(bi_entry); } } } { // blurb if (an_object["blurb_nugget"].length == 0) { comp_obj_ = set_object_heading("lev1", "empty", "empty", "(skip) there is no Blurb section"); comp_obj_.metainfo.identifier = ""; comp_obj_.metainfo.object_number_off = true; comp_obj_.metainfo.object_number_type = 0; comp_obj_.tags.segment_anchor_tag_epub = ""; comp_obj_.tags.anchor_tag_html = ""; comp_obj_.tags.in_segment_html = ""; the_document_blurb_section ~= comp_obj_; } debug(blurb) { foreach (blurb; the_document_blurb_section) { writeln(blurb.text); } } } { // toc gather backmatter the_document_toc_section ~= backmatter_gather_table_of_contents(the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section); // } { // document head and body the_document_head_section ~= the_document_body_section[0]; the_document_body_section = the_document_body_section[1..$]; } /+ ↓ document ancestors: moved to run after every sections ocn have been generated (the backmatter section headings are given their ocn in the per section loops below) and ahead of descendants & children which depend on parent_ocn +/ { // document segnames ST_segnames get_segnames; get_segnames = the_document_body_section.after_doc_determine_segnames(the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, segnames, html_segnames_ptr_cntr, html_segnames_ptr); segnames = get_segnames.segnames; html_segnames_ptr_cntr = get_segnames.html_segnames_ptr_cntr; html_segnames_ptr = get_segnames.html_segnames_ptr; } // document head string[] segnames_0_to_4; foreach (ref obj; the_document_head_section) { if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; } if (obj.metainfo.heading_lev_markup == 0) { // TODO second hit (of two) with same assertion failure, check, fix and reinstate // assert( obj.metainfo.ocn == 1, // "Title OCN should be 1 not: " ~ obj.metainfo.ocn.to!string); // bug introduced 0.18.1 obj.metainfo.ocn = 1; obj.metainfo.identifier = "1"; obj.metainfo.object_number_type = OCNtype.ocn; } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } obj = _links(obj); } if (the_document_toc_section.length > 1) { // writeln("toc"); // scroll dom_structure_markedup_tags_status_buffer = dom_structure_markedup_tags_status.dup; dom_structure_collapsed_tags_status_buffer = dom_structure_collapsed_tags_status.dup; foreach (ref obj; the_document_toc_section) { if (obj.metainfo.is_a == "heading") { if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } obj = _links(obj); } } // images string[] _images; // multiple 1~ levels, loop through document body if (the_document_body_section.length > 1) { // writeln("body"); foreach (ref obj; the_document_body_section) { if (!(obj.metainfo.identifier.empty)) { if (!(((obj.metainfo.identifier) in tag_assoc) && ("seg_lv4" in tag_assoc[(obj.metainfo.identifier)])) ) { tag_assoc[(obj.metainfo.identifier)]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; } /+ ↓ An object that is not written into any segment still carries an identifier, and can share it: a poem block and its verses have one ocn between them, the verses are written and the block itself is not. The block comes second, so an unguarded assignment replaced a good segment name with an empty one, and the link came out as ".xhtml#297". seg_lv4 above is guarded already, which is why this showed in the epub and not in the html. +/ if (!(obj.tags.epub_segment_anchor_tag_is.empty)) { tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"] = obj.tags.epub_segment_anchor_tag_is; } } if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.lev4_subtoc = lev4_subtoc[obj.tags.anchor_tag_html]; obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } else if (obj.metainfo.is_a == "para") { _images ~= extract_images(obj.text); obj = _image_dimensions(obj, manifested); } obj.metainfo.sha256 = obj.obj_digest(manifested); obj = _links(obj); } } auto image_list = (_images.sort()).uniq; // endnotes optional only one 1~ level if (the_document_endnotes_section.length > 1) { // writeln("endnotes"); dom_structure_markedup_tags_status_buffer = dom_structure_markedup_tags_status.dup; dom_structure_collapsed_tags_status_buffer = dom_structure_collapsed_tags_status.dup; dom_structure_markedup_tags_status = dom_structure_markedup_tags_status_buffer.dup; dom_structure_collapsed_tags_status = dom_structure_collapsed_tags_status_buffer.dup; foreach (ref obj; the_document_endnotes_section) { if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup == 1) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } obj = _links(obj); } } // glossary optional only one 1~ level if (the_document_glossary_section.length > 1) { // writeln("glossary"); foreach (ref obj; the_document_glossary_section) { if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup == 1) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } else if (obj.metainfo.is_a == "glossary" && !(obj.text.empty)) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } obj.metainfo.sha256 = obj.obj_digest(manifested); obj = _links(obj); } } // bibliography optional only one 1~ level if (the_document_bibliography_section.length > 1) { // writeln("bibliography"); foreach (ref obj; the_document_bibliography_section) { if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup == 1) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } else if (obj.metainfo.is_a == "bibliography") { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } obj.metainfo.sha256 = obj.obj_digest(manifested); obj = _links(obj); } } // book index, optional only one 1~ level int ocn_ = obj_cite_digits.object_number; int ocn_bkidx_ = 0; int ocn_bidx_; if (the_document_bookindex_section.length > 1) { // writeln("book index"); // scroll dom_structure_markedup_tags_status_buffer = dom_structure_markedup_tags_status.dup; dom_structure_collapsed_tags_status_buffer = dom_structure_collapsed_tags_status.dup; foreach (ref obj; the_document_bookindex_section) { if (obj.metainfo.is_a == "heading") { // debug(dom) { } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; } if (obj.metainfo.heading_lev_markup == 1) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } if (obj.metainfo.heading_lev_markup <= 4) { if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } else if (obj.metainfo.is_a == "bookindex") { obj_cite_digits = ocn_emit(eN.ocn.bkidx); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; obj.metainfo.o_n_book_index = obj_cite_digits.bkidx; obj.metainfo.object_number_type = OCNtype.bkidx; } obj.metainfo.sha256 = obj.obj_digest(manifested); obj = _links(obj); } // TODO assert failure, reinstate // assert(obj_cite_digit_bkidx == ocn_bidx_ obj_cite_digit_bkidx ~ " == ocn_" ~ ocn_ ~ "?"); } // blurb optional only one 1~ level if (the_document_blurb_section.length > 1) { // writeln("blurb"); foreach (ref obj; the_document_blurb_section) { if (obj.metainfo.is_a == "heading") { debug(dom) { writeln(obj.text); } if (obj.metainfo.heading_lev_markup == 1) { obj_cite_digits = ocn_emit(eN.ocn.on); obj.metainfo.ocn = obj_cite_digits.object_number; obj.metainfo.identifier = obj_cite_digits.identifier; } if (obj.metainfo.heading_lev_markup <= 4) { segnames_0_to_4 ~= obj.tags.segment_anchor_tag_epub; if (obj.metainfo.heading_lev_markup == 4) { obj.tags.segname_prev = segnames["html"][obj.ptr.html_segnames - 1]; if (segnames["html"].length > obj.ptr.html_segnames + 1) { obj.tags.segname_next = segnames["html"][obj.ptr.html_segnames + 1]; } assert(obj.tags.anchor_tag_html == segnames["html"][obj.ptr.html_segnames], obj.tags.anchor_tag_html ~ "!=" ~ segnames["html"][obj.ptr.html_segnames]); } } // dom structure (marked up & collapsed) if (opt_action.meta_processing_xml_dom) { obj = obj.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, obj.metainfo.heading_lev_markup); obj = obj.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, obj.metainfo.heading_lev_collapsed); } obj = obj.obj_heading_ancestors(lv_ancestors_txt); } else if (obj.metainfo.is_a == "blurb") { obj_cite_digits = ocn_emit(eN.ocn.off); obj.metainfo.object_number_off = obj_cite_digits.off; obj.metainfo.object_number_type = OCNtype.non; } obj.metainfo.sha256 = obj.obj_digest(manifested); obj = _links(obj); } } { // document ancestors ST_ancestors get_ancestors; get_ancestors = the_document_body_section.after_doc_determine_ancestors( the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, the_document_head_section ); the_document_body_section = get_ancestors.the_document_body_section; the_document_head_section = get_ancestors.the_document_head_section; the_document_endnotes_section = get_ancestors.the_document_endnotes_section; the_document_glossary_section = get_ancestors.the_document_glossary_section; the_document_bibliography_section = get_ancestors.the_document_bibliography_section; the_document_bookindex_section = get_ancestors.the_document_bookindex_section; the_document_blurb_section = get_ancestors.the_document_blurb_section; } /+ ↓ the html segment a cross reference to a heading lands on. a heading above level 4 opens no html segment, so a link to it goes to the level 4 heading that follows it. The build loop cannot know that yet and back-fills tag_assoc when the level 4 heading arrives (see lv0to3_tags); this is the same answer, kept on the object, so that it travels with the abstraction instead of living only in a map the parser holds. Walking backwards, the last level 4 seen is the next one for everything above it. +/ { string _next_lv4; foreach_reverse (ref obj; the_document_body_section) { if (obj.metainfo.is_a != "heading") { continue; } if (obj.metainfo.heading_lev_markup == 4) { _next_lv4 = obj.tags.anchor_tag_html; } else if (obj.metainfo.heading_lev_markup < 4) { obj.tags.segment_lv4_is = _next_lv4; } } /+ ↓ the head section's title sits above the body and resolves to the body's first level 4 heading, which is what _next_lv4 now holds +/ foreach_reverse (ref obj; the_document_head_section) { if (obj.metainfo.is_a != "heading") { continue; } if (obj.metainfo.heading_lev_markup == 4) { _next_lv4 = obj.tags.anchor_tag_html; } else if (obj.metainfo.heading_lev_markup < 4) { obj.tags.segment_lv4_is = _next_lv4; } } } // get descendants if (the_document_body_section.length > 1) { auto pairs = after_doc_get_descendants( the_document_head_section ~ the_document_body_section ~ the_document_endnotes_section ~ the_document_glossary_section ~ the_document_bibliography_section ~ the_document_bookindex_section ~ the_document_blurb_section ~ the_document_xml_dom_tail_section ); debug(descendants_tuple) { pairs = pairs.sort(); foreach (pair; pairs) { // (pair; pairs.sort()) writeln(pair[0], "..", pair[1]); } } foreach (ref obj; the_document_head_section) { if (obj.metainfo.is_a == "heading") { if (obj.metainfo.heading_lev_markup == 0) { // sha for document root heading, caught here, should have been done earlier obj.metainfo.sha256 = obj.obj_digest(manifested); } foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } if (the_document_body_section.length > 1) { foreach (ref obj; the_document_body_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_endnotes_section.length > 1) { foreach (ref obj; the_document_endnotes_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_glossary_section.length > 1) { foreach (ref obj; the_document_glossary_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_bibliography_section.length > 1) { foreach (ref obj; the_document_bibliography_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_bookindex_section.length > 1) { foreach (ref obj; the_document_bookindex_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_blurb_section.length > 1) { foreach (ref obj; the_document_blurb_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } if (the_document_xml_dom_tail_section.length > 1) { foreach (ref obj; the_document_xml_dom_tail_section) { if (obj.metainfo.is_a == "heading") { foreach (pair; pairs) { if (obj.metainfo.ocn == pair[0]) { obj.metainfo.last_descendant_ocn = pair[1]; } } } } } } /+ ↓ compute children_headings for heading objects +/ { int[][int] heading_children; /+ ↓ substantive headings only (ocn 0 is an author suppressed heading and is not citable, it does not have a place in the citation structure), gathered across every section not the body alone +/ foreach (section; [ the_document_head_section, the_document_body_section, the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, ]) { foreach (obj; section) { if (obj.metainfo.is_a == "heading" && obj.metainfo.ocn != 0 && obj.metainfo.parent_ocn != 0 && obj.metainfo.parent_ocn != obj.metainfo.ocn ) { heading_children[obj.metainfo.parent_ocn] ~= obj.metainfo.ocn; } } } /+ ↓ slice copies, the elements written to are the same elements +/ foreach (section; [ the_document_head_section, the_document_body_section, the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, ]) { foreach (ref obj; section) { if (obj.metainfo.is_a == "heading" && obj.metainfo.ocn in heading_children) { obj.metainfo.children_headings = heading_children[obj.metainfo.ocn]; } } } // if (opt_action.vox_gt_2) { // foreach (section; [the_document_head_section, the_document_body_section, the_document_bookindex_section, the_document_bibliography_section, the_document_glossary_section, the_document_blurb_section]) { // foreach (obj; section) { // if (section.length > 0) { // if (obj.metainfo.sha256.summary.length > 0) { // writeln(obj.metainfo.sha256.summary); // } // } // } // } // } } // TODO // - note create/insert heading object sole purpose eof close all open tags // sort out: // - obj.metainfo.dom_structure_markedup_tags_status = dom_structure_markedup_tags_status; // - obj.metainfo.dom_structure_collapsed_tags_status = dom_structure_collapsed_tags_status; comp_obj_ = set_object_heading("lev1", "empty", "empty", ""); comp_obj_.metainfo.identifier = ""; comp_obj_.metainfo.dummy_heading = true; comp_obj_.metainfo.object_number_off = true; comp_obj_.metainfo.object_number_type = 0; comp_obj_.tags.segment_anchor_tag_epub = ""; comp_obj_.tags.anchor_tag_html = ""; comp_obj_.tags.in_segment_html = ""; comp_obj_.tags.html_segment_anchor_tag_is = ""; comp_obj_.tags.epub_segment_anchor_tag_is = ""; comp_obj_.metainfo.heading_lev_markup = 9; comp_obj_.metainfo.heading_lev_collapsed = 9; comp_obj_.metainfo.parent_ocn = 0; comp_obj_.metainfo.parent_lev_markup = 0; comp_obj_.metainfo.dom_structure_markedup_tags_status = dom_structure_markedup_tags_status.dup; comp_obj_.metainfo.dom_structure_collapsed_tags_status = dom_structure_collapsed_tags_status.dup; comp_obj_ = comp_obj_.obj_dom_structure_set_markup_tags(dom_structure_markedup_tags_status, 0); comp_obj_ = comp_obj_.obj_dom_set_collapsed_tags(dom_structure_collapsed_tags_status, 0); comp_obj_ = comp_obj_.obj_heading_ancestors(lv_ancestors_txt); // the_dom_tail_section ~= comp_obj_; // remove tail for now, decide on later // the doc ObjGenericComposite[][string] document_the = [ "head": the_document_head_section, "toc": the_document_toc_section, // substantive/body: "body": the_document_body_section, // backmatter: "endnotes": the_document_endnotes_section, "glossary": the_document_glossary_section, "bibliography": the_document_bibliography_section, "bookindex": the_document_bookindex_section, "blurb": the_document_blurb_section, // dom tail only "tail": the_document_xml_dom_tail_section, ]; /+ ↓ document level counts of inline markup. The block counters (codeblock, poem, group, block, quote, table) are incremented as the blocks are recognised; the inline markup is counted here, over the assembled body objects. The body is counted alone: the endnotes section is a rendering of the notes the body objects carry inline. Links: those _links() stowed are counted from stow.link, their place in the text having been taken by the stow index (a numeric target); the link markup left in the text (anchors, and urls _links() does not stow) is counted from the text +/ foreach (obj_; document_the["body"]) { dochas["inline_links"] += obj_.stow.link.length.to!uint; foreach (m; obj_.text.matchAll(rgx.inline_link)) { if (!(m["link"].all!(c => (c >= '0' && c <= '9')))) { ++dochas["inline_links"]; } } foreach (m; obj_.text.matchAll(rgx.inline_notes_al_regular_number_note)) { ++dochas["inline_notes"]; } foreach (m; obj_.text.matchAll(rgx.inline_notes_al_special_char_note)) { if (m["char"].canFind("*")) { ++dochas["inline_notes_star"]; } } } // document parts keys as needed string[][string] document_section_keys_sequenced = [ "scroll": ["head", "toc", "body",], "seg": ["head", "toc", "body",], "sql": ["head", "body",], "latex": ["head", "toc", "body",], "text": ["head", "toc", "body",], ]; if (document_the["endnotes"].length > 1) { document_section_keys_sequenced["scroll"] ~= "endnotes"; document_section_keys_sequenced["seg"] ~= "endnotes"; document_section_keys_sequenced["latex"] ~= "endnotes"; document_section_keys_sequenced["text"] ~= "endnotes"; } if (document_the["glossary"].length > 1) { document_section_keys_sequenced["scroll"] ~= "glossary"; document_section_keys_sequenced["seg"] ~= "glossary"; document_section_keys_sequenced["sql"] ~= "glossary"; document_section_keys_sequenced["latex"] ~= "glossary"; document_section_keys_sequenced["text"] ~= "glossary"; } if (document_the["bibliography"].length > 1) { document_section_keys_sequenced["scroll"] ~= "bibliography"; document_section_keys_sequenced["seg"] ~= "bibliography"; document_section_keys_sequenced["sql"] ~= "bibliography"; document_section_keys_sequenced["latex"] ~= "bibliography"; document_section_keys_sequenced["text"] ~= "bibliography"; } if (document_the["bookindex"].length > 1) { document_section_keys_sequenced["scroll"] ~= "bookindex"; document_section_keys_sequenced["seg"] ~= "bookindex"; document_section_keys_sequenced["sql"] ~= "bookindex"; document_section_keys_sequenced["latex"] ~= "bookindex"; document_section_keys_sequenced["text"] ~= "bookindex"; } if (document_the["blurb"].length > 1) { document_section_keys_sequenced["scroll"] ~= "blurb"; document_section_keys_sequenced["seg"] ~= "blurb"; document_section_keys_sequenced["sql"] ~= "blurb"; document_section_keys_sequenced["latex"] ~= "blurb"; document_section_keys_sequenced["text"] ~= "blurb"; } if ((opt_action.html) || (opt_action.html_scroll) || (opt_action.html_seg) || (opt_action.epub)) { document_section_keys_sequenced["scroll"] ~= "tail"; document_section_keys_sequenced["seg"] ~= "tail"; } // segnames string[] segnames_4 = segnames["html"].dup; string[] segnames_lv1to4 = segnames["epub"].dup; debug(segnames) { writeln("segnames_lv4: ", segnames_4); writeln("segnames_lv1to4: ", segnames_lv1to4); } #+END_SRC *** _return structure_ #+NAME: docAbstractionReturnsStruct #+HEADER: :noweb yes #+BEGIN_SRC d // restart destroy(the_document_head_section); destroy(the_document_toc_section); destroy(the_document_body_section); destroy(the_document_endnotes_section); destroy(the_document_glossary_section); destroy(the_document_bibliography_section); destroy(the_document_bookindex_section); destroy(the_document_blurb_section); destroy(the_document_xml_dom_tail_section); destroy(segnames); destroy(bookindex_unordered_hashes); destroy(an_object); obj_cite_digits = ocn_emit(eN.ocn.reset); biblio_arr_json = []; obj_cite_digit_ = 0; html_segnames_ptr = 0; html_segnames_ptr_cntr = 0; content_non_header = "8"; lev_anchor_tag = ""; anchor_tag = ""; _heading_anchor_tags_seen = _reserved_heading_anchor_tags(); // identify parts /+ ↓ what the document has, as plain data. ST_DocHas is in metadoc_object_setter so that it can also be built from a loaded abstraction, without the parser's locals to hand +/ ST_DocHas doc_has_ = { inline_links : dochas["inline_links"], inline_notes_reg : dochas["inline_notes"], inline_notes_star : dochas["inline_notes_star"], codeblocks : dochas["codeblock"], tables : dochas["table"], blocks : dochas["block"], groups : dochas["group"], poems : dochas["poem"], quotes : dochas["quote"], imagelist : image_list.array, section_keys_sequenced : document_section_keys_sequenced, segnames_lv4 : segnames_4, segnames_lv_0_to_4 : segnames_0_to_4, tag_associations : tag_assoc, }; // the doc to be returned struct ST_docAbstraction { ObjGenericComposite[][string] document_the; ST_DocHas doc_has; } ST_docAbstraction ret; { ret.document_the = document_the; ret.doc_has = doc_has_; } return ret; #+END_SRC * ocda align #+HEADER: :tangle "../src/sisudoc/ocda/meta/ocn_align.d" #+HEADER: :noweb yes #+BEGIN_SRC d <> /++ ocn alignment across the languages of a document

the check that the translations of a document still number their objects the same way

[sisudoc.ocda.meta.ocn_align] +/ module sisudoc.ocda.meta.ocn_align; @safe: /+ ↓ the languages of a document share their object numbering. . This is the basis of a citation in a multi-language document: an ocn names the same object in every language, so a reference given in one language is followable in any of them. Nothing enforces it. It holds because the translations follow the source object for object, and it stops holding the moment a translator drops a paragraph, merges two, or turns a heading into a sentence. The document still builds, every output is produced, and the numbering has quietly diverges. . This says so. Each language leaves a profile as it is abstracted: how many numbered objects it has, and what kind of object each ocn is. After the last language of a document, the profiles are compared against the document's first language, the one the manifest names first. . Two kinds of divergence, in the order they are worth hearing: . count the languages do not have the same number of numbered objects. Everything after the first difference is renumbered, so a single dropped paragraph moves every ocn below it. This is reported alone, because a kind comparison after it would report hundreds of consequences of the one cause. . type the same ocn is a different type/kind of object. The numbering still lines up, but the object at that number is a heading in one language and a paragraph in another, which is the signature of a heading translated as running text. . A warning by default; --strict makes it a failure. . It is deliberately not part of the abstraction. Nothing here changes what a document is, and nothing is written into the .ssp: a language cannot know whether it aligns with its siblings until they have all been read, and an artefact that recorded it would be recording something about the run rather than about the document. +/ /+ ↓ mixed in as a named mixin (mixin spineOcnAlign _name;) and its imports are inside its functions rather than at template scope, both for the same reason: what a mixin template declares at its own scope is visible in the scope it is mixed into, and an import there can hijack a name the host was already resolving by UFCS. +/ template spineOcnAlign() { /+ ↓ what one language of a document leaves behind for the comparison. kinds is the ocn to object kind map, and it is the whole of what a language has to say here: the text is expected to differ, the structure is not. +/ struct ST_OcnProfile { string doc_key; // the document, without its language string lang; bool is_reference; // the language the manifest names first string db_file; // where to note the outcome, "" if no database int ocn_max; int ocn_count; string[int] kinds; } /+ ↓ one language's answer, and why +/ struct ST_OcnAlignResult { string doc_key; string lang; string reference_lang; bool aligned; string[] notes; } /+ ↓ the kind of an object, for comparison. a heading carries its level, because a B heading becoming a C heading is a structural change at the same ocn and is worth the same warning as a heading becoming a paragraph. +/ private string _kindOf(O)(O obj) { return (obj.metainfo.is_a == "heading") ? "heading:" ~ obj.metainfo.marked_up_level : obj.metainfo.is_a; } /+ ↓ the profile of one language, taken from its abstraction. only numbered objects: an object with no ocn is not citable and is not part of what the languages promise each other. +/ ST_OcnProfile ocnProfile(D)(D doc) { import std.algorithm : canFind, sort; import std.conv : to; ST_OcnProfile _p; _p.doc_key = doc.matters.src.doc_uid_out_no_lang; _p.lang = doc.matters.src.language; _p.is_reference = (doc.matters.pod.manifest_list_of_languages.length > 0) ? (doc.matters.src.language == doc.matters.pod.manifest_list_of_languages[0]) : true; /+ ↓ document order, the same section order the .ssp is written in, and not the abstraction's key order: an associative array does not promise one, and two runs over the same document would then be entitled to profile it differently. Any section not on the list is still swept, by name, so that a new one is not silently skipped. +/ string[] _sections = ["head", "toc", "body", "endnotes", "glossary", "bibliography", "bookindex", "blurb", "tail"]; string[] _extra; foreach (_k; doc.abstraction.byKey) { if (!_sections.canFind(_k)) { _extra ~= _k; } } foreach (_k; _extra.sort) { _sections ~= _k; } foreach (section; _sections) { if (section !in doc.abstraction) { continue; } foreach (obj; doc.abstraction[section]) { int _ocn = obj.metainfo.ocn.to!int; if (_ocn <= 0) { continue; } /+ ↓ first in document order wins. An ocn is not unique: a poem and its first verse share one, by design, so the same number names two objects and the first of them is the one both languages will have reached first. +/ if (_ocn !in _p.kinds) { _p.kinds[_ocn] = _kindOf(obj); } _p.ocn_count += 1; if (_ocn > _p.ocn_max) { _p.ocn_max = _ocn; } } } return _p; } /+ ↓ how many differing ocns to name before saying "and N more". enough to see the shape of the divergence, few enough that a document whose translation has genuinely gone its own way does not bury the rest of the run. +/ enum size_t OCN_ALIGN_NOTES_MAX = 5; /+ ↓ the comparison, for every document in the run at once. the profiles arrive in whatever order the run produced them, which under --parallel is not the manifest order, so the reference language is taken from the flag each profile carries rather than from position. +/ ST_OcnAlignResult[] ocnAlignCompare(ST_OcnProfile[] _profiles) { import std.algorithm : sort; import std.conv : to; ST_OcnAlignResult[] _out; ST_OcnProfile[][string] _by_doc; foreach (_p; _profiles) { _by_doc[_p.doc_key] ~= _p; } foreach (_doc_key; _by_doc.keys.sort) { /+ ↓ by language name, not in the order the run produced them: under --parallel that order is whatever the threads finished in, and a check people read should say the same thing twice running +/ auto _langs = _by_doc[_doc_key].sort!((a, b) => a.lang < b.lang).release; if (_langs.length < 2) { continue; } // nothing to compare against ST_OcnProfile _ref; bool _have_ref = false; foreach (_p; _langs) { if (_p.is_reference) { _ref = _p; _have_ref = true; break; } } if (!_have_ref) { continue; } foreach (_p; _langs) { if (_p.lang == _ref.lang) { continue; } ST_OcnAlignResult _r; _r.doc_key = _doc_key; _r.lang = _p.lang; _r.reference_lang = _ref.lang; _r.aligned = true; if (_p.ocn_count != _ref.ocn_count) { _r.aligned = false; long _diff = _p.ocn_count.to!long - _ref.ocn_count.to!long; _r.notes ~= "numbered objects: " ~ _ref.lang ~ " " ~ _ref.ocn_count.to!string ~ ", " ~ _p.lang ~ " " ~ _p.ocn_count.to!string ~ " (" ~ ((_diff > 0) ? "+" : "") ~ _diff.to!string ~ ")"; if (_p.ocn_max != _ref.ocn_max) { _r.notes ~= "highest ocn: " ~ _ref.lang ~ " " ~ _ref.ocn_max.to!string ~ ", " ~ _p.lang ~ " " ~ _p.ocn_max.to!string; } /+ ↓ no type/kind comparison after a count difference: past the first dropped or added object every ocn names something else, and the hundreds of lines that follow are all the one fault +/ _out ~= _r; continue; } size_t _kind_diffs = 0; foreach (_ocn; _ref.kinds.keys.sort) { if (auto _k = _ocn in _p.kinds) { if (*_k == _ref.kinds[_ocn]) { continue; } _kind_diffs += 1; if (_kind_diffs <= OCN_ALIGN_NOTES_MAX) { _r.notes ~= "ocn " ~ _ocn.to!string ~ ": " ~ _ref.kinds[_ocn] ~ " in " ~ _ref.lang ~ ", " ~ *_k ~ " in " ~ _p.lang; } } else { _kind_diffs += 1; if (_kind_diffs <= OCN_ALIGN_NOTES_MAX) { _r.notes ~= "ocn " ~ _ocn.to!string ~ ": " ~ _ref.kinds[_ocn] ~ " in " ~ _ref.lang ~ ", absent in " ~ _p.lang; } } } if (_kind_diffs > OCN_ALIGN_NOTES_MAX) { _r.notes ~= "and " ~ (_kind_diffs - OCN_ALIGN_NOTES_MAX).to!string ~ " more"; } if (_kind_diffs > 0) { _r.aligned = false; } _out ~= _r; } } return _out; } /+ ↓ the report, as the lines to print. the document is named once and its languages indented under it, so a run over a collection reads as a list of documents rather than a list of languages. +/ string[] ocnAlignReportLines(ST_OcnAlignResult[] _results) { string[] _out; string _last_doc; foreach (_r; _results) { if (_r.aligned) { continue; } if (_r.doc_key != _last_doc) { _out ~= "WARNING ocn alignment: " ~ _r.doc_key; _last_doc = _r.doc_key; } _out ~= " [" ~ _r.lang ~ "] differs from [" ~ _r.reference_lang ~ "]"; foreach (_n; _r.notes) { _out ~= " " ~ _n; } } return _out; } } #+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__