diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/sisudoc/ocda/abstraction/db_in.d | 17 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/ssp.d | 88 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/ssp_in.d | 77 | ||||
| -rw-r--r-- | src/sisudoc/outputs/io_out/create_ocda_db.d | 94 | ||||
| -rw-r--r-- | src/sisudoc/spine.d | 6 |
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); |
