aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sisudoc
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-07 11:06:33 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-09 17:28:22 -0400
commit176335fe3cb0d572e0093b081186d0c78bf9f331 (patch)
treea35ecb75cd897cad79e23a975a296888f1ab3772 /src/sisudoc
parent.ssp: three additional fields the writers read (diff)
ssp: definition of an object record, for readers
one definition of an object record, so a reader can be held to it. Pure refactor: the per-object emission moves out of the write loop into template sspObjectRecord, taking the object and its section and returning the lines. The writer mixes it in and calls it. The point is the reader that follows. With one definition of what an object record looks like, a round trip can be checked against the writer itself rather than against a second, hand-kept description of the format, which is how the .ssp and the abstraction db drifted apart in the first place. (emitted text is byte identical) (assisted by Claude-Code)
Diffstat (limited to 'src/sisudoc')
-rw-r--r--src/sisudoc/ocda/abstraction/ssp.d611
1 files changed, 310 insertions, 301 deletions
diff --git a/src/sisudoc/ocda/abstraction/ssp.d b/src/sisudoc/ocda/abstraction/ssp.d
index 1ec2d41..181e785 100644
--- a/src/sisudoc/ocda/abstraction/ssp.d
+++ b/src/sisudoc/ocda/abstraction/ssp.d
@@ -50,6 +50,314 @@
module sisudoc.ocda.abstraction.ssp;
@safe:
/+ ↓ write document abstraction as human-readable .ssp text file +/
+/+ ↓ one object record: the single definition of what an object looks
+ like in a .ssp file. the writer emits with it, and the reader is
+ checked against it by round trip, so the two cannot drift
++/
+template sspObjectRecord() {
+ import std.conv : to;
+ import std.digest : toHexString;
+ import std.stdio;
+ import std.string;
+ import std.array;
+ string[] sspObjectRecord(O)(O obj, string section, bool _vox = false) {
+ string[] output;
+ /+ ↓ object declaration line. a heading carries its identifier here,
+ on the declaration line, where it is not the ocn; every other
+ object carries it as .identifier below, on the same condition
+ +/
+ string obj_decl = "[" ~ obj.metainfo.ocn.to!string ~ "] ";
+ if (obj.metainfo.is_a == "heading") {
+ string lev = obj.metainfo.marked_up_level;
+ obj_decl ~= "heading :" ~ lev;
+ if (obj.metainfo.identifier.length > 0
+ && obj.metainfo.identifier != obj.metainfo.ocn.to!string) {
+ obj_decl ~= " " ~ obj.metainfo.identifier;
+ }
+ } else {
+ obj_decl ~= obj.metainfo.is_a;
+ }
+ output ~= obj_decl;
+ /+ ↓ properties (only non-default values) +/
+ if (obj.metainfo.is_a != "heading"
+ && obj.metainfo.identifier.length > 0
+ && obj.metainfo.identifier != obj.metainfo.ocn.to!string) {
+ output ~= ".identifier: " ~ obj.metainfo.identifier;
+ }
+ if (obj.metainfo.is_of_part.length > 0) {
+ output ~= ".part: " ~ obj.metainfo.is_of_part;
+ }
+ if (obj.metainfo.is_of_section.length > 0
+ && obj.metainfo.is_of_section != section) {
+ output ~= ".section: " ~ obj.metainfo.is_of_section;
+ }
+ /+ ↓ parent: emitted for every object except:
+ - the document root, the one object that has no parent
+ - toc entries, generated furniture rather than authored objects,
+ whose place is given by their indent level; the toc heading
+ itself does carry its parent (the document root)
+ A parent ocn of 0 is not "no parent recorded", it is a parent
+ heading the author has made non-substantive (ocn suppressed, or a
+ dummy heading introduced to segment an output); it is structurally
+ truthful and is stated explicitly rather than omitted so that the
+ two cases remain distinguishable in the file
+ +/
+ if (!(obj.metainfo.is_a == "heading"
+ && obj.metainfo.heading_lev_markup == 0)
+ && obj.metainfo.is_a != "toc"
+ ) {
+ output ~= ".parent: " ~ obj.metainfo.parent_ocn.to!string;
+ }
+ if (obj.metainfo.last_descendant_ocn != 0) {
+ output ~= ".last_descendant: " ~ obj.metainfo.last_descendant_ocn.to!string;
+ }
+ /+ ↓ child headings (from pre-computed map) +/
+ if (obj.metainfo.children_headings.length > 0) {
+ string[] ch;
+ foreach (c; obj.metainfo.children_headings) {
+ ch ~= c.to!string;
+ }
+ output ~= ".children: " ~ ch.join(" ");
+ }
+ /+ ↓ ancestors (only if non-zero) +/
+ {
+ bool has_anc = false;
+ foreach (a; obj.metainfo.markedup_ancestors) {
+ if (a != 0) { has_anc = true; break; }
+ }
+ if (has_anc) {
+ string anc;
+ foreach (i, a; obj.metainfo.markedup_ancestors) {
+ if (i > 0) { anc ~= " "; }
+ anc ~= a.to!string;
+ }
+ output ~= ".ancestors: " ~ anc;
+ }
+ }
+ /+ ↓ collapsed ancestors (only if non-zero) +/
+ {
+ bool has_anc_c = false;
+ foreach (a; obj.metainfo.collapsed_ancestors) {
+ if (a != 0) { has_anc_c = true; break; }
+ }
+ if (has_anc_c) {
+ string anc;
+ foreach (i, a; obj.metainfo.collapsed_ancestors) {
+ if (i > 0) anc ~= " ";
+ anc ~= a.to!string;
+ }
+ output ~= ".ancestors_collapsed: " ~ anc;
+ }
+ }
+ /+ ↓ dom structure status (only if non-zero) +/
+ {
+ bool has_dom = false;
+ foreach (d; obj.metainfo.dom_structure_markedup_tags_status) {
+ if (d != 0) { has_dom = true; break; }
+ }
+ if (has_dom) {
+ string ds;
+ foreach (i, d; obj.metainfo.dom_structure_markedup_tags_status) {
+ if (i > 0) { ds ~= " "; }
+ ds ~= d.to!string;
+ }
+ output ~= ".dom_status: " ~ ds;
+ }
+ }
+ {
+ bool has_dom_c = false;
+ foreach (d; obj.metainfo.dom_structure_collapsed_tags_status) {
+ if (d != 0) { has_dom_c = true; break; }
+ }
+ if (has_dom_c) {
+ string ds;
+ foreach (i, d; obj.metainfo.dom_structure_collapsed_tags_status) {
+ if (i > 0) { ds ~= " "; }
+ ds ~= d.to!string;
+ }
+ output ~= ".dom_status_collapsed: " ~ ds;
+ }
+ }
+ if (obj.metainfo.heading_lev_collapsed < 9) {
+ output ~= ".heading_lev_collapsed: " ~ obj.metainfo.heading_lev_collapsed.to!string;
+ }
+ if (obj.metainfo.parent_lev_markup != 0) {
+ output ~= ".parent_lev: " ~ obj.metainfo.parent_lev_markup.to!string;
+ }
+ if (obj.metainfo.dummy_heading) {
+ output ~= ".dummy: true";
+ }
+ if (obj.metainfo.object_number_off) {
+ output ~= ".ocn_off: true";
+ }
+ if (obj.metainfo.is_of_type.length > 0) {
+ output ~= ".is_of_type: " ~ obj.metainfo.is_of_type;
+ }
+ if (obj.metainfo.attrib.length > 0) {
+ output ~= ".attrib: " ~ obj.metainfo.attrib;
+ }
+ if (obj.metainfo.lang.length > 0) {
+ output ~= ".meta_lang: " ~ obj.metainfo.lang;
+ }
+ if (obj.metainfo.syntax.length > 0) {
+ output ~= ".meta_syntax: " ~ obj.metainfo.syntax;
+ }
+ /+ ↓ sha256 digest +/
+ {
+ bool has_sha = false;
+ foreach (b; obj.metainfo.sha256.text) {
+ if (b != 0) { has_sha = true; break; }
+ }
+ if (has_sha) {
+ if (_vox) {
+ writeln(obj.metainfo.sha256.summary);
+ }
+ output ~= ".sha256: " ~ obj.metainfo.sha256.text.toHexString.to!string;
+ }
+ }
+ /+ ↓ images, repeatable record, one per image in order of appearance:
+ .image: <filename> sha256:<HEX> bytes:<n> px:<width>x<height>
+ .image: <filename> missing:true
+ filename first (the object's handle on the image), remaining fields
+ self-describing key:value tokens so that further fields can be
+ added later without invalidating existing readers. px: is the
+ file's own pixel size, not the display dimensions (w##h##) which
+ the object text already carries
+ +/
+ if (obj.metainfo.sha256.images.length > 0) {
+ foreach (i; obj.metainfo.sha256.images) {
+ output ~= (i.fileMissing)
+ ? ".image: " ~ i.fileName
+ ~ " missing:true"
+ : ".image: " ~ i.fileName
+ ~ " sha256:" ~ i.fileHash_sha256.toHexString.to!string
+ ~ " bytes:" ~ i.fileSize.to!string
+ ~ ((i.imageWidth > 0 && i.imageHeight > 0)
+ ? " px:" ~ i.imageWidth.to!string
+ ~ "x" ~ i.imageHeight.to!string
+ : "");
+ }
+ }
+ /+ ↓ text attributes +/
+ if (obj.attrib.indent_base != 0 || obj.attrib.indent_hang != 0) {
+ output ~= ".indent: " ~ obj.attrib.indent_base.to!string
+ ~ " " ~ obj.attrib.indent_hang.to!string;
+ }
+ if (obj.attrib.bullet) {
+ output ~= ".bullet: true";
+ }
+ if (obj.attrib.language.length > 0) {
+ output ~= ".lang: " ~ obj.attrib.language;
+ }
+ /+ ↓ has flags +/
+ {
+ string[] has_flags;
+ if (obj.has.inline_links) { has_flags ~= "links"; }
+ if (obj.has.inline_notes_reg) { has_flags ~= "notes_reg"; }
+ if (obj.has.inline_notes_star) { has_flags ~= "notes_star"; }
+ if (obj.has.images) { has_flags ~= "images"; }
+ if (obj.has.image_without_dimensions) { has_flags ~= "images_no_dim"; }
+ if (has_flags.length > 0) {
+ output ~= ".has: " ~ has_flags.join(" ");
+ }
+ }
+ /+ ↓ table properties +/
+ if (obj.metainfo.is_a == "table" && obj.table.number_of_columns > 0) {
+ output ~= ".table_cols: " ~ obj.table.number_of_columns.to!string;
+ if (obj.table.column_widths.length > 0) {
+ string[] ws;
+ foreach (w; obj.table.column_widths) ws ~= w.to!string;
+ output ~= ".table_widths: " ~ ws.join(" ");
+ }
+ if (obj.table.column_aligns.length > 0) {
+ output ~= ".table_aligns: " ~ obj.table.column_aligns.join(" ");
+ }
+ if (obj.table.heading) {
+ output ~= ".table_header: true";
+ }
+ }
+ /+ ↓ code block properties +/
+ if (obj.metainfo.is_a == "code") {
+ if (obj.code_block.linenumbers) {
+ output ~= ".code_linenumbers: true";
+ }
+ }
+ /+ ↓ stow (extracted links) +/
+ if (obj.stow.link.length > 0) {
+ foreach (lnk; obj.stow.link) {
+ if (lnk.length > 0) {
+ output ~= ".stow_link: " ~ lnk;
+ }
+ }
+ }
+ /+ ↓ tag properties +/
+ if (obj.tags.in_segment_html.length > 0) {
+ output ~= ".segment: " ~ obj.tags.in_segment_html;
+ }
+ if (obj.tags.anchor_tag_html.length > 0
+ && obj.tags.anchor_tag_html != obj.tags.in_segment_html) {
+ output ~= ".anchor: " ~ obj.tags.anchor_tag_html;
+ }
+ if (obj.tags.segname_prev.length > 0) {
+ output ~= ".segment_prev: " ~ obj.tags.segname_prev;
+ }
+ if (obj.tags.segname_next.length > 0) {
+ output ~= ".segment_next: " ~ obj.tags.segname_next;
+ }
+ if (obj.tags.heading_lev_anchor_tag.length > 0) {
+ output ~= ".heading_lev_anchor: " ~ obj.tags.heading_lev_anchor_tag;
+ }
+ if (obj.tags.segment_anchor_tag_epub.length > 0) {
+ output ~= ".segment_epub: " ~ obj.tags.segment_anchor_tag_epub;
+ }
+ /+ ↓ the segment an object belongs to, as opposed to the segment a
+ heading opens. .segment (in_segment_html) is set on headings
+ only, so without these two a reader cannot tell which segment a
+ paragraph is in; the sqlite search writer reads the html one
+ +/
+ if (obj.tags.html_segment_anchor_tag_is.length > 0) {
+ output ~= ".segment_html_is: " ~ obj.tags.html_segment_anchor_tag_is;
+ }
+ if (obj.tags.epub_segment_anchor_tag_is.length > 0) {
+ output ~= ".segment_epub_is: " ~ obj.tags.epub_segment_anchor_tag_is;
+ }
+ /+ ↓ heading ancestors text +/
+ {
+ bool has_hat = false;
+ foreach (h; obj.tags.heading_ancestors_text) {
+ if (h.length > 0) { has_hat = true; break; }
+ }
+ if (has_hat) {
+ output ~= ".heading_ancestors_text: " ~ obj.tags.heading_ancestors_text.join("|");
+ }
+ }
+ /+ ↓ lev4 subtoc +/
+ if (obj.tags.lev4_subtoc.length > 0) {
+ foreach (st; obj.tags.lev4_subtoc) {
+ if (st.length > 0) {
+ output ~= ".lev4_subtoc: " ~ st;
+ }
+ }
+ }
+ /+ ↓ anchor tags +/
+ if (obj.tags.anchor_tags.length > 0) {
+ foreach (at; obj.tags.anchor_tags) {
+ if (at.length > 0) {
+ output ~= ".anchor_tag: " ~ at;
+ }
+ }
+ }
+ /+ ↓ text content +/
+ if (obj.text.length > 0) {
+ foreach (line; obj.text.split("\n")) {
+ output ~= "| " ~ line;
+ }
+ }
+ output ~= "";
+ return output;
+ }
+}
+
template spineAbstractionTxt() {
import std.conv : to;
import std.digest : toHexString;
@@ -58,6 +366,7 @@ template spineAbstractionTxt() {
import std.stdio;
import std.string;
import std.array;
+ mixin sspObjectRecord;
void spineAbstractionTxt(D)(D doc) {
auto doc_abstraction = doc.abstraction;
auto doc_matters = doc.matters;
@@ -195,307 +504,7 @@ template spineAbstractionTxt() {
output ~= "@" ~ section ~ " {";
output ~= "";
foreach (obj; section_objs) {
- /+ ↓ object declaration line. a heading carries its identifier here,
- on the declaration line, where it is not the ocn; every other
- object carries it as .identifier below, on the same condition
- +/
- string obj_decl = "[" ~ obj.metainfo.ocn.to!string ~ "] ";
- if (obj.metainfo.is_a == "heading") {
- string lev = obj.metainfo.marked_up_level;
- obj_decl ~= "heading :" ~ lev;
- if (obj.metainfo.identifier.length > 0
- && obj.metainfo.identifier != obj.metainfo.ocn.to!string) {
- obj_decl ~= " " ~ obj.metainfo.identifier;
- }
- } else {
- obj_decl ~= obj.metainfo.is_a;
- }
- output ~= obj_decl;
- /+ ↓ properties (only non-default values) +/
- if (obj.metainfo.is_a != "heading"
- && obj.metainfo.identifier.length > 0
- && obj.metainfo.identifier != obj.metainfo.ocn.to!string) {
- output ~= ".identifier: " ~ obj.metainfo.identifier;
- }
- if (obj.metainfo.is_of_part.length > 0) {
- output ~= ".part: " ~ obj.metainfo.is_of_part;
- }
- if (obj.metainfo.is_of_section.length > 0
- && obj.metainfo.is_of_section != section) {
- output ~= ".section: " ~ obj.metainfo.is_of_section;
- }
- /+ ↓ parent: emitted for every object except:
- - the document root, the one object that has no parent
- - toc entries, generated furniture rather than authored objects,
- whose place is given by their indent level; the toc heading
- itself does carry its parent (the document root)
- A parent ocn of 0 is not "no parent recorded", it is a parent
- heading the author has made non-substantive (ocn suppressed, or a
- dummy heading introduced to segment an output); it is structurally
- truthful and is stated explicitly rather than omitted so that the
- two cases remain distinguishable in the file
- +/
- if (!(obj.metainfo.is_a == "heading"
- && obj.metainfo.heading_lev_markup == 0)
- && obj.metainfo.is_a != "toc"
- ) {
- output ~= ".parent: " ~ obj.metainfo.parent_ocn.to!string;
- }
- if (obj.metainfo.last_descendant_ocn != 0) {
- output ~= ".last_descendant: " ~ obj.metainfo.last_descendant_ocn.to!string;
- }
- /+ ↓ child headings (from pre-computed map) +/
- if (obj.metainfo.children_headings.length > 0) {
- string[] ch;
- foreach (c; obj.metainfo.children_headings) {
- ch ~= c.to!string;
- }
- output ~= ".children: " ~ ch.join(" ");
- }
- /+ ↓ ancestors (only if non-zero) +/
- {
- bool has_anc = false;
- foreach (a; obj.metainfo.markedup_ancestors) {
- if (a != 0) { has_anc = true; break; }
- }
- if (has_anc) {
- string anc;
- foreach (i, a; obj.metainfo.markedup_ancestors) {
- if (i > 0) { anc ~= " "; }
- anc ~= a.to!string;
- }
- output ~= ".ancestors: " ~ anc;
- }
- }
- /+ ↓ collapsed ancestors (only if non-zero) +/
- {
- bool has_anc_c = false;
- foreach (a; obj.metainfo.collapsed_ancestors) {
- if (a != 0) { has_anc_c = true; break; }
- }
- if (has_anc_c) {
- string anc;
- foreach (i, a; obj.metainfo.collapsed_ancestors) {
- if (i > 0) anc ~= " ";
- anc ~= a.to!string;
- }
- output ~= ".ancestors_collapsed: " ~ anc;
- }
- }
- /+ ↓ dom structure status (only if non-zero) +/
- {
- bool has_dom = false;
- foreach (d; obj.metainfo.dom_structure_markedup_tags_status) {
- if (d != 0) { has_dom = true; break; }
- }
- if (has_dom) {
- string ds;
- foreach (i, d; obj.metainfo.dom_structure_markedup_tags_status) {
- if (i > 0) { ds ~= " "; }
- ds ~= d.to!string;
- }
- output ~= ".dom_status: " ~ ds;
- }
- }
- {
- bool has_dom_c = false;
- foreach (d; obj.metainfo.dom_structure_collapsed_tags_status) {
- if (d != 0) { has_dom_c = true; break; }
- }
- if (has_dom_c) {
- string ds;
- foreach (i, d; obj.metainfo.dom_structure_collapsed_tags_status) {
- if (i > 0) { ds ~= " "; }
- ds ~= d.to!string;
- }
- output ~= ".dom_status_collapsed: " ~ ds;
- }
- }
- if (obj.metainfo.heading_lev_collapsed < 9) {
- output ~= ".heading_lev_collapsed: " ~ obj.metainfo.heading_lev_collapsed.to!string;
- }
- if (obj.metainfo.parent_lev_markup != 0) {
- output ~= ".parent_lev: " ~ obj.metainfo.parent_lev_markup.to!string;
- }
- if (obj.metainfo.dummy_heading) {
- output ~= ".dummy: true";
- }
- if (obj.metainfo.object_number_off) {
- output ~= ".ocn_off: true";
- }
- // if (obj.metainfo.o_n_type != 0) {
- // output ~= ".o_n_type: " ~ obj.metainfo.o_n_type.to!string;
- // }
- if (obj.metainfo.is_of_type.length > 0) {
- output ~= ".is_of_type: " ~ obj.metainfo.is_of_type;
- }
- if (obj.metainfo.attrib.length > 0) {
- output ~= ".attrib: " ~ obj.metainfo.attrib;
- }
- if (obj.metainfo.lang.length > 0) {
- output ~= ".meta_lang: " ~ obj.metainfo.lang;
- }
- if (obj.metainfo.syntax.length > 0) {
- output ~= ".meta_syntax: " ~ obj.metainfo.syntax;
- }
- /+ ↓ sha256 digest +/
- {
- bool has_sha = false;
- foreach (b; obj.metainfo.sha256.text) {
- if (b != 0) { has_sha = true; break; }
- }
- if (has_sha) {
- if (doc_matters.opt.action.vox_gt_3) {
- writeln(obj.metainfo.sha256.summary);
- }
- output ~= ".sha256: " ~ obj.metainfo.sha256.text.toHexString.to!string;
- }
- }
- /+ ↓ images, repeatable record, one per image in order of appearance:
- .image: <filename> sha256:<HEX> bytes:<n> px:<width>x<height>
- .image: <filename> missing:true
- filename first (the object's handle on the image), remaining fields
- self-describing key:value tokens so that further fields can be
- added later without invalidating existing readers. px: is the
- file's own pixel size, not the display dimensions (w##h##) which
- the object text already carries
- +/
- if (obj.metainfo.sha256.images.length > 0) {
- foreach (i; obj.metainfo.sha256.images) {
- output ~= (i.fileMissing)
- ? ".image: " ~ i.fileName
- ~ " missing:true"
- : ".image: " ~ i.fileName
- ~ " sha256:" ~ i.fileHash_sha256.toHexString.to!string
- ~ " bytes:" ~ i.fileSize.to!string
- ~ ((i.imageWidth > 0 && i.imageHeight > 0)
- ? " px:" ~ i.imageWidth.to!string
- ~ "x" ~ i.imageHeight.to!string
- : "");
- }
- }
- /+ ↓ text attributes +/
- if (obj.attrib.indent_base != 0 || obj.attrib.indent_hang != 0) {
- output ~= ".indent: " ~ obj.attrib.indent_base.to!string
- ~ " " ~ obj.attrib.indent_hang.to!string;
- }
- if (obj.attrib.bullet) {
- output ~= ".bullet: true";
- }
- if (obj.attrib.language.length > 0) {
- output ~= ".lang: " ~ obj.attrib.language;
- }
- /+ ↓ has flags +/
- {
- string[] has_flags;
- if (obj.has.inline_links) { has_flags ~= "links"; }
- if (obj.has.inline_notes_reg) { has_flags ~= "notes_reg"; }
- if (obj.has.inline_notes_star) { has_flags ~= "notes_star"; }
- if (obj.has.images) { has_flags ~= "images"; }
- if (obj.has.image_without_dimensions) { has_flags ~= "images_no_dim"; }
- if (has_flags.length > 0) {
- output ~= ".has: " ~ has_flags.join(" ");
- }
- }
- /+ ↓ table properties +/
- if (obj.metainfo.is_a == "table" && obj.table.number_of_columns > 0) {
- output ~= ".table_cols: " ~ obj.table.number_of_columns.to!string;
- if (obj.table.column_widths.length > 0) {
- string[] ws;
- foreach (w; obj.table.column_widths) ws ~= w.to!string;
- output ~= ".table_widths: " ~ ws.join(" ");
- }
- if (obj.table.column_aligns.length > 0) {
- output ~= ".table_aligns: " ~ obj.table.column_aligns.join(" ");
- }
- if (obj.table.heading) {
- output ~= ".table_header: true";
- }
- // if (obj.table.walls) {
- // output ~= ".table_walls: true";
- // }
- }
- /+ ↓ code block properties +/
- if (obj.metainfo.is_a == "code") {
- // if (obj.code_block.syntax.length > 0) {
- // output ~= ".code_syntax: " ~ obj.code_block.syntax;
- // }
- if (obj.code_block.linenumbers) {
- output ~= ".code_linenumbers: true";
- }
- }
- /+ ↓ stow (extracted links) +/
- if (obj.stow.link.length > 0) {
- foreach (lnk; obj.stow.link) {
- if (lnk.length > 0) {
- output ~= ".stow_link: " ~ lnk;
- }
- }
- }
- /+ ↓ tag properties +/
- if (obj.tags.in_segment_html.length > 0) {
- output ~= ".segment: " ~ obj.tags.in_segment_html;
- }
- if (obj.tags.anchor_tag_html.length > 0
- && obj.tags.anchor_tag_html != obj.tags.in_segment_html) {
- output ~= ".anchor: " ~ obj.tags.anchor_tag_html;
- }
- if (obj.tags.segname_prev.length > 0) {
- output ~= ".segment_prev: " ~ obj.tags.segname_prev;
- }
- if (obj.tags.segname_next.length > 0) {
- output ~= ".segment_next: " ~ obj.tags.segname_next;
- }
- if (obj.tags.heading_lev_anchor_tag.length > 0) {
- output ~= ".heading_lev_anchor: " ~ obj.tags.heading_lev_anchor_tag;
- }
- if (obj.tags.segment_anchor_tag_epub.length > 0) {
- output ~= ".segment_epub: " ~ obj.tags.segment_anchor_tag_epub;
- }
- /+ ↓ the segment an object belongs to, as opposed to the segment a
- heading opens. .segment (in_segment_html) is set on headings
- only, so without these two a reader cannot tell which segment a
- paragraph is in; the sqlite search writer reads the html one
- +/
- if (obj.tags.html_segment_anchor_tag_is.length > 0) {
- output ~= ".segment_html_is: " ~ obj.tags.html_segment_anchor_tag_is;
- }
- if (obj.tags.epub_segment_anchor_tag_is.length > 0) {
- output ~= ".segment_epub_is: " ~ obj.tags.epub_segment_anchor_tag_is;
- }
- /+ ↓ heading ancestors text +/
- {
- bool has_hat = false;
- foreach (h; obj.tags.heading_ancestors_text) {
- if (h.length > 0) { has_hat = true; break; }
- }
- if (has_hat) {
- output ~= ".heading_ancestors_text: " ~ obj.tags.heading_ancestors_text.join("|");
- }
- }
- /+ ↓ lev4 subtoc +/
- if (obj.tags.lev4_subtoc.length > 0) {
- foreach (st; obj.tags.lev4_subtoc) {
- if (st.length > 0) {
- output ~= ".lev4_subtoc: " ~ st;
- }
- }
- }
- /+ ↓ anchor tags +/
- if (obj.tags.anchor_tags.length > 0) {
- foreach (at; obj.tags.anchor_tags) {
- if (at.length > 0) {
- output ~= ".anchor_tag: " ~ at;
- }
- }
- }
- /+ ↓ text content +/
- if (obj.text.length > 0) {
- foreach (line; obj.text.split("\n")) {
- output ~= "| " ~ line;
- }
- }
- output ~= "";
+ output ~= sspObjectRecord(obj, section, doc_matters.opt.action.vox_gt_3);
}
output ~= "}";
output ~= "";