/+ - 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 ocda.dump; /+ ↓ the model, written out canonically. . Not an output format: an *oracle*. Two libraries in two languages implement the same model from the same specification, and the cheap way to ask whether they agree is to have each write the whole loaded document as text and diff the two. That is only useful if the text is fully determined by the model, so: . - every line is " ", the value running to end of line, and only one free-form value per line; - a field at its default is *omitted*, so the output says what the document has rather than what the struct declares; - dictionaries are emitted in sorted key order, because neither language's hash map has an order worth relying on; - sections come in the fixed order of the format, not alphabetically. . Anything that reads this to reconstruct a document has misunderstood it. Load the .ssp or the .ocda.db. +/ import std.algorithm : sort, splitter; import std.array : appender, array, join; import std.conv : to; import ocda.model; /+ ↓ the nine sections, in format order +/ static immutable string[] dump_sections = ["head", "toc", "body", "endnotes", "glossary", "bibliography", "bookindex", "blurb", "tail"]; string dumpDocument(ref OcdaDocument doc) { auto out_ = appender!string; void kv(string key, string value) { if (value.length == 0) { return; } out_ ~= key; out_ ~= " "; out_ ~= value; out_ ~= "\n"; } void kn(string key, long value) { if (value == 0) { return; } kv(key, value.to!string); } void kb(string key, bool value) { if (!value) { return; } kv(key, "1"); } void dict(string prefix, string[string] d) { foreach (key; d.keys.dup.sort) { out_ ~= prefix; out_ ~= " "; out_ ~= key; out_ ~= " "; out_ ~= d[key]; out_ ~= "\n"; } } kv("version", doc.format_version); kv("source_filename", doc.source_filename); dict("source", doc.source); dict("meta", doc.metadata); dict("make", doc.make); dict("doc_has", doc.doc_has); foreach (name; dump_sections) { auto sec = doc.sectionByName(name); if (sec is null || (*sec).length == 0) { continue; } kv("section", name ~ " " ~ (*sec).length.to!string); } foreach (name; dump_sections) { auto sec = doc.sectionByName(name); if (sec is null) { continue; } foreach (i, obj; *sec) { out_ ~= "object "; out_ ~= name; out_ ~= " "; out_ ~= i.to!string; out_ ~= "\n"; kn("ocn", obj.ocn); kv("is_a", obj.is_a); kv("own_section", obj.own_section); kv("level", obj.heading_level); kv("identifier", obj.identifier); kv("part", obj.part); kv("is_of_type", obj.is_of_type); kv("segment_html_is", obj.segment_html_is); kv("segment_epub", obj.segment_epub); kv("segment_epub_is", obj.segment_epub_is); kn("parent", obj.parent_ocn); kn("last_descendant", obj.last_descendant_ocn); kn("ocn_range_first", obj.ocn_range_first); kn("ocn_range_last", obj.ocn_range_last); kv("children", ints(obj.children)); kv("ancestors", ints(obj.ancestors)); kb("dummy", obj.dummy_heading); kb("ocn_off", obj.ocn_off); kn("indent_base", obj.indent_base); kn("indent_hang", obj.indent_hang); kb("bullet", obj.bullet); kv("lang", obj.lang); kb("has_links", obj.has_links); kb("has_notes_reg", obj.has_notes_reg); kb("has_notes_star", obj.has_notes_star); kb("has_images", obj.has_images); kv("segment", obj.segment); kv("segment_prev", obj.segment_prev); kv("segment_next", obj.segment_next); kv("anchor", obj.anchor); kv("heading_lev_anchor", obj.heading_lev_anchor); kn("table_cols", obj.table_cols); kv("table_widths", obj.table_widths); kv("table_aligns", obj.table_aligns); kb("table_header", obj.table_header); kv("code_syntax", obj.code_syntax); kb("code_linenumbers", obj.code_linenumbers); kv("heading_ancestors_text", obj.heading_ancestors_text); kv("sha256", obj.sha256); foreach (url; obj.stow_link) { kv("stow_link", url); } foreach (a; obj.anchor_tags) { kv("anchor_tag", a); } foreach (img; obj.images) { /+ ↓ the name last, because a filename may hold a space where none of the other fields can +/ kv("image", img.sha256 ~ " " ~ img.bytes.to!string ~ " " ~ img.width.to!string ~ "x" ~ img.height.to!string ~ " " ~ (img.missing ? "1" : "0") ~ " " ~ img.name); foreach (key; img.fields.keys.dup.sort) { kv("image_field", key ~ " " ~ img.fields[key]); } } /+ ↓ text, when there is any. An object whose only text line is blank holds "" here, indistinguishable from no text at all, because the model joins the lines into one string. Both libraries lose the same distinction in the same place, which is what makes them comparable; it is recorded rather than hidden. +/ if (obj.text.length > 0) { auto lines = obj.text.splitter("\n").array; kv("text", lines.length.to!string); foreach (line; lines) { out_ ~= "| "; out_ ~= line; out_ ~= "\n"; } } } } return out_.data; } /+ ↓ what the reader met and did not understand, sorted. Separate from the document dump: it describes the reader, not the document, and the .ocda.db loader has nothing to report where the .ssp loader does. +/ string dumpUnknown(ref OcdaDocument doc) { auto out_ = appender!string; void counts(string prefix, size_t[string] d) { foreach (key; d.keys.dup.sort) { out_ ~= prefix; out_ ~= " "; out_ ~= key; out_ ~= " "; out_ ~= d[key].to!string; out_ ~= "\n"; } } counts("property", doc.unknown.property); counts("block", doc.unknown.block); counts("line", doc.unknown.line); return out_.data; } private string ints(int[] xs) { if (xs.length == 0) { return ""; } string[] parts; foreach (x; xs) { parts ~= x.to!string; } return parts.join(","); }