diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-07 22:30:03 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-09 17:45:07 -0400 |
| commit | da575bf8a80cc2900bd5984718485cdacca0f527 (patch) | |
| tree | caf36b04621a87d6f063914bd2a62340a11d401c /src/sisudoc | |
| parent | ocda: the eight slot arrays are fixed length (diff) | |
ocda: clean heading text used for navigation
heading text used for navigation is normalised, and | escaped
(assisted by Claude-Code)
Diffstat (limited to 'src/sisudoc')
| -rw-r--r-- | src/sisudoc/ocda/abstraction/ssp.d | 49 | ||||
| -rw-r--r-- | src/sisudoc/ocda/abstraction/ssp_in.d | 25 | ||||
| -rw-r--r-- | src/sisudoc/ocda/meta/metadoc_from_src_functions.d | 43 |
3 files changed, 97 insertions, 20 deletions
diff --git a/src/sisudoc/ocda/abstraction/ssp.d b/src/sisudoc/ocda/abstraction/ssp.d index 181e785..6aa55c2 100644 --- a/src/sisudoc/ocda/abstraction/ssp.d +++ b/src/sisudoc/ocda/abstraction/ssp.d @@ -50,6 +50,48 @@ module sisudoc.ocda.abstraction.ssp; @safe: /+ ↓ write document abstraction as human-readable .ssp text file +/ +/+ ↓ escaping for values that are folded onto one line + + a .ssp property value runs to the end of its line, and + .heading_ancestors_text joins its eight slots with "|", so a slot that + itself contains "|", a backslash or a line break has to be escaped or + the value cannot be read back. the pair here is the whole of the rule, + used by the writer and by the reader. ++/ +template sspEscape() { + import std.array : replace; + string sspEscapeValue(string _s) { + return _s + .replace("\\", "\\\\") + .replace("|", "\\|") + .replace("\n", "\\n") + .replace("\r", "\\r"); + } + string[] sspUnescapeSplit(string _s) { + string[] _out; + string _cur; + bool _esc; + foreach (c; _s) { + if (_esc) { + switch (c) { + case 'n': _cur ~= '\n'; break; + case 'r': _cur ~= '\r'; break; + default: _cur ~= c; break; // covers \\ and \| + } + _esc = false; + } else if (c == '\\') { + _esc = true; + } else if (c == '|') { + _out ~= _cur; + _cur = ""; + } else { + _cur ~= c; + } + } + _out ~= _cur; + return _out; + } +} /+ ↓ one object record: the single definition of what an object looks like in a .ssp file. the writer emits with it, and the reader is checked against it by round trip, so the two cannot drift @@ -60,6 +102,7 @@ template sspObjectRecord() { import std.stdio; import std.string; import std.array; + mixin sspEscape; string[] sspObjectRecord(O)(O obj, string section, bool _vox = false) { string[] output; /+ ↓ object declaration line. a heading carries its identifier here, @@ -328,7 +371,11 @@ template sspObjectRecord() { if (h.length > 0) { has_hat = true; break; } } if (has_hat) { - output ~= ".heading_ancestors_text: " ~ obj.tags.heading_ancestors_text.join("|"); + string[] _hat; + foreach (h; obj.tags.heading_ancestors_text) { + _hat ~= sspEscapeValue(h.to!string); + } + output ~= ".heading_ancestors_text: " ~ _hat.join("|"); } } /+ ↓ lev4 subtoc +/ diff --git a/src/sisudoc/ocda/abstraction/ssp_in.d b/src/sisudoc/ocda/abstraction/ssp_in.d index b393ac0..f0fb963 100644 --- a/src/sisudoc/ocda/abstraction/ssp_in.d +++ b/src/sisudoc/ocda/abstraction/ssp_in.d @@ -50,12 +50,12 @@ module sisudoc.ocda.abstraction.ssp_in; @safe: /+ ↓ read a .ssp file back into the document abstraction - + . the reverse of sisudoc.ocda.abstraction.ssp. what it returns is the same value the parser produces, ObjGenericComposite[][string], so anything that consumes the abstraction can be fed from a .ssp instead of from markup. - + . the check that it is faithful is a round trip: load a .ssp, emit each object again with sspObjectRecord (the writer's own definition of a record) and require the result to be byte identical to the file read. @@ -69,9 +69,12 @@ template spineAbstractionRead() { import std.stdio; import std.string; import sisudoc.ocda.meta.metadoc_object_setter; + import sisudoc.ocda.abstraction.ssp : sspEscape; + mixin sspEscape; mixin ObjectSetter; /+ ↓ what a .ssp file holds: the three header blocks as key/value, and the - object sections in the order the file gives them +/ + object sections in the order the file gives them + +/ struct SSPdocument { string format; // "% SiSU Document Abstraction v0.1" string source; // "% Source: ..." @@ -105,9 +108,10 @@ template spineAbstractionRead() { return _out; } /+ ↓ the eight slot arrays are int[] with a literal default, so every - default constructed object shares one array; write into a fresh one +/ - private int[] _eight(string val) { - int[] _out = [ 0, 0, 0, 0, 0, 0, 0, 0, ]; + default constructed object shares one array; write into a fresh one + +/ + private int[8] _eight(string val) { + int[8] _out; foreach (i, v; _ints(val)) { if (i < _out.length) { _out[i] = v; } } @@ -297,9 +301,9 @@ template spineAbstractionRead() { case "segment_epub_is": obj.tags.epub_segment_anchor_tag_is = _val; break; case "heading_ancestors_text": { - string[] _h = [ "", "", "", "", "", "", "", "", ]; - foreach (i, h; _val.split("|")) { - if (i < _h.length) { _h[i] = h.to!string; } + string[8] _h; + foreach (i, h; sspUnescapeSplit(_val)) { + if (i < _h.length) { _h[i] = h; } } obj.tags.heading_ancestors_text = _h; } @@ -311,7 +315,8 @@ template spineAbstractionRead() { } _closeObject(); /+ ↓ .anchor is written only when it differs from .segment, and .section - only when it differs from the enclosing block; restore both +/ + only when it differs from the enclosing block; restore both + +/ foreach (sect; doc.section_order) { foreach (ref o; doc.abstraction[sect]) { if (o.tags.anchor_tag_html.length == 0 diff --git a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d index 9e9f8ba..f51f6fe 100644 --- a/src/sisudoc/ocda/meta/metadoc_from_src_functions.d +++ b/src/sisudoc/ocda/meta/metadoc_from_src_functions.d @@ -144,41 +144,65 @@ template docAbstractionFunctions() { invariant() { } } - pure ObjGenericComposite obj_heading_ancestors()( + /+ ↓ a heading's text as it is used for navigation, in + tags.heading_ancestors_text and in the lev4 subtoc entries. + . + these are one line labels, so what is dropped is what cannot be shown + on one line and means nothing there: the note itself (the reference + marker goes with it), the line break markers, and a line break that + came from a heading wrapped over two lines in the source. what is + kept is the text and its inline markup. + +/ + string heading_text_for_navigation()(string _txt) { + mixin InternalMarkup; + static auto mkup = InlineMarkup(); + static auto rgx = RgxI(); + string _out = _txt + .replaceAll(rgx.inline_notes_al, "") // 【...】 and its marker + .replaceAll(rgx.inline_notes_curly_gen, "") // ~{...}~ if any remains + .replace(mkup.br_line, " ") + .replace(mkup.br_line_inline, " ") + .replace(mkup.br_line_spaced, " ") + .replace("\n", " ") + .replace("\r", " "); + while (_out.canFind(" ")) { _out = _out.replace(" ", " "); } + return _out.strip; + } + ObjGenericComposite obj_heading_ancestors()( ObjGenericComposite obj, string[] lv_ancestors_txt, ) { switch (obj.metainfo.heading_lev_markup) { case 0: - lv_ancestors_txt[0] = obj.text.to!string; + lv_ancestors_txt[0] = heading_text_for_navigation(obj.text.to!string); foreach(k; 1..8) { lv_ancestors_txt[k] = ""; } goto default; case 1: - lv_ancestors_txt[1] = obj.text.to!string; + lv_ancestors_txt[1] = heading_text_for_navigation(obj.text.to!string); foreach(k; 2..8) { lv_ancestors_txt[k] = ""; } goto default; case 2: - lv_ancestors_txt[2] = obj.text.to!string; + lv_ancestors_txt[2] = heading_text_for_navigation(obj.text.to!string); foreach(k; 3..8) { lv_ancestors_txt[k] = ""; } goto default; case 3: - lv_ancestors_txt[3] = obj.text.to!string; + lv_ancestors_txt[3] = heading_text_for_navigation(obj.text.to!string); foreach(k; 4..8) { lv_ancestors_txt[k] = ""; } goto default; case 4: - lv_ancestors_txt[4] = obj.text.to!string; + lv_ancestors_txt[4] = heading_text_for_navigation(obj.text.to!string); foreach(k; 5..8) { lv_ancestors_txt[k] = ""; } goto default; case 5: - lv_ancestors_txt[5] = obj.text.to!string; + lv_ancestors_txt[5] = heading_text_for_navigation(obj.text.to!string); foreach(k; 6..8) { lv_ancestors_txt[k] = ""; } goto default; case 6: - lv_ancestors_txt[6] = obj.text.to!string; + lv_ancestors_txt[6] = heading_text_for_navigation(obj.text.to!string); lv_ancestors_txt[7] = ""; goto default; case 7: - lv_ancestors_txt[7] = obj.text.to!string; + lv_ancestors_txt[7] = heading_text_for_navigation(obj.text.to!string); goto default; default: obj.tags.heading_ancestors_text = lv_ancestors_txt.dup; @@ -2671,6 +2695,7 @@ template docAbstractionFunctions() { char[] heading_toc_ = (obj_["substantive"].dup.strip.to!(char[])) .replaceAll(rgx.inline_notes_al, ""); heading_toc_ = _clean_heading_toc_(heading_toc_); + heading_toc_ = heading_text_for_navigation(heading_toc_.to!string).to!(char[]); auto attrib = ""; string toc_txt_, subtoc_txt_; int[string] indent; |
