aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--org/in_source_artefact.org284
-rw-r--r--org/in_source_files.org12
-rw-r--r--org/meta_conf_make_meta.org33
-rw-r--r--org/ocda.org1
-rw-r--r--org/ocda_get.org43
-rw-r--r--org/ocda_obj_setter.org64
-rw-r--r--org/spine.org54
-rw-r--r--src/sisudoc/ocda/abstraction/db_in.d8
-rw-r--r--src/sisudoc/ocda/abstraction/doc_from_artefact.d279
-rw-r--r--src/sisudoc/ocda/abstraction/doc_has.d43
-rw-r--r--src/sisudoc/ocda/abstraction/package.d1
-rw-r--r--src/sisudoc/ocda/abstraction/ssp_in.d4
-rw-r--r--src/sisudoc/ocda/meta/conf_make_meta_yaml.d33
-rw-r--r--src/sisudoc/ocda/meta/topic_register.d105
-rw-r--r--src/sisudoc/spine.d54
15 files changed, 924 insertions, 94 deletions
diff --git a/org/in_source_artefact.org b/org/in_source_artefact.org
new file mode 100644
index 0000000..0f7077d
--- /dev/null
+++ b/org/in_source_artefact.org
@@ -0,0 +1,284 @@
+-*- mode: org -*-
+#+TITLE: sisudoc spine (doc_reform) markup source raw
+#+DESCRIPTION: documents - structuring, publishing in multiple formats & search
+#+FILETAGS: :spine:sourcefile:read:
+#+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/]]
+
+* A. get doc from artefact
+** _module template_ :module:config_files:
+
+#+HEADER: :tangle "../src/sisudoc/ocda/abstraction/doc_from_artefact.d"
+#+HEADER: :noweb yes
+#+BEGIN_SRC d
+<<doc_header_including_copyright_and_license>>
+module sisudoc.ocda.abstraction.doc_from_artefact;
+@safe:
+/+ ↓ a document, from an artefact rather than from markup
+
+ spineAbstraction() parses markup and hands the output writers a doc:
+ an abstraction and a doc_matters. This does the same from a .ssp or a
+ .ocda.db, filling the same docMattersMake() arguments, so that what
+ comes out is the value the writers already take and they never learn
+ which of the two made it.
+
+ Three of the seven arguments come from the run and are the same either
+ way: program_info, opt_action, and the conf half of conf_make_meta,
+ which is site and run scoped (urls, papersize, the search database) and
+ has no business in a document's own artefact. The other four come from
+ the artefact:
+
+ conf_make_meta meta and make from the @meta and @make blocks, over
+ the site config the run assembled
+ doc_has docHasFromAbstraction, over the objects
+ doc_digest the markup digest from @source
+ manifest synthesised from the artefact's path and the
+ document's own name and language
+
+ What is not here, deliberately: insert_file_list, which is read only by
+ source_pod.d for --source and --pod2. No artefact carries the markup,
+ so a pod cannot be rebuilt from one, and the empty list is the truthful
+ answer rather than a gap.
++/
+template spineDocFromArtefact() {
+ import std.algorithm : endsWith;
+ import std.array;
+ import std.conv : to;
+ import std.path;
+ import std.regex;
+ import std.stdio;
+ import std.string;
+ import sisudoc.ocda.meta.conf_make_meta_structs;
+ import sisudoc.ocda.meta.doc_matters;
+ import sisudoc.ocda.io_in.paths_source;
+ import sisudoc.ocda.abstraction.doc_has;
+ import sisudoc.ocda.abstraction.load;
+ import sisudoc.ocda.meta.topic_register;
+ mixin spineDocMatters;
+ mixin spineDocHasFromAbstraction;
+ mixin spineAbstractionLoad;
+ mixin spineTopicRegister;
+ /+ ↓ a .ssp header key from a field name: the first underscore becomes the
+ dot that separates the group, so title_main is title.main and
+ rights_copyright_text is rights.copyright_text. The writer's keys are
+ formed the same way, which is what lets this be read off the struct
+ rather than kept as a second list to fall out of step with it.
+ +/
+ private string _sspKeyOf(string _member) {
+ foreach (i, c; _member) {
+ if (c == '_') { return _member[0 .. i] ~ "." ~ _member[i + 1 .. $]; }
+ }
+ return _member;
+ }
+ /+ ↓ the document's own header, over the site config the run assembled.
+
+ every string field of MetaComposite is looked for under its own key,
+ so a field added there and emitted by the writer arrives here without
+ anything being added. The arr fields are split from the string they
+ come from, as agreed: that is parsing a value, not deriving a
+ structure.
+ +/
+ ConfComposite confMakeMetaFromHeader(S)(S _ssp, ConfComposite _site_conf) {
+ ConfComposite _out = _site_conf; // conf: site and run scoped, kept
+ static foreach (_m; __traits(allMembers, MetaComposite)) {
+ static if (is(typeof(__traits(getMember, _out.meta, _m)) == string)) {{
+ enum string _key = _sspKeyOf(_m);
+ if (auto _v = _key in _ssp.meta) { __traits(getMember, _out.meta, _m) = *_v; }
+ }}
+ }
+ /+ ↓ title_sub is a copy of title_subtitle, made where the header is
+ read, and is what epub3 puts in dc:title id="subtitle". It is not
+ emitted, being the same string twice; it is remade here the way the
+ yaml reader makes it +/
+ _out.meta.title_sub = _out.meta.title_subtitle;
+ /+ ↓ creator.author is the author list joined with ", ", so splitting on
+ it gives the list back +/
+ if (_out.meta.creator_author.length > 0) {
+ foreach (_a; _out.meta.creator_author.split(", ")) {
+ if (_a.strip.length > 0) { _out.meta.creator_author_arr ~= _a.strip; }
+ }
+ }
+ /+ ↓ the topic register arrays, by the one rule the yaml reader uses:
+ the split is not a plain one and deriving it twice would be two
+ answers to the same question +/
+ {
+ auto _tr = topicRegisterArrays(_out.meta.classify_topic_register);
+ _out.meta.classify_topic_register_arr = _tr.arr;
+ _out.meta.classify_topic_register_expanded_arr = _tr.expanded;
+ }
+ /+ ↓ make: the keys are the field names, with no group to separate +/
+ if (auto _v = "doc_type" in _ssp.make) {
+ _out.make.doc_type = *_v; _out.make_str.doc_type = *_v;
+ }
+ if (auto _v = "auto_num_top_at_level" in _ssp.make) {
+ _out.make.auto_num_top_at_level = *_v;
+ }
+ if (auto _v = "auto_num_top_lv" in _ssp.make) {
+ try { _out.make.auto_num_top_lv = (*_v).to!int; } catch (Exception ex) {}
+ }
+ if (auto _v = "auto_num_depth" in _ssp.make) {
+ try { _out.make.auto_num_depth = (*_v).to!int; } catch (Exception ex) {}
+ }
+ if (auto _v = "breaks" in _ssp.make) {
+ _out.make.breaks = *_v; _out.make_str.breaks = *_v;
+ }
+ if (auto _v = "home_button_text" in _ssp.make) {
+ _out.make.home_button_text = *_v; _out.make_str.home_button_text = *_v;
+ }
+ /+ ↓ footer is a list, joined with the separator the format escapes +/
+ if (auto _v = "footer" in _ssp.make) {
+ _out.make.footer = sspUnescapeSplit(*_v);
+ _out.make_str.footer = _out.make.footer;
+ }
+ return _out;
+ }
+ /+ ↓ where a document loaded from an artefact says it is.
+
+ an artefact names its document (% Source:) and its language (@source
+ language) but not where the pod holding its images sits, so that is
+ taken from where the artefact itself was found:
+
+ pod/<doc>/media/abstraction/<uid>.ssp -> pod/<doc>/
+ pod/<uid>.ocda.db -> pod/<doc>/
+
+ which is where --pod2 puts them both. The images beside a .ssp are
+ then found where html.d already looks; a .ocda.db carries its own and
+ does not need the directory to exist.
+ +/
+ struct ST_ArtefactPaths {
+ string pod_dir;
+ string src_file_with_path;
+ }
+ ST_ArtefactPaths artefactPaths(S)(string _artefact, S _ssp) {
+ ST_ArtefactPaths _out;
+ string _lang = ("language" in _ssp.source_info) ? _ssp.source_info["language"] : "en";
+ string _dir = _artefact.dirName;
+ if (_artefact.endsWith(".ssp")) {
+ /+ ↓ <pod>/media/abstraction/<uid>.ssp: two directories up is the pod +/
+ _out.pod_dir = (_dir.baseName == "abstraction")
+ ? _dir.dirName.dirName : _dir;
+ } else {
+ /+ ↓ <dir>/<uid>.ocda.db, the pod tree beside it named for the document +/
+ string _uid = _artefact.baseName;
+ foreach (_sfx; [".ocda.db", ".db"]) {
+ if (_uid.endsWith(_sfx)) { _uid = _uid[0 .. $ - _sfx.length]; break; }
+ }
+ if (_uid.endsWith("." ~ _lang)) { _uid = _uid[0 .. $ - _lang.length - 1]; }
+ _out.pod_dir = (_dir.chainPath(_uid).array).to!string;
+ }
+ _out.src_file_with_path = (_out.pod_dir
+ .chainPath("media").chainPath("text").chainPath(_lang)
+ .chainPath(_ssp.source).array).to!string;
+ return _out;
+ }
+ /+ ↓ the doc the output writers take, from an artefact.
+
+ the reading is done here rather than by the caller, and that is not
+ only tidiness: main() is @system, so a template mixed in there infers
+ @system for everything it declares, and the output writers are @safe
+ and will not take objects of such a type. Doing it inside this module,
+ which is @safe:, is what makes the loaded objects the same kind of
+ thing the parser's are. Called as spineDocFromArtefact!()(...), the
+ way spineAbstraction!() is, rather than mixed in.
+ +/
+ auto spineDocFromArtefact(P,O,E)(
+ string _artefact,
+ P program_info,
+ O _opt_action,
+ E _env,
+ ConfComposite _site_conf,
+ ) {
+ auto _loaded = abstractionLoad(_artefact);
+ auto _ssp = _loaded.doc;
+ auto _pths = artefactPaths(_artefact, _ssp);
+ auto _manifest = PathMatters!()(_opt_action, _env, _pths.pod_dir, _pths.src_file_with_path);
+ auto _cmm = confMakeMetaFromHeader(_ssp, _site_conf);
+ auto _has = docHasFromAbstraction(_ssp.abstraction, _ssp.doc_has, _opt_action);
+ /+ ↓ the digests: the markup digest is what @source carries, and it is
+ the one that says which source this abstraction was made from. The
+ header and text digests are of parts an artefact does not keep
+ separately, and are left unset rather than guessed at
+ +/
+ ST_ArtefactDigest _dig;
+ if (auto _v = "digest" in _ssp.source_info) {
+ _dig.markup_doc = _hexToDigest(*_v);
+ }
+ auto _abstraction = _ssp.abstraction;
+ auto _matters = docMattersMake(
+ program_info, _opt_action, _manifest, _cmm, _has, _dig, string[].init,
+ );
+ /+ ↓ the same shape spineAbstraction() returns, built the same way: a
+ struct of accessors over the locals, with whether the artefact
+ could be read at all, which the caller has to know +/
+ auto theDOC() {
+ struct ST_DOC {
+ const auto abstraction() { return _abstraction; }
+ auto matters() { return _matters; }
+ bool loaded() { return _loaded.loaded; }
+ string note() { return _loaded.note; }
+ string source_name() { return abstractionSourceName(_loaded.source); }
+ }
+ return ST_DOC();
+ }
+ return theDOC();
+ }
+ /+ ↓ the digests doc_matters carries. Only markup_doc can be filled from
+ an artefact, @source having recorded it; the header and text digests
+ are of parts no artefact keeps separately and are left unset rather
+ than guessed at. Shaped to match ST_doc_digest, which is declared
+ inside spineRawMarkupContent and reachable only by parsing a file.
+ +/
+ struct ST_ArtefactDigest {
+ ubyte[32] markup_doc;
+ ubyte[32] header;
+ ubyte[32] text;
+ }
+ private ubyte[32] _hexToDigest(string _hex) {
+ ubyte[32] _out;
+ if (_hex.length < 64) { return _out; }
+ foreach (i; 0..32) {
+ try { _out[i] = _hex[i*2 .. i*2+2].to!ubyte(16); } catch (Exception ex) {}
+ }
+ 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/org/in_source_files.org b/org/in_source_files.org
index 4ba1b10..7d869c0 100644
--- a/org/in_source_files.org
+++ b/org/in_source_files.org
@@ -798,7 +798,7 @@ module sisudoc.ocda.abstraction.ssp_in;
record) and require the result to be byte identical to the file read.
+/
template spineAbstractionRead() {
- import std.algorithm : startsWith;
+ import std.algorithm : startsWith, findSplit;
import std.array;
import std.conv : to;
import std.file;
@@ -1136,7 +1136,7 @@ template spineAbstractionRead() {
the digest is going to be recorded, and on the largest document in the
sample set it is a tenth of what reading the file costs
+/
- SSPdocument sspReadFile(string file_path, bool with_digest = false) {
+ @trusted SSPdocument sspReadFile(string file_path, bool with_digest = false) {
string _text;
string[] _lines;
try {
@@ -1184,7 +1184,7 @@ template spineAbstractionDbRead() {
import d2sqlite3;
import sisudoc.ocda.abstraction.ssp_in : spineAbstractionRead;
mixin spineAbstractionRead; // for SSPdocument, and the shared unescape
- private int[8] _jsonEight(string _s) {
+ @trusted private int[8] _jsonEight(string _s) {
int[8] _out;
if (_s.length == 0) { return _out; }
try {
@@ -1195,7 +1195,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private int[] _jsonInts(string _s) {
+ @trusted private int[] _jsonInts(string _s) {
int[] _out;
if (_s.length == 0) { return _out; }
try {
@@ -1204,7 +1204,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private string[] _jsonStrs(string _s) {
+ @trusted private string[] _jsonStrs(string _s) {
string[] _out;
if (_s.length == 0) { return _out; }
try {
@@ -1213,7 +1213,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private double[] _jsonDoubles(string _s) {
+ @trusted private double[] _jsonDoubles(string _s) {
double[] _out;
if (_s.length == 0) { return _out; }
try {
diff --git a/org/meta_conf_make_meta.org b/org/meta_conf_make_meta.org
index f9c13ef..55b7705 100644
--- a/org/meta_conf_make_meta.org
+++ b/org/meta_conf_make_meta.org
@@ -632,6 +632,7 @@ template contentYAMLtoSpineStruct() {
import sisudoc.ocda.meta.defaults;
import sisudoc.ocda.meta.rgx_yaml;
import sisudoc.ocda.meta.rgx;
+ import sisudoc.ocda.meta.topic_register;
ConfComposite _struct_composite;
@system ConfComposite contentYAMLtoSpineStruct(C,Y,M,O,Cfg)(
C _struct_composite,
@@ -644,6 +645,7 @@ template contentYAMLtoSpineStruct() {
mixin spineRgxIn;
static auto rgx = RgxI();
mixin spineRgxYamlTags;
+ mixin spineTopicRegister;
static auto rgx_y = RgxYaml();
<<meta_check_input_markup>>
<<meta_extract_yaml>>
@@ -1277,32 +1279,13 @@ if ("classify" in _yaml
.replaceAll(wrds, "\"$1\"")
.replaceAll(mkp_delim, ":$1$2");
}
- string[] main_topics_ = _struct_composite.meta.classify_topic_register.strip.split(rgx.topic_register_main_terms_split);
- string[] topics;
- string topics_tmp;
- string[] multiple_sub_terms;
- foreach (mt; main_topics_) {
- topics_tmp = mt.replaceAll(rgx.topic_register_main_term_plus_rest_split, mkup.sep);
- if (auto m = topics_tmp.match(rgx.topic_register_multiple_sub_terms_split)) {
- multiple_sub_terms = m.captures[1].split(rgx.topic_register_sub_terms_split);
- foreach (subterm; multiple_sub_terms) {
- topics ~= m.captures.pre ~ mkup.sep ~ subterm;
- }
- } else {
- topics ~= topics_tmp;
- }
- }
- _struct_composite.meta.classify_topic_register_arr = topics;
- string[] topics_expanded;
- if (_struct_composite.meta.classify_topic_register_arr.length > 0) {
- foreach (i, topic; _struct_composite.meta.classify_topic_register_arr) {
- string[] subject_tree = topic.split(mkup.sep);
- if (topic.length > 0) {
- topics_expanded ~= subject_tree.join(", ");
- }
- }
+ /+ ↓ one rule, shared with a document loaded from an artefact,
+ whose header carries the string but not the arrays +/
+ {
+ auto _tr = topicRegisterArrays(_struct_composite.meta.classify_topic_register);
+ _struct_composite.meta.classify_topic_register_arr = _tr.arr;
+ _struct_composite.meta.classify_topic_register_expanded_arr = _tr.expanded;
}
- _struct_composite.meta.classify_topic_register_expanded_arr = topics_expanded;
// writeln("\n------\n", _struct_composite.meta.title_full);
// writeln(_struct_composite.meta.classify_topic_register);
// writeln(_struct_composite.meta.classify_topic_register_expanded_arr.sort.join("\n"));
diff --git a/org/ocda.org b/org/ocda.org
index db24139..ee490f4 100644
--- a/org/ocda.org
+++ b/org/ocda.org
@@ -97,6 +97,7 @@ public import sisudoc.ocda.abstraction.ssp_in; // spineAbstractionRead (.ssp
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:
diff --git a/org/ocda_get.org b/org/ocda_get.org
index 64fff57..40effc5 100644
--- a/org/ocda_get.org
+++ b/org/ocda_get.org
@@ -165,22 +165,6 @@ template spineDocHasFromAbstraction() {
_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
@@ -237,6 +221,33 @@ template spineDocHasFromAbstraction() {
foreach (_section; tail_sections) {
if (_has_section(_abst, _section)) { _out.segnames_lv4 ~= _section; }
}
+ /+ ↓ the body's objects, in a pass of their own after every anchor above
+ has been recorded, because the parser does it in that order and the
+ order is what decides two of these entries.
+ .
+ each body object's identifier maps to the segment it is in. seg_lv4
+ is set only where it is not already, an anchor of the same name
+ having said it *names* a segment rather than sits in one; seg_lv1to4
+ is set always. A heading's own anchor can be a bare number taken
+ from its text ("2. Shorter Terms" gives the anchor "2"), which then
+ collides with the ocn of an unrelated object, and this is the order
+ that resolves the collision the way the parser resolves it: in
+ free_culture, ocn 2 keeps seg_lv4 "them" from the anchor and takes
+ seg_lv1to4 "_part_1" from itself.
+ +/
+ if (_has_section(_abst, "body")) {
+ foreach (obj; _abst["body"]) {
+ if (obj.metainfo.identifier.length == 0) { continue; }
+ 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;
+ }
+ }
_out.imagelist = _images.sort.uniq.array;
_out.tag_associations = _tag_assoc;
_out.section_keys_sequenced = sectionKeysSequenced(_abst, _opt_action);
diff --git a/org/ocda_obj_setter.org b/org/ocda_obj_setter.org
index e0e7363..cd90f36 100644
--- a/org/ocda_obj_setter.org
+++ b/org/ocda_obj_setter.org
@@ -505,6 +505,70 @@ static auto eN() {
}
#+END_SRC
+* topic register
+
+#+HEADER: :tangle "../src/sisudoc/ocda/meta/topic_register.d"
+#+HEADER: :noweb yes
+#+BEGIN_SRC d
+<<doc_header_including_copyright_and_license>>
+module sisudoc.ocda.meta.topic_register;
+@safe:
+/+ ↓ the topic register, as the two arrays the curate writers walk
+ .
+ classify.topic_register is authored as one string, and the topic index
+ needs it as a list of paths and as a list of readable labels. The rule
+ is not a plain split: a main term is separated from the rest, sub terms
+ divided by "|" become one entry each, and the parts are joined with the
+ internal separator that the writers then split on again.
+ .
+ It lives here because two callers need the same answer from the same
+ string: the yaml reader, which has just read it from a document header,
+ and a document loaded from a .ssp or a .ocda.db, whose header carries
+ the string but not the arrays. Deriving it twice is how two answers to
+ one question get into a program.
++/
+template spineTopicRegister() {
+ import std.algorithm : sort, uniq;
+ import std.array;
+ import std.regex;
+ import std.string;
+ import sisudoc.ocda.meta.rgx;
+ import sisudoc.ocda.meta.defaults;
+ mixin spineRgxIn;
+ mixin InternalMarkup;
+ struct ST_TopicRegister {
+ string[] arr; // topic paths, joined with the internal separator
+ string[] expanded; // the same, as readable ", " separated labels
+ }
+ ST_TopicRegister topicRegisterArrays(string _topic_register) {
+ static auto rgx = RgxI();
+ auto mkup = InlineMarkup();
+ ST_TopicRegister _out;
+ if (_topic_register.length == 0) { return _out; }
+ string[] _main_topics = _topic_register.strip.split(rgx.topic_register_main_terms_split);
+ string[] _topics;
+ string _topics_tmp;
+ string[] _multiple_sub_terms;
+ foreach (_mt; _main_topics) {
+ _topics_tmp = _mt.replaceAll(rgx.topic_register_main_term_plus_rest_split, mkup.sep);
+ if (auto m = _topics_tmp.match(rgx.topic_register_multiple_sub_terms_split)) {
+ _multiple_sub_terms = m.captures[1].split(rgx.topic_register_sub_terms_split);
+ foreach (_subterm; _multiple_sub_terms) {
+ _topics ~= m.captures.pre ~ mkup.sep ~ _subterm;
+ }
+ } else {
+ _topics ~= _topics_tmp;
+ }
+ }
+ _out.arr = _topics;
+ foreach (_topic; _out.arr) {
+ if (_topic.length > 0) { _out.expanded ~= _topic.split(mkup.sep).join(", "); }
+ }
+ return _out;
+ }
+}
+#+END_SRC
+
* org includes
** project version
diff --git a/org/spine.org b/org/spine.org
index 3ff3c05..d3a5276 100644
--- a/org/spine.org
+++ b/org/spine.org
@@ -98,6 +98,54 @@ string program_name = "spine";
}
}
}
+ /+ ↓ documents given as an abstraction artefact rather than as markup.
+ .ssp and .ocda.db are read, not parsed, and what comes back is the
+ doc the output writers already take, so this loop is the markup one
+ with its first step replaced. It is serial: the artefacts are read
+ one at a time and there is nothing here that parallelising would
+ help with, the reading being the cheap half.
+ --source and --pod2 are refused rather than half done: no artefact
+ carries the markup, so a pod cannot be rebuilt from one.
+ +/
+ if (_artefact_args.length > 0 && _opt_action.abstraction) {
+ import sisudoc.ocda.abstraction.doc_from_artefact;
+ if (!(_opt_action.skip_output)) {
+ outputHubInitialize!()(_opt_action, program_info);
+ }
+ if (_opt_action.source_or_pod) {
+ writeln("WARNING: --source and --pod2 are not available from an abstraction",
+ " source: no artefact carries the markup. Skipped for: ",
+ _artefact_args.join(", "));
+ }
+ foreach (_artefact; _artefact_args) {
+ auto doc = spineDocFromArtefact!()(
+ _artefact, program_info, _opt_action, _env, _make_and_meta_struct,
+ );
+ if (!doc.loaded) {
+ stderr.writeln("ERROR: could not load ", _artefact,
+ (doc.note.length > 0) ? ": " ~ doc.note : "");
+ continue;
+ }
+ if (_opt_action.vox_gt_1) {
+ writeln("-- ~ document from ", doc.source_name,
+ " ~ ------------------------------------");
+ writeln(" ", _artefact);
+ }
+ if (doc.matters.opt.action.curate) {
+ auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
+ if (_hvst.title.length > 0 && _hvst.author_surname_fn.length > 0) {
+ hvst.curates ~= _hvst;
+ }
+ }
+ if (!(doc.matters.opt.action.skip_output)) {
+ doc.outputHub!();
+ }
+ if (_opt_action.vox_gt_1) {
+ writeln("processed artefact: ", _artefact.baseName,
+ " [", doc.matters.src.language, "]");
+ }
+ }
+ }
if (hvst.curates.length > 0) {
if (_opt_action.curate_topics) {
spineMetaDocCuratesTopics!()(hvst, _make_and_meta_struct, _opt_action);
@@ -1218,6 +1266,7 @@ auto _env = [
#+BEGIN_SRC d
auto _manifested = PathMatters!()(_opt_action, _env, "");
auto _manifests = [ _manifested ];
+string[] _artefact_args; /+ .ssp and .ocda.db given on the command line +/
auto _conf_file_details = configFilePaths!()(_manifested, _env, _opt_action.config_path_set);
/+ ↓ track extracted zip pod temp directories for cleanup +/
mixin spineExtractZipPod;
@@ -1613,6 +1662,11 @@ foreach(arg; _resolved_args) {
}
}
}
+ } else if (arg.endsWith(".ssp") || arg.endsWith(".ocda.db")) {
+ /+ ↓ an abstraction artefact: the document is read rather than
+ parsed, and joins the run as a document like any other
+ +/
+ _artefact_args ~= arg;
} else { // anything remaining, unused
arg_unrecognized ~= " " ~ arg;
}
diff --git a/src/sisudoc/ocda/abstraction/db_in.d b/src/sisudoc/ocda/abstraction/db_in.d
index 6e9b0e2..7f920bd 100644
--- a/src/sisudoc/ocda/abstraction/db_in.d
+++ b/src/sisudoc/ocda/abstraction/db_in.d
@@ -70,7 +70,7 @@ template spineAbstractionDbRead() {
import d2sqlite3;
import sisudoc.ocda.abstraction.ssp_in : spineAbstractionRead;
mixin spineAbstractionRead; // for SSPdocument, and the shared unescape
- private int[8] _jsonEight(string _s) {
+ @trusted private int[8] _jsonEight(string _s) {
int[8] _out;
if (_s.length == 0) { return _out; }
try {
@@ -81,7 +81,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private int[] _jsonInts(string _s) {
+ @trusted private int[] _jsonInts(string _s) {
int[] _out;
if (_s.length == 0) { return _out; }
try {
@@ -90,7 +90,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private string[] _jsonStrs(string _s) {
+ @trusted private string[] _jsonStrs(string _s) {
string[] _out;
if (_s.length == 0) { return _out; }
try {
@@ -99,7 +99,7 @@ template spineAbstractionDbRead() {
} catch (Exception ex) {}
return _out;
}
- private double[] _jsonDoubles(string _s) {
+ @trusted private double[] _jsonDoubles(string _s) {
double[] _out;
if (_s.length == 0) { return _out; }
try {
diff --git a/src/sisudoc/ocda/abstraction/doc_from_artefact.d b/src/sisudoc/ocda/abstraction/doc_from_artefact.d
new file mode 100644
index 0000000..e45cbf0
--- /dev/null
+++ b/src/sisudoc/ocda/abstraction/doc_from_artefact.d
@@ -0,0 +1,279 @@
+/+
+- 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_from_artefact;
+@safe:
+/+ ↓ a document, from an artefact rather than from markup
+
+ spineAbstraction() parses markup and hands the output writers a doc:
+ an abstraction and a doc_matters. This does the same from a .ssp or a
+ .ocda.db, filling the same docMattersMake() arguments, so that what
+ comes out is the value the writers already take and they never learn
+ which of the two made it.
+
+ Three of the seven arguments come from the run and are the same either
+ way: program_info, opt_action, and the conf half of conf_make_meta,
+ which is site and run scoped (urls, papersize, the search database) and
+ has no business in a document's own artefact. The other four come from
+ the artefact:
+
+ conf_make_meta meta and make from the @meta and @make blocks, over
+ the site config the run assembled
+ doc_has docHasFromAbstraction, over the objects
+ doc_digest the markup digest from @source
+ manifest synthesised from the artefact's path and the
+ document's own name and language
+
+ What is not here, deliberately: insert_file_list, which is read only by
+ source_pod.d for --source and --pod2. No artefact carries the markup,
+ so a pod cannot be rebuilt from one, and the empty list is the truthful
+ answer rather than a gap.
++/
+template spineDocFromArtefact() {
+ import std.algorithm : endsWith;
+ import std.array;
+ import std.conv : to;
+ import std.path;
+ import std.regex;
+ import std.stdio;
+ import std.string;
+ import sisudoc.ocda.meta.conf_make_meta_structs;
+ import sisudoc.ocda.meta.doc_matters;
+ import sisudoc.ocda.io_in.paths_source;
+ import sisudoc.ocda.abstraction.doc_has;
+ import sisudoc.ocda.abstraction.load;
+ import sisudoc.ocda.meta.topic_register;
+ mixin spineDocMatters;
+ mixin spineDocHasFromAbstraction;
+ mixin spineAbstractionLoad;
+ mixin spineTopicRegister;
+ /+ ↓ a .ssp header key from a field name: the first underscore becomes the
+ dot that separates the group, so title_main is title.main and
+ rights_copyright_text is rights.copyright_text. The writer's keys are
+ formed the same way, which is what lets this be read off the struct
+ rather than kept as a second list to fall out of step with it.
+ +/
+ private string _sspKeyOf(string _member) {
+ foreach (i, c; _member) {
+ if (c == '_') { return _member[0 .. i] ~ "." ~ _member[i + 1 .. $]; }
+ }
+ return _member;
+ }
+ /+ ↓ the document's own header, over the site config the run assembled.
+
+ every string field of MetaComposite is looked for under its own key,
+ so a field added there and emitted by the writer arrives here without
+ anything being added. The arr fields are split from the string they
+ come from, as agreed: that is parsing a value, not deriving a
+ structure.
+ +/
+ ConfComposite confMakeMetaFromHeader(S)(S _ssp, ConfComposite _site_conf) {
+ ConfComposite _out = _site_conf; // conf: site and run scoped, kept
+ static foreach (_m; __traits(allMembers, MetaComposite)) {
+ static if (is(typeof(__traits(getMember, _out.meta, _m)) == string)) {{
+ enum string _key = _sspKeyOf(_m);
+ if (auto _v = _key in _ssp.meta) { __traits(getMember, _out.meta, _m) = *_v; }
+ }}
+ }
+ /+ ↓ title_sub is a copy of title_subtitle, made where the header is
+ read, and is what epub3 puts in dc:title id="subtitle". It is not
+ emitted, being the same string twice; it is remade here the way the
+ yaml reader makes it +/
+ _out.meta.title_sub = _out.meta.title_subtitle;
+ /+ ↓ creator.author is the author list joined with ", ", so splitting on
+ it gives the list back +/
+ if (_out.meta.creator_author.length > 0) {
+ foreach (_a; _out.meta.creator_author.split(", ")) {
+ if (_a.strip.length > 0) { _out.meta.creator_author_arr ~= _a.strip; }
+ }
+ }
+ /+ ↓ the topic register arrays, by the one rule the yaml reader uses:
+ the split is not a plain one and deriving it twice would be two
+ answers to the same question +/
+ {
+ auto _tr = topicRegisterArrays(_out.meta.classify_topic_register);
+ _out.meta.classify_topic_register_arr = _tr.arr;
+ _out.meta.classify_topic_register_expanded_arr = _tr.expanded;
+ }
+ /+ ↓ make: the keys are the field names, with no group to separate +/
+ if (auto _v = "doc_type" in _ssp.make) {
+ _out.make.doc_type = *_v; _out.make_str.doc_type = *_v;
+ }
+ if (auto _v = "auto_num_top_at_level" in _ssp.make) {
+ _out.make.auto_num_top_at_level = *_v;
+ }
+ if (auto _v = "auto_num_top_lv" in _ssp.make) {
+ try { _out.make.auto_num_top_lv = (*_v).to!int; } catch (Exception ex) {}
+ }
+ if (auto _v = "auto_num_depth" in _ssp.make) {
+ try { _out.make.auto_num_depth = (*_v).to!int; } catch (Exception ex) {}
+ }
+ if (auto _v = "breaks" in _ssp.make) {
+ _out.make.breaks = *_v; _out.make_str.breaks = *_v;
+ }
+ if (auto _v = "home_button_text" in _ssp.make) {
+ _out.make.home_button_text = *_v; _out.make_str.home_button_text = *_v;
+ }
+ /+ ↓ footer is a list, joined with the separator the format escapes +/
+ if (auto _v = "footer" in _ssp.make) {
+ _out.make.footer = sspUnescapeSplit(*_v);
+ _out.make_str.footer = _out.make.footer;
+ }
+ return _out;
+ }
+ /+ ↓ where a document loaded from an artefact says it is.
+
+ an artefact names its document (% Source:) and its language (@source
+ language) but not where the pod holding its images sits, so that is
+ taken from where the artefact itself was found:
+
+ pod/<doc>/media/abstraction/<uid>.ssp -> pod/<doc>/
+ pod/<uid>.ocda.db -> pod/<doc>/
+
+ which is where --pod2 puts them both. The images beside a .ssp are
+ then found where html.d already looks; a .ocda.db carries its own and
+ does not need the directory to exist.
+ +/
+ struct ST_ArtefactPaths {
+ string pod_dir;
+ string src_file_with_path;
+ }
+ ST_ArtefactPaths artefactPaths(S)(string _artefact, S _ssp) {
+ ST_ArtefactPaths _out;
+ string _lang = ("language" in _ssp.source_info) ? _ssp.source_info["language"] : "en";
+ string _dir = _artefact.dirName;
+ if (_artefact.endsWith(".ssp")) {
+ /+ ↓ <pod>/media/abstraction/<uid>.ssp: two directories up is the pod +/
+ _out.pod_dir = (_dir.baseName == "abstraction")
+ ? _dir.dirName.dirName : _dir;
+ } else {
+ /+ ↓ <dir>/<uid>.ocda.db, the pod tree beside it named for the document +/
+ string _uid = _artefact.baseName;
+ foreach (_sfx; [".ocda.db", ".db"]) {
+ if (_uid.endsWith(_sfx)) { _uid = _uid[0 .. $ - _sfx.length]; break; }
+ }
+ if (_uid.endsWith("." ~ _lang)) { _uid = _uid[0 .. $ - _lang.length - 1]; }
+ _out.pod_dir = (_dir.chainPath(_uid).array).to!string;
+ }
+ _out.src_file_with_path = (_out.pod_dir
+ .chainPath("media").chainPath("text").chainPath(_lang)
+ .chainPath(_ssp.source).array).to!string;
+ return _out;
+ }
+ /+ ↓ the doc the output writers take, from an artefact.
+
+ the reading is done here rather than by the caller, and that is not
+ only tidiness: main() is @system, so a template mixed in there infers
+ @system for everything it declares, and the output writers are @safe
+ and will not take objects of such a type. Doing it inside this module,
+ which is @safe:, is what makes the loaded objects the same kind of
+ thing the parser's are. Called as spineDocFromArtefact!()(...), the
+ way spineAbstraction!() is, rather than mixed in.
+ +/
+ auto spineDocFromArtefact(P,O,E)(
+ string _artefact,
+ P program_info,
+ O _opt_action,
+ E _env,
+ ConfComposite _site_conf,
+ ) {
+ auto _loaded = abstractionLoad(_artefact);
+ auto _ssp = _loaded.doc;
+ auto _pths = artefactPaths(_artefact, _ssp);
+ auto _manifest = PathMatters!()(_opt_action, _env, _pths.pod_dir, _pths.src_file_with_path);
+ auto _cmm = confMakeMetaFromHeader(_ssp, _site_conf);
+ auto _has = docHasFromAbstraction(_ssp.abstraction, _ssp.doc_has, _opt_action);
+ /+ ↓ the digests: the markup digest is what @source carries, and it is
+ the one that says which source this abstraction was made from. The
+ header and text digests are of parts an artefact does not keep
+ separately, and are left unset rather than guessed at
+ +/
+ ST_ArtefactDigest _dig;
+ if (auto _v = "digest" in _ssp.source_info) {
+ _dig.markup_doc = _hexToDigest(*_v);
+ }
+ auto _abstraction = _ssp.abstraction;
+ auto _matters = docMattersMake(
+ program_info, _opt_action, _manifest, _cmm, _has, _dig, string[].init,
+ );
+ /+ ↓ the same shape spineAbstraction() returns, built the same way: a
+ struct of accessors over the locals, with whether the artefact
+ could be read at all, which the caller has to know +/
+ auto theDOC() {
+ struct ST_DOC {
+ const auto abstraction() { return _abstraction; }
+ auto matters() { return _matters; }
+ bool loaded() { return _loaded.loaded; }
+ string note() { return _loaded.note; }
+ string source_name() { return abstractionSourceName(_loaded.source); }
+ }
+ return ST_DOC();
+ }
+ return theDOC();
+ }
+ /+ ↓ the digests doc_matters carries. Only markup_doc can be filled from
+ an artefact, @source having recorded it; the header and text digests
+ are of parts no artefact keeps separately and are left unset rather
+ than guessed at. Shaped to match ST_doc_digest, which is declared
+ inside spineRawMarkupContent and reachable only by parsing a file.
+ +/
+ struct ST_ArtefactDigest {
+ ubyte[32] markup_doc;
+ ubyte[32] header;
+ ubyte[32] text;
+ }
+ private ubyte[32] _hexToDigest(string _hex) {
+ ubyte[32] _out;
+ if (_hex.length < 64) { return _out; }
+ foreach (i; 0..32) {
+ try { _out[i] = _hex[i*2 .. i*2+2].to!ubyte(16); } catch (Exception ex) {}
+ }
+ return _out;
+ }
+}
diff --git a/src/sisudoc/ocda/abstraction/doc_has.d b/src/sisudoc/ocda/abstraction/doc_has.d
index 051da33..ecdef25 100644
--- a/src/sisudoc/ocda/abstraction/doc_has.d
+++ b/src/sisudoc/ocda/abstraction/doc_has.d
@@ -185,22 +185,6 @@ template spineDocHasFromAbstraction() {
_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
@@ -257,6 +241,33 @@ template spineDocHasFromAbstraction() {
foreach (_section; tail_sections) {
if (_has_section(_abst, _section)) { _out.segnames_lv4 ~= _section; }
}
+ /+ ↓ the body's objects, in a pass of their own after every anchor above
+ has been recorded, because the parser does it in that order and the
+ order is what decides two of these entries.
+ .
+ each body object's identifier maps to the segment it is in. seg_lv4
+ is set only where it is not already, an anchor of the same name
+ having said it *names* a segment rather than sits in one; seg_lv1to4
+ is set always. A heading's own anchor can be a bare number taken
+ from its text ("2. Shorter Terms" gives the anchor "2"), which then
+ collides with the ocn of an unrelated object, and this is the order
+ that resolves the collision the way the parser resolves it: in
+ free_culture, ocn 2 keeps seg_lv4 "them" from the anchor and takes
+ seg_lv1to4 "_part_1" from itself.
+ +/
+ if (_has_section(_abst, "body")) {
+ foreach (obj; _abst["body"]) {
+ if (obj.metainfo.identifier.length == 0) { continue; }
+ 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;
+ }
+ }
_out.imagelist = _images.sort.uniq.array;
_out.tag_associations = _tag_assoc;
_out.section_keys_sequenced = sectionKeysSequenced(_abst, _opt_action);
diff --git a/src/sisudoc/ocda/abstraction/package.d b/src/sisudoc/ocda/abstraction/package.d
index 05897f6..5598898 100644
--- a/src/sisudoc/ocda/abstraction/package.d
+++ b/src/sisudoc/ocda/abstraction/package.d
@@ -88,3 +88,4 @@ public import sisudoc.ocda.abstraction.ssp_in; // spineAbstractionRead (.ssp
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)
diff --git a/src/sisudoc/ocda/abstraction/ssp_in.d b/src/sisudoc/ocda/abstraction/ssp_in.d
index 59a5b80..a71087a 100644
--- a/src/sisudoc/ocda/abstraction/ssp_in.d
+++ b/src/sisudoc/ocda/abstraction/ssp_in.d
@@ -61,7 +61,7 @@ module sisudoc.ocda.abstraction.ssp_in;
record) and require the result to be byte identical to the file read.
+/
template spineAbstractionRead() {
- import std.algorithm : startsWith;
+ import std.algorithm : startsWith, findSplit;
import std.array;
import std.conv : to;
import std.file;
@@ -399,7 +399,7 @@ template spineAbstractionRead() {
the digest is going to be recorded, and on the largest document in the
sample set it is a tenth of what reading the file costs
+/
- SSPdocument sspReadFile(string file_path, bool with_digest = false) {
+ @trusted SSPdocument sspReadFile(string file_path, bool with_digest = false) {
string _text;
string[] _lines;
try {
diff --git a/src/sisudoc/ocda/meta/conf_make_meta_yaml.d b/src/sisudoc/ocda/meta/conf_make_meta_yaml.d
index e73a843..6dd045d 100644
--- a/src/sisudoc/ocda/meta/conf_make_meta_yaml.d
+++ b/src/sisudoc/ocda/meta/conf_make_meta_yaml.d
@@ -68,6 +68,7 @@ template contentYAMLtoSpineStruct() {
import sisudoc.ocda.meta.defaults;
import sisudoc.ocda.meta.rgx_yaml;
import sisudoc.ocda.meta.rgx;
+ import sisudoc.ocda.meta.topic_register;
ConfComposite _struct_composite;
@system ConfComposite contentYAMLtoSpineStruct(C,Y,M,O,Cfg)(
C _struct_composite,
@@ -80,6 +81,7 @@ template contentYAMLtoSpineStruct() {
mixin spineRgxIn;
static auto rgx = RgxI();
mixin spineRgxYamlTags;
+ mixin spineTopicRegister;
static auto rgx_y = RgxYaml();
string check_input_markup()(
string _txt,
@@ -738,32 +740,13 @@ template contentYAMLtoSpineStruct() {
.replaceAll(wrds, "\"$1\"")
.replaceAll(mkp_delim, ":$1$2");
}
- string[] main_topics_ = _struct_composite.meta.classify_topic_register.strip.split(rgx.topic_register_main_terms_split);
- string[] topics;
- string topics_tmp;
- string[] multiple_sub_terms;
- foreach (mt; main_topics_) {
- topics_tmp = mt.replaceAll(rgx.topic_register_main_term_plus_rest_split, mkup.sep);
- if (auto m = topics_tmp.match(rgx.topic_register_multiple_sub_terms_split)) {
- multiple_sub_terms = m.captures[1].split(rgx.topic_register_sub_terms_split);
- foreach (subterm; multiple_sub_terms) {
- topics ~= m.captures.pre ~ mkup.sep ~ subterm;
- }
- } else {
- topics ~= topics_tmp;
- }
- }
- _struct_composite.meta.classify_topic_register_arr = topics;
- string[] topics_expanded;
- if (_struct_composite.meta.classify_topic_register_arr.length > 0) {
- foreach (i, topic; _struct_composite.meta.classify_topic_register_arr) {
- string[] subject_tree = topic.split(mkup.sep);
- if (topic.length > 0) {
- topics_expanded ~= subject_tree.join(", ");
- }
- }
+ /+ ↓ one rule, shared with a document loaded from an artefact,
+ whose header carries the string but not the arrays +/
+ {
+ auto _tr = topicRegisterArrays(_struct_composite.meta.classify_topic_register);
+ _struct_composite.meta.classify_topic_register_arr = _tr.arr;
+ _struct_composite.meta.classify_topic_register_expanded_arr = _tr.expanded;
}
- _struct_composite.meta.classify_topic_register_expanded_arr = topics_expanded;
// writeln("\n------\n", _struct_composite.meta.title_full);
// writeln(_struct_composite.meta.classify_topic_register);
// writeln(_struct_composite.meta.classify_topic_register_expanded_arr.sort.join("\n"));
diff --git a/src/sisudoc/ocda/meta/topic_register.d b/src/sisudoc/ocda/meta/topic_register.d
new file mode 100644
index 0000000..a22c70c
--- /dev/null
+++ b/src/sisudoc/ocda/meta/topic_register.d
@@ -0,0 +1,105 @@
+/+
+- 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.meta.topic_register;
+@safe:
+/+ ↓ the topic register, as the two arrays the curate writers walk
+ .
+ classify.topic_register is authored as one string, and the topic index
+ needs it as a list of paths and as a list of readable labels. The rule
+ is not a plain split: a main term is separated from the rest, sub terms
+ divided by "|" become one entry each, and the parts are joined with the
+ internal separator that the writers then split on again.
+ .
+ It lives here because two callers need the same answer from the same
+ string: the yaml reader, which has just read it from a document header,
+ and a document loaded from a .ssp or a .ocda.db, whose header carries
+ the string but not the arrays. Deriving it twice is how two answers to
+ one question get into a program.
++/
+template spineTopicRegister() {
+ import std.algorithm : sort, uniq;
+ import std.array;
+ import std.regex;
+ import std.string;
+ import sisudoc.ocda.meta.rgx;
+ import sisudoc.ocda.meta.defaults;
+ mixin spineRgxIn;
+ mixin InternalMarkup;
+ struct ST_TopicRegister {
+ string[] arr; // topic paths, joined with the internal separator
+ string[] expanded; // the same, as readable ", " separated labels
+ }
+ ST_TopicRegister topicRegisterArrays(string _topic_register) {
+ static auto rgx = RgxI();
+ auto mkup = InlineMarkup();
+ ST_TopicRegister _out;
+ if (_topic_register.length == 0) { return _out; }
+ string[] _main_topics = _topic_register.strip.split(rgx.topic_register_main_terms_split);
+ string[] _topics;
+ string _topics_tmp;
+ string[] _multiple_sub_terms;
+ foreach (_mt; _main_topics) {
+ _topics_tmp = _mt.replaceAll(rgx.topic_register_main_term_plus_rest_split, mkup.sep);
+ if (auto m = _topics_tmp.match(rgx.topic_register_multiple_sub_terms_split)) {
+ _multiple_sub_terms = m.captures[1].split(rgx.topic_register_sub_terms_split);
+ foreach (_subterm; _multiple_sub_terms) {
+ _topics ~= m.captures.pre ~ mkup.sep ~ _subterm;
+ }
+ } else {
+ _topics ~= _topics_tmp;
+ }
+ }
+ _out.arr = _topics;
+ foreach (_topic; _out.arr) {
+ if (_topic.length > 0) { _out.expanded ~= _topic.split(mkup.sep).join(", "); }
+ }
+ return _out;
+ }
+}
diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d
index 5560f99..98c729c 100644
--- a/src/sisudoc/spine.d
+++ b/src/sisudoc/spine.d
@@ -990,6 +990,7 @@ string program_name = "spine";
];
auto _manifested = PathMatters!()(_opt_action, _env, "");
auto _manifests = [ _manifested ];
+ string[] _artefact_args; /+ .ssp and .ocda.db given on the command line +/
auto _conf_file_details = configFilePaths!()(_manifested, _env, _opt_action.config_path_set);
/+ ↓ track extracted zip pod temp directories for cleanup +/
mixin spineExtractZipPod;
@@ -1373,6 +1374,11 @@ string program_name = "spine";
}
}
}
+ } else if (arg.endsWith(".ssp") || arg.endsWith(".ocda.db")) {
+ /+ ↓ an abstraction artefact: the document is read rather than
+ parsed, and joins the run as a document like any other
+ +/
+ _artefact_args ~= arg;
} else { // anything remaining, unused
arg_unrecognized ~= " " ~ arg;
}
@@ -1616,6 +1622,54 @@ string program_name = "spine";
}
}
}
+ /+ ↓ documents given as an abstraction artefact rather than as markup.
+ .ssp and .ocda.db are read, not parsed, and what comes back is the
+ doc the output writers already take, so this loop is the markup one
+ with its first step replaced. It is serial: the artefacts are read
+ one at a time and there is nothing here that parallelising would
+ help with, the reading being the cheap half.
+ --source and --pod2 are refused rather than half done: no artefact
+ carries the markup, so a pod cannot be rebuilt from one.
+ +/
+ if (_artefact_args.length > 0 && _opt_action.abstraction) {
+ import sisudoc.ocda.abstraction.doc_from_artefact;
+ if (!(_opt_action.skip_output)) {
+ outputHubInitialize!()(_opt_action, program_info);
+ }
+ if (_opt_action.source_or_pod) {
+ writeln("WARNING: --source and --pod2 are not available from an abstraction",
+ " source: no artefact carries the markup. Skipped for: ",
+ _artefact_args.join(", "));
+ }
+ foreach (_artefact; _artefact_args) {
+ auto doc = spineDocFromArtefact!()(
+ _artefact, program_info, _opt_action, _env, _make_and_meta_struct,
+ );
+ if (!doc.loaded) {
+ stderr.writeln("ERROR: could not load ", _artefact,
+ (doc.note.length > 0) ? ": " ~ doc.note : "");
+ continue;
+ }
+ if (_opt_action.vox_gt_1) {
+ writeln("-- ~ document from ", doc.source_name,
+ " ~ ------------------------------------");
+ writeln(" ", _artefact);
+ }
+ if (doc.matters.opt.action.curate) {
+ auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
+ if (_hvst.title.length > 0 && _hvst.author_surname_fn.length > 0) {
+ hvst.curates ~= _hvst;
+ }
+ }
+ if (!(doc.matters.opt.action.skip_output)) {
+ doc.outputHub!();
+ }
+ if (_opt_action.vox_gt_1) {
+ writeln("processed artefact: ", _artefact.baseName,
+ " [", doc.matters.src.language, "]");
+ }
+ }
+ }
if (hvst.curates.length > 0) {
if (_opt_action.curate_topics) {
spineMetaDocCuratesTopics!()(hvst, _make_and_meta_struct, _opt_action);