aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/sisudoc/ocda/abstraction/db_in.d17
-rw-r--r--src/sisudoc/ocda/abstraction/ssp.d88
-rw-r--r--src/sisudoc/ocda/abstraction/ssp_in.d77
-rw-r--r--src/sisudoc/outputs/io_out/create_ocda_db.d94
-rw-r--r--src/sisudoc/spine.d6
5 files changed, 207 insertions, 75 deletions
diff --git a/src/sisudoc/ocda/abstraction/db_in.d b/src/sisudoc/ocda/abstraction/db_in.d
index 0da123d..3d10540 100644
--- a/src/sisudoc/ocda/abstraction/db_in.d
+++ b/src/sisudoc/ocda/abstraction/db_in.d
@@ -136,7 +136,7 @@ template spineAbstractionDbRead() {
return doc;
}
auto db = Database(db_file, SQLITE_OPEN_READONLY);
- /+ ↓ the three header blocks, from the metadata table. the key prefix
+ /+ ↓ the four header blocks, from the metadata table. the key prefix
says which block a row belongs to, and insertion order is kept +/
foreach (row; db.execute("SELECT key, value FROM metadata ORDER BY rowid")) {
string _k = row["key"].as!string;
@@ -150,16 +150,25 @@ template spineAbstractionDbRead() {
doc.doc_has[_kk] = _v;
doc.doc_has_order ~= _kk;
} else if (_k.startsWith("schema.")) {
- if (_k == "schema.version") { doc.format = "% SiSU Document Abstraction v0.1"; }
+ if (_k == "schema.version") {
+ doc.format = "% SiSU Document Abstraction v" ~ _v;
+ }
} else if (_k == "source.filename") {
doc.source = _v;
- } else if (_k == "source.language") {
- // carried in the path, not in the .ssp header
+ } else if (_k == "source.ssp_digest") {
+ /+ ↓ this database's own note of the .ssp it was built from. it is
+ not part of the .ssp, so it does not go back into the block +/
+ doc.ssp_digest = _v;
+ } else if (_k.startsWith("source.")) {
+ string _kk = _k["source.".length .. $];
+ doc.source_info[_kk] = _v;
+ doc.source_info_order ~= _kk;
} else {
doc.meta[_k] = _v;
doc.meta_order ~= _k;
}
}
+ if (!sspFormatVersionOk(doc.format, db_file)) { return SSPdocument(); }
/+ ↓ the objects, section by section, in the order they were written +/
string[] _sections;
foreach (row; db.execute(
diff --git a/src/sisudoc/ocda/abstraction/ssp.d b/src/sisudoc/ocda/abstraction/ssp.d
index d29737b..6b487d7 100644
--- a/src/sisudoc/ocda/abstraction/ssp.d
+++ b/src/sisudoc/ocda/abstraction/ssp.d
@@ -50,6 +50,21 @@
module sisudoc.ocda.abstraction.ssp;
@safe:
/+ ↓ write document abstraction as human-readable .ssp text file +/
+/+ ↓ the format version, carried by both serialisations: the .ssp says it in
+ its "% SiSU Document Abstraction v" header line and the .ocda.db in its
+ metadata schema.version row, so that one bump moves both.
+
+ a reader checks the major part and refuses what it does not know, rather
+ than loading an older artefact half populated in silence. bump the minor
+ part when a property is added (a reader of the newer format still reads
+ the older one), the major part when one changes meaning or goes away.
+
+ 1.1 2026-09-10 @source block; the document metadata the output writers
+ read carried in full (14 meta and 3 make properties
+ added); was 0.1
++/
+enum ssp_format_version = "1.1";
+enum ssp_format_version_major = "1";
/+ ↓ escaping for values that are folded onto one line
a .ssp property value runs to the end of its line, and
@@ -431,9 +446,22 @@ template spineAbstractionTxt() {
auto doc_matters = doc.matters;
string[] output;
/+ ↓ header comment +/
- output ~= "% SiSU Document Abstraction v0.1";
+ output ~= "% SiSU Document Abstraction v" ~ ssp_format_version;
output ~= "% Source: " ~ doc_matters.src.filename;
output ~= "";
+ /+ ↓ @source block: what this abstraction was made from, so a reader can
+ say whether it is looking at the right images and whether the
+ markup has moved on since. the filename is in the % Source: line
+ above and is not repeated here
+ +/
+ output ~= "@source {";
+ output ~= " language: " ~ doc_matters.src.language;
+ if (doc_matters.pod.manifest_list_of_languages.length > 0) {
+ output ~= " languages: " ~ doc_matters.pod.manifest_list_of_languages.join(" ");
+ }
+ output ~= " digest: " ~ doc_matters.doc_digest.markup_doc.toHexString.to!string;
+ output ~= "}";
+ output ~= "";
/+ ↓ @meta block +/
output ~= "@meta {";
auto meta = doc_matters.conf_make_meta.meta;
@@ -449,6 +477,9 @@ template spineAbstractionTxt() {
if (meta.title_language.length > 0) {
output ~= " title.language: " ~ meta.title_language;
}
+ if (meta.title_edition.length > 0) {
+ output ~= " title.edition: " ~ meta.title_edition;
+ }
if (meta.creator_author.length > 0) {
output ~= " creator.author: " ~ meta.creator_author;
}
@@ -485,9 +516,33 @@ template spineAbstractionTxt() {
if (meta.date_valid.length > 0) {
output ~= " date.valid: " ~ meta.date_valid;
}
+ if (meta.date_added_to_site.length > 0) {
+ output ~= " date.added_to_site: " ~ meta.date_added_to_site;
+ }
if (meta.rights_copyright.length > 0) {
output ~= " rights.copyright: " ~ meta.rights_copyright;
}
+ if (meta.rights_copyright_text.length > 0) {
+ output ~= " rights.copyright_text: " ~ meta.rights_copyright_text;
+ }
+ if (meta.rights_copyright_translation.length > 0) {
+ output ~= " rights.copyright_translation: " ~ meta.rights_copyright_translation;
+ }
+ if (meta.rights_copyright_illustrations.length > 0) {
+ output ~= " rights.copyright_illustrations: " ~ meta.rights_copyright_illustrations;
+ }
+ if (meta.rights_copyright_photographs.length > 0) {
+ output ~= " rights.copyright_photographs: " ~ meta.rights_copyright_photographs;
+ }
+ if (meta.rights_copyright_cover.length > 0) {
+ output ~= " rights.copyright_cover: " ~ meta.rights_copyright_cover;
+ }
+ if (meta.rights_copyright_audio.length > 0) {
+ output ~= " rights.copyright_audio: " ~ meta.rights_copyright_audio;
+ }
+ if (meta.rights_copyright_video.length > 0) {
+ output ~= " rights.copyright_video: " ~ meta.rights_copyright_video;
+ }
if (meta.rights_license.length > 0) {
output ~= " rights.license: " ~ meta.rights_license;
}
@@ -515,6 +570,21 @@ template spineAbstractionTxt() {
if (meta.language_document.length > 0) {
output ~= " language.document: " ~ meta.language_document;
}
+ if (meta.language_document_char.length > 0) {
+ output ~= " language.document_char: " ~ meta.language_document_char;
+ }
+ if (meta.original_title.length > 0) {
+ output ~= " original.title: " ~ meta.original_title;
+ }
+ if (meta.original_source.length > 0) {
+ output ~= " original.source: " ~ meta.original_source;
+ }
+ if (meta.original_language.length > 0) {
+ output ~= " original.language: " ~ meta.original_language;
+ }
+ if (meta.original_language_char.length > 0) {
+ output ~= " original.language_char: " ~ meta.original_language_char;
+ }
if (meta.notes_abstract.length > 0) {
output ~= " notes.abstract: " ~ meta.notes_abstract;
}
@@ -537,6 +607,22 @@ template spineAbstractionTxt() {
} // check
output ~= " auto_num_top_lv: " ~ make.auto_num_top_lv.to!string;
output ~= " auto_num_depth: " ~ make.auto_num_depth.to!string;
+ /+ ↓ the make settings the output writers read. the rest of make
+ (italics, bold, emphasis, substitute, headings) is read at parse
+ time and its effect is already in the objects, so it does not
+ travel; these three are read in outputs/io_out/ and must
+ +/
+ if (make.breaks.length > 0) {
+ output ~= " breaks: " ~ make.breaks;
+ }
+ if (make.footer.length > 0) { // a list: joined, each slot escaped
+ string[] _ft;
+ foreach (_f; make.footer) { _ft ~= sspEscapeValue(_f); }
+ output ~= " footer: " ~ _ft.join("|");
+ }
+ if (make.home_button_text.length > 0) {
+ output ~= " home_button_text: " ~ make.home_button_text;
+ }
output ~= "}";
output ~= "";
/+ ↓ @doc_has block +/
diff --git a/src/sisudoc/ocda/abstraction/ssp_in.d b/src/sisudoc/ocda/abstraction/ssp_in.d
index 220bfe3..71daf50 100644
--- a/src/sisudoc/ocda/abstraction/ssp_in.d
+++ b/src/sisudoc/ocda/abstraction/ssp_in.d
@@ -76,8 +76,11 @@ template spineAbstractionRead() {
object sections in the order the file gives them
+/
struct SSPdocument {
- string format; // "% SiSU Document Abstraction v0.1"
+ string format; // "% SiSU Document Abstraction v1.1"
string source; // "% Source: ..."
+ string[string] source_info; // the @source block
+ string[] source_info_order;
+ string ssp_digest; // sha256 of the .ssp read, hex
string[string] meta;
string[] meta_order;
string[string] make;
@@ -152,7 +155,7 @@ template spineAbstractionRead() {
_closeObject();
string _name = line[1 .. $-2];
switch (_name) {
- case "meta": case "make": case "doc_has":
+ case "source": case "meta": case "make": case "doc_has":
block = _name; section = ""; break;
default:
block = ""; section = _name;
@@ -167,6 +170,10 @@ template spineAbstractionRead() {
auto _kv = line.stripLeft.findSplit(": "); // leading only: a value may end in a space
if (_kv[1].length > 0) {
final switch (block) {
+ case "source":
+ doc.source_info[_kv[0].to!string] = _kv[2].to!string;
+ doc.source_info_order ~= _kv[0].to!string;
+ break;
case "meta":
doc.meta[_kv[0].to!string] = _kv[2].to!string;
doc.meta_order ~= _kv[0].to!string;
@@ -332,24 +339,74 @@ template spineAbstractionRead() {
}
return doc;
}
- /+ ↓ the abstraction as it survives a trip through the .ssp text: emitted
- as lines by the writer, read straight back. what a consumer built
- from this cannot see, the .ssp does not carry, which is what makes a
- second artefact built from it unable to drift
+ /+ ↓ the whole document as it survives a trip through the .ssp text:
+ emitted as lines by the writer, read straight back. what a consumer
+ built from this cannot see, the .ssp does not carry, which is what
+ makes a second artefact built from it unable to drift. the header
+ blocks come back with it, so the database is filled from these and
+ not from a second list read off doc_matters
+/
- auto sspRoundTripAbstraction(D)(D doc) {
+ SSPdocument sspRoundTripDocument(D)(D doc) {
import sisudoc.ocda.abstraction.ssp;
mixin spineAbstractionTxt;
- return sspRead(sspDocumentLines(doc)).abstraction;
+ string[] _lines = sspDocumentLines(doc);
+ auto _out = sspRead(_lines);
+ /+ ↓ the writer emits one line per entry, each with its newline, so this
+ is the digest of the file that is written beside the pod +/
+ _out.ssp_digest = sspDigestOfText(_lines.join("\n") ~ "\n");
+ return _out;
+ }
+ /+ ↓ sha256 of a .ssp, upper case hex, as digests.txt gives it +/
+ string sspDigestOfText(string _text) {
+ import std.digest : toHexString;
+ import std.digest.sha : sha256Of;
+ return _text.sha256Of.toHexString.to!string;
+ }
+ auto sspRoundTripAbstraction(D)(D doc) {
+ return sspRoundTripDocument(doc).abstraction;
+ }
+ /+ ↓ the version a reader will accept.
+
+ the major part must match: a format whose properties have changed
+ meaning is not readable by guesswork, and loading it half populated in
+ silence is the failure this check exists to stop. a newer minor
+ version is accepted, since a minor bump only adds properties and an
+ unknown property line is ignored.
+ +/
+ bool sspFormatVersionOk(string _format_line, string _what) {
+ import sisudoc.ocda.abstraction.ssp : ssp_format_version, ssp_format_version_major;
+ enum _prefix = "% SiSU Document Abstraction v";
+ if (_format_line.length == 0) {
+ writeln("ERROR: ", _what, ": no format line, not a .ssp");
+ return false;
+ }
+ if (!_format_line.startsWith(_prefix)) {
+ writeln("ERROR: ", _what, ": unrecognised format line: ", _format_line);
+ return false;
+ }
+ string _ver = _format_line[_prefix.length .. $].strip;
+ string _major = _ver.findSplit(".")[0];
+ if (_major != ssp_format_version_major) {
+ writeln("ERROR: ", _what, ": .ssp format v", _ver,
+ ", this spine reads v", ssp_format_version_major, ".x (v",
+ ssp_format_version, "). Regenerate the abstraction from its markup.");
+ return false;
+ }
+ return true;
}
SSPdocument sspReadFile(string file_path) {
+ string _text;
string[] _lines;
try {
- _lines = (cast(char[]) file_path.read).to!string.split("\n");
+ _text = (cast(char[]) file_path.read).to!string;
+ _lines = _text.split("\n");
} catch (Exception ex) {
writeln("ERROR: could not read ", file_path, ": ", ex.msg);
return SSPdocument();
}
- return sspRead(_lines);
+ auto _doc = sspRead(_lines);
+ if (!sspFormatVersionOk(_doc.format, file_path)) { return SSPdocument(); }
+ _doc.ssp_digest = sspDigestOfText(_text);
+ return _doc;
}
}
diff --git a/src/sisudoc/outputs/io_out/create_ocda_db.d b/src/sisudoc/outputs/io_out/create_ocda_db.d
index 8f915fa..f184854 100644
--- a/src/sisudoc/outputs/io_out/create_ocda_db.d
+++ b/src/sisudoc/outputs/io_out/create_ocda_db.d
@@ -61,13 +61,17 @@ template spineAbstractionDb() {
import d2sqlite3;
import sisudoc.outputs.io_out.paths_output;
import sisudoc.ocda.io_in.paths_source;
- /+ ↓ the abstraction is passed in rather than taken from doc, because the
+ import sisudoc.ocda.abstraction.ssp : ssp_format_version;
+ /+ ↓ the document is passed in rather than taken from doc, because the
caller hands over one that has been through the .ssp: written as text
and read back. so the database is a function of the .ssp and cannot
- carry anything the .ssp does not
+ carry anything the .ssp does not, header blocks included: the metadata
+ table below is filled from what came back, not from a second list read
+ off doc_matters, which is how the two drifted apart before
+/
- void spineAbstractionDb(D,A)(D doc, A doc_abstraction) {
+ void spineAbstractionDb(D,S)(D doc, S ssp_doc) {
auto doc_matters = doc.matters;
+ auto doc_abstraction = ssp_doc.abstraction;
/+ ↓ output path: beside the pod's zip and digests, not under
<lang>/abstraction/. the database carries its own images, so it is a
distributable artefact in its own right and belongs with the others:
@@ -259,13 +263,18 @@ template spineAbstractionDb() {
SELECT role, name, bytes, sha256, width, height FROM files ORDER BY role, name;
");
- /+ ↓ populate metadata +/
+ /+ ↓ populate metadata, from the header blocks the .ssp gave back.
+ one list, in ssp.d, not two: a property added to the .ssp header
+ arrives here without anything being changed, and one that is not in
+ the .ssp cannot be here at all. the keys are the .ssp's own
+ (title.main, make.doc_type, doc_has.images), prefixed by their block
+ where the block name is not already part of the key.
+ +/
db.run("BEGIN TRANSACTION");
auto meta_stmt = db.prepare(
"INSERT INTO metadata (key, value) VALUES (:key, :value)"
);
- auto meta = doc_matters.conf_make_meta.meta;
void insertMeta(string key, string value) {
if (value.length > 0) {
@@ -275,62 +284,31 @@ template spineAbstractionDb() {
meta_stmt.reset();
}
}
+ void insertBlock(string prefix, string[] keys_in_order, string[string] kv) {
+ foreach (k; keys_in_order) {
+ if (k in kv) { insertMeta(prefix ~ k, kv[k]); }
+ }
+ }
- /+ ↓ what this file is, so a reader can tell before it starts +/
+ /+ ↓ what this file is, so a reader can tell before it starts. the
+ version is the .ssp's own: the two serialisations are one format and
+ move together
+ +/
insertMeta("schema.name", "sisu-abstraction-db");
- insertMeta("schema.version", "1");
- insertMeta("source.filename", doc_matters.src.filename);
- insertMeta("source.language", doc_matters.src.language);
-
- insertMeta("title.main", meta.title_main);
- insertMeta("title.subtitle", meta.title_subtitle);
- insertMeta("title.full", meta.title_full);
- insertMeta("title.language", meta.title_language);
- insertMeta("creator.author", meta.creator_author);
- insertMeta("creator.author_surname", meta.creator_author_surname);
- insertMeta("creator.author_surname_fn", meta.creator_author_surname_fn);
- insertMeta("creator.author_email", meta.creator_author_email);
- insertMeta("creator.illustrator", meta.creator_illustrator);
- insertMeta("creator.translator", meta.creator_translator);
- insertMeta("date.published", meta.date_published);
- insertMeta("date.created", meta.date_created);
- insertMeta("date.issued", meta.date_issued);
- insertMeta("date.available", meta.date_available);
- insertMeta("date.modified", meta.date_modified);
- insertMeta("date.valid", meta.date_valid);
- insertMeta("rights.copyright", meta.rights_copyright);
- insertMeta("rights.license", meta.rights_license);
- insertMeta("classify.topic_register", meta.classify_topic_register);
- insertMeta("classify.subject", meta.classify_subject);
- insertMeta("classify.keywords", meta.classify_keywords);
- insertMeta("classify.loc", meta.classify_loc);
- insertMeta("classify.dewey", meta.classify_dewey);
- insertMeta("identifier.isbn", meta.identifier_isbn);
- insertMeta("identifier.oclc", meta.identifier_oclc);
- insertMeta("language.document", meta.language_document);
- insertMeta("notes.abstract", meta.notes_abstract);
- insertMeta("notes.description", meta.notes_description);
- insertMeta("notes.summary", meta.notes_summary);
-
- /+ ↓ make settings +/
- auto make = doc_matters.conf_make_meta.make;
- insertMeta("make.doc_type", make.doc_type);
- insertMeta("make.auto_num_top_at_level", make.auto_num_top_at_level);
- insertMeta("make.auto_num_top_lv", make.auto_num_top_lv.to!string);
- insertMeta("make.auto_num_depth", make.auto_num_depth.to!string);
-
- /+ ↓ doc_has counts +/
- insertMeta("doc_has.inline_links", doc_matters.has.inline_links.to!string);
- insertMeta("doc_has.inline_notes_reg", doc_matters.has.inline_notes_reg.to!string);
- insertMeta("doc_has.inline_notes_star", doc_matters.has.inline_notes_star.to!string);
- insertMeta("doc_has.tables", doc_matters.has.tables.to!string);
- insertMeta("doc_has.codeblocks", doc_matters.has.codeblocks.to!string);
- insertMeta("doc_has.images", doc_matters.has.images.to!string);
- insertMeta("doc_has.poems", doc_matters.has.poems.to!string);
- insertMeta("doc_has.groups", doc_matters.has.groups.to!string);
- insertMeta("doc_has.blocks", doc_matters.has.blocks.to!string);
- insertMeta("doc_has.quotes", doc_matters.has.quotes.to!string);
+ insertMeta("schema.version", ssp_format_version);
+ insertMeta("source.filename", ssp_doc.source);
+ insertBlock("source.", ssp_doc.source_info_order, ssp_doc.source_info);
+ /+ ↓ the digest of the .ssp this database was built from. a file cannot
+ hold its own hash, so this one is not in the .ssp and is taken here:
+ source.digest says which markup the abstraction came from,
+ source.ssp_digest says which abstraction this database came from,
+ and the chain .sst -> .ssp -> .ocda.db is checkable end to end
+ +/
+ insertMeta("source.ssp_digest", ssp_doc.ssp_digest);
+ insertBlock("", ssp_doc.meta_order, ssp_doc.meta);
+ insertBlock("make.", ssp_doc.make_order, ssp_doc.make);
+ insertBlock("doc_has.", ssp_doc.doc_has_order, ssp_doc.doc_has);
meta_stmt.finalize();
/+ ↓ populate objects +/
diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d
index 9bbe10f..5560f99 100644
--- a/src/sisudoc/spine.d
+++ b/src/sisudoc/spine.d
@@ -383,6 +383,7 @@ string program_name = "spine";
_out ~= "}";
_out ~= "";
}
+ _block("source", _doc.source_info_order, _doc.source_info);
_block("meta", _doc.meta_order, _doc.meta);
_block("make", _doc.make_order, _doc.make);
_block("doc_has", _doc.doc_has_order, _doc.doc_has);
@@ -425,6 +426,7 @@ string program_name = "spine";
_out ~= "}";
_out ~= "";
}
+ _blockdb("source", _doc.source_info_order, _doc.source_info);
_blockdb("meta", _doc.meta_order, _doc.meta);
_blockdb("make", _doc.make_order, _doc.make);
_blockdb("doc_has", _doc.doc_has_order, _doc.doc_has);
@@ -1449,7 +1451,7 @@ string program_name = "spine";
import sisudoc.ocda.abstraction.ssp_in;
import sisudoc.outputs.io_out.create_ocda_db;
mixin spineAbstractionRead;
- spineAbstractionDb!()(doc, sspRoundTripAbstraction(doc));
+ spineAbstractionDb!()(doc, sspRoundTripDocument(doc));
}
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
@@ -1564,7 +1566,7 @@ string program_name = "spine";
import sisudoc.ocda.abstraction.ssp_in;
import sisudoc.outputs.io_out.create_ocda_db;
mixin spineAbstractionRead;
- spineAbstractionDb!()(doc, sspRoundTripAbstraction(doc));
+ spineAbstractionDb!()(doc, sspRoundTripDocument(doc));
}
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);