aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-21 16:12:56 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-23 08:22:15 -0400
commit8b81e9a11c7af642c4d0d8584a9cb348cc887d6e (patch)
tree8ba9c356ff71bf18f5623c22d30a8b2513321cfe /src
parentuid and paths separator "~" in place of ":" (diff)
ocda: warn if heading claims reserved segment name
spine automatically builds some segments, including: (toc, endnotes, glossary, bibliography, bookindex, blurb, _the_title) these are now identified as reserved names and a user is now warned if any of these names have been manually assigned to a heading by markup. A document still builds but to disambiguate the ocn of the heading is attached to the markup (reserved) name and this is seeded before a document not after. It is read off the finished abstraction rather than reported by the parser, and beside the ocn alignment check for the same reason: it is a statement about a document rather than a step in building one. WARNING reserved segment name: the_autonomous_contract... [en] heading 1 at ocn 135 asks for "endnotes", which spine gives its own generated section it is named "endnotes-135" instead; ... A warning: the document is correct and complete and the name it ends up with works. --strict makes it a failure for the run, as it does for ocn alignment, and by the same reasoning: the outputs are written and can be looked at, and the exit status is taken at the end. Two of the thirty-six sample documents have reserved segment names, "1~endnotes" heading. Output is unchanged: nothing here touches the abstraction. (assisted by Claude-Code)
Diffstat (limited to 'src')
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src.d12
-rw-r--r--src/sisudoc/ocda/meta/metadoc_from_src_functions.d15
-rw-r--r--src/sisudoc/ocda/meta/reserved_names.d206
-rw-r--r--src/sisudoc/spine.d58
4 files changed, 281 insertions, 10 deletions
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src.d b/src/sisudoc/ocda/meta/metadoc_from_src.d
index 2731573..3670a39 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src.d
@@ -97,6 +97,18 @@ template docAbstraction() {
anchor_tag = "";
_heading_anchor_tags_seen = _reserved_heading_anchor_tags();
}
+ /+ ↓ seeded here, on the way in, which is the seeding that matters.
+ .
+ The two assignments in scope(exit) and at the tail of this function run
+ after a document has been abstracted, so they seed the *next* one. The
+ first document of a run had nothing before it and began with the set as
+ declared, empty: it alone was not held to the reserved names, and a heading
+ of its own claiming "endnotes" kept the name of the generated section.
+ Which document that was depended on what else was in the run, so a
+ document's abstraction depended on its company, and a single document
+ re-parsed to check an artefact made in a collection run disagreed with it.
+ +/
+ _heading_anchor_tags_seen = _reserved_heading_anchor_tags();
mixin spineNode;
int[string] node_para_int_ = node_metadata_para_int;
string[string] node_para_str_ = node_metadata_para_str;
diff --git a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
index 0deeb72..c5a63cd 100644
--- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
+++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d
@@ -77,16 +77,15 @@ template docAbstractionFunctions() {
these names means the document's heading is disambiguated the same
way a repeated anchor tag is, and the generated section keeps the
name that the table of contents links to.
+ .
+ The list itself is in sisudoc.ocda.meta.reserved_names, with the
+ check that reports such a heading to its author. Two lists would be
+ two answers to what spine has taken.
+/
bool[string] _reserved_heading_anchor_tags() {
- bool[string] _reserved;
- foreach (_name; [
- "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb",
- "_the_title",
- ]) {
- _reserved[_name] = true;
- }
- return _reserved;
+ import sisudoc.ocda.meta.reserved_names;
+ mixin spineReservedNames _rn;
+ return _rn.reservedHeadingAnchorTags();
}
string lev_anchor_tag;
string[string][string] tag_assoc;
diff --git a/src/sisudoc/ocda/meta/reserved_names.d b/src/sisudoc/ocda/meta/reserved_names.d
new file mode 100644
index 0000000..8e49ae5
--- /dev/null
+++ b/src/sisudoc/ocda/meta/reserved_names.d
@@ -0,0 +1,206 @@
+/+
+- 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/]
+
++/
+/++
+ the segment names spine keeps for itself<br><br>
+
+ the one list of them, and the check that says when a document's own
+ heading has claimed one<br><br>
+
+ [sisudoc.ocda.meta.reserved_names]
++/
+module sisudoc.ocda.meta.reserved_names;
+@safe:
+/+ ↓ spine names the sections it generates, these names are reserved.
+ .
+ The table of contents, the gathered endnotes, the glossary, the
+ bibliography, the book index and the blurb are not in the markup: spine
+ makes them, and gives each a segment name of its own. A document whose
+ own heading asks for one of those names ("1~endnotes Endnote") is asking
+ for a name that is already taken, and before this was noticed both wrote
+ into the same file: the generated section and the author's heading, with
+ the navigation listing that file twice, the second time behind its own
+ earlier entry in reading order.
+ .
+ The parser therefore seeds the set of heading anchor names it has seen
+ with these before it reads a document, so that a heading claiming one is
+ disambiguated by ocn suffix the same way a repeated anchor tag is, and
+ the generated section keeps the name the table of contents links to.
+ .
+ Two things live here rather than in the parser:
+ .
+ the list so that the parser's seeding and the check below cannot
+ come to hold different ideas of what is reserved;
+ .
+ the check which runs over a finished abstraction, names the
+ headings that were renamed, and is what tells an author
+ that a heading of theirs is not called what they wrote.
+ A warning: the document is correct and complete, and the
+ name it ends up with works. --strict makes it a failure,
+ for the run that is meant to be publishable.
+ .
+ The renaming is not something the author has to accept. A heading given
+ any other anchor name ("1~my-notes Endnote") keeps it, and the warning
+ goes away.
++/
+/+ ↓ mixed in as a named mixin (mixin spineReservedNames _name;) and its
+ imports are inside its functions rather than at template scope: what a
+ mixin template declares at its own scope is visible in the scope it is
+ mixed into, and an import there can hijack a name the host was already
+ resolving by UFCS.
++/
+template spineReservedNames() {
+ /+ ↓ the names spine makes for itself, in one place.
+ _the_title is the head section's own; the rest are the generated
+ sections, and are the section keys of the abstraction.
+ +/
+ string[] reservedHeadingAnchorNames() {
+ return [
+ "toc", "endnotes", "glossary", "bibliography", "bookindex", "blurb",
+ "_the_title",
+ ];
+ }
+ /+ ↓ the same, as the set the parser seeds itself with +/
+ bool[string] reservedHeadingAnchorTags() {
+ bool[string] _reserved;
+ foreach (_name; reservedHeadingAnchorNames()) { _reserved[_name] = true; }
+ return _reserved;
+ }
+ /+ ↓ one heading that asked for a name spine had already taken +/
+ struct ST_ReservedNameUse {
+ string section; // where in the abstraction it sits
+ string reserved; // the name it asked for
+ string anchor; // the name it was given instead
+ string level; // as marked up, :A :B 1 2 ...
+ int ocn;
+ }
+ /+ ↓ was this heading renamed out of the way of a reserved name?
+ .
+ Read off the anchor rather than reported by the parser: the parser
+ renames by appending the object's number, so "endnotes" becomes
+ "endnotes-135" on the object whose ocn is 135, and that pairing is what
+ identifies it. Requiring the number to be the object's own is what keeps
+ a heading an author really did call "endnotes-135" from being reported as
+ something it is not.
+ +/
+ private string _reservedNameClaimedBy(O)(O obj) {
+ import std.conv : to;
+ string _anchor = obj.tags.anchor_tag_html;
+ if (_anchor.length == 0) { return ""; }
+ string _suffix = "-" ~ obj.metainfo.ocn.to!string;
+ if (_anchor.length <= _suffix.length) { return ""; }
+ if (_anchor[($ - _suffix.length) .. $] != _suffix) { return ""; }
+ string _stem = _anchor[0 .. ($ - _suffix.length)];
+ foreach (_name; reservedHeadingAnchorNames()) {
+ if (_stem == _name) { return _name; }
+ }
+ return "";
+ }
+ /+ ↓ every heading of a document that claimed a reserved name.
+ document order, the section order the .ssp is written in, so that two
+ runs over the same document report it the same way round.
+ +/
+ ST_ReservedNameUse[] reservedNameHeadings(D)(D doc) {
+ import std.algorithm : canFind, sort;
+ import std.conv : to;
+ ST_ReservedNameUse[] _out;
+ string[] _sections = ["head", "toc", "body", "endnotes",
+ "glossary", "bibliography", "bookindex", "blurb", "tail"];
+ string[] _extra;
+ foreach (_k; doc.abstraction.byKey) {
+ if (!_sections.canFind(_k)) { _extra ~= _k; }
+ }
+ foreach (_k; _extra.sort) { _sections ~= _k; }
+ foreach (section; _sections) {
+ if (section !in doc.abstraction) { continue; }
+ foreach (obj; doc.abstraction[section]) {
+ if (obj.metainfo.is_a != "heading") { continue; }
+ string _claimed = _reservedNameClaimedBy(obj);
+ if (_claimed.length == 0) { continue; }
+ /+ ↓ the generated sections carry the reserved names themselves and are
+ not what this is about: they are headings spine made, and they keep
+ the plain name. Only an object that was renamed reaches here, and a
+ generated one never is.
+ +/
+ ST_ReservedNameUse _u;
+ _u.section = section;
+ _u.reserved = _claimed;
+ _u.anchor = obj.tags.anchor_tag_html;
+ _u.level = obj.metainfo.marked_up_level;
+ _u.ocn = obj.metainfo.ocn.to!int;
+ _out ~= _u;
+ }
+ }
+ return _out;
+ }
+ /+ ↓ the report, as the lines to print.
+ the document is named once and its headings indented under it, as the ocn
+ alignment check does, so that a run over a collection reads as a list of
+ documents.
+ +/
+ string[] reservedNameReportLines(
+ string doc_key,
+ string lang,
+ ST_ReservedNameUse[] _uses,
+ ) {
+ import std.conv : to;
+ string[] _out;
+ if (_uses.length == 0) { return _out; }
+ _out ~= "WARNING reserved segment name: " ~ doc_key
+ ~ ((lang.length > 0) ? " [" ~ lang ~ "]" : "");
+ foreach (_u; _uses) {
+ _out ~= " heading " ~ _u.level ~ " at ocn " ~ _u.ocn.to!string
+ ~ " asks for \"" ~ _u.reserved
+ ~ "\", which spine gives its own generated section";
+ _out ~= " it is named \"" ~ _u.anchor
+ ~ "\" instead; give the heading another name to keep one of your own";
+ }
+ return _out;
+ }
+}
diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d
index 18beda2..c1a3f63 100644
--- a/src/sisudoc/spine.d
+++ b/src/sisudoc/spine.d
@@ -72,6 +72,7 @@ import sisudoc.ocda.meta.conf_make_meta_json;
import sisudoc.ocda.meta.defaults;
import sisudoc.ocda.meta.doc_debugs;
import sisudoc.ocda.meta.ocn_align;
+import sisudoc.ocda.meta.reserved_names;
import sisudoc.ocda.meta.rgx;
import sisudoc.ocda.meta.rgx_yaml;
import sisudoc.ocda.meta.rgx_files;
@@ -438,8 +439,11 @@ Verifying:
check is otherwise automatic and runs before
anything is written.
- --strict document checks that warn become failures, ocn
- alignment between languages among them.
+ --strict document checks that warn become failures: ocn
+ alignment between languages, and a heading that
+ claims a segment name spine keeps for a section of
+ its own (toc, endnotes, glossary, bibliography,
+ bookindex, blurb).
┃");
/+ ↓ --help is a question answered on stdout, not a run: the
scope(success) banner would be the last line of the answer
@@ -1681,6 +1685,12 @@ Verifying:
+/
mixin spineOcnAlign _ocna;
_ocna.ST_OcnProfile[] _ocn_profiles;
+ /+ ↓ headings that claimed a name spine keeps for a generated section.
+ Reported as each document goes by, where the document can be named, and
+ remembered here only so that --strict has something to fail on.
+ +/
+ mixin spineReservedNames _rsvd;
+ bool _reserved_name_used = false;
/+ ↓ what verify found, gathered as the documents go by +/
size_t _verify_checked = 0;
size_t _verify_failed = 0;
@@ -1854,6 +1864,23 @@ Verifying:
}
synchronized { _ocn_profiles ~= _ocn_p; }
}
+ /+ ↓ a heading of this document that asked for a name spine uses for a section
+ of its own. Said here, where the document can be named, and per language:
+ the markup is per language and so is the answer.
+ +/
+ {
+ auto _rsvd_uses = _rsvd.reservedNameHeadings(doc);
+ if (_rsvd_uses.length > 0) {
+ foreach (_line; _rsvd.reservedNameReportLines(
+ doc.matters.src.doc_uid_out_no_lang,
+ doc.matters.src.language,
+ _rsvd_uses,
+ )) {
+ stderr.writeln(_line);
+ }
+ synchronized { _reserved_name_used = true; }
+ }
+ }
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
if (
@@ -2058,6 +2085,20 @@ Verifying:
}
_ocn_profiles ~= _ocn_p;
}
+ /+ ↓ reserved segment names, as above +/
+ {
+ auto _rsvd_uses = _rsvd.reservedNameHeadings(doc);
+ if (_rsvd_uses.length > 0) {
+ foreach (_line; _rsvd.reservedNameReportLines(
+ doc.matters.src.doc_uid_out_no_lang,
+ doc.matters.src.language,
+ _rsvd_uses,
+ )) {
+ stderr.writeln(_line);
+ }
+ _reserved_name_used = true;
+ }
+ }
if (doc.matters.opt.action.curate) {
auto _hvst = spineMetaDocCurate!()(doc.matters, hvst);
if (
@@ -2288,6 +2329,19 @@ Verifying:
import core.stdc.stdlib : exit;
exit(1);
}
+ /+ ↓ --strict and a heading that claimed a name spine keeps for itself.
+ The same shape as the check above: the warning has already named the
+ document and the heading, the output of the run is complete and can be
+ looked at, and this is the exit status. A publishable run should not
+ contain a heading called something other than what its author wrote.
+ +/
+ if (_reserved_name_used && _opt_action.strict) {
+ stderr.writeln(
+ "~ run FAILED ~ --strict: a heading claims a reserved segment name"
+ );
+ import core.stdc.stdlib : exit;
+ exit(1);
+ }
/+ ↓ a verification that failed is a failed run, whatever else went well.
--no-verify has already said its piece per document and does not reach
here: it turns the failure into a warning and the output is produced