aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-30 16:18:59 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-30 23:14:23 -0400
commit993a3f655359f98dc26d3d3222e07792e2ccddf7 (patch)
tree1633a8183b2affec4a2951c8357ab89e9073bd25 /src
parentsearch database: show all included notes (diff)
abstraction 2.1: a poem states its object range
- A poem is a container of verse, and stores its range of verse ocn, - its verse are the citable units with ocn. - A note in the last verse of a poem previously was not gathered into the endnotes section, this now is fixed Format 2.0 -> 2.1: the property is an addition, the poem is not a citable object (but contans a range of objects), its verse are (individual citable objects), and every reader checks the major version. A 2.0 database reads as having no ranges. (assisted by Claude-Code)
Diffstat (limited to 'src')
-rw-r--r--src/sisudoc/ocda/abstraction/db_in.d5
-rw-r--r--src/sisudoc/ocda/abstraction/doc_has.d12
-rw-r--r--src/sisudoc/ocda/abstraction/ssp.d17
-rw-r--r--src/sisudoc/ocda/abstraction/ssp_in.d10
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src.d50
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src_functions.d12
-rw-r--r--src/sisudoc/ocda/meta/metadoc_object_setter.d6
-rw-r--r--src/sisudoc/ocda/meta/ocn_align.d9
-rw-r--r--src/sisudoc/outputs/io_out/sqlite_ocda_db.d8
9 files changed, 106 insertions, 23 deletions
diff --git a/src/sisudoc/ocda/abstraction/db_in.d b/src/sisudoc/ocda/abstraction/db_in.d
index 986a09d..28d6423 100644
--- a/src/sisudoc/ocda/abstraction/db_in.d
+++ b/src/sisudoc/ocda/abstraction/db_in.d
@@ -347,6 +347,11 @@ template spineAbstractionDbRead() {
obj.metainfo.parent_ocn = row[_col["parent_ocn"]].as!int;
obj.metainfo.parent_lev_markup = row[_col["parent_lev"]].as!int;
obj.metainfo.last_descendant_ocn = row[_col["last_descendant_ocn"]].as!int;
+ /+ ↓ 2.1: a 2.0 database has no such columns, and reads as no range +/
+ if ("ocn_range_first" in _col) {
+ obj.metainfo.ocn_range_first = row[_col["ocn_range_first"]].as!int;
+ obj.metainfo.ocn_range_last = row[_col["ocn_range_last"]].as!int;
+ }
obj.metainfo.children_headings = _jsonInts(row[_col["children"]].as!string);
obj.metainfo.markedup_ancestors = _jsonEight(row[_col["ancestors"]].as!string);
obj.metainfo.collapsed_ancestors = _jsonEight(row[_col["ancestors_collapsed"]].as!string);
diff --git a/src/sisudoc/ocda/abstraction/doc_has.d b/src/sisudoc/ocda/abstraction/doc_has.d
index 4ecf018..f01af78 100644
--- a/src/sisudoc/ocda/abstraction/doc_has.d
+++ b/src/sisudoc/ocda/abstraction/doc_has.d
@@ -264,13 +264,11 @@ template spineDocHasFromAbstraction() {
_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.
+ /+ ↓ An object that is not written into a segment still carries an
+ identifier, and could share it: a poem block carried the ocn of
+ its last verse until spec:2.1, with an issue affecting epub output.
+ Since spec:2.1 the poem has no identifier; the guard stays, for any
+ object that is not in a segment.
+/
if (obj.tags.epub_segment_anchor_tag_is.length > 0) {
_tag_assoc[obj.metainfo.identifier]["seg_lv1to4"]
diff --git a/src/sisudoc/ocda/abstraction/ssp.d b/src/sisudoc/ocda/abstraction/ssp.d
index c22741b..dfb8c10 100644
--- a/src/sisudoc/ocda/abstraction/ssp.d
+++ b/src/sisudoc/ocda/abstraction/ssp.d
@@ -59,6 +59,16 @@ module sisudoc.ocda.abstraction.ssp;
part when a property is added (a reader of the newer format still reads
the older one), the major part when one changes meaning or goes away.
+ 2.1 2026-09-29 .ocn_range, the first and last ocn of what a
+ container holds. A poem is a container of verses and
+ is cited as their range: it now opens its run of
+ verses rather than closing it, carries ocn 0 and no
+ identifier, and states the range. It used to carry
+ the last verse's ocn and identifier, so one number
+ named two objects. A 2.0 reader reads a 2.1 file:
+ the poem was never rendered and has no text, the
+ verses are unchanged, and the property is an
+ addition.
2.0 2026-09-18 source.digest changes meaning: it now covers every
markup file the document is made of, where it was the
sha256 of the master file alone. For a .sst that was
@@ -74,7 +84,7 @@ module sisudoc.ocda.abstraction.ssp;
reference to a heading above level 4 lands on;
was 0.1
+/
-enum ssp_format_version = "2.0";
+enum ssp_format_version = "2.1";
enum ssp_format_version_major = "2";
/+ ↓ escaping for values that are folded onto one line
@@ -181,6 +191,11 @@ template sspObjectRecord() {
if (obj.metainfo.last_descendant_ocn != 0) {
output ~= ".last_descendant: " ~ obj.metainfo.last_descendant_ocn.to!string;
}
+ /+ ↓ a container: the first and last ocn of what it holds (2.1) +/
+ if (obj.metainfo.ocn_range_first != 0) {
+ output ~= ".ocn_range: " ~ obj.metainfo.ocn_range_first.to!string
+ ~ " " ~ obj.metainfo.ocn_range_last.to!string;
+ }
/+ ↓ child headings (from pre-computed map) +/
if (obj.metainfo.children_headings.length > 0) {
string[] ch;
diff --git a/src/sisudoc/ocda/abstraction/ssp_in.d b/src/sisudoc/ocda/abstraction/ssp_in.d
index 0e461ad..0c67ab2 100644
--- a/src/sisudoc/ocda/abstraction/ssp_in.d
+++ b/src/sisudoc/ocda/abstraction/ssp_in.d
@@ -76,7 +76,7 @@ template spineAbstractionRead() {
object sections in the order the file gives them
+/
struct SSPdocument {
- string format; // "% SiSU Document Abstraction v2.0"
+ string format; // "% SiSU Document Abstraction v2.1"
string source; // "% Source: ..."
string[string] source_info; // the @source block
string[] source_info_order;
@@ -230,6 +230,14 @@ template spineAbstractionRead() {
case "section": obj.metainfo.is_of_section = _val; break;
case "parent": obj.metainfo.parent_ocn = _val.to!int; break;
case "last_descendant": obj.metainfo.last_descendant_ocn = _val.to!int; break;
+ case "ocn_range": {
+ auto _r = _ints(_val);
+ if (_r.length == 2) {
+ obj.metainfo.ocn_range_first = _r[0];
+ obj.metainfo.ocn_range_last = _r[1];
+ }
+ break;
+ }
case "children": obj.metainfo.children_headings = _ints(_val); break;
case "ancestors":
obj.metainfo.markedup_ancestors = _eight(_val);
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src.d b/src/sisudoc/ocda/meta/metadoc_from_src.d
index 3670a39..cbd25e2 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src.d
@@ -148,6 +148,8 @@ template docAbstraction() {
"start" : "",
"end" : ""
];
+ /+ ↓ where the open poem's first verse will go, -1 when no poem is open +/
+ long _poem_open_idx = -1;
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,];
@@ -426,6 +428,7 @@ template docAbstraction() {
object_reset(an_object);
processing.remove("verse");
object_number_poem["start"] = obj_cite_digits.object_number.to!string;
+ _poem_open_idx = the_document_body_section.length.to!long;
}
{
ST_txt_by_line_block_start _get = line.txt_by_line_block_start(pith, dochas, object_number_poem);
@@ -602,6 +605,46 @@ template docAbstraction() {
pith = _get.pith;
}
}
+ /+ ↓ poem closed: poem object has been appended after its last verse.
+ It is a container, not a citable unit - its verses are - so it
+ carries no ocn of its own and states the range of its verses
+ instead, and it goes to the head of them, so that a writer
+ meeting it knows what follows and where it ends, and can wrap the
+ verses in one pass. (an issue with a note in the last verse of a
+ poem has been addressed)
+ +/
+ if (_poem_open_idx >= 0
+ && the_document_body_section.length > _poem_open_idx + 1
+ && the_document_body_section[$-1].metainfo.is_a == "poem"
+ ) {
+ size_t _s = _poem_open_idx.to!size_t;
+ size_t _p = the_document_body_section.length - 1;
+ size_t _first = _s;
+ while (_first < _p && the_document_body_section[_first].metainfo.is_a != "verse") { ++_first; }
+ size_t _last = _p;
+ while (_last > _s && the_document_body_section[_last - 1].metainfo.is_a != "verse") { --_last; }
+ if (_first < _p && _last > _s) {
+ --_last;
+ if ((the_document_body_section[_last].text).match(rgx.inline_notes_al_all_note)) {
+ object_notes = note_section.gather_notes_for_endnote_section(
+ the_document_body_section,
+ tag_in_seg,
+ (_last).to!int,
+ );
+ }
+ auto _poem = the_document_body_section[_p];
+ _poem.metainfo.ocn_range_first = the_document_body_section[_first].metainfo.ocn;
+ _poem.metainfo.ocn_range_last = the_document_body_section[_last].metainfo.ocn;
+ _poem.tags.html_segment_anchor_tag_is = the_document_body_section[_first].tags.html_segment_anchor_tag_is;
+ _poem.tags.epub_segment_anchor_tag_is = the_document_body_section[_first].tags.epub_segment_anchor_tag_is;
+ the_document_body_section
+ = the_document_body_section[0 .. _s]
+ ~ _poem
+ ~ the_document_body_section[_s .. _p];
+ previous_length = the_document_body_section.length.to!int;
+ }
+ _poem_open_idx = -1;
+ }
} else {
// line.empty, post contents, empty variables:
assert(
@@ -1031,12 +1074,13 @@ template docAbstraction() {
= 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.
+ identifier, and could share it: a poem block carried the ocn of its last
+ verse until 2.1, and the verses are written where the block 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.
+ showed in the epub and not in the html. Since 2.1 the poem has no
+ identifier; the guard stays, for any object that is not in a segment.
+/
if (!(obj.tags.epub_segment_anchor_tag_is.empty)) {
tag_assoc[(obj.metainfo.identifier)]["seg_lv1to4"]
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
index 922f389..fc12087 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
@@ -2163,11 +2163,13 @@ template docAbstractionFunctions() {
heading_ptr-1,
an_object["is"]
);
- comp_obj_poem_ocn = set_object_generic("body", "body", "block", "poem", "", obj_cite_digits.object_number);
- comp_obj_poem_ocn.metainfo.identifier = obj_cite_digits.identifier;
- comp_obj_poem_ocn.metainfo.object_number_off = obj_cite_digits.off;
- comp_obj_poem_ocn.metainfo.o_n_book_index = obj_cite_digits.bkidx;
- comp_obj_poem_ocn.metainfo.object_number_type = obj_cite_digits.type;
+ /+ ↓ the poem: a container of the verses just written, not a citable unit
+ on its own, it now states the range of its verses
+ (ocn_range_first/_last), (it previously carried the last verse's ocn
+ and identifier, one number occurring on two objects)
+ +/
+ comp_obj_poem_ocn = set_object_generic("body", "body", "block", "poem", "", 0);
+ comp_obj_poem_ocn.metainfo.identifier = "";
the_document_body_section ~= comp_obj_poem_ocn;
pith["block_is"] = eN.blk_is.poem;
pith["block_state"] = eN.blk_state.off;
diff --git a/src/sisudoc/ocda/meta/metadoc_object_setter.d b/src/sisudoc/ocda/meta/metadoc_object_setter.d
index 6c090db..eff01a2 100644
--- a/src/sisudoc/ocda/meta/metadoc_object_setter.d
+++ b/src/sisudoc/ocda/meta/metadoc_object_setter.d
@@ -230,6 +230,12 @@ template ObjectSetter() {
int parent_lev_markup = 0;
int parent_ocn = 0;
int last_descendant_ocn = 0;
+ /+ ↓ the objects a container holds, as the first and last of their ocns: a
+ poem is not itself a citable object, its verses are, and it is cited as
+ the range of citable objects, ocn ("173-180"). 0 where not a container.
+ +/
+ int ocn_range_first = 0;
+ int ocn_range_last = 0;
int[] children_headings;
SHA_256_Hashes_ sha256;
}
diff --git a/src/sisudoc/ocda/meta/ocn_align.d b/src/sisudoc/ocda/meta/ocn_align.d
index 9f9fdaf..e93f12a 100644
--- a/src/sisudoc/ocda/meta/ocn_align.d
+++ b/src/sisudoc/ocda/meta/ocn_align.d
@@ -163,10 +163,11 @@ template spineOcnAlign() {
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.
+ /+ ↓ first in document order wins. Until format spec:2.1 a (verse) ocn
+ was not unique: a poem carried the ocn of its last verse, so the
+ same number named two objects. A poem now carries 0 and is passed
+ over above; the rule stays, and costs nothing, for an artefact
+ written before spec:2.1.
+/
if (_ocn !in _p.kinds) { _p.kinds[_ocn] = _kindOf(obj); }
_p.ocn_count += 1;
diff --git a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
index f312d91..561d89c 100644
--- a/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
+++ b/src/sisudoc/outputs/io_out/sqlite_ocda_db.d
@@ -182,6 +182,8 @@ template spineAbstractionDb() {
parent_ocn INTEGER DEFAULT 0,
parent_lev INTEGER DEFAULT 0,
last_descendant_ocn INTEGER DEFAULT 0,
+ ocn_range_first INTEGER DEFAULT 0,
+ ocn_range_last INTEGER DEFAULT 0,
children TEXT
CHECK(children IS NULL OR json_valid(children)),
ancestors TEXT
@@ -436,7 +438,7 @@ template spineAbstractionDb() {
"INSERT INTO objects ("
~ "doc_id, section, seq, ocn, is_a, is_of_part, is_of_section, is_of_type,"
~ "heading_level, heading_lev_collapsed, identifier,"
- ~ "parent_ocn, parent_lev, last_descendant_ocn,"
+ ~ "parent_ocn, parent_lev, last_descendant_ocn, ocn_range_first, ocn_range_last,"
~ "children, ancestors, ancestors_collapsed,"
~ "dom_status, dom_status_collapsed,"
~ "dummy_heading, object_number_off,"
@@ -451,7 +453,7 @@ template spineAbstractionDb() {
~ ") VALUES ("
~ ":doc_id, :section, :seq, :ocn, :is_a, :is_of_part, :is_of_section, :is_of_type,"
~ ":heading_level, :heading_lev_collapsed, :identifier,"
- ~ ":parent_ocn, :parent_lev, :last_descendant_ocn,"
+ ~ ":parent_ocn, :parent_lev, :last_descendant_ocn, :ocn_range_first, :ocn_range_last,"
~ ":children, :ancestors, :ancestors_collapsed,"
~ ":dom_status, :dom_status_collapsed,"
~ ":dummy_heading, :object_number_off,"
@@ -560,6 +562,8 @@ template spineAbstractionDb() {
obj_stmt.bind(":parent_ocn", obj.metainfo.parent_ocn);
obj_stmt.bind(":parent_lev", obj.metainfo.parent_lev_markup);
obj_stmt.bind(":last_descendant_ocn", obj.metainfo.last_descendant_ocn);
+ obj_stmt.bind(":ocn_range_first", obj.metainfo.ocn_range_first);
+ obj_stmt.bind(":ocn_range_last", obj.metainfo.ocn_range_last);
bindJsonInts(":children", obj.metainfo.children_headings);
bindJsonInts(":ancestors", obj.metainfo.markedup_ancestors);