From 297410fc013135e992842b8347c2e0bbec042d20 Mon Sep 17 00:00:00 2001 From: Ralph Amissah Date: Fri, 3 Oct 2025 12:15:42 -0400 Subject: a text output (and skel an outline) - spine --text [--output=output path] [markup source] --- src/sisudoc/io_out/hub.d | 14 +- src/sisudoc/io_out/metadata.d | 5 + src/sisudoc/io_out/paths_output.d | 51 ++- src/sisudoc/io_out/rgx.d | 23 +- src/sisudoc/io_out/skel.d | 268 +++++++++++++++ src/sisudoc/io_out/text.d | 475 ++++++++++++++++++++++++++ src/sisudoc/meta/metadoc_from_src.d | 10 +- src/sisudoc/meta/metadoc_from_src_functions.d | 17 +- src/sisudoc/meta/rgx.d | 33 +- src/sisudoc/spine.d | 30 +- 10 files changed, 885 insertions(+), 41 deletions(-) create mode 100644 src/sisudoc/io_out/skel.d create mode 100644 src/sisudoc/io_out/text.d (limited to 'src') diff --git a/src/sisudoc/io_out/hub.d b/src/sisudoc/io_out/hub.d index 0e25811..f98be01 100644 --- a/src/sisudoc/io_out/hub.d +++ b/src/sisudoc/io_out/hub.d @@ -62,7 +62,7 @@ template outputHub() { @system void outputHub(D)(D doc) { mixin Msg; auto msg = Msg!()(doc.matters); - enum outTask { source_or_pod, sqlite, sqlite_multi, latex, odt, epub, html_scroll, html_seg, html_stuff } + enum outTask { source_or_pod, sqlite, sqlite_multi, latex, odt, epub, html_scroll, html_seg, html_stuff, text, skel } void Scheduled(D)(int sched, D doc) { auto msg = Msg!()(doc.matters); if (sched == outTask.source_or_pod) { @@ -118,6 +118,12 @@ template outputHub() { outputLaTeX!()(doc.abstraction, doc.matters); msg.vv("latex done"); } + if (sched == outTask.text) { + msg.v("text processing... "); + import sisudoc.io_out.text; + outputText!()(doc.abstraction, doc.matters); + msg.vv("text done"); + } if (sched == outTask.odt) { msg.v("odf:odt processing... "); import sisudoc.io_out.odt; @@ -130,6 +136,12 @@ template outputHub() { doc.SQLiteHubDiscreteBuildTablesAndPopulate!(); msg.vv("sqlite done"); } + if (sched == outTask.skel) { + msg.v("skel processing... "); + import sisudoc.io_out.skel; + outputSkel!()(doc.abstraction, doc.matters); + msg.vv("skel done"); + } } if (doc.matters.opt.action.vox_gt_1) { writeln(doc.matters.src.filename_base); } if (!(doc.matters.opt.action.parallelise_subprocesses)) { diff --git a/src/sisudoc/io_out/metadata.d b/src/sisudoc/io_out/metadata.d index 6e6183b..a89b31a 100644 --- a/src/sisudoc/io_out/metadata.d +++ b/src/sisudoc/io_out/metadata.d @@ -417,6 +417,7 @@ string theme_light_1 = format(q"┃ } auto pth_html = spinePathsHTML!()(doc_matters.output_path, doc_matters.src.language); auto pth_epub = spinePathsEPUB!()(doc_matters.output_path, doc_matters.src.language); + auto pth_text = spinePathsText!()(doc_matters); auto pth_pdf = spinePathsPDF!()(doc_matters); auto pth_pod = spinePathsPods!()(doc_matters); metadata_ ~= format(q"┃ @@ -498,6 +499,10 @@ string theme_light_1 = format(q"┃ ~ "." ~ doc_matters.src.language ~ ".letter.portrait.pdf\" class=\"lnkicon\">" ~ " □ pdf (U.S. letter) ] "; } + if (doc_matters.opt.action.html_link_text) { + metadata_ ~= "  [" + ~ " □ txt ] "; + } metadata_ ~= "

"; if (doc_matters.opt.action.html_link_markup_source) { metadata_ ~= "

source: " ~ doc_matters.src.filename_base ~ "

"; diff --git a/src/sisudoc/io_out/paths_output.d b/src/sisudoc/io_out/paths_output.d index a5b73a0..c3e677d 100644 --- a/src/sisudoc/io_out/paths_output.d +++ b/src/sisudoc/io_out/paths_output.d @@ -471,7 +471,7 @@ template spinePathsODT() { auto spinePathsODT(M)( M doc_matters, ) { - auto out_pth = spineOutPaths!()( doc_matters.output_path, doc_matters.src.language); + auto out_pth = spineOutPaths!()(doc_matters.output_path, doc_matters.src.language); string base_dir = "odf"; struct _PathsStruct { string base_pth() { // dir will contain odt document file (also debug file tree) @@ -668,3 +668,52 @@ template spinePathsSQLite() { return _PathsStruct(); } } + +template spinePathsText() { + import std.conv; + auto spinePathsText(M)( + M doc_matters, + ) { + auto out_pth = spineOutPaths!()(doc_matters.output_path, doc_matters.src.language); + string base_dir = "text"; + struct _PathsStruct { + string base_pth() { + return (((out_pth.output_base).chainPath(base_dir)).asNormalizedPath).array; + } + string base_filename(string fn_src) { + return fn_src.baseName.stripExtension; + } + string text_file() { + return ((base_pth.chainPath(doc_matters.src.doc_uid_out ~ ".txt")).asNormalizedPath).array; + } + string dirtop() { + return "".chainPath("").array; + } + } + return _PathsStruct(); + } +} +template spinePathsSkel() { + import std.conv; + auto spinePathsSkel(M)( + M doc_matters, + ) { + auto out_pth = spineOutPaths!()(doc_matters.output_path, doc_matters.src.language); + string base_dir = "skel"; + struct _PathsStruct { + string base_pth() { + return (((out_pth.output_base).chainPath(base_dir)).asNormalizedPath).array; + } + string base_filename(string fn_src) { + return fn_src.baseName.stripExtension; + } + string skel_file() { + return ((base_pth.chainPath(doc_matters.src.doc_uid_out ~ ".skel")).asNormalizedPath).array; + } + string dirtop() { + return "".chainPath("").array; + } + } + return _PathsStruct(); + } +} diff --git a/src/sisudoc/io_out/rgx.d b/src/sisudoc/io_out/rgx.d index 9c70c1e..666e71f 100644 --- a/src/sisudoc/io_out/rgx.d +++ b/src/sisudoc/io_out/rgx.d @@ -78,9 +78,9 @@ static template spineRgxOut() { static br_empty_line = ctRegex!(`\n[ ]*\n`, "mg"); static br_linebreaks_newlines = ctRegex!(`[\n┘┙]`, "mg"); static br_linebreaks = ctRegex!(`[┘┙]`, "mg"); - static br_line = ctRegex!(`┘`, "mg"); - static br_line_inline = ctRegex!(`┙`, "mg"); - static br_line_spaced = ctRegex!(`┚`, "mg"); + static br_line = ctRegex!(`\s*┘\s*`, "mg"); + static br_line_inline = ctRegex!(`\s*┙\s*`, "mg"); + static br_line_spaced = ctRegex!(`\s*┚\s*`, "mg"); /+ quotation marks +/ static quotes_open_and_close = ctRegex!(`[“”]`, "mg"); /+ inline markup footnotes endnotes +/ @@ -90,6 +90,8 @@ static template spineRgxOut() { static inline_notes_al_gen_text = ctRegex!(`【(?P.+?)】`, "m"); static inline_notes_al_all_note = ctRegex!(`【(?P\d+|(?:[*]|[+])+)\s+(?P.+?)\s*】`, "mg"); static inline_notes_al_regular_number_note = ctRegex!(`【(?P\d+)\s+(?P.+?)\s*】`, "mg"); + // static inline_notes_al_all_note = ctRegex!(`【(?P\d+|(?:[*]|[+])+)\s+(?P.+?)\s*(≫\s\d+)?\s*】`, "mg"); // ocn of origin would be useful in endnote section + // static inline_notes_al_regular_number_note = ctRegex!(`【(?P\d+)\s+(?P.+?)\s*(≫\s\d+)?\s*】`, "mg"); // ocn of origin would be useful in endnote section static inline_notes_al_special_char_note = ctRegex!(`【(?P(?:[*]|[+])+)\s+(?P.+?)】`, "mg"); static inline_al_delimiter_open_regular = ctRegex!(`【\s`, "m"); static inline_al_delimiter_open_symbol_star = ctRegex!(`【[*]\s`, "m"); @@ -100,13 +102,14 @@ static template spineRgxOut() { static inline_image_without_dimensions = ctRegex!(`(?P
┥)☼(?P(?P[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P0)h(?P0))\s*(?P.*?┝┤.*?├)`, "mg");
     static inline_image_info                        = ctRegex!(`☼?(?P[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P\d+)h(?P\d+)`, "mg");
     static inline_link_anchor                       = ctRegex!(`┃(?P\S+?)┃`, "mg"); // TODO *~text_link_anchor
-    static inline_link                              = ctRegex!(`┥(?P.+?)┝┤(?P#?(\S+?))├`, "mg");
-    static inline_link_empty                        = ctRegex!(`┥(?P.+?)┝┤├`, "mg");
-    static inline_link_number                       = ctRegex!(`┥(?P.+?)┝┤(?P[0-9]+)├`, "mg"); // not used
-    static inline_link_number_only                  = ctRegex!(`(?P┥.+?┝)┤(?P[0-9]+)├`, "mg");
-    static inline_link_stow_uri                     = ctRegex!(`┥(?P.+?)┝┤(?P[^ 0-9#┥┝┤├][^ 0-9┥┝┤├]+)├`, "mg"); // will not stow (stowed links) or object number internal links
-    static inline_link_hash                         = ctRegex!(`┥(?P.+?)┝┤(?P#(?P\S+?))├`, "mg");
-    static inline_link_seg_and_hash                 = ctRegex!(`┥(?P.+?)┝┤(?P(?P[^/#├]*)#(?P.+?))├`, "mg");
+    // space cleaning should not be necessary
+    static inline_link                              = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P#?(\S+?))├`, "mg");
+    static inline_link_empty                        = ctRegex!(`┥\s*(?P.+?)\s*┝┤├`, "mg");
+    static inline_link_number                       = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P[0-9]+)├`, "mg"); // not used
+    static inline_link_number_only                  = ctRegex!(`\s*(?P\s*┥.+?┝)┤(?P[0-9]+)├`, "mg");
+    static inline_link_stow_uri                     = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P[^ 0-9#┥┝┤├][^ 0-9┥┝┤├]+)├`, "mg"); // will not stow (stowed links) or object number internal links
+    static inline_link_hash                         = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P#(?P\S+?))├`, "mg");
+    static inline_link_seg_and_hash                 = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P(?P[^/#├]*)#(?P.+?))├`, "mg");
     static inline_link_clean                        = ctRegex!(`┤(?:.+?)├|[┥┝]`, "mg");
     static inline_link_toc_to_backmatter            = ctRegex!(`┤#(?Pendnotes|bibliography|bookindex|glossary|blurb)├`, "mg");
     static url                                      = ctRegex!(`https?://`, "mg");
diff --git a/src/sisudoc/io_out/skel.d b/src/sisudoc/io_out/skel.d
new file mode 100644
index 0000000..b616695
--- /dev/null
+++ b/src/sisudoc/io_out/skel.d
@@ -0,0 +1,268 @@
+/+
+- 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 - 2025 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 sisudoc.io_out.skel;
+@safe:
+template outputSkel() {
+  template munge() {
+    import std.stdio;
+    import std.conv;
+    void puts(string _obj_is) {
+      writeln(__FILE__, ":", __LINE__, ": ", _obj_is);
+    }
+    string newline = "\n";
+    string newlines = "\n\n";
+    string toc(O)(O obj) {
+      // puts(obj.metainfo.is_a);
+      // return "toc\n";
+      return obj.text ~ newline;
+    }
+    string heading(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string para(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string group(O)(O obj) {
+      /+
+        The "group" is different from the "block" mark in that "group" does not
+        preserve whitespace, the "block" mark does. The text falling within the
+        block is a single object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string block(O)(O obj) {
+      /+
+        The "block" is different from the "group" mark in that the "block" mark
+        (like the "poem" mark) preserves whitespace, the "group" mark does not.
+        The text falling within the "block" is a single object, which is different
+        from the "poem" mark where each identified verse is an object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string poem(O)(O obj) {
+      /+
+        The "poem" mark like the "block" preserves whitespace. Text followed by
+        two newlines are identified as verse and each verse is an object i.e. a
+        poem may consist of multiple verse each of which is identified as an
+        object, unlike a text "block" which is identified as a single object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      // return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+      return obj.text ~ newlines;
+    }
+    string verse(O)(O obj) {
+      /+
+        See description of poem, the poem is demarkated but the verse is the
+        object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string code(O)(O obj) {
+      /+
+        "Code" blocks are a single text object, in which the original text is
+        preserved.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string quote(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string table(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string endnote(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string bookindex(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string bibliography(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string glossary(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string blurb(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string comment(O)(O obj) {
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+  }
+  template theDocument() {
+    import std.stdio;
+    import sisudoc.io_out;
+    // static auto rgx = RgxO();
+    string skel_head(M)(
+            M  doc_matters,
+    ) {
+      return "head";
+    }
+    string skel_body(D,M)(
+      const D  doc_abstraction,
+            M  doc_matters,
+    ) {
+      string doc_object = "";
+      foreach (section; doc_matters.has.keys_seq.scroll) {
+        foreach (obj; doc_abstraction[section]) {
+          if (obj.metainfo.is_a == "toc")          { doc_object ~= munge!().toc(obj); }
+          if (obj.metainfo.is_a == "heading")      { doc_object ~= munge!().heading(obj); }
+          if (obj.metainfo.is_a == "para")         { doc_object ~= munge!().para(obj); }
+          if (obj.metainfo.is_a == "group")        { doc_object ~= munge!().group(obj); }
+          if (obj.metainfo.is_a == "block")        { doc_object ~= munge!().block(obj); }
+          if (obj.metainfo.is_a == "poem")         { doc_object ~= munge!().poem(obj); }
+          if (obj.metainfo.is_a == "verse")        { doc_object ~= munge!().verse(obj); }
+          if (obj.metainfo.is_a == "code")         { doc_object ~= munge!().code(obj); }
+          if (obj.metainfo.is_a == "quote")        { doc_object ~= munge!().quote(obj); }
+          if (obj.metainfo.is_a == "table")        { doc_object ~= munge!().table(obj); }
+          if (obj.metainfo.is_a == "endnote")      { doc_object ~= munge!().endnote(obj); }
+          if (obj.metainfo.is_a == "bookindex")    { doc_object ~= munge!().bookindex(obj); }
+          if (obj.metainfo.is_a == "bibliography") { doc_object ~= munge!().bibliography(obj); }
+          if (obj.metainfo.is_a == "glossary")     { doc_object ~= munge!().glossary(obj); }
+          if (obj.metainfo.is_a == "blurb")        { doc_object ~= munge!().blurb(obj); }
+          if (obj.metainfo.is_a == "comment")      { doc_object ~= munge!().comment(obj); }
+        }
+      }
+      return doc_object;
+    }
+    string skel_tail(M)(
+            M  doc_matters,
+    ) {
+      return "tail";
+    }
+  }
+  void outputSkel(D,M) (
+    const D  doc_abstraction,
+          M  doc_matters,
+  ) {
+    import std.stdio;
+    import sisudoc.io_out;
+    void skel_out(D,M)(
+      const D  doc_abstraction,
+            M  doc_matters,
+    ) {
+      struct Skel {
+        string head;
+        string content;
+        string tail;
+      }
+      auto skel           = Skel();
+      skel.head           = theDocument!().skel_head(doc_matters);
+      skel.content        = theDocument!().skel_body(doc_abstraction, doc_matters);
+      skel.tail           = theDocument!().skel_tail(doc_matters);
+      auto pth_skel = spinePathsSkel(doc_matters);
+      try {
+        import std.file;
+        if (!exists(pth_skel.base_pth)) {
+          (pth_skel.base_pth).mkdirRecurse;
+        }
+      } catch (ErrnoException ex) {
+      }
+      if (doc_matters.opt.action.vox_gt_1) {
+        writeln(" ", pth_skel.skel_file);
+      }
+      // writeln(pth_skel.base_pth);
+      auto f = File(pth_skel.skel_file, "w");
+      f.writeln(skel.head);
+      f.writeln(skel.content);
+      f.writeln(skel.tail);
+    }
+    skel_out(doc_abstraction, doc_matters);
+  }
+}
diff --git a/src/sisudoc/io_out/text.d b/src/sisudoc/io_out/text.d
new file mode 100644
index 0000000..da0e2b6
--- /dev/null
+++ b/src/sisudoc/io_out/text.d
@@ -0,0 +1,475 @@
+/+
+- 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 - 2025 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 sisudoc.io_out.text;
+@safe:
+template outputText() {
+  template munge() {
+    import sisudoc.io_out;
+    import sisudoc.io_out.rgx;
+    import std.stdio;
+    import std.conv;
+    import std.conv : to;
+    import std.typecons : Nullable;
+    mixin spineRgxOut;
+    static auto rgx = RgxO();
+    void puts(string _obj_is) {
+      writeln(__FILE__, ":", __LINE__, ": ", _obj_is);
+    }
+    string newline = "\n";
+    string newlines = "\n\n";
+    template special_characters_and_font_face() {
+      string code(string _txt){
+         _txt = _txt
+           .replaceAll(rgx.nbsp_char,          " ");
+        return _txt;
+      }
+      string general(string _txt) {
+         _txt = _txt
+           .replaceAll(rgx.nbsp_char,          " ")
+           .replaceAll(rgx.br_line,            "\n")
+           .replaceAll(rgx.br_line_inline,     "\n")
+           .replaceAll(rgx.br_line_spaced,     "\n\n")
+           .replaceAll(rgx.inline_strike,      "-{$1}-")
+           .replaceAll(rgx.inline_insert,      "+{$1}+")
+           .replaceAll(rgx.inline_cite,        "\"{$1}\"")
+           .replaceAll(rgx.inline_emphasis,    "!{$1}!")
+           .replaceAll(rgx.inline_bold,        "*{$1}*")
+           .replaceAll(rgx.inline_italics,     "/{$1}/")
+           .replaceAll(rgx.inline_underscore,  "_{$1}_")
+           .replaceAll(rgx.inline_superscript, "^{$1}^")
+           .replaceAll(rgx.inline_subscript,   ",{$1},")
+           .replaceAll(rgx.inline_mono,        "#{$1}#");
+        return _txt;
+      }
+      string links_and_images(string _txt){
+        if (_txt.matchFirst(rgx.inline_link)) {
+          foreach (m; _txt.matchAll(rgx.inline_link)) {
+            if (m.captures[3] == "0") {
+              _txt = _txt
+                .replaceFirst(rgx.inline_link,        (m.captures[1]));
+            } else {
+              _txt = _txt
+                .replaceFirst(rgx.inline_link,        (m.captures[1] ~ " ≫" ~ m.captures[3]));
+            }
+          }
+        }
+        if (_txt.matchFirst(rgx.inline_image)) {
+          foreach (m; _txt.matchAll(rgx.inline_image)) {
+              _txt = _txt
+                .replaceFirst(rgx.inline_image,        (m.captures[3]));
+          }
+        }
+        return _txt;
+      }
+    }
+    string generalMunge(O,M)(O obj, M doc_matters) {
+      string _txt = obj.text;
+      string _notes;
+      string _ocn;
+      string general_munge;
+      if (obj.metainfo.ocn == 0 || doc_matters.opt.action.ocn_off) {
+        _ocn = "";
+      } else {
+        _ocn =  "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newline;
+      }
+      if (_txt.matchFirst(rgx.inline_notes_al_gen)) {
+        foreach (m; _txt.matchAll(rgx.inline_notes_al_regular_number_note)) {
+          _notes ~= newlines ~ m["num"] ~ ". " ~ m["note"];
+        }
+      }
+      _txt = _txt.replaceAll(rgx.inline_notes_al_regular_number_note, "[$1]");
+      if (obj.metainfo.is_a == "code") {
+        _txt = special_characters_and_font_face!().code(_txt);
+      } else {
+        _txt = special_characters_and_font_face!().general(_txt);
+      }
+      _txt = special_characters_and_font_face!().links_and_images(_txt);
+      if (obj.metainfo.is_a == "heading") {
+        general_munge = newline ~ _txt ~ _notes ~ newline ~ _ocn ~ newline;
+      } else {
+        general_munge = _txt ~ _notes ~ newline ~ _ocn ~ newline;
+      }
+      return general_munge;
+    }
+    string toc(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return "toc\n";
+      // _txt =  _special_characters_and_font_face(obj.text);
+      string _txt = special_characters_and_font_face!().general(obj.text);
+      string _spaces;
+      switch (obj.attrib.indent_hang) {
+        case 1: _spaces = "";
+          break;
+        case 2: _spaces = ":";
+          break;
+        case 3: _spaces = "∴";
+          break;
+        case 4: _spaces = "  ";
+          break;
+        case 5: _spaces = "    ";
+          break;
+        case 6: _spaces = "      ";
+          break;
+        case 7: _spaces = "        ";
+          break;
+        case 8: _spaces = "          ";
+          break;
+        default:
+          break;
+      }
+      _txt = _txt.replaceAll(rgx.inline_link,  (_spaces ~ "$1   ≫ $3"));
+      return _txt ~ newline;
+    }
+    string heading(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string para(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string group(O,M)(O obj, M doc_matters) {
+      /+
+        The "group" is different from the "block" mark in that "group" does not
+        preserve whitespace, the "block" mark does. The text falling within the
+        block is a single object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string block(O,M)(O obj, M doc_matters) {
+      /+
+        The "block" is different from the "group" mark in that the "block" mark
+        (like the "poem" mark) preserves whitespace, the "group" mark does not.
+        The text falling within the "block" is a single object, which is different
+        from the "poem" mark where each identified verse is an object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string poem(O,M)(O obj, M doc_matters) { // LATER
+      /+
+        The "poem" mark like the "block" preserves whitespace. Text followed by
+        two newlines are identified as verse and each verse is an object i.e. a
+        poem may consist of multiple verse each of which is identified as an
+        object, unlike a text "block" which is identified as a single object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+    string verse(O,M)(O obj, M doc_matters) {
+      /+
+        See description of poem, the poem is demarkated but the verse is the
+        object.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string code(O,M)(O obj, M doc_matters) {
+      /+
+        "Code" blocks are a single text object, in which the original text is
+        preserved.
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string quote(O,M)(O obj, M doc_matters) { // LATER
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newline ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string table(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      auto tablarize(O)(
+        string            _txt,
+        const        O    obj,
+      ) {
+        string[] _table_rows = (_txt).split(rgx.table_delimiter_row);
+        string[] _table_cols;
+        string _table = "";
+        string _tablenote = "";
+        int[] _col_width;
+        _col_width.length = obj.table.number_of_columns.to!ulong;
+        foreach(row_idx, row; _table_rows) {
+          _table_cols = row.split(rgx.table_delimiter_col);
+          _table ~= "";
+          foreach(col_idx, cell; _table_cols) {
+            if (!((_table_cols.length == 1)
+            && (_table_rows.length <= row_idx+2))) {
+              if (_col_width[col_idx] < (cell.length.to!int)) {
+                _col_width[col_idx] = cell.length.to!int;
+              }
+            }
+          }
+        }
+        foreach(row_idx, row; _table_rows) {
+          _table_cols = row.split(rgx.table_delimiter_col);
+          foreach(col_idx, cell; _table_cols) {
+            if ((_table_cols.length == 1)
+            && (_table_rows.length <= row_idx+2)) { // check row_idx+2 (rather than == ++row_idx)
+              _tablenote ~= cell ~ newline;
+            } else {
+              if (obj.table.column_aligns[col_idx] == "l") {
+                _table ~= format(q"┃%-*s%s┃",
+                  _col_width[col_idx],
+                  cell,
+                  (_table_cols.length > (col_idx + 1)) ? " ┊ " : ""
+                );
+              } else {
+                _table ~= format(q"┃%*s%s┃",
+                  _col_width[col_idx],
+                  cell,
+                  (_table_cols.length > (col_idx + 1)) ? " ┊ " : ""
+                );
+              }
+              _table = _table
+                .replaceAll(regex("\\s*$"),  "");
+            }
+          }
+          _table ~= newline;
+        }
+        Tuple!(string, string) t = tuple(
+          _table,
+          _tablenote,
+        );
+        return t;
+      }
+      // string _txt = obj.text;
+      // writeln(obj.table.column_widths);
+      auto _t = tablarize(obj.text, obj);
+      string _txt = _t[0];
+      string _tablenote = _t[1];
+      return _txt ~ _tablenote ~ "「" ~ obj.metainfo.ocn.to!string ~ "」" ~ newlines;
+    }
+    string endnote(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _ocn;
+      _ocn =  "「" ~ obj.metainfo.ocn.to!string ~ "」";
+      string _txt = obj.text;
+      _txt = _txt
+        .replaceFirst(rgx.inline_link,  ("$1"))
+        .replaceFirst(rgx.inline_superscript,  ("$1"));
+      _txt = special_characters_and_font_face!().general(_txt);
+      return _txt ~ newlines;
+    }
+    string bookindex(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _txt = obj.text;
+      _txt = _txt
+        .replaceAll(rgx.inline_link,  ("≫$1"))
+        .replaceAll(regex("\\s*\\\\"),  "");
+      _txt = special_characters_and_font_face!().general(_txt);
+      return _txt ~ newlines;
+    }
+    string bibliography(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _txt = obj.text;
+      _txt = special_characters_and_font_face!().general(_txt);
+      return _txt ~ newlines;
+      // ALT:
+      // string _general_munge = generalMunge(obj,doc_matters);
+      // return _general_munge;
+    }
+    string glossary(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _txt = obj.text;
+      _txt = special_characters_and_font_face!().general(_txt);
+      return _txt;
+    }
+    string blurb(O,M)(O obj, M doc_matters) {
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      string _general_munge = generalMunge(obj,doc_matters);
+      return _general_munge;
+    }
+    string comment(O,M)(O obj, M doc_matters) { // LATER
+      /+
+      +/
+      // puts(obj.metainfo.is_a);
+      // return obj.metainfo.is_a;
+      return obj.text ~ newlines;
+    }
+  }
+  template theDocument() {
+    import std.stdio;
+    import sisudoc.io_out;
+    string text_head(M)(
+            M  doc_matters,
+    ) {
+      return "head";
+    }
+    string text_body(D,M)(
+      const D  doc_abstraction,
+            M  doc_matters,
+    ) {
+      string doc_object = "";
+      foreach (section; doc_matters.has.keys_seq.scroll) {
+        foreach (obj; doc_abstraction[section]) {
+          if (obj.metainfo.is_a == "toc")          { doc_object ~= munge!().toc(obj, doc_matters); }
+          if (obj.metainfo.is_a == "heading")      { doc_object ~= munge!().heading(obj, doc_matters); }
+          if (obj.metainfo.is_a == "para")         { doc_object ~= munge!().para(obj, doc_matters); }
+          if (obj.metainfo.is_a == "group")        { doc_object ~= munge!().group(obj, doc_matters); }
+          if (obj.metainfo.is_a == "block")        { doc_object ~= munge!().block(obj, doc_matters); }
+          if (obj.metainfo.is_a == "poem")         { doc_object ~= munge!().poem(obj, doc_matters); }         // CHECK
+          if (obj.metainfo.is_a == "verse")        { doc_object ~= munge!().verse(obj, doc_matters); }        // CHECK
+          if (obj.metainfo.is_a == "code")         { doc_object ~= munge!().code(obj, doc_matters); }
+          if (obj.metainfo.is_a == "quote")        { doc_object ~= munge!().quote(obj, doc_matters); }        // LATER
+          if (obj.metainfo.is_a == "table")        { doc_object ~= munge!().table(obj, doc_matters); }
+          if (obj.metainfo.is_a == "endnote")      { doc_object ~= munge!().endnote(obj, doc_matters); }
+          if (obj.metainfo.is_a == "bookindex")    { doc_object ~= munge!().bookindex(obj, doc_matters); }    // CHECK
+          if (obj.metainfo.is_a == "bibliography") { doc_object ~= munge!().bibliography(obj, doc_matters); } // CHECK
+          if (obj.metainfo.is_a == "glossary")     { doc_object ~= munge!().glossary(obj, doc_matters); }     // CHECK
+          if (obj.metainfo.is_a == "blurb")        { doc_object ~= munge!().blurb(obj, doc_matters); }        // CHECK
+          if (obj.metainfo.is_a == "comment")      { doc_object ~= munge!().comment(obj, doc_matters); }      // LATER
+        }
+      }
+      return doc_object;
+    }
+    string text_tail(M)(
+            M  doc_matters,
+    ) {
+      string metadata_;
+      if (doc_matters.opt.action.debug_do) {
+        writeln(doc_matters.src.filename_base);
+        writeln("Title:       ", doc_matters.conf_make_meta.meta.title_full);
+        writeln("  Author:    ", doc_matters.conf_make_meta.meta.creator_author);
+        writeln("  Published: ", doc_matters.conf_make_meta.meta.date_published);
+        writeln("  Copyright: ", doc_matters.conf_make_meta.meta.rights_copyright);
+        writeln("  License:   ", doc_matters.conf_make_meta.meta.rights_license);
+      }
+      if (!(doc_matters.conf_make_meta.meta.title_full.empty)) {
+        metadata_ ~= "Title: " ~ doc_matters.conf_make_meta.meta.title_full ~ "\n\n";
+      } else if (doc_matters.opt.action.debug_do || doc_matters.opt.action.vox_gt_3) {
+        writeln("ERROR no Title information provided in document header ", doc_matters.src.filename_base);
+      }
+      if (!(doc_matters.conf_make_meta.meta.creator_author.empty)) {
+        if (doc_matters.opt.action.html_link_curate) {
+          metadata_ ~= "Author: " ~ doc_matters.conf_make_meta.meta.creator_author_surname.translate([' ' : "_"])
+                    ~ doc_matters.conf_make_meta.meta.creator_author ~ "\n\n";
+        } else {
+          metadata_ ~= "Author: "
+                    ~ doc_matters.conf_make_meta.meta.creator_author ~ "\n\n";
+        }
+      } else if (doc_matters.opt.action.debug_do || doc_matters.opt.action.vox_gt_3) {
+        writeln("ERROR no Author information provided in document header ", doc_matters.src.filename_base);
+      }
+      metadata_ ~= "Published: "   ~ doc_matters.conf_make_meta.meta.date_published ~ "\n\n";
+      if (!(doc_matters.conf_make_meta.meta.rights_copyright.empty)) {
+        metadata_ ~= "Copyright: "   ~ doc_matters.conf_make_meta.meta.rights_copyright ~ "\n\n";
+      } else if (doc_matters.opt.action.debug_do || doc_matters.opt.action.vox_gt_3) {
+        writeln("WARNING no Copyright information provided in document header ", doc_matters.src.filename_base);
+      }
+      if (!(doc_matters.conf_make_meta.meta.rights_license.empty)) {
+        metadata_ ~= "License:   "   ~ doc_matters.conf_make_meta.meta.rights_license ~ "\n\n";
+      } else if (doc_matters.opt.action.debug_do || doc_matters.opt.action.vox_gt_3) {
+        writeln("WARNING no License information provided in document header ", doc_matters.src.filename_base);
+      }
+      metadata_ ~= doc_matters.generator_program.project_name.strip ~ "\n";
+      metadata_ ~= doc_matters.generator_program.url_home.strip;
+      return metadata_;
+    }
+  }
+  void outputText(D,M) (
+    const D  doc_abstraction,
+          M  doc_matters,
+  ) {
+    import std.stdio;
+    import sisudoc.io_out;
+    void text_out(D,M)(
+      const D  doc_abstraction,
+            M  doc_matters,
+    ) {
+      struct Text {
+        string head;
+        string content;
+        string tail;
+      }
+      auto text           = Text();
+      // text.head           = theDocument!().text_head(doc_matters);
+      text.content        = theDocument!().text_body(doc_abstraction, doc_matters);
+      text.tail           = theDocument!().text_tail(doc_matters);
+      auto pth_text = spinePathsText(doc_matters);
+      try {
+        import std.file;
+        if (!exists(pth_text.base_pth)) {
+          (pth_text.base_pth).mkdirRecurse;
+        }
+      } catch (ErrnoException ex) {
+      }
+      if (doc_matters.opt.action.vox_gt_1) {
+        writeln(" ", pth_text.text_file);
+      }
+      // writeln(pth_text.base_pth);
+      auto f = File(pth_text.text_file, "w");
+      // f.writeln(text.head);
+      f.writeln(text.content);
+      f.writeln(text.tail);
+    }
+    text_out(doc_abstraction, doc_matters);
+  }
+}
diff --git a/src/sisudoc/meta/metadoc_from_src.d b/src/sisudoc/meta/metadoc_from_src.d
index 24ae935..904444a 100644
--- a/src/sisudoc/meta/metadoc_from_src.d
+++ b/src/sisudoc/meta/metadoc_from_src.d
@@ -939,7 +939,7 @@ template docAbstraction() {
     }
     { // document segnames
       ST_segnames get_segnames;
-      get_segnames = the_document_body_section.after_doc_determine_segnames(the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, segnames, html_segnames_ptr_cntr, html_segnames_ptr); //
+      get_segnames = the_document_body_section.after_doc_determine_segnames(the_document_endnotes_section, the_document_glossary_section, the_document_bibliography_section, the_document_bookindex_section, the_document_blurb_section, segnames, html_segnames_ptr_cntr, html_segnames_ptr);
       segnames                          = get_segnames.segnames;
       html_segnames_ptr_cntr            = get_segnames.html_segnames_ptr_cntr;
       html_segnames_ptr                 = get_segnames.html_segnames_ptr;
@@ -1376,36 +1376,42 @@ template docAbstraction() {
       "scroll": ["head", "toc", "body",],
       "seg":    ["head", "toc", "body",],
       "sql":    ["head", "body",],
-      "latex":  ["head", "toc", "body",]
+      "latex":  ["head", "toc", "body",],
+      "text":   ["head", "toc", "body",],
     ];
     if (document_the["endnotes"].length > 1) {
       document_section_keys_sequenced["scroll"] ~= "endnotes";
       document_section_keys_sequenced["seg"]    ~= "endnotes";
       document_section_keys_sequenced["latex"]  ~= "endnotes";
+      document_section_keys_sequenced["text"]   ~= "endnotes";
     }
     if (document_the["glossary"].length > 1) {
       document_section_keys_sequenced["scroll"] ~= "glossary";
       document_section_keys_sequenced["seg"]    ~= "glossary";
       document_section_keys_sequenced["sql"]    ~= "glossary";
       document_section_keys_sequenced["latex"]  ~= "glossary";
+      document_section_keys_sequenced["text"]  ~= "glossary";
     }
     if (document_the["bibliography"].length > 1) {
       document_section_keys_sequenced["scroll"] ~= "bibliography";
       document_section_keys_sequenced["seg"]    ~= "bibliography";
       document_section_keys_sequenced["sql"]    ~= "bibliography";
       document_section_keys_sequenced["latex"]  ~= "bibliography";
+      document_section_keys_sequenced["text"]  ~= "bibliography";
     }
     if (document_the["bookindex"].length > 1) {
       document_section_keys_sequenced["scroll"] ~= "bookindex";
       document_section_keys_sequenced["seg"]    ~= "bookindex";
       document_section_keys_sequenced["sql"]    ~= "bookindex";
       document_section_keys_sequenced["latex"]  ~= "bookindex";
+      document_section_keys_sequenced["text"]   ~= "bookindex";
     }
     if (document_the["blurb"].length > 1) {
       document_section_keys_sequenced["scroll"] ~= "blurb";
       document_section_keys_sequenced["seg"]    ~= "blurb";
       document_section_keys_sequenced["sql"]    ~= "blurb";
       document_section_keys_sequenced["latex"]  ~= "blurb";
+      document_section_keys_sequenced["text"]   ~= "blurb";
     }
     if ((opt_action.html)
     || (opt_action.html_scroll)
diff --git a/src/sisudoc/meta/metadoc_from_src_functions.d b/src/sisudoc/meta/metadoc_from_src_functions.d
index 3ae10d1..6718e82 100644
--- a/src/sisudoc/meta/metadoc_from_src_functions.d
+++ b/src/sisudoc/meta/metadoc_from_src_functions.d
@@ -2557,10 +2557,8 @@ template docAbstractionFunctions() {
       CMM              conf_make_meta,
       Flag!"_new_doc"  _new_doc
     ) {
-      obj_txt["munge"]                                = obj_[obj_key_].dup;
-      obj_txt["munge"]                                = (obj_["is"].match(ctRegex!(`verse|code`)))
-      ? obj_txt["munge"]
-      : obj_txt["munge"].strip;
+      obj_txt["munge"]                  = obj_[obj_key_].dup;
+      obj_txt["munge"]                  = (obj_["is"].match(ctRegex!(`verse|code`))) ? obj_txt["munge"] : obj_txt["munge"].strip;
       if (_new_doc) {
         anchor_tag = "";
       }
@@ -2579,8 +2577,8 @@ template docAbstractionFunctions() {
         || (obj_["is"] == "group")
         || (obj_["is"] == "block")
         || (obj_["is"] == "verse")) {
-        obj_txt["munge"]                              = (obj_txt["munge"]).inline_markup_faces;
-        obj_txt["munge"]                              = (obj_txt["munge"]).links_and_images;
+        obj_txt["munge"]                = (obj_txt["munge"]).inline_markup_faces;
+        obj_txt["munge"]                = (obj_txt["munge"]).links_and_images;
       }
       switch (obj_["is"]) {
       case "heading":
@@ -3299,8 +3297,8 @@ template docAbstractionFunctions() {
   // ↓ - endnotes
   struct NotesSection {
     string[string] object_notes;
-    int previous_count;
-    int mkn;
+    int            previous_count;
+    int            mkn;
     static auto rgx = RgxI();
     private auto gather_notes_for_endnote_section(
       ObjGenericComposite[] contents_am,
@@ -5406,6 +5404,9 @@ template docSectKeysSeq() {
       string[] latex() {
         return document_section_keys_sequenced["latex"];
       }
+      string[] text() {
+        return document_section_keys_sequenced["text"];
+      }
     }
     return doc_sect_keys_seq();
   }
diff --git a/src/sisudoc/meta/rgx.d b/src/sisudoc/meta/rgx.d
index 259ab82..1a26f73 100644
--- a/src/sisudoc/meta/rgx.d
+++ b/src/sisudoc/meta/rgx.d
@@ -148,16 +148,16 @@ static template spineRgxIn() {
     static table_col_separator_nl                   = ctRegex!(`[┊]$`, "mg");
     /+ inline markup footnotes endnotes +/
     static inline_notes_curly_gen                   = ctRegex!(`~\{.+?\}~`, "m");
-    static inline_notes_curly                       = ctRegex!(`~\{\s*(.+?)\}~`, "mg");
-    static inline_notes_curly_sp_asterisk           = ctRegex!(`~\{[*]+\s+(.+?)\}~`, "m");
-    static inline_notes_curly_sp_plus               = ctRegex!(`~\{[+]+\s+(.+?)\}~`, "m");
+    static inline_notes_curly                       = ctRegex!(`~\{\s*(.+?)\s*\}~`, "mg");
+    static inline_notes_curly_sp_asterisk           = ctRegex!(`~\{[*]+\s+(.+?)\s*\}~`, "m");
+    static inline_notes_curly_sp_plus               = ctRegex!(`~\{[+]+\s+(.+?)\s*\}~`, "m");
     static note_ref                                 = ctRegex!(`^\S+?noteref_(?P[0-9]+)`, "mg");     // {^{73.}^}#noteref_73
     static smid_inline_url_generic                        = ctRegex!(`(?:^|[}(\[ ])(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)[a-zA-Z0-9_#]`, "mg");
     static smid_inline_url                                = ctRegex!(`((?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)[a-zA-Z0-9_]\S*)`, "mg");
     static smid_inline_link_naked_url                     = ctRegex!(`(?P
^|[ (\[])(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤)\S+?)(?=[.,;:?!'"]?([ )\]]|$))`, "mg");
     static smid_inline_link_markup_regular                = ctRegex!(`(?P
^|[ (\[])\{\s*(?P.+?)\s*\}(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+?)(?=[;:!,?.]?([ )\]]|$))`, "mg");
-    static smid_inline_link_endnote_url_helper_punctuated = ctRegex!(`\{~\^\s+(?P.+?)\}(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+?)(?=[.,;:?!]?([ ]|$))`, "mg");
-    static smid_inline_link_endnote_url_helper            = ctRegex!(`\{~\^\s+(?P.+?)\}(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+)`, "mg");
+    static smid_inline_link_endnote_url_helper_punctuated = ctRegex!(`\{~\^\s+(?P.+?)\s*\}(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+?)(?=[.,;:?!]?([ ]|$))`, "mg");
+    static smid_inline_link_endnote_url_helper            = ctRegex!(`\{~\^\s+(?P.+?)\s*\}(?P(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+)`, "mg");
     static image                                    = ctRegex!(`([a-zA-Z0-9._-]+?\.(?:png|gif|jpg))`, "mg");
     static smid_image                               = ctRegex!(`(?P
(?:^|[ ])[{┥](?:~\^\s+|\s*))(?P[a-zA-Z0-9._-]+?\.(?:png|gif|jpg))(?P(?:.*?)\s*[}┝](?:image|┤.*?├|(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+?)(?=[;:!,?.]?([ )\]]|$)))`, "mg");
     static smid_image_generic                       = ctRegex!(`(?:^|[ ])[{┥](?:~\^\s+|\s*)\S+\.(?:png|gif|jpg).*?[}┝](?:image|┤.*?├|(?:(?:https?|git):\/\/|¤?\.\.\/|¤?\.\/|¤|#)\S+?)(?=[;:!,?.]?([ )\]]|$))`, "mg");
@@ -221,9 +221,9 @@ static template spineRgxIn() {
     static br_empty_line                            = ctRegex!(`\n[ ]*\n`, "mg");
     static br_linebreaks_newlines                   = ctRegex!(`[\n┘┙]`, "mg");
     static br_linebreaks                            = ctRegex!(`[┘┙]`, "mg");
-    static br_line                                  = ctRegex!(`┘`, "mg");
-    static br_line_inline                           = ctRegex!(`┙`, "mg");
-    static br_line_spaced                           = ctRegex!(`┚`, "mg");
+    static br_line                                  = ctRegex!(`\s*┘\s*`, "mg");
+    static br_line_inline                           = ctRegex!(`\s*┙\s*`, "mg");
+    static br_line_spaced                           = ctRegex!(`\s*┚\s*`, "mg");
     /+ inline markup footnotes endnotes +/
     static inline_notes_al                          = ctRegex!(`【(?:[*+]\s+|\s*)(.+?)】`, "mg");
     static inline_notes_al_special                  = ctRegex!(`【(?:[*+]\s+)(.+?)】`, "mg"); // TODO remove match when special footnotes are implemented
@@ -231,6 +231,8 @@ static template spineRgxIn() {
     static inline_notes_al_gen_text                 = ctRegex!(`【(?P.+?)】`, "m");
     static inline_notes_al_all_note                 = ctRegex!(`【(?P\d+|(?:[*]|[+])+)\s+(?P.+?)\s*】`, "mg");
     static inline_notes_al_regular_number_note      = ctRegex!(`【(?P\d+)\s+(?P.+?)\s*】`, "mg");
+    // static inline_notes_al_all_note                 = ctRegex!(`【(?P\d+|(?:[*]|[+])+)\s+(?P.+?)\s*(≫\s\d+)?\s*】`, "mg"); // ocn of origin would be useful in endnote section
+    // static inline_notes_al_regular_number_note      = ctRegex!(`【(?P\d+)\s+(?P.+?)\s*(≫\s\d+)?\s*】`, "mg"); // ocn of origin would be useful in endnote section
     static inline_notes_al_special_char_note        = ctRegex!(`【(?P(?:[*]|[+])+)\s+(?P.+?)】`, "mg");
     static inline_al_delimiter_open_regular         = ctRegex!(`【\s`, "m");
     static inline_al_delimiter_open_symbol_star     = ctRegex!(`【[*]\s`, "m");
@@ -241,13 +243,14 @@ static template spineRgxIn() {
     static inline_image_without_dimensions          = ctRegex!(`(?P
┥)☼(?P(?P[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P0)h(?P0))\s*(?P.*?┝┤.*?├)`, "mg");
     static inline_image_info                        = ctRegex!(`☼?(?P[a-zA-Z0-9._-]+?\.(?:jpg|gif|png)),w(?P\d+)h(?P\d+)`, "mg");
     static inline_link_anchor                       = ctRegex!(`┃(?P\S+?)┃`, "mg"); // TODO *~text_link_anchor
-    static inline_link                              = ctRegex!(`┥(?P.+?)┝┤(?P#?(\S+?))├`, "mg");
-    static inline_link_empty                        = ctRegex!(`┥(?P.+?)┝┤├`, "mg");
-    static inline_link_number                       = ctRegex!(`┥(?P.+?)┝┤(?P[0-9]+)├`, "mg"); // not used
-    static inline_link_number_only                  = ctRegex!(`(?P┥.+?┝)┤(?P[0-9]+)├`, "mg");
-    static inline_link_stow_uri                     = ctRegex!(`┥(?P.+?)┝┤(?P[^ 0-9#┥┝┤├][^ 0-9┥┝┤├]+)├`, "mg"); // will not stow (stowed links) or object number internal links
-    static inline_link_hash                         = ctRegex!(`┥(?P.+?)┝┤(?P#(?P\S+?))├`, "mg");
-    static inline_link_seg_and_hash                 = ctRegex!(`┥(?P.+?)┝┤(?P(?P[^/#├]*)#(?P.+?))├`, "mg");
+    // space cleaning should not be necessary
+    static inline_link                              = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P#?(\S+?))├`, "mg");
+    static inline_link_empty                        = ctRegex!(`┥\s*(?P.+?)\s*┝┤├`, "mg");
+    static inline_link_number                       = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P[0-9]+)├`, "mg"); // not used
+    static inline_link_number_only                  = ctRegex!(`\s*(?P\s*┥.+?┝)┤(?P[0-9]+)├`, "mg");
+    static inline_link_stow_uri                     = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P[^ 0-9#┥┝┤├][^ 0-9┥┝┤├]+)├`, "mg"); // will not stow (stowed links) or object number internal links
+    static inline_link_hash                         = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P#(?P\S+?))├`, "mg");
+    static inline_link_seg_and_hash                 = ctRegex!(`┥\s*(?P.+?)\s*┝┤(?P(?P[^/#├]*)#(?P.+?))├`, "mg");
     static inline_link_clean                        = ctRegex!(`┤(?:.+?)├|[┥┝]`, "mg");
     static inline_link_toc_to_backmatter            = ctRegex!(`┤#(?Pendnotes|bibliography|bookindex|glossary|blurb)├`, "mg");
     static url                                      = ctRegex!(`https?://`, "mg");
diff --git a/src/sisudoc/spine.d b/src/sisudoc/spine.d
index eceaf51..82138f7 100755
--- a/src/sisudoc/spine.d
+++ b/src/sisudoc/spine.d
@@ -144,6 +144,7 @@ string program_name = "spine";
     "html-link-pdf-a4"            : false,
     "html-link-pdf-letter"        : false,
     "html-link-search"            : false,
+    "html-link-text"              : false,
     "html-seg"                    : false,
     "html-scroll"                 : false,
     "latex"                       : false,
@@ -176,6 +177,7 @@ string program_name = "spine";
     "show-pod"                    : false,
     "show-sqlite"                 : false,
     "show-summary"                : false,
+    "skel"                        : false,
     "source"                      : false,
     "sqlite-discrete"             : false,
     "sqlite-db-create"            : false,
@@ -250,6 +252,7 @@ string program_name = "spine";
     "html-link-pdf",              "provide html link to pdf a4 & letter output",                    &opts["html-link-pdf"],
     "html-link-pdf-a4",           "provide html link to pdf a4 output",                             &opts["html-link-pdf-a4"],
     "html-link-pdf-letter",       "provide html link to pdf letter size output",                    &opts["html-link-pdf-letter"],
+    "html-link-text",             "provide html link to text output",                               &opts["html-link-text"],
     "html-link-search",           "html embedded search submission",                                &opts["html-link-search"],
     "html-seg",                   "process html output",                                            &opts["html-seg"],
     "html-scroll",                "process html output",                                            &opts["html-scroll"],
@@ -299,6 +302,7 @@ string program_name = "spine";
     "set-digest",                 "default hash digest type (e.g. sha256)",                         &settings["set-digest"],
     "set-papersize",              "default papersize (latex pdf eg. a4 or a5 or b4 or letter)",     &settings["set-papersize"],
     "set-textwrap",               "default textwrap (e.g. 80 (characters)",                         &settings["set-textwrap"],
+    "skel",                       "skel (dummy outline)",                                           &opts["skel"],
     "sqlite-discrete",            "process discrete sqlite output",                                 &opts["sqlite-discrete"],
     "sqlite-db-create",           "create db, create tables",                                       &opts["sqlite-db-create"],
     "sqlite-db-drop",             "drop tables & db",                                               &opts["sqlite-db-drop"],
@@ -339,7 +343,7 @@ string program_name = "spine";
   if (helpInfo.helpWanted) {
     defaultGetoptPrinter("Some information about the program.", helpInfo.options);
   }
-  enum outTask { source_or_pod, sqlite, sqlite_multi, latex, odt, epub, html_scroll, html_seg, html_stuff }
+  enum outTask { source_or_pod, sqlite, sqlite_multi, latex, odt, epub, html_scroll, html_seg, html_stuff, text, skel }
   struct OptActions {
     @trusted bool allow_downloads() {
       return opts["allow-downloads"];
@@ -443,6 +447,12 @@ string program_name = "spine";
     @trusted bool html_link_pdf_letter() {
       return (opts["html-link-pdf-letter"]) ? true : false;
     }
+    @trusted bool html_link_text() {
+      return (opts["html-link-text"]) ? true : false;
+    }
+    @trusted bool text_link_curate() {
+      return (opts["text-link-curate"]) ? true : false;
+    }
     @trusted bool html_link_search() {
       return (opts["html-link-search"]) ? true : false;
     }
@@ -551,6 +561,12 @@ string program_name = "spine";
         || opts["sqlite-update"]
       ) ? true : false;
     }
+    @trusted bool skel() {
+      return opts["skel"];
+    }
+    @trusted bool text() {
+      return opts["text"];
+    }
     @trusted bool vox_0() { // --silent
       return opts["vox_is0"];
     }
@@ -587,9 +603,6 @@ string program_name = "spine";
     @trusted bool vox_default()      { return vox_gt_1; } // defalt, & above
     @trusted bool vox_verbose()      { return vox_gt_2; } // --verbose -v & above
     @trusted bool vox_very_verbose() { return vox_gt_3; } // --very-verbose
-    @trusted bool text() {
-      return opts["text"];
-    }
     @trusted bool xhtml() {
       return opts["xhtml"];
     }
@@ -703,6 +716,8 @@ string program_name = "spine";
         || latex
         || manifest
         || sqlite_discrete
+        || text
+        || skel
       ) {
         _is = true;
       } else { _is = false; }
@@ -721,6 +736,8 @@ string program_name = "spine";
       if (html_stuff)      { schedule ~= outTask.html_stuff; }
       if (odt)             { schedule ~= outTask.odt; }
       if (latex)           { schedule ~= outTask.latex; }
+      if (text)            { schedule ~= outTask.text; }
+      if (skel)            { schedule ~= outTask.skel; }
       return schedule.sort().uniq;
     }
     @trusted bool abstraction() {
@@ -737,6 +754,8 @@ string program_name = "spine";
         || sqlite_discrete
         || sqlite_delete
         || sqlite_update
+        || text
+        || skel
       ) ? true : false;
     }
     @trusted bool require_processing_files() {
@@ -758,6 +777,7 @@ string program_name = "spine";
         || sqlite_update
         || text
         || xhtml
+        || skel
       ) ? true : false;
     }
     @trusted bool meta_processing_general() {
@@ -770,6 +790,8 @@ string program_name = "spine";
         || latex
         || sqlite_discrete
         || sqlite_update
+        || text
+        || skel
       ) ? true :false;
     }
     @trusted bool meta_processing_xml_dom() {
-- 
cgit v1.2.3