lmi-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[lmi-commits] [lmi] master 408ba18 156/156: Merge branch 'gwc-no-xslfo'


From: Greg Chicares
Subject: [lmi-commits] [lmi] master 408ba18 156/156: Merge branch 'gwc-no-xslfo' [387]
Date: Tue, 30 Jan 2018 17:22:36 -0500 (EST)

branch: master
commit 408ba18d6d140fb8d6bda522e5a4cbd19e6c2be6
Merge: 70fb246 47bddbf
Author: Gregory W. Chicares <address@hidden>
Commit: Gregory W. Chicares <address@hidden>

    Merge branch 'gwc-no-xslfo' [387]
---
 Makefile.am                                |   21 +-
 contract_numbers.mst                       |   46 +
 cover.mst                                  |  148 ++
 dollar_units.mst                           |   32 +
 emit_ledger.cpp                            |    2 +
 group_quote_pdf_gen_wx.cpp                 |  556 ++----
 html.cpp                                   |  128 ++
 html.hpp                                   |  329 ++++
 imprimatur.mst                             |   40 +
 interpolate_string.cpp                     |  296 +++
 interpolate_string.hpp                     |   69 +
 interpolate_string_test.cpp                |  233 +++
 ledger.hpp                                 |    5 +
 ledger_evaluator.cpp                       |  864 ++++++++
 ledger_evaluator.hpp                       |   59 +
 ledger_pdf.cpp                             |   56 +
 ledger_xsl.hpp => ledger_pdf.hpp           |   13 +-
 ledger_xsl.hpp => ledger_pdf_generator.cpp |   34 +-
 ledger_pdf_generator.hpp                   |   63 +
 ledger_pdf_generator_wx.cpp                | 2940 ++++++++++++++++++++++++++++
 ledger_xsl.cpp                             |    4 +-
 ledger_xsl.hpp                             |    2 +-
 main_wx.cpp                                |    1 +
 main_wx_test.cpp                           |    1 +
 nasd_assumption_detail.mst                 |   29 +
 nasd_basic.mst                             |   35 +
 nasd_column_headings.mst                   |   96 +
 nasd_footer_lower.mst                      |   40 +
 nasd_footer_upper.mst                      |   34 +
 nasd_header.mst                            |   29 +
 nasd_header_lower.mst                      |   84 +
 nasd_header_upper.mst                      |   39 +
 nasd_notes1.mst                            |  137 ++
 nasd_notes2.mst                            |  244 +++
 nasd_supp.mst                              |   29 +
 nasd_supp_report.mst                       |   28 +
 objects.make                               |   22 +-
 ledger_xsl.hpp => output_mode.hpp          |   27 +-
 pdf_writer_wx.cpp                          |  249 +++
 pdf_writer_wx.hpp                          |  100 +
 reg_column_headings.mst                    |  202 ++
 reg_d_group_basic.mst                      |   36 +
 reg_d_group_column_headings.mst            |   56 +
 reg_d_group_footer_lower.mst               |   40 +
 reg_d_group_footer_upper.mst               |   33 +
 reg_d_group_header.mst                     |   29 +
 reg_d_group_header_lower.mst               |   77 +
 reg_d_group_header_upper.mst               |   39 +
 reg_d_group_narr_summary.mst               |  123 ++
 reg_d_group_narr_summary2.mst              |  161 ++
 reg_d_group_supp_report.mst                |   28 +
 reg_d_indiv_cover_page.mst                 |  130 ++
 reg_d_indiv_curr.mst                       |   34 +
 reg_d_indiv_curr_irr.mst                   |   34 +
 reg_d_indiv_footer_lower.mst               |   40 +
 reg_d_indiv_footer_upper.mst               |   28 +
 reg_d_indiv_guar_irr.mst                   |   34 +
 reg_d_indiv_header.mst                     |   90 +
 reg_d_indiv_notes1.mst                     |  135 ++
 reg_d_indiv_notes2.mst                     |   56 +
 reg_d_indiv_notes3.mst                     |  186 ++
 reg_d_indiv_supp_report.mst                |   32 +
 reg_footer.mst                             |   40 +
 reg_footer_disclaimer.mst                  |   32 +
 reg_header.mst                             |  131 ++
 reg_narr_summary.mst                       |  203 ++
 reg_narr_summary2.mst                      |  258 +++
 reg_numeric_summary.mst                    |  203 ++
 reg_supp_report.mst                        |   28 +
 reg_tabular_details.mst                    |   29 +
 reg_tabular_details2.mst                   |   27 +
 test_coding_rules.cpp                      |   20 +-
 workhorse.make                             |    2 +-
 wx_table_generator.cpp                     |  168 +-
 wx_table_generator.hpp                     |   59 +-
 75 files changed, 9536 insertions(+), 451 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index aad985a..060d5ff 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -110,6 +110,7 @@ TESTS = \
     test_ieee754 \
     test_input_seq \
     test_input \
+    test_interpolate_string \
     test_irc7702a \
     test_istream_to_string \
     test_loads \
@@ -172,6 +173,7 @@ libskeleton_la_SOURCES = \
     illustration_document.cpp \
     illustration_view.cpp \
     input_sequence_entry.cpp \
+    ledger_pdf_generator_wx.cpp \
     main_common.cpp \
     mec_document.cpp \
     mec_view.cpp \
@@ -180,6 +182,7 @@ libskeleton_la_SOURCES = \
     multidimgrid_tools.cpp \
     mvc_controller.cpp \
     mvc_view.cpp \
+    pdf_writer_wx.cpp \
     policy_document.cpp \
     policy_view.cpp \
     preferences_view.cpp \
@@ -316,6 +319,7 @@ liblmi_common_sources = \
     global_settings.cpp \
     group_values.cpp \
     group_quote_pdf_gen.cpp \
+    html.cpp \
     illustrator.cpp \
     input.cpp \
     input_harmonization.cpp \
@@ -325,13 +329,15 @@ liblmi_common_sources = \
     input_sequence_parser.cpp \
     input_xml_io.cpp \
     interest_rates.cpp \
+    interpolate_string.cpp \
     ledger.cpp \
     ledger_base.cpp \
+    ledger_evaluator.cpp \
     ledger_invariant.cpp \
+    ledger_pdf.cpp \
+    ledger_pdf_generator.cpp \
     ledger_text_formats.cpp \
     ledger_variant.cpp \
-    ledger_xml_io.cpp \
-    ledger_xsl.cpp \
     ledgervalues.cpp \
     license.cpp \
     loads.cpp \
@@ -775,6 +781,11 @@ test_input_LDADD = \
   $(BOOST_LIBS) \
   $(XMLWRAPP_LIBS)
 
+test_interpolate_string_SOURCES = \
+  $(common_test_objects) \
+  interpolate_string.cpp \
+  interpolate_string_test.cpp
+
 test_irc7702a_SOURCES = \
   $(common_test_objects) \
   ihs_irc7702a.cpp \
@@ -1150,6 +1161,7 @@ noinst_HEADERS = \
     group_quote_pdf_gen.hpp \
     group_values.hpp \
     handle_exceptions.hpp \
+    html.hpp \
     icon_monger.hpp \
     ieee754.hpp \
     ihs_irc7702.hpp \
@@ -1166,13 +1178,16 @@ noinst_HEADERS = \
     input_sequence_interval.hpp \
     input_sequence_parser.hpp \
     interest_rates.hpp \
+    interpolate_string.hpp \
     istream_to_string.hpp \
     ledger.hpp \
     ledger_base.hpp \
+    ledger_evaluator.hpp \
     ledger_invariant.hpp \
+    ledger_pdf.hpp \
+    ledger_pdf_generator.hpp \
     ledger_text_formats.hpp \
     ledger_variant.hpp \
-    ledger_xsl.hpp \
     ledgervalues.hpp \
     license.hpp \
     lmi.hpp \
diff --git a/contract_numbers.mst b/contract_numbers.mst
new file mode 100644
index 0000000..b4142bc
--- /dev/null
+++ b/contract_numbers.mst
@@ -0,0 +1,46 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{#HasMasterContract}}
+    Master contract:
+    {{#HasPolicyNumber}}
+        {{^Composite}}
+            {{MasterContractNumberAbbrev15}}
+        {{/Composite}}
+        {{#Composite}}
+            {{MasterContractNumberAbbrev30}}
+        {{/Composite}}
+    {{/HasPolicyNumber}}
+    {{^HasPolicyNumber}}
+        {{MasterContractNumberAbbrev30}}
+    {{/HasPolicyNumber}}
+{{/HasMasterContract}}
+{{#HasPolicyNumber}}
+    {{^Composite}}
+        Contract number:
+        {{#HasMasterContract}}
+            {{ContractNumberAbbrev15}}
+        {{/HasMasterContract}}
+        {{^HasMasterContract}}
+            {{ContractNumberAbbrev30}}
+        {{/HasMasterContract}}
+    {{/Composite}}
+{{/HasPolicyNumber}}
diff --git a/cover.mst b/cover.mst
new file mode 100644
index 0000000..f99f76d
--- /dev/null
+++ b/cover.mst
@@ -0,0 +1,148 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<table width="100%">
+    <!--
+    Rows such as this are used as separators between visible table elements,
+    this is unfortunately necessary because wxHTML doesn't support
+    margin/padding on table elements.
+    -->
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr>
+        <td align="center">
+            <font size="+4">
+                <b>{{PolicyMktgName}}</b>
+            </font>
+        </td>
+    </tr>
+    <tr>
+    </tr>
+    <tr>
+        <td align="center">
+            <font size="+4">
+                <b>
+    {{#IsInforce}}
+    In Force Life Insurance Illustration
+    {{/IsInforce}}
+    {{^IsInforce}}
+    Life Insurance Illustration
+    {{/IsInforce}}
+                </b>
+            </font>
+        </td>
+    </tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                <b>Prepared for:</b>
+            </font>
+        </td>
+    </tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+    {{#Composite}}
+    {{CorpName}}
+    {{/Composite}}
+    {{^Composite}}
+    {{Insured1}}
+    {{/Composite}}
+            </font>
+        </td>
+    </tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                <b>Presented by:</b>
+            </font>
+        </td>
+    </tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                {{ProducerName}}
+            </font>
+        </td>
+    </tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                {{ProducerStreet}}
+            </font>
+        </td>
+    </tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                {{ProducerCity}}
+            </font>
+        </td>
+    </tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr>
+        <td align="center">
+            <font size="+2">
+                {{date_prepared}}
+            </font>
+        </td>
+    </tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+    <tr><td>&nbsp;</td></tr>
+</table>
+
+<!--
+This is ugly and error-prone, but there is no way to put the paragraph below at
+the bottom of the page currently, so we just insert enough white space to push
+it roughly where we want it to appear.
+-->
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+
+<p align="center"><scaled_image inv_factor="0.6" 
src="company_logo.png"></scaled_image></p>
+
+<br></br>
+<br></br>
+<br></br>
+
+<font size="-1">
+
+<p align="center">{{MarketingNameFootnote}}</p>
+
+<p>{{StateMarketingImprimatur}}</p>
+
+</font>
diff --git a/dollar_units.mst b/dollar_units.mst
new file mode 100644
index 0000000..8f888d9
--- /dev/null
+++ b/dollar_units.mst
@@ -0,0 +1,32 @@
+{{!
+    Define a fragment common to several pages.
+
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<p align="center">
+<font size="-1">
+Values shown are in
+{{#HasScaleUnit}}
+{{ScaleUnit}}s of
+{{/HasScaleUnit}}
+dollars
+</font>
+</p>
diff --git a/emit_ledger.cpp b/emit_ledger.cpp
index 82d3249..2dddb93 100644
--- a/emit_ledger.cpp
+++ b/emit_ledger.cpp
@@ -30,7 +30,9 @@
 #include "file_command.hpp"
 #include "group_quote_pdf_gen.hpp"
 #include "ledger.hpp"
+#include "ledger_pdf.hpp"
 #include "ledger_text_formats.hpp"
+// PDF !! Expunge the next line:
 #include "ledger_xsl.hpp"
 #include "miscellany.hpp"               // ios_out_trunc_binary()
 #include "path_utility.hpp"             // unique_filepath()
diff --git a/group_quote_pdf_gen_wx.cpp b/group_quote_pdf_gen_wx.cpp
index 2221777..8c5bed5 100644
--- a/group_quote_pdf_gen_wx.cpp
+++ b/group_quote_pdf_gen_wx.cpp
@@ -28,6 +28,7 @@
 #include "calendar_date.hpp"            // jdn_t()
 #include "data_directory.hpp"           // AddDataDir()
 #include "force_linking.hpp"
+#include "html.hpp"
 #include "ledger.hpp"
 #include "ledger_invariant.hpp"
 #include "ledger_text_formats.hpp"      // ledger_format()
@@ -36,6 +37,7 @@
 #include "miscellany.hpp"               // split_into_lines()
 #include "oecumenic_enumerations.hpp"   // oenum_format_style
 #include "path_utility.hpp"             // fs::path inserter
+#include "pdf_writer_wx.hpp"
 #include "version.hpp"
 #include "wx_table_generator.hpp"
 #include "wx_utility.hpp"               // ConvertDateToWx()
@@ -44,10 +46,7 @@
 #include <boost/filesystem/path.hpp>
 
 #include <wx/datetime.h>
-#include <wx/html/htmlcell.h>
-#include <wx/html/winpars.h>
 #include <wx/image.h>
-#include <wx/pdfdc.h>
 
 #include <cstring>                      // strstr()
 #include <limits>
@@ -61,119 +60,28 @@ LMI_FORCE_LINKING_IN_SITU(group_quote_pdf_generator_wx)
 namespace
 {
 
-enum enum_output_mode
-    {e_output_normal
-    ,e_output_measure_only
-    };
-
-/// Escape special XML characters in the given string, ensuring that it appears
-/// correctly inside HTML element contents. Notice that we don't need to escape
-/// quotes here as we never use the result of this function inside an HTML
-/// attribute, only inside HTML elements.
-
-wxString escape_for_html_elem(std::string const& s)
-{
-    wxString const u = wxString::FromUTF8(s.c_str());
-
-    wxString z;
-    z.reserve(u.length());
-    for(auto const& i : u)
-        {
-        switch(i.GetValue())
-            {
-            case '<': z += "&lt;" ; break;
-            case '>': z += "&gt;" ; break;
-            case '&': z += "&amp;"; break;
-            default : z += i      ;
-            }
-        }
-    return z;
-}
-
-/// Namespace for helpers used for HTML generation.
-
-namespace html
-{
-
-/// Namespace for the support HTML tags.
-///
-/// Tags are only used as template arguments, so they don't need to be defined,
-/// just declared -- and tag_info below specialized for them.
+/// Transform 's' -> '<br><br>s', but return empty string unchanged.
 
-namespace tag
+html::text brbr(std::string const& s)
 {
+    using namespace html;
 
-struct b;
-struct br;
-
-} // namespace tag
-
-template<typename T>
-struct tag_info;
-
-template<>
-struct tag_info<tag::b>
-{
-    static char const* get_name() { return "b"; }
-    static bool has_end() { return true; }
-};
-
-template<>
-struct tag_info<tag::br>
-{
-    static char const* get_name() { return "br"; }
-    static bool has_end() { return false; }
-};
-
-} // namespace html
-
-/// Wrap the given text in an HTML tag if it is not empty, otherwise just
-/// return an empty string.
-///
-/// For the tags without matching closing tags, such as e.g. "<br>", wrapping
-/// the text means just prepending the tag to it. This is still done only if
-/// the text is not empty.
-
-template<typename T>
-wxString wrap_if_not_empty(wxString const& html)
-{
-    wxString result;
-    if(!html.empty())
-        {
-        result << '<' << html::tag_info<T>::get_name() << '>' << html;
-        if(html::tag_info<T>::has_end())
-            {
-            result << "</" << html::tag_info<T>::get_name() << '>';
-            }
-        }
-
-    return result;
+    return s.empty()
+        ? text()
+        : tag::br + tag::br + text::from(s)
+        ;
 }
 
-/// Transform 'html' -> '<br><br>html', but return empty string unchanged.
+/// Transform 's' -> '<br><br><b>s</b>', but return empty string unchanged.
 
-wxString brbr(std::string const& html)
+html::text brbrb(std::string const& s)
 {
-    return
-        wrap_if_not_empty<html::tag::br>
-            (wrap_if_not_empty<html::tag::br>
-                (escape_for_html_elem(html)
-                )
-            );
-}
-
-/// Transform 'html' -> '<br><br><b>html</b>', but return empty string 
unchanged.
+    using namespace html;
 
-wxString brbrb(std::string const& html)
-{
-    return
-        wrap_if_not_empty<html::tag::br>
-            (wrap_if_not_empty<html::tag::br>
-                (wrap_if_not_empty<html::tag::b>
-                    (escape_for_html_elem(html)
-                    )
-                )
-            );
+    return s.empty()
+        ? text()
+        : tag::br + tag::br + tag::b(text::from(s))
+        ;
 }
 
 /// Generate HTML representation of a field name and value in an HTML table.
@@ -181,18 +89,27 @@ wxString brbrb(std::string const& html)
 /// The HTML fragment generated by this function contains two <td> tags with
 /// the given contents.
 
-wxString name_value_as_html_table_data
+html::text name_value_as_html_table_data
     (std::string const& name
     ,std::string const& value
     )
 {
-    return wxString::Format
-        ("<td nowrap align=\"right\"><b>%s%s&nbsp;&nbsp;</b></td>"
-         "<td>%s&nbsp;&nbsp;&nbsp;&nbsp;</td>"
-        ,escape_for_html_elem(name)
-        ,(value.empty() ? "" : ":")
-        ,escape_for_html_elem(value)
-        );
+    using namespace html;
+
+    auto const nbsp2 = text::nbsp() + text::nbsp();
+
+    return
+        tag::td[attr::nowrap][attr::align("right")]
+            (tag::b
+                (text::from(name))
+                (text::from(value.empty() ? "" : ":"))
+                (nbsp2)
+            )
+        +
+        tag::td
+            (text::from(value))
+            (nbsp2 + nbsp2)
+        ;
 }
 
 /// Simple description of a custom field, consisting of a non-empty name and a
@@ -301,100 +218,6 @@ wxImage load_image(char const* file)
     return image;
 }
 
-/// Output an image at the given scale into the PDF.
-///
-/// The scale specifies how many times the image should be shrunk:
-/// scale > 1 makes the image smaller, while scale < 1 makes it larger.
-///
-/// Updates pos_y by increasing it by the height of the specified
-/// image at the given scale.
-
-void output_image
-    (wxPdfDC&         pdf_dc
-    ,wxImage const&   image
-    ,char const*      image_name
-    ,double           scale
-    ,int              x
-    ,int*             pos_y
-    ,enum_output_mode output_mode = e_output_normal
-    )
-{
-    int const y = wxRound(image.GetHeight() / scale);
-
-    switch(output_mode)
-        {
-        case e_output_normal:
-            {
-            // Use wxPdfDocument API directly as wxDC doesn't provide a way to
-            // set the image scale at PDF level and also because passing via
-            // wxDC wastefully converts wxImage to wxBitmap only to convert it
-            // back to wxImage when embedding it into the PDF.
-            wxPdfDocument* const pdf_doc = pdf_dc.GetPdfDocument();
-            LMI_ASSERT(pdf_doc);
-
-            pdf_doc->SetImageScale(scale);
-            pdf_doc->Image(image_name, image, x, *pos_y);
-            pdf_doc->SetImageScale(1);
-            }
-            break;
-        case e_output_measure_only:
-            // Do nothing.
-            break;
-        default:
-            {
-            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
-            }
-        }
-
-    *pos_y += y;
-}
-
-/// Render, or just pretend rendering in order to measure it, the given HTML
-/// contents at the specified position wrapping it at the given width.
-/// Return the height of the output (using this width).
-
-int output_html
-    (wxHtmlWinParser& html_parser
-    ,int x
-    ,int y
-    ,int width
-    ,wxString const& html
-    ,enum_output_mode output_mode = e_output_normal
-    )
-{
-    std::unique_ptr<wxHtmlContainerCell> const cell
-        (static_cast<wxHtmlContainerCell*>(html_parser.Parse(html))
-        );
-    LMI_ASSERT(cell);
-
-    cell->Layout(width);
-    switch(output_mode)
-        {
-        case e_output_normal:
-            {
-            wxHtmlRenderingInfo rendering_info;
-            cell->Draw
-                (*html_parser.GetDC()
-                ,x
-                ,y
-                ,0
-                ,std::numeric_limits<int>::max()
-                ,rendering_info
-                );
-            }
-            break;
-        case e_output_measure_only:
-            // Do nothing.
-            break;
-        default:
-            {
-            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
-            }
-        }
-
-    return cell->GetHeight();
-}
-
 enum enum_group_quote_columns
     {e_col_number
     ,e_col_name
@@ -450,52 +273,46 @@ class group_quote_pdf_generator_wx
     void save(std::string const& output_filename) override;
 
   private:
-    // These margins are arbitrary and can be changed to conform to subjective
+    // This value is arbitrary and can be changed to conform to subjective
     // preferences.
-    static int const horz_margin = 24;
-    static int const vert_margin = 36;
-    static int const vert_skip   = 12;
+    static int const vert_skip = 12;
 
     // Ctor is private as it is only used by do_create().
     group_quote_pdf_generator_wx() = default;
 
-    // Generate the PDF once we have all the data.
-    void do_generate_pdf(wxPdfDC& pdf_dc);
-
     // Compute the number of pages needed by the table rows in the output given
     // the space remaining on the first page, the heights of the header, one
     // table row and the footer and the last row position.
     // Remaining space contains the space on the first page on input and is
     // updated with the space remaining on the last page on output.
     int compute_pages_for_table_rows
-        (int* remaining_space
+        (pdf_writer_wx& pdf_writer
+        ,int* remaining_space
         ,int  header_height
         ,int  row_height
         ,int  last_row_y
         );
 
     void output_page_number_and_version
-        (wxPdfDC& pdf_dc
+        (pdf_writer_wx& pdf_writer
         ,int      total_pages
         ,int      current_page
         );
     void output_image_header
-        (wxPdfDC& pdf_dc
+        (pdf_writer_wx& pdf_writer
         ,int*     pos_y
         );
     void output_document_header
-        (wxPdfDC&         pdf_dc
-        ,wxHtmlWinParser& html_parser
+        (pdf_writer_wx&   pdf_writer
         ,int*             pos_y
         );
     void output_aggregate_values
-        (wxPdfDC&            pdf_dc
+        (pdf_writer_wx&      pdf_writer
         ,wx_table_generator& table_gen
         ,int*                pos_y
         );
     void output_footer
-        (wxPdfDC&         pdf_dc
-        ,wxHtmlWinParser& html_parser
+        (pdf_writer_wx&   pdf_writer
         ,int*             pos_y
         ,enum_output_mode output_mode = e_output_normal
         );
@@ -514,7 +331,7 @@ class group_quote_pdf_generator_wx
         std::string premium_mode_;
         std::string contract_state_;
         std::string effective_date_;
-        wxString    footer_html_;
+        html::text  footer_html_;
 
         // Dynamically-determined fields.
         std::string elected_riders_;
@@ -561,24 +378,6 @@ class group_quote_pdf_generator_wx
     };
     totals_data totals_;
 
-    struct page_metrics
-        {
-        page_metrics()
-            :width_(0)
-            {
-            }
-
-        void initialize(wxDC const& dc)
-            {
-            total_size_ = dc.GetSize();
-            width_ = total_size_.x - 2 * horz_margin;
-            }
-
-        wxSize total_size_;
-        int width_;
-        };
-    page_metrics page_;
-
     int row_num_              {0};
     int individual_selection_ {99};
 };
@@ -858,59 +657,20 @@ void group_quote_pdf_generator_wx::add_ledger(Ledger 
const& ledger)
 
 void group_quote_pdf_generator_wx::save(std::string const& output_filename)
 {
-    // Create a wxPrintData object just to describe the paper to use.
-    wxPrintData print_data;
-    print_data.SetOrientation(wxLANDSCAPE);
-    print_data.SetPaperId(wxPAPER_LETTER);
-    print_data.SetFilename(output_filename);
-
-    wxPdfDC pdf_dc(print_data);
-    page_.initialize(pdf_dc);
-    do_generate_pdf(pdf_dc);
-    pdf_dc.EndDoc();
-}
-
-void group_quote_pdf_generator_wx::do_generate_pdf(wxPdfDC& pdf_dc)
-{
-    // Ensure that the output is independent of the current display resolution:
-    // it seems that this is only the case with the PDF map mode and wxDC mode
-    // different from wxMM_TEXT.
-    pdf_dc.SetMapModeStyle(wxPDF_MAPMODESTYLE_PDF);
-
-    // For simplicity, use points for everything: font sizers are expressed in
-    // them anyhow, so it's convenient to use them for everything else too.
-    pdf_dc.SetMapMode(wxMM_POINTS);
-
-    pdf_dc.StartDoc(wxString()); // Argument is not used.
-    pdf_dc.StartPage();
-
-    // Use a standard PDF Helvetica font (without embedding any custom fonts in
-    // the generated file, the only other realistic choice is Times New Roman).
-    pdf_dc.SetFont
-        (wxFontInfo(8).Family(wxFONTFAMILY_SWISS).FaceName("Helvetica")
-        );
-
-    // Create an HTML parser to allow easily adding HTML contents to the 
output.
-    wxHtmlWinParser html_parser(nullptr);
-    html_parser.SetDC(&pdf_dc);
-    html_parser.SetStandardFonts
-        (pdf_dc.GetFont().GetPointSize()
-        ,"Helvetica"
-        ,"Courier"
-        );
+    pdf_writer_wx pdf_writer(output_filename, wxLANDSCAPE);
 
     int pos_y = 0;
 
-    output_image_header(pdf_dc, &pos_y);
+    output_image_header(pdf_writer, &pos_y);
     pos_y += 2 * vert_skip;
 
-    output_document_header(pdf_dc, html_parser, &pos_y);
+    output_document_header(pdf_writer, &pos_y);
     pos_y += 2 * vert_skip;
 
     wx_table_generator table_gen
-        (pdf_dc
-        ,horz_margin
-        ,page_.width_
+        (pdf_writer.dc()
+        ,pdf_writer.get_horz_margin()
+        ,pdf_writer.get_page_width()
         );
 
     // Some of the table columns don't need to be shown if all the values in
@@ -976,21 +736,22 @@ void 
group_quote_pdf_generator_wx::do_generate_pdf(wxPdfDC& pdf_dc)
         table_gen.add_column(header, cd.widest_text_);
         }
 
-    output_aggregate_values(pdf_dc, table_gen, &pos_y);
+    output_aggregate_values(pdf_writer, table_gen, &pos_y);
 
     int const y_before_header = pos_y;
     table_gen.output_header(&pos_y);
     int const header_height = pos_y - y_before_header;
 
     int y_after_footer = pos_y;
-    output_footer(pdf_dc, html_parser, &y_after_footer, e_output_measure_only);
+    output_footer(pdf_writer, &y_after_footer, e_output_measure_only);
     int const footer_height = y_after_footer - pos_y;
 
-    int const last_row_y = page_.total_size_.y - vert_margin;
+    int const last_row_y = pdf_writer.get_page_bottom();
     int remaining_space = last_row_y - pos_y;
 
     int total_pages = compute_pages_for_table_rows
-        (&remaining_space
+        (pdf_writer
+        ,&remaining_space
         ,header_height
         ,table_gen.row_height()
         ,last_row_y
@@ -1013,38 +774,39 @@ void 
group_quote_pdf_generator_wx::do_generate_pdf(wxPdfDC& pdf_dc)
 
         if(last_row_y <= pos_y)
             {
-            output_page_number_and_version(pdf_dc, total_pages, current_page);
+            output_page_number_and_version(pdf_writer, total_pages, 
current_page);
 
             current_page++;
-            pdf_dc.StartPage();
+            pdf_writer.dc().StartPage();
 
-            pos_y = vert_margin;
+            pos_y = pdf_writer.get_vert_margin();
             table_gen.output_header(&pos_y);
             }
         }
 
     if(footer_on_its_own_page)
         {
-        output_page_number_and_version(pdf_dc, total_pages, current_page);
+        output_page_number_and_version(pdf_writer, total_pages, current_page);
 
         current_page++;
-        pdf_dc.StartPage();
+        pdf_writer.dc().StartPage();
 
-        pos_y = vert_margin;
+        pos_y = pdf_writer.get_vert_margin();
         }
     else
         {
         pos_y += 2 * vert_skip;
         }
 
-    output_footer(pdf_dc, html_parser, &pos_y);
+    output_footer(pdf_writer, &pos_y);
 
     LMI_ASSERT(current_page == total_pages);
-    output_page_number_and_version(pdf_dc, total_pages, current_page);
+    output_page_number_and_version(pdf_writer, total_pages, current_page);
 }
 
 int group_quote_pdf_generator_wx::compute_pages_for_table_rows
-    (int* remaining_space
+    (pdf_writer_wx& pdf_writer
+    ,int* remaining_space
     ,int header_height
     ,int row_height
     ,int last_row_y
@@ -1060,7 +822,8 @@ int 
group_quote_pdf_generator_wx::compute_pages_for_table_rows
         // rest of them.
         remaining_rows -= max_rows_on_first_page;
 
-        int const page_area_y = last_row_y - vert_margin - header_height;
+        int const first_row_y = pdf_writer.get_vert_margin() + header_height;
+        int const page_area_y = last_row_y - first_row_y;
         int const rows_per_page = page_area_y / row_height;
         total_pages += (remaining_rows + rows_per_page - 1) / rows_per_page;
         *remaining_space = page_area_y;
@@ -1073,18 +836,20 @@ int 
group_quote_pdf_generator_wx::compute_pages_for_table_rows
 }
 
 void group_quote_pdf_generator_wx::output_page_number_and_version
-    (wxPdfDC& pdf_dc
+    (pdf_writer_wx& pdf_writer
     ,int total_pages
     ,int current_page
     )
 {
     wxRect const footer_area
-        (horz_margin
-        ,page_.total_size_.y - vert_margin
-        ,page_.width_
-        ,vert_margin
+        (pdf_writer.get_horz_margin()
+        ,pdf_writer.get_page_bottom()
+        ,pdf_writer.get_page_width()
+        ,pdf_writer.get_vert_margin()
         );
 
+    auto& pdf_dc = pdf_writer.dc();
+
     pdf_dc.DrawLabel
         (wxString::Format("System version: %s", LMI_VERSION)
         ,footer_area
@@ -1099,7 +864,7 @@ void 
group_quote_pdf_generator_wx::output_page_number_and_version
 }
 
 void group_quote_pdf_generator_wx::output_image_header
-    (wxPdfDC& pdf_dc
+    (pdf_writer_wx& pdf_writer
     ,int* pos_y
     )
 {
@@ -1110,15 +875,17 @@ void group_quote_pdf_generator_wx::output_image_header
         }
 
     // Set the scale to fit the image to the document width.
-    double const
-        scale = static_cast<double>(banner_image.GetWidth()) / 
page_.total_size_.x;
+    double const image_width = banner_image.GetWidth();
+    double const scale = image_width / pdf_writer.get_total_width();
     int const pos_top = *pos_y;
-    output_image(pdf_dc, banner_image, "banner", scale, 0, pos_y);
+    pdf_writer.output_image(banner_image, "banner", scale, 0, pos_y);
+
+    auto& pdf_dc = pdf_writer.dc();
 
     wxDCFontChanger set_bigger_font(pdf_dc, pdf_dc.GetFont().Scaled(1.5));
     wxDCTextColourChanger set_white_text(pdf_dc, *wxWHITE);
 
-    // Don't use escape_for_html_elem() here: instead, call
+    // Don't use html::text::from() here: instead, call
     // wxString::FromUTF8() directly, e.g., to preserve literal '&'.
     wxString const image_text
         (wxString::FromUTF8(report_data_.short_product_.c_str())
@@ -1128,7 +895,7 @@ void group_quote_pdf_generator_wx::output_image_header
     pdf_dc.DrawLabel
         (image_text
         ,wxRect
-            (wxPoint(horz_margin, (pos_top + *pos_y) / 2),
+            (wxPoint(pdf_writer.get_horz_margin(), (pos_top + *pos_y) / 2),
              pdf_dc.GetMultiLineTextExtent(image_text)
             )
         ,wxALIGN_CENTER_HORIZONTAL
@@ -1136,45 +903,53 @@ void group_quote_pdf_generator_wx::output_image_header
 }
 
 void group_quote_pdf_generator_wx::output_document_header
-    (wxPdfDC& pdf_dc
-    ,wxHtmlWinParser& html_parser
+    (pdf_writer_wx& pdf_writer
     ,int* pos_y
     )
 {
-    wxString const title_html = wxString::Format
-        ("<table width=\"100%%\">"
-         "<tr>"
-         "<td align=\"center\"><i><font size=\"+1\">%s</font></i></td>"
-         "</tr>"
-         "<tr>"
-         "<td align=\"center\"><i>Prepared Date: %s</i></td>"
-         "</tr>"
-         "<tr>"
-         "<td align=\"center\"><i>Prepared By: %s</i></td>"
-         "</tr>"
-         "</table>"
-        ,escape_for_html_elem(report_data_.company_)
-        ,wxDateTime::Today().FormatDate()
-        ,escape_for_html_elem(report_data_.prepared_by_)
+    using namespace html;
+
+    auto title_html =
+        tag::table[attr::width("100%")]
+            (tag::tr
+                (tag::td[attr::align("center")]
+                    (tag::i
+                        (tag::font[attr::size("+1")]
+                            (text::from(report_data_.company_)
+                            )
+                        )
+                    )
+                )
+            )
+            (tag::tr
+                (tag::td[attr::align("center")]
+                    (tag::i
+                        (text::from
+                            ("Prepared Date: "
+                            +wxDateTime::Today().FormatDate().ToStdString()
+                            )
+                        )
+                    )
+                )
+            )
+            (tag::tr
+                (tag::td[attr::align("center")]
+                    (tag::i
+                        (text::from("Prepared By: " + 
report_data_.prepared_by_)
+                        )
+                    )
+                )
+            );
+
+    pdf_writer.output_html
+        (pdf_writer.get_horz_margin()
+        ,*pos_y
+        ,pdf_writer.get_page_width() / 2
+        ,title_html
         );
 
-    output_html(html_parser, horz_margin, *pos_y, page_.width_ / 2, 
title_html);
-
-    // Build the summary table with all the mandatory fields.
-    wxString summary_html =
-         "<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">"
-         // This extra top empty row works around a bug in wxHTML
-         // table positioning code: it uses the provided ordinate
-         // coordinate as a base line of the first table line and
-         // not as its top, as it ought to, so without this line
-         // the rectangle drawn below wouldn't contain the header.
-         "<tr>"
-         "<td align=\"center\" colspan=\"4\">&nbsp;</td>"
-         "</tr>"
-         "<tr>"
-         "<td align=\"center\" colspan=\"4\"><font size=\"+1\">Plan Details 
Summary</font></td>"
-         "</tr>"
-         ;
+    // Build the summary table with all the mandatory fields, starting by
+    // building the (partly) dynamic fields rows part.
 
     // Add fixed fields first, then any additional ones,
     // in left-to-right then top-to-bottom order.
@@ -1196,34 +971,66 @@ void group_quote_pdf_generator_wx::output_document_header
     std::vector<extra_summary_field> const& f = report_data_.extra_fields_;
     fields.insert(fields.end(), f.begin(), f.end());
 
-    bool parity = true;
-    for(auto const& i : fields)
+    text fields_html;
+    for(std::size_t i = 0; i < fields.size(); i += 2)
         {
-        summary_html += parity ? "<tr>" : "";
-        summary_html += name_value_as_html_table_data(i.name, i.value);
-        summary_html += parity ? "" : "</tr>";
-        parity = !parity;
+        auto row_html = name_value_as_html_table_data
+            (fields[i].name, fields[i].value
+            )
+            ;
+
+        if(i + 1 < fields.size())
+            {
+            row_html += name_value_as_html_table_data
+                    (fields[i + 1].name, fields[i + 1].value
+                    )
+                    ;
+            }
+
+        fields_html += tag::tr(row_html);
         }
-    summary_html += parity ? "" : "</tr>";
 
     // Finally close the summary table.
-    summary_html += "</table>";
+    auto const summary_html =
+        tag::table[attr::width("100%")]
+                  [attr::cellspacing("0")]
+                  [attr::cellpadding("0")]
+            // This extra top empty row works around a bug in wxHTML
+            // table positioning code: it uses the provided ordinate
+            // coordinate as a base line of the first table line and
+            // not as its top, as it ought to, so without this line
+            // the rectangle drawn below wouldn't contain the header.
+            (tag::tr
+                (tag::td[attr::align("center")][attr::colspan("4")]
+                    (text::nbsp())
+                )
+            )
+            (tag::tr
+                (tag::td[attr::align("center")][attr::colspan("4")]
+                    (tag::font[attr::size("+1")]
+                        (text::from("Plan Details Summary"))
+                    )
+                )
+            )
+            (fields_html
+            )
+            ;
 
-    int const summary_height = output_html
-        (html_parser
-        ,horz_margin + page_.width_ / 2
+    int const summary_height = pdf_writer.output_html
+        (pdf_writer.get_horz_margin() + pdf_writer.get_page_width() / 2
         ,*pos_y
-        ,page_.width_ / 2
+        ,pdf_writer.get_page_width() / 2
         ,summary_html
         );
 
     // wxHTML tables don't support "frame" attribute, so draw the border around
     // the table manually.
+    auto& pdf_dc = pdf_writer.dc();
     pdf_dc.SetBrush(*wxTRANSPARENT_BRUSH);
     pdf_dc.DrawRectangle
-        (horz_margin + page_.width_ / 2
+        (pdf_writer.get_horz_margin() + pdf_writer.get_page_width() / 2
         ,*pos_y
-        ,page_.width_ / 2
+        ,pdf_writer.get_page_width() / 2
         ,summary_height
         );
 
@@ -1231,7 +1038,7 @@ void group_quote_pdf_generator_wx::output_document_header
 }
 
 void group_quote_pdf_generator_wx::output_aggregate_values
-    (wxPdfDC& pdf_dc
+    (pdf_writer_wx& pdf_writer
     ,wx_table_generator& table_gen
     ,int* pos_y
     )
@@ -1248,6 +1055,8 @@ void group_quote_pdf_generator_wx::output_aggregate_values
     table_gen.output_vert_separator(e_col_number, y);
     table_gen.output_vert_separator(e_col_number, y_next);
 
+    auto& pdf_dc = pdf_writer.dc();
+
     // Render "Census" in bold.
     wxDCFontChanger set_bold_font(pdf_dc, pdf_dc.GetFont().Bold());
     pdf_dc.DrawLabel
@@ -1364,8 +1173,7 @@ void group_quote_pdf_generator_wx::output_aggregate_values
 }
 
 void group_quote_pdf_generator_wx::output_footer
-    (wxPdfDC& pdf_dc
-    ,wxHtmlWinParser& html_parser
+    (pdf_writer_wx& pdf_writer
     ,int* pos_y
     ,enum_output_mode output_mode
     )
@@ -1375,18 +1183,24 @@ void group_quote_pdf_generator_wx::output_footer
         {
         // Arbitrarily scale down the logo by a factor of 2 to avoid making it
         // too big.
-        output_image(pdf_dc, logo_image, "company_logo", 2.0, horz_margin, 
pos_y, output_mode);
+        pdf_writer.output_image
+            (logo_image
+            ,"company_logo"
+            ,2.0
+            ,pdf_writer.get_horz_margin()
+            ,pos_y
+            ,output_mode
+            );
 
         *pos_y += vert_skip;
         }
 
-    wxString const footer_html = "<p>" + report_data_.footer_html_ + "</p>";
+    auto footer_html = html::tag::p(report_data_.footer_html_);
 
-    *pos_y += output_html
-        (html_parser
-        ,horz_margin
+    *pos_y += pdf_writer.output_html
+        (pdf_writer.get_horz_margin()
         ,*pos_y
-        ,page_.width_
+        ,pdf_writer.get_page_width()
         ,footer_html
         ,output_mode
         );
diff --git a/html.cpp b/html.cpp
new file mode 100644
index 0000000..c3427ee
--- /dev/null
+++ b/html.cpp
@@ -0,0 +1,128 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "html.hpp"
+
+#include <cstring>
+
+namespace html
+{
+
+namespace attr
+{
+
+extern attribute const align        ("align");
+extern attribute const cellpadding  ("cellpadding");
+extern attribute const cellspacing  ("cellspacing");
+extern attribute const colspan      ("colspan");
+extern attribute const nowrap       ("nowrap");
+extern attribute const size         ("size");
+extern attribute const valign       ("valign");
+extern attribute const width        ("width");
+
+} // namespace attr
+
+namespace tag
+{
+
+extern element      const b         ("b");
+extern void_element const br        ("br");
+extern element      const font      ("font");
+extern element      const i         ("i");
+extern element      const p         ("p");
+extern element      const table     ("table");
+extern element      const td        ("td");
+extern element      const tr        ("tr");
+
+} // namespace tag
+
+std::string attribute::as_string() const
+{
+    std::string s(name_);
+    if(!value_.empty())
+        {
+        s += "=";
+        // TODO: Escape quotes.
+        s += value_;
+        }
+    return s;
+}
+
+namespace detail
+{
+
+std::string any_element::get_start() const
+{
+    std::string s("<");
+    // Extra +1 for the space before attributes, even if it's not needed.
+    s.reserve(1 + std::strlen(name_) + 1 + attributes_.length() + 1);
+    s += name_;
+    if(!attributes_.empty())
+        {
+        s += " ";
+        s += attributes_;
+        }
+    s += ">";
+    return s;
+}
+
+void any_element::update_attributes(attribute const& attr)
+{
+    if(attributes_.empty())
+        {
+        attributes_ = attr.as_string();
+        }
+    else
+        {
+        attributes_ += " ";
+        attributes_ += attr.as_string();
+        }
+}
+
+} // namespace detail
+
+void element::update_contents(std::string&& contents)
+{
+    if(contents_.empty())
+        {
+        contents_ = std::move(contents);
+        }
+    else
+        {
+        contents_ += contents;
+        }
+}
+
+element::operator text() const
+{
+    std::string s(get_start());
+    s.reserve(s.length() + contents_.length() + 2 + std::strlen(name_) + 1);
+    s += contents_;
+    s += "</";
+    s += name_;
+    s += ">";
+
+    return text::from_html(std::move(s));
+}
+
+} // namespace html
diff --git a/html.hpp b/html.hpp
new file mode 100644
index 0000000..8ebb071
--- /dev/null
+++ b/html.hpp
@@ -0,0 +1,329 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef html_hpp
+#define html_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <string>
+#include <utility>                      // std::move
+
+/// Namespace for helpers used for HTML generation.
+///
+/// Main idea is to avoid generating HTML using raw strings, which is error
+/// prone and difficult to read and maintain. One source of errors is
+/// forgetting to escape special characters, such as "<" or "&", and html::text
+/// class helps with this by providing from() method doing it automatically.
+///
+/// Another one is forgetting to close a tag (or closing a wrong one) and while
+/// html::text is too low level to help with this, html::element can be used
+/// for structured HTML generation, which guarantees that the result is
+/// well-formed. By using predefined constants in html::tag and html::attr
+/// namespaces, typos in the element names can also be automatically avoided.
+namespace html
+{
+
+/// Represents a piece of text containing HTML.
+///
+/// This is a separate type for type safety, e.g. to avoid passing raw,
+/// unescaped, strings to a function expecting HTML (or, less catastrophically,
+/// but still wrongly, passing already escaped HTML to a function doing
+/// escaping internally).
+///
+/// As it still needs to be converted to a string sooner or later to be really
+/// used, it does provide a conversion -- but it can be used only once.
+class text
+{
+  public:
+    // This type has value semantics.
+    text() = default;
+    text(text const&) = default;
+    text(text&&) = default;
+    text& operator=(text const&) = default;
+    text& operator=(text&&) = default;
+
+    /// Escape special XML characters in the given string, ensuring that it
+    /// appears correctly inside HTML element contents. Notice that we don't
+    /// need to escape quotes here as we never use the result of this function
+    /// inside an HTML attribute, only inside HTML elements.
+    static text from(std::string const& s)
+    {
+        std::string z;
+        z.reserve(s.length());
+        for(auto const& c : s)
+            {
+            switch(c)
+                {
+                case '<': z += "&lt;" ; break;
+                case '>': z += "&gt;" ; break;
+                case '&': z += "&amp;"; break;
+                default : z += c      ;
+                }
+            }
+
+        return text{std::move(z)};
+    }
+
+    /// Use the given string with HTML inside it directly. No escaping is done
+    /// by this ctor.
+    static text from_html(std::string s)
+    {
+        return text{std::move(s)};
+    }
+
+    /// Just a symbolic name for a non breaking space HTML entiry.
+    static text nbsp()
+    {
+        return text::from_html("&nbsp;");
+    }
+
+    /// Append another text fragment to this one.
+    ///
+    /// This method allows chained invocation for appending more than one
+    /// fragment at once.
+    text& operator+=(text const& t)
+    {
+        m_html += t.m_html;
+
+        return *this;
+    }
+
+    std::string const& as_html() const&
+    {
+        return m_html;
+    }
+
+    std::string&& as_html() &&
+    {
+        return std::move(m_html);
+    }
+
+  private:
+    // This move ctor is private and does not perform any escaping.
+    explicit text(std::string&& html)
+        :m_html{html}
+    {
+    }
+
+    std::string m_html;
+};
+
+/// Represents a single attribute of an HTML element.
+class attribute
+{
+  public:
+    explicit attribute(char const* name)
+        :name_{name}
+    {
+    }
+
+    attribute operator()(std::string value) const
+    {
+        return attribute(name_, std::move(value));
+    }
+
+    std::string as_string() const;
+
+  private:
+    attribute(char const* name, std::string&& value)
+        :name_{name}
+        ,value_{std::move(value)}
+    {
+    }
+
+    char const* const name_;
+    std::string const value_;
+};
+
+namespace detail
+{
+
+class LMI_SO any_element
+{
+  public:
+    /// Ctor should only be used with literal strings as argument.
+    explicit any_element(char const* name)
+        :name_(name)
+    {
+    }
+
+  protected:
+    // Return the opening tag of the element, with attributes, if any.
+    std::string get_start() const;
+
+    // Add the given attribute to our attributes string.
+    void update_attributes(attribute const& attr);
+
+    char const* const name_;
+
+  private:
+    std::string       attributes_;
+};
+
+} // namespace detail
+
+/// Represents a normal HTML element which can have content inside it.
+///
+/// This class uses the so called fluent API model in which calls to its
+/// different methods return the object itself and so can be chained together.
+/// For example (assuming an implicit "using namespace html"):
+///
+///     auto para_with_link =
+///         tag::p[attr::align("center")]
+///             (text("Link to "))
+///             (tag::a[attr::href("http://lmi.nongnu.org/";)]
+///                 (text::from("lmi project page"))
+///             )
+///         ;
+
+class LMI_SO element : private detail::any_element
+{
+  public:
+    /// Ctor should only be used with literal strings as argument.
+    explicit element(char const* name)
+        :detail::any_element(name)
+    {
+    }
+
+    element(element const&) = default;
+    element(element&&) = default;
+
+    /// Add an attribute.
+    element operator[](attribute const& attr) const&
+    {
+        element e{*this};
+        e.update_attributes(attr);
+        return e;
+    }
+
+    element&& operator[](attribute const& attr) &&
+    {
+        update_attributes(attr);
+        return std::move(*this);
+    }
+
+    /// Add inner contents.
+    element operator()(text contents) const&
+    {
+        element e{*this};
+        e.update_contents(std::move(contents).as_html());
+        return e;
+    }
+
+    element&& operator()(text contents) &&
+    {
+        update_contents(std::move(contents).as_html());
+        return std::move(*this);
+    }
+
+    /// Convert to HTML text with this element and its contents.
+    ///
+    /// This implicit conversion operator is not really dangerous as it is
+    /// normal to represent an HTML element as HTML text and it's very
+    /// convenient to have it as it allows to accept either another element or
+    /// text in our own operator() and also use operator+() defined below to
+    /// concatenate HTML elements without having to convert them to text
+    /// beforehand.
+    operator text() const;
+
+  private:
+    void update_contents(std::string&& contents);
+
+    std::string contents_;
+};
+
+/// Represents a void HTML element which can't have anything inside it.
+class void_element : private detail::any_element
+{
+  public:
+    explicit void_element(char const* name)
+        :detail::any_element(name)
+    {
+    }
+
+    void_element(void_element const&) = default;
+    void_element(void_element&&) = default;
+
+    void_element operator[](attribute const& attr) const&
+    {
+        void_element e{*this};
+        e.update_attributes(std::move(attr));
+        return e;
+    }
+
+    void_element&& operator[](attribute const& attr) &&
+    {
+        update_attributes(std::move(attr));
+        return std::move(*this);
+    }
+
+    operator text() const
+    {
+        return text::from_html(get_start());
+    }
+};
+
+/// Namespace for HTML attributes.
+
+namespace attr
+{
+
+extern LMI_SO attribute const align;
+extern LMI_SO attribute const cellpadding;
+extern LMI_SO attribute const cellspacing;
+extern LMI_SO attribute const colspan;
+extern LMI_SO attribute const nowrap;
+extern LMI_SO attribute const size;
+extern LMI_SO attribute const valign;
+extern LMI_SO attribute const width;
+
+} // namespace attr
+
+/// Namespace for HTML tags.
+
+namespace tag
+{
+
+extern LMI_SO element      const b;
+extern LMI_SO void_element const br;
+extern LMI_SO element      const font;
+extern LMI_SO element      const i;
+extern LMI_SO element      const p;
+extern LMI_SO element      const table;
+extern LMI_SO element      const td;
+extern LMI_SO element      const tr;
+
+} // namespace tag
+
+inline
+text operator+(text t1, text const& t2)
+{
+    text t{std::move(t1)};
+    t += t2;
+    return t;
+}
+
+} // namespace html
+
+#endif // html_hpp
diff --git a/imprimatur.mst b/imprimatur.mst
new file mode 100644
index 0000000..ff83332
--- /dev/null
+++ b/imprimatur.mst
@@ -0,0 +1,40 @@
+{{!
+    This template defines the compliance tracking number used in a few
+    different places to avoid duplicating this logic in all of them.
+
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{#IsInforce}}
+    {{#Composite}}
+        {{ImprimaturInforceComposite}}
+    {{/Composite}}
+    {{^Composite}}
+        {{ImprimaturInforce}}
+    {{/Composite}}
+{{/IsInforce}}
+{{^IsInforce}}
+    {{#Composite}}
+        {{ImprimaturPresaleComposite}}
+    {{/Composite}}
+    {{^Composite}}
+        {{ImprimaturPresale}}
+    {{/Composite}}
+{{/IsInforce}}
diff --git a/interpolate_string.cpp b/interpolate_string.cpp
new file mode 100644
index 0000000..a372230
--- /dev/null
+++ b/interpolate_string.cpp
@@ -0,0 +1,296 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "alert.hpp"
+
+#include <stack>
+#include <stdexcept>
+#include <vector>
+
+namespace
+{
+
+// Information about a single section that we're currently in.
+struct section_info
+{
+    section_info(std::string const& name, bool active)
+        :name_(name)
+        ,active_(active)
+    {
+    }
+
+    // Name of the section, i.e. the part after "#".
+    //
+    // TODO: In C++14 this could be replaced with string_view which would
+    // save on memory allocations without compromising safety, as we know
+    // that the input string doesn't change during this function execution.
+    std::string const name_;
+
+    // If true, output section contents, otherwise simply eat it.
+    bool const active_;
+
+    // Note: we could also store the position of the section start here to
+    // improve error reporting. Currently this is done as templates we use
+    // are small and errors shouldn't be difficult to find even without the
+    // exact position, but this could change in the future.
+};
+
+// The only context we need is the stack of sections entered so far.
+using context = std::stack<section_info, std::vector<section_info>>;
+
+// The real interpolation recursive function, called by the public one to do
+// all the work.
+void do_interpolate_string_in_context
+    (char const* s
+    ,lookup_function const& lookup
+    ,std::string& out
+    ,context& sections
+    ,std::string const& partial = std::string()
+    ,int recursion_level = 0
+    )
+{
+    // Guard against too deep recursion to avoid crashing on code using too
+    // many nested partials (either unintentionally, e.g. due to including a
+    // partial from itself, or maliciously).
+    //
+    // The maximum recursion level is chosen completely arbitrarily, the only
+    // criteria are that it shouldn't be too big to crash due to stack overflow
+    // before it is reached nor too small to break legitimate use cases.
+    if(recursion_level >= 100)
+        {
+        alarum()
+            << "Nesting level too deep while expanding the partial \""
+            << partial
+            << "\""
+            << std::flush
+            ;
+        }
+
+    // Check if the output is currently active or suppressed because we're
+    // inside an inactive section.
+    auto const is_active = [&sections]()
+        {
+            return sections.empty() || sections.top().active_;
+        };
+
+    for(char const* p = s; *p; ++p)
+        {
+        // As we know that the string is NUL-terminated, it is safe to check
+        // the next character.
+        if(p[0] == '{' && p[1] == '{')
+            {
+            std::string name;
+            auto const pos_start = p - s + 1;
+            for(p += 2;; ++p)
+                {
+                if(*p == '\0')
+                    {
+                    alarum()
+                        << "Unmatched opening brace at position "
+                        << pos_start
+                        << std::flush
+                        ;
+                    }
+
+                if(p[0] == '}' && p[1] == '}')
+                    {
+                    switch(name.empty() ? '\0' : name[0])
+                        {
+                        case '#':
+                        case '^':
+                            {
+                            auto const real_name = name.substr(1);
+                            // If we're inside a disabled section, it doesn't
+                            // matter whether this one is active or not.
+                            bool active = is_active();
+                            if(active)
+                                {
+                                auto const value = lookup
+                                    (real_name
+                                    ,interpolate_lookup_kind::section
+                                    );
+                                if(value == "1")
+                                    {
+                                    active = true;
+                                    }
+                                else if(value == "0")
+                                    {
+                                    active = false;
+                                    }
+                                else
+                                    {
+                                    alarum()
+                                        << "Invalid value '"
+                                        << value
+                                        << "' of section '"
+                                        << real_name
+                                        << "' at position "
+                                        << pos_start
+                                        << ", only \"0\" or \"1\" allowed"
+                                        << std::flush
+                                        ;
+                                    }
+
+                                if(name[0] == '^')
+                                    {
+                                    active = !active;
+                                    }
+                                }
+
+                            sections.emplace(real_name, active);
+                            }
+                            break;
+
+                        case '/':
+                            if(sections.empty())
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " without previous section start"
+                                    << std::flush
+                                    ;
+                                }
+                            if(name.compare(1, std::string::npos, 
sections.top().name_) != 0)
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " while inside the section '"
+                                    << sections.top().name_
+                                    << "'"
+                                    << std::flush
+                                    ;
+                                }
+                            sections.pop();
+                            break;
+
+                        case '>':
+                            if(is_active())
+                                {
+                                auto const& real_name = name.substr(1);
+
+                                do_interpolate_string_in_context
+                                    (lookup
+                                        (real_name
+                                        ,interpolate_lookup_kind::partial
+                                        ).c_str()
+                                    ,lookup
+                                    ,out
+                                    ,sections
+                                    ,real_name
+                                    ,recursion_level + 1
+                                    );
+                                }
+                            break;
+
+                        case '!':
+                            // This is a comment, we just ignore it completely.
+                            break;
+
+                        default:
+                            if(is_active())
+                                {
+                                // We don't check here if name is not empty, as
+                                // there is no real reason to do it. Empty
+                                // variable name may seem strange, but why not
+                                // allow using "{{}}" to insert something into
+                                // the interpolated string, after all?
+                                out += lookup
+                                    (name
+                                    ,interpolate_lookup_kind::variable
+                                    );
+                                }
+                        }
+
+                    // We consume two characters here ("}}"), not one, as in a
+                    // usual loop iteration.
+                    ++p;
+                    break;
+                    }
+
+                if(p[0] == '{' && p[1] == '{')
+                    {
+                    // We don't allow nested interpolations, so this can only
+                    // be result of an error, e.g. a forgotten "}}" somewhere.
+                    alarum()
+                        << "Unexpected nested interpolation at position "
+                        << pos_start
+                        << " (outer interpolation starts at "
+                        << (p - s - 1 - name.length())
+                        << ")"
+                        << std::flush
+                        ;
+                    }
+
+                // We don't impose any restrictions on the kind of characters
+                // that can occur in the variable names neither because there
+                // just doesn't seem to be anything to gain from it.
+                name += *p;
+                }
+            }
+        else if(is_active())
+            {
+            out += *p;
+            }
+        }
+}
+
+} // Unnamed namespace.
+
+std::string interpolate_string
+    (char const* s
+    ,lookup_function const& lookup
+    )
+{
+    std::string out;
+
+    // This is probably not going to be enough as replacements of the
+    // interpolated variables tend to be longer than the variables names
+    // themselves, but it's difficult to estimate the resulting string length
+    // any better than this.
+    out.reserve(strlen(s));
+
+    // The stack contains all the sections that we're currently in.
+    std::stack<section_info, std::vector<section_info>> sections;
+
+    do_interpolate_string_in_context(s, lookup, out, sections);
+
+    if(!sections.empty())
+        {
+        alarum()
+            << "Unclosed section '"
+            << sections.top().name_
+            << "'"
+            << std::flush
+            ;
+        }
+
+    return out;
+}
diff --git a/interpolate_string.hpp b/interpolate_string.hpp
new file mode 100644
index 0000000..7515553
--- /dev/null
+++ b/interpolate_string.hpp
@@ -0,0 +1,69 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef interpolate_string_hpp
+#define interpolate_string_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <functional>
+#include <string>
+
+enum class interpolate_lookup_kind
+{
+    variable,
+    section,
+    partial
+};
+
+using lookup_function
+    = std::function<std::string (std::string const&, interpolate_lookup_kind)>;
+
+/// Interpolate string containing embedded variable references.
+///
+/// Return the input string after replacing all {{variable}} references in it
+/// with the value of the variable as returned by the provided function. The
+/// syntax is a (strict) subset of Mustache templates, the following features
+/// are supported:
+///  - Simple variable expansion for {{variable}}.
+///  - Conditional expansion using {{#variable}}...{{/variable}}.
+///  - Negated checks of the form {{^variable}}...{{/variable}}.
+///  - Partials support, i.e. {{>filename}}.
+///
+/// The following features are explicitly _not_ supported:
+///  - HTML escaping: this is done by a separate html::text class.
+///  - Separate types: 0/1 is false/true, anything else is an error.
+///  - Lists/section iteration (not needed yet).
+///  - Lambdas, comments, delimiter changes: omitted for simplicity.
+///
+/// To allow embedding literal "{{" fragment into the returned string, create a
+/// pseudo-variable expanding to these characters as its expansion, there is no
+/// built-in way to escape them.
+///
+/// Throw if the lookup function throws or if the string uses invalid syntax.
+std::string LMI_SO interpolate_string
+    (char const* s
+    ,lookup_function const& lookup
+    );
+
+#endif // interpolate_string_hpp
diff --git a/interpolate_string_test.cpp b/interpolate_string_test.cpp
new file mode 100644
index 0000000..cccd486
--- /dev/null
+++ b/interpolate_string_test.cpp
@@ -0,0 +1,233 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "test_tools.hpp"
+
+int test_main(int, char*[])
+{
+    auto const test_interpolate = [](char const* s)
+        {
+        return interpolate_string
+            (s
+            ,[](std::string const& k, interpolate_lookup_kind) { return k; }
+            );
+        };
+
+    // Check that basic interpolation works.
+    BOOST_TEST_EQUAL( test_interpolate(""),               ""        );
+    BOOST_TEST_EQUAL( test_interpolate("literal"),        "literal" );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}"),        "foo"     );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{bar}}"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}{{bar}}"), "foobar"  );
+
+    // Comments should be just ignored.
+    BOOST_TEST_EQUAL( test_interpolate("{{! ignore me}}"), ""       );
+    BOOST_TEST_EQUAL( test_interpolate("{{! too}}{{x}}"),  "x"      );
+    BOOST_TEST_EQUAL( test_interpolate("{{x}}{{!also}}"),  "x"      );
+
+    // Sections.
+    auto const section_test = [](char const* str)
+        {
+        return interpolate_string
+            (str
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "var0") return "0";
+                if(s == "var1") return "1";
+                if(s == "var" ) return "" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL( section_test("x{{#var1}}y{{/var1}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{#var0}}y{{/var0}}z"),   "xz"     );
+    BOOST_TEST_EQUAL( section_test("x{{^var0}}y{{/var0}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{^var1}}y{{/var1}}z"),   "xz"     );
+
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var1}}c{{/var1}}d{{/var1}}e")
+        ,"abcde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"abde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{^var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+
+    // Partials.
+    auto const partial_test = [](char const* str)
+        {
+        return interpolate_string
+            (str
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "header")       return "[header with {{var}}]";
+                if(s == "footer")       return "[footer with {{var}}]";
+                if(s == "nested")       return "[header with {{>footer}}]";
+                if(s == "recursive")    return "{{>recursive}}";
+                if(s == "sec" )         return "1" ;
+                if(s == "var" )         return "variable" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}{{var}} in body{{>footer}}")
+        ,"[header with variable]variable in body[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{#sec}}{{>header}}{{/sec}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("only{{^sec}}{{>header}}{{/sec}}{{>footer}}")
+        ,"only[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>nested}}")
+        ,"[header with [footer with variable]]"
+        );
+
+    BOOST_TEST_THROW
+        (partial_test("{{>recursive}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Nesting level too deep")
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("no {{^sec}}{{>recursive}}{{/sec}} problem")
+        ,"no  problem"
+        );
+
+    // Some special cases.
+    BOOST_TEST_EQUAL
+        (interpolate_string
+            ("{{expanded}}"
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "expanded")
+                    {
+                    return "{{unexpanded}}";
+                    }
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            )
+        ,"{{unexpanded}}"
+        );
+
+    // Check that the kind of variable being expanded is correct.
+    BOOST_TEST_EQUAL
+        (interpolate_string
+            ("{{>test}}"
+             "{{#section1}}{{^section0}}{{variable}}{{/section0}}{{/section1}}"
+             ,[](std::string const& s, interpolate_lookup_kind kind) -> 
std::string
+                {
+                switch(kind)
+                    {
+                    case interpolate_lookup_kind::variable:
+                        return "value of " + s;
+
+                    case interpolate_lookup_kind::section:
+                        // Get rid of the "section" prefix.
+                        return s.substr(7);
+
+                    case interpolate_lookup_kind::partial:
+                        return s + " partial included\n";
+                    }
+
+                throw std::runtime_error("invalid lookup kind");
+                }
+            )
+        ,"test partial included\nvalue of variable"
+        );
+
+    // Should throw if the input syntax is invalid.
+    BOOST_TEST_THROW
+        (test_interpolate("{{x")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unmatched opening brace")
+        );
+    BOOST_TEST_THROW
+        (test_interpolate("{{x{{y}}}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected nested interpolation")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var1'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{^var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var0'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{/var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}{{/var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
+
+    // Or because the lookup function throws.
+    BOOST_TEST_THROW
+        (interpolate_string
+            ("{{x}}"
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            )
+            ,std::runtime_error
+        ,"no such variable 'x'"
+        );
+
+    return EXIT_SUCCESS;
+}
diff --git a/ledger.hpp b/ledger.hpp
index 32f7f48..0bd8821 100644
--- a/ledger.hpp
+++ b/ledger.hpp
@@ -24,6 +24,7 @@
 
 #include "config.hpp"
 
+#include "ledger_evaluator.hpp"
 #include "mc_enum_type_enums.hpp"
 #include "so_attributes.hpp"
 #include "xml_lmi.hpp"
@@ -102,6 +103,10 @@ class LMI_SO Ledger
     unsigned int CalculateCRC() const;
     void Spew(std::ostream& os) const;
 
+    ledger_evaluator make_evaluator() const;
+
+    // PDF !! Expunge the following six function declarations:
+
     void read (xml::element const&);
     void write(xml::element&) const;
     int                class_version() const;
diff --git a/ledger_evaluator.cpp b/ledger_evaluator.cpp
new file mode 100644
index 0000000..0a81492
--- /dev/null
+++ b/ledger_evaluator.cpp
@@ -0,0 +1,864 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_evaluator.hpp"
+
+#include "alert.hpp"
+#include "authenticity.hpp"
+#include "calendar_date.hpp"
+#include "configurable_settings.hpp"
+#include "contains.hpp"
+#include "global_settings.hpp"
+#include "handle_exceptions.hpp"
+#include "ledger.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_text_formats.hpp"      // ledger_format()
+#include "ledger_variant.hpp"
+#include "map_lookup.hpp"
+#include "mc_enum_aux.hpp"              // mc_e_vector_to_string_vector()
+#include "miscellany.hpp"               // each_equal(), lmi_array_size()
+#include "oecumenic_enumerations.hpp"
+#include "value_cast.hpp"
+#include "version.hpp"
+
+#include <algorithm>                    // transform()
+#include <functional>                   // minus
+#include <unordered_map>
+#include <utility>                      // pair
+
+namespace
+{
+int const n = 7;
+
+char const* char_p_suffixes[n] =
+    {"_Current"        // mce_run_gen_curr_sep_full
+    ,"_Guaranteed"     // mce_run_gen_guar_sep_full
+    ,"_Midpoint"       // mce_run_gen_mdpt_sep_full
+    ,"_CurrentZero"    // mce_run_gen_curr_sep_zero
+    ,"_GuaranteedZero" // mce_run_gen_guar_sep_zero
+    ,"_CurrentHalf"    // mce_run_gen_curr_sep_half
+    ,"_GuaranteedHalf" // mce_run_gen_guar_sep_half
+    };
+
+std::vector<std::string> const suffixes
+    (char_p_suffixes
+    ,char_p_suffixes + n
+    );
+
+typedef std::unordered_map<std::string, std::pair<int,oenum_format_style>> 
format_map_t;
+typedef std::unordered_map<std::string, std::string> title_map_t;
+
+// For all numbers (so-called 'scalars' and 'vectors', but not
+// 'strings') grabbed from all ledgers, look for a format. If one
+// is found, use it to turn the number into a string. If not, and
+// the field is named in unavailable(), then it's ignored. Otherwise,
+// format_exists() displays a warning and ignores the field (because
+// throwing an exception would cause only the first warning to be
+// displayed).
+//
+// Rationale: Silently falling back on some default format can't be
+// right, because it masks defects that should be fixed: no default
+// can be universally appropriate.
+//
+// For names formed as
+//   basename + '_' + suffix
+// only the basename is used as a map key. Lookups in the format map
+// are strict, as they must be, else one key like "A" would match
+// anything beginning with that letter.
+//
+// Some of the unavailable fields could easily be made available
+// someday; perhaps others should be eliminated from class Ledger.
+
+bool unavailable(std::string const& s)
+{
+    static std::string const a[] =
+        {"DateOfBirthJdn"        // used by group quotes
+        ,"EffDateJdn"            // used by group quotes
+        ,"ListBillDateJdn"       // probably not needed
+        ,"InforceAsOfDateJdn"    // probably not needed
+        ,"InitDacTaxRate"        // used by PrintRosterTabDelimited(); not 
cents
+        ,"InitPremTaxRate"       // used by PrintRosterTabDelimited(); not 
cents
+        ,"SubstdTable"           // probably not needed
+        ,"InitMlyPolFee"         // used by PrintRosterTabDelimited()
+        ,"InitTgtPremHiLoadRate" // used by PrintRosterTabDelimited(); not 
cents
+        };
+    static std::vector<std::string> const v(a, a + lmi_array_size(a));
+    return contains(v, s);
+}
+
+bool format_exists
+    (std::string  const& s
+    ,std::string  const& suffix
+    ,format_map_t const& m
+    )
+{
+    if(contains(m, s))
+        {
+        return true;
+        }
+    else if(unavailable(s))
+        {
+        return false;
+        }
+    else
+        {
+        warning() << "No format found for " << s << suffix << LMI_FLUSH;
+        return false;
+        }
+}
+
+} // Unnamed namespace.
+
+std::string ledger_evaluator::operator()(std::string const& scalar) const
+{
+    return map_lookup(scalars_, scalar);
+}
+
+std::string ledger_evaluator::operator()
+    (std::string const& vector
+    ,std::size_t index
+    ) const
+{
+    return map_lookup(vectors_, vector).at(index);
+}
+
+ledger_evaluator Ledger::make_evaluator() const
+{
+    title_map_t title_map;
+
+// Can't seem to get a literal &nbsp; into the output.
+
+// Original:   title_map["AttainedAge"                     ] = " 
&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; 
End of &#xA0;&#xA0;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;
 End of &&#xA0;&&#xA0;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
End of &nbsp;&nbsp;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
 End of &amp;nbsp;&amp;nbsp;Year Age";
+// No good:    title_map["AttainedAge"                     ] = "<![CDATA[ 
&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; 
End of &#xA0;&#xA0;Year Age]]>";
+// No good:    title_map["AttainedAge"                     ] = " ááááááááááááá 
End of ááYear Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 
End of &#160;&#160;Year Age";
+
+//  Here are the columns to be listed in the user interface
+//  as well as their corresponding titles.
+
+    // Current and guaranteed variants are generally given for columns
+    // that vary by basis. Some offer only a current variant because
+    // they are defined only on a current basis--experience-rating
+    // columns, e.g.
+
+    title_map["AVGenAcct_CurrentZero"           ] = "Curr 
Charges\nAccount\nValue\nGen Acct";
+    title_map["AVGenAcct_GuaranteedZero"        ] = "Guar 
Charges\nAccount\nValue\nGen Acct";
+    title_map["AVRelOnDeath_Current"            ] = 
"Account\nValue\nReleased\non Death";
+    title_map["AVSepAcct_CurrentZero"           ] = "Curr Charges\n0% 
Account\nValue\nSep Acct";
+    title_map["AVSepAcct_GuaranteedZero"        ] = "Guar Charges\n0% 
Account\nValue\nSep Acct";
+    title_map["AcctVal_Current"                 ] = "Curr Account\nValue";
+    title_map["AcctVal_CurrentZero"             ] = "Curr Charges\n0% 
Account\nValue";
+    title_map["AcctVal_Guaranteed"              ] = "Guar Account\nValue";
+    title_map["AcctVal_GuaranteedZero"          ] = "Guar Charges\n0% 
Account\nValue";
+    title_map["AddonCompOnAssets"               ] = "Additional\nComp 
on\nAssets";
+    title_map["AddonCompOnPremium"              ] = "Additional\nComp 
on\nPremium";
+    title_map["AddonMonthlyFee"                 ] = "Additional\nMonthly\nFee";
+    title_map["AnnGAIntRate_Current"            ] = "Curr Ann\nGen Acct\nInt 
Rate";
+    title_map["AnnGAIntRate_Guaranteed"         ] = "Guar Ann\nGen Acct\nInt 
Rate";
+    title_map["AnnHoneymoonValueRate_Current"   ] = "Curr 
Ann\nHoneymoon\nValue Rate";
+    title_map["AnnHoneymoonValueRate_Guaranteed"] = "Guar 
Ann\nHoneymoon\nValue Rate";
+    title_map["AnnPostHoneymoonRate_Current"    ] = "Curr 
Post\nHoneymoon\nRate";
+    title_map["AnnPostHoneymoonRate_Guaranteed" ] = "Guar 
Post\nHoneymoon\nRate";
+    title_map["AnnSAIntRate_Current"            ] = "Curr Ann\nSep Acct\nInt 
Rate";
+    title_map["AnnSAIntRate_Guaranteed"         ] = "Guar Ann\nSep Acct\nInt 
Rate";
+    title_map["AttainedAge"                     ] = "End of\nYear Age";
+    title_map["AvgDeathBft_Current"             ] = "Curr Avg\nDeath\nBenefit";
+    title_map["AvgDeathBft_Guaranteed"          ] = "Guar Avg\nDeath\nBenefit";
+    title_map["BaseDeathBft_Current"            ] = "Curr 
Base\nDeath\nBenefit";
+    title_map["BaseDeathBft_Guaranteed"         ] = "Guar 
Base\nDeath\nBenefit";
+    title_map["COICharge_Current"               ] = "Curr COI\nCharge";
+    title_map["COICharge_Guaranteed"            ] = "Guar COI\nCharge";
+    title_map["CSVNet_Current"                  ] = "Curr Net\nCash\nSurr 
Value";
+    title_map["CSVNet_CurrentZero"              ] = "Curr Charges\n0% Net 
Cash\nSurr Value";
+    title_map["CSVNet_Guaranteed"               ] = "Guar Net\nCash\nSurr 
Value";
+    title_map["CSVNet_GuaranteedZero"           ] = "Guar Charges\n0% Net 
Cash\nSurr Value";
+    title_map["CV7702_Current"                  ] = "Curr 7702\nCash Value";
+    title_map["CV7702_Guaranteed"               ] = "Guar 7702\nCash Value";
+    title_map["ClaimsPaid_Current"              ] = "Curr\nClaims\nPaid";
+    title_map["ClaimsPaid_Guaranteed"           ] = "Guar\nClaims\nPaid";
+    title_map["CorpTaxBracket"                  ] = "Corp Tax\nBracket";
+    title_map["CorridorFactor"                  ] = "Corridor\nFactor";
+    title_map["CurrMandE"                       ] = 
"Mortality\nand\nExpense\nCharge";
+    title_map["DBOpt"                           ] = "Death\nBenefit\nOption";
+    title_map["DacTaxLoad_Current"              ] = "Curr DAC\nTax\nLoad";
+    title_map["DacTaxLoad_Guaranteed"           ] = "Guar DAC\nTax\nLoad";
+    title_map["DacTaxRsv_Current"               ] = "Curr DAC\nTax\nReserve";
+    title_map["DacTaxRsv_Guaranteed"            ] = "Guar DAC\nTax\nReserve";
+    title_map["DeathProceedsPaid_Current"       ] = "Curr 
Death\nProceeds\nPaid";
+    title_map["DeathProceedsPaid_Guaranteed"    ] = "Guar 
Death\nProceeds\nPaid";
+    title_map["EOYDeathBft_Current"             ] = "Curr EOY\nDeath\nBenefit";
+    title_map["EOYDeathBft_Guaranteed"          ] = "Guar EOY\nDeath\nBenefit";
+    title_map["EeGrossPmt"                      ] = "EE Gross\nPayment";
+    title_map["EeModalMinimumPremium"           ] = "EE 
Modal\nMinimum\nPremium";
+    title_map["EeMode"                          ] = "EE\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'EeGrossPmt' 
above.
+    title_map["EePmt"                           ] = "EE\nPayment\nMode";
+    title_map["ErGrossPmt"                      ] = "ER Gross\nPayment";
+    title_map["ErModalMinimumPremium"           ] = "ER 
Modal\nMinimum\nPremium";
+    title_map["ErMode"                          ] = "ER\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'ErGrossPmt' 
above.
+    title_map["ErPmt"                           ] = "ER\nPayment\nMode";
+    title_map["ExpenseCharges_Current"          ] = "Curr\nExpense\nCharge";
+    title_map["ExpenseCharges_Guaranteed"       ] = "Guar\nExpense\nCharge";
+    title_map["ExperienceReserve_Current"       ] = 
"Experience\nRating\nReserve";
+    title_map["GptForceout"                     ] = "Forceout";
+    title_map["GrossIntCredited_Current"        ] = "Curr 
Gross\nInt\nCredited";
+    title_map["GrossIntCredited_Guaranteed"     ] = "Guar 
Gross\nInt\nCredited";
+    title_map["GrossPmt"                        ] = "Premium\nOutlay";
+    title_map["HoneymoonValueSpread"            ] = "Honeymoon\nValue\nSpread";
+    title_map["IndvTaxBracket"                  ] = "EE Tax\nBracket";
+    title_map["InforceLives"                    ] = "BOY\nLives\nInforce";
+    title_map["IrrCsv_Current"                  ] = "Curr IRR\non CSV";
+    title_map["IrrCsv_Guaranteed"               ] = "Guar IRR\non CSV";
+    title_map["IrrDb_Current"                   ] = "Curr IRR\non DB";
+    title_map["IrrDb_Guaranteed"                ] = "Guar IRR\non DB";
+    title_map["KFactor_Current"                 ] = "Experience\nRating\nK 
Factor";
+    title_map["LoanIntAccrued_Current"          ] = "Curr Loan\nInt\nAccrued";
+    title_map["LoanIntAccrued_Guaranteed"       ] = "Guar Loan\nInt\nAccrued";
+    title_map["MlyGAIntRate_Current"            ] = "Curr Monthly\nGen 
Acct\nInt Rate";
+    title_map["MlyGAIntRate_Guaranteed"         ] = "Guar Monthly\nGen 
Acct\nInt Rate";
+    title_map["MlyHoneymoonValueRate_Current"   ] = "Curr 
Monthly\nHoneymoon\nValue Rate";
+    title_map["MlyHoneymoonValueRate_Guaranteed"] = "Guar 
Monthly\nHoneymoon\nValue Rate";
+    title_map["MlyPostHoneymoonRate_Current"    ] = "Curr 
Monthly\nPost\nHoneymoon\nRate";
+    title_map["MlyPostHoneymoonRate_Guaranteed" ] = "Guar 
Monthly\nPost\nHoneymoon\nRate";
+    title_map["MlySAIntRate_Current"            ] = "Curr Monthly\nSep 
Acct\nInt Rate";
+    title_map["MlySAIntRate_Guaranteed"         ] = "Guar Monthly\nSep 
Acct\nInt Rate";
+    title_map["ModalMinimumPremium"             ] = "Modal\nMinimum\nPremium";
+    title_map["AnnualFlatExtra"                 ] = "Annual\nFlat\nExtra";
+//    title_map["NaarForceout"                    ] = "Forced\nWithdrawal\ndue 
to\nNAAR Limit";
+    title_map["NetCOICharge_Current"            ] = "Experience\nRating\nNet 
COI\nCharge";
+    title_map["NetClaims_Current"               ] = "Curr Net\nClaims";
+    title_map["NetClaims_Guaranteed"            ] = "Guar Net\nClaims";
+    title_map["NetIntCredited_Current"          ] = "Curr Net\nInt\nCredited";
+    title_map["NetIntCredited_Guaranteed"       ] = "Guar Net\nInt\nCredited";
+    title_map["NetPmt_Current"                  ] = "Curr Net\nPayment";
+    title_map["NetPmt_Guaranteed"               ] = "Guar Net\nPayment";
+    title_map["NetWD"                           ] = "Withdrawal";
+    title_map["NewCashLoan"                     ] = "Annual Loan";
+    title_map["Outlay"                          ] = "Net Outlay";
+    title_map["PartMortTableMult"               ] = 
"Partial\nMortality\nMuliplier";
+    title_map["PolicyFee_Current"               ] = "Curr\nPolicy\nFee";
+    title_map["PolicyFee_Guaranteed"            ] = "Guar\nPolicy\nFee";
+    title_map["PolicyYear"                      ] = "Policy\nYear";
+    title_map["PrefLoanBalance_Current"         ] = "Curr\nPreferred\nLoan 
Bal";
+    title_map["PrefLoanBalance_Guaranteed"      ] = "Guar\nPreferred\nLoan 
Bal";
+    title_map["PremTaxLoad_Current"             ] = "Curr\nPremium\nTax Load";
+    title_map["PremTaxLoad_Guaranteed"          ] = "Guar\nPremium\nTax Load";
+// Excluded because it's unimplemented:
+//    title_map["ProducerCompensation"            ] = "Producer\nCompensation";
+    title_map["ProjectedCoiCharge_Current"      ] = 
"Experience\nRating\nProjected\nCOI Charge";
+    title_map["RefundableSalesLoad"             ] = "Refundable\nSales\nLoad";
+    title_map["RiderCharges_Current"            ] = "Curr Rider\nCharges";
+    title_map["Salary"                          ] = "Salary";
+    title_map["SepAcctCharges_Current"          ] = "Curr Sep\nAcct\nCharges";
+    title_map["SepAcctCharges_Guaranteed"       ] = "Guar Sep\nAcct\nCharges";
+    title_map["SpecAmt"                         ] = "Specified\nAmount";
+    title_map["SpecAmtLoad_Current"             ] = "Curr Spec\nAmt Load";
+    title_map["SpecAmtLoad_Guaranteed"          ] = "Guar Spec\nAmt Load";
+    title_map["SurrChg_Current"                 ] = "Curr Surr\nCharge";
+    title_map["SurrChg_Guaranteed"              ] = "Guar Surr\nCharge";
+    title_map["TermPurchased_Current"           ] = "Curr 
Term\nAmt\nPurchased";
+    title_map["TermPurchased_Guaranteed"        ] = "Guar 
Term\nAmt\nPurchased";
+    title_map["TermSpecAmt"                     ] = "Term\nSpecified\nAmount";
+    title_map["TgtPrem"                         ] = "Target\nPremium";
+    title_map["TotalIMF"                        ] = "Total\nInvestment\nMgt 
Fee";
+    title_map["TotalLoanBalance_Current"        ] = "Curr 
Total\nLoan\nBalance";
+    title_map["TotalLoanBalance_Guaranteed"     ] = "Guar 
Total\nLoan\nBalance";
+
+    // TODO ?? Titles ought to be read from an external file that
+    // permits flexible customization. Compliance might require that
+    // 'AcctVal_Current' be called "Cash Value" for one policy form,
+    // and "Account Value" for another, in order to match the terms
+    // used in the contract exactly. Therefore, these titles probably
+    // belong in the product database, which permits variation by
+    // product--though it does not accommodate strings as this is
+    // written in 2006-07. DATABASE !! So consider adding them there
+    // when the database is revamped.
+
+// Here's my top-level analysis of the formatting specification.
+//
+// Formats
+//
+// F0: zero decimals
+// F1: zero decimals, commas
+// F2: two decimals, commas
+// F3: scaled by 100, zero decimals, with '%' at end:
+// F4: scaled by 100, two decimals, with '%' at end:
+//
+// Presumably all use commas as thousands-separators, so that
+// an IRR of 12345.67% would be formatted as "12,345.67%".
+//
+// So the differences are:
+//   'precision' (number of decimal places)
+//   percentage (scaled by 100, '%' at end) or not
+// and therefore F0 is equivalent to F1
+
+    std::pair<int,oenum_format_style> f1(0, oe_format_normal);
+    std::pair<int,oenum_format_style> f2(2, oe_format_normal);
+    std::pair<int,oenum_format_style> f3(0, oe_format_percentage);
+    std::pair<int,oenum_format_style> f4(2, oe_format_percentage);
+
+    format_map_t format_map;
+
+// > Special Formatting for Scalar Items
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage "0.00%"
+// >
+    format_map["GuarMaxMandE"                      ] = f4;
+    format_map["InitAnnGenAcctInt"                 ] = f4;
+    format_map["InitAnnLoanCredRate"               ] = f4;
+    format_map["InitAnnLoanDueRate"                ] = f4;
+    format_map["InitAnnSepAcctCurrGross0Rate"      ] = f4;
+    format_map["InitAnnSepAcctCurrGrossHalfRate"   ] = f4;
+    format_map["InitAnnSepAcctCurrNet0Rate"        ] = f4;
+    format_map["InitAnnSepAcctCurrNetHalfRate"     ] = f4;
+    format_map["InitAnnSepAcctGrossInt"            ] = f4;
+    format_map["InitAnnSepAcctGuarGross0Rate"      ] = f4;
+    format_map["InitAnnSepAcctGuarGrossHalfRate"   ] = f4;
+    format_map["InitAnnSepAcctGuarNet0Rate"        ] = f4;
+    format_map["InitAnnSepAcctGuarNetHalfRate"     ] = f4;
+    format_map["InitAnnSepAcctNetInt"              ] = f4;
+    format_map["PostHoneymoonSpread"               ] = f4;
+    format_map["Preferred"                         ] = f4;
+    format_map["PremTaxRate"                       ] = f4;
+
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+    format_map["SalesLoadRefundRate0"              ] = f3;
+    format_map["SalesLoadRefundRate1"              ] = f3;
+    format_map["GenAcctAllocationPercent"          ] = f3;
+    format_map["GenAcctAllocationComplementPercent"] = f3;
+
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places 
(#,###,###.00)
+// >
+    format_map["CurrentCoiMultiplier"              ] = f2;
+    format_map["EeListBillPremium"                 ] = f2;
+    format_map["ErListBillPremium"                 ] = f2;
+    format_map["GuarPrem"                          ] = f2;
+    format_map["InforceTaxBasis"                   ] = f2;
+    format_map["InforceUnloanedAV"                 ] = f2;
+    format_map["InitGLP"                           ] = f2;
+    format_map["InitGSP"                           ] = f2;
+    format_map["InitPrem"                          ] = f2;
+    format_map["InitSevenPayPrem"                  ] = f2;
+    format_map["InitTgtPrem"                       ] = f2;
+    format_map["InitMinPrem"                       ] = f2;
+    format_map["ListBillPremium"                   ] = f2;
+    format_map["ModalMinimumDumpin"                ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places 
(#,###,###)
+// >
+    format_map["Age"                               ] = f1;
+    format_map["AllowDbo3"                         ] = f1;
+    format_map["AvgFund"                           ] = f1;
+    format_map["ChildRiderAmount"                  ] = f1;
+    format_map["CustomFund"                        ] = f1;
+    format_map["Dumpin"                            ] = f1;
+    format_map["EndtAge"                           ] = f1;
+    format_map["External1035Amount"                ] = f1;
+    format_map["GenAcctAllocation"                 ] = f1;
+    format_map["GenderBlended"                     ] = f1;
+    format_map["GenderDistinct"                    ] = f1;
+    format_map["Has1035ExchCharge"                 ] = f1;
+    format_map["HasADD"                            ] = f1;
+    format_map["HasChildRider"                     ] = f1;
+    format_map["HasHoneymoon"                      ] = f1;
+    format_map["HasSalesLoadRefund"                ] = f1;
+    format_map["HasSpouseRider"                    ] = f1;
+    format_map["HasSupplSpecAmt"                   ] = f1;
+    format_map["HasTerm"                           ] = f1;
+    format_map["HasWP"                             ] = f1;
+    format_map["InforceIsMec"                      ] = f1;
+    format_map["InforceMonth"                      ] = f1;
+    format_map["InforceYear"                       ] = f1;
+    format_map["InitBaseSpecAmt"                   ] = f1;
+    format_map["InitTermSpecAmt"                   ] = f1;
+    format_map["InitTotalSA"                       ] = f1;
+    format_map["Internal1035Amount"                ] = f1;
+    format_map["IsInforce"                         ] = f1;
+    format_map["IsMec"                             ] = f1;
+    format_map["LapseMonth"                        ] = f1;
+    format_map["LapseYear"                         ] = f1;
+    format_map["MaxDuration"                       ] = f1;
+    format_map["MecMonth"                          ] = f1;
+    format_map["MecYear"                           ] = f1;
+    format_map["NoLapse"                           ] = f1;
+    format_map["NoLapseAlwaysActive"               ] = f1;
+    format_map["NoLapseMinAge"                     ] = f1;
+    format_map["NoLapseMinDur"                     ] = f1;
+    format_map["RetAge"                            ] = f1;
+    format_map["SmokerBlended"                     ] = f1;
+    format_map["SmokerDistinct"                    ] = f1;
+    format_map["SplitFundAllocation"               ] = f1;
+    format_map["SplitMinPrem"                      ] = f1;
+    format_map["SpouseIssueAge"                    ] = f1;
+    format_map["SupplementalReport"                ] = f1;
+    format_map["UseExperienceRating"               ] = f1;
+    format_map["GroupIndivSelection"               ] = f1;
+    format_map["UsePartialMort"                    ] = f1;
+
+// > Vector Formatting
+// >
+// > Here are the vectors enumerated
+// >
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+// >
+    format_map["CorridorFactor"                    ] = f3;
+    format_map["FundAllocations"                   ] = f3;
+    format_map["MaleProportion"                    ] = f3;
+    format_map["NonsmokerProportion"               ] = f3;
+    format_map["PartMortTableMult"                 ] = f3;
+
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage with two decimal places (##0.00%)
+// >
+    format_map["AnnGAIntRate"                      ] = f4;
+    format_map["AnnHoneymoonValueRate"             ] = f4;
+    format_map["AnnPostHoneymoonRate"              ] = f4;
+    format_map["AnnSAIntRate"                      ] = f4;
+    format_map["CashFlowIRR"                       ] = f4;
+    format_map["CorpTaxBracket"                    ] = f4;
+    format_map["CurrMandE"                         ] = f4;
+    format_map["HoneymoonValueSpread"              ] = f4;
+    format_map["IndvTaxBracket"                    ] = f4;
+    format_map["InforceHMVector"                   ] = f4;
+
+    format_map["IrrCsv_Current"                    ] = f4;
+    format_map["IrrCsv_CurrentZero"                ] = f4;
+    format_map["IrrCsv_Guaranteed"                 ] = f4;
+    format_map["IrrCsv_GuaranteedZero"             ] = f4;
+    format_map["IrrDb_Current"                     ] = f4;
+    format_map["IrrDb_CurrentZero"                 ] = f4;
+    format_map["IrrDb_Guaranteed"                  ] = f4;
+    format_map["IrrDb_GuaranteedZero"              ] = f4;
+
+    format_map["MlyGAIntRate"                      ] = f4;
+    format_map["MlyHoneymoonValueRate"             ] = f4;
+    format_map["MlyPostHoneymoonRate"              ] = f4;
+    format_map["MlySAIntRate"                      ] = f4;
+    format_map["TotalIMF"                          ] = f4;
+// >
+// F0: zero decimals
+// > Format as a number no thousand separator or decimal point (##0%)
+// >
+    format_map["AttainedAge"                       ] = f1;
+    format_map["Duration"                          ] = f1;
+    format_map["LapseYears"                        ] = f1;
+    format_map["PolicyYear"                        ] = f1;
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places 
(#,###,###.00)
+// >
+    format_map["AddonMonthlyFee"                   ] = f2;
+// TODO ?? The precision of 'InforceLives' and 'KFactor' is inadequate.
+// Is every other format OK?
+    format_map["InforceLives"                      ] = f2;
+    format_map["KFactor"                           ] = f2;
+    format_map["AnnualFlatExtra"                   ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places 
(#,###,##0)
+// >
+    format_map["AcctVal"                           ] = f1;
+    format_map["AccumulatedPremium"                ] = f1;
+    format_map["AddonCompOnAssets"                 ] = f1;
+    format_map["AddonCompOnPremium"                ] = f1;
+    format_map["AvgDeathBft"                       ] = f1;
+    format_map["AVGenAcct"                         ] = f1;
+    format_map["AVRelOnDeath"                      ] = f1;
+    format_map["AVSepAcct"                         ] = f1;
+    format_map["BaseDeathBft"                      ] = f1;
+    format_map["BOYAssets"                         ] = f1;
+    format_map["ClaimsPaid"                        ] = f1;
+    format_map["COICharge"                         ] = f1;
+    format_map["Composite"                         ] = f1;
+    format_map["CSVNet"                            ] = f1;
+    format_map["CV7702"                            ] = f1;
+    format_map["DacTaxLoad"                        ] = f1;
+    format_map["DacTaxRsv"                         ] = f1;
+    format_map["DeathProceedsPaid"                 ] = f1;
+    format_map["EeGrossPmt"                        ] = f1;
+    format_map["EeModalMinimumPremium"             ] = f1;
+//    format_map["EeMode"                            ] = f1; // Not numeric.
+    format_map["EePmt"                             ] = f1;
+    format_map["EOYDeathBft"                       ] = f1;
+    format_map["ErGrossPmt"                        ] = f1;
+    format_map["ErModalMinimumPremium"             ] = f1;
+//    format_map["ErMode"                            ] = f1; // Not numeric.
+    format_map["ErPmt"                             ] = f1;
+    format_map["ExpenseCharges"                    ] = f1;
+    format_map["ExperienceReserve"                 ] = f1;
+    format_map["FundNumbers"                       ] = f1;
+    format_map["GptForceout"                       ] = f1;
+    format_map["GrossIntCredited"                  ] = f1;
+    format_map["GrossPmt"                          ] = f1;
+    format_map["Loads"                             ] = f1;
+    format_map["LoanInt"                           ] = f1;
+    format_map["LoanIntAccrued"                    ] = f1;
+    format_map["ModalMinimumPremium"               ] = f1;
+    format_map["NaarForceout"                      ] = f1;
+    format_map["NetClaims"                         ] = f1;
+    format_map["NetCOICharge"                      ] = f1;
+    format_map["NetIntCredited"                    ] = f1;
+    format_map["NetPmt"                            ] = f1;
+    format_map["NetWD"                             ] = f1;
+    format_map["NewCashLoan"                       ] = f1;
+    format_map["Outlay"                            ] = f1;
+    format_map["PolicyFee"                         ] = f1;
+    format_map["PrefLoanBalance"                   ] = f1;
+    format_map["PremTaxLoad"                       ] = f1;
+    format_map["ProducerCompensation"              ] = f1;
+    format_map["ProjectedCoiCharge"                ] = f1;
+    format_map["RefundableSalesLoad"               ] = f1;
+    format_map["RiderCharges"                      ] = f1;
+    format_map["Salary"                            ] = f1;
+    format_map["SepAcctCharges"                    ] = f1;
+    format_map["SpecAmt"                           ] = f1;
+    format_map["SpecAmtLoad"                       ] = f1;
+    format_map["SpouseRiderAmount"                 ] = f1;
+    format_map["SurrChg"                           ] = f1;
+    format_map["TermPurchased"                     ] = f1;
+    format_map["TermSpecAmt"                       ] = f1;
+    format_map["TgtPrem"                           ] = f1;
+    format_map["TotalLoanBalance"                  ] = f1;
+
+    // This is a little tricky. We have some stuff that
+    // isn't in the maps inside the ledger classes. We're going to
+    // stuff it into a copy of the invariant-ledger class's data.
+    // To avoid copying, we'll use pointers to the data. Most of
+    // this stuff is invariant anyway, so that's a reasonable
+    // place to put it.
+    //
+    // First we make a copy of the invariant ledger:
+
+    double_vector_map   vectors = ledger_invariant_->AllVectors;
+    scalar_map          scalars = ledger_invariant_->AllScalars;
+    string_map          strings = ledger_invariant_->Strings;
+
+    // Now we add the stuff that wasn't in the invariant
+    // ledger's class's maps (indexable by name). Because we're
+    // working with maps of pointers, we need pointers here.
+    //
+    // The IRRs are the worst of all.
+
+    if(!ledger_invariant_->IsInforce)
+        {
+        ledger_invariant_->CalculateIrrs(*this);
+        }
+    vectors["IrrCsv_GuaranteedZero" ] = &ledger_invariant_->IrrCsvGuar0    ;
+    vectors["IrrDb_GuaranteedZero"  ] = &ledger_invariant_->IrrDbGuar0     ;
+    vectors["IrrCsv_CurrentZero"    ] = &ledger_invariant_->IrrCsvCurr0    ;
+    vectors["IrrDb_CurrentZero"     ] = &ledger_invariant_->IrrDbCurr0     ;
+    vectors["IrrCsv_Guaranteed"     ] = &ledger_invariant_->IrrCsvGuarInput;
+    vectors["IrrDb_Guaranteed"      ] = &ledger_invariant_->IrrDbGuarInput ;
+    vectors["IrrCsv_Current"        ] = &ledger_invariant_->IrrCsvCurrInput;
+    vectors["IrrDb_Current"         ] = &ledger_invariant_->IrrDbCurrInput ;
+
+// GetMaxLength() is max *composite* length.
+//    int max_length = GetMaxLength();
+    double MaxDuration = ledger_invariant_->EndtAge - ledger_invariant_->Age;
+    scalars["MaxDuration"] = &MaxDuration;
+    int max_duration = static_cast<int>(MaxDuration);
+
+    std::vector<double> PolicyYear;
+    std::vector<double> AttainedAge;
+
+    PolicyYear .resize(max_duration);
+    AttainedAge.resize(max_duration);
+
+    int issue_age = static_cast<int>(ledger_invariant_->Age);
+    for(int j = 0; j < max_duration; ++j)
+        {
+        PolicyYear[j]  = 1 + j;
+        AttainedAge[j] = 1 + j + issue_age;
+        }
+
+// TODO ?? An attained-age column is meaningless in a composite. So
+// are several others--notably those affected by partial mortaility.
+    vectors["AttainedAge"] = &AttainedAge;
+    vectors["PolicyYear" ] = &PolicyYear ;
+
+    std::vector<double> InitAnnLoanDueRate(max_duration);
+    std::fill
+        (InitAnnLoanDueRate.begin()
+        ,InitAnnLoanDueRate.end()
+        ,ledger_invariant_->GetInitAnnLoanDueRate()
+        );
+    vectors["InitAnnLoanDueRate"] = &InitAnnLoanDueRate;
+
+    vectors["InforceLives"] = &ledger_invariant_->InforceLives;
+
+    vectors["FundNumbers"    ] = &ledger_invariant_->FundNumbers    ;
+    vectors["FundAllocations"] = &ledger_invariant_->FundAllocations;
+
+    // The Ledger object should contain a basic minimal set of columns
+    // from which others may be derived. It must be kept small because
+    // its size imposes a practical limit on the number of lives that
+    // can be run as part of a single census.
+    //
+    // TODO ?? A really good design would give users the power to
+    // define and store their own derived-column definitions. For now,
+    // however, code changes are required, and this is as appropriate
+    // a place as any to make them.
+
+    LedgerInvariant const& Invar = GetLedgerInvariant();
+    LedgerVariant   const& Curr_ = GetCurrFull();
+    LedgerVariant   const& Guar_ = GetGuarFull();
+
+    std::vector<double> PremiumLoads(max_duration);
+    std::vector<double> AdminCharges(max_duration);
+    for(int j = 0; j < max_duration; ++j)
+        {
+        PremiumLoads[j] = Invar.GrossPmt[j] - Curr_.NetPmt[j];
+        AdminCharges[j] = Curr_.SpecAmtLoad[j] + Curr_.PolicyFee[j];
+        }
+
+    vectors   ["PremiumLoads"] = &PremiumLoads;
+    format_map["PremiumLoads"] = f1;
+    vectors   ["AdminCharges"] = &AdminCharges;
+    format_map["AdminCharges"] = f1;
+
+    // ET !! Easier to write as
+    //   std::vector<double> NetDeathBenefit =
+    //     Curr_.EOYDeathBft - Curr_.TotalLoanBalance;
+    std::vector<double> NetDeathBenefit(Curr_.EOYDeathBft);
+    std::transform
+        (NetDeathBenefit.begin()
+        ,NetDeathBenefit.end()
+        ,Curr_.TotalLoanBalance.begin()
+        ,NetDeathBenefit.begin()
+        ,std::minus<double>()
+        );
+    vectors   ["NetDeathBenefit"] = &NetDeathBenefit;
+    title_map ["NetDeathBenefit"] = "Net\nDeath\nBenefit";
+    format_map["NetDeathBenefit"] = f1;
+
+    std::vector<double> SupplDeathBft_Current   (Curr_.TermPurchased);
+    std::vector<double> SupplDeathBft_Guaranteed(Guar_.TermPurchased);
+    vectors   ["SupplDeathBft_Current"   ] = &SupplDeathBft_Current;
+    vectors   ["SupplDeathBft_Guaranteed"] = &SupplDeathBft_Guaranteed;
+    title_map ["SupplDeathBft_Current"   ] = "Curr Suppl\nDeath\nBenefit";
+    title_map ["SupplDeathBft_Guaranteed"] = "Guar Suppl\nDeath\nBenefit";
+    format_map["SupplDeathBft_Current"   ] = f1;
+    format_map["SupplDeathBft_Guaranteed"] = f1;
+
+    std::vector<double> SupplSpecAmt(Invar.TermSpecAmt);
+    vectors   ["SupplSpecAmt"            ] = &SupplSpecAmt;
+    title_map ["SupplSpecAmt"            ] = "Suppl\nSpecified\nAmount";
+    format_map["SupplSpecAmt"            ] = f1;
+
+    // [End of derived columns.]
+
+    double Composite = is_composite();
+    scalars["Composite"] = &Composite;
+
+    double NoLapse =
+            0 != ledger_invariant_->NoLapseMinDur
+        ||  0 != ledger_invariant_->NoLapseMinAge
+        ;
+    scalars["NoLapse"] = &NoLapse;
+
+    std::string LmiVersion(LMI_VERSION);
+    calendar_date prep_date;
+
+    // Skip authentication for non-interactive regression testing.
+    if(!global_settings::instance().regression_testing())
+        {
+        authenticate_system();
+        }
+    else
+        {
+        // For regression tests,
+        //   - use an invariant string as version
+        //   - use EffDate as date prepared
+        // in order to avoid gratuitous failures.
+        LmiVersion = "Regression testing";
+        
prep_date.julian_day_number(static_cast<int>(ledger_invariant_->EffDateJdn));
+        }
+
+    strings["LmiVersion"] = &LmiVersion;
+
+    std::string PrepYear  = value_cast<std::string>(prep_date.year());
+    std::string PrepMonth = month_name(prep_date.month());
+    std::string PrepDay   = value_cast<std::string>(prep_date.day());
+
+    strings["PrepYear" ] = &PrepYear;
+    strings["PrepMonth"] = &PrepMonth;
+    strings["PrepDay"  ] = &PrepDay;
+
+    double HasSalesLoadRefund =
+        !each_equal(ledger_invariant_->RefundableSalesLoad, 0.0);
+    double SalesLoadRefundRate0 = ledger_invariant_->RefundableSalesLoad[0];
+    double SalesLoadRefundRate1 = ledger_invariant_->RefundableSalesLoad[1];
+
+    scalars["HasSalesLoadRefund"  ] = &HasSalesLoadRefund  ;
+    scalars["SalesLoadRefundRate0"] = &SalesLoadRefundRate0;
+    scalars["SalesLoadRefundRate1"] = &SalesLoadRefundRate1;
+
+    double GenAcctAllocation           = ledger_invariant_->GenAcctAllocation;
+    double GenAcctAllocationComplement = 1. - GenAcctAllocation;
+
+    scalars["GenAcctAllocationPercent"          ] = &GenAcctAllocation;
+    scalars["GenAcctAllocationComplementPercent"] = 
&GenAcctAllocationComplement;
+
+    std::string ScaleUnit = ledger_invariant_->ScaleUnit();
+    strings["ScaleUnit"] = &ScaleUnit;
+
+    double InitTotalSA =
+            ledger_invariant_->InitBaseSpecAmt
+        +   ledger_invariant_->InitTermSpecAmt
+        ;
+    scalars["InitTotalSA"] = &InitTotalSA;
+
+    // Maps to hold the results of formatting numeric data.
+
+    std::unordered_map<std::string, std::string> stringscalars;
+    std::unordered_map<std::string, std::vector<std::string>> stringvectors;
+
+    stringvectors["FundNames"] = ledger_invariant_->FundNames;
+
+    // Map the data, formatting it as necessary.
+
+    // First we'll get the invariant stuff--the copy we made,
+    // along with all the stuff we plugged into it above.
+    {
+    std::string suffix = "";
+    for(auto const& j : scalars)
+        {
+        if(format_exists(j.first, suffix, format_map))
+            stringscalars[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+        }
+    for(auto const& j : strings)
+        {
+        stringscalars[j.first + suffix] = *j.second;
+        }
+    for(auto const& j : vectors)
+        {
+        if(format_exists(j.first, suffix, format_map))
+            stringvectors[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+        }
+    }
+
+//    stringscalars["GuarMaxMandE"] = ledger_format(*scalars["GuarMaxMandE"], 
2, true);
+//    stringvectors["CorridorFactor"] = 
ledger_format(*vectors["CorridorFactor"], 0, true);
+//    stringscalars["InitAnnGenAcctInt_Current"] = 
ledger_format(*scalars["InitAnnGenAcctInt_Current"], 0, true);
+
+    // That was the tricky part. Now it's all downhill.
+
+    for(auto const& i : ledger_map_->held())
+        {
+        std::string suffix = suffixes[i.first];
+        for(auto const& j : i.second.AllScalars)
+            {
+//            scalars[j.first + suffix] = j.second;
+            if(format_exists(j.first, suffix, format_map))
+                stringscalars[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+            }
+        for(auto const& j : i.second.Strings)
+            {
+            strings[j.first + suffix] = j.second;
+            }
+        for(auto const& j : i.second.AllVectors)
+            {
+//            vectors[j.first + suffix] = j.second;
+            if(format_exists(j.first, suffix, format_map))
+                stringvectors[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+            }
+        }
+
+    stringvectors["EeMode"] = 
mc_e_vector_to_string_vector(ledger_invariant_->EeMode);
+    stringvectors["ErMode"] = 
mc_e_vector_to_string_vector(ledger_invariant_->ErMode);
+    stringvectors["DBOpt"]  = 
mc_e_vector_to_string_vector(ledger_invariant_->DBOpt );
+
+// TODO ?? Here I copied some stuff from the ledger class files: the
+// parts that speak of odd members that aren't in those class's
+// maps. This may reveal incomplete or incorrect systems analysis.
+
+// Invariant
+//
+//    // Special-case vectors (not <double>, or different length than others).
+//    EeMode              .reserve(Length);
+//    ErMode              .reserve(Length);
+//    DBOpt               .reserve(Length);
+//
+//    std::vector<int>            FundNumbers; [not handled yet]
+//    std::vector<std::string>    FundNames;   [not handled yet]
+//    std::vector<int>            FundAllocs;  [not handled yet]
+//
+//    std::vector<double> InforceLives;
+//
+//    // Special-case strings.
+//    std::string     EffDate; [furnished as PrepYear, PrepMonth, PrepDay]
+//
+// Variant
+//
+// [None of these are stored, and I think none is wanted.]
+//
+//    // special cases
+//    int              Length;
+//    mcenum_gen_basis GenBasis_;
+//    mcenum_sep_basis SepBasis_;
+//    bool             FullyInitialized;   // I.e. by Init(BasicValues const* 
b)
+
+    if(ledger_invariant_->SupplementalReport)
+        {
+        std::vector<std::string> SupplementalReportColumns;
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn00);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn01);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn02);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn03);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn04);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn05);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn06);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn07);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn08);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn09);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn10);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn11);
+
+        // Eventually customize the report name.
+        stringscalars["SupplementalReportTitle"] = "Supplemental Report";
+
+        std::vector<std::string> SupplementalReportColumnsTitles;
+        
SupplementalReportColumnsTitles.reserve(SupplementalReportColumns.size());
+
+        for(auto const& j : SupplementalReportColumns)
+            {
+            SupplementalReportColumnsTitles.push_back(title_map[j]);
+            }
+
+        stringvectors["SupplementalReportColumnsNames"] = 
std::move(SupplementalReportColumns);
+        stringvectors["SupplementalReportColumnsTitles"] = 
std::move(SupplementalReportColumnsTitles);
+        }
+
+    return ledger_evaluator(std::move(stringscalars), 
std::move(stringvectors));
+}
diff --git a/ledger_evaluator.hpp b/ledger_evaluator.hpp
new file mode 100644
index 0000000..23bc7cc
--- /dev/null
+++ b/ledger_evaluator.hpp
@@ -0,0 +1,59 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_evaluator_hpp
+#define ledger_evaluator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <cstddef>                      // size_t
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+/// Class allowing to retrieve the string representation of any scalar or
+/// vector stored in a ledger.
+class LMI_SO ledger_evaluator
+{
+  public:
+    std::string operator()(std::string const& scalar) const;
+    std::string operator()(std::string const& vector, std::size_t index) const;
+
+  private:
+    using all_scalars = std::unordered_map<std::string,            std::string 
>;
+    using all_vectors = 
std::unordered_map<std::string,std::vector<std::string>>;
+
+    // Objects of this class can only be created by Ledger::make_evaluator().
+    ledger_evaluator(all_scalars&& scalars, all_vectors&& vectors)
+        :scalars_(scalars)
+        ,vectors_(vectors)
+    {
+    }
+
+    all_scalars const scalars_;
+    all_vectors const vectors_;
+
+    friend class Ledger;
+};
+
+#endif // ledger_evaluator_hpp
diff --git a/ledger_pdf.cpp b/ledger_pdf.cpp
new file mode 100644
index 0000000..d9703ec
--- /dev/null
+++ b/ledger_pdf.cpp
@@ -0,0 +1,56 @@
+// Ledger PDF generation.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_pdf.hpp"
+
+#include "configurable_settings.hpp"
+#include "global_settings.hpp" // PDF !! expunge
+#include "ledger.hpp"
+#include "ledger_pdf_generator.hpp"
+#include "ledger_xsl.hpp" // PDF !! expunge
+#include "path_utility.hpp"
+
+/// Write ledger as pdf.
+
+std::string write_ledger_as_pdf(Ledger const& ledger, fs::path const& filepath)
+{
+    // PDF !! Expunge this conditional block:
+    if(global_settings::instance().ash_nazg())
+        {
+        // Execute both the new and the old code so that their results
+        // may be compared.
+        write_ledger_as_pdf_via_xsl(ledger, filepath);
+        }
+
+    throw_if_interdicted(ledger);
+
+    fs::path print_dir(configurable_settings::instance().print_directory());
+    // PDF !! Either orthodox_filename() should be used here, or its
+    // use should be reconsidered everywhere else.
+    fs::path pdf_out_file = unique_filepath(print_dir / filepath, ".pdf");
+
+    auto const pdf = ledger_pdf_generator::create();
+    pdf->write(ledger, pdf_out_file);
+
+    return pdf_out_file.string();
+}
diff --git a/ledger_xsl.hpp b/ledger_pdf.hpp
similarity index 78%
copy from ledger_xsl.hpp
copy to ledger_pdf.hpp
index dd2af40..5158f54 100644
--- a/ledger_xsl.hpp
+++ b/ledger_pdf.hpp
@@ -1,6 +1,6 @@
-// Ledger xsl operations.
+// Ledger PDF generation.
 //
-// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017, 2018 Gregory W. Chicares.
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
 //
 // This program is free software; you can redistribute it and/or modify
 // it under the terms of the GNU General Public License version 2 as
@@ -19,8 +19,8 @@
 // email: <address@hidden>
 // snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
 
-#ifndef ledger_xsl_hpp
-#define ledger_xsl_hpp
+#ifndef ledger_pdf_hpp
+#define ledger_pdf_hpp
 
 #include "config.hpp"
 
@@ -32,7 +32,4 @@ class Ledger;
 
 std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
 
-fs::path xsl_filepath(Ledger const&);
-
-#endif // ledger_xsl_hpp
-
+#endif // ledger_pdf_hpp
diff --git a/ledger_xsl.hpp b/ledger_pdf_generator.cpp
similarity index 53%
copy from ledger_xsl.hpp
copy to ledger_pdf_generator.cpp
index dd2af40..54f6543 100644
--- a/ledger_xsl.hpp
+++ b/ledger_pdf_generator.cpp
@@ -1,6 +1,6 @@
-// Ledger xsl operations.
+// Generate PDF files with ledger data.
 //
-// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017, 2018 Gregory W. Chicares.
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
 //
 // This program is free software; you can redistribute it and/or modify
 // it under the terms of the GNU General Public License version 2 as
@@ -19,20 +19,28 @@
 // email: <address@hidden>
 // snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
 
-#ifndef ledger_xsl_hpp
-#define ledger_xsl_hpp
+#include "pchfile.hpp"
 
-#include "config.hpp"
+#include "ledger_pdf_generator.hpp"
 
-#include <boost/filesystem/path.hpp>
+#include "callback.hpp"
 
-#include <string>
+namespace
+{
+callback<ledger_pdf_generator::creator_type>
+    group_quote_pdf_generator_create_callback;
+} // Unnamed namespace.
 
-class Ledger;
+typedef ledger_pdf_generator::creator_type FunctionPointer;
+template<> FunctionPointer callback<FunctionPointer>::function_pointer_ = 
nullptr;
 
-std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
-
-fs::path xsl_filepath(Ledger const&);
-
-#endif // ledger_xsl_hpp
+bool ledger_pdf_generator::set_creator(creator_type f)
+{
+    group_quote_pdf_generator_create_callback.initialize(f);
+    return true;
+}
 
+std::shared_ptr<ledger_pdf_generator> ledger_pdf_generator::create()
+{
+    return group_quote_pdf_generator_create_callback()();
+}
diff --git a/ledger_pdf_generator.hpp b/ledger_pdf_generator.hpp
new file mode 100644
index 0000000..b047179
--- /dev/null
+++ b/ledger_pdf_generator.hpp
@@ -0,0 +1,63 @@
+// Generate PDF files with ledger data.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_pdf_generator_hpp
+#define ledger_pdf_generator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <boost/filesystem/path.hpp>
+
+#include <memory>                       // std::shared_ptr
+
+class Ledger;
+
+/// Abstract base class for generating PDFs with ledger data.
+///
+/// Although there is currently only a single concrete implementation of this
+/// abstract base class and no other implementations are planned, splitting the
+/// PDF generation functionality into an abstract base and the concrete derived
+/// class is still needed because the former is part of liblmi while the latter
+/// uses wxPdfDocument and other wx facilities and is only part of libskeleton.
+
+class LMI_SO ledger_pdf_generator
+{
+  public:
+    typedef std::shared_ptr<ledger_pdf_generator> (*creator_type)();
+
+    static bool set_creator(creator_type);
+    static std::shared_ptr<ledger_pdf_generator> create();
+
+    virtual ~ledger_pdf_generator() = default;
+
+    virtual void write(Ledger const& ledger, fs::path const& output) = 0;
+
+  protected:
+    ledger_pdf_generator() = default;
+
+  private:
+    ledger_pdf_generator(ledger_pdf_generator const&) = delete;
+    ledger_pdf_generator& operator=(ledger_pdf_generator const&) = delete;
+};
+
+#endif // ledger_pdf_generator_hpp
diff --git a/ledger_pdf_generator_wx.cpp b/ledger_pdf_generator_wx.cpp
new file mode 100644
index 0000000..cbaec4e
--- /dev/null
+++ b/ledger_pdf_generator_wx.cpp
@@ -0,0 +1,2940 @@
+// Generate PDF files with ledger data using wxPdfDocument library.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "ledger_pdf_generator.hpp"
+
+#include "alert.hpp"
+#include "assert_lmi.hpp"
+#include "authenticity.hpp"
+#include "bourn_cast.hpp"
+#include "calendar_date.hpp"
+#include "data_directory.hpp"           // AddDataDir()
+#include "force_linking.hpp"
+#include "html.hpp"
+#include "interpolate_string.hpp"
+#include "istream_to_string.hpp"
+#include "ledger.hpp"
+#include "ledger_evaluator.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_variant.hpp"
+#include "miscellany.hpp"               // lmi_tolower()
+#include "pdf_writer_wx.hpp"
+#include "version.hpp"
+#include "wx_table_generator.hpp"
+
+#include <wx/pdfdc.h>
+
+#include <wx/image.h>
+#include <wx/log.h>
+
+#include <wx/html/m_templ.h>
+
+#include <cstdint>                      // SIZE_MAX
+#include <fstream>
+#include <map>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+#include <type_traits>                  // std::conditional
+#include <vector>
+
+LMI_FORCE_LINKING_IN_SITU(ledger_pdf_generator_wx)
+
+namespace
+{
+
+// Colour used for lines and border in the generated illustrations.
+const wxColour HIGHLIGHT_COL(0x00, 0x2f, 0x6c);
+
+// This function is also provided in <boost/algorithm/string/predicate.hpp>,
+// but it's arguably not worth adding dependency on this Boost library just for
+// this function.
+inline
+bool starts_with(std::string const& s, char const* prefix)
+{
+    return s.compare(0, strlen(prefix), prefix) == 0;
+}
+
+// Helper enums identifying the possible {Guaranteed,Current}{Zero,}
+// combinations.
+enum class base
+    {guaranteed
+    ,current
+    };
+
+enum class interest_rate
+    {zero
+    ,non_zero
+    };
+
+// And functions to retrieve their string representation.
+std::string base_suffix(base guar_or_curr)
+{
+    switch(guar_or_curr)
+        {
+        case base::guaranteed: return "Guaranteed";
+        case base::current:    return "Current"   ;
+        }
+    throw "Unreachable--unknown base value";
+}
+
+std::string ir_suffix(interest_rate zero_or_not)
+{
+    switch(zero_or_not)
+        {
+        case interest_rate::zero:     return "Zero";
+        case interest_rate::non_zero: return ""    ;
+        }
+    throw "Unreachable--unknown interest_rate value";
+}
+
+// Helper class grouping functions for dealing with interpolating strings
+// containing variable references.
+class html_interpolator
+{
+  public:
+    // Ctor takes the object used to interpolate the variables not explicitly
+    // defined using add_variable().
+    explicit html_interpolator(ledger_evaluator&& evaluator)
+        :evaluator_(evaluator)
+    {
+    }
+
+    // This function is provided to be able to delegate to it in custom
+    // interpolation functions, but usually shouldn't be called directly, just
+    // use operator() below instead.
+    std::string interpolation_func
+        (std::string const& s
+        ,interpolate_lookup_kind kind
+        ) const
+    {
+        switch(kind)
+            {
+            case interpolate_lookup_kind::variable:
+            case interpolate_lookup_kind::section:
+                return expand_html(s).as_html();
+
+            case interpolate_lookup_kind::partial:
+                return load_partial_from_file(s);
+            }
+
+        throw std::runtime_error("invalid lookup kind");
+    }
+
+    // A method which can be used to interpolate an HTML string containing
+    // references to the variables defined for this illustration. The general
+    // syntax is the same as in the global interpolate_string() function, i.e.
+    // variables are of the form "{{name}}" and section of the form
+    // "{{#name}}..{{/name}}" or "{{^name}}..{{/name}}" are also allowed and
+    // their contents is included in the expansion if and only if the variable
+    // with the given name has value "1" for the former or "0" for the latter.
+    //
+    // The variable names recognized by this function are either those defined
+    // by ledger_evaluator, i.e. scalar and vector fields of the ledger, or any
+    // variables explicitly defined by add_variable() calls.
+    html::text operator()(char const* s) const
+    {
+        return html::text::from_html
+            (interpolate_string
+                (s
+                ,[this]
+                    (std::string const& str
+                    ,interpolate_lookup_kind kind
+                    )
+                    {
+                        return interpolation_func(str, kind);
+                    }
+                )
+            );
+    }
+
+    html::text operator()(std::string const& s) const
+    {
+        return (*this)(s.c_str());
+    }
+
+    // Add a variable, providing either its raw text or already escaped HTML
+    // representation. Boolean values are converted to strings "0" or "1" as
+    // expected.
+    void add_variable(std::string const& name, html::text const& value)
+    {
+        vars_[name] = value;
+    }
+
+    void add_variable(std::string const& name, std::string const& value)
+    {
+        add_variable(name, html::text::from(value));
+    }
+
+    void add_variable(std::string const& name, int value)
+    {
+        std::ostringstream oss;
+        oss << value;
+        add_variable(name, oss.str());
+    }
+
+    void add_variable(std::string const& name, bool value)
+    {
+        add_variable(name, std::string(value ? "1" : "0"));
+    }
+
+    // Detect, at compile-time, mistaken attempts to add floating point
+    // variables: all those are only available from ledger_evaluator as they
+    // must be formatted correctly.
+    void add_variable(std::string const& name, double value) = delete;
+
+    // Test a boolean variable: the value must be "0" or "1", which is mapped
+    // to false or true respectively. Anything else results in an exception.
+    bool test_variable(std::string const& name) const
+    {
+        auto const z = expand_html(name).as_html();
+        return
+              z == "1" ? true
+            : z == "0" ? false
+            : throw std::runtime_error
+                ("Variable '" + name + "' has non-boolean value '" + z + "'"
+                )
+            ;
+    }
+
+    // Return the value of a single scalar variable.
+    std::string evaluate(std::string const& name) const
+    {
+        return evaluator_(name);
+    }
+
+    // Return a single value of a vector variable.
+    std::string evaluate(std::string const& name, std::size_t index) const
+    {
+        return evaluator_(name, index);
+    }
+
+    // Interpolate the contents of the given external template.
+    //
+    // This is exactly the same as interpolating "{{>template_name}}" string
+    // but a bit more convenient to use and simpler to read.
+    html::text expand_template(std::string const& template_name) const
+    {
+        return (*this)("{{>" + template_name + "}}");
+    }
+
+  private:
+    // The expansion function used with interpolate_string().
+    html::text expand_html(std::string const& s) const
+    {
+        // Check our own variables first:
+        auto const it = vars_.find(s);
+        if(it != vars_.end())
+            {
+            return it->second;
+            }
+
+        // Then look in the ledger, either as a scalar or a vector depending on
+        // whether it has "[index]" part or not.
+        if(!s.empty() && *s.rbegin() == ']')
+            {
+            auto const open_pos = s.find('[');
+            if(open_pos == std::string::npos)
+                {
+                throw std::runtime_error
+                    ("Variable '" + s + "' doesn't have the expected '['"
+                    );
+                }
+
+            char* stop = nullptr;
+            auto const index = std::strtoul(s.c_str() + open_pos + 1, &stop, 
10);
+
+            // Conversion must have stopped at the closing bracket character
+            // and also check for overflow (notice that index == SIZE_MAX
+            // doesn't, in theory, need to indicate overflow, but in practice
+            // we're never going to have valid indices close to this number).
+            if(stop != s.c_str() + s.length() - 1 || index >= SIZE_MAX)
+                {
+                throw std::runtime_error
+                    ("Index of vector variable '" + s + "' is not a valid 
number"
+                    );
+                }
+
+            // Cast below is valid because of the check for overflow above.
+            return html::text::from
+                (evaluator_
+                    (s.substr(0, open_pos)
+                    ,static_cast<std::size_t>(index)
+                    )
+                );
+            }
+
+        return html::text::from(evaluator_(s));
+    }
+
+    std::string load_partial_from_file(std::string const& file) const
+    {
+        std::ifstream ifs(AddDataDir(file + ".mst"));
+        if(!ifs)
+            {
+            alarum()
+                << "Template file \""
+                << file
+                << ".mst\" not found."
+                << std::flush
+                ;
+            }
+        std::string partial;
+        istream_to_string(ifs, partial);
+        return partial;
+    }
+
+    // Object used for variables expansion.
+    ledger_evaluator const evaluator_;
+
+    // Variables defined for all pages of this illustration.
+    std::map<std::string, html::text> vars_;
+};
+
+// A slightly specialized table generator for the tables used in the
+// illustrations.
+class illustration_table_generator : public wx_table_generator
+{
+  public:
+    static int const rows_per_group = 5;
+
+    explicit illustration_table_generator(pdf_writer_wx& writer)
+        :wx_table_generator
+            (writer.dc()
+            ,writer.get_horz_margin()
+            ,writer.get_page_width()
+            )
+    {
+        use_condensed_style();
+        align_right();
+    }
+
+    // Return the amount of vertical space taken by separator lines in the
+    // table headers.
+    int get_separator_line_height() const
+    {
+        // This is completely arbitrary and chosen just because it seems to
+        // look well.
+        return row_height() / 2;
+    }
+};
+
+// A helper mix-in class for pages using tables which is also reused by the
+// custom wxHtmlCell showing a table.
+//
+// Derived classes must provide get_table_columns() and may also override
+// should_show_column() to hide some of these columns dynamically and then can
+// use create_table_generator() to obtain the generator object that can be used
+// to render a table with the specified columns.
+class using_illustration_table
+{
+  protected:
+    // Description of a single table column.
+    struct illustration_table_column
+    {
+        std::string const variable_name;
+        std::string const label;
+        std::string const widest_text;
+    };
+
+    using illustration_table_columns = std::vector<illustration_table_column>;
+
+    // Must be overridden to return the description of the table columns.
+    virtual illustration_table_columns const& get_table_columns() const = 0;
+
+    // May be overridden to return false if the given column shouldn't be shown
+    // for the specific ledger values (currently used to exclude individual
+    // columns from composite illustrations).
+    virtual bool should_show_column(Ledger const& ledger, int column) const
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(column);
+        return true;
+    }
+
+    // Useful helper for creating the table generator using the columns defined
+    // by the separate (and simpler to implement) get_table_columns() pure
+    // virtual method.
+    illustration_table_generator create_table_generator
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ) const
+    {
+        // Set the smaller font used for all tables before creating the table
+        // generator which uses the DC font for its measurements.
+        auto& dc = writer.dc();
+        auto font = dc.GetFont();
+        font.SetPointSize(9);
+        dc.SetFont(font);
+
+        illustration_table_generator table(writer);
+
+        // But set the highlight colour for drawing separator lines after
+        // creating it to override its default pen.
+        dc.SetPen(HIGHLIGHT_COL);
+
+        int column = 0;
+        for(auto const& i : get_table_columns())
+            {
+            std::string label;
+            if(should_show_column(ledger, column++))
+                {
+                label = i.label;
+                }
+            //else: Leave the label empty to avoid showing the column.
+
+            table.add_column(label, i.widest_text);
+            }
+
+        return table;
+    }
+};
+
+// Base class for our custom HTML cells providing a way to pass them
+// information about the PDF document being generated and the ledger used to
+// generate it.
+class html_cell_for_pdf_output : public wxHtmlCell
+{
+  public:
+    // Before using this class a pdf_context_setter object needs to be
+    // instantiated (and remain alive for as long as this class is used).
+    class pdf_context_setter
+    {
+      public:
+        // References passed to the ctor must have lifetime greater than that
+        // of this object itself.
+        explicit pdf_context_setter
+            (Ledger const& ledger
+            ,pdf_writer_wx& writer
+            ,html_interpolator const& interpolate_html
+            )
+        {
+            html_cell_for_pdf_output::pdf_context_for_html_output.set
+                (&ledger
+                ,&writer
+                ,&interpolate_html
+                );
+        }
+
+        ~pdf_context_setter()
+        {
+            html_cell_for_pdf_output::pdf_context_for_html_output.set
+                (nullptr
+                ,nullptr
+                ,nullptr
+                );
+        }
+    };
+
+  protected:
+    // This is ugly, but we have to use a global variable to make pdf_writer_wx
+    // and wxDC objects used by the main code accessible to this cell class,
+    // there is no way to pass them as parameters through wxHTML machinery.
+    //
+    // To at least make it a little bit safer to deal with this, the variable
+    // itself is private and a public pdf_context_setter class is provided to
+    // actually set it.
+    class pdf_context
+    {
+      public:
+        void set
+            (Ledger const* ledger
+            ,pdf_writer_wx* writer
+            ,html_interpolator const* interpolate_html
+            )
+        {
+            ledger_ = ledger;
+            writer_ = writer;
+            interpolate_html_ = interpolate_html;
+        }
+
+        Ledger const& ledger() const
+        {
+            LMI_ASSERT(ledger_);
+            return *ledger_;
+        }
+
+        pdf_writer_wx& writer() const
+        {
+            LMI_ASSERT(writer_);
+            return *writer_;
+        }
+
+        html_interpolator const& interpolate_html() const
+        {
+            LMI_ASSERT(interpolate_html_);
+            return *interpolate_html_;
+        }
+
+      private:
+        Ledger const* ledger_ = nullptr;
+        pdf_writer_wx* writer_ = nullptr;
+        html_interpolator const* interpolate_html_ = nullptr;
+    };
+
+    // Small helper to check that we're using the expected DC and, also, acting
+    // as a sink for the never used parameters of Draw().
+    void draw_check_precondition
+        (wxDC& dc
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        )
+    {
+        // The DC passed to this function is supposed to be the same as the one
+        // associated with the writer we will use for rendering, but check that
+        // this is really so in order to avoid unexpectedly drawing the table
+        // on something else.
+        LMI_ASSERT(&dc == &pdf_context_for_html_output.writer().dc());
+
+        // There is no need to optimize drawing by restricting it to the
+        // currently shown positions, we always render the cell entirely.
+        stifle_warning_for_unused_value(view_y1);
+        stifle_warning_for_unused_value(view_y2);
+
+        // We don't care about rendering state as we don't support interactive
+        // selection anyhow.
+        stifle_warning_for_unused_value(info);
+    }
+
+    static pdf_context pdf_context_for_html_output;
+
+    friend pdf_context_setter;
+};
+
+html_cell_for_pdf_output::pdf_context
+html_cell_for_pdf_output::pdf_context_for_html_output;
+
+// Define scaffolding for a custom HTML "scaled_image" tag which must be used
+// instead of the standard "a" in order to allow specifying the scaling factor
+// that we want to use for the image in the PDF. Unfortunately this can't be
+// achieved by simply using "width" and/or "height" attributes of the "a" tag
+// because their values can only be integers which is not precise enough to
+// avoid (slightly but noticeably) distorting the image due to the aspect ratio
+// being not quite right.
+
+class scaled_image_cell : public html_cell_for_pdf_output
+{
+  public:
+    scaled_image_cell
+        (wxImage const& image
+        ,wxString const& src
+        ,double scale_factor
+        )
+        :image_(image)
+        ,src_(src)
+        ,scale_factor_(scale_factor)
+    {
+        m_Width  = wxRound(image.GetWidth () / scale_factor);
+        m_Height = wxRound(image.GetHeight() / scale_factor);
+    }
+
+    // Override the base class method to actually render the image.
+    void Draw
+        (wxDC& dc
+        ,int x
+        ,int y
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        ) override
+    {
+        draw_check_precondition(dc, view_y1, view_y2, info);
+
+        auto& writer = pdf_context_for_html_output.writer();
+
+        x += m_PosX;
+
+        int pos_y = y + m_PosY;
+        writer.output_image(image_, src_.utf8_str(), scale_factor_, x, &pos_y);
+    }
+
+  private:
+    wxImage const image_;
+    wxString const src_;
+    double const scale_factor_;
+};
+
+TAG_HANDLER_BEGIN(scaled_image, "SCALED_IMAGE")
+    TAG_HANDLER_PROC(tag)
+    {
+        wxString src;
+        if (!tag.GetParamAsString("SRC", &src))
+            {
+            throw std::runtime_error
+                ("missing mandatory \"src\" attribute of \"scaled_image\" tag"
+                );
+            }
+
+        // The scale factor is optional.
+        double scale_factor = 1.;
+
+        // But if it is given, we currently specify its inverse in HTML just
+        // because it so happens that for the scale factors we use the inverse
+        // can be expressed exactly in decimal notation, while the factor
+        // itself can't. In principle, the converse could also happen and we
+        // might add support for "factor" attribute too in this case. Or we
+        // could use separate "numerator" and "denominator" attributes. But for
+        // now implement just the bare minimum of what we need.
+        wxString inv_factor_str;
+        if (tag.GetParamAsString("INV_FACTOR", &inv_factor_str))
+            {
+            double inv_factor = 0.;
+            if (!inv_factor_str.ToCDouble(&inv_factor) || inv_factor == 0.)
+                {
+                throw std::runtime_error
+                    ( "invalid value for \"inv_factor\" attribute of "
+                      "\"scaled_image\" tag: \""
+                    + inv_factor_str.ToStdString()
+                    + "\""
+                    );
+                }
+
+            scale_factor = 1./inv_factor;
+            }
+
+        wxImage image;
+        // Disable error logging, we'll simply ignore the tag if the image is
+        // not present.
+            {
+            wxLogNull noLog;
+            image.LoadFile(src);
+            }
+
+        if (image.IsOk())
+            {
+            m_WParser->GetContainer()->InsertCell
+                (new scaled_image_cell(image, src, scale_factor)
+                );
+            }
+
+        // This tag isn't supposed to have any inner contents, so return true
+        // to not even try parsing it.
+        return true;
+    }
+TAG_HANDLER_END(scaled_image)
+
+class pdf_illustration;
+
+// Base class for all logical illustration pages.
+//
+// A single logical page may result in multiple physical pages of output, e.g.
+// if it contains a table not fitting on one page, but mostly these page
+// objects correspond to a single physical page of the resulting illustration.
+class page
+{
+  public:
+    page() = default;
+
+    // Pages are not value-like objects, so prohibit copying them.
+    page(page const&) = delete;
+    page& operator=(page const&) = delete;
+
+    // Make base class dtor virtual.
+    virtual ~page() = default;
+
+    // Associate the illustration object using this page with it.
+    //
+    // This object is not passed as a ctor argument because it would be
+    // redundant, instead it is associated with the page when it's added to an
+    // illustration. This method is supposed to be called only once and only by
+    // pdf_illustration this page is being added to.
+    void illustration(pdf_illustration const& illustration)
+    {
+        LMI_ASSERT(!illustration_);
+
+        illustration_ = &illustration;
+    }
+
+    // Called before rendering any pages to prepare for doing this, e.g. by
+    // computing the number of pages needed.
+    //
+    // This method must not draw anything on the wxDC, it is provided only for
+    // measurement purposes.
+    virtual void pre_render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        )
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(writer);
+        stifle_warning_for_unused_value(interpolate_html);
+    }
+
+    // Render this page contents.
+    virtual void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) = 0;
+
+  protected:
+    // Helper method for rendering the contents of the given external template,
+    // which is expected to be found in the file with the provided name and
+    // ".mst" extension in the data directory.
+    //
+    // Return the height of the page contents.
+    int render_page_template
+        (std::string const& template_name
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        )
+    {
+        return writer.output_html
+            (writer.get_horz_margin()
+            ,writer.get_vert_margin()
+            ,writer.get_page_width()
+            ,interpolate_html.expand_template(template_name)
+            );
+    }
+
+    // The associated illustration, which will be non-null by the time our
+    // virtual methods such as pre_render() and render() are called.
+    pdf_illustration const* illustration_ = nullptr;
+};
+
+// Base class for the different kinds of illustrations.
+//
+// This object contains pages, added to it using its add() method, as well as
+// illustration-global data registered as variables with html_interpolator and
+// so available for the pages when expanding the external templates defining
+// their contents.
+class pdf_illustration : protected html_interpolator
+{
+  public:
+    pdf_illustration(Ledger const& ledger
+                    ,fs::path const& output
+                    )
+        :html_interpolator(ledger.make_evaluator())
+        ,writer_(output.string(), wxPORTRAIT, &html_font_sizes)
+        ,ledger_(ledger)
+    {
+        init_variables();
+    }
+
+    // Make base class dtor virtual.
+    virtual ~pdf_illustration() = default;
+
+    // Add a page.
+    //
+    // This is a template just in order to save on writing std::make_unique<>()
+    // in the code using it to make it slightly shorter.
+    template<typename T, typename... Args>
+    void add(Args&&... args)
+    {
+        auto page = std::make_unique<T>(std::forward<Args>(args)...);
+        page->illustration(*this);
+        pages_.emplace_back(std::move(page));
+    }
+
+    // Render all pages.
+    void render_all()
+    {
+        html_cell_for_pdf_output::pdf_context_setter
+            set_pdf_context(ledger_, writer_, *this);
+
+        for(auto const& page : pages_)
+            {
+            page->pre_render(ledger_, writer_, *this);
+            }
+
+        bool first = true;
+        for(auto const& page : pages_)
+            {
+            if(first)
+                {
+                // We shouldn't start a new page before the very first one.
+                first = false;
+                }
+            else
+                {
+                // Do start a new physical page before rendering all the
+                // subsequent pages (notice that a page is also free to call
+                // StartPage() from its render()).
+                writer_.dc().StartPage();
+                }
+
+            page->render(ledger_, writer_, *this);
+            }
+    }
+
+    // Methods to be implemented by the derived classes to indicate which
+    // templates should be used for the upper (above the separating line) and
+    // the lower parts of the footer. The upper template name may be empty if
+    // it is not used at all.
+    //
+    // Notice that the upper footer template name can be overridden at the page
+    // level, the methods here define the default for all illustration pages.
+    //
+    // These methods are used by the pages deriving from page_with_footer.
+    virtual std::string get_upper_footer_template_name() const = 0;
+    virtual std::string get_lower_footer_template_name() const = 0;
+
+  protected:
+    // Explicitly retrieve the base class.
+    html_interpolator const& get_interpolator() const {return *this;}
+
+    // Helper for abbreviating a string to at most the given length (in bytes).
+    static std::string abbreviate_if_necessary(std::string s, size_t len)
+    {
+        if(s.length() > len && len > 3)
+            {
+            s.replace(len - 3, std::string::npos, "...");
+            }
+
+        return s;
+    }
+
+    // Helper for creating abbreviated variables in the derived classes: such
+    // variables have the name based on the name of the original variable with
+    // "Abbrev" and "len" appended to it and their value is at most "len" bytes
+    // long.
+    void add_abbreviated_variable(std::string const& var, size_t len)
+    {
+        add_variable
+            (var + "Abbrev" + std::to_string(len)
+            ,abbreviate_if_necessary(evaluate(var), len)
+            );
+    }
+
+  private:
+    // Define variables that can be used when interpolating pages contents.
+    void init_variables()
+    {
+        // The variables defined here are used by all, or at least more than
+        // one, illustration kinds. Variables only used in the templates of a
+        // single illustration type should be defined in the corresponding
+        // derived pdf_illustration_xxx class instead.
+
+        add_variable
+            ("date_prepared"
+            , html::text::from(evaluate("PrepMonth"))
+            + html::text::nbsp()
+            + html::text::from(evaluate("PrepDay"))
+            + html::text::from(", ")
+            + html::text::from(evaluate("PrepYear"))
+            );
+
+        auto indent = html::text::nbsp();
+        add_variable("Space1", indent);
+
+        indent += indent;
+        add_variable("Space2", indent);
+
+        indent += indent;
+        add_variable("Space4", indent);
+
+        indent += indent;
+        add_variable("Space8", indent);
+
+        indent += indent;
+        add_variable("Space16", indent);
+
+        indent += indent;
+        add_variable("Space32", indent);
+
+        indent += indent;
+        add_variable("Space64", indent);
+
+        auto const& invar = ledger_.GetLedgerInvariant();
+
+        add_abbreviated_variable("CorpName", 60);
+        add_abbreviated_variable("Insured1", 30);
+
+        // Define the variables needed by contract_numbers template.
+        add_variable
+            ("HasMasterContract"
+            ,!invar.MasterContractNumber.empty()
+            );
+        add_variable
+            ("HasPolicyNumber"
+            ,!invar.ContractNumber.empty()
+            );
+
+        size_t const full_abbrev_length = 30;
+        add_abbreviated_variable("MasterContractNumber", full_abbrev_length);
+        add_abbreviated_variable("MasterContractNumber", full_abbrev_length / 
2);
+        add_abbreviated_variable("ContractNumber", full_abbrev_length);
+        add_abbreviated_variable("ContractNumber", full_abbrev_length / 2);
+
+        add_variable
+            ("HasComplianceTrackingNumber"
+            ,expand_template("imprimatur")
+                .as_html().find_first_not_of(" \n")
+                != std::string::npos
+            );
+
+        add_variable
+            ("HasScaleUnit"
+            ,!invar.ScaleUnit().empty()
+            );
+
+        add_variable
+            ("DefnLifeInsIsGPT"
+            ,invar.DefnLifeIns == "GPT"
+            );
+
+        add_variable
+            ("MecYearPlus1"
+            ,bourn_cast<int>(invar.MecYear) + 1
+            );
+
+        add_variable
+            ("UWTypeIsMedical"
+            ,invar.UWType == "Medical"
+            );
+
+        add_variable
+            ("UWClassIsRated"
+            ,invar.UWClass == "Rated"
+            );
+
+        auto const& policy_name = invar.PolicyLegalName;
+        add_variable
+            ("GroupCarveout"
+            ,policy_name == "Group Flexible Premium Adjustable Life Insurance 
Certificate"
+            );
+
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+        add_variable
+            ("StateIsCarolina"
+            ,state_abbrev == "NC" || state_abbrev == "SC"
+            );
+
+        add_variable
+            ("StateIsMaryland"
+            ,state_abbrev == "MD"
+            );
+    }
+
+    // This array stores the non-default font sizes that are used to make it
+    // simpler to replicate the existing illustrations.
+    static std::array<int, 7> const html_font_sizes;
+
+    // Writer object used for the page metrics and higher level functions.
+    pdf_writer_wx writer_;
+
+    // Source of the data.
+    Ledger const& ledger_;
+
+    // All the pages of this illustration.
+    std::vector<std::unique_ptr<page>> pages_;
+};
+
+std::array<int, 7> const pdf_illustration::html_font_sizes
+    {
+    { 8
+    , 9
+    ,10
+    ,12
+    ,14
+    ,18
+    ,20
+    }
+    };
+
+// Cover page used by several different illustration kinds.
+class cover_page : public page
+{
+  public:
+    void render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        int const height_contents = render_page_template
+            ("cover"
+            ,writer
+            ,interpolate_html
+            );
+
+        // There is no way to draw a border around the page contents in wxHTML
+        // currently, so do it manually.
+        auto& dc = writer.dc();
+
+        dc.SetPen(wxPen(HIGHLIGHT_COL, 2));
+        dc.SetBrush(*wxTRANSPARENT_BRUSH);
+
+        dc.DrawRectangle
+            (writer.get_horz_margin()
+            ,writer.get_vert_margin()
+            ,writer.get_page_width()
+            ,height_contents
+            );
+    }
+};
+
+// Base class for all pages with a footer.
+class page_with_footer : public page
+{
+  public:
+    // Override pre_render() to compute footer_top_ which is needed in the
+    // derived classes overridden get_extra_pages_needed().
+    void pre_render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        auto const frame_horz_margin = writer.get_horz_margin();
+        auto const frame_width       = writer.get_page_width();
+
+        // We implicitly assume here that get_footer_lower_html() result
+        // doesn't materially depend on the exact value of the page number as
+        // we don't know its definitive value here yet. In theory, this doesn't
+        // need to be true, e.g. we may later discover that 10 pages are needed
+        // instead of 9 and the extra digit might result in a line wrapping on
+        // a new line and this increasing the footer height, but in practice
+        // this doesn't risk happening and taking into account this possibility
+        // wouldn't be simple at all, so just ignore this possibility.
+        auto footer_height = writer.output_html
+            (frame_horz_margin
+            ,0
+            ,frame_width
+            ,get_footer_lower_html(interpolate_html)
+            ,e_output_measure_only
+            );
+
+        auto const& upper_template = get_upper_footer_template_name();
+        if(!upper_template.empty())
+            {
+            footer_height += writer.output_html
+                (frame_horz_margin
+                ,0
+                ,frame_width
+                ,interpolate_html.expand_template(upper_template)
+                ,e_output_measure_only
+                );
+
+            // Leave a gap between the upper part of the footer and the main
+            // page contents to separate them in absence of a separator line
+            // which delimits the lower part.
+            footer_height += writer.dc().GetCharHeight();
+            }
+
+        footer_top_ = writer.get_page_bottom() - footer_height;
+    }
+
+    void render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        auto const frame_horz_margin = writer.get_horz_margin();
+        auto const frame_width       = writer.get_page_width();
+
+        auto& dc = writer.dc();
+
+        auto y = footer_top_;
+
+        auto const& upper_template = get_upper_footer_template_name();
+        if(!upper_template.empty())
+            {
+            y += dc.GetCharHeight();
+
+            y += writer.output_html
+                (frame_horz_margin
+                ,y
+                ,frame_width
+                ,interpolate_html.expand_template(upper_template)
+                );
+            }
+
+        writer.output_html
+            (frame_horz_margin
+            ,y
+            ,frame_width
+            ,get_footer_lower_html(interpolate_html)
+            );
+
+        dc.SetPen(HIGHLIGHT_COL);
+        dc.DrawLine
+            (frame_horz_margin
+            ,y
+            ,frame_width + frame_horz_margin
+            ,y
+            );
+    }
+
+  protected:
+    // Helper for the derived pages to get the vertical position of the footer.
+    // Notice that it can only be used after calling our pre_render() method
+    // as this is where it is computed.
+    int get_footer_top() const
+    {
+        LMI_ASSERT(footer_top_ != 0);
+
+        return footer_top_;
+    }
+
+  private:
+    // Method to be overridden in the base class which should actually return
+    // the page number or equivalent string (e.g. "Appendix").
+    virtual std::string get_page_number() const = 0;
+
+    // This method forwards to the illustration by default, but can be
+    // overridden to define a page-specific footer if necessary.
+    virtual std::string get_upper_footer_template_name() const
+    {
+        return illustration_->get_upper_footer_template_name();
+    }
+
+    // This method uses get_page_number() and returns the HTML wrapping it
+    // and other fixed information appearing in the lower part of the footer.
+    html::text get_footer_lower_html(html_interpolator const& 
interpolate_html) const
+    {
+        auto const page_number_str = get_page_number();
+
+        auto const templ = illustration_->get_lower_footer_template_name();
+
+        // Use our own interpolation function to handle the special
+        // "page_number" variable that is replaced with the actual
+        // (possibly dynamic) page number.
+        return html::text::from_html
+            (interpolate_string
+                (("{{>" + templ + "}}").c_str()
+                ,[page_number_str, interpolate_html]
+                    (std::string const& s
+                    ,interpolate_lookup_kind kind
+                    ) -> std::string
+                    {
+                    if(s == "page_number")
+                        {
+                        return page_number_str;
+                        }
+
+                    return interpolate_html.interpolation_func(s, kind);
+                    }
+                )
+            );
+    }
+
+    int footer_top_ = 0;
+};
+
+// Base class for attachment pages.
+class attachment_page : public page_with_footer
+{
+  private:
+    std::string get_page_number() const override
+    {
+        return "Attachment";
+    }
+};
+
+// Base class for all pages showing the page number in the footer.
+//
+// In addition to actually providing page_with_footer with the correct string
+// to show in the footer, this class implicitly handles the page count by
+// incrementing it whenever a new object of this class is pre-rendered.
+class numbered_page : public page_with_footer
+{
+  public:
+    // Must be called before creating the first numbered page.
+    static void start_numbering()
+    {
+        last_page_number_ = 0;
+    }
+
+    numbered_page()
+    {
+        // This assert would fail if start_numbering() hadn't been called
+        // before creating a numbered page, as it should be.
+        LMI_ASSERT(last_page_number_ >= 0);
+    }
+
+    void pre_render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        page_with_footer::pre_render(ledger, writer, interpolate_html);
+
+        this_page_number_ = ++last_page_number_;
+
+        extra_pages_ = get_extra_pages_needed
+            (ledger
+            ,writer
+            ,interpolate_html
+            );
+
+        LMI_ASSERT(extra_pages_ >= 0);
+
+        last_page_number_ += extra_pages_;
+    }
+
+    ~numbered_page() override
+    {
+        // Check that next_page() was called the expected number of times.
+        // Unfortunately we can't use LMI_ASSERT() in the (noexcept) dtor, so
+        // use warning() instead.
+        if(extra_pages_)
+            {
+            warning()
+                << "Logic error: "
+                << extra_pages_
+                << " missing extra pages."
+                << LMI_FLUSH
+                ;
+            }
+    }
+
+  protected:
+    void next_page(pdf_writer_wx& writer)
+    {
+        // This method may only be called if we had reserved enough physical
+        // pages for this logical pages by overriding get_extra_pages_needed().
+        LMI_ASSERT(extra_pages_ > 0);
+
+        writer.dc().StartPage();
+
+        this_page_number_++;
+        extra_pages_--;
+    }
+
+  private:
+    // Derived classes may override this method if they may need more than one
+    // physical page to show their contents.
+    virtual int get_extra_pages_needed
+        (Ledger const&              ledger
+        ,pdf_writer_wx&             writer
+        ,html_interpolator const&   interpolate_html
+        ) const
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(writer);
+        stifle_warning_for_unused_value(interpolate_html);
+
+        return 0;
+    }
+
+    std::string get_page_number() const override
+    {
+        std::ostringstream oss;
+        oss << "Page " << this_page_number_ << " of " << last_page_number_;
+        return oss.str();
+    }
+
+    static int last_page_number_;
+    int        this_page_number_     = 0;
+    int        extra_pages_          = 0;
+};
+
+// Initial value is invalid, use start_numbering() to change it.
+int numbered_page::last_page_number_ = -1;
+
+// Simplest possible page which is entirely defined by its external template
+// whose name must be specified when constructing it.
+class standard_page : public numbered_page
+{
+  public:
+    // Accept only string literals as template names, there should be no need
+    // to use anything else.
+    template<int N>
+    explicit standard_page(char const (&page_template_name)[N])
+        :page_template_name_(page_template_name)
+    {
+    }
+
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_page::render(ledger, writer, interpolate_html);
+
+        render_page_template(page_template_name_, writer, interpolate_html);
+    }
+
+  private:
+    char const* const page_template_name_;
+};
+
+// Helper classes used to show the numeric summary table. The approach used
+// here is to define a custom HTML tag (<numeric_summary_table>) and use the
+// existing illustration_table_generator to replace it with the actual table
+// when rendering.
+//
+// Notice that we currently make the simplifying assumption that this table is
+// always short enough so that everything fits on the same page as it would be
+// much more complicated to handle page breaks in the table in the middle of a
+// page (page_with_tabular_report below handles them only for the table at the
+// bottom of the page, after all the other contents, and this is already more
+// complicated and can't be done with just a custom HTML tag as we do it here).
+
+// An HTML cell showing the contents of the numeric summary table.
+class numeric_summary_table_cell
+    :public html_cell_for_pdf_output
+    ,private using_illustration_table
+{
+  public:
+    numeric_summary_table_cell()
+    {
+        m_Height = render_or_measure(0, e_output_measure_only);
+    }
+
+    // Override the base class method to actually render the table.
+    void Draw
+        (wxDC& dc
+        ,int x
+        ,int y
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        ) override
+    {
+        draw_check_precondition(dc, view_y1, view_y2, info);
+
+        // We ignore the horizontal coordinate which is always 0 for this cell
+        // anyhow.
+        stifle_warning_for_unused_value(x);
+
+        render_or_measure(y + m_PosY, e_output_normal);
+    }
+
+  private:
+    enum
+        {column_policy_year
+        ,column_premium_outlay
+        ,column_guar_account_value
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_separator_guar_non_guar
+        ,column_mid_account_value
+        ,column_mid_cash_surr_value
+        ,column_mid_death_benefit
+        ,column_separator_mid_curr
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"             , "Policy\nYear"       ,       "999" }
+            ,{ "GrossPmt"               , "Premium\nOutlay"    ,   "999,999" }
+            ,{ "AcctVal_Guaranteed"     , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Guaranteed"      , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,         "-" }
+            ,{ "AcctVal_Midpoint"       , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Midpoint"        , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Midpoint"   , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,         "-" }
+            ,{ "AcctVal_Current"        , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Current"         , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Current"    , "Death\nBenefit"     , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    int render_or_measure(int pos_y, enum_output_mode output_mode)
+    {
+        auto const& ledger = pdf_context_for_html_output.ledger();
+        auto& writer = pdf_context_for_html_output.writer();
+
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        // Output multiple rows of headers.
+
+        // Make a copy because we want pos_y to be modified only once, not
+        // twice, by both output_super_header() calls.
+        auto y_copy = pos_y;
+        table.output_super_header
+            ("Guaranteed Values"
+            ,column_guar_account_value
+            ,column_separator_guar_non_guar
+            ,&y_copy
+            ,output_mode
+            );
+        table.output_super_header
+            ("Non-Guaranteed Values"
+            ,column_mid_account_value
+            ,column_max
+            ,&pos_y
+            ,output_mode
+            );
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar_account_value
+            ,column_separator_guar_non_guar
+            ,pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_mid_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        y_copy = pos_y;
+        table.output_super_header
+            ("Midpoint Values"
+            ,column_mid_account_value
+            ,column_separator_mid_curr
+            ,&y_copy
+            ,output_mode
+            );
+
+        table.output_super_header
+            ("Current Values"
+            ,column_curr_account_value
+            ,column_max
+            ,&pos_y
+            ,output_mode
+            );
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_mid_account_value
+            ,column_separator_mid_curr
+            ,pos_y
+            ,output_mode
+            );
+
+        table.output_horz_separator
+            (column_curr_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        table.output_header(&pos_y, output_mode);
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator(0, column_max, pos_y, output_mode);
+
+        // And now the table values themselves.
+        auto const& columns = get_table_columns();
+        std::vector<std::string> values(columns.size());
+
+        auto const& invar = ledger.GetLedgerInvariant();
+        auto const& interpolate_html = 
pdf_context_for_html_output.interpolate_html();
+
+        int const year_max = 
pdf_context_for_html_output.ledger().GetMaxLength();
+        int const age_last = 70;
+        std::array<int, 4> const summary_years =
+            {{5, 10, 20, age_last - bourn_cast<int>(invar.Age)}
+            };
+        for(auto const& year : summary_years)
+            {
+            // Skip row if it doesn't exist. For instance, if the issue
+            // age is 85 and the contract remains in force until age 100,
+            // then there is no twentieth duration and no age-70 row.
+            if(!(0 < year && year <= year_max))
+                {
+                continue;
+                }
+
+            // Last row, showing the values for "Age 70" normally, needs to be
+            // handled specially.
+            bool const is_last_row = &year == &summary_years.back();
+
+            // For composite ledgers, "Age" doesn't make sense and so this row
+            // should be just skipped for them.
+            if(is_last_row && ledger.is_composite())
+                {
+                continue;
+                }
+
+            switch(output_mode)
+                {
+                case e_output_measure_only:
+                    pos_y += table.row_height();
+                    break;
+
+                case e_output_normal:
+                    for(std::size_t col = 0; col < columns.size(); ++col)
+                        {
+                        std::string const variable_name = 
columns[col].variable_name;
+
+                        // According to regulations, we need to replace the
+                        // policy year in the last row with the age.
+                        if(col == column_policy_year)
+                            {
+                            if(is_last_row)
+                                {
+                                std::ostringstream oss;
+                                oss << "Age " << age_last;
+                                values[col] = oss.str();
+                                continue;
+                                }
+                            }
+
+                        // Special hack for the dummy columns whose value is 
always
+                        // empty as it's used only as separator.
+                        values[col] = variable_name.empty()
+                            ? std::string{}
+                            : interpolate_html.evaluate(variable_name, year - 
1)
+                            ;
+                        }
+
+                    table.output_row(&pos_y, values.data());
+                    break;
+                }
+            }
+
+        return pos_y;
+    }
+};
+
+// Custom tag which is replaced by the numeric summary table.
+TAG_HANDLER_BEGIN(numeric_summary_table, "NUMERIC_SUMMARY_TABLE")
+    TAG_HANDLER_PROC(tag)
+    {
+        // The tag argument would be useful if we defined any parameters for
+        // it, but currently we don't.
+        stifle_warning_for_unused_value(tag);
+
+        m_WParser->GetContainer()->InsertCell(new 
numeric_summary_table_cell());
+
+        // This tag isn't supposed to have any inner contents, so return true
+        // to not even try parsing it.
+        return true;
+    }
+TAG_HANDLER_END(numeric_summary_table)
+
+// In wxWidgets versions prior to 3.1.1, there is an extra semicolon at the end
+// of TAGS_MODULE_BEGIN() expansion resulting in a warning with -pedantic used
+// by lmi, so suppress this warning here (this could be removed once 3.1.1 is
+// required).
+wxGCC_WARNING_SUPPRESS(pedantic)
+
+TAGS_MODULE_BEGIN(lmi_illustration)
+    TAGS_MODULE_ADD(scaled_image)
+    TAGS_MODULE_ADD(numeric_summary_table)
+TAGS_MODULE_END(lmi_illustration)
+
+wxGCC_WARNING_RESTORE(pedantic)
+
+// Numeric summary page appears twice, once as a normal page and once as an
+// attachment, with the only difference being that the base class is different,
+// so make it a template to avoid duplicating the code.
+
+// Just a helper alias.
+template<bool is_attachment>
+using numbered_or_attachment_base = typename std::conditional
+    <is_attachment
+    ,attachment_page
+    ,numbered_page
+    >::type;
+
+template<bool is_attachment>
+class reg_numeric_summary_or_attachment_page
+    : public numbered_or_attachment_base<is_attachment>
+{
+  public:
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_or_attachment_base<is_attachment>::render
+            (ledger
+            ,writer
+            ,interpolate_html
+            );
+
+        this->render_page_template
+            ("reg_numeric_summary"
+            ,writer
+            ,interpolate_html
+            );
+    }
+};
+
+// Helper base class for pages showing a table displaying values for all
+// contract years after some fixed content.
+class page_with_tabular_report
+    :public numbered_page
+    ,protected using_illustration_table
+{
+  public:
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_page::render(ledger, writer, interpolate_html);
+
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        auto const& columns = get_table_columns();
+
+        // Just some cached values used inside the loop below.
+        auto const row_height = table.row_height();
+        auto const page_bottom = get_footer_top();
+        auto const rows_per_group = 
illustration_table_generator::rows_per_group;
+        std::vector<std::string> values(columns.size());
+
+        // The table may need several pages, loop over them.
+        int const year_max = ledger.GetMaxLength();
+        for(int year = 0; year < year_max; ++year)
+            {
+            int pos_y = render_or_measure_fixed_page_part
+                (table
+                ,writer
+                ,interpolate_html
+                ,e_output_normal
+                );
+
+            for(; year < year_max; ++year)
+                {
+                for(std::size_t col = 0; col < columns.size(); ++col)
+                    {
+                    std::string const variable_name = 
columns[col].variable_name;
+
+                    // Special hack for the dummy columns used in some reports,
+                    // whose value is always empty as it's used only as
+                    // separator.
+                    values[col] = variable_name.empty()
+                        ? std::string{}
+                        : interpolate_html.evaluate(variable_name, year)
+                        ;
+                    }
+
+                table.output_row(&pos_y, values.data());
+
+                if((year + 1) % rows_per_group == 0)
+                    {
+                    // We need a group break.
+                    pos_y += row_height;
+
+                    // And possibly a page break, which will be necessary if 
we don't
+                    // have enough space for another full group because we 
don't want
+                    // to have page breaks in the middle of a group.
+                    if(pos_y >= page_bottom - rows_per_group*row_height)
+                        {
+                        next_page(writer);
+                        numbered_page::render(ledger, writer, 
interpolate_html);
+                        break;
+                        }
+                    }
+                }
+            }
+    }
+
+  protected:
+    // Must be overridden to return the template containing the fixed page 
part.
+    virtual std::string get_fixed_page_contents_template_name() const = 0;
+
+    // May be overridden to render (only if output_mode is e_output_normal)
+    // the extra headers just above the regular table headers.
+    //
+    // If this function does anything, it must show the first super-header at
+    // pos_y and update it to account for the added lines. The base class
+    // version does nothing.
+    virtual void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const
+    {
+        stifle_warning_for_unused_value(table);
+        stifle_warning_for_unused_value(interpolate_html);
+        stifle_warning_for_unused_value(pos_y);
+        stifle_warning_for_unused_value(output_mode);
+    }
+
+  private:
+    // Render (only if output_mode is e_output_normal) the fixed page part and
+    // (in any case) return the vertical coordinate of its bottom, where the
+    // tabular report starts.
+    int render_or_measure_fixed_page_part
+        (illustration_table_generator&  table
+        ,pdf_writer_wx&                 writer
+        ,html_interpolator const&       interpolate_html
+        ,enum_output_mode               output_mode
+        ) const
+    {
+        int pos_y = writer.get_vert_margin();
+
+        pos_y += writer.output_html
+            (writer.get_horz_margin()
+            ,pos_y
+            ,writer.get_page_width()
+            ,interpolate_html.expand_template
+                (get_fixed_page_contents_template_name()
+                )
+            ,output_mode
+            );
+
+        render_or_measure_extra_headers
+            (table
+            ,interpolate_html
+            ,&pos_y
+            ,output_mode
+            );
+
+        table.output_header(&pos_y, output_mode);
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (0
+            ,table.columns_count()
+            ,pos_y
+            ,output_mode
+            );
+
+        return pos_y;
+    }
+
+    // Override the base class method as the table may overflow onto the next
+    // page(s).
+    int get_extra_pages_needed
+        (Ledger const&              ledger
+        ,pdf_writer_wx&             writer
+        ,html_interpolator const&   interpolate_html
+        ) const override
+    {
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        int const pos_y = render_or_measure_fixed_page_part
+            (table
+            ,writer
+            ,interpolate_html
+            ,e_output_measure_only
+            );
+
+        int const rows_per_page = (get_footer_top() - pos_y) / 
table.row_height();
+
+        int const rows_per_group = 
illustration_table_generator::rows_per_group;
+
+        if(rows_per_page < rows_per_group)
+            {
+            // We can't afford to continue in this case as we can never output
+            // the table as the template simply doesn't leave enough space for
+            // it on the page.
+            throw std::runtime_error("no space left for tabular report");
+            }
+
+        // Each group actually takes rows_per_group+1 rows because of the
+        // separator row between groups, hence the second +1, but there is no
+        // need for the separator after the last group, hence the first +1.
+        int const groups_per_page = (rows_per_page + 1) / (rows_per_group + 1);
+
+        // But we are actually interested in the number of years per page and
+        // not the number of groups.
+        int const years_per_page = groups_per_page * rows_per_group;
+
+        // Finally determine how many pages we need to show all the years.
+        return ledger.GetMaxLength() / years_per_page;
+    }
+};
+
+class reg_tabular_detail_page : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_guar_account_value
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_dummy_separator
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_tabular_details";
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        stifle_warning_for_unused_value(interpolate_html);
+
+        // Make a copy because we want the real pos_y to be modified only once,
+        // not twice, by both output_super_header() calls.
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            ("Guaranteed Values"
+            ,column_guar_account_value
+            ,column_dummy_separator
+            ,&pos_y_copy
+            ,output_mode
+            );
+        table.output_super_header
+            ("Non-Guaranteed Values"
+            ,column_curr_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar_account_value
+            ,column_dummy_separator
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_curr_account_value
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"             , "Policy\nYear"       ,       "999" }
+            ,{ "AttainedAge"            , "End of\nYear Age"   ,       "999" }
+            ,{ "GrossPmt"               , "Premium\nOutlay"    ,   "999,999" }
+            ,{ "AcctVal_Guaranteed"     , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Guaranteed"      , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,      "----" }
+            ,{ "AcctVal_Current"        , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Current"         , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Current"    , "Death\nBenefit"     , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+};
+
+class reg_tabular_detail2_page : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_ill_crediting_rate
+        ,column_selected_face_amount
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_tabular_details2";
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"          , "Policy\nYear"               ,         
"999" }
+            ,{ "AttainedAge"         , "End of\nYear Age"           ,         
"999" }
+            ,{ "AnnGAIntRate_Current", "Illustrated\nCrediting Rate",      
"99.99%" }
+            ,{ "SpecAmt"             , "Selected\nFace Amount"      , 
"999,000,000" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+};
+
+// Class for pages showing supplemental report after the fixed template
+// contents. It can be either used directly or further derived from, e.g. to
+// override some of its inherited virtual methods such as
+// get_upper_footer_template_name() as done below.
+class standard_supplemental_report : public page_with_tabular_report
+{
+  public:
+    explicit standard_supplemental_report
+        (html_interpolator const& interpolate_html
+        ,std::string       const& page_template
+        )
+        :columns_(build_columns(interpolate_html))
+        ,page_template_(page_template)
+    {
+    }
+
+  private:
+    illustration_table_columns const& get_table_columns() const override
+    {
+        return columns_;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return page_template_;
+    }
+
+    // Helper function used by the ctor to initialize the const columns_ field.
+    illustration_table_columns build_columns
+        (html_interpolator const& interpolate_html
+        )
+    {
+        constexpr std::size_t max_columns = 12;
+        std::string const empty_column_name("[none]");
+
+        illustration_table_columns columns;
+        for(std::size_t i = 0; i < max_columns; ++i)
+            {
+            auto name = 
interpolate_html.evaluate("SupplementalReportColumnsNames", i);
+            if(name != empty_column_name)
+                {
+                // We currently don't have the field width information for
+                // arbitrary fields, so use fixed width that should be
+                // sufficient for almost all of them.
+                columns.emplace_back
+                    (illustration_table_column
+                        {std::move(name)
+                        
,interpolate_html.evaluate("SupplementalReportColumnsTitles", i)
+                        ,"999,999"
+                        }
+                    );
+                }
+            }
+
+        return columns;
+    }
+
+    illustration_table_columns const columns_      ;
+    std::string                const page_template_;
+};
+
+class reg_supplemental_report : public standard_supplemental_report
+{
+  public:
+    explicit reg_supplemental_report(html_interpolator const& interpolate_html)
+        :standard_supplemental_report(interpolate_html, "reg_supp_report")
+    {
+    }
+
+  private:
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+};
+
+// Regular illustration.
+class pdf_illustration_regular : public pdf_illustration
+{
+  public:
+    pdf_illustration_regular(Ledger const& ledger
+                            ,fs::path const& output
+                            )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+        auto const& policy_name = invar.PolicyLegalName;
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+
+        // Define variables specific to this illustration which doesn't use the
+        // standard 60/30 lengths for whatever reason.
+        add_abbreviated_variable("CorpName", 50);
+        add_abbreviated_variable("Insured1", 50);
+
+        add_variable
+            ("ModifiedSinglePremium"
+            ,starts_with(policy_name, "Single") && state_abbrev == "MA"
+            );
+
+        add_variable
+            ("ModifiedSinglePremium0"
+            ,starts_with(policy_name, "Modified")
+            );
+
+        add_variable
+            ("ModifiedSinglePremiumOrModifiedSinglePremium0"
+            , test_variable("ModifiedSinglePremium")
+            ||test_variable("ModifiedSinglePremium0")
+            );
+
+        add_variable
+            ("SinglePremium"
+            ,starts_with(policy_name, "Single") || starts_with(policy_name, 
"Modified")
+            );
+
+        add_variable
+            ("GroupExperienceRating"
+            ,policy_name == "Group Flexible Premium Adjustable Life Insurance 
Policy"
+            );
+
+        // Variable representing the premium payment frequency with the
+        // appropriate indefinite article preceding it, e.g. "an annual"
+        // or "a monthly".
+        auto const mode0 = invar.InitErMode;
+        if(!mode0.empty())
+            {
+            auto const mode0_first = lmi_tolower(mode0[0]);
+            add_variable
+                ("ErModeLCWithArticle"
+                ,(strchr("aeiou", mode0_first) ? "an" : "a") + mode0.substr(1)
+                );
+            }
+
+        add_variable
+            ("HasProducerCity"
+            ,invar.ProducerCity != "0"
+            );
+
+        add_variable
+            ("HasInterestDisclaimer"
+            ,!invar.InterestDisclaimer.empty()
+            );
+
+        add_variable
+            ("HasGuarPrem"
+            ,invar.GuarPrem != 0
+            );
+
+        add_variable
+            ("StateIsIllinois"
+            ,state_abbrev == "IL"
+            );
+
+        add_variable
+            ("StateIsTexas"
+            ,state_abbrev == "TX"
+            );
+
+        add_variable
+            ("StateIsIllinoisOrTexas"
+            ,state_abbrev == "IL" || state_abbrev == "TX"
+            );
+
+        add_variable
+            ("UltimateInterestRate"
+            ,evaluate("AnnGAIntRate_Current", invar.InforceYear + 1)
+            );
+
+        auto const max_duration = invar.EndtAge - invar.Age;
+        auto const lapse_year_guaruanteed = ledger.GetGuarFull().LapseYear;
+        auto const lapse_year_midpoint = ledger.GetMdptFull().LapseYear;
+        auto const lapse_year_current = ledger.GetCurrFull().LapseYear;
+
+        add_variable
+            ("LapseYear_Guaranteed_LT_MaxDuration"
+            ,lapse_year_guaruanteed < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Guaranteed_Plus1"
+            ,bourn_cast<int>(lapse_year_guaruanteed) + 1
+            );
+
+        add_variable
+            ("LapseYear_Midpoint_LT_MaxDuration"
+            ,lapse_year_midpoint < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Midpoint_Plus1"
+            ,bourn_cast<int>(lapse_year_midpoint) + 1
+            );
+
+        add_variable
+            ("LapseYear_Current_LT_MaxDuration"
+            ,lapse_year_current < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Current_Plus1"
+            ,bourn_cast<int>(lapse_year_current) + 1
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<standard_page>("reg_narr_summary");
+        add<standard_page>("reg_narr_summary2");
+        add<standard_page>("reg_column_headings");
+        if(!invar.IsInforce)
+            {
+            add<reg_numeric_summary_or_attachment_page<false>>();
+            }
+        add<reg_tabular_detail_page>();
+        add<reg_tabular_detail2_page>();
+        if(invar.SupplementalReport)
+            {
+            add<reg_supplemental_report>(get_interpolator());
+            }
+        if(!invar.IsInforce)
+            {
+            add<reg_numeric_summary_or_attachment_page<true>>();
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+        { return {}; }
+    std::string get_lower_footer_template_name() const override
+        { return "reg_footer"; }
+};
+
+// Common base class for basic illustration pages using the same columns in
+// both NASD and private group placement illustrations.
+class page_with_basic_tabular_report : public page_with_tabular_report
+{
+  private:
+    // This method must be overridden to return the text of the super-header
+    // used for all pairs of "cash surrogate value" and "death benefit"
+    // columns. The return value is subject to HTML interpolation and so may
+    // contain {{variables}} and also can be multiline but, if so, it must have
+    // the same number of lines for all input arguments.
+    //
+    // The base and interest_rate arguments can be used to construct the full
+    // name of the variable appropriate for the current column pair, with the
+    // help of base_suffix() and ir_suffix() functions.
+    virtual std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const = 0;
+
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_guar0_cash_surr_value
+        ,column_guar0_death_benefit
+        ,column_separator_guar0_guar
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_separator_guar_curr0
+        ,column_curr0_cash_surr_value
+        ,column_curr0_death_benefit
+        ,column_separator_curr0_curr
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"     ,       "999" 
}
+            ,{ "AttainedAge"                , "End of\nYear Age" ,       "999" 
}
+            ,{ "GrossPmt"                   , "Premium\nOutlay"  ,   "999,999" 
}
+            ,{ "CSVNet_GuaranteedZero"      , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_Guaranteed"          , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_Guaranteed"     , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_CurrentZero"         , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_CurrentZero"    , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_Current"             , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_Current"        , "Death\nBenefit"   , "9,999,999" 
}
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        // Output the first super header row.
+
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            ("Using guaranteed charges"
+            ,column_guar0_cash_surr_value
+            ,column_separator_guar_curr0
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y = pos_y_copy;
+        table.output_super_header
+            ("Using current charges"
+            ,column_curr0_cash_surr_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar0_cash_surr_value
+            ,column_separator_guar_curr0
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_curr0_cash_surr_value
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+
+        // Output the second super header row which is composed of three
+        // physical lines.
+
+        // This function outputs all lines of a single header, corresponding to
+        // the "Guaranteed" or "Current", "Zero" or not, column and returns the
+        // vertical position below the header.
+        auto const output_two_column_super_header = [=,&table]
+            (base           guar_or_curr
+            ,interest_rate  zero_or_not
+            ,std::size_t    begin_column
+            ) -> int
+            {
+                std::size_t end_column = begin_column + 2;
+                LMI_ASSERT(end_column <= column_max);
+
+                auto y = *pos_y;
+
+                auto const header = get_two_column_header
+                    (guar_or_curr
+                    ,zero_or_not
+                    );
+                table.output_super_header
+                    (interpolate_html(header).as_html()
+                    ,begin_column
+                    ,end_column
+                    ,&y
+                    ,output_mode
+                    );
+
+                y += table.get_separator_line_height();
+                table.output_horz_separator
+                    (begin_column
+                    ,end_column
+                    ,y
+                    ,output_mode
+                    );
+
+                return y;
+            };
+
+        output_two_column_super_header
+            (base::guaranteed
+            ,interest_rate::zero
+            ,column_guar0_cash_surr_value
+            );
+
+        output_two_column_super_header
+            (base::guaranteed
+            ,interest_rate::non_zero
+            ,column_guar_cash_surr_value
+            );
+
+        output_two_column_super_header
+            (base::current
+            ,interest_rate::zero
+            ,column_curr0_cash_surr_value
+            );
+
+        *pos_y = output_two_column_super_header
+            (base::current
+            ,interest_rate::non_zero
+            ,column_curr_cash_surr_value
+            );
+    }
+};
+
+class nasd_basic : public page_with_basic_tabular_report
+{
+  private:
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_basic";
+    }
+
+    std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const override
+    {
+        std::ostringstream oss;
+        oss
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} "
+            << "Assumed Sep Acct\n"
+            << "Gross Rate* "
+            << "({{InitAnnSepAcctNetInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} net)\n"
+            << "{{InitAnnGenAcctInt_"
+            << base_suffix(guar_or_curr)
+            << "}} GPA rate"
+            ;
+        return oss.str();
+    }
+};
+
+class nasd_supplemental : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_er_gross_payment
+        ,column_ee_gross_payment
+        ,column_premium_outlay
+        ,column_admin_charge
+        ,column_premium_tax_load
+        ,column_dac_tax_load
+        ,column_er_min_premium
+        ,column_ee_min_premium
+        ,column_net_premium
+        ,column_cost_of_insurance_charges
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_supp";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"           , "Policy\nYear"               ,       
"999" }
+            ,{ "AttainedAge"          , "End of\nYear Age"           ,       
"999" }
+            ,{ "ErGrossPmt"           , "ER Gross\nPayment"          ,   
"999,999" }
+            ,{ "EeGrossPmt"           , "EE Gross\nPayment"          ,   
"999,999" }
+            ,{ "GrossPmt"             , "Premium\nOutlay"            ,   
"999,999" }
+            ,{ "PolicyFee_Current"    , "Admin\nCharge"              ,   
"999,999" }
+            ,{ "PremTaxLoad_Current"  , "Premium\nTax Load"          ,   
"999,999" }
+            ,{ "DacTaxLoad_Current"   , "DAC\nTax Load"              ,   
"999,999" }
+            ,{ "ErModalMinimumPremium", "ER Modal\nMinimum\nPremium" ,   
"999,999" }
+            ,{ "EeModalMinimumPremium", "EE Modal\nMinimum\nPremium" ,   
"999,999" }
+            ,{ "NetPmt_Current"       , "Net\nPremium"               ,   
"999,999" }
+            ,{ "COICharge_Current"    , "Cost of\nInsurance\nCharges",   
"999,999" }
+            ,{ "AcctVal_Current"      , "Current\nAccount\nValue"    ,   
"999,999" }
+            ,{ "CSVNet_Current"       , "Current\nCash Surr\nValue"  ,   
"999,999" }
+            ,{ "EOYDeathBft_Current"  , "Current\nDeath\nBenefit"    , 
"9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // The supplemental page in NASD illustrations exists in two versions:
+        // default one and one with split premiums. Hide columns that are not
+        // needed for the current illustration.
+        switch(column)
+            {
+            case column_end_of_year_age:
+                // This column doesn't make sense for composite ledgers.
+                return !ledger.is_composite();
+
+            case column_admin_charge:
+            case column_premium_tax_load:
+            case column_dac_tax_load:
+                // These columns only appear in non-split premiums case.
+                return invar.SplitMinPrem == 0.;
+
+            case column_er_gross_payment:
+            case column_ee_gross_payment:
+            case column_er_min_premium:
+            case column_ee_min_premium:
+                // While those only appear in split premiums case.
+                return invar.SplitMinPrem == 1.;
+
+            case column_policy_year:
+            case column_premium_outlay:
+            case column_net_premium:
+            case column_cost_of_insurance_charges:
+            case column_curr_account_value:
+            case column_curr_cash_surr_value:
+            case column_curr_death_benefit:
+            case column_max:
+                // These columns are common to both cases and never hidden.
+                break;
+            }
+
+        return true;
+    }
+};
+
+class nasd_assumption_detail : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_sep_acct_crediting_rate
+        ,column_gen_acct_crediting_rate
+        ,column_m_and_e
+        ,column_ee_payment_mode
+        ,column_er_payment_mode
+        ,column_assumed_loan_interest
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_assumption_detail";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"          , "Policy\nYear"                ,        
"999" }
+            ,{ "AttainedAge"         , "End of\nYear Age"            ,        
"999" }
+            ,{ "AnnSAIntRate_Current", "Sep Acct Net\nInv Rate"      ,     
"99.99%" }
+            ,{ "AnnGAIntRate_Current", "Gen Acct\nCurrent Rate"      ,     
"99.99%" }
+            ,{ "CurrMandE"           , "M&E"                         ,     
"99.99%" }
+            ,{ "EeMode"              , "Indiv\nPmt Mode"             , 
"Semiannual" }
+            ,{ "ErMode"              , "Corp\nPmt Mode"              , 
"Semiannual" }
+            ,{ "InitAnnLoanDueRate"  , "Assumed\nLoan Interest"      ,     
"99.99%" }
+            };
+
+        return columns;
+    }
+
+    // Notice that there is no need to override should_show_column() in this
+    // class as this page is not included in composite illustrations and hence
+    // all of its columns, including the "AttainedAge" one, are always shown.
+};
+
+// NASD illustration.
+class pdf_illustration_nasd : public pdf_illustration
+{
+  public:
+    pdf_illustration_nasd
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // Define variables specific to this illustration.
+        if(!invar.ContractName.empty())
+            {
+            std::string s = invar.ContractName;
+            for(auto& c : s)
+                {
+                c = lmi_tolower(c);
+                }
+            s[0] = lmi_toupper(s[0]);
+
+            add_variable("ContractNameCap", s);
+            }
+
+        add_variable
+            ("UWTypeIsGuaranteedIssueInTexasWithFootnote"
+            ,invar.UWType == "Guaranteed issue"
+            );
+
+        add_variable
+            ("HasTermOrSupplSpecAmt"
+            ,test_variable("HasTerm") || test_variable("HasSupplSpecAmt")
+            );
+
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+        add_variable
+            ("StateIsNewYork"
+            ,state_abbrev == "NY"
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<nasd_basic>();
+        add<nasd_supplemental>();
+        add<standard_page>("nasd_column_headings");
+        add<standard_page>("nasd_notes1");
+        add<standard_page>("nasd_notes2");
+        if(!ledger.is_composite())
+            {
+            add<nasd_assumption_detail>();
+            }
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"nasd_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "nasd_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "nasd_footer_lower";
+    }
+};
+
+// Basic illustration page of the private group placement illustration.
+class reg_d_group_basic : public page_with_basic_tabular_report
+{
+  private:
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_group_basic";
+    }
+
+    std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const override
+    {
+        std::ostringstream oss;
+        oss
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} "
+            << "Hypothetical Gross\n"
+            << "Return ({{InitAnnSepAcctNetInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} net)"
+            ;
+        return oss.str();
+    }
+};
+
+// Private group placement illustration.
+class pdf_illustration_reg_d_group : public pdf_illustration
+{
+  public:
+    pdf_illustration_reg_d_group
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        // Define variables specific to this illustration.
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        add_variable
+            ("MecYearIs0"
+            ,invar.MecYear == 0
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<reg_d_group_basic>();
+        add<standard_page>("reg_d_group_column_headings");
+        add<standard_page>("reg_d_group_narr_summary");
+        add<standard_page>("reg_d_group_narr_summary2");
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"reg_d_group_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_d_group_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "reg_d_group_footer_lower";
+    }
+};
+
+// This page exists in two almost identical versions, one using guaranteed and
+// the other one using current values, use a base class to share the common
+// parts.
+class reg_d_individual_irr_base : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_zero_cash_surr_value
+        ,column_zero_death_benefit
+        ,column_zero_irr_surr_value
+        ,column_zero_irr_death_benefit
+        ,column_separator
+        ,column_nonzero_cash_surr_value
+        ,column_nonzero_death_benefit
+        ,column_nonzero_irr_surr_value
+        ,column_nonzero_irr_death_benefit
+        ,column_max
+        };
+
+    // Must be overridden to return the base being used.
+    virtual base get_base() const = 0;
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        std::ostringstream header_zero;
+        header_zero
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(get_base())
+            << ir_suffix(interest_rate::zero)
+            << "}} Hypothetical Rate of\n"
+            << "Return*"
+            ;
+
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            (interpolate_html(header_zero.str()).as_html()
+            ,column_zero_cash_surr_value
+            ,column_zero_irr_surr_value
+            ,pos_y
+            ,output_mode
+            );
+
+        std::ostringstream header_nonzero;
+        header_nonzero
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(get_base())
+            << ir_suffix(interest_rate::non_zero)
+            << "}} Hypothetical Rate of\n"
+            << "Return*"
+            ;
+
+        *pos_y = pos_y_copy;
+        table.output_super_header
+            (interpolate_html(header_nonzero.str()).as_html()
+            ,column_nonzero_cash_surr_value
+            ,column_nonzero_irr_surr_value
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_zero_cash_surr_value
+            ,column_zero_irr_surr_value
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_nonzero_cash_surr_value
+            ,column_nonzero_irr_surr_value
+            ,*pos_y
+            ,output_mode
+            );
+    }
+};
+
+class reg_d_individual_guar_irr : public reg_d_individual_irr_base
+{
+  private:
+    base get_base() const override
+    {
+        return base::guaranteed;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_guar_irr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"       ,       
"999" }
+            ,{ "AttainedAge"                , "End of\nYear Age"   ,       
"999" }
+            ,{ "GrossPmt"                   , "Premium\nOutlay"    ,   
"999,999" }
+            ,{ "CSVNet_GuaranteedZero"      , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_GuaranteedZero"      , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_GuaranteedZero"       , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            ,{ ""                           , " "                  ,         
"-" }
+            ,{ "CSVNet_Guaranteed"          , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_Guaranteed"     , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_Guaranteed"          , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_Guaranteed"           , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            };
+
+        return columns;
+    }
+};
+
+class reg_d_individual_curr_irr : public reg_d_individual_irr_base
+{
+  private:
+    base get_base() const override
+    {
+        return base::current;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_curr_irr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"       ,       
"999" }
+            ,{ "AttainedAge"                , "End of\nYear Age"   ,       
"999" }
+            ,{ "GrossPmt"                   , "Premium\nOutlay"    ,   
"999,999" }
+            ,{ "CSVNet_CurrentZero"         , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_CurrentZero"    , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_CurrentZero"         , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_CurrentZero"          , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            ,{ ""                           , " "                  ,         
"-" }
+            ,{ "CSVNet_Current"             , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_Current"        , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_Current"             , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_Current"              , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            };
+
+        return columns;
+    }
+};
+
+class reg_d_individual_curr : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_premium_loads
+        ,column_admin_charges
+        ,column_curr_mortality_charges
+        ,column_curr_asset_charges
+        ,column_curr_investment_income
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_curr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"              , "Policy\nYear"      ,       "999" }
+            ,{ "AttainedAge"             , "End of\nYear Age"  ,       "999" }
+            ,{ "GrossPmt"                , "Premium\nOutlay"   ,   "999,999" }
+            ,{ "PremiumLoads"            , "Premium\nLoads"    ,   "999,999" }
+            ,{ "AdminCharges"            , "Admin\nCharges"    ,   "999,999" }
+            ,{ "COICharge_Current"       , "Mortality\nCharges",   "999,999" }
+            ,{ "SepAcctCharges_Current"  , "Asset\nCharges"    ,   "999,999" }
+            ,{ "GrossIntCredited_Current", "Investment\nIncome",   "999,999" }
+            ,{ "AcctVal_Current"         , "Account\nValue"    ,   "999,999" }
+            ,{ "CSVNet_Current"          , "Cash\nSurr Value"  ,   "999,999" }
+            ,{ "EOYDeathBft_Current"     , "Death\nBenefit"    , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        table.output_super_header
+            (interpolate_html
+                ("{{InitAnnSepAcctGrossInt_Guaranteed}} Hypothetical Rate of 
Return*"
+                ).as_html()
+            ,column_curr_investment_income
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_curr_investment_income
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+    }
+};
+
+// Private individual placement illustration.
+class pdf_illustration_reg_d_individual : public pdf_illustration
+{
+  public:
+    pdf_illustration_reg_d_individual
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // Define variables specific to this illustration.
+        add_abbreviated_variable("CorpName", 140);
+        add_abbreviated_variable("Insured1", 140);
+
+        // Add all the pages.
+        numbered_page::start_numbering();
+        add<standard_page>("reg_d_indiv_cover_page");
+        add<reg_d_individual_guar_irr>();
+        add<reg_d_individual_curr_irr>();
+        add<reg_d_individual_curr>();
+        add<standard_page>("reg_d_indiv_notes1");
+        add<standard_page>("reg_d_indiv_notes2");
+        add<standard_page>("reg_d_indiv_notes3");
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"reg_d_indiv_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_d_indiv_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "reg_d_indiv_footer_lower";
+    }
+};
+
+class ledger_pdf_generator_wx : public ledger_pdf_generator
+{
+  public:
+    static std::shared_ptr<ledger_pdf_generator> do_create()
+        {
+        return std::make_shared<ledger_pdf_generator_wx>();
+        }
+
+    ledger_pdf_generator_wx() = default;
+    ledger_pdf_generator_wx(ledger_pdf_generator_wx const&) = delete;
+    ledger_pdf_generator_wx& operator=(ledger_pdf_generator_wx const&) = 
delete;
+
+    void write(Ledger const& ledger, fs::path const& output) override;
+
+  private:
+};
+
+void ledger_pdf_generator_wx::write
+    (Ledger const& ledger
+    ,fs::path const& output
+    )
+{
+    std::unique_ptr<pdf_illustration> pdf_ill;
+
+    auto const z = ledger.ledger_type();
+    switch(z)
+        {
+        case mce_ill_reg:
+            pdf_ill = std::make_unique<pdf_illustration_regular>(ledger, 
output);
+            break;
+        case mce_nasd:
+            pdf_ill = std::make_unique<pdf_illustration_nasd>(ledger, output);
+            break;
+        case mce_group_private_placement:
+            pdf_ill = std::make_unique<pdf_illustration_reg_d_group>(ledger, 
output);
+            break;
+        case mce_individual_private_placement:
+            pdf_ill = 
std::make_unique<pdf_illustration_reg_d_individual>(ledger, output);
+            break;
+        default:
+            alarum() << "Unknown ledger type '" << z << "'." << LMI_FLUSH;
+        }
+
+    pdf_ill->render_all();
+}
+
+volatile bool ensure_setup = ledger_pdf_generator::set_creator
+    (ledger_pdf_generator_wx::do_create
+    );
+
+} // Unnamed namespace.
diff --git a/ledger_xsl.cpp b/ledger_xsl.cpp
index 99a721e..bdbecb4 100644
--- a/ledger_xsl.cpp
+++ b/ledger_xsl.cpp
@@ -94,11 +94,13 @@ fs::path xsl_filepath(Ledger const& ledger)
 /// filenames must be transformed is that apache fop is java, and
 /// java is "portable".
 
-std::string write_ledger_as_pdf(Ledger const& ledger, fs::path const& filepath)
+std::string write_ledger_as_pdf_via_xsl(Ledger const& ledger, fs::path const& 
filepath)
 {
     throw_if_interdicted(ledger);
 
     fs::path print_dir(configurable_settings::instance().print_directory());
+    // Old implementation: write to a distinctive subdirectory.
+    print_dir = print_dir / "old";
 
     fs::path real_filepath(orthodox_filename(filepath.leaf()));
     LMI_ASSERT(fs::portable_name(real_filepath.string()));
diff --git a/ledger_xsl.hpp b/ledger_xsl.hpp
index dd2af40..6f99f31 100644
--- a/ledger_xsl.hpp
+++ b/ledger_xsl.hpp
@@ -30,7 +30,7 @@
 
 class Ledger;
 
-std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
+std::string write_ledger_as_pdf_via_xsl(Ledger const&, fs::path const&);
 
 fs::path xsl_filepath(Ledger const&);
 
diff --git a/main_wx.cpp b/main_wx.cpp
index 8a96920..6f9ad4a 100644
--- a/main_wx.cpp
+++ b/main_wx.cpp
@@ -46,6 +46,7 @@
 LMI_FORCE_LINKING_EX_SITU(alert_wx)
 LMI_FORCE_LINKING_EX_SITU(file_command_wx)
 LMI_FORCE_LINKING_EX_SITU(group_quote_pdf_generator_wx)
+LMI_FORCE_LINKING_EX_SITU(ledger_pdf_generator_wx)
 LMI_FORCE_LINKING_EX_SITU(progress_meter_wx)
 LMI_FORCE_LINKING_EX_SITU(system_command_wx)
 
diff --git a/main_wx_test.cpp b/main_wx_test.cpp
index 28b7211..64ec510 100644
--- a/main_wx_test.cpp
+++ b/main_wx_test.cpp
@@ -56,6 +56,7 @@
 
 LMI_FORCE_LINKING_EX_SITU(file_command_wx)
 LMI_FORCE_LINKING_EX_SITU(group_quote_pdf_generator_wx)
+LMI_FORCE_LINKING_EX_SITU(ledger_pdf_generator_wx)
 LMI_FORCE_LINKING_EX_SITU(progress_meter_wx)
 LMI_FORCE_LINKING_EX_SITU(system_command_wx)
 
diff --git a/nasd_assumption_detail.mst b/nasd_assumption_detail.mst
new file mode 100644
index 0000000..761b74b
--- /dev/null
+++ b/nasd_assumption_detail.mst
@@ -0,0 +1,29 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header_upper}}
+<p align="center">Illustration Assumption Detail</p>
+{{>nasd_header_lower}}
+
+<br></br>
+<br></br>
+
+{{! Illustration assumption detail table is created from C++ code }}
diff --git a/nasd_basic.mst b/nasd_basic.mst
new file mode 100644
index 0000000..a0c4122
--- /dev/null
+++ b/nasd_basic.mst
@@ -0,0 +1,35 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header_upper}}
+{{#IsInforce}}
+    <p align="center">
+    In Force Illustration
+    </p>
+{{/IsInforce}}
+{{>nasd_header_lower}}
+
+{{>dollar_units}}
+
+<br></br>
+<br></br>
+
+{{! Basic illustration table is created from C++ code }}
diff --git a/nasd_column_headings.mst b/nasd_column_headings.mst
new file mode 100644
index 0000000..71cc54a
--- /dev/null
+++ b/nasd_column_headings.mst
@@ -0,0 +1,96 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header}}
+
+<p align="center"><b>Column Definitions and Key Terms</b></p>
+
+<p>
+<b>Account Value</b>
+<br>
+{{AccountValueFootnote}}
+</p>
+
+<p>
+<b>Cash Surrender Value</b>
+<br>
+{{CashSurrValueFootnote}}
+</p>
+
+<p>
+<b>Death Benefit</b>
+<br>
+{{DeathBenefitFootnote}}
+</p>
+
+{{^Composite}}
+<p>
+<b>End of Year Age</b>
+<br>
+{{AttainedAgeFootnote}}
+</p>
+{{/Composite}}
+
+{{#GroupCarveout}}
+<p>
+<b>Gross Payment</b>
+<br>
+{{GrossPremiumFootnote}}
+</p>
+{{/GroupCarveout}}
+
+<p>
+<b>Gross Rate</b>
+<br>
+{{GrossRateFootnote}}
+</p>
+
+{{#GroupCarveout}}
+<p>
+<b>Minimum Premium</b>
+<br>
+{{InitialPremiumFootnote}}
+</p>
+{{/GroupCarveout}}
+
+<p>
+<b>Net Premium</b>
+<br>
+{{NetPremiumFootnote}}
+</p>
+
+<p>
+<b>Net Rate</b>
+<br>
+{{NetRateFootnote}}
+</p>
+
+<p>
+<b>Policy Year</b>
+<br>
+{{PolicyYearFootnote}}
+</p>
+
+<p>
+<b>Premium Outlay</b>
+<br>
+{{OutlayFootnote}}
+</p>
diff --git a/nasd_footer_lower.mst b/nasd_footer_lower.mst
new file mode 100644
index 0000000..f404919
--- /dev/null
+++ b/nasd_footer_lower.mst
@@ -0,0 +1,40 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<table width="100%" cellspacing="0" cellpadding="0">
+    <tr>
+        <td colspan="3">&nbsp;</td>
+    </tr>
+    <tr>
+        <td>{{InsCoName}}</td>
+        <td align="center">Date Prepared: {{date_prepared}}</td>
+        <td align="right">Policy Form: {{PolicyForm}}</td>
+    </tr>
+    <tr>
+        <td>{{InsCoAddr}}</td>
+        <td align="center">System Version: {{LmiVersion}}</td>
+        <td align="right">{{page_number}}</td>
+    </tr>
+</table>
+
+</font>
diff --git a/nasd_footer_upper.mst b/nasd_footer_upper.mst
new file mode 100644
index 0000000..e551e54
--- /dev/null
+++ b/nasd_footer_upper.mst
@@ -0,0 +1,34 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<p>
+* This illustration is based on the assumed Gross Rate shown.
+The Net Rate is provided for information purposes only.
+<br>
+** {{PremAllocationFootnote}}
+<br>
+See the Explanatory Notes for important {{ContractName}} information.
+This illustration is not complete without all pages.
+</p>
+
+</font>
diff --git a/nasd_header.mst b/nasd_header.mst
new file mode 100644
index 0000000..4af4e2b
--- /dev/null
+++ b/nasd_header.mst
@@ -0,0 +1,29 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{!
+    The header is split in two parts, upper and lower one, only to allow
+    inserting the title between them, as a couple of pages do. But we still
+    provide this "full header" template for the pages that don't have any
+    title to make them simpler.
+}}
+{{>nasd_header_upper}}
+{{>nasd_header_lower}}
diff --git a/nasd_header_lower.mst b/nasd_header_lower.mst
new file mode 100644
index 0000000..1e7c856
--- /dev/null
+++ b/nasd_header_lower.mst
@@ -0,0 +1,84 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<br>
+<br>
+
+<table width="100%" cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td>
+        {{#Composite}}
+            {{CorpNameAbbrev60}}
+        {{/Composite}}
+        {{^Composite}}
+            {{Insured1Abbrev30}}
+        {{/Composite}},
+        {{Gender}} {{Smoker}} rates, Age {{Age}}<br>
+        {{#Composite}}
+            Composite<br>
+        {{/Composite}}
+        {{ContractNameCap}}: {{PolicyMktgName}}<br>
+        {{#GroupCarveout}}
+            Minimum Initial Premium: ${{InitMinPrem}}<br>
+        {{/GroupCarveout}}
+        First Year Premium: ${{InitPrem}}<br>
+        {{^Composite}}
+            {{#UWTypeIsMedical}}
+              Fully underwritten,
+            {{/UWTypeIsMedical}}
+            {{^UWTypeIsMedical}}
+                {{#UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+                    Substandard ***,
+                {{/UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+                {{^UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+                    {{UWType}},
+                {{/UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+            {{/UWTypeIsMedical}}
+            {{UWClass}}, Initial Death Benefit Option: {{InitDBOpt}}<br>
+            {{#UWClassIsRated}}
+                Table Rating: {{SubstandardTable}}<br>
+            {{/UWClassIsRated}}
+        {{/Composite}}
+        {{>contract_numbers}}
+        </td>
+        <td>
+            Assumed Premium Allocation:**<br>
+            &nbsp;&nbsp;&nbsp;&nbsp;Separate Account: 
{{GenAcctAllocationComplementPercent}}<br>
+            &nbsp;&nbsp;&nbsp;&nbsp;Guaranteed Principal Account (GPA): 
{{GenAcctAllocationPercent}}<br>
+            Initial
+                {{#HasTermOrSupplSpecAmt}}
+                    Total
+                {{/HasTermOrSupplSpecAmt}}
+                Face Amount: ${{InitTotalSA}}<br>
+            {{#HasTermOrSupplSpecAmt}}
+                Initial Base Face Amount: ${{InitBaseSpecAmt}}<br>
+                Initial
+                    {{#HasTerm}}
+                        Term Face Amount:
+                    {{/HasTerm}}
+                    {{#HasSupplSpecAmt}}
+                        Supplemental Face Amount:
+                    {{/HasSupplSpecAmt}}
+                  ${{InitTermSpecAmt}}<br>
+            {{/HasTermOrSupplSpecAmt}}
+        </td>
+    </tr>
+</table>
diff --git a/nasd_header_upper.mst b/nasd_header_upper.mst
new file mode 100644
index 0000000..b7fd5ae
--- /dev/null
+++ b/nasd_header_upper.mst
@@ -0,0 +1,39 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<br></br>
+<br></br>
+
+<p>
+The purpose of this illustration is to show how the performance
+of the underlying investment account could affect the
+{{ContractName}} account value and death benefit.
+<b>
+These hypothetical returns do not reflect past performance
+and are not predictive of future results. Actual results could be
+less than or greater than the hypothetical results
+and in all likelihood will vary from year to year.
+</b>
+</p>
+
+<br></br>
diff --git a/nasd_notes1.mst b/nasd_notes1.mst
new file mode 100644
index 0000000..38c461c
--- /dev/null
+++ b/nasd_notes1.mst
@@ -0,0 +1,137 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header}}
+
+<font size="-1">
+
+<p align="center"><b>IMPORTANT TAX DISCLOSURE</b></p>
+
+<p>
+As illustrated, this {{ContractName}}
+{{#IsMec}}
+    becomes
+{{/IsMec}}
+{{^IsMec}}
+    would not become
+{{/IsMec}}
+a Modified Endowment Contract (MEC)
+under the Internal Revenue Code
+{{#IsMec}}
+    in year {{MecYearPlus1}}
+{{/IsMec}}
+To the extent of gain in the {{ContractName}}, loans, distributions
+and withdrawals from a MEC are subject to income tax
+and may also trigger a penalty tax.
+</p>
+
+{{^IsInforce}}
+    <p>
+    The initial 7-pay premium limit is ${{InitSevenPayPrem}}.
+    </p>
+{{/IsInforce}}
+
+<p>
+No tax charge is made to the Separate Account. However,
+such a charge may be made in the future.
+</p>
+
+<p>
+<b>
+The information contained in this illustration is not written
+or intended as tax or legal advice.
+Neither {{InsCoShortName}}
+nor any of its employees or representatives are authorized
+to give tax or legal advice. For more information pertaining
+to the tax consequences of purchasing or owning this
+{{ContractName}},
+consult with your own independent tax or legal counsel.
+</b>
+</p>
+
+<br></br>
+
+<p align="center"><b>EXPLANATORY NOTES</b></p>
+
+<p>
+{{PolicyMktgName}} is a {{PolicyLegalName}} issued by {{InsCoName}}.
+{{ProductDescription}}
+</p>
+
+<p>
+{{NoVanishPremiumFootnote}}
+</p>
+
+{{#NoLapse}}
+    {{^StateIsNewYork}}
+        <p>
+        {{NoLapseProvisionName}}:
+        The {{NoLapseProvisionName}} is a lapse protection feature. If met,
+        this test allows your {{ContractName}} to stay in force for a period of
+        time even if there is insufficient {{AvName}} Value to cover the
+        {{AvName}} Value charges. Refer to your {{ContractName}} for specific
+        requirements of meeting the {{NoLapseProvisionName}}.
+        </p>
+    {{/StateIsNewYork}}
+{{/NoLapse}}
+
+{{#NoLapseAlwaysActive}}
+    <p>
+    No-Lapse Guarantee: The {{ContractName}} will remain in force after the
+    first premium has been paid, even if there is insufficient {{AvName}} Value
+    to cover the monthly charges provided that the insured is not in a
+    substandard rating class and the {{ContractName}} debt does not exceed
+    {{AvName}} Value.
+    </p>
+{{/NoLapseAlwaysActive}}
+
+<p>
+The definition of life insurance elected for this
+{{ContractName}} is the
+{{#DefnLifeInsIsGPT}}
+    guideline premium test. The guideline single premium is
+    ${{InitGSP}} and the guideline
+    level premium is ${{InitGLP}}.
+{{/DefnLifeInsIsGPT}}
+{{^DefnLifeInsIsGPT}}
+    cash value accumulation test.
+{{/DefnLifeInsIsGPT}}
+</p>
+
+{{#DefnLifeInsIsGPT}}
+<p>
+{{GptFootnote}}
+</p>
+{{/DefnLifeInsIsGPT}}
+
+<p>
+{{InsCoName}} has the right to promptly refund any amount of premium paid if it
+will increase the net amount at risk (referred to in the {{ContractName}} as
+the Amount of Insurance that Requires a Charge).
+</p>
+
+<p>
+Premium payments are assumed to be made at the beginning of the year. Account
+values, cash surrender values, and death benefits are illustrated as of the end
+of the year. {{MinimumPremiumFootnote}}
+</p>
+
+</font>
diff --git a/nasd_notes2.mst b/nasd_notes2.mst
new file mode 100644
index 0000000..399d064
--- /dev/null
+++ b/nasd_notes2.mst
@@ -0,0 +1,244 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header}}
+
+<font size="-1">
+
+<p align="center"><b>EXPLANATORY NOTES</b></p>
+
+<p>
+{{GuarMortalityFootnote}}
+</p>
+
+<p>
+This illustration assumes death of the insured at
+age {{EndtAge}}.
+</p>
+
+{{#StateIsCarolina}}
+    <p>
+    In the states of North Carolina and South Carolina, Guaranteed Issue
+    Underwriting is referred to as "Limited Underwriting" and Simplified Issue
+    Underwriting is referred to as "Simplified Underwriting".
+    </p>
+{{/StateIsCarolina}}
+
+{{#StateIsMaryland}}
+    <p>
+    In the state of Maryland, Guaranteed Issue Underwriting
+    is referred to as "Nonstandard Limited Underwriting"
+    and Simplified Issue Underwriting is referred to as
+    "Nonstandard Simplified Underwriting".
+    </p>
+{{/StateIsMaryland}}
+
+<p>
+{{LoanFootnote}}
+</p>
+
+<p>
+{{PortabilityFootnote}}
+</p>
+
+{{#HasTerm}}
+    <p>
+    {{TermFootnote}}
+    </p>
+{{/HasTerm}}
+
+{{#HasSupplSpecAmt}}
+    <p>
+    {{TermFootnote}}
+    </p>
+{{/HasSupplSpecAmt}}
+
+{{#HasWP}}
+    <p>
+    {{WaiverFootnote}}
+    </p>
+{{/HasWP}}
+
+<p>
+{{AccelBftRiderFootnote}}
+</p>
+
+{{#DefnLifeInsIsGPT}}
+    <p>
+    {{OverloanRiderFootnote}}
+    </p>
+{{/DefnLifeInsIsGPT}}
+
+{{#HasADD}}
+    <p>
+    {{ADDFootnote}}
+    </p>
+{{/HasADD}}
+
+{{#HasChildRider}}
+    <p>
+    The ${{ChildRiderAmount}}&nbsp; {{ChildFootnote}}
+    </p>
+{{/HasChildRider}}
+
+{{#HasSpouseRider}}
+    <p>
+    The ${{SpouseRiderAmount}}&nbsp; {{SpouseFootnote}}
+    </p>
+{{/HasSpouseRider}}
+
+{{#UsePartialMort}}
+    <p>
+    This illustration reflects the client's mortality assumption
+    of {{PartMortTableMult[1]}} of the {{PartMortTableName}}
+    table with all deaths at the end of the year.
+    </p>
+{{/UsePartialMort}}
+
+{{#GenderBlended}}
+    <p>
+    Custom blending of cost of insurance charges is based
+    on the plan's assumed distribution of initial selected
+    face amount by gender and tobacco use. This illustration
+    assumes that the distribution remains constant
+    in future years. Custom blending is available only on plans
+    of 100 or more lives. Custom blend assumptions:
+    {{#SmokerBlended}}
+        tobacco = {{SmokerPct}};
+    {{/SmokerBlended}}
+    {{^SmokerBlended}}
+        no blending by tobacco use;
+    {{/SmokerBlended}}
+    male = {{MalePct}}.
+    </p>
+{{/GenderBlended}}
+{{#SmokerBlended}}
+    <p>
+    Custom blending of cost of insurance charges is based
+    on the plan's assumed distribution of initial selected
+    face amount by gender and tobacco use. This illustration
+    assumes that the distribution remains constant
+    in future years. Custom blending is available only on plans
+    of 100 or more lives. Custom blend assumptions:
+    tobacco = {{SmokerPct}};
+    {{#GenderBlended}}
+        male = {{MalePct}}.
+    {{/GenderBlended}}
+    {{^GenderBlended}}
+        no blending by gender.
+    {{/GenderBlended}}
+    </p>
+{{/SmokerBlended}}
+
+<p>
+The state of issue is
+{{StatePostalAbbrev}}.
+</p>
+
+{{#IsInforce}}
+    <p>
+    This illustration assumes a beginning account value of
+    ${{InforceUnloanedAV}} as
+    of the date of this illustration.
+    </p>
+
+    <p>
+    This illustration assumes a beginning cost basis of
+    ${{InforceTaxBasis}} as
+    of the date of this illustration; the actual cost basis
+    may be higher or lower. Consult the Home Office for cost
+    basis information.
+    </p>
+{{/IsInforce}}
+
+{{#HasComplianceTrackingNumber}}
+    <p>
+    Compliance tracking number: {{>imprimatur}}
+    </p>
+{{/HasComplianceTrackingNumber}}
+
+{{#UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+    <p>
+    *** This policy is classified as substandard guaranteed issue
+    per the requirements of the Texas Insurance Department.
+    </p>
+{{/UWTypeIsGuaranteedIssueInTexasWithFootnote}}
+
+<p align="center"><b>GUARANTEED PRINCIPAL ACCOUNT</b></p>
+
+<p>
+The Guaranteed Principal Account (GPA) has a guaranteed
+minimum annual interest rate of
+{{InitAnnGenAcctInt_Guaranteed}}.
+Guarantees are based on the claims-paying ability of the
+issuing company or companies.
+</p>
+
+<p align="center"><b>SEPARATE ACCOUNT</b></p>
+
+<p>
+{{HypotheticalRatesFootnote}}
+</p>
+
+{{^Composite}}
+    <p>
+    This illustration is based on total Separate Account
+    fund expenses of {{TotalIMF[1]}}.
+    </p>
+    {{#AvgFund}}
+        <p>
+        Investment management fees are based on equal
+        initial allocations among the available funds.
+        </p>
+    {{/AvgFund}}
+    {{#CustomFund}}
+        <p>
+        Investment management fees are based on an initial allocation
+        of 100% to a custom fund selected by the purchaser.
+        </p>
+    {{/CustomFund}}
+{{/Composite}}
+
+<p>
+<b>
+This material must be preceded or accompanied by the current prospectus for the
+{{PolicyMktgName}} Insurance {{ContractName}} and the prospectuses (or summary
+prospectuses, if available) for its underlying investment choices. Before
+purchasing a {{ContractName}}, you should carefully consider the investment
+objectives, risks, charges and expenses of the {{ContractName}} and its
+underlying investment choices. Please read the prospectuses carefully before
+investing or sending money.
+</b>
+</p>
+
+<p>
+{{SubsidiaryFootnote}}
+</p>
+
+<p>
+Securities offered through registered representatives of
+{{CoUnderwriter}}&nbsp;
+{{CoUnderwriterAddress}}
+or a broker-dealer that has a selling agreement with
+{{MainUnderwriter}}.
+</p>
+
+</font>
diff --git a/nasd_supp.mst b/nasd_supp.mst
new file mode 100644
index 0000000..706a1ef
--- /dev/null
+++ b/nasd_supp.mst
@@ -0,0 +1,29 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header}}
+
+{{>dollar_units}}
+
+<br></br>
+<br></br>
+
+{{! Supplemental illustration table is created from C++ code }}
diff --git a/nasd_supp_report.mst b/nasd_supp_report.mst
new file mode 100644
index 0000000..ed7bc7d
--- /dev/null
+++ b/nasd_supp_report.mst
@@ -0,0 +1,28 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>nasd_header_upper}}
+<p align="center">{{SupplementalReportTitle}}</p>
+{{>nasd_header_lower}}
+
+{{>dollar_units}}
+
+{{! The supplemental report table itself is generated from C++ code }}
diff --git a/objects.make b/objects.make
index aeeb181..f01b04c 100644
--- a/objects.make
+++ b/objects.make
@@ -174,6 +174,11 @@ cli_objects := \
 
 # Illustrations: files shared by the antediluvian and production branches.
 
+# PDF !! Expunge these two object files
+#  ledger_xml_io.o
+#  ledger_xsl.o
+# from the list below.
+
 common_common_objects := \
   $(boost_filesystem_objects) \
   $(xmlwrapp_objects) \
@@ -205,6 +210,7 @@ common_common_objects := \
   global_settings.o \
   group_quote_pdf_gen.o \
   group_values.o \
+  html.o \
   illustrator.o \
   input.o \
   input_harmonization.o \
@@ -214,13 +220,17 @@ common_common_objects := \
   input_sequence_parser.o \
   input_xml_io.o \
   interest_rates.o \
+  interpolate_string.o \
   ledger.o \
   ledger_base.o \
+  ledger_evaluator.o \
   ledger_invariant.o \
-  ledger_text_formats.o \
-  ledger_variant.o \
+  ledger_pdf.o \
+  ledger_pdf_generator.o \
   ledger_xml_io.o \
   ledger_xsl.o \
+  ledger_text_formats.o \
+  ledger_variant.o \
   ledgervalues.o \
   license.o \
   loads.o \
@@ -322,6 +332,7 @@ skeleton_objects := \
   illustration_document.o \
   illustration_view.o \
   input_sequence_entry.o \
+  ledger_pdf_generator_wx.o \
   main_common.o \
   mec_document.o \
   mec_view.o \
@@ -330,6 +341,7 @@ skeleton_objects := \
   multidimgrid_tools.o \
   mvc_controller.o \
   mvc_view.o \
+  pdf_writer_wx.o \
   policy_document.o \
   policy_view.o \
   preferences_view.o \
@@ -421,6 +433,7 @@ unit_test_targets := \
   ieee754_test \
   input_sequence_test \
   input_test \
+  interpolate_string_test \
   irc7702a_test \
   istream_to_string_test \
   loads_test \
@@ -690,6 +703,11 @@ input_test$(EXEEXT): \
   xml_lmi.o \
   yare_input.o \
 
+interpolate_string_test$(EXEEXT): \
+  $(common_test_objects) \
+  interpolate_string.o \
+  interpolate_string_test.o \
+
 irc7702a_test$(EXEEXT): \
   $(boost_filesystem_objects) \
   $(common_test_objects) \
diff --git a/ledger_xsl.hpp b/output_mode.hpp
similarity index 65%
copy from ledger_xsl.hpp
copy to output_mode.hpp
index dd2af40..c074280 100644
--- a/ledger_xsl.hpp
+++ b/output_mode.hpp
@@ -1,6 +1,6 @@
-// Ledger xsl operations.
+// Output mode enum used in PDF generation helpers.
 //
-// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017, 2018 Gregory W. Chicares.
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
 //
 // This program is free software; you can redistribute it and/or modify
 // it under the terms of the GNU General Public License version 2 as
@@ -19,20 +19,17 @@
 // email: <address@hidden>
 // snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
 
-#ifndef ledger_xsl_hpp
-#define ledger_xsl_hpp
+#ifndef output_mode_hpp
+#define output_mode_hpp
 
 #include "config.hpp"
 
-#include <boost/filesystem/path.hpp>
-
-#include <string>
-
-class Ledger;
-
-std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
-
-fs::path xsl_filepath(Ledger const&);
-
-#endif // ledger_xsl_hpp
+/// Convenient enum used with functions that can either actually render
+/// something or just pretend doing it in order to compute the space that would
+/// be taken by it, in the layout phase.
+enum enum_output_mode
+    {e_output_normal
+    ,e_output_measure_only
+    };
 
+#endif // output_mode_hpp
diff --git a/pdf_writer_wx.cpp b/pdf_writer_wx.cpp
new file mode 100644
index 0000000..82740a8
--- /dev/null
+++ b/pdf_writer_wx.cpp
@@ -0,0 +1,249 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "pdf_writer_wx.hpp"
+
+#include "alert.hpp"
+#include "global_settings.hpp"
+#include "html.hpp"
+
+#include <wx/filesys.h>
+#include <wx/html/htmlcell.h>
+
+namespace
+{
+
+// These margins are arbitrary and can be changed to conform to subjective
+// preferences.
+constexpr int horz_margin = 24;
+constexpr int vert_margin = 36;
+
+wxPrintData make_print_data
+    (wxString const&    output_filename
+    ,wxPrintOrientation orientation
+    )
+{
+    wxPrintData print_data;
+    print_data.SetPaperId(wxPAPER_LETTER);
+    print_data.SetFilename(output_filename);
+    print_data.SetOrientation(orientation);
+    return print_data;
+}
+
+} // Unnamed namespace.
+
+pdf_writer_wx::pdf_writer_wx
+    (wxString const&    output_filename
+    ,wxPrintOrientation orientation
+    ,std::array<int, 7> const* html_font_sizes
+    )
+    :print_data_        {make_print_data(output_filename, orientation)}
+    ,pdf_dc_            {print_data_}
+    ,html_parser_       {nullptr}
+    ,total_page_size_   {pdf_dc_.GetSize()}
+{
+    // Ensure that the output is independent of the current display resolution:
+    // it seems that this is only the case with the PDF map mode and wxDC mode
+    // different from wxMM_TEXT.
+    pdf_dc_.SetMapModeStyle(wxPDF_MAPMODESTYLE_PDF);
+
+    // For simplicity, use points for everything: font sizers are expressed in
+    // them anyhow, so it's convenient to use them for everything else too.
+    pdf_dc_.SetMapMode(wxMM_POINTS);
+
+    pdf_dc_.StartDoc(wxString()); // Argument is not used.
+    pdf_dc_.StartPage();
+
+    // Use a standard PDF Helvetica font (without embedding any custom fonts in
+    // the generated file, the only other realistic choice is Times New Roman).
+    pdf_dc_.SetFont
+        (wxFontInfo
+            (html_font_sizes
+                ? html_font_sizes->at(2)
+                : 8
+            )
+            .Family(wxFONTFAMILY_SWISS)
+            .FaceName("Helvetica")
+        );
+
+    // Create an HTML parser to allow easily adding HTML contents to the 
output.
+    html_parser_.SetDC(&pdf_dc_);
+    if(html_font_sizes)
+        {
+        html_parser_.SetFonts
+            ("Helvetica"
+            ,"Courier"
+            ,html_font_sizes->data()
+            );
+        }
+    else
+        {
+        html_parser_.SetStandardFonts
+            (pdf_dc_.GetFont().GetPointSize()
+            ,"Helvetica"
+            ,"Courier"
+            );
+        }
+
+    // Create the virtual file system object for loading images referenced from
+    // HTML and interpret relative paths from the data directory.
+    html_vfs_.reset(new wxFileSystem());
+    html_vfs_->ChangePathTo
+        (global_settings::instance().data_directory().string()
+        ,true /* argument is a directory, not file path */
+        );
+    html_parser_.SetFS(html_vfs_.get());
+}
+
+/// Output an image at the given scale into the PDF.
+///
+/// The scale specifies how many times the image should be shrunk:
+/// scale > 1 makes the image smaller, while scale < 1 makes it larger.
+///
+/// Updates pos_y by increasing it by the height of the specified
+/// image at the given scale.
+
+void pdf_writer_wx::output_image
+    (wxImage const&   image
+    ,char const*      image_name
+    ,double           scale
+    ,int              x
+    ,int*             pos_y
+    ,enum_output_mode output_mode
+    )
+{
+    int const y = wxRound(image.GetHeight() / scale);
+
+    switch(output_mode)
+        {
+        case e_output_normal:
+            {
+            // Use wxPdfDocument API directly as wxDC doesn't provide a way to
+            // set the image scale at PDF level and also because passing via
+            // wxDC wastefully converts wxImage to wxBitmap only to convert it
+            // back to wxImage when embedding it into the PDF.
+            wxPdfDocument* const pdf_doc = pdf_dc_.GetPdfDocument();
+            LMI_ASSERT(pdf_doc);
+
+            pdf_doc->SetImageScale(scale);
+            pdf_doc->Image(image_name, image, x, *pos_y);
+            pdf_doc->SetImageScale(1);
+            }
+            break;
+        case e_output_measure_only:
+            // Do nothing.
+            break;
+        default:
+            {
+            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+            }
+        }
+
+    *pos_y += y;
+}
+
+/// Render, or just pretend rendering in order to measure it, the given HTML
+/// contents at the specified position wrapping it at the given width.
+/// Return the height of the output (using this width).
+
+int pdf_writer_wx::output_html
+    (int x
+    ,int y
+    ,int width
+    ,html::text const& html
+    ,enum_output_mode output_mode
+    )
+{
+    // We don't really want to change the font, but to preserve the current DC
+    // font which is changed by rendering the HTML contents.
+    wxDCFontChanger preserve_font(pdf_dc_, wxFont());
+
+    auto const html_str = wxString::FromUTF8(html.as_html());
+    std::unique_ptr<wxHtmlContainerCell> const cell
+        (static_cast<wxHtmlContainerCell*>(html_parser_.Parse(html_str))
+        );
+    LMI_ASSERT(cell);
+
+    cell->Layout(width);
+    switch(output_mode)
+        {
+        case e_output_normal:
+            {
+            wxHtmlRenderingInfo rendering_info;
+            cell->Draw
+                (pdf_dc_
+                ,x
+                ,y
+                ,0
+                ,std::numeric_limits<int>::max()
+                ,rendering_info
+                );
+            }
+            break;
+        case e_output_measure_only:
+            // Do nothing.
+            break;
+        default:
+            {
+            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+            }
+        }
+
+    return cell->GetHeight();
+}
+
+int pdf_writer_wx::get_horz_margin() const
+{
+    return horz_margin;
+}
+
+int pdf_writer_wx::get_vert_margin() const
+{
+    return vert_margin;
+}
+
+int pdf_writer_wx::get_page_width()  const
+{
+    return total_page_size_.x - 2 * horz_margin;
+}
+
+int pdf_writer_wx::get_total_width() const
+{
+    return total_page_size_.x;
+}
+
+int pdf_writer_wx::get_page_height() const
+{
+    return total_page_size_.y - 2 * vert_margin;
+}
+
+int pdf_writer_wx::get_page_bottom() const
+{
+    return total_page_size_.y - vert_margin;
+}
+
+pdf_writer_wx::~pdf_writer_wx()
+{
+    // This will finally generate the PDF file.
+    pdf_dc_.EndDoc();
+}
diff --git a/pdf_writer_wx.hpp b/pdf_writer_wx.hpp
new file mode 100644
index 0000000..73f6e61
--- /dev/null
+++ b/pdf_writer_wx.hpp
@@ -0,0 +1,100 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017, 2018 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// 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, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef pdf_writer_wx_hpp
+#define pdf_writer_wx_hpp
+
+#include "config.hpp"
+
+#include "assert_lmi.hpp"
+#include "output_mode.hpp"
+
+#include <wx/html/winpars.h>
+
+#include <wx/pdfdc.h>
+
+#include <array>
+#include <memory>                       // std::unique_ptr
+
+class wxFileSystem;
+
+namespace html { class text; }
+
+class pdf_writer_wx
+{
+  public:
+    // Optional html_font_sizes array allows to override default font sizes for
+    // the standard HTML3 fonts (1..7).
+    pdf_writer_wx
+        (wxString const&           output_filename
+        ,wxPrintOrientation        orientation
+        ,std::array<int, 7> const* html_font_sizes = nullptr
+        );
+
+    pdf_writer_wx(pdf_writer_wx const&) = delete;
+    pdf_writer_wx& operator=(pdf_writer_wx const&) = delete;
+
+    ~pdf_writer_wx();
+
+    // High level functions which should be preferably used if possible.
+    int output_html
+        (int x
+        ,int y
+        ,int width
+        ,html::text const& html
+        ,enum_output_mode output_mode = e_output_normal
+        );
+
+    void output_image
+        (wxImage const&   image
+        ,char const*      image_name
+        ,double           scale
+        ,int              x
+        ,int*             pos_y
+        ,enum_output_mode output_mode = e_output_normal
+        );
+
+    // Accessors allowing to use lower level wxDC API directly.
+    wxDC& dc() { return pdf_dc_; }
+
+    // Page metrics: the page width and height are the size of the page region
+    // reserved for the normal contents, excluding horizontal and vertical
+    // margins. Total width and height include the margins.
+    int get_horz_margin() const;
+    int get_vert_margin() const;
+    int get_page_width()  const;
+    int get_total_width() const;
+    int get_page_height() const;
+    int get_page_bottom() const;
+
+  private:
+    wxPrintData print_data_;
+    wxPdfDC pdf_dc_;
+
+    // Order is potentially important here: html_parser_ uses html_vfs_, so
+    // must be declared after it in order to be destroyed before it.
+    std::unique_ptr<wxFileSystem> html_vfs_;
+    wxHtmlWinParser html_parser_;
+
+    wxSize const total_page_size_;
+};
+
+#endif // pdf_writer_wx_hpp
diff --git a/reg_column_headings.mst b/reg_column_headings.mst
new file mode 100644
index 0000000..79191f5
--- /dev/null
+++ b/reg_column_headings.mst
@@ -0,0 +1,202 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<!-- No header for this page, just the logo -->
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<p align="center">
+Column Headings and Key Terms Used in This Illustration
+</p>
+<font size="-1">
+<p>
+<b>{{AvName}} Value:</b>
+The accumulation at interest of the net premiums paid,
+{{#SinglePremium}}
+less any withdrawals,
+{{/SinglePremium}}
+less any monthly charges deducted.
+</p>
+<p>
+<b>{{CsvName}} Value:</b>
+{{AvName}} Value less policy debt.
+{{#Has1035ExchCharge}}
+{{CashSurrValueFootnote}}
+{{/Has1035ExchCharge}}
+</p>
+{{^IsInforce}}
+<p>
+<b>Current Illustrated Crediting Rate:</b>
+{{CreditingRateFootnote}}
+</p>
+{{/IsInforce}}
+<p>
+<b>Current Values:</b>
+Values assuming current interest crediting rates
+and current monthly charges. These values are not guaranteed
+and are based on the assumption that premium is paid
+as illustrated.
+</p>
+<p>
+<b>Death Benefit:</b>
+The amount of benefit provided by the Death Benefit Option
+in effect on the date of death, prior to adjustments
+for policy debt and monthly charges payable to the date of death.
+<!--
+Presumably the description of death benefit options should be moved into
+'ProductDescription' (as has already been done in other '.xsl' files).
+-->
+</p>
+<p>
+<b>Death Benefit Option 1:</b>
+Option in which the death benefit is equal to the selected
+face amount of the contract on the date of death of the insured,
+or if greater the {{AvName}} Value
+{{#HasSalesLoadRefund}}
+plus the refund of sales loads (if applicable)
+{{/HasSalesLoadRefund}}
+on the insured's date of death multiplied by the minimum face
+amount percentage for the insured's attained age at death
+(minimum face amount). Please refer to the contract
+for a detailed schedule of death benefit factors.
+</p>
+<!-- Group Experience Rating Logic -->
+{{^GroupExperienceRating}}
+<p>
+<b>Death Benefit Option 2:</b>
+Option in which the death benefit is equal to the selected
+face amount of the contract
+plus the {{AvName}} Value
+on the date of death of the insured, or if greater,
+the {{AvName}} Value
+{{#HasSalesLoadRefund}}
+plus the refund of sales loads (if applicable)
+{{/HasSalesLoadRefund}}
+on the insured's date of death multiplied
+by the death benefit factor for the insured's attained age
+at death (minimum face amount). Please refer to the contract
+for a detailed schedule of death benefit factors.
+{{/GroupExperienceRating}}
+<!-- Group Experience Rating Logic -->
+{{#GroupExperienceRating}}
+</p>
+<p>
+<b>Experience Rating Risk Charge:</b>
+Applies only to certain experience rated groups.
+This charge is based on the cost of insurance charges
+assessed during the certificate year. It may be assessed against
+the account value once per certificate anniversary date
+and upon surrender of the group policy.
+</p>
+{{/GroupExperienceRating}}
+<!-- Group Experience Rating Logic -->
+{{^GroupExperienceRating}}
+{{#Has1035ExchCharge}}
+<p>
+<b>Exchange Charge:</b>
+{{ExchangeChargeFootnote1}}
+</p>
+{{/Has1035ExchCharge}}
+{{/GroupExperienceRating}}
+
+<!-- Single Premium Logic -->
+{{^SinglePremium}}
+<p>
+<b>Flexible Premiums:</b>
+Premiums that may be increased, reduced, or not paid,
+if the account value is sufficient to cover the monthly charges.
+{{/SinglePremium}}
+</p>
+<p>
+<b>Guaranteed Values:</b>
+Values assuming the guaranteed crediting rate
+and the guaranteed maximum monthly charges. These values
+are based on the assumption that premium is paid as illustrated.
+</p>
+<p>
+<b>MEC:</b>
+Modified Endowment Contract - this classification is given
+to a contract in violation of TAMRA
+(Technical and Miscellaneous Revenue Act), which limits
+the amount of premium that can be paid into a life
+insurance contract. To the extent of gain in the contract, loans,
+distributions and withdrawals from a MEC are subject
+to income tax and may also trigger a tax penalty.
+</p>
+{{^IsInforce}}
+<p>
+<b>Midpoint Values:</b>
+Values assuming interest rates that are the average
+of the illustrated current crediting rates
+and the guaranteed minimum interest rate, and monthly charges
+that are the average of the current monthly charges
+and the guaranteed monthly charges.
+These values are not guaranteed and are based on the assumption
+that premium is paid as illustrated.
+</p>
+{{/IsInforce}}
+<!-- Single Premium Logic -->
+{{#ModifiedSinglePremiumOrModifiedSinglePremium0}}
+<p>
+<b>Modified Single Premium:</b>
+After the single premium is paid, additional payment
+under this policy will only be accepted for repayment
+of policy debt, payment required to keep the policy
+from lapsing, or payment required to reinstate the policy.
+</p>
+{{/ModifiedSinglePremiumOrModifiedSinglePremium0}}
+<p>
+<b>Monthly Charges:</b>
+The monthly charges for the following month which include:
+cost of insurance, plus face amount charges (if applicable),
+plus the administrative charge shown
+on the contract schedule page.
+</p>
+<p>
+<b>Premium Outlay:</b>
+The amount of premium assumed to be paid by the contract owner
+or other premium payor.
+</p>
+<p>
+<b>Selected Face Amount:</b>
+The Selected Face Amount may be decreased upon written application
+satisfactory to {{InsCoName}}. A
+requested decrease is allowed only once per Policy Year, and the
+resulting Selected Face Amount after decrease must meet the
+minimum defined in your policy.
+</p>
+<!-- Single Premium Logic -->
+{{#SinglePremium}}
+{{#ModifiedSinglePremiumOrModifiedSinglePremium0}}
+<p>
+<b>Single Premium:</b>
+After the single premium is paid, additional payment
+under this policy will only be accepted for repayment
+of policy debt, payment required to keep the policy
+from lapsing, or payment required to reinstate the policy.
+</p>
+{{/ModifiedSinglePremiumOrModifiedSinglePremium0}}
+<!-- Single Premium Logic -->
+<p>
+<b>Ultimate Illustrated Crediting Rate:</b>
+{{UltCreditingRateFootnote}}
+{{/SinglePremium}}
+</p>
+</font>
diff --git a/reg_d_group_basic.mst b/reg_d_group_basic.mst
new file mode 100644
index 0000000..ac47311
--- /dev/null
+++ b/reg_d_group_basic.mst
@@ -0,0 +1,36 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_group_header_upper}}
+<p align="center">
+{{#IsInforce}}
+    In Force
+{{/IsInforce}}
+Basic Illustration
+</p>
+{{>reg_d_group_header_lower}}
+
+{{>dollar_units}}
+
+<br></br>
+<br></br>
+
+{{! Basic illustration table is created from C++ code }}
diff --git a/reg_d_group_column_headings.mst b/reg_d_group_column_headings.mst
new file mode 100644
index 0000000..63d8834
--- /dev/null
+++ b/reg_d_group_column_headings.mst
@@ -0,0 +1,56 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_group_header}}
+
+<p align="center"><b>Column Definitions</b></p>
+
+<p>
+<b>Policy Year</b>
+<br>
+{{PolicyYearFootnote}}
+</p>
+
+{{^Composite}}
+    <p>
+    <b>End of Year Age</b>
+    <br>
+    {{AttainedAgeFootnote}}
+    </p>
+{{/Composite}}
+
+<p>
+<b>Premium Outlay</b>
+<br>
+{{OutlayFootnote}}
+</p>
+
+<p>
+<b>Cash Surrender Value</b>
+<br>
+{{CashSurrValueFootnote}}
+</p>
+
+<p>
+<b>Death Benefit</b>
+<br>
+{{DeathBenefitFootnote}}
+</p>
diff --git a/reg_d_group_footer_lower.mst b/reg_d_group_footer_lower.mst
new file mode 100644
index 0000000..f404919
--- /dev/null
+++ b/reg_d_group_footer_lower.mst
@@ -0,0 +1,40 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<table width="100%" cellspacing="0" cellpadding="0">
+    <tr>
+        <td colspan="3">&nbsp;</td>
+    </tr>
+    <tr>
+        <td>{{InsCoName}}</td>
+        <td align="center">Date Prepared: {{date_prepared}}</td>
+        <td align="right">Policy Form: {{PolicyForm}}</td>
+    </tr>
+    <tr>
+        <td>{{InsCoAddr}}</td>
+        <td align="center">System Version: {{LmiVersion}}</td>
+        <td align="right">{{page_number}}</td>
+    </tr>
+</table>
+
+</font>
diff --git a/reg_d_group_footer_upper.mst b/reg_d_group_footer_upper.mst
new file mode 100644
index 0000000..5ccb8bd
--- /dev/null
+++ b/reg_d_group_footer_upper.mst
@@ -0,0 +1,33 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<p>
+* This illustration is based on the assumed Gross Rate shown.
+The Net Rate is provided for information purposes only.
+</p>
+<p>
+See the Narrative Summary for important policy information.
+This illustration is not complete without all pages.
+</p>
+
+</font>
diff --git a/reg_d_group_header.mst b/reg_d_group_header.mst
new file mode 100644
index 0000000..2fb8b4e
--- /dev/null
+++ b/reg_d_group_header.mst
@@ -0,0 +1,29 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{!
+    The header is split in two parts, upper and lower one, only to allow
+    inserting the title between them, as a couple of pages do. But we still
+    provide this "full header" template for the pages that don't have any
+    title to make them simpler.
+}}
+{{>reg_d_group_header_upper}}
+{{>reg_d_group_header_lower}}
diff --git a/reg_d_group_header_lower.mst b/reg_d_group_header_lower.mst
new file mode 100644
index 0000000..c8310cf
--- /dev/null
+++ b/reg_d_group_header_lower.mst
@@ -0,0 +1,77 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<br>
+<br>
+
+<table width="100%" cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td>
+        {{#Composite}}
+            {{CorpNameAbbrev60}}
+        {{/Composite}}
+        {{^Composite}}
+            {{Insured1Abbrev30}}
+        {{/Composite}},
+        {{Gender}} {{Smoker}} rates, Age {{Age}}<br>
+        {{#Composite}}
+            Composite<br>
+        {{/Composite}}
+        Contract: {{PolicyMktgName}}<br>
+        Initial Premium: ${{InitPrem}}<br>
+        {{#GroupCarveout}}
+            Minimum Initial Premium: ${{InitMinPrem}}<br>
+        {{/GroupCarveout}}
+        First Year Premium: ${{InitPrem}}<br>
+        {{^Composite}}
+            {{#UWTypeIsMedical}}
+              Fully underwritten,
+            {{/UWTypeIsMedical}}
+            {{^UWTypeIsMedical}}
+              {{UWType}},
+            {{/UWTypeIsMedical}}
+            {{UWClass}}, Initial Death Benefit Option: {{InitDBOpt}}<br>
+        {{/Composite}}
+        {{#HasWP}}
+            Waiver of Monthly Charges Rider elected.<br>
+        {{/HasWP}}
+        {{>contract_numbers}}
+        </td>
+        <td>
+        Assumed Gross Rate: {{InitAnnSepAcctGrossInt_Current}}
+        ({{InitAnnSepAcctNetInt_Current}} Net)*<br>
+        Initial
+        {{#HasTerm}}
+            Total
+        {{/HasTerm}}
+        Face Amount: ${{InitTotalSA}}<br>
+        {{#HasTerm}}
+            Initial Base Face Amount: ${{InitBaseSpecAmt}}<br>
+            Initial Term Face Amount: ${{InitTermSpecAmt}}<br>
+        {{/HasTerm}}
+        {{^Composite}}
+            {{#UWClassIsRated}}
+                  Table Rating: {{SubstandardTable}}<br>
+            {{/UWClassIsRated}}
+        {{/Composite}}
+        </td>
+    </tr>
+</table>
diff --git a/reg_d_group_header_upper.mst b/reg_d_group_header_upper.mst
new file mode 100644
index 0000000..4dd5b04
--- /dev/null
+++ b/reg_d_group_header_upper.mst
@@ -0,0 +1,39 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<br></br>
+<br></br>
+
+<p>
+The purpose of this illustration is to show how the performance
+of the underlying separate account divisions could affect
+the contract account value and death benefit.
+<b>
+The hypothetical returns do not reflect past performance
+and are not predictive of future results. Actual results
+could be less than or greater than the hypothetical results
+and in all likelihood will vary year to year.
+</b>
+</p>
+
+<br></br>
diff --git a/reg_d_group_narr_summary.mst b/reg_d_group_narr_summary.mst
new file mode 100644
index 0000000..33e51bc
--- /dev/null
+++ b/reg_d_group_narr_summary.mst
@@ -0,0 +1,123 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_group_header_upper}}
+<p align="center">Narrative Summary</p>
+{{>reg_d_group_header_lower}}
+
+<p>
+{{PolicyMktgName}} is a {{PolicyLegalName}} issued by {{InsCoName}}.
+{{ProductDescription}}
+</p>
+
+<p>
+<u>Stable Value Feature</u>&nbsp;{{StableValueFootnote}}
+</p>
+
+<p>
+This illustration assumes Initial Death Benefit Option {{InitDBOpt}}.
+</p>
+
+<p>
+This policy is only available to entities that meet the definition
+of "accredited investor" or "qualified purchaser" under applicable
+Federal securities laws.  Purchase of this policy is suitable
+only for entities of substantial economic means. Each prospective
+purchaser will be required to represent that it is familiar
+with and understands the fundamental risks and financial hazards
+of purchasing the policy. Each prospective purchaser must
+also represent that it meets minimum financial and other
+suitability standards.
+</p>
+
+<p>
+{{NoVanishPremiumFootnote}}
+</p>
+
+<p>
+The definition of life insurance elected for this contract
+is the
+{{#DefnLifeInsIsGPT}}
+    guideline premium test. The guideline single premium
+    is ${{InitGSP}}
+    and the guideline level premium
+    is ${{InitGLP}}.
+{{/DefnLifeInsIsGPT}}
+{{^DefnLifeInsIsGPT}}
+    cash value accumulation test.
+{{/DefnLifeInsIsGPT}}
+</p>
+
+<p>
+{{InsCoName}}
+{{RejectPremiumFootnote}}
+</p>
+
+<p>
+Account values, cash surrender values, and death benefits
+are illustrated as of the end of the year.
+</p>
+
+<p>
+PLEASE READ THE FOLLOWING IMPORTANT TAX DISCLOSURE
+<br>
+The initial 7-pay premium limit is ${{InitSevenPayPrem}}.
+As illustrated, this contract
+{{#IsMec}}
+    fails
+{{/IsMec}}
+{{^IsMec}}
+    passes
+{{/IsMec}}
+the seven-pay test defined in Section 7702A
+of the Internal Revenue Code and therefore
+{{#IsMec}}
+    {{^MecYearIs0}}
+        becomes a Modified Endowment Contract (MEC) in policy year
+        {{MecYearPlus1}}.
+    {{/MecYearIs0}}
+    {{#MecYearIs0}}
+        is a Modified Endowment Contract (MEC).
+    {{/MecYearIs0}}
+    As a MEC, any loans or withdrawals are taxed to the extent
+    of any gain in the contract, and an additional 10% penalty
+    tax will apply to the taxable portion of the loan
+    or withdrawal.
+{{/IsMec}}
+{{^IsMec}}
+    is not a Modified Endowment Contract (MEC).
+    Subsequent changes in the contract, including but not limited
+    to increases and decreases in premiums or benefits, may cause
+    the contract to be retested and may result in the contract
+    becoming a MEC.
+{{/IsMec}}
+</p>
+
+<p>
+<b>
+This illustration is not written or intended as tax
+or legal advice and may not be relied on for purposes
+of avoiding any federal tax penalties. For more information
+pertaining to the tax consequences of purchasing
+or owning this policy, consult with your own independent tax
+or legal counsel.
+</b>
+</p>
diff --git a/reg_d_group_narr_summary2.mst b/reg_d_group_narr_summary2.mst
new file mode 100644
index 0000000..696c778
--- /dev/null
+++ b/reg_d_group_narr_summary2.mst
@@ -0,0 +1,161 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_group_header_upper}}
+<p align="center">Narrative Summary</p>
+{{>reg_d_group_header_lower}}
+
+<p>
+{{GuarMortalityFootnote}}
+</p>
+
+<p>
+The loan interest rate may be fixed or adjustable as elected
+by the policy owner.
+</p>
+
+<p>
+{{#UseExperienceRating}}
+    {{ExpRatingFootnote}}
+{{/UseExperienceRating}}
+{{^UseExperienceRating}}
+    The illustration does not assume any mortality
+    experience rating.
+{{/UseExperienceRating}}
+</p>
+
+<p>
+<b>
+This illustration may only be used when preceded or accompanied
+by the offering memorandum for {{PolicyLegalName}} ({{PolicyMktgName}})
+insurance contract and its underlying investment choices.
+For a complete listing of the underlying investment choices,
+please refer to the offering memorandum. Before purchasing
+a variable life insurance contract, investors should
+carefully consider the investment objectives, risks, charges
+and expenses of the variable life insurance contract
+and its underlying investment choices. Please read
+the offering memorandum carefully before investing
+or sending money.
+</b>
+</p>
+
+{{#HasTerm}}
+    <p>
+    {{TermFootnote}}
+    </p>
+{{/HasTerm}}
+
+{{#HasWP}}
+    <p>
+    {{WaiverFootnote}}
+    </p>
+{{/HasWP}}
+
+{{#UsePartialMort}}
+    <p>
+    This illustration reflects an initial mortality assumption
+    of {{PartMortTableMult[1]}} of the {{PartMortTableName}} table
+    with all deaths at the end of the year.
+    </p>
+{{/UsePartialMort}}
+
+{{#GenderBlended}}
+    <p>
+    {{MortalityBlendFootnote}}
+    Custom blend assumptions:
+    {{#SmokerBlended}}
+        tobacco = {{SmokerPct}};
+    {{/SmokerBlended}}
+    {{^SmokerBlended}}
+        no blending by tobacco use;
+    {{/SmokerBlended}}
+    male = {{MalePct}}.
+    </p>
+{{/GenderBlended}}
+{{^GenderBlended}}
+    {{#SmokerBlended}}
+        <p>
+        {{MortalityBlendFootnote}}
+        Custom blend assumptions:
+        tobacco = {{SmokerPct}};
+        {{#GenderBlended}}
+            male = {{MalePct}}.
+        {{/GenderBlended}}
+        {{^GenderBlended}}
+            no blending by gender.
+        {{/GenderBlended}}
+      </p>
+    {{/SmokerBlended}}
+{{/GenderBlended}}
+
+<p>
+The state of issue is {{StatePostalAbbrev}}.
+</p>
+
+{{#HasComplianceTrackingNumber}}
+    <p>
+    Compliance tracking number: {{>imprimatur}}
+    </p>
+{{/HasComplianceTrackingNumber}}
+
+<p>
+SEPARATE ACCOUNT
+</p>
+
+<p>
+{{HypotheticalRatesFootnote}}
+</p>
+
+<p>
+This illustration is based on total Separate Account fund expenses
+of {{TotalIMF[1]}}.
+</p>
+
+<p>
+<b>
+This illustration may not reflect your actual tax
+and accounting consequences and is not intended as tax advice
+nor may it be relied on for purposes of avoiding any federal
+tax penalties. Consult professional tax advisors for tax advice.
+</b>
+</p>
+
+<p>
+Placement Agents:
+{{CoUnderwriter}} serves as the
+placement agent for contracts sold by its registered
+representatives.
+&nbsp;{{MainUnderwriter}}
+serves as the placement agent for contracts sold by registered
+representatives of other broker-dealers that have entered into
+distribution agreements with
+&nbsp;{{MainUnderwriter}}.
+</p>
+
+<p>
+{{CoUnderwriter}} and
+&nbsp;{{MainUnderwriter}} are
+subsidiaries of {{InsCoName}}
+({{InsCoShortName}}) and are
+located at {{InsCoStreet}}.
+</p>
+</p>
diff --git a/reg_d_group_supp_report.mst b/reg_d_group_supp_report.mst
new file mode 100644
index 0000000..31dcfba
--- /dev/null
+++ b/reg_d_group_supp_report.mst
@@ -0,0 +1,28 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_group_header_upper}}
+<p align="center">{{SupplementalReportTitle}}</p>
+{{>reg_d_group_header_lower}}
+
+{{>dollar_units}}
+
+{{! The supplemental report table itself is generated from C++ code }}
diff --git a/reg_d_indiv_cover_page.mst b/reg_d_indiv_cover_page.mst
new file mode 100644
index 0000000..1de1f64
--- /dev/null
+++ b/reg_d_indiv_cover_page.mst
@@ -0,0 +1,130 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+
+<p align="center">
+<b>
+
+<font size="+3">
+{{PolicyMktgName}}
+</font>
+
+<br></br>
+<br></br>
+
+<font size="+2">
+Hypothetical Life Insurance Illustration
+</font>
+
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+
+<font size="+1">
+Prepared by: {{InsCoShortName}}
+
+<br></br>
+<br></br>
+
+Prepared for:
+{{^Composite}}
+    {{Insured1Abbrev140}}
+{{/Composite}}
+{{^Composite}}
+    {{CorpNameAbbrev140}}
+{{/Composite}}
+
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+
+Date Prepared: {{date_prepared}}
+
+</font>
+</b>
+</p>
+
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+<br></br>
+
+<font size="-1">
+
+<p>
+The purpose of this illustration is to show how hypothetical rates
+of return will affect policy account value. These hypothetical
+returns do not reflect past performance and are not predictive
+of future results. Actual results could be less than or greater
+than the hypothetical rates and in all likelihood will vary from
+year to year.
+</p>
+
+<p>
+<b>
+This material must be preceded or accompanied by the current
+Confidential Private Placement Memorandum for {{PolicyMktgName}}
+and the current prospectuses (or summary prospectuses,
+if available) and private placement memoranda
+for its underlying investment choices. Investors should
+carefully consider the investment objectives, risks,
+charges and expenses of the policy and its underlying investment
+choices. Please read the prospectuses and private placement
+memoranda carefully before investing or sending money.
+</b>
+</p>
+
+<p>
+{{PolicyMktgName}} &nbsp; {{IssuingCompanyFootnote}}
+</p>
+
+<p>
+Placement Agents: &nbsp; {{PlacementAgentFootnote}}
+</p>
+
+<p>
+{{SubsidiaryFootnote}}
+</p>
+
+<p>
+{{MarketingNameFootnote}}
+</p>
+
+<p>
+Compliance tracking number: {{>imprimatur}}
+</p>
+
+</font>
diff --git a/reg_d_indiv_curr.mst b/reg_d_indiv_curr.mst
new file mode 100644
index 0000000..f30b922
--- /dev/null
+++ b/reg_d_indiv_curr.mst
@@ -0,0 +1,34 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_indiv_header}}
+
+<font size="-1">
+
+<p align="center">
+End of Year Policy Values using Current Charges
+</p>
+
+{{>dollar_units}}
+
+</font>
+
+{{! The current charges values table is generated from C++ code }}
diff --git a/reg_d_indiv_curr_irr.mst b/reg_d_indiv_curr_irr.mst
new file mode 100644
index 0000000..4235b46
--- /dev/null
+++ b/reg_d_indiv_curr_irr.mst
@@ -0,0 +1,34 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_indiv_header}}
+
+<font size="-1">
+
+<p align="center">
+End of Year Policy Values using Current Charges
+</p>
+
+{{>dollar_units}}
+
+</font>
+
+{{! The IRR values table is generated from C++ code }}
diff --git a/reg_d_indiv_footer_lower.mst b/reg_d_indiv_footer_lower.mst
new file mode 100644
index 0000000..07b749e
--- /dev/null
+++ b/reg_d_indiv_footer_lower.mst
@@ -0,0 +1,40 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<table width="100%" cellspacing="0" cellpadding="0">
+    <tr>
+        <td colspan="3">&nbsp;</td>
+    </tr>
+    <tr>
+        <td>{{InsCoName}}</td>
+        <td align="center">System Version: {{LmiVersion}}</td>
+        <td align="right">Policy Form: {{PolicyForm}}</td>
+    </tr>
+    <tr>
+        <td>{{InsCoAddr}}</td>
+        <td>&nbsp;</td>
+        <td align="right">{{page_number}}</td>
+    </tr>
+</table>
+
+</font>
diff --git a/reg_d_indiv_footer_upper.mst b/reg_d_indiv_footer_upper.mst
new file mode 100644
index 0000000..e82a17c
--- /dev/null
+++ b/reg_d_indiv_footer_upper.mst
@@ -0,0 +1,28 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<p>
+This illustration is not complete unless all pages as noted below are included.
+</p>
+
+</font>
diff --git a/reg_d_indiv_guar_irr.mst b/reg_d_indiv_guar_irr.mst
new file mode 100644
index 0000000..7f2e6d2
--- /dev/null
+++ b/reg_d_indiv_guar_irr.mst
@@ -0,0 +1,34 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_indiv_header}}
+
+<font size="-1">
+
+<p align="center">
+End of Year Policy Values using Guaranteed Charges
+</p>
+
+{{>dollar_units}}
+
+</font>
+
+{{! The IRR values table is generated from C++ code }}
diff --git a/reg_d_indiv_header.mst b/reg_d_indiv_header.mst
new file mode 100644
index 0000000..3bb9a39
--- /dev/null
+++ b/reg_d_indiv_header.mst
@@ -0,0 +1,90 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<font size="-1">
+
+<p align="center">
+{{^IsInforce}}
+    Illustration for Flexible Premium Variable Adjustable
+    Life Insurance Contract.
+{{/IsInforce}}
+{{#IsInforce}}
+    In Force Illustration for Flexible Premium Variable Adjustable
+    Life Insurance Contract.
+{{/IsInforce}}
+
+<br></br>
+<br></br>
+
+The purpose of the Illustration is to show how the performance of the
+underlying separate account divisions could affect the policy's
+account values and death benefits. This Illustration is hypothetical
+and may not be used to project or predict investment results.
+</p>
+
+<br></br>
+<br></br>
+
+<table width="100%" cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td width="60%">
+            Date Prepared: {{date_prepared}}<br>
+            {{#Composite}}
+                Composite of individuals<br>
+            {{/Composite}}
+            {{^Composite}}
+                Prepared for: {{Insured1Abbrev30}}<br>
+                Age: {{Age}}<br>
+                Issue State: {{StatePostalAbbrev}}<br>
+            {{/Composite}}
+            Selected Face Amount: ${{InitTotalSA}}<br>
+            Initial Death Benefit Option: {{InitDBOpt}}<br>
+        </td>
+        <td width="40%">
+            Policy: {{PolicyMktgName}}<br>
+            {{^Composite}}
+                Underwriting Type:
+                {{#UWTypeIsMedical}}
+                    Fully underwritten<br>
+                {{/UWTypeIsMedical}}
+                {{^UWTypeIsMedical}}
+                    {{UWType}}<br>
+                {{/UWTypeIsMedical}}
+
+                Rate Classification: {{Gender}}, {{Smoker}}, {{UWClass}}<br>
+
+                {{#UWClassIsRated}}
+                    Table Rating: {{SubstandardTable}}<br>
+                    {{!
+                        Flats don't require "Rated" class; and does this value 
actually print anyway?
+                        These questions apply as well to the original:
+                        
http://svn.savannah.nongnu.org/viewvc/lmi/trunk/individual_private_placement.xsl?annotate=696&root=lmi&pathrev=3585
+                    }}
+                    Initial Annual Flat Extra: {{AnnualFlatExtra[1]}} per 
1,000<br>
+                {{/UWClassIsRated}}
+            {{/Composite}}
+        </td>
+    </tr>
+</table>
+
+</font>
diff --git a/reg_d_indiv_notes1.mst b/reg_d_indiv_notes1.mst
new file mode 100644
index 0000000..0a79bec
--- /dev/null
+++ b/reg_d_indiv_notes1.mst
@@ -0,0 +1,135 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{! No header on this page, but still use the logo. }}
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<p align="center"><font size="+1"><b>Explanatory Notes</b></font></p>
+
+<font size="-1">
+
+<p>
+<font size="+1"><b><u>The Illustration</u></b></font>
+</p>
+
+<p>
+This illustration is not a policy or an offer or solicitation
+to purchase a policy. Offers are made only through the
+Confidential Private Placement Memorandum ("the PPM") for {{PolicyMktgName}}.
+</p>
+
+<p>
+  {{ProductDescription}}.
+</p>
+
+<br></br>
+<br></br>
+
+<p>
+<font size="+1"><b><u>The Policy</u></b></font>
+</p>
+
+<p>
+<b>Policy Requirements:</b>
+This policy is only available to persons who are deemed
+accredited investors and qualified purchasers under applicable
+federal securities laws. The policy owner must be able to bear
+the risk of loss of the entire investment in the policy. The
+policy owner must be familiar with and understand the fundamental
+risks and financial hazards of investing in the policy, and be
+willing to represent as such to {{InsCoShortName}}.
+</p>
+
+<p>
+<b>Maximum Net Amount at Risk Limitation:</b> {{MaxNaarFootnote}}
+</p>
+
+<p>
+<b>Monthly Charges:</b> &nbsp; {{MonthlyChargesFootnote}}
+</p>
+
+<p>
+<b>Surrender Charges:</b> &nbsp; {{PremTaxSurrChgFootnote}}
+</p>
+
+<br></br>
+<br></br>
+
+<p>
+<font size="+1"><b><u>Column Definitions</u></b></font>
+</p>
+
+<p>
+<b>Account Value:</b> &nbsp; {{AccountValueFootnote}}
+</p>
+
+<p>
+<b>Administrative Charge:</b> &nbsp; {{PolicyFeeFootnote}}
+</p>
+
+<p>
+<b>Asset Charges:</b> &nbsp; {{AssetChargeFootnote}}
+</p>
+
+<p>
+<b>Cash Surrender Value:</b> &nbsp; {{CashSurrValueFootnote}}
+</p>
+
+<p>
+<b>Death Benefit:</b> &nbsp; {{DeathBenefitFootnote}}
+</p>
+
+<p>
+<b>End of Year Age:</b> &nbsp; {{AttainedAgeFootnote}}
+</p>
+
+<p>
+<b>Investment Income:</b> &nbsp; {{InvestmentIncomeFootnote}}
+</p>
+
+<p>
+<b>IRR on Death Benefit:</b> &nbsp; {{IrrDbFootnote}}
+</p>
+
+<p>
+<b>IRR on Surrender Value:</b> &nbsp; {{IrrCsvFootnote}}
+</p>
+
+<p>
+<b>Mortality Charges:</b> &nbsp; {{MortalityChargesFootnote}}
+</p>
+
+<p>
+<b>Policy Year:</b> &nbsp; {{PolicyYearFootnote}}
+</p>
+
+<p>
+<b>Premium Loads:</b> The applicable state premium tax, deferred acquisition
+cost tax charge, sales load (if any), separate account administrative charge
+(if any), and other charges incurred as a result of retaining an unaffiliated
+money manager (if any).
+</p>
+
+<p>
+<b>Premium Outlay:</b> &nbsp; {{OutlayFootnote}}
+</p>
+
+</font>
diff --git a/reg_d_indiv_notes2.mst b/reg_d_indiv_notes2.mst
new file mode 100644
index 0000000..7f7c327
--- /dev/null
+++ b/reg_d_indiv_notes2.mst
@@ -0,0 +1,56 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{! No header on this page, but still use the logo. }}
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<p align="center"><font size="+1"><b>Explanatory Notes</b></font></p>
+
+<font size="-1">
+
+<p>
+<font size="+1"><b><u>Separate Account</u></b></font>
+</p>
+
+<p>
+{{HypotheticalRatesFootnote}}
+</p>
+
+<br></br>
+<br></br>
+
+<p>
+<font size="+1"><b><u>Withdrawals and Policy Loans</u></b></font>
+</p>
+
+<p>
+If applicable, withdrawals and policy loans will appear on a
+supplemental report at the end of the illustration. Withdrawals
+and policy loans are assumed to be taken at the beginning of the
+applicable policy year. The illustration and supplemental report
+reflect a fixed policy loan interest rate of {{InitAnnLoanDueRate}}.
+</p>
+
+<p>
+{{LoanAndWithdrawalFootnote}}
+</p>
+
+</font>
diff --git a/reg_d_indiv_notes3.mst b/reg_d_indiv_notes3.mst
new file mode 100644
index 0000000..f8d1adb
--- /dev/null
+++ b/reg_d_indiv_notes3.mst
@@ -0,0 +1,186 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{! No header on this page, but still use the logo. }}
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<p align="center"><font size="+1"><b>Explanatory Notes</b></font></p>
+
+<font size="-1">
+
+<p>
+<font size="+1"><b><u>Important Tax Disclosures</u></b></font>
+</p>
+
+<p>
+The definition of life insurance elected for this policy is
+{{#DefnLifeInsIsGPT}}
+    the guideline premium test. The guideline single premium
+    is ${{InitGSP}}
+    and the guideline level premium
+    is ${{InitGLP}}.
+{{/DefnLifeInsIsGPT}}
+{{^DefnLifeInsIsGPT}}
+    the cash value accumulation test.
+{{/DefnLifeInsIsGPT}}
+</p>
+
+<p>
+The initial 7-pay premium limit is ${{InitSevenPayPrem}}.
+As illustrated, this policy
+{{#IsMec}}
+    fails
+{{/IsMec}}
+{{^IsMec}}
+    passes
+{{/IsMec}}
+the seven-pay test defined in Section 7702A
+of the Internal Revenue Code and therefore
+{{#IsMec}}
+    becomes a Modified Endowment Contract (MEC)
+    in policy year {{MecYearPlus1}}.
+{{/IsMec}}
+{{^IsMec}}
+    is not a Modified Endowment Contract (MEC). Subsequent changes
+    to the policy, including but not limited to increases
+    and decreases in premiums or benefits, may cause the policy
+    to be retested and may result in the policy becoming a MEC.
+{{/IsMec}}
+</p>
+
+<p>
+If a policy is a MEC, withdrawals and policy loans are taxed to
+the extent of any gain in the policy, and an additional penalty
+tax may apply to the taxable portion of the distribution.
+</p>
+
+<p>
+<b>
+The information provided in this illustration is not written or
+intended as tax or legal advice and may not be relied on for
+purposes of avoiding any federal tax penalties.
+{{InsCoShortName}}, its employees
+and representatives are not authorized to give tax or legal advice.
+Individuals are encouraged to seek advice from their own tax or
+legal counsel.
+</b>
+</p>
+
+{{^IsInforce}}
+
+<br>
+<br>
+<br>
+
+<p align="center">
+<font size="+2"><b>Certification Statements</b></font>
+</p>
+
+<p>
+<br>
+<br>
+POLICY OWNER / APPLICANT<br>
+
+I have received a copy of this illustration, and I understand
+that any non-guaranteed charges illustrated are subject
+to change and could be either higher or lower. Additionally,
+I have been informed by my agent that these values
+are not guaranteed.
+</p>
+
+<br>
+<br>
+<br>
+<br>
+
+<table cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td>
+            <u>{{Space64}}{{Space32}}</u>
+        </td>
+        <td>
+            {{Space32}}
+        </td>
+        <td>
+            <u>{{Space32}}</u>
+        </td>
+    </tr>
+    <tr>
+        <td>
+            POLICY OWNER OR APPLICANT SIGNATURE
+        </td>
+        <td>
+            {{Space32}}
+        </td>
+        <td>
+            DATE
+        </td>
+    </tr>
+</table>
+
+<br>
+<br>
+<br>
+<br>
+<br>
+
+<p>
+AGENT / AUTHORIZED REPRESENTATIVE<br>
+
+I certify that this illustration has been presented
+to the applicant, and that I have explained that any
+non-guaranteed charges illustrated are subject to change. I have
+made no statements that are inconsistent with the illustration.
+</p>
+
+<br>
+<br>
+<br>
+<br>
+
+<table cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td>
+            <u>{{Space64}}{{Space32}}</u>
+        </td>
+        <td>
+            {{Space32}}
+        </td>
+        <td>
+            <u>{{Space32}}</u>
+        </td>
+    </tr>
+    <tr>
+        <td>
+            AGENT OR AUTHORIZED REPRESENTATIVE
+        </td>
+        <td>
+            {{Space32}}
+        </td>
+        <td>
+            DATE
+        </td>
+    </tr>
+</table>
+
+{{/IsInforce}}
+
+</font>
diff --git a/reg_d_indiv_supp_report.mst b/reg_d_indiv_supp_report.mst
new file mode 100644
index 0000000..81ad63c
--- /dev/null
+++ b/reg_d_indiv_supp_report.mst
@@ -0,0 +1,32 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_d_indiv_header}}
+
+<font size="-1">
+
+<p align="center">{{SupplementalReportTitle}}</p>
+
+</font>
+
+{{>dollar_units}}
+
+{{! The supplemental report table itself is generated from C++ code }}
diff --git a/reg_footer.mst b/reg_footer.mst
new file mode 100644
index 0000000..f4d0dce
--- /dev/null
+++ b/reg_footer.mst
@@ -0,0 +1,40 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+<table width="100%" cellspacing="0" cellpadding="0">
+    <tr>
+        <td colspan="3">&nbsp;</td>
+    </tr>
+    <tr>
+        <td>Date Prepared: {{date_prepared}}</td>
+        <td align="center">{{page_number}}</td>
+        <td align="right">{{InsCoName}}</td>
+    </tr>
+    <tr>
+        <td>System Version: {{LmiVersion}}</td>
+        <td>&nbsp;</td>
+        <td align="right">{{>imprimatur}}</td>
+    </tr>
+</table>
+
+</font>
diff --git a/reg_footer_disclaimer.mst b/reg_footer_disclaimer.mst
new file mode 100644
index 0000000..f97e6da
--- /dev/null
+++ b/reg_footer_disclaimer.mst
@@ -0,0 +1,32 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<font size="-2">
+
+{{^IsInforce}}
+    {{NonGuaranteedFootnote}}
+{{/IsInforce}}
+{{#IsInforce}}
+    {{InforceNonGuaranteedFootnote0}}<br>
+    <b>{{InforceNonGuaranteedFootnote3}}</b>
+{{/IsInforce}}
+
+</font>
diff --git a/reg_header.mst b/reg_header.mst
new file mode 100644
index 0000000..edd29de
--- /dev/null
+++ b/reg_header.mst
@@ -0,0 +1,131 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<font size=-1>
+<p align="center">
+    {{#IsInforce}}
+        LIFE INSURANCE IN FORCE BASIC ILLUSTRATION
+    {{/IsInforce}}
+    {{^IsInforce}}
+        LIFE INSURANCE BASIC ILLUSTRATION
+    {{/IsInforce}}
+
+    <br>{{InsCoName}}
+    <br>Presented by: {{ProducerName}}
+    <br>{{ProducerStreet}}
+    {{#HasProducerCity}}
+        <br>{{ProducerCity}}
+    {{/HasProducerCity}}
+</p>
+<p>
+    &nbsp;
+</p>
+<table width="100%" cellspacing="0" cellpadding="0" valign="top">
+    <tr>
+        <td width="60%">
+            Prepared for:<br>
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Group Name: 
{{CorpNameAbbrev50}}<br>
+            {{#Composite}}
+                &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Composite 
Illustration<br>
+            {{/Composite}}
+            {{^Composite}}
+                &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Insured: 
{{Insured1Abbrev50}}<br>
+                &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Age: 
{{Age}}<br>
+            {{/Composite}}
+            Product: {{PolicyForm}}&nbsp;{{PolicyMktgName}}<br>
+            {{#ModifiedSinglePremium}}
+                Modified Single Premium Adjustable Life Insurance Policy
+            {{/ModifiedSinglePremium}}
+            {{^ModifiedSinglePremium}}
+                {{PolicyLegalName}}
+            {{/ModifiedSinglePremium}}
+            <br>
+
+            {{^IsInforce}}
+                {{^SinglePremium}}
+                    Initial Premium:
+                {{/SinglePremium}}
+                {{#SinglePremium}}
+                    Single Premium:
+                {{/SinglePremium}}
+                &nbsp;${{InitPrem}}
+            {{/IsInforce}}
+            <br>
+
+            {{^Composite}}
+                Initial Death Benefit Option: {{InitDBOpt}}<br>
+            {{/Composite}}
+            {{>contract_numbers}}
+        </td>
+        <td width="40%">
+            Initial {{#HasTerm}}Total{{/HasTerm}}
+            Selected Face Amount: ${{InitTotalSA}}<br>
+
+            {{#HasTerm}}
+                Initial Base Face Amount: ${{InitBaseSpecAmt}}<br>
+                Initial Term Face Amount: ${{InitTermSpecAmt}}<br>
+            {{/HasTerm}}
+
+            Guaranteed Crediting Rate: {{InitAnnGenAcctInt_Guaranteed}}<br>
+
+            Current Illustrated Crediting Rate:
+            {{#InforceYear}}
+                {{UltimateInterestRate}}
+            {{/InforceYear}}
+            {{^InforceYear}}
+                {{InitAnnGenAcctInt_Current}}
+            {{/InforceYear}}
+            <br>
+
+            {{#SinglePremium}}
+                {{#InforceYearLE4}}
+                    Ultimate Illustrated Crediting Rate:
+                    {{#ModifiedSinglePremium0}}
+                        {{AnnGAIntRate_Current[11]}}
+                    {{/ModifiedSinglePremium0}}
+                    {{^ModifiedSinglePremium0}}
+                        {{AnnGAIntRate_Current[6]}}
+                    {{/ModifiedSinglePremium0}}
+                    <br>
+                {{/InforceYearLE4}}
+            {{/SinglePremium}}
+
+            {{^Composite}}
+                Underwriting Type:
+                {{#UWTypeIsMedical}}
+                    Fully underwritten
+                {{/UWTypeIsMedical}}
+                {{^UWTypeIsMedical}}
+                    {{UWType}}
+                {{/UWTypeIsMedical}}
+                <br>
+            {{/Composite}}
+
+            Rate Classification: {{UWClass}}, {{Smoker}}, {{Gender}}<br>
+            {{#UWClassIsRated}}
+            &nbsp;&nbsp;&nbsp;Table Rating: {{SubstandardTable}}<br>
+            {{/UWClassIsRated}}
+        </td>
+    </tr>
+</table>
+</font>
diff --git a/reg_narr_summary.mst b/reg_narr_summary.mst
new file mode 100644
index 0000000..0752d6b
--- /dev/null
+++ b/reg_narr_summary.mst
@@ -0,0 +1,203 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_header}}
+
+<p align="center">NARRATIVE SUMMARY</p>
+
+<font size="-1">
+
+<p>
+{{^SinglePremium}}
+    {{PolicyMktgName}} is a
+    {{#GroupExperienceRating}}group{{/GroupExperienceRating}}
+    {{#GroupCarveout}}group{{/GroupCarveout}}
+    flexible premium adjustable life insurance contract.
+    {{#GroupExperienceRating}}
+    It is a no-load policy and is intended for large case sales.
+    It is primarily marketed to financial institutions
+    to fund certain corporate liabilities.
+    {{/GroupExperienceRating}}
+    It features accumulating account values, adjustable benefits,
+    and flexible premiums.
+{{/SinglePremium}}
+{{#SinglePremium}}
+    {{#ModifiedSinglePremiumOrModifiedSinglePremium0}}
+        {{PolicyMktgName}}
+        is a modified single premium adjustable life
+        insurance contract. It features accumulating
+        account values, adjustable benefits, and single premium.
+    {{/ModifiedSinglePremiumOrModifiedSinglePremium0}}
+    {{^ModifiedSinglePremiumOrModifiedSinglePremium0}}
+        {{PolicyMktgName}}
+        is a single premium adjustable life insurance contract.
+        It features accumulating account values,
+        adjustable benefits, and single premium.
+    {{/ModifiedSinglePremiumOrModifiedSinglePremium0}}
+{{/SinglePremium}}
+</p>
+
+{{^IsInforce}}
+    <p>
+    Coverage may be available on a Guaranteed Standard Issue basis.
+    All proposals are based on case characteristics and must
+    be approved by the {{InsCoShortName}}
+    Home Office. For details regarding underwriting
+    and coverage limitations refer to your offer letter
+    or contact your {{InsCoShortName}} representative.
+    </p>
+{{/IsInforce}}
+
+<p>
+{{AvName}} &nbsp; {{MonthlyChargesPaymentFootnote}}
+</p>
+
+<p>
+{{^SinglePremium}}
+    Premiums are assumed to be paid on {{ErModeLCWithArticle}}
+    basis and received at the beginning of the contract year.
+{{/SinglePremium}}
+{{#SinglePremium}}
+    The single premium is assumed to be paid at the beginning
+    of the contract year.
+{{/SinglePremium}}
+
+{{AvName}} Values, {{CsvName}} Values,
+and death benefits are illustrated as of the end
+of the contract year. The method we use to allocate
+overhead expenses is the fully allocated expense method.
+</p>
+
+{{^SinglePremium}}
+    <p>
+    In order to guarantee coverage to age
+    {{EndtAge}}, {{ErModeLCWithArticle}} premium
+    {{#HasGuarPrem}}
+    of ${{GuarPrem}} must be paid.
+    {{/HasGuarPrem}}
+    {{^HasGuarPrem}}
+    is defined.
+    {{/HasGuarPrem}}
+    This amount is based on the guaranteed monthly charges
+    and the guaranteed interest crediting rate.
+    {{#DefnLifeInsIsGPT}}
+    This premium may be in excess of the maximum premium allowable
+    in order to qualify this policy as life insurance.
+    {{/DefnLifeInsIsGPT}}
+    </p>
+{{/SinglePremium}}
+
+<p>
+Loaned amounts of the {{AvName}}
+Value will be credited a rate equal to the loan interest rate less
+a spread, guaranteed not to exceed
+{{#GroupCarveout}}
+1.25%.
+{{/GroupCarveout}}
+{{^GroupCarveout}}
+3.00%.
+{{/GroupCarveout}}
+</p>
+
+{{#HasTerm}}
+    <p>
+    The term rider provides the option to purchase monthly
+    term insurance on the life of the insured. The term rider
+    selected face amount supplements the selected face amount
+    of the contract. If the term rider is attached, the policy
+    to which it is attached may have a lower annual cutoff premium
+    and, as a result, the lower overall sales loads paid may be
+    lower than a contract having the same total face amount,
+    but with no term rider.
+    {{#NoLapse}}
+      Also, the lapse protection feature of the contract's
+      {{NoLapseProvisionName}}
+      does not apply to the term rider's selected face amount.
+    {{/NoLapse}}
+    </p>
+{{/HasTerm}}
+
+{{#HasWP}}
+    <p>
+    The Waiver of Monthly Charges Rider provides for waiver
+    of monthly charges in the event of the disability
+    of the insured that begins before attained age 65
+    and continues for at least 6 months, as described in the rider.
+    An additional charge is associated with this rider. Please refer
+    to your contract for specific provisions and a detailed schedule
+    of charges.
+    </p>
+{{/HasWP}}
+
+{{#HasADD}}
+    <p>
+    The Accidental Death benefit provides an additional benefit
+    if death is due to accident. An additional charge is associated
+    with this rider. Please refer to your contract
+    for specific provisions and a detailed schedule of charges.
+    </p>
+{{/HasADD}}
+
+<p>
+The definition of life insurance for this contract is the
+{{#DefnLifeInsIsGPT}}
+    guideline premium test. The guideline single premium
+    is ${{InitGSP}}
+    and the guideline level premium
+    is ${{InitGLP}}
+{{/DefnLifeInsIsGPT}}
+{{^DefnLifeInsIsGPT}}
+    cash value accumulation test.
+{{/DefnLifeInsIsGPT}}
+</p>
+
+<p>
+This is an illustration only. An illustration is not intended
+to predict actual performance. Interest rates
+and values set forth in the illustration are not guaranteed.
+</p>
+
+<p>
+{{^StateIsTexas}}
+    This illustration assumes that the currently illustrated
+    non-guaranteed elements will continue unchanged
+    for all years shown. This is not likely to occur
+    and actual results may be more or less favorable than shown.
+    The non-guaranteed benefits and values are not guaranteed
+    and are based on assumptions such as interest credited
+    and current monthly charges, which are subject to change by
+    {{InsCoName}}.
+{{/StateIsTexas}}
+{{#StateIsTexas}}
+    This illustration is based on both non-guaranteed
+    and guaranteed assumptions. Non-guaranteed assumptions
+    include interest rates and monthly charges.
+    This illustration assumes that the currently illustrated
+    non-guaranteed elements will continue unchanged
+    for all years shown. This is not likely to occur
+    and actual results may be more or less favorable than shown.
+    Factors that may affect future policy performance include
+    the company's expectations for future mortality, investments,
+    persistency, profits and expenses.
+{{/StateIsTexas}}
+</p>
+
+</font>
diff --git a/reg_narr_summary2.mst b/reg_narr_summary2.mst
new file mode 100644
index 0000000..4e667dd
--- /dev/null
+++ b/reg_narr_summary2.mst
@@ -0,0 +1,258 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+<!-- No header for this page, just the logo -->
+<scaled_image inv_factor="0.36" src="company_logo.png"></scaled_image>
+
+<p align="center">NARRATIVE SUMMARY (Continued)</p>
+
+<br></br>
+
+<font size="-1">
+
+{{#HasSalesLoadRefund}}
+    <p>
+    Sales Load Refund: We will refund a portion of the sales load
+    to you, as part of your {{CsvName}}
+    Value, if you surrender your contract within the first two
+    policy years. In policy year 1, we will refund
+    {{SalesLoadRefundRate0}}
+    of the first contract year sales load collected
+    and in contract year 2, we will refund
+    {{SalesLoadRefundRate1}}
+    of the first contract year sales load collected.
+    </p>
+{{/HasSalesLoadRefund}}
+
+{{#NoLapse}}
+    <p>
+    {{NoLapseProvisionName}}:
+    The contract will remain in force after the first premium
+    has been paid, even if there is insufficient
+    {{AvName}} Value
+    to cover the monthly charges provided that the insured
+    is not in a substandard rating class and the policy debt
+    does not exceed {{AvName}} Value.
+    </p>
+{{/NoLapse}}
+
+<p>
+{{GuarMortalityFootnote}}
+</p>
+
+<p>
+This illustration assumes death of the insured at age {{EndtAge}}.
+</p>
+
+<p>
+The loan interest rate is fixed at {{InitAnnLoanDueRate}} per year.
+</p>
+
+<p>
+The state of issue is {{StatePostalAbbrev}}.
+</p>
+
+<p>
+This illustration assumes an initial Selected Face Amount of
+${{InitBaseSpecAmt}}. Selected Face
+Amount reductions assumed in this illustration (if any) are shown
+in the Tabular Detail.
+</p>
+
+{{#IsInforce}}
+    <p>
+    This illustration assumes a beginning account value of
+    ${{InforceUnloanedAV}} as of the date of this illustration.
+    </p>
+
+    <p>
+    This illustration assumes a beginning cost basis of
+    ${{InforceTaxBasis}} as
+    of the date of this illustration; the actual cost basis
+    may be higher or lower. Consult the Home Office for cost
+    basis information.
+    </p>
+{{/IsInforce}}
+
+{{#Composite}}
+    <p>
+    Please see the attached census, listing the face amounts,
+    underwriting classes and issue ages for individual participants.
+    </p>
+{{/Composite}}
+
+{{#StateIsCarolina}}
+    <p>
+    In the states of North Carolina and South Carolina,
+    Guaranteed Issue Underwriting is referred
+    to as "Limited Underwriting" and Simplified
+    Issue Underwriting is referred to as "Simplified Underwriting".
+    </p>
+{{/StateIsCarolina}}
+
+{{#StateIsMaryland}}
+    <p>
+    In the state of Maryland, Guaranteed Issue Underwriting
+    is referred to as "Nonstandard Limited Underwriting"
+    and Simplified Issue Underwriting is referred to as
+    "Nonstandard Simplified Underwriting".
+    </p>
+{{/StateIsMaryland}}
+
+{{#GroupExperienceRating}}
+    <p>
+    We may assess a Market Value Adjustment upon a surrender
+    of the certificate when the surrender proceeds are intended
+    to be applied to an insurance policy issued by an insurer
+    unaffiliated with
+    {{InsCoShortName}}
+    with an intent to qualify the exchange as a tax free exchange
+    under IRC section 1035.
+    </p>
+
+    {{^UseExperienceRating}}
+        <p>
+        This illustration does not reflect experience rating.
+        </p>
+    {{/UseExperienceRating}}
+
+    <p>
+    The guaranteed values reflect the maximum charges permitted
+    by the contract, which may include an Experience Rating
+    Risk Charge.
+    </p>
+
+    <p>
+    No Experience Rating Risk Charge or a distribution
+    of an Experience Rating Reserve Credit is reflected
+    in the current, non-guaranteed values. Actual charges
+    and credits will be based on the actual experience of the group.
+    </p>
+{{/GroupExperienceRating}}
+
+{{#Has1035ExchCharge}}
+    <p>
+    {{^SinglePremium}}
+        Upon surrender of this policy, where the surrender
+        proceeds are intended to be applied to an insurance policy
+        or certificate issued in conjunction with an intent
+        to qualify the exchange as a tax free exchange
+        under Section 1035 of the Internal Revenue Code,
+        we may assess an Exchange Charge. The Exchange Charge
+        is the greater of the Market Value Adjustment Charge
+        and the Percentage of Premium Charge. In the states
+        of Florida or Indiana, the Exchange charge
+        (referred to as Assignment Charge in Florida)
+        will be the Percentage of Premium Charge only.
+        The Exchange Charge will potentially reduce
+        the surrender proceeds, but will never increase
+        the surrender proceeds. Please refer to your policy
+        for details.
+    {{/SinglePremium}}
+    {{#SinglePremium}}
+        Upon surrender of this policy, where the surrender proceeds
+        are intended to be applied to an insurance policy
+        or certificate issued in conjunction with an intent
+        to qualify the exchange as a tax free exchange
+        under Section 1035 of the Internal Revenue Code (1035
+        Exchange), we may assess an Exchange Charge. The Exchange
+        Charge will potentially reduce the surrender proceeds,
+        but will never increase the surrender proceeds.
+        Please refer to your policy for details.
+    {{/SinglePremium}}
+    </p>
+{{/Has1035ExchCharge}}
+
+{{#HasSpouseRider}}
+    <p>
+    The ${{SpouseRiderAmount}} Spouse
+    rider provides term life insurance on the spouse
+    (issue age {{SpouseIssueAge}})
+    for a limited duration, for an extra charge.
+    Please refer to your contract for specific provisions
+    and a detailed schedule of charges.
+    </p>
+{{/HasSpouseRider}}
+
+{{#HasChildRider}}
+    <p>
+    The ${{ChildRiderAmount}} Child
+    rider provides term life insurance on the insured's children
+    for a limited duration, for an extra charge. Please refer
+    to your contract for specific provisions
+    and a detailed schedule of charges.
+    </p>
+{{/HasChildRider}}
+
+<p>{{SurrenderFootnote}}</p>
+
+<p>{{FundRateFootnote}}</p>
+
+<p>&nbsp;&nbsp;{{FundRateFootnote0}}</p>
+
+<p>&nbsp;&nbsp;{{FundRateFootnote1}}</p>
+
+</font>
+
+<p align="center">IMPORTANT TAX DISCLOSURE</p>
+
+<font size="-1">
+
+<p>
+As illustrated, this contract
+{{#IsMec}}
+  becomes
+{{/IsMec}}
+{{^IsMec}}
+  would not become
+{{/IsMec}}
+a Modified Endowment Contract (MEC)
+under the Internal Revenue Code
+{{#IsMec}}
+in year {{MecYearPlus1}}
+{{/IsMec}}
+To the extent of gain in the contract, loans, distributions
+and withdrawals from a MEC are subject to income tax
+and may also trigger a penalty tax.
+</p>
+
+{{^SinglePremium}}
+    {{^IsInforce}}
+        <p>
+        The initial 7-pay premium limit is ${{InitSevenPayPrem}}.
+        </p>
+    {{/IsInforce}}
+{{/SinglePremium}}
+
+<p>
+<b>
+The information contained in this illustration is not written
+or intended as tax or legal advice, and may not be relied upon
+for purposes of avoiding any federal tax penalties.
+Neither {{InsCoShortName}} nor any
+of its employees or representatives are authorized to give tax
+or legal advice. For more information pertaining
+to the tax consequences of purchasing or owning this policy,
+consult with your own independent tax or legal counsel.
+</b>
+</p>
+
+</font>
diff --git a/reg_numeric_summary.mst b/reg_numeric_summary.mst
new file mode 100644
index 0000000..4e953b7
--- /dev/null
+++ b/reg_numeric_summary.mst
@@ -0,0 +1,203 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_header}}
+
+<p align="center">Numeric Summary</p>
+
+{{>dollar_units}}
+
+<font size="-1">
+
+<br></br>
+<br></br>
+
+<numeric_summary_table></numeric_summary_table>
+
+<br></br>
+<br></br>
+
+<!--
+This is a workaround for what looks like a wxHTML bug, without explicitly
+resetting the alignment here, the next paragraph would be centered, somehow
+keeping the center alignment of the last "p" tag above.
+-->
+<p align="left"></p>
+
+{{#Composite}}
+<p>
+    The year of policy lapse on a guaranteed, midpoint
+    and current basis is not depicted in the above table of values
+    for this composite illustration because it is not applicable
+    on a case basis.
+</p>
+{{/Composite}}
+
+{{#LapseYear_Guaranteed_LT_MaxDuration}}
+<p>
+    Additional premium will be required
+    in year {{LapseYear_Guaranteed_Plus1}}
+    or contract will lapse based on guaranteed monthly charges
+    and interest rate.
+</p>
+{{/LapseYear_Guaranteed_LT_MaxDuration}}
+
+{{#LapseYear_Midpoint_LT_MaxDuration}}
+<p>
+    Additional premium will be required
+    in year {{LapseYear_Midpoint_Plus1}}
+    or contract will lapse based on midpoint monthly charges
+    and interest rate.
+</p>
+{{/LapseYear_Midpoint_LT_MaxDuration}}
+
+{{#LapseYear_Current_LT_MaxDuration}}
+<p>
+    Additional premium will be required
+    in year {{LapseYear_Current_Plus1}}
+    or contract will lapse based on current monthly charges
+    and interest rate.
+</p>
+{{/LapseYear_Current_LT_MaxDuration}}
+
+{{#IsMec}}
+<p>
+            IMPORTANT TAX DISCLOSURE: This is a Modified Endowment Contract.
+            Please refer to the Narrative Summary for additional information.
+</p>
+{{/IsMec}}
+
+<br>
+<br>
+
+<p align="center">Certification Statements</p>
+    <p>
+      CONTRACT OWNER / APPLICANT
+    </p>
+    {{#HasInterestDisclaimer}}
+      <p>
+        I understand that at the present time higher current interest rates
+        are credited for policies with case premiums in the amount
+        of {{InterestDisclaimer}}
+      </p>
+    {{/HasInterestDisclaimer}}
+    <p>
+    {{#StateIsIllinoisOrTexas}}
+        {{#StateIsIllinois}}
+            I have received a copy of this illustration and understand
+            that this illustration assumes that the currently illustrated
+            non-guaranteed elements will continue unchanged
+            for all years shown. This is not likely to occur,
+            and actual results may be more or less favorable than those shown.
+        {{/StateIsIllinois}}
+        {{#StateIsTexas}}
+              A copy of this illustration has been provided
+              to the Applicant/Policyowner.
+        {{/StateIsTexas}}
+    {{/StateIsIllinoisOrTexas}}
+    {{^StateIsIllinoisOrTexas}}
+          I have received a copy of this illustration, and I understand
+          that any non-guaranteed elements illustrated are subject
+          to change and could be either higher or lower. Additionally,
+          I have been informed by my agent that these values
+          are not guaranteed.
+    {{/StateIsIllinoisOrTexas}}
+    </p>
+    <br>
+    <br>
+    <table cellspacing="0" cellpadding="0" valign="top">
+        <tr>
+            <td>
+                <u>{{Space64}}{{Space32}}</u>
+            </td>
+            <td>
+                {{Space32}}
+            </td>
+            <td>
+                <u>{{Space32}}</u>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                CONTRACT OWNER OR APPLICANT SIGNATURE
+            </td>
+            <td>
+                {{Space32}}
+            </td>
+            <td>
+                DATE
+            </td>
+        </tr>
+    </table>
+    <br>
+    <br>
+    <p>
+      AGENT / AUTHORIZED REPRESENTATIVE
+    </p>
+    <p>
+        {{#StateIsIllinoisOrTexas}}
+            {{#StateIsIllinois}}
+                I have informed the applicant or policyowner
+                that this illustration assumes that the currently illustrated
+                non-guaranteed elements will continue unchanged
+                for all years shown. This is not likely to occur,
+                and actual results may be more or less favorable than those 
shown.
+            {{/StateIsIllinois}}
+            {{#StateIsTexas}}
+                A copy of this illustration has been provided
+                to the Applicant/Policyowner.
+            {{/StateIsTexas}}
+        {{/StateIsIllinoisOrTexas}}
+        {{^StateIsIllinoisOrTexas}}
+            I certify that this illustration has been presented
+            to the applicant, and that I have explained
+            that any non-guaranteed elements illustrated
+            are subject to change. I have made no statements
+            that are inconsistent with the illustration.
+        {{/StateIsIllinoisOrTexas}}
+    </p>
+    <br>
+    <br>
+    <table cellspacing="0" cellpadding="0" valign="top">
+        <tr>
+            <td>
+                <u>{{Space64}}{{Space32}}</u>
+            </td>
+            <td>
+                {{Space32}}
+            </td>
+            <td>
+                <u>{{Space32}}</u>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                AGENT OR AUTHORIZED REPRESENTATIVE
+            </td>
+            <td>
+                {{Space32}}
+            </td>
+            <td>
+                DATE
+            </td>
+        </tr>
+    </table>
+</font>
diff --git a/reg_supp_report.mst b/reg_supp_report.mst
new file mode 100644
index 0000000..1352417
--- /dev/null
+++ b/reg_supp_report.mst
@@ -0,0 +1,28 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_header}}
+
+<p align="center">{{SupplementalReportTitle}}</p>
+
+{{>dollar_units}}
+
+{{! The supplemental report table itself is generated from C++ code }}
diff --git a/reg_tabular_details.mst b/reg_tabular_details.mst
new file mode 100644
index 0000000..c955c08
--- /dev/null
+++ b/reg_tabular_details.mst
@@ -0,0 +1,29 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_header}}
+
+<p align="center">Tabular Detail</p>
+
+{{>dollar_units}}
+
+<br></br>
+<br></br>
diff --git a/reg_tabular_details2.mst b/reg_tabular_details2.mst
new file mode 100644
index 0000000..3dbf744
--- /dev/null
+++ b/reg_tabular_details2.mst
@@ -0,0 +1,27 @@
+{{!
+    Copyright (C) 2017, 2018 Gregory W. Chicares.
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2 as
+    published by the Free Software Foundation.
+
+    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, write to the Free Software Foundation,
+    Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+    http://savannah.nongnu.org/projects/lmi
+    email: <address@hidden>
+    snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+}}
+
+{{>reg_header}}
+
+<p align="center">Tabular Detail, continued</p>
+
+<br></br>
+<br></br>
diff --git a/test_coding_rules.cpp b/test_coding_rules.cpp
index ce98024..81974e8 100644
--- a/test_coding_rules.cpp
+++ b/test_coding_rules.cpp
@@ -49,6 +49,7 @@ std::string my_taboo_indulgence();       // See 
'my_test_coding_rules.cpp'.
 
 std::map<std::string, bool> my_taboos(); // See 'my_test_coding_rules.cpp'.
 
+// Sort these enumerators alphabetically.
 enum enum_phylum
     {e_no_phylum  = 0
     ,e_binary     = 1 <<  0
@@ -63,14 +64,15 @@ enum enum_phylum
     ,e_log        = 1 <<  9
     ,e_make       = 1 << 10
     ,e_md5        = 1 << 11
-    ,e_patch      = 1 << 12
-    ,e_rates      = 1 << 13
-    ,e_relax_ng   = 1 << 14
-    ,e_script     = 1 << 15
-    ,e_synopsis   = 1 << 16
-    ,e_touchstone = 1 << 17
-    ,e_xml_input  = 1 << 18
-    ,e_xml_other  = 1 << 19
+    ,e_mustache   = 1 << 12
+    ,e_patch      = 1 << 13
+    ,e_rates      = 1 << 14
+    ,e_relax_ng   = 1 << 15
+    ,e_script     = 1 << 16
+    ,e_synopsis   = 1 << 17
+    ,e_touchstone = 1 << 18
+    ,e_xml_input  = 1 << 19
+    ,e_xml_other  = 1 << 20
     };
 
 enum enum_kingdom
@@ -171,6 +173,7 @@ file::file(std::string const& file_path)
         : ".html"       == extension() ? e_html
         : ".make"       == extension() ? e_make
         : ".md5sums"    == extension() ? e_md5
+        : ".mst"        == extension() ? e_mustache
         : ".patch"      == extension() ? e_patch
         : ".rates"      == extension() ? e_rates
         : ".rnc"        == extension() ? e_relax_ng
@@ -669,6 +672,7 @@ void check_defect_markers(file const& f)
             &&  "IHS "         != z[1]
             &&  "INELEGANT "   != z[1]
             &&  "INPUT "       != z[1]
+            &&  "PDF "         != z[1]
             &&  "PORT "        != z[1]
             &&  "SOMEDAY "     != z[1]
             &&  "TAXATION "    != z[1]
diff --git a/workhorse.make b/workhorse.make
index 55c446d..3ddb6e2 100644
--- a/workhorse.make
+++ b/workhorse.make
@@ -896,7 +896,7 @@ test_dir       := $(exec_prefix)/test
 touchstone_dir := $(exec_prefix)/touchstone
 
 data_files := \
-  $(wildcard $(addprefix $(srcdir)/,*.ico *.png *.xml *.xrc *.xsd *.xsl)) \
+  $(wildcard $(addprefix $(srcdir)/,*.ico *.mst *.png *.xml *.xrc *.xsd 
*.xsl)) \
 
 help_files := \
   $(wildcard $(addprefix $(srcdir)/,*.html)) \
diff --git a/wx_table_generator.cpp b/wx_table_generator.cpp
index 237fbf2..8ebe75d 100644
--- a/wx_table_generator.cpp
+++ b/wx_table_generator.cpp
@@ -53,6 +53,7 @@ wx_table_generator::wx_table_generator
     ,total_width_(total_width)
     ,char_height_(dc_.GetCharHeight())
     ,row_height_((4 * char_height_ + 2) / 3) // Arbitrarily use 1.333 line 
spacing.
+    ,column_margin_(dc_.GetTextExtent("M").x)
     ,has_column_widths_(false)
     ,max_header_lines_(1)
 {
@@ -64,6 +65,18 @@ wx_table_generator::wx_table_generator
     dc_.SetPen(pen);
 }
 
+void wx_table_generator::use_condensed_style()
+{
+    row_height_ = char_height_;
+    draw_separators_ = false;
+    use_bold_headers_ = false;
+}
+
+void wx_table_generator::align_right()
+{
+    align_right_ = true;
+}
+
 void wx_table_generator::add_column
     (std::string const& header
     ,std::string const& widest_text
@@ -77,7 +90,11 @@ void wx_table_generator::add_column
         }
     else
         {
-        wxDCFontChanger set_header_font(dc_, get_header_font());
+        wxDCFontChanger set_header_font(dc_);
+        if(use_bold_headers_)
+            {
+            set_header_font.Set(get_header_font());
+            }
 
         // Set width to the special value of 0 for the variable width columns.
         width = widest_text.empty() ? 0 : dc_.GetTextExtent(widest_text).x;
@@ -94,8 +111,7 @@ void wx_table_generator::add_column
             {
             increase_to_if_smaller(width, 
dc_.GetMultiLineTextExtent(header).x);
 
-            // Add roughly 1 em margins on both sides.
-            width += dc_.GetTextExtent("MM").x;
+            width += 2*column_margin_;
             }
         }
 
@@ -157,6 +173,8 @@ void 
wx_table_generator::do_compute_column_widths_if_necessary()
         return;
         }
 
+    has_column_widths_ = true;
+
     int num_expand = 0;
     int total_fixed = 0;
 
@@ -179,7 +197,43 @@ void 
wx_table_generator::do_compute_column_widths_if_necessary()
 
     if(total_width_ < total_fixed)
         {
-        warning() << "Not enough space for all fixed columns." << LMI_FLUSH;
+        auto const overflow = total_fixed - total_width_;
+
+        // If we have only fixed columns, try to make them fit by decreasing
+        // the margins around them if this can help, assuming that we can
+        // reduce them by up to half if really needed.
+        if(!num_expand)
+            {
+            int const num_columns = columns_.size();
+            auto const overflow_per_column =
+                (overflow + num_columns - 1)/num_columns;
+            if(overflow_per_column <= column_margin_)
+                {
+                for(auto& i : columns_)
+                    {
+                    if(i.is_hidden())
+                        {
+                        continue;
+                        }
+
+                    i.width_ -= overflow_per_column;
+                    }
+
+                column_margin_ -= (overflow_per_column + 1)/2;
+
+                // We condensed the columns enough to make them fit, so no need
+                // for the warning and we don't have any expanding columns, so
+                // we're done.
+                return;
+                }
+            }
+
+        warning()
+            << "Not enough space for all fixed columns: "
+            << overflow
+            << " more pixels needed."
+            << LMI_FLUSH
+            ;
         return;
         }
 
@@ -201,8 +255,6 @@ void 
wx_table_generator::do_compute_column_widths_if_necessary()
                 }
             }
         }
-
-    has_column_widths_ = true;
 }
 
 void wx_table_generator::do_output_values
@@ -216,7 +268,10 @@ void wx_table_generator::do_output_values
     int const y_text = y + char_height_;
     y += row_height_;
 
-    do_output_vert_separator(x, y_top, y);
+    if(draw_separators_)
+        {
+        do_output_vert_separator(x, y_top, y);
+        }
 
     std::size_t const num_columns = columns_.size();
     for(std::size_t col = 0; col < num_columns; ++col)
@@ -233,21 +288,31 @@ void wx_table_generator::do_output_values
         if(!s.empty())
             {
             int x_text = x;
-            if(ci.is_centered_)
+
+            if(align_right_)
                 {
-                // Centre the text for the columns configured to do it.
-                x_text += (width - dc_.GetTextExtent(s).x) / 2;
+                x_text += width - dc_.GetTextExtent(s).x;
                 }
             else
                 {
-                // Otherwise just offset it by ~1 em.
-                x_text += dc_.GetTextExtent("M").x;
-                }
+                if(ci.is_centered_)
+                    {
+                    // Centre the text for the columns configured to do it.
+                    x_text += (width - dc_.GetTextExtent(s).x) / 2;
+                    }
+                else
+                    {
+                    x_text += column_margin_;
+                    }
+            }
 
             dc_.DrawText(s, x_text, y_text);
             }
         x += width;
-        do_output_vert_separator(x, y_top, y);
+        if(draw_separators_)
+            {
+            do_output_vert_separator(x, y_top, y);
+            }
         }
 }
 
@@ -269,8 +334,17 @@ void wx_table_generator::output_horz_separator
     (std::size_t begin_column
     ,std::size_t end_column
     ,int y
+    ,enum_output_mode output_mode
     )
 {
+    switch(output_mode)
+        {
+        case e_output_normal:
+            break;
+        case e_output_measure_only:
+            return;
+        }
+
     LMI_ASSERT(begin_column < end_column);
     LMI_ASSERT(end_column <= columns_.size());
 
@@ -287,11 +361,24 @@ void wx_table_generator::output_horz_separator
     do_output_horz_separator(x1, x2, y);
 }
 
-void wx_table_generator::output_header(int* pos_y)
+void wx_table_generator::output_header(int* pos_y, enum_output_mode 
output_mode)
 {
+    switch(output_mode)
+        {
+        case e_output_normal:
+            break;
+        case e_output_measure_only:
+            *pos_y += max_header_lines_ * row_height_;
+            return;
+        }
+
     do_compute_column_widths_if_necessary();
 
-    wxDCFontChanger set_header_font(dc_, get_header_font());
+    wxDCFontChanger set_header_font(dc_);
+    if(use_bold_headers_)
+        {
+        set_header_font.Set(get_header_font());
+        }
 
     // Split headers in single lines and fill up the entire columns*lines 2D
     // matrix, using empty strings for the headers with less than the maximal
@@ -328,9 +415,47 @@ void wx_table_generator::output_header(int* pos_y)
         }
 
     // Finally draw the separators above and (a double one) below them.
-    do_output_horz_separator(left_margin_, x,  y_top    );
-    do_output_horz_separator(left_margin_, x, *pos_y - 1);
-    do_output_horz_separator(left_margin_, x, *pos_y    );
+    if(draw_separators_)
+        {
+        do_output_horz_separator(left_margin_, x,  y_top    );
+        do_output_horz_separator(left_margin_, x, *pos_y - 1);
+        do_output_horz_separator(left_margin_, x, *pos_y    );
+        }
+}
+
+void wx_table_generator::output_super_header
+        (std::string const& header
+        ,std::size_t        begin_column
+        ,std::size_t        end_column
+        ,int*               pos_y
+        ,enum_output_mode   output_mode
+        )
+{
+    std::vector<std::string> const lines(split_into_lines(header));
+
+    switch(output_mode)
+        {
+        case e_output_normal:
+            break;
+        case e_output_measure_only:
+            *pos_y += row_height_*lines.size();
+            return;
+        }
+
+    // We don't have a function for getting the rectangle of a span of columns,
+    // but we can reuse the existing text_rect() if we just increase its width
+    // by the width of all the extra (i.e. not counting the starting one)
+    // columns in this span.
+    auto rect = text_rect(begin_column, *pos_y);
+    rect.width += do_get_cell_x(end_column) - do_get_cell_x(begin_column + 1);
+
+    for(auto const& line : lines)
+        {
+        dc_.DrawLabel(line, rect, wxALIGN_CENTER_HORIZONTAL);
+
+        rect.y += row_height_;
+        *pos_y += row_height_;
+        }
 }
 
 void wx_table_generator::output_row
@@ -341,7 +466,10 @@ void wx_table_generator::output_row
     int x = left_margin_;
     do_output_values(x, *pos_y, values);
 
-    do_output_horz_separator(left_margin_, x, *pos_y);
+    if(draw_separators_)
+        {
+        do_output_horz_separator(left_margin_, x, *pos_y);
+        }
 }
 
 void wx_table_generator::output_highlighted_cell
diff --git a/wx_table_generator.hpp b/wx_table_generator.hpp
index 515a3ce..e9df872 100644
--- a/wx_table_generator.hpp
+++ b/wx_table_generator.hpp
@@ -24,6 +24,8 @@
 
 #include "config.hpp"
 
+#include "output_mode.hpp"
+
 #include <wx/dc.h>
 #include <wx/font.h>
 
@@ -52,19 +54,34 @@ class wx_table_generator
     // Adds a column to the table. The total number of added columns determines
     // the number of the expected value in output_row() calls.
     //
-    // Providing an empty header suppresses the table display, while still
-    // taking into account in output_row(), providing a convenient way to hide
-    // a single column without changing the data representation.
+    // Providing an empty header suppresses the column display, while still
+    // taking it into account in output_row(), providing a convenient way to
+    // hide a single column without changing the data representation.
     //
     // Each column must either have a fixed width, specified as the width of
     // the longest text that may appear in this column, or be expandable
     // meaning that the rest of the page width is allocated to it which will be
     // the case if widest_text is empty.
+    //
     // Notice that column headers may be multiline strings.
     void add_column(std::string const& header, std::string const& widest_text);
 
     // Render the headers at the given position and update it.
-    void output_header(int* pos_y);
+    void output_header
+        (int*             pos_y
+        ,enum_output_mode output_mode = e_output_normal
+        );
+
+    // Render a super-header, i.e. a header spanning over several columns. The
+    // columns range is specified as a close/open interval, as usual in C++.
+    // The header string may be multiline, just as with normal headers.
+    void output_super_header
+        (std::string const& header
+        ,std::size_t        begin_column
+        ,std::size_t        end_column
+        ,int*               pos_y
+        ,enum_output_mode   output_mode = e_output_normal
+        );
 
     // Render a row with the given values at the given position and update it.
     // The values here can be single-line only and there must be exactly the
@@ -79,6 +96,9 @@ class wx_table_generator
         ,std::string const& value
         );
 
+    // Return the number of columns.
+    std::size_t columns_count() const {return columns_.size();}
+
     // Return the height of a single table row.
     int row_height() const {return row_height_;}
 
@@ -94,9 +114,10 @@ class wx_table_generator
     // Output a horizontal separator line across the specified columns,
     // using the usual C++ close/open interval convention.
     void output_horz_separator
-        (std::size_t begin_column
-        ,std::size_t end_column
-        ,int         y
+        (std::size_t      begin_column
+        ,std::size_t      end_column
+        ,int              y
+        ,enum_output_mode output_mode = e_output_normal
         );
 
     // Output a vertical separator line before the given column. Notice that
@@ -104,6 +125,15 @@ class wx_table_generator
     // output a separator after the last column.
     void output_vert_separator(std::size_t before_column, int y);
 
+    // Use condensed style: don't draw separators between rows and make them
+    // smaller.
+    void use_condensed_style();
+
+    // By default, columns are centered if they have fixed size or left-aligned
+    // otherwise. By calling this method, this alignment auto-detection is
+    // turned off and all columns are right-aligned.
+    void align_right();
+
   private:
     // Return the font used for the headers.
     wxFont get_header_font() const;
@@ -128,7 +158,8 @@ class wx_table_generator
 
     // These values could be recomputed, but cache them for performance.
     int const char_height_;
-    int const row_height_;
+    int row_height_;
+    int column_margin_;
 
     struct column_info
     {
@@ -160,6 +191,18 @@ class wx_table_generator
     // Maximal number of lines in any column header, initially 1 but can be
     // higher if multiline headers are used.
     std::size_t max_header_lines_;
+
+    // If false, separator lines are not drawn automatically (they can still be
+    // drawn by calling output_horz_separator() or output_vert_separator()
+    // explicitly).
+    bool draw_separators_ = true;
+
+    // If true, headers are drawn in bold.
+    bool use_bold_headers_ = true;
+
+    // If true, force right alignment for all columns instead of centering them
+    // automatically if they have fixed size.
+    bool align_right_ = false;
 };
 
 #endif // wx_table_generator_hpp



reply via email to

[Prev in Thread] Current Thread [Next in Thread]