diff options
| -rw-r--r-- | org/in_source_artefact.org | 284 | ||||
| -rw-r--r-- | org/in_source_files.org | 12 | ||||
| -rw-r--r-- | org/meta_conf_make_meta.org | 33 | ||||
| -rw-r--r-- | org/ocda.org | 1 | ||||
| -rw-r--r-- | org/ocda_get.org | 43 | ||||
| -rw-r--r-- | org/ocda_obj_setter.org | 64 | ||||
| -rw-r--r-- | org/spine.org | 54 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/db_in.d | 8 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/doc_from_artefact.d | 279 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/doc_has.d | 43 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/package.d | 1 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/ssp_in.d | 4 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/conf_make_meta_yaml.d | 33 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/topic_register.d | 105 | ||||
| -rw-r--r-- | src/sisudoc/spine.d | 54 |
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); |
