aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sisudoc/ocda
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-08 13:52:43 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-09 18:22:47 -0400
commitc7049608de7d163137521495665753ae7070be08 (patch)
treeb9617c6e84ea4be4a41b794a7d515986eef01af1 /src/sisudoc/ocda
parentocda db: <doc>.ocda.db, and a script to run 4 tests (diff)
ocda loader: one way in whatever the source
sisudoc.ocda.abstraction.load names the five things a document can be read from, tells them apart, and loads the two that are self-describing artefacts: .sst / .ssm + images the markup source pod (dir) + images the same, bundled pod .zip the same, zipped .ssp + images the abstraction, as text .ocda.db the abstraction, sqlite, images inside abstractionSourceOf(path) is the detection, by name and for a directory by whether it holds pod.manifest. abstractionLoad(path) returns a LoadedAbstraction: the source kind, whether it was loaded, why not when it was not, and the document itself. The three source forms are deliberately not loaded here. Reading them is the parser's job (sisudoc.ocda.meta.metadoc spineAbstraction) and it needs the manifest, environment and configuration that spine.d assembles, none of which belongs in a loader. What this gives that case is the dispatch and a plain statement of where it is handled, rather than a silent empty result. spine --abstraction-source=<path> says what a path is and, for an artefact, loads it and reports what came back: the document, title and author, header block sizes, object counts and the objects in each section. Exit 0 when an abstraction was loaded, 1 when not. (assisted by Claude-Code)
Diffstat (limited to 'src/sisudoc/ocda')
-rw-r--r--src/sisudoc/ocda/abstraction/load.d205
-rw-r--r--src/sisudoc/ocda/abstraction/package.d1
2 files changed, 206 insertions, 0 deletions
diff --git a/src/sisudoc/ocda/abstraction/load.d b/src/sisudoc/ocda/abstraction/load.d
new file mode 100644
index 0000000..f429595
--- /dev/null
+++ b/src/sisudoc/ocda/abstraction/load.d
@@ -0,0 +1,205 @@
+/+
+- 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.load;
+@safe:
+/+ ↓ one way in, whatever the document is being read from
+
+ spine's pipeline is markup -> abstraction -> output. once the abstraction
+ is serialised, there is more than one thing an abstraction can be read
+ from, and a consumer should not have to know which it was handed:
+
+ .sst / .ssm + images the markup source
+ pod (dir) + images the same, bundled
+ pod .zip the same, zipped
+ .ssp + images the abstraction, as text
+ .ocda.db the abstraction, sqlite, images inside
+
+ This module names those sources, tells them apart, and loads the two
+ that are self-describing artefacts, returning the value the parser
+ produces.
+
+ The three source forms are deliberately *not* loaded here. They are the
+ parser's job (sisudoc.ocda.meta.metadoc, spineAbstraction), and it needs
+ the environment, the options and the configuration that spine.d
+ assembles, none of which belongs in a loader. What this module gives
+ that case is the dispatch and a plain statement of where to go.
++/
+template spineAbstractionLoad() {
+ import std.algorithm : endsWith;
+ import std.conv : to;
+ import std.file;
+ import std.path;
+ import std.stdio;
+ import std.string;
+ import sisudoc.ocda.abstraction.db_in;
+ mixin spineAbstractionDbRead; // brings the .ssp reader with it
+
+ enum AbstractionSource {
+ unknown,
+ markup, // .sst or .ssm, with its images beside it
+ pod, // a directory holding pod.manifest
+ pod_zip, // that directory, zipped
+ ssp, // .ssp, the abstraction as text, images beside it
+ ocda_db, // .ocda.db, the abstraction as sqlite, images inside it
+ }
+
+ string abstractionSourceName(AbstractionSource _s) {
+ final switch (_s) {
+ case AbstractionSource.unknown: return "unknown";
+ case AbstractionSource.markup: return "markup source (.sst/.ssm)";
+ case AbstractionSource.pod: return "pod directory";
+ case AbstractionSource.pod_zip: return "pod zip";
+ case AbstractionSource.ssp: return ".ssp (abstraction as text)";
+ case AbstractionSource.ocda_db: return ".ocda.db (abstraction as sqlite)";
+ }
+ }
+
+ /+ ↓ what is this? by name, and for a directory by what it holds +/
+ AbstractionSource abstractionSourceOf(string _path) {
+ if (_path.length == 0) { return AbstractionSource.unknown; }
+ if (_path.isValidPath && _path.exists && _path.isDir) {
+ return (_path.chainPath("pod.manifest").array.exists)
+ ? AbstractionSource.pod : AbstractionSource.unknown;
+ }
+ if (_path.endsWith(".ocda.db")) { return AbstractionSource.ocda_db; }
+ if (_path.endsWith(".ssp")) { return AbstractionSource.ssp; }
+ if (_path.endsWith(".sst") || _path.endsWith(".ssm")) { return AbstractionSource.markup; }
+ if (_path.endsWith(".zip")) { return AbstractionSource.pod_zip; }
+ if (_path.endsWith(".db")) { return AbstractionSource.ocda_db; }
+ return AbstractionSource.unknown;
+ }
+
+ struct LoadedAbstraction {
+ AbstractionSource source;
+ string path;
+ bool loaded; // is .doc filled
+ string note; // why not, when it is not
+ SSPdocument doc;
+ }
+
+ /+ ↓ load what can be loaded from the path alone.
+
+ .ssp and .ocda.db come back filled. the three source forms come back
+ with loaded = false and a note saying where they are handled, because
+ reading them needs the manifest, environment and configuration that
+ spine.d builds, not a file path.
+ +/
+ LoadedAbstraction abstractionLoad(string _path) {
+ LoadedAbstraction _out;
+ _out.path = _path;
+ _out.source = abstractionSourceOf(_path);
+ if (!_path.exists) {
+ _out.note = "no such file or directory";
+ return _out;
+ }
+ final switch (_out.source) {
+ case AbstractionSource.ssp:
+ _out.doc = sspReadFile(_path);
+ _out.loaded = (_out.doc.section_order.length > 0);
+ if (!_out.loaded) { _out.note = "no object sections found"; }
+ break;
+ case AbstractionSource.ocda_db:
+ _out.doc = dbReadFile(_path);
+ _out.loaded = (_out.doc.section_order.length > 0);
+ if (!_out.loaded) { _out.note = "no object sections found"; }
+ break;
+ case AbstractionSource.markup:
+ case AbstractionSource.pod:
+ case AbstractionSource.pod_zip:
+ _out.note = "a source form: read by the parser,"
+ ~ " sisudoc.ocda.meta.metadoc spineAbstraction, which needs the"
+ ~ " manifest, environment and configuration spine assembles";
+ break;
+ case AbstractionSource.unknown:
+ _out.note = "not a document source spine knows";
+ break;
+ }
+ return _out;
+ }
+
+ /+ ↓ what was loaded, in a few lines: for the eye, and for a check that a
+ given artefact really does hold what it should +/
+ string[] abstractionLoadSummary(LoadedAbstraction _l) {
+ string[] _out;
+ _out ~= "source: " ~ abstractionSourceName(_l.source);
+ _out ~= "path: " ~ _l.path;
+ if (!_l.loaded) {
+ _out ~= "loaded: no";
+ if (_l.note.length > 0) { _out ~= "note: " ~ _l.note; }
+ return _out;
+ }
+ _out ~= "loaded: yes";
+ if (_l.doc.source.length > 0) { _out ~= "document: " ~ _l.doc.source; }
+ if ("title.main" in _l.doc.meta) {
+ _out ~= "title: " ~ _l.doc.meta["title.main"];
+ }
+ if ("creator.author" in _l.doc.meta) {
+ _out ~= "author: " ~ _l.doc.meta["creator.author"];
+ }
+ _out ~= "header: " ~ _l.doc.meta.length.to!string ~ " meta, "
+ ~ _l.doc.make.length.to!string ~ " make, "
+ ~ _l.doc.doc_has.length.to!string ~ " doc_has";
+ int _objs, _citable, _headings;
+ string[] _sections;
+ foreach (section; _l.doc.section_order) {
+ int _n;
+ foreach (obj; _l.doc.abstraction[section]) {
+ ++_objs; ++_n;
+ if (obj.metainfo.ocn > 0) { ++_citable; }
+ if (obj.metainfo.is_a == "heading") { ++_headings; }
+ }
+ _sections ~= section ~ " " ~ _n.to!string;
+ }
+ _out ~= "objects: " ~ _objs.to!string ~ " (" ~ _citable.to!string
+ ~ " citable, " ~ _headings.to!string ~ " headings)";
+ _out ~= "sections: " ~ _sections.join(", ");
+ return _out;
+ }
+}
diff --git a/src/sisudoc/ocda/abstraction/package.d b/src/sisudoc/ocda/abstraction/package.d
index 3a58b2e..4131a90 100644
--- a/src/sisudoc/ocda/abstraction/package.d
+++ b/src/sisudoc/ocda/abstraction/package.d
@@ -86,3 +86,4 @@ public import sisudoc.ocda.meta.metadoc_from_src; // docAbstraction (B-layer)
public import sisudoc.ocda.abstraction.ssp; // spineAbstractionTxt (.ssp)
public import sisudoc.ocda.abstraction.ssp_in; // spineAbstractionRead (.ssp back in)
public import sisudoc.ocda.abstraction.db_in; // spineAbstractionDbRead (.db back in)
+public import sisudoc.ocda.abstraction.load; // spineAbstractionLoad (one way in)