diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-08 13:52:43 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-09 18:22:47 -0400 |
| commit | c7049608de7d163137521495665753ae7070be08 (patch) | |
| tree | b9617c6e84ea4be4a41b794a7d515986eef01af1 | |
| parent | ocda 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)
| -rw-r--r-- | org/in_source_files.org | 165 | ||||
| -rw-r--r-- | org/ocda.org | 1 | ||||
| -rw-r--r-- | org/spine.org | 18 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/load.d | 205 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/package.d | 1 | ||||
| -rw-r--r-- | src/sisudoc/spine.d | 18 |
6 files changed, 408 insertions, 0 deletions
diff --git a/org/in_source_files.org b/org/in_source_files.org index 04792ec..dc6571e 100644 --- a/org/in_source_files.org +++ b/org/in_source_files.org @@ -1312,6 +1312,171 @@ template spineAbstractionDbRead() { } #+END_SRC +* E. read a .db file into the document abstraction +** _module template_ :module:db_files: + +#+HEADER: :tangle "../src/sisudoc/ocda/abstraction/load.d" +#+HEADER: :noweb yes +#+BEGIN_SRC d +<<doc_header_including_copyright_and_license>> +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; + } +} +#+END_SRC + * org includes ** project version diff --git a/org/ocda.org b/org/ocda.org index 3ca708b..535cd94 100644 --- a/org/ocda.org +++ b/org/ocda.org @@ -95,6 +95,7 @@ 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) #+END_SRC ** _module template_ :module:metadoc_from_src: diff --git a/org/spine.org b/org/spine.org index 438bdd8..2d969e1 100644 --- a/org/spine.org +++ b/org/spine.org @@ -397,6 +397,7 @@ string[string] settings = [ "output" : "", "ssp-round-trip" : "", "db-round-trip" : "", + "abstraction-source" : "", "www-http" : "", "www-host" : "", "www-host-doc-root" : "", @@ -465,6 +466,7 @@ auto helpInfo = getopt(args, "output", "=/path/to/output/dir specify where to place output", &settings["output"], "ssp-round-trip", "=/path/to/file.ssp read a .ssp back and re-emit it on stdout", &settings["ssp-round-trip"], "db-round-trip", "=/path/to/file.ocda.db read it back and emit .ssp on stdout", &settings["db-round-trip"], + "abstraction-source", "=/path/to/(.sst|pod|.ssp|.ocda.db) identify it, and load it if it is an abstraction", &settings["abstraction-source"], "parallel", "parallelisation", &opts["parallel"], "parallel-subprocesses", "nested parallelisation", &opts["parallel-subprocesses"], "pdf", "latex output for pdfs", &opts["pdf"], @@ -632,6 +634,22 @@ if (settings["db-round-trip"].length > 0) { stdout.flush; exit(0); } +/+ ↓ say what a path is and, where it is an abstraction artefact, load it + and report what came back. one way in for any of the sources spine + knows, which is what the loader package is for. + exit 0 when an abstraction was loaded, 1 when it was not, with the + reason given: an unknown path, or a source form that goes through the + parser rather than through a file read ++/ +if (settings["abstraction-source"].length > 0) { + import sisudoc.ocda.abstraction.load; + mixin spineAbstractionLoad; + auto _loaded = abstractionLoad(settings["abstraction-source"]); + foreach (line; abstractionLoadSummary(_loaded)) { writeln(line); } + import core.stdc.stdlib : exit; + stdout.flush; + exit(_loaded.loaded ? 0 : 1); +} #+END_SRC **** getopt hash to struct 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) diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d index a1b30bd..1c68045 100644 --- a/src/sisudoc/spine.d +++ b/src/sisudoc/spine.d @@ -215,6 +215,7 @@ string program_name = "spine"; "output" : "", "ssp-round-trip" : "", "db-round-trip" : "", + "abstraction-source" : "", "www-http" : "", "www-host" : "", "www-host-doc-root" : "", @@ -277,6 +278,7 @@ string program_name = "spine"; "output", "=/path/to/output/dir specify where to place output", &settings["output"], "ssp-round-trip", "=/path/to/file.ssp read a .ssp back and re-emit it on stdout", &settings["ssp-round-trip"], "db-round-trip", "=/path/to/file.ocda.db read it back and emit .ssp on stdout", &settings["db-round-trip"], + "abstraction-source", "=/path/to/(.sst|pod|.ssp|.ocda.db) identify it, and load it if it is an abstraction", &settings["abstraction-source"], "parallel", "parallelisation", &opts["parallel"], "parallel-subprocesses", "nested parallelisation", &opts["parallel-subprocesses"], "pdf", "latex output for pdfs", &opts["pdf"], @@ -438,6 +440,22 @@ string program_name = "spine"; stdout.flush; exit(0); } + /+ ↓ say what a path is and, where it is an abstraction artefact, load it + and report what came back. one way in for any of the sources spine + knows, which is what the loader package is for. + exit 0 when an abstraction was loaded, 1 when it was not, with the + reason given: an unknown path, or a source form that goes through the + parser rather than through a file read + +/ + if (settings["abstraction-source"].length > 0) { + import sisudoc.ocda.abstraction.load; + mixin spineAbstractionLoad; + auto _loaded = abstractionLoad(settings["abstraction-source"]); + foreach (line; abstractionLoadSummary(_loaded)) { writeln(line); } + import core.stdc.stdlib : exit; + stdout.flush; + exit(_loaded.loaded ? 0 : 1); + } enum outTask { source_or_pod, sqlite, sqlite_multi, latex, odt, epub, html_scroll, html_seg, html_stuff, text, skel } struct OptActions { @trusted bool allow_downloads() { |
