diff options
| -rw-r--r-- | org/ocda.org | 1 | ||||
| -rw-r--r-- | org/ocda_get.org | 277 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/doc_has.d | 269 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/package.d | 1 |
4 files changed, 548 insertions, 0 deletions
diff --git a/org/ocda.org b/org/ocda.org index 1fd3cb1..f58e955 100644 --- a/org/ocda.org +++ b/org/ocda.org @@ -96,6 +96,7 @@ 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) #+END_SRC ** _module template_ :module:metadoc_from_src: diff --git a/org/ocda_get.org b/org/ocda_get.org new file mode 100644 index 0000000..7ea9260 --- /dev/null +++ b/org/ocda_get.org @@ -0,0 +1,277 @@ +-*- mode: org -*- +#+TITLE: sisudoc spine (doc_reform) object-centric document abstraction +#+DESCRIPTION: documents - structuring, publishing in multiple formats & search +#+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/]] + +* Get OCD + +get abstracted objects for downstream processing + +** _module template_ + +#+HEADER: :tangle "../src/sisudoc/ocda/abstraction/doc_has.d" +#+HEADER: :noweb yes +#+BEGIN_SRC d +<<doc_header_including_copyright_and_license>> +module sisudoc.ocda.abstraction.doc_has; +@safe: +/+ ↓ ST_DocHas from a loaded abstraction + + the parser builds ST_DocHas as it goes, with the markup in front of it. + A document loaded from a .ssp or a .ocda.db has to arrive at the same + value from what the artefact carries, and this is where that is done. + + Every one of these is an *index over properties the artefact already + holds*, not a re-derivation of something it does not: + + imagelist the .image records' filenames + segnames_lv4 .anchor of each body heading at level 4 + segnames_lv_0_to_4 .segment_epub of each heading at level 4 or less + tag_associations .anchor, .anchor_tag and .segment_* per object + section_keys_sequenced which sections are non-empty, plus the run's + own output flags + + That distinction is the line the .ssp skill draws: assembling an index + is fine and cannot drift, because a wrong index would not survive the + round trip that produced its inputs; recomputing dom_status or ancestors + would be re-derivation and is exactly what ocda exists to remove. If a + value is not in the file and cannot be indexed out of what is, it must + be added to the artefact instead of reconstructed here. + + The counts come from the @doc_has block rather than being recounted, for + the same reason. ++/ +template spineDocHasFromAbstraction() { + import std.algorithm : sort, uniq; + import std.array; + import std.conv : to; + import std.json : JSONValue; + import std.regex; + import sisudoc.ocda.meta.metadoc_object_setter; + import sisudoc.ocda.meta.rgx; + mixin ObjectSetter; + mixin spineRgxIn; + /+ ↓ the sections, in the order a document holds them +/ + enum string[] doc_sections = [ + "head", "toc", "body", "endnotes", + "glossary", "bibliography", "bookindex", "blurb", "tail", + ]; + /+ ↓ the tail sections that become segments of their own, in the order + after_doc_determine_segnames appends them +/ + enum string[] tail_sections = [ + "endnotes", "glossary", "bibliography", "bookindex", "blurb", + ]; + private uint _count(string[string] _doc_has, string _key) { + if (auto _v = _key in _doc_has) { + try { return (*_v).to!uint; } catch (Exception ex) { return 0; } + } + return 0; + } + private bool _has_section(A)(A _abst, string _section) { + if (auto _s = _section in _abst) { return (*_s).length > 1; } + return false; + } + /+ ↓ which document sections each output format walks, and in what order. + + the same rule the parser applies: the three that are always there, + then each tail section that the document actually has, then "tail" + for the formats that close with one. It depends on the run as well as + on the document, since only html and epub take the tail. + +/ + string[][string] sectionKeysSequenced(A,O)(A _abst, O _opt_action) { + string[][string] _keys = [ + "scroll": ["head", "toc", "body",], + "seg": ["head", "toc", "body",], + "sql": ["head", "body",], + "latex": ["head", "toc", "body",], + "text": ["head", "toc", "body",], + ]; + if (_has_section(_abst, "endnotes")) { + _keys["scroll"] ~= "endnotes"; + _keys["seg"] ~= "endnotes"; + _keys["latex"] ~= "endnotes"; + _keys["text"] ~= "endnotes"; + } + if (_has_section(_abst, "glossary")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "glossary"; } + } + if (_has_section(_abst, "bibliography")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "bibliography"; } + } + if (_has_section(_abst, "bookindex")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "bookindex"; } + } + if (_has_section(_abst, "blurb")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "blurb"; } + } + if (_opt_action.html_scroll || _opt_action.html_seg || _opt_action.epub) { + _keys["scroll"] ~= "tail"; + _keys["seg"] ~= "tail"; + } + return _keys; + } + /+ ↓ the whole of it +/ + ST_DocHas docHasFromAbstraction(A,O)( + A _abst, + string[string] _doc_has_block, + O _opt_action, + ) { + static auto rgx = RgxI(); + ST_DocHas _out; + _out.inline_links = _count(_doc_has_block, "inline_links"); + _out.inline_notes_reg = _count(_doc_has_block, "inline_notes_reg"); + _out.inline_notes_star = _count(_doc_has_block, "inline_notes_star"); + _out.codeblocks = _count(_doc_has_block, "codeblocks"); + _out.tables = _count(_doc_has_block, "tables"); + _out.blocks = _count(_doc_has_block, "blocks"); + _out.groups = _count(_doc_has_block, "groups"); + _out.poems = _count(_doc_has_block, "poems"); + _out.quotes = _count(_doc_has_block, "quotes"); + string[] _images; + string[string][string] _tag_assoc; + /+ ↓ keys waiting for the level 4 segment that will hold them. + + a heading above level 4 opens no html segment, so a cross reference + to it has to land on the next level 4 heading, which has not been + seen yet when the heading is read. The parser does this the same + way, back-filling a pending tag when the level 4 heading arrives + (metadoc_from_src.d, "tag_assoc[lv0_to_lv3_html_tag]"), and the + order of the objects is what both are reading it from. + +/ + string[] _awaiting_lv4; + void _backfill(string _seg) { + foreach (_k; _awaiting_lv4) { _tag_assoc[_k]["seg_lv4"] = _seg; } + _awaiting_lv4 = []; + } + /+ ↓ segnames["html"] is seeded with the toc, which is a segment of its + own before any level 4 heading opens one +/ + _out.segnames_lv4 ~= "toc"; + foreach (_section; doc_sections) { + if (_section !in _abst) { continue; } + /+ ↓ head is always walked; every other section only when it holds + more than the placeholder object, which is the guard the parser + puts on each of its section loops + +/ + if (_section != "head" && !_has_section(_abst, _section)) { continue; } + foreach (obj; _abst[_section]) { + /+ ↓ the images this object names, as the abstraction recorded them +/ + foreach (_img; obj.metainfo.sha256.images) { _images ~= _img.fileName; } + /+ ↓ every object: its html anchor is in a segment, and its epub + segment anchor stands for itself +/ + if (obj.tags.anchor_tag_html.length > 0) { + _tag_assoc[obj.tags.anchor_tag_html]["seg_lv4"] = obj.tags.in_segment_html; + } + if (obj.tags.segment_anchor_tag_epub.length > 0) { + _tag_assoc[obj.tags.segment_anchor_tag_epub]["seg_lv1to4"] + = obj.tags.segment_anchor_tag_epub; + } + /+ ↓ the body's objects are what a citation or a cross reference + points at, so each one's identifier maps to the segment it is + in. seg_lv4 is only set where it is not already, since an + anchor of the same name set above names a segment rather than + sits in one + +/ + if (_section == "body" && obj.metainfo.identifier.length > 0) { + 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; + } + _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } + /+ ↓ a heading's own anchor tags, and the anchors declared inline in + an object's text, both point at the segment the object is in +/ + /+ ↓ a heading above level 4 opens no html segment of its own, so a + cross reference to it has to land on the level 4 segment that + follows it. .segment is exactly that (the segment a heading + opens or falls into), where .segment_html_is is the segment an + object sits in; epub segments go down to level 1, so there + .segment_epub stands for the heading itself + +/ + if (obj.tags.heading_lev_anchor_tag.length > 0 + && obj.tags.in_segment_html.length > 0 + ) { + _tag_assoc[obj.tags.heading_lev_anchor_tag]["seg_lv4"] + = obj.tags.in_segment_html; + if (obj.tags.segment_anchor_tag_epub.length > 0) { + _tag_assoc[obj.tags.heading_lev_anchor_tag]["seg_lv1to4"] + = obj.tags.segment_anchor_tag_epub; + } + } + foreach (m; obj.text.matchAll(rgx.inline_link_anchor)) { + string _a = m["anchor"].to!string; + if (_a.length == 0 || _a in _tag_assoc) { continue; } + _tag_assoc[_a]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; + _tag_assoc[_a]["seg_lv1to4"] = obj.tags.epub_segment_anchor_tag_is; + } + /+ ↓ the segment name lists, in document order +/ + if (obj.metainfo.is_a == "heading") { + if (obj.metainfo.heading_lev_markup <= 4) { + _out.segnames_lv_0_to_4 ~= obj.tags.segment_anchor_tag_epub; + } + if (obj.metainfo.heading_lev_markup == 4) { + if (_section == "body") { _out.segnames_lv4 ~= obj.tags.anchor_tag_html; } + /+ ↓ this is the segment the headings above it were waiting for +/ + _backfill(obj.tags.in_segment_html); + } else if (obj.metainfo.heading_lev_markup < 4) { + foreach (_k; [obj.metainfo.identifier, obj.tags.heading_lev_anchor_tag, + obj.tags.anchor_tag_html]) { + if (_k.length > 0) { _awaiting_lv4 ~= _k; } + } + } + } + } + } + foreach (_section; tail_sections) { + if (_has_section(_abst, _section)) { _out.segnames_lv4 ~= _section; } + } + _out.imagelist = _images.sort.uniq.array; + _out.tag_associations = _tag_assoc; + _out.section_keys_sequenced = sectionKeysSequenced(_abst, _opt_action); + 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__ diff --git a/src/sisudoc/ocda/abstraction/doc_has.d b/src/sisudoc/ocda/abstraction/doc_has.d new file mode 100644 index 0000000..75a4640 --- /dev/null +++ b/src/sisudoc/ocda/abstraction/doc_has.d @@ -0,0 +1,269 @@ +/+ +- Name: SisuDoc Spine, Doc Reform [a part of] + - Description: documents, structuring, processing, publishing, search + - static content generator + + - Author: Ralph Amissah + [ralph.amissah@gmail.com] + + - Copyright: (C) 2015 (continuously updated, current 2026) Ralph Amissah, All Rights Reserved. + + - License: AGPL 3 or later: + + Spine (SiSU), a framework for document structuring, publishing and + search + + Copyright (C) Ralph Amissah + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU AFERO General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along with + this program. If not, see [https://www.gnu.org/licenses/]. + + If you have Internet connection, the latest version of the AGPL should be + available at these locations: + [https://www.fsf.org/licensing/licenses/agpl.html] + [https://www.gnu.org/licenses/agpl.html] + + - Spine (by Doc Reform, related to SiSU) uses standard: + - docReform markup syntax + - standard SiSU markup syntax with modified headers and minor modifications + - docReform object numbering + - standard SiSU object citation numbering & system + + - Homepages: + [https://www.sisudoc.org] + [https://www.doc-reform.org] + + - Git + [https://git.sisudoc.org/] + ++/ +module sisudoc.ocda.abstraction.doc_has; +@safe: +/+ ↓ ST_DocHas from a loaded abstraction + + the parser builds ST_DocHas as it goes, with the markup in front of it. + A document loaded from a .ssp or a .ocda.db has to arrive at the same + value from what the artefact carries, and this is where that is done. + + Every one of these is an *index over properties the artefact already + holds*, not a re-derivation of something it does not: + + imagelist the .image records' filenames + segnames_lv4 .anchor of each body heading at level 4 + segnames_lv_0_to_4 .segment_epub of each heading at level 4 or less + tag_associations .anchor, .anchor_tag and .segment_* per object + section_keys_sequenced which sections are non-empty, plus the run's + own output flags + + That distinction is the line the .ssp skill draws: assembling an index + is fine and cannot drift, because a wrong index would not survive the + round trip that produced its inputs; recomputing dom_status or ancestors + would be re-derivation and is exactly what ocda exists to remove. If a + value is not in the file and cannot be indexed out of what is, it must + be added to the artefact instead of reconstructed here. + + The counts come from the @doc_has block rather than being recounted, for + the same reason. ++/ +template spineDocHasFromAbstraction() { + import std.algorithm : sort, uniq; + import std.array; + import std.conv : to; + import std.json : JSONValue; + import std.regex; + import sisudoc.ocda.meta.metadoc_object_setter; + import sisudoc.ocda.meta.rgx; + mixin ObjectSetter; + mixin spineRgxIn; + /+ ↓ the sections, in the order a document holds them +/ + enum string[] doc_sections = [ + "head", "toc", "body", "endnotes", + "glossary", "bibliography", "bookindex", "blurb", "tail", + ]; + /+ ↓ the tail sections that become segments of their own, in the order + after_doc_determine_segnames appends them +/ + enum string[] tail_sections = [ + "endnotes", "glossary", "bibliography", "bookindex", "blurb", + ]; + private uint _count(string[string] _doc_has, string _key) { + if (auto _v = _key in _doc_has) { + try { return (*_v).to!uint; } catch (Exception ex) { return 0; } + } + return 0; + } + private bool _has_section(A)(A _abst, string _section) { + if (auto _s = _section in _abst) { return (*_s).length > 1; } + return false; + } + /+ ↓ which document sections each output format walks, and in what order. + + the same rule the parser applies: the three that are always there, + then each tail section that the document actually has, then "tail" + for the formats that close with one. It depends on the run as well as + on the document, since only html and epub take the tail. + +/ + string[][string] sectionKeysSequenced(A,O)(A _abst, O _opt_action) { + string[][string] _keys = [ + "scroll": ["head", "toc", "body",], + "seg": ["head", "toc", "body",], + "sql": ["head", "body",], + "latex": ["head", "toc", "body",], + "text": ["head", "toc", "body",], + ]; + if (_has_section(_abst, "endnotes")) { + _keys["scroll"] ~= "endnotes"; + _keys["seg"] ~= "endnotes"; + _keys["latex"] ~= "endnotes"; + _keys["text"] ~= "endnotes"; + } + if (_has_section(_abst, "glossary")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "glossary"; } + } + if (_has_section(_abst, "bibliography")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "bibliography"; } + } + if (_has_section(_abst, "bookindex")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "bookindex"; } + } + if (_has_section(_abst, "blurb")) { + foreach (_k; ["scroll", "seg", "sql", "latex", "text"]) { _keys[_k] ~= "blurb"; } + } + if (_opt_action.html_scroll || _opt_action.html_seg || _opt_action.epub) { + _keys["scroll"] ~= "tail"; + _keys["seg"] ~= "tail"; + } + return _keys; + } + /+ ↓ the whole of it +/ + ST_DocHas docHasFromAbstraction(A,O)( + A _abst, + string[string] _doc_has_block, + O _opt_action, + ) { + static auto rgx = RgxI(); + ST_DocHas _out; + _out.inline_links = _count(_doc_has_block, "inline_links"); + _out.inline_notes_reg = _count(_doc_has_block, "inline_notes_reg"); + _out.inline_notes_star = _count(_doc_has_block, "inline_notes_star"); + _out.codeblocks = _count(_doc_has_block, "codeblocks"); + _out.tables = _count(_doc_has_block, "tables"); + _out.blocks = _count(_doc_has_block, "blocks"); + _out.groups = _count(_doc_has_block, "groups"); + _out.poems = _count(_doc_has_block, "poems"); + _out.quotes = _count(_doc_has_block, "quotes"); + string[] _images; + string[string][string] _tag_assoc; + /+ ↓ keys waiting for the level 4 segment that will hold them. + + a heading above level 4 opens no html segment, so a cross reference + to it has to land on the next level 4 heading, which has not been + seen yet when the heading is read. The parser does this the same + way, back-filling a pending tag when the level 4 heading arrives + (metadoc_from_src.d, "tag_assoc[lv0_to_lv3_html_tag]"), and the + order of the objects is what both are reading it from. + +/ + string[] _awaiting_lv4; + void _backfill(string _seg) { + foreach (_k; _awaiting_lv4) { _tag_assoc[_k]["seg_lv4"] = _seg; } + _awaiting_lv4 = []; + } + /+ ↓ segnames["html"] is seeded with the toc, which is a segment of its + own before any level 4 heading opens one +/ + _out.segnames_lv4 ~= "toc"; + foreach (_section; doc_sections) { + if (_section !in _abst) { continue; } + /+ ↓ head is always walked; every other section only when it holds + more than the placeholder object, which is the guard the parser + puts on each of its section loops + +/ + if (_section != "head" && !_has_section(_abst, _section)) { continue; } + foreach (obj; _abst[_section]) { + /+ ↓ the images this object names, as the abstraction recorded them +/ + foreach (_img; obj.metainfo.sha256.images) { _images ~= _img.fileName; } + /+ ↓ every object: its html anchor is in a segment, and its epub + segment anchor stands for itself +/ + if (obj.tags.anchor_tag_html.length > 0) { + _tag_assoc[obj.tags.anchor_tag_html]["seg_lv4"] = obj.tags.in_segment_html; + } + if (obj.tags.segment_anchor_tag_epub.length > 0) { + _tag_assoc[obj.tags.segment_anchor_tag_epub]["seg_lv1to4"] + = obj.tags.segment_anchor_tag_epub; + } + /+ ↓ the body's objects are what a citation or a cross reference + points at, so each one's identifier maps to the segment it is + in. seg_lv4 is only set where it is not already, since an + anchor of the same name set above names a segment rather than + sits in one + +/ + if (_section == "body" && obj.metainfo.identifier.length > 0) { + 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; + } + _tag_assoc[obj.metainfo.identifier]["seg_lv1to4"] + = obj.tags.epub_segment_anchor_tag_is; + } + /+ ↓ a heading's own anchor tags, and the anchors declared inline in + an object's text, both point at the segment the object is in +/ + /+ ↓ a heading above level 4 opens no html segment of its own, so a + cross reference to it has to land on the level 4 segment that + follows it. .segment is exactly that (the segment a heading + opens or falls into), where .segment_html_is is the segment an + object sits in; epub segments go down to level 1, so there + .segment_epub stands for the heading itself + +/ + if (obj.tags.heading_lev_anchor_tag.length > 0 + && obj.tags.in_segment_html.length > 0 + ) { + _tag_assoc[obj.tags.heading_lev_anchor_tag]["seg_lv4"] + = obj.tags.in_segment_html; + if (obj.tags.segment_anchor_tag_epub.length > 0) { + _tag_assoc[obj.tags.heading_lev_anchor_tag]["seg_lv1to4"] + = obj.tags.segment_anchor_tag_epub; + } + } + foreach (m; obj.text.matchAll(rgx.inline_link_anchor)) { + string _a = m["anchor"].to!string; + if (_a.length == 0 || _a in _tag_assoc) { continue; } + _tag_assoc[_a]["seg_lv4"] = obj.tags.html_segment_anchor_tag_is; + _tag_assoc[_a]["seg_lv1to4"] = obj.tags.epub_segment_anchor_tag_is; + } + /+ ↓ the segment name lists, in document order +/ + if (obj.metainfo.is_a == "heading") { + if (obj.metainfo.heading_lev_markup <= 4) { + _out.segnames_lv_0_to_4 ~= obj.tags.segment_anchor_tag_epub; + } + if (obj.metainfo.heading_lev_markup == 4) { + if (_section == "body") { _out.segnames_lv4 ~= obj.tags.anchor_tag_html; } + /+ ↓ this is the segment the headings above it were waiting for +/ + _backfill(obj.tags.in_segment_html); + } else if (obj.metainfo.heading_lev_markup < 4) { + foreach (_k; [obj.metainfo.identifier, obj.tags.heading_lev_anchor_tag, + obj.tags.anchor_tag_html]) { + if (_k.length > 0) { _awaiting_lv4 ~= _k; } + } + } + } + } + } + foreach (_section; tail_sections) { + if (_has_section(_abst, _section)) { _out.segnames_lv4 ~= _section; } + } + _out.imagelist = _images.sort.uniq.array; + _out.tag_associations = _tag_assoc; + _out.section_keys_sequenced = sectionKeysSequenced(_abst, _opt_action); + return _out; + } +} diff --git a/src/sisudoc/ocda/abstraction/package.d b/src/sisudoc/ocda/abstraction/package.d index 4131a90..05897f6 100644 --- a/src/sisudoc/ocda/abstraction/package.d +++ b/src/sisudoc/ocda/abstraction/package.d @@ -87,3 +87,4 @@ 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) |
