packages feed

kvitable (empty) → 1.0.0.0

raw patch · 16 files changed

+3747/−0 lines, 16 filesdep +basedep +containersdep +html-parsesetup-changed

Dependencies added: base, containers, html-parse, kvitable, lucid, microlens, pretty-show, prettyprinter, tasty, tasty-hunit, template-haskell, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for KVITable++## 0.1.0.0 -- 2021-01-30++* Initial version
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright (c) 2021 Kevin Quick++Permission to use, copy, modify, and/or distribute this software for any purpose+with or without fee is hereby granted, provided that the above copyright notice+and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF+THIS SOFTWARE.
+ README.md view
@@ -0,0 +1,558 @@+The `KVITable` is similar to a `Map`, but the keys to the map are a+list of `Key=Val` data.  Although the `KVITable` is perfectly useable+as a container in this fashion, the main use of the `KVITable` is in+rendering this data in various configurations; because of this focus,+there is no particular attention to other container aspects such as:+performance, space usage, etc.++An example table can be created via:++```haskell+{-# LANGUAGE OverloadedStrings #-}+import qualified Data.Text as T+import Data.KVITable++nestedTable = foldl foldlInsert+              (mempty+                & keyVals .~ [ ("millions",  ["0"])+                             , ("thousands", ["0"])+                             , ("hundreds",  ["0"])+                             , ("tens",      ["0"])+                             , ("ones",      ["0"])+                             ]+              )+              [ ([("millions", T.pack $ show m)+                 ,("thousands", T.pack $ show t)+                 ,("hundreds", T.pack $ show h)+                 ,("tens", T.pack $ show d)+                 ,("ones", T.pack $ show o)],+                  if (o `rem` 2) == 1 then "odd" else "even")+              | m <- [0..2 :: Int]+              , t <- [0..2 :: Int]+              , h <- [1..2 :: Int]+              , d <- [2..2 :: Int]+              , o <- [0..1 :: Int]+              ]+```++This Haskell code generates a table where the keys are the various+scale indicators along with a corresponding key value.  The table+entries themselves are the words `odd` or `even`.++Rendering this table in ASCII mode, with blank rows and columns+hidden, and enabling column stacking at the "hundreds" key column can+be done with the following code:++```haskell+import Data.KVITable.Render.ASCII++render (defaultRenderConfig { sortKeyVals   = True+                            , rowRepeat     = False+                            , hideBlankCols = True+                            , hideBlankRows = True+                            , equisizedCols = False+                            , colStackAt    = Just "hundreds"+                            }) nestedTable+```++The output from this rendering will look like:++```+____ snip vv ____+| millions | thousands | ___ 1 ____ | ___ 2 ____ | <- hundreds+|          |           | ___ 2 ____ | ___ 2 ____ | <- tens+|          |           |    0 |   1 |    0 |   1 | <- ones++----------+-----------+------+-----+------+-----++|        0 |         0 | even | odd | even | odd |+|          |         1 | even | odd | even | odd |+|          |         2 | even | odd | even | odd |+|        1 |         0 | even | odd | even | odd |+|          |         1 | even | odd | even | odd |+|          |         2 | even | odd | even | odd |+|        2 |         0 | even | odd | even | odd |+|          |         1 | even | odd | even | odd |+|          |         2 | even | odd | even | odd |+____ snip ^^ ____+```++When rendered to HTML instead by using the same code but importing+`Data.KVITable.Render.HTML` (and please use a little CSS to make+things prettier), the following is obtained:++****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">millions</th>+      <th rowspan="3" class="kvitable_th">thousands</th>+      <th colspan="2" class="kvitable_th multicol">1</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;hundreds</th>+    </tr>+    <tr class="kvitable_tr">+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;tens</th>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;ones</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">0</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+  </tbody>+</table>++****++## Different ColStack specification++By changing the `colStackAt` specification in the rendering+configuration from the "hundreds" column to the "thousands" column,+the column at which `Key Val`'s are shown as column headers instead of+rows is changed and the following ASCII results are obtained:++```+____ snip vv ____+| millions | __________ 0 __________ | __________ 1 __________ | __________ 2 __________ | <- thousands+|          | ___ 1 ____ | ___ 2 ____ | ___ 1 ____ | ___ 2 ____ | ___ 1 ____ | ___ 2 ____ | <- hundreds+|          | ___ 2 ____ | ___ 2 ____ | ___ 2 ____ | ___ 2 ____ | ___ 2 ____ | ___ 2 ____ | <- tens+|          |    0 |   1 |    0 |   1 |    0 |   1 |    0 |   1 |    0 |   1 |    0 |   1 | <- ones++----------+------+-----+------+-----+------+-----+------+-----+------+-----+------+-----++|        0 | even | odd | even | odd | even | odd | even | odd | even | odd | even | odd |+|        1 | even | odd | even | odd | even | odd | even | odd | even | odd | even | odd |+|        2 | even | odd | even | odd | even | odd | even | odd | even | odd | even | odd |+____ snip ^^ ____+```++or as HTML:++****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">millions</th>+      <th colspan="4" class="kvitable_th multicol">0</th>+      <th colspan="4" class="kvitable_th multicol">1</th>+      <th colspan="4" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;thousands</th>+    </tr>+    <tr class="kvitable_tr">+      <th colspan="2" class="kvitable_th multicol">1</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">1</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">1</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;hundreds</th>+    </tr>+    <tr class="kvitable_tr">+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;tens</th>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;ones</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+  </tbody>+</table>++****++## No column stacking++Alternatively, the `colStackAt` rendering configuration parameter may+be specified as `Nothing`, indicating that all `Key Val` values are to+be specified on separate rows, with no stacked columns.  The ASCII+form of this is:++```+____ snip vv ____+| millions | thousands | hundreds | tens | ones | Value |++----------+-----------+----------+------+------+-------++|        0 |         0 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|        1 |         0 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|        2 |         0 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        1 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+____ snip ^^ ____+```++and the HTML form is++****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th class="kvitable_th">millions</th>+      <th class="kvitable_th">thousands</th>+      <th class="kvitable_th">hundreds</th>+      <th class="kvitable_th">tens</th>+      <th class="kvitable_th">ones</th>+      <th class="kvitable_th">Value</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th rowspan="12" class="kvitable_th">0</th>+      <th rowspan="4" class="kvitable_th">0</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="12" class="kvitable_th">1</th>+      <th rowspan="4" class="kvitable_th">0</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="12" class="kvitable_th">2</th>+      <th rowspan="4" class="kvitable_th">0</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="4" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">1</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+      <tr class="kvitable_tr">+      <th rowspan="2" class="kvitable_th">2</th>+      <th rowspan="2" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td">even</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td">odd</td>+    </tr>+  </tbody>+</table>++****++More examples can be found in the examples subdirectory, including:++ * [Zoo Inventory](examples/zoo.md)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/hundreds_all.md view
@@ -0,0 +1,241 @@+This example uses the same generated `KVITable` data that was+described in the README, but formats the output differently.++```haskell+render (defaultRenderConfig { KTR.sortKeyVals   = True+                            , KTR.rowRepeat     = False+                            , KTR.hideBlankCols = False+                            , KTR.hideBlankRows = False+                            , KTR.equisizedCols = False+                            , KTR.colStackAt    = Just "hundreds"+                            }+       )+       nestedTable+```++Here, the hiding of blank rows and columns is disabled, which makes+the table significantly larger:++```+____ snip vv ____+| millions | thousands | _____ 0 _____ | _______ 1 ________ | _______ 2 ________ | <- hundreds+|          |           | _ 0 _ | _ 2 _ | _ 0 _ | ___ 2 ____ | _ 0 _ | ___ 2 ____ | <- tens+|          |           | 0 | 1 | 0 | 1 | 0 | 1 |    0 |   1 | 0 | 1 |    0 |   1 | <- ones++----------+-----------+---+---+---+---+---+---+------+-----+---+---+------+-----++|        0 |         0 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         1 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         2 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|        1 |         0 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         1 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         2 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|        2 |         0 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         1 |   |   |   |   |   |   | even | odd |   |   | even | odd |+|          |         2 |   |   |   |   |   |   | even | odd |   |   | even | odd |+____ snip ^^ ____+```++*****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">millions</th>+      <th rowspan="3" class="kvitable_th">thousands</th>+      <th colspan="4" class="kvitable_th multicol">0</th>+      <th colspan="4" class="kvitable_th multicol">1</th>+      <th colspan="4" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;hundreds</th>+    </tr>+    <tr class="kvitable_tr">+      <th colspan="2" class="kvitable_th multicol">0</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">0</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th colspan="2" class="kvitable_th multicol">0</th>+      <th colspan="2" class="kvitable_th multicol">2</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;tens</th>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <th class="kvitable_th">1</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;ones</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">0</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">1</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th rowspan="3" class="kvitable_th">2</th>+      <th class="kvitable_th">0</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">1</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th">2</th>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td"></td>+      <td class="kvitable_td">even</td>+      <td class="kvitable_td">odd</td>+    </tr>+  </tbody>+</table>++*****++## Equisized columns++For the ASCII layout, the rendering configuration can set+`equisizedCols` to `True` to keep each column the same size.++```+____ snip vv ____+| millions | thousands | ___________ 0 ___________ | ___________ 1 ___________ | ___________ 2 ___________ | <- hundreds+|          |           | ____ 0 ____ | ____ 2 ____ | ____ 0 ____ | ____ 2 ____ | ____ 0 ____ | ____ 2 ____ | <- tens+|          |           |    0 |    1 |    0 |    1 |    0 |    1 |    0 |    1 |    0 |    1 |    0 |    1 | <- ones++----------+-----------+------+------+------+------+------+------+------+------+------+------+------+------++|        0 |         0 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         1 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         2 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|        1 |         0 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         1 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         2 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|        2 |         0 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         1 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+|          |         2 |      |      |      |      |      |      | even |  odd |      |      | even |  odd |+____ snip ^^ ____+```++This setting has no effect on the HTML layout; to achieve the same+effect for HTML, CSS settings should be used.
+ examples/zoo.md view
@@ -0,0 +1,433 @@+In this example, the inventory of various Zoo animals from different+City Zoos is maintained in a `KVITable`, where each animal is+additionally classified by parameters such as Biome and Diet.++The `KVITable` creation is somewhat lengthy due to the size of the+inventory; the [SampleTables.hs](../test/SampleTables.hs) file should be+consulted for the definition of the `KVITable` `zooTable` and+`zooTable2`.++Rendering the data without any column stacking, but with multiple row+groupings:++```+import Data.KVITable.Render.ASCII++render (defaultRenderConfig+        { sortKeyVals = True+        , rowRepeat   = False+        , rowGroup = [ "Location", "Biome", "Category" ]+        })+       zooTable2+```++results in the following:++```+____ snip vv ____+|  Location |    Biome | Category |      Diet |    Name |     Subtype | Count |++-----------+----------+----------+-----------+---------+-------------+-------++|        LA |   Jungle |   Animal | Herbivore |   Hippo |             |     1 |+|           |----------+----------+-----------+---------+-------------+-------++|           | Savannah |   Animal | Carnivore |    Lion |             |     4 |+|           |          |          | Herbivore | Giraffe |             |     2 |+|           |          |          |           |   Rhino |             |     3 |++-----------+----------+----------+-----------+---------+-------------+-------++|     Miami |    Polar |     Bird | Carnivore | Penguin |      Gentoo |    20 |+|           |----------+----------+-----------+---------+-------------+-------++|           | Savannah |   Animal | Carnivore |    Lion |             |     2 |+|           |          |          | Herbivore | Giraffe | Reticulated |     3 |++-----------+----------+----------+-----------+---------+-------------+-------++|  New York | Savannah |   Animal | Carnivore |    Lion |             |     3 |++-----------+----------+----------+-----------+---------+-------------+-------++| San Diego |   Jungle |   Animal |  Omnivore |    Bear |         Sun |     1 |+|           |----------+----------+-----------+---------+-------------+-------++|           |   Plains |   Animal |  Omnivore |    Bear |       Black |     1 |+|           |          |          |           |         |       Brown |     1 |+|           |----------+----------+-----------+---------+-------------+-------++|           |    Polar |   Animal |  Omnivore |    Bear |       Polar |     1 |+|           |          |----------+-----------+---------+-------------+-------++|           |          |     Bird | Carnivore | Penguin |     Emperor |     8 |+|           |          |          |           |         |      Gentoo |     2 |+|           |----------+----------+-----------+---------+-------------+-------++|           | Savannah |   Animal | Carnivore |    Lion |             |     9 |++-----------+----------+----------+-----------+---------+-------------+-------++____ snip ^^ ____+```++or in HTML (with CSS inherited from the Markdown+configuration... imagine how much nicer this could be with your own+CSS styling!):++******++<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th class="kvitable_th">Location</th>+      <th class="kvitable_th">Biome</th>+      <th class="kvitable_th">Category</th>+      <th class="kvitable_th">Diet</th>+      <th class="kvitable_th">Name</th>+      <th class="kvitable_th">Subtype</th>+      <th class="kvitable_th">Count</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=4>LA</th>+      <th class="kvitable_th last_in_group">Jungle</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Herbivore</th>+      <th class="kvitable_th last_in_group">Hippo</th>+      <th class="kvitable_th last_in_group"></th>+      <td class="kvitable_td last_in_group">1</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=3>Savannah</th>+      <th class="kvitable_th last_in_group" rowspan=3>Animal</th>+      <th class="kvitable_th">Carnivore</th>+      <th class="kvitable_th">Lion</th>+      <th class="kvitable_th"></th>+      <td class="kvitable_td">4</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th" rowspan=2>Herbivore</th>+      <th class="kvitable_th">Giraffe</th>+      <th class="kvitable_th"></th>+      <td class="kvitable_td">2</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Rhino</th>+      <th class="kvitable_th last_in_group"></th>+      <td class="kvitable_td last_in_group">3</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=3>Miami</th>+      <th class="kvitable_th last_in_group">Polar</th>+      <th class="kvitable_th last_in_group">Bird</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <th class="kvitable_th last_in_group">Penguin</th>+      <th class="kvitable_th last_in_group">Gentoo</th>+      <td class="kvitable_td last_in_group">20</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Savannah</th>+      <th class="kvitable_th last_in_group" rowspan=2>Animal</th>+      <th class="kvitable_th">Carnivore</th>+      <th class="kvitable_th">Lion</th>+      <th class="kvitable_th"></th>+      <td class="kvitable_td">2</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Herbivore</th>+      <th class="kvitable_th last_in_group">Giraffe</th>+      <th class="kvitable_th last_in_group">Reticulated</th>+      <td class="kvitable_td last_in_group">3</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">New York</th>+      <th class="kvitable_th last_in_group">Savannah</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <th class="kvitable_th last_in_group">Lion</th>+      <th class="kvitable_th last_in_group"></th>+      <td class="kvitable_td last_in_group">3</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=7>San Diego</th>+      <th class="kvitable_th last_in_group">Jungle</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Omnivore</th>+      <th class="kvitable_th last_in_group">Bear</th>+      <th class="kvitable_th last_in_group">Sun</th>+      <td class="kvitable_td last_in_group">1</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Plains</th>+      <th class="kvitable_th last_in_group" rowspan=2>Animal</th>+      <th class="kvitable_th" rowspan=2>Omnivore</th>+      <th class="kvitable_th" rowspan=2>Bear</th>+      <th class="kvitable_th">Black</th>+      <td class="kvitable_td">1</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Brown</th>+      <td class="kvitable_td last_in_group">1</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=3>Polar</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Omnivore</th>+      <th class="kvitable_th last_in_group">Bear</th>+      <th class="kvitable_th last_in_group">Polar</th>+      <td class="kvitable_td last_in_group">1</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Bird</th>+      <th class="kvitable_th" rowspan=2>Carnivore</th>+      <th class="kvitable_th" rowspan=2>Penguin</th>+      <th class="kvitable_th">Emperor</th>+      <td class="kvitable_td">8</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Gentoo</th>+      <td class="kvitable_td last_in_group">2</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Savannah</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <th class="kvitable_th last_in_group">Lion</th>+      <th class="kvitable_th last_in_group"></th>+      <td class="kvitable_td last_in_group">9</td>+    </tr>+  </tbody>+</table>++******++## With Column Stacking++Removing the "Subtype" field from the zoo table and then modifying the+rendering configuration to add column stacking on the `"Name"` key+`Val`s results in:++```+____ snip vv ____+|  Location |    Biome | Category |      Diet | Bear | Giraffe | Hippo | Lion | Penguin | Rhino | <- Name++-----------+----------+----------+-----------+------+---------+-------+------+---------+-------++|        LA |   Jungle |   Animal | Herbivore |      |         |     1 |      |         |       |+|           |----------+----------+-----------+------+---------+-------+------+---------+-------++|           | Savannah |   Animal | Carnivore |      |         |       |    4 |         |       |+|           |          |          | Herbivore |      |       2 |       |      |         |     3 |++-----------+----------+----------+-----------+------+---------+-------+------+---------+-------++|     Miami |    Polar |     Bird | Carnivore |      |         |       |      |      20 |       |+|           |----------+----------+-----------+------+---------+-------+------+---------+-------++|           | Savannah |   Animal | Carnivore |      |         |       |    2 |         |       |+|           |          |          | Herbivore |      |       3 |       |      |         |       |++-----------+----------+----------+-----------+------+---------+-------+------+---------+-------++|  New York | Savannah |   Animal | Carnivore |      |         |       |    3 |         |       |++-----------+----------+----------+-----------+------+---------+-------+------+---------+-------++| San Diego |   Jungle |   Animal |  Omnivore |    1 |         |       |      |         |       |+|           |----------+----------+-----------+------+---------+-------+------+---------+-------++|           |   Plains |   Animal |  Omnivore |    2 |         |       |      |         |       |+|           |----------+----------+-----------+------+---------+-------+------+---------+-------++|           |    Polar |   Animal |  Omnivore |    1 |         |       |      |         |       |+|           |          |----------+-----------+------+---------+-------+------+---------+-------++|           |          |     Bird | Carnivore |      |         |       |      |      10 |       |+|           |----------+----------+-----------+------+---------+-------+------+---------+-------++|           | Savannah |   Animal | Carnivore |      |         |       |    9 |         |       |++-----------+----------+----------+-----------+------+---------+-------+------+---------+-------++____ snip ^^ ____+```++And as HTML:++******+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th class="kvitable_th">Location</th>+      <th class="kvitable_th">Biome</th>+      <th class="kvitable_th">Category</th>+      <th class="kvitable_th">Diet</th>+      <th class="kvitable_th">Bear</th>+      <th class="kvitable_th">Giraffe</th>+      <th class="kvitable_th">Hippo</th>+      <th class="kvitable_th">Lion</th>+      <th class="kvitable_th">Penguin</th>+      <th class="kvitable_th">Rhino</th>+      <th class="rightlabel kvitable_th">&nbsp;&larr;Name</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=3>LA</th>+      <th class="kvitable_th last_in_group">Jungle</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Herbivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">1</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Savannah</th>+      <th class="kvitable_th last_in_group" rowspan=2>Animal</th>+      <th class="kvitable_th">Carnivore</th>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">4</td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Herbivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">2</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">3</td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=3>Miami</th>+      <th class="kvitable_th last_in_group">Polar</th>+      <th class="kvitable_th last_in_group">Bird</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">20</td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Savannah</th>+      <th class="kvitable_th last_in_group" rowspan=2>Animal</th>+      <th class="kvitable_th">Carnivore</th>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">2</td>+      <td class="kvitable_td">+      </td>+      <td class="kvitable_td">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Herbivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">3</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">New York</th>+      <th class="kvitable_th last_in_group">Savannah</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">3</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=5>San Diego</th>+      <th class="kvitable_th last_in_group">Jungle</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Omnivore</th>+      <td class="kvitable_td last_in_group">1</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Plains</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Omnivore</th>+      <td class="kvitable_td last_in_group">2</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group" rowspan=2>Polar</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Omnivore</th>+      <td class="kvitable_td last_in_group">1</td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Bird</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">+      </td>+      <td class="kvitable_td last_in_group">10</td>+      <td class="kvitable_td last_in_group">+      </td>+    </tr>+    <tr class="kvitable_tr">+      <th class="kvitable_th last_in_group">Savannah</th>+      <th class="kvitable_th last_in_group">Animal</th>+      <th class="kvitable_th last_in_group">Carnivore</th>+      <td class="kvitable_td last_in_group"></td>+      <td class="kvitable_td last_in_group"></td>+      <td class="kvitable_td last_in_group"></td>+      <td class="kvitable_td last_in_group">9</td>+      <td class="kvitable_td last_in_group"></td>+      <td class="kvitable_td last_in_group"></td>+    </tr>+  </tbody>+</table>++******
+ kvitable.cabal view
@@ -0,0 +1,74 @@+cabal-version:       >=1.10+name:                kvitable+version:             1.0.0.0+synopsis:            Key/Value Indexed Table container and formatting library+description:+   .+   Allows creation of a table from a set of of Key+Value Indices.+   This differs from the standard 'Map' structure in that the 'Map'+   simply indexes by value but the KVI table indexes by a+   heterogeneous list of keys along with their associated values.+   This effectively creates an N-dimensional table, where+   @N=Product(Count(Values[key]))@.  The table contents can be sparse.+   .+   This library also provides the ability to format multi-dimensional+   data in a table presentation.  The table is automatically formatted+   and can be output in a number of different styles (ascii, html,+   etc.)+   .+   Multi-dimensional data is more difficult to represent than simple+   two-dimensional data; this package provides the ability to select+   which dimensions should be represented as sub-rows and which+   dimensions should be represented as sub-columns.  See the README+   for examples++-- bug-reports:+license:             ISC+license-file:        LICENSE+author:              Kevin Quick+maintainer:          kquick@galois.com+copyright:           Kevin Quick, 2021+category:            Text+build-type:          Simple+tested-with:         GHC ==8.6.5 GHC ==8.8.4 GHC ==8.10.4 GHC ==9.0.1+extra-source-files:  CHANGELOG.md+                   , examples/hundreds_all.md+                   , examples/zoo.md+                   , README.md++library+  hs-source-dirs:      src+  default-language:    Haskell2010+  GHC-options:         -Wall -Wcompat -Wincomplete-uni-patterns -Wpartial-fields -fhide-source-paths+  -- other-extensions:+  exposed-modules:     Data.KVITable+                     , Data.KVITable.Render+                     , Data.KVITable.Render.ASCII+                     , Data.KVITable.Render.HTML+  -- other-modules:+  build-depends:       base >=4.12 && <4.16+                     , containers >= 0.6 && < 0.7+                     , lucid  >= 2.9 && < 2.10+                     , microlens >= 0.4 && < 0.5+                     , prettyprinter >= 1.7 && < 1.8+                     , text >= 1.2 && < 1.3++test-suite test-kvitable+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  default-language:    Haskell2010+  GHC-options:         -fhide-source-paths+  main-is:             TestMain.hs+  other-modules:       AsciiRenderTests+                     , HTMLRenderTests+                     , SampleTables+                     , TestQQDefs+  build-depends: base+               , html-parse+               , kvitable+               , microlens+               , pretty-show+               , tasty+               , tasty-hunit+               , template-haskell+               , text
+ src/Data/KVITable.hs view
@@ -0,0 +1,343 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}++-- | The 'KVITable' is similar to a 'Data.Map', but the keys for a+-- 'KVITable' are made up of sequences of @Key=Val@ values.  The+-- primary use of a 'KVITable' is for rendering the information in+-- various configurations and formats, although it may be used like+-- any other container.++module Data.KVITable+  (+    KVITable(KVITable)+  , Key+  , KeyVal+  , KeyVals+  , KeySpec+  , fromList+  , toList+  , Data.KVITable.lookup+  , keyVals+  , keyValGen+  , valueColName+  , insert+  , foldlInsert+  , Data.KVITable.filter+  , adjust+  , adjustWithKey+  , delete+  , update+  , updateWithKey+  , rows+  )+where++import           Data.Function ( on )+import qualified Data.List as L+import qualified Data.Map as Map+import           Data.Text ( Text )+import qualified GHC.Exts+import           Lens.Micro ( Lens' )+++-- | The core KeyValue Indexed Table.  This table is similar to a Map,+-- but the values are indexed by a list of Key+Value combinations, and+-- the table contents can be sparse.++-- KWQ: make fields strict?  check with tasty-bench++data KVITable v = KVITable+  { keyvals      :: KeyVals -- ^ allowed value for keys (in order)++  , keyvalGen    :: Key -> KeyVal+    -- ^ Function to generate the keyval if the keyval is not+    -- explicitly provided.  Provided with the Key and returns the+    -- KeyVal that should be used.++  , contents     :: Map.Map KeySpec v+    -- ^ Internal contents of the KVITable++    -- The invariant for the KVITable contents is that each KeySpec+    -- contains all keys listed in keyvals (in the same order) with+    -- the defaultKeyVal for any keys not explicitly provided for that+    -- value.++  , valuecolName :: Text  -- ^ name of the value cells+  }++instance Eq v => Eq (KVITable v) where+  -- n.b. keyvals (i.e. metadata) are _not_ used for equality, only contents+  (==) = (==) `on` contents++instance Show v => Show (KVITable v) where+  show t = "KVITable {" <>+           " keyvals = " <> show (keyvals t) <>+           " contents = " <> show (contents t) <>+           ", valuecolName = " <> show (valuecolName t) <>+           "}"++-- | The 'Key' is the first half of a tuple that makes up the list of+-- keys (the 'KeySpec').  The second half is the 'KeyVal'.+type Key = Text++-- | The 'KeyVal' is the first half of a tuple that makes up the list of+-- keys (the 'KeySpec').  The first half is the 'Key'.+type KeyVal = Text++-- | The 'KeySpec' is the list of tuples and defines the unique key+-- for a value in the 'KVITable'.+type KeySpec = [ (Key,  KeyVal ) ]++-- | The 'KeyVals' specifies all valid values for a particular 'Key'+-- in the 'KVITable'.  The set of 'KeyVals' can be provided at the+-- initialization of the 'KVITable' to ensure specific values are+-- considered (especially if rendering includes blank rows or+-- columns); if entries are added to the table with a 'KeyVal'+-- previously unknown for the 'Key', the 'KeyVals' for the table is+-- automatically updated to include the new 'KeyVal'.+type KeyVals = [ (Key, [KeyVal]) ]++-- | The KVITable semigroup is left biased (same as Data.Map).  Note+-- that joining tables can result in a table that has a different+-- keyVals sequence than either input table.++instance Semigroup (KVITable v) where+  a <> b = foldl foldlInsert+           (mempty { valuecolName = valuecolName a+                   , keyvals = keyvals a+                   })+           (toList b <> toList a)++instance Monoid (KVITable v) where+  mempty = KVITable { keyvals = mempty+                    , keyvalGen = const ""+                    , contents = mempty+                    , valuecolName = "Value"+                    }++instance Functor KVITable where+  fmap f t = KVITable { contents = fmap f (contents t)+                      , keyvalGen = keyvalGen t+                      , keyvals = keyvals t+                      , valuecolName = valuecolName t+                      }++instance Foldable KVITable where+  foldMap f = foldMap f . contents++instance Traversable KVITable where+  traverse f t = (\c -> KVITable { contents = c+                                 , valuecolName = valuecolName t+                                 , keyvals = keyvals t+                                 , keyvalGen = keyvalGen t+                                 }+                 ) <$> traverse f (contents t)++instance GHC.Exts.IsList (KVITable v) where+  type Item (KVITable v) = (KeySpec, v)+  fromList = foldl foldlInsert mempty+  toList = GHC.Exts.toList . contents+++-- | Converts a list of @([(Key,Val)], Value)@ tuples to a KVI table.++fromList :: [ GHC.Exts.Item (KVITable v) ] -> KVITable v+fromList = GHC.Exts.fromList++-- | Converts a KVI table to a list of @([(Key,Val)], Value)@ tuples.++toList :: KVITable v -> [ GHC.Exts.Item (KVITable v) ]+toList = GHC.Exts.toList+++-- | Fetch or set the keyvals list via lenses. Note that setting the+-- keyval list will drop any current contents in the table that do not+-- have entries in the keyvals list.++keyVals :: Lens' (KVITable v) KeyVals+keyVals f t = (\kvs ->+                 t { keyvals = kvs+                   , contents =+                     let inKVS spec _ = inkv spec kvs+                         inkv [] [] = True+                         inkv ((sk,sv):srs) ((k,vs):kv)+                           | sk == k && sv `elem` vs = inkv srs kv+                         inkv _ _ = False+                     in Map.filterWithKey inKVS (contents t)+                   }+              ) <$> f (keyvals t)+++-- | Fetch or set the default 'KeyVal' generator for this 'KVITable'++keyValGen :: Lens' (KVITable v) (Key -> KeyVal)+keyValGen f t = (\n -> t { keyvalGen = n } ) <$> f (keyvalGen t)++-- | Fetch or set the column name for the actual value cell in the+-- 'KVITable'.++valueColName :: Lens' (KVITable v) Text+valueColName f t = (\n -> t { valuecolName = n } ) <$> f (valuecolName t)+++-- | Retrieve an entry from the KVITable given a keyspec.  The keyspec+-- may be minimally specified (i.e. it does not need to contain keys+-- whose value is the default key value) and it may present the keys+-- out of order and the lookup will still succeed (if there is a value+-- for the normalized keyspec), but it will be faster to use the+-- normalized key directly.++lookup :: KeySpec -> KVITable v -> Maybe v+lookup keyspec t = case Map.lookup keyspec $ contents t of+                     Just v -> Just v+                     Nothing ->+                       -- keyspec might be under-specified or in a different order+                       let ks = normalizeKeySpec t keyspec+                       in Map.lookup ks $ contents t++normalizeKeySpec :: KVITable v -> KeySpec -> KeySpec+normalizeKeySpec t keyspec =+  let keyandval s (k,vs) = case L.lookup k keyspec of+        Just v -> if v `elem` vs then s <> [(k,v)]+                  else s -- no level added, so this should never match in the Map+        Nothing -> s <> [(k, keyvalGen t k)]+  in foldl keyandval [] (keyvals t)++-- | Inserts a new cell value into the table at the specified keyspec+-- location.  The keyspec may be minimally specified and out-of-order.+--+-- This may be an expensive operation if it has to extend the keyvals+-- for the table.  In general, insertion is expected to be less+-- frequent than lookups so computation costs are biased towards the+-- insertion operation.++insert :: KeySpec -> v -> KVITable v -> KVITable v+insert keyspec val t = endset t val (keyvals t) keyspec [] []++remainingKeyValDefaults :: KVITable v -> [(Key,a)] -> KeySpec+remainingKeyValDefaults t = fmap (\(k,_) -> (k, keyvalGen t k))++addDefVal :: KVITable v -> (Key, [KeyVal]) ->  (Key, [KeyVal])+addDefVal t e@(k,vs) = if (keyvalGen t k) `elem` vs+                       then e+                       else (k, keyvalGen t k : vs)++endset :: KVITable v -> v -> KeyVals -> KeySpec -> KeySpec -> KeyVals -> KVITable v+endset t val rkv [] tspec kvbld =+        -- Reached the end of the user's keyspec but there are more+        -- known keyvals in this KVITable, so add the entry with the+        -- default KeyVal for the remaining keyspec (and ensure the+        -- default KeyVal is listed in the table's keyvals).+        let spec = tspec <> remainingKeyValDefaults t rkv+        in t { contents = Map.insert spec val (contents t)+             , keyvals = kvbld <> (addDefVal t <$> rkv)+             }++endset t val [] spec tspec kvbld =+  -- Reached the end of the known keyvals for this table but the+  -- user's keyspec has additional elements.  This should extend+  -- the tables keyvals with the remaining keyspec; also all+  -- existing table values should be pushed out to use the+  -- default values for the new keys in their keyspec.+  let spec' = tspec <> spec+      keySpecElemToKeyVals (k,v) = (k, if null curTblList+                                       then [v]+                                       else [v, keyvalGen t k])+      keyvals' = kvbld <> (keySpecElemToKeyVals <$> spec)+      curTblList = Map.toList $ contents t+      defaultsExtension = remainingKeyValDefaults t spec+      updTblList = fmap (\(ks,v) -> (ks <> defaultsExtension, v)) curTblList+  in t { contents = Map.insert spec' val $ Map.fromList updTblList+       , keyvals = keyvals'+       }++endset t val kvs@((k,vs):rkvs) ((sk,sv):srs) tspec kvbld =+  if k == sk+  then let kv' = if sv `elem` vs+                 then kvbld <> [(k, vs)]+                 else kvbld <> [(k, sv : vs)]+       in endset t val rkvs srs (tspec <> [(k,sv)]) kv'+  else+    -- re-arrange user spec crudely by throwing invalid+    -- candidates to the end and retrying.  This isn't+    -- necessarily efficient, but keyspecs aren't expected to be+    -- longer than about a dozen entries.+    if sk `elem` (fst <$> rkvs) && k `elem` (fst <$> srs)+    then endset t val kvs (srs <> [(sk,sv)]) tspec kvbld+    else+      if any (`elem` (fst <$> kvs)) (fst <$> srs)+      then endset t val kvs (srs <> [(sk,sv)]) tspec kvbld+      else+        let defVal = keyvalGen t k+            vs' = if defVal `elem` vs then vs else (defVal : vs)+        in endset t val rkvs ((sk,sv):srs) (tspec <> [(k,defVal)]) (kvbld <> [(k,vs')])+++-- | The foldlInsert is a convenience function that can be specified+-- as the function argument of a foldl operation over the list form of+-- a KVITable to generate the associated KVITable.++foldlInsert :: KVITable v -> (KeySpec, v) -> KVITable v+foldlInsert t (k,v) = insert k v t+++-- | Filter 'KVITable' to retain only the elements that satisfy some predicate.++filter :: ((KeySpec, v) -> Bool) -> KVITable v -> KVITable v+filter f t = foldl chkInsert (emptyClone t) $ toList t+  where emptyClone o = o { contents = mempty }+        chkInsert o (k,v) = if f (k,v) then insert k v o else o++-- | Delete the value at the specified keyspec location in the+-- 'KVITable'.  If the keyspec does not exist, the original table is+-- returned.++delete :: KeySpec -> KVITable v -> KVITable v+delete k t = t { contents = Map.delete (normalizeKeySpec t k) $ contents t }++-- | Adjust a value at the specified keyspec; return the original+-- 'KVITable' if that keyspec is not found in the table.++adjustWithKey :: (KeySpec -> v -> v) -> KeySpec -> KVITable v -> KVITable v+adjustWithKey f k t =+  t { contents = Map.adjustWithKey f (normalizeKeySpec t k) $ contents t }++-- | Adjust a value at the specified keyspec; return the original+-- 'KVITable' if that keyspec is not found in the table.++adjust :: (v -> v) -> KeySpec -> KVITable v -> KVITable v+adjust f k t = t { contents = Map.adjust f (normalizeKeySpec t k) $ contents t }++-- | Update the 'KVITable' to remove or set a new value for the+-- specified entry if the updating function returns @Nothing@ or @Just+-- v@, respectively.  The update function is passed both the keyspec+-- and the current value at that key.  If the value does not exist in+-- the table, the original table is returned.++updateWithKey :: (KeySpec -> v -> Maybe v) -> KeySpec -> KVITable v -> KVITable v+updateWithKey f k t =+  t { contents = Map.updateWithKey f (normalizeKeySpec t k) $ contents t }++-- | Update the 'KVITable' to remove or set a new value for the+-- specified entry if the updating function returns @Nothing@ or @Just+-- v@, respectively.  The update function is passed the value for the+-- keyspec to be updated. If the value does not exist in the table,+-- the original table is returned.++update :: (v -> Maybe v) -> KeySpec -> KVITable v -> KVITable v+update f k t = t { contents = Map.update f (normalizeKeySpec t k) $ contents t }++-- | The 'rows' function returns a set of rows for the 'KVITable' as a+-- list structure, where each list entry is a different row.  A row+-- consists of the /values/ of the keys for that row followed by the+-- value of the entry (to get the names of the keys, use 'keyVals').++rows :: KVITable v -> [ ([KeyVal], v) ]+rows t = go (keyvals t) []+  where+    go [] spec = let spec' = reverse spec+                 in case Map.lookup spec' (contents t) of+                      Nothing -> []+                      Just v -> [ (snd <$> spec', v) ]+    go ((key, vals):kvs) spec =+      concatMap (\v -> let spec' = (key,v):spec in go kvs spec') vals
+ src/Data/KVITable/Render.hs view
@@ -0,0 +1,96 @@+-- | Common definitions (and support functions) for rendering a+-- 'KVITable'.++module Data.KVITable.Render+  (+    RenderConfig(..)+  , defaultRenderConfig+  , sortWithNums+  )+where++import           Data.KVITable+import qualified Data.List as L+import           Data.Text ( Text )+import qualified Data.Text as T++-- | Returns the default rendering configuration, to be used with a+-- format-specific @render@ call.++defaultRenderConfig :: RenderConfig+defaultRenderConfig = RenderConfig+  { hideBlankRows = True+  , hideBlankCols = True+  , equisizedCols = True+  , sortKeyVals   = False+  , colStackAt    = Nothing+  , rowRepeat     = True+  , rowGroup      = []+  , caption       = Nothing+  }++-- | The 'RenderConfig' specifies the various controls and+-- configurations used when rendering a 'KVITable' in various formats.+-- The 'RenderConfig' is global t oall formats, although some of the+-- fields in the 'RenderConfig' will be ignored as not-applicable by+-- some formats.++data RenderConfig = RenderConfig+  {+    hideBlankRows :: Bool+    -- ^ 'True' (default) removes rows for which there are no values++  , hideBlankCols :: Bool+    -- ^ 'True' (default) removes columns for which there are no values++  , equisizedCols :: Bool+    -- ^ 'True' (default) to maintain a consistent column width,+    -- otherwise the columns are shunk to the minimum size needed to+    -- display the title and values.  Not applicable for some backends+    -- (e.g. HTML) where the backend provides table rendering+    -- functionality.++  , sortKeyVals :: Bool+    -- ^ 'True' (default is False) to sort the KeyVal entries when+    -- rendering a table.++  , colStackAt :: Maybe Key+    -- ^ Column key to begin stacking keys in columns and sub-columns+    -- rather than creating additional sub-rows.++  , rowRepeat :: Bool+    -- ^ 'True' (default) if an identical 'KeyVal' is to be repeated+    -- in subsequent applicable rows.++  , rowGroup :: [Key]+    -- ^ List of Key names that should by grouped by inserting+    -- horizontal row lines between KeyVals++  , caption :: Maybe Text+    -- ^ Caption to render for table for backends which support+    -- captions; otherwise ignored.+  }++++-- | Sorting for KeyVals.  If the value starts or ends with a digit,+-- then this should do a rough numeric sort on the expectation that+-- the digits represent a version or some other numeric value.  As an+-- approximation of a numeric sort, sort by word size and then string+-- value.  This will result in [ "1", "2", "10", "50", "400" ], but+-- would fail with [ "v1.0", "v2.0", "v3.0", "v2.0.5", "v1.0.0.3" ],+-- but it's a reasonably fast heuristic and probably better than a+-- straight ascii sort.+--+-- This function is used by the 'KVITable' rendering functions.++sortWithNums :: [KeyVal] -> [KeyVal]+sortWithNums kvs =+  let skvs = zip (rank <$> kvs) kvs+      rank e = if (not $ T.null e) &&+                  or [ T.head e `elem` ['0'..'9']+                     , T.last e `elem` ['0'..'9']+                     ]+               then T.length e+               else 0+  in snd <$> L.sort skvs
+ src/Data/KVITable/Render/ASCII.hs view
@@ -0,0 +1,245 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++-- | This module provides the 'KVITable' 'render' function for+-- rendering the table in a plain ASCII format.++module Data.KVITable.Render.ASCII+  (+    render+    -- re-export Render definitions to save the caller an additional import+  , RenderConfig(..)+  , defaultRenderConfig+  )+where++import qualified Data.List as L+import           Data.Maybe ( fromMaybe, isNothing )+import           Data.Text ( Text )+import qualified Data.Text as T+import           Lens.Micro ( (^.) )+import qualified Prettyprinter as PP++import           Data.KVITable+import           Data.KVITable.Render++import           Prelude hiding ( lookup )+++-- | Renders the specified table in ASCII format, using the specified+-- 'RenderConfig' controls.++render :: PP.Pretty v => RenderConfig -> KVITable v -> Text+render cfg t =+  let kseq = fst <$> t ^. keyVals+      (fmt, hdr) = renderHdrs cfg t kseq+      bdy = renderSeq cfg fmt kseq t+  in T.unlines $ hdr <> bdy++----------------------------------------------------------------------++data FmtLine = FmtLine [Int] Sigils Sigils  -- last is for sepline+data Sigils = Sigils { sep :: Text, pad :: Text, cap :: Text }++fmtLine :: [Int] -> FmtLine+fmtLine cols = FmtLine cols+               Sigils { sep = "|", pad = " ", cap = "_" }+               Sigils { sep = "+", pad = "-", cap = "_" }++fmtColCnt :: FmtLine -> Int+fmtColCnt (FmtLine cols _ _) = length cols++perColOvhd :: Int+perColOvhd = 2 -- pad chars on either side of each column's entry++-- | Formatted width of output, including pad on either side of each+-- column's value (but not the outer set), and a separator between columns.+--+-- Note that a column size of 0 indicates that hideBlankCols is active+-- and the column was found to be empty of values, so it should not be+-- counted.+fmtWidth :: FmtLine -> Int+fmtWidth (FmtLine cols _ _) =+  let cols' = L.filter (/= 0) cols+  in sum cols' + ((perColOvhd + 1) * (length cols' - 1))++fmtEmptyCols :: FmtLine -> Bool+fmtEmptyCols (FmtLine cols _ _) = sum cols == 0++fmtAddColLeft :: Int -> FmtLine -> FmtLine+fmtAddColLeft leftCol (FmtLine cols s s') = FmtLine (leftCol : cols) s s'++data FmtVal = Separator | TxtVal Text | CenterVal Text++fmtRender :: FmtLine -> [FmtVal] -> Text+fmtRender (FmtLine [] _sigils _sepsigils) [] = ""+fmtRender (FmtLine cols sigils sepsigils) vals =+  if length cols == length vals+  then let sig f o = case o of+                       Separator   -> f sepsigils+                       TxtVal _    -> f sigils+                       CenterVal _ -> f sigils+           l = sig sep $ head vals+       in l <>+          T.concat+          [ sig pad fld <>+            (case fld of+                Separator   -> T.pack (replicate sz '-')+                TxtVal v    -> T.pack (replicate (sz - T.length v) ' ') <> v+                CenterVal t -> let (w,e) = (sz - T.length t - 2) `divMod` 2+                                   m = cap sigils+                                   ls = T.replicate (w + 0) m+                                   rs = T.replicate (w + e) m+                               in if T.length t + 2 >= sz+                                  then (T.replicate (sz - T.length t) " ") <> t+                                  else ls <> " " <> t <> " " <> rs+            ) <>+            sig pad fld <>+            sig sep fld  -- KWQ or if next fld is Nothing+          | (sz,fld) <- zip cols vals, sz /= 0+          ]+  else error ("Insufficient arguments (" <>+              show (length vals) <> ")" <>+              " for FmtLine " <> show (length cols))++----------------------------------------------------------------------++data HeaderLine = HdrLine FmtLine HdrVals Trailer+type HdrVals = [FmtVal]+type Trailer = Text++hdrFmt :: HeaderLine -> FmtLine+hdrFmt (HdrLine fmt _ _) = fmt++renderHdrs :: PP.Pretty v => RenderConfig -> KVITable v -> [Key] -> (FmtLine, [Text])+renderHdrs cfg t keys =+  ( lastFmt+  , [ fmtRender fmt hdrvals+      <> (if T.null trailer then "" else (" <- " <> trailer))+    | (HdrLine fmt hdrvals trailer) <- hrows+    ] <>+    [ fmtRender lastFmt (replicate (fmtColCnt lastFmt) Separator) ])+  where+    hrows = hdrstep cfg t keys+    lastFmt = if null hrows then fmtLine [] else hdrFmt $ head $ reverse hrows++hdrstep :: PP.Pretty v => RenderConfig -> KVITable v -> [Key] -> [HeaderLine]+hdrstep _cfg t [] =+  -- colStackAt wasn't recognized, so devolve into a non-colstack table+  let valcoltxt = t ^. valueColName+      valcoltsz = T.length valcoltxt+      valsizes  = length . show . PP.pretty . snd <$> toList t+      valwidth  = maximum $ valcoltsz : valsizes+  in [ HdrLine (fmtLine [valwidth]) [TxtVal valcoltxt] "" ]+hdrstep cfg t (key:keys) =+  if colStackAt cfg == Just key+  then hdrvalstep cfg t [] (key:keys)  -- switch to column-stacking mode+  else+    let keyw = maximum ( T.length key :+                         fmap T.length (fromMaybe [] $ L.lookup key $ t ^. keyVals) )+        mkhdr (hs, v) (HdrLine fmt hdrvals trailer) =+          ( HdrLine (fmtAddColLeft keyw fmt) (TxtVal v : hdrvals) trailer : hs , "")+    in reverse $ fst $ foldl mkhdr ([], key) $ hdrstep cfg t keys+         -- first line shows hdrval for non-colstack'd columns, others are blank++hdrvalstep :: PP.Pretty v => RenderConfig -> KVITable v -> KeySpec -> [Key] -> [HeaderLine]+hdrvalstep _ _ _ [] = error "ASCII hdrvalstep with empty keys after matching colStackAt -- impossible"+hdrvalstep cfg t steppath (key:[]) =+  let titles = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+      ordering = if sortKeyVals cfg then sortWithNums else id+      cvalWidths kv = fmap (length . show . PP.pretty . snd) $+                      L.filter ((L.isSuffixOf (steppath <> [(key, kv)])) . fst) $+                      toList t+      colWidth kv = let cvw = cvalWidths kv+                    in if and [ hideBlankCols cfg, sum cvw == 0 ]+                    then 0+                    else maximum $ T.length kv : cvw+      cwidths = fmap colWidth titles+      fmtcols = if equisizedCols cfg+                then (replicate (length cwidths) (maximum cwidths))+                else cwidths+  in [ HdrLine (fmtLine $ fmtcols) (TxtVal <$> titles) key ]+hdrvalstep cfg t steppath (key:keys) =+  let vals = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+      ordering = if sortKeyVals cfg then sortWithNums else id+      subhdrsV v = hdrvalstep cfg t (steppath <> [(key,v)]) keys+      subTtlHdrs = let subAtVal v = (T.length v, subhdrsV v)+                   in fmap subAtVal vals+      szexts = let subVW = fmtWidth . hdrFmt . head+                   subW (hl,sh) = let sv = subVW sh+                                  in if and [ hideBlankCols cfg,+                                              fmtEmptyCols $ hdrFmt $ head sh+                                            ]+                                     then (0, 0)+                                     else (hl, sv)+               in fmap (uncurry max . subW) subTtlHdrs+      rsz_extsubhdrs = fmap hdrJoin $+                       L.transpose $+                       fmap (uncurry rsz_hdrstack) $+                       zip szhdrs $ fmap snd subTtlHdrs+      largest = maximum szexts+      szhdrs = if equisizedCols cfg && not (hideBlankCols cfg)+               then replicate (length vals) largest+               else szexts+      rsz_hdrstack s vhs = fmap (rsz_hdrs s) vhs+      rsz_hdrs hw (HdrLine (FmtLine c s j) v r) =+        let nzCols = L.filter (/= 0) c+            pcw = sum nzCols + ((perColOvhd + 1) * (length nzCols - 1))+            (ew,w0) = let l = length nzCols+                      in if l == 0 then (0,0)+                         else max 0 (hw - pcw) `divMod` length nzCols+            c' = fst $ foldl (\(c'',n) w -> (c''<>[n+w],ew)) ([],ew+w0) c+        in HdrLine (FmtLine c' s j) v r+      hdrJoin hl = foldl hlJoin (HdrLine (fmtLine []) [] "") hl+      hlJoin (HdrLine (FmtLine c s j) v _) (HdrLine (FmtLine c' _ _) v' r) =+        HdrLine (FmtLine (c<>c') s j) (v<>v') r+      tvals = fmap CenterVal vals+  in HdrLine (fmtLine szhdrs) tvals key : rsz_extsubhdrs++renderSeq :: PP.Pretty v => RenderConfig -> FmtLine -> [Key] -> KVITable v -> [Text]+renderSeq cfg fmt keys kvitbl = fmtRender fmt . snd <$> asciiRows keys []+  where+    asciiRows :: [Key] -> KeySpec -> [ (Bool, [FmtVal]) ]+    asciiRows [] path =+      let v = lookup path kvitbl+          skip = case v of+                   Nothing -> hideBlankRows cfg+                   Just _  -> False+      in if skip then []+         else [ (False, [ maybe (TxtVal "") TxtVal (T.pack . show . PP.pretty <$> v) ]) ]+    asciiRows (key:kseq) path+      | colStackAt cfg == Just key =+        let filterOrDefaultBlankRows =+              fmap (fmap defaultBlanks) .+              if hideBlankRows cfg+              then L.filter (not . all isNothing . snd)+              else id+            defaultBlanks = fmap (\v -> maybe (TxtVal "") TxtVal v)+        in filterOrDefaultBlankRows $ [ (False, multivalRows (key:kseq) path) ]+      | otherwise =+        let subrows keyval = asciiRows kseq $ path <> [ (key, keyval) ]+            grprow subs = if key `elem` rowGroup cfg && not (null subs)+                          then let subl = [ (True, replicate (length $ snd $ head subs) Separator) ]+                               in if fst (last subs)+                                  then init subs <> subl+                                  else subs <> subl+                          else subs+            addSubrows ret keyval = ret <> (grprow $ fst $+                                            foldl leftAdd ([],keyval) $ subrows keyval)+            leftAdd (acc,kv) (b,subrow) = (acc <> [ (b, TxtVal kv : subrow) ],+                                           if rowRepeat cfg then kv else "")+            ordering = if sortKeyVals cfg then sortWithNums else id+        in foldl addSubrows [] $ ordering $ fromMaybe [] $ L.lookup key $ kvitbl ^. keyVals+    multivalRows :: [Key] -> KeySpec -> [ Maybe Text ]+    multivalRows (key:[]) path =+      let ordering = if sortKeyVals cfg then sortWithNums else id+          keyvals = ordering $ fromMaybe [] $ L.lookup key $ kvitbl ^. keyVals+          showEnt = T.pack . show . PP.pretty+      in (\v -> (showEnt <$> (lookup (path <> [(key,v)]) kvitbl))) <$> keyvals+    multivalRows (key:kseq) path =+      let ordering = if sortKeyVals cfg then sortWithNums else id+          keyvals = ordering $ fromMaybe [] $ L.lookup key  $ kvitbl ^. keyVals+      in concatMap (\v -> multivalRows kseq (path <> [(key,v)])) keyvals+    multivalRows [] _ = error "multivalRows cannot be called with no keys!"
+ src/Data/KVITable/Render/HTML.hs view
@@ -0,0 +1,238 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}++-- | This module provides the 'KVITable' 'render' function for+-- rendering the table in a HTML table format.  The various HTML table+-- entries have class designators that allow the user to provide CSS+-- to adjust the appearance of the table.++module Data.KVITable.Render.HTML+  (+    render+    -- re-export Render definitions to save the caller an additional import+  , RenderConfig(..)+  , defaultRenderConfig+  )+where++import qualified Data.Foldable as F+import qualified Data.List as L+import           Data.Maybe ( fromMaybe, isNothing )+import           Data.Text ( Text )+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import           Lens.Micro ( (^.) )+import           Lucid+import qualified Prettyprinter as PP++import           Data.KVITable+import           Data.KVITable.Render++import           Prelude hiding ( lookup )+++-- | Renders the specified table in HTML format, using the specified+-- 'RenderConfig' controls.  The output is only the @<table>@+-- definition; it is intended to be embedded in a larger HTML+-- document.++render :: PP.Pretty v => RenderConfig -> KVITable v -> Text+render cfg t =+  let kseq = fst <$> t ^. keyVals+      (fmt, hdr) = renderHdrs cfg t kseq+      bdy = renderSeq cfg fmt kseq t+  in TL.toStrict $ renderText $+     table_ [ class_ "kvitable" ] $+     do maybe mempty (caption_ . toHtml) $ Data.KVITable.Render.caption cfg+        thead_ [ class_ "kvitable_head" ] hdr+        tbody_ [ class_ "kvitable_body" ] bdy++----------------------------------------------------------------------++data FmtLine = FmtLine [Int]  -- colspans, length is # columns++instance Semigroup FmtLine where+  (FmtLine c1) <> (FmtLine c2) = FmtLine $ c1 <> c2++instance Monoid FmtLine where+  mempty = FmtLine mempty++fmtAddColLeft :: Int -> FmtLine -> FmtLine+fmtAddColLeft lspan (FmtLine col) = FmtLine $ lspan : col++data FmtVal = Val Height LastInGroup Text+            | Hdr Height LastInGroup Text+            deriving Show+type Height = Int+type LastInGroup = Bool+type RightLabel = Text++fmtRender :: FmtLine -> [FmtVal] -> Maybe RightLabel -> Html ()+fmtRender (FmtLine cols) vals mbRLabel = do+  tr_ [ class_ "kvitable_tr" ] $+    let excessColCnt = length cols - length vals+        cell (w,Hdr h l v) =+          let a = [ [ class_ "kvitable_th" ]+                  , if h == 1 then []+                    else [ rowspan_ $ T.pack $ show h ]+                  , if w == 1 then []+                    else [ colspan_ $ T.pack $ show w+                         , class_ " multicol" ]+                  , if l then [ class_ " last_in_group" ] else []+                  ]+          in th_ (concat $ reverse a) (toHtml v)+        cell (w,Val h l v) =+          let a = [ [ class_ "kvitable_td" ]+                  , if h == 1 then []+                    else [ rowspan_ $ T.pack $ show h ]+                  , if w == 1 then []+                    else [ colspan_ $ T.pack $ show w+                         , class_ " multicol" ]+                  , if l then [ class_ " last_in_group" ] else []+                  ]+          in td_ (concat $ reverse a) (toHtml v)+        labelMark = toHtmlRaw ("&nbsp;&larr;" :: Text)+        labelHtml = th_ [ class_ "rightlabel kvitable_th" ] .+                    (labelMark <>) .+                    toHtml+    in do mapM_ cell $ L.filter ((/= 0) . fst) $+            zip (drop excessColCnt cols) vals+          maybe mempty labelHtml mbRLabel+++----------------------------------------------------------------------++data HeaderLine = HdrLine FmtLine HdrVals Trailer+type HdrVals = [FmtVal]+type Trailer = Maybe Text++instance Semigroup HeaderLine where+  (HdrLine fmt1 hv1 t1) <> (HdrLine fmt2 hv2 _) =+    HdrLine (fmt1 <> fmt2) (hv1 <> hv2) t1++hdrFmt :: HeaderLine -> FmtLine+hdrFmt (HdrLine fmt _ _) = fmt++renderHdrs :: PP.Pretty v+           => RenderConfig -> KVITable v -> [Key]+           -> ( FmtLine, Html () )+renderHdrs cfg t keys =+  ( rowfmt, sequence_ [ fmtRender fmt hdrvals trailer+                      | (HdrLine fmt hdrvals trailer) <- hrows+                      ])+  where+    (hrows, rowfmt) = hdrstep cfg t keys++hdrstep :: PP.Pretty v+        => RenderConfig -> KVITable v -> [Key] -> ([HeaderLine], FmtLine)+hdrstep _cfg t [] =+  ( [ HdrLine (FmtLine [1]) [Hdr 1 False $ t ^. valueColName] Nothing ]+  , FmtLine [1]+  )+hdrstep cfg t (key:keys) =+  if colStackAt cfg == Just key+  then hdrvalstep cfg t [] (key:keys) -- switch to column stacking mode+  else+    let (nexthdrs, lowestfmt) = hdrstep cfg t keys+        (HdrLine fmt vals tr) = head nexthdrs -- safe: there were keys+        fmt' = fmtAddColLeft 1 fmt+        val = Hdr (length nexthdrs) False key+    in ( (HdrLine fmt' (val : vals) tr) : tail nexthdrs+       , fmtAddColLeft 1 lowestfmt+       )++hdrvalstep :: PP.Pretty v+           => RenderConfig -> KVITable v -> KeySpec -> [Key]+           -> ([HeaderLine], FmtLine)+hdrvalstep _ _ _ [] = error "HTML hdrvalstep with empty keys after matching colStackAt -- impossible"+hdrvalstep cfg t steppath (key:[]) =+  let titles = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+      ordering = if sortKeyVals cfg then sortWithNums else id+      cvalWidths kv = fmap (length . show . PP.pretty . snd) $+                      L.filter ((L.isSuffixOf (steppath <> [(key, kv)])) . fst) $+                      toList t+      cwidth c = if and [ hideBlankCols cfg+                        , 0 == (sum $ cvalWidths c) ]+                 then 0+                 else 1+      fmt = FmtLine $ fmap cwidth titles+  in ( [ HdrLine fmt (Hdr 1 False <$> titles) (Just key) ], fmt)+hdrvalstep cfg t steppath (key:keys) =+  let titles = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+      ordering = if sortKeyVals cfg then sortWithNums else id+      subhdrsV v = hdrvalstep cfg t (steppath <> [(key,v)]) keys+      subTtlHdrs :: [ ([HeaderLine], FmtLine) ]+      subTtlHdrs = subhdrsV <$> titles+      subhdrs = if hideBlankCols cfg+                then subTtlHdrs+                else L.replicate (length titles) $ head subTtlHdrs+      subhdr_rollup = joinHdrs <$> L.transpose (fst <$> subhdrs)+      joinHdrs hl = foldl (<>) (head hl) (tail hl)+      superFmt sub = let FmtLine subcols = hdrFmt $ last $ fst sub+                     in if sum subcols == 0+                        then 0+                        else length $ L.filter (/= 0) subcols+      topfmt = FmtLine (superFmt <$> subhdrs)+      tophdr = HdrLine topfmt (Hdr 1 False <$> titles) $ Just key+  in ( tophdr : subhdr_rollup, F.fold (snd <$> subTtlHdrs))++----------------------------------------------------------------------++renderSeq :: PP.Pretty v+          => RenderConfig -> FmtLine -> [Key] -> KVITable v -> Html ()+renderSeq cfg fmt keys t =+  mapM_ (flip (fmtRender fmt) Nothing) $ htmlRows keys []+  where+    mkVal = Val 1 False . T.pack . show . PP.pretty+    htmlRows :: [Key] -> KeySpec -> [ [FmtVal] ]+    htmlRows [] path =+      let v = lookup path t+          skip = case v of+            Nothing -> hideBlankRows cfg+            Just _ -> False+          row = maybe (Val 1 False "") mkVal v+      in if skip then [] else [ [row] ]+    htmlRows (key:kseq) path+      | colStackAt cfg == Just key =+          let filterOrDefaultBlankRows =+                fmap (fmap (maybe (Val 1 False "") id)) .+                if hideBlankRows cfg+                then L.filter (not . all isNothing)+                else id+          in filterOrDefaultBlankRows $+             [ multivalRows (key:kseq) path ]+      | otherwise =+          let keyvals = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+              ordering = if sortKeyVals cfg then sortWithNums else id+              subrows keyval = htmlRows kseq $ path <> [(key,keyval)]+              endOfGroup = key `elem` rowGroup cfg+              addSubrows ret keyval =+                let sr = subrows keyval+                in ret <> (fst $+                           foldl (leftAdd (length sr)) ([],Just keyval) $+                           reverse $ zip (endOfGroup: L.repeat False) $ reverse sr)+              leftAdd nrows (acc,mb'kv) (endGrp, subrow) =+                let sr = setValGrouping endGrp <$> subrow+                    setValGrouping g (Val h g' v) = Val h (g || g') v+                    setValGrouping g (Hdr h g' v) = Hdr h (g || g') v+                in ( acc <> [ (case mb'kv of+                                    Nothing -> sr+                                    Just kv -> let w = if rowRepeat cfg+                                                       then 1+                                                       else nrows+                                               in Hdr w endOfGroup kv : sr+                              ) ]+                   , if rowRepeat cfg then mb'kv else Nothing)+          in foldl addSubrows [] keyvals+++    multivalRows [] _ = error "HTML multivalRows cannot be called with no keys!"+    multivalRows (key:[]) path =+      let keyvals = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+          ordering = if sortKeyVals cfg then sortWithNums else id+      in (\v -> mkVal <$> lookup (path <> [(key,v)]) t) <$> keyvals+    multivalRows (key:kseq) path =+      let keyvals = ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals+          ordering = if sortKeyVals cfg then sortWithNums else id+      in concatMap (\v -> multivalRows kseq (path <> [(key,v)])) keyvals
+ test/AsciiRenderTests.hs view
@@ -0,0 +1,832 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}++module AsciiRenderTests where++import           Control.Monad ( unless )+import           Data.Text ( Text )+import qualified Data.Text as T+import           Lens.Micro ( (^.), (.~), (%~), (&) )+import           Test.Tasty+import           Test.Tasty.HUnit++import           SampleTables+import           TestQQDefs++import qualified Data.KVITable as KVI+import qualified Data.KVITable.Render as KTR+import qualified Data.KVITable.Render.ASCII as KTRA+++cmpTables :: Text -> Text -> Text -> IO ()+cmpTables nm actual expected =+  unless (expected == actual) $ do+    let dl (e,a) = if e == a then db e else de " ↱" e <> "\n    " <> da " ↳" a+        db b = "|        > " <> b+        de m e = "|" <> m <> "expect> " <> e+        da m a = "|" <> m <> "actual> " <> a+        el = T.lines expected+        al = T.lines actual+        addnum n l = let nt = T.pack (show n)+                         nl = T.length nt+                     in T.take (4 - nl) "    " <> nt <> l+    let details = ("MISMATCH between " <>+                   T.pack (show $ length el) <> " expected and " <>+                   T.pack (show $ length al) <> " actual for " <> nm) :+                  (fmap (uncurry addnum) $+                   zip [1..] $ concat $+                   -- Highly simplistic "diff" output assumes+                   -- correlated lines: added or removed lines just+                   -- cause everything to shown as different from that+                   -- point forward.+                   [ fmap dl $ zip el al+                   , fmap (de "∌ ") $ drop (length al) el+                   , fmap (da "∹ ") $ drop (length el) al+                   ])+    assertFailure $ T.unpack $ T.unlines details++++testAsciiRendering =+  testGroup "ASCII rendering" $+  let kvi0 = mempty :: KVI.KVITable Text+      cfg0 = KTR.defaultRenderConfig+      cfgWBlankRows = cfg0 { KTR.hideBlankRows = False }+  in+    [+      testCase "empty table" $+      cmpTables "empty table" (KTRA.render cfg0 kvi0) [sq|+****+| Value |++-------++****+|]++    , testCase "empty table with blanks" $+      cmpTables "empty table" (KTRA.render cfgWBlankRows kvi0) [sq|+****+| Value |++-------++|       |+****+|]++    , testCase "empty table with labels" $+      let kvi = mempty & KVI.keyVals @Float .~ [ ("foo", []), ("dog", []) ]+      in cmpTables "empty table with labels" (KTRA.render cfg0 kvi) [sq|+****+| foo | dog | Value |++-----+-----+-------++****|]++    , testCase "add key" $+      let t0 = mempty & KVI.keyVals .~ [ ("foo", ["bar", "baz"]) ] :: KVI.KVITable Text+          t1 = KVI.insert [ ("foo", "baz"), ("dog", "woof") ] "yo" $+               KVI.insert [ ("foo", "bar") ] "hi"+               t0+      in do KVI.rows t1 @?= [ ([ "bar", "" ], "hi")+                        , ([ "baz", "woof"], "yo")+                        ]+            cmpTables "add key" (KTRA.render cfg0 t1) [sq|+****+| foo |  dog | Value |++-----+------+-------++| bar |      |    hi |+| baz | woof |    yo |+****|]+++    , testCase "deep add" $+      let t0 = mempty+               & KVI.keyVals .~ [ ("foo", ["bar", "baz"])+                                , ("moon", ["beam", "pie"])+                                ]+               & KVI.valueColName .~ "says"+               & KVI.keyValGen .~ const "?"+               :: KVI.KVITable Text+          t1 = KVI.insert [ ("foo", "Bill"), ("moon", "Ted"), ("dog", "arf arf") ] "Excellent!" $+               KVI.insert [ ("foo", "baz"), ("moon", "beam"), ("dog", "woof") ] "yo" $+               KVI.insert [ ("foo", "bar"), ("moon", "pie") ] "hi"+               t0+      in cmpTables "add key" (KTRA.render cfg0 t1) [sq|+****+|  foo | moon |     dog |       says |++------+------+---------+------------++| Bill |  Ted | arf arf | Excellent! |+|  bar |  pie |       ? |         hi |+|  baz | beam |    woof |         yo |+****|]++    , testCase "medium sized table render, sorted" $+      cmpTables "medium table"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|     gcc7 |    no |            1 |  good |+|     gcc7 |   yes |            0 |  good |+|     gcc7 |   yes |            3 |  ugly |+|     gcc8 |   yes |            0 |  good |+|     gcc8 |   yes |            1 |   bad |+|     gcc8 |   yes |            3 |  true |+|   clang6 |   yes |            0 |    ok |+|   clang7 |    no |            0 |  good |+|   clang7 |    no |            1 |  good |+|   clang7 |    no |            3 |  good |+|   clang7 |   yes |            3 |  good |+|  clang10 |    no |            3 |  good |+|  clang10 |   yes |            3 |  good |+****|]++    , testCase "medium sized table render, unsorted" $+      cmpTables "medium table"+      (KTRA.render (cfg0 { KTR.sortKeyVals = False }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |   yes |            0 |  good |+|     gcc7 |   yes |            3 |  ugly |+|     gcc7 |    no |            0 |   bad |+|     gcc7 |    no |            1 |  good |+|     gcc8 |   yes |            0 |  good |+|     gcc8 |   yes |            1 |   bad |+|     gcc8 |   yes |            3 |  true |+|   clang6 |   yes |            0 |    ok |+|  clang10 |   yes |            3 |  good |+|  clang10 |    no |            3 |  good |+|   clang7 |   yes |            3 |  good |+|   clang7 |    no |            0 |  good |+|   clang7 |    no |            1 |  good |+|   clang7 |    no |            3 |  good |+****|]++    , testCase "medium table, sorted, blank, row skip, no repeats" $+      cmpTables "medium table"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|          |       |            1 |  good |+|          |   yes |            0 |  good |+|          |       |            3 |  ugly |+|     gcc8 |   yes |            0 |  good |+|          |       |            1 |   bad |+|          |       |            3 |  true |+|   clang6 |   yes |            0 |    ok |+|   clang7 |    no |            0 |  good |+|          |       |            1 |  good |+|          |       |            3 |  good |+|          |   yes |            3 |  good |+|  clang10 |    no |            3 |  good |+|          |   yes |            3 |  good |+****|]++    , testCase "medium table, sorted, blank row skip, no repeats, group unknown" $+      cmpTables "medium table s 0brow 0rep [unknown]"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.rowGroup = ["unknown"]+                         }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|          |       |            1 |  good |+|          |   yes |            0 |  good |+|          |       |            3 |  ugly |+|     gcc8 |   yes |            0 |  good |+|          |       |            1 |   bad |+|          |       |            3 |  true |+|   clang6 |   yes |            0 |    ok |+|   clang7 |    no |            0 |  good |+|          |       |            1 |  good |+|          |       |            3 |  good |+|          |   yes |            3 |  good |+|  clang10 |    no |            3 |  good |+|          |   yes |            3 |  good |+****|]++    , testCase "medium table, sorted, blank row skip, no repeats, group compiler" $+      cmpTables "medium table s 0brow 0rep [unknown]"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.rowGroup = ["compiler"]+                         }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|          |       |            1 |  good |+|          |   yes |            0 |  good |+|          |       |            3 |  ugly |++----------+-------+--------------+-------++|     gcc8 |   yes |            0 |  good |+|          |       |            1 |   bad |+|          |       |            3 |  true |++----------+-------+--------------+-------++|   clang6 |   yes |            0 |    ok |++----------+-------+--------------+-------++|   clang7 |    no |            0 |  good |+|          |       |            1 |  good |+|          |       |            3 |  good |+|          |   yes |            3 |  good |++----------+-------+--------------+-------++|  clang10 |    no |            3 |  good |+|          |   yes |            3 |  good |++----------+-------+--------------+-------++****|]++    , testCase "medium table, sorted, blank row skip, no repeats, multi-group" $+      cmpTables "medium table s 0brow 0rep [compiler,debug]"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.rowGroup = ["unknown", "compiler", "unk", "debug", "huh"]+                         }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|          |       |            1 |  good |+|          |-------+--------------+-------++|          |   yes |            0 |  good |+|          |       |            3 |  ugly |++----------+-------+--------------+-------++|     gcc8 |   yes |            0 |  good |+|          |       |            1 |   bad |+|          |       |            3 |  true |++----------+-------+--------------+-------++|   clang6 |   yes |            0 |    ok |++----------+-------+--------------+-------++|   clang7 |    no |            0 |  good |+|          |       |            1 |  good |+|          |       |            3 |  good |+|          |-------+--------------+-------++|          |   yes |            3 |  good |++----------+-------+--------------+-------++|  clang10 |    no |            3 |  good |+|          |-------+--------------+-------++|          |   yes |            3 |  good |++----------+-------+--------------+-------++****|]++    , testCase "medium table, sorted, blank" $+      cmpTables "medium table"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.hideBlankRows = False }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|     gcc7 |    no |            1 |  good |+|     gcc7 |    no |            3 |       |+|     gcc7 |   yes |            0 |  good |+|     gcc7 |   yes |            1 |       |+|     gcc7 |   yes |            3 |  ugly |+|     gcc8 |    no |            0 |       |+|     gcc8 |    no |            1 |       |+|     gcc8 |    no |            3 |       |+|     gcc8 |   yes |            0 |  good |+|     gcc8 |   yes |            1 |   bad |+|     gcc8 |   yes |            3 |  true |+|   clang6 |    no |            0 |       |+|   clang6 |    no |            1 |       |+|   clang6 |    no |            3 |       |+|   clang6 |   yes |            0 |    ok |+|   clang6 |   yes |            1 |       |+|   clang6 |   yes |            3 |       |+|   clang7 |    no |            0 |  good |+|   clang7 |    no |            1 |  good |+|   clang7 |    no |            3 |  good |+|   clang7 |   yes |            0 |       |+|   clang7 |   yes |            1 |       |+|   clang7 |   yes |            3 |  good |+|  clang10 |    no |            0 |       |+|  clang10 |    no |            1 |       |+|  clang10 |    no |            3 |  good |+|  clang10 |   yes |            0 |       |+|  clang10 |   yes |            1 |       |+|  clang10 |   yes |            3 |  good |+****|]++    , testCase "medium sized table render, sorted, no blank, colstack unknown" $+      cmpTables "medium table s 0blnk colstk=unknown"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.colStackAt  = Just "unknown"+                         }) mediumKVI) [sq|+****+| compiler | debug | optimization | Value |++----------+-------+--------------+-------++|     gcc7 |    no |            0 |   bad |+|     gcc7 |    no |            1 |  good |+|     gcc7 |   yes |            0 |  good |+|     gcc7 |   yes |            3 |  ugly |+|     gcc8 |   yes |            0 |  good |+|     gcc8 |   yes |            1 |   bad |+|     gcc8 |   yes |            3 |  true |+|   clang6 |   yes |            0 |    ok |+|   clang7 |    no |            0 |  good |+|   clang7 |    no |            1 |  good |+|   clang7 |    no |            3 |  good |+|   clang7 |   yes |            3 |  good |+|  clang10 |    no |            3 |  good |+|  clang10 |   yes |            3 |  good |+****|]++    , testCase "medium sized table render, sorted, !blank, colstk optimization" $+      cmpTables "medium table s 0blnk colstk=optimization"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.hideBlankRows = True+                         , KTR.colStackAt  = Just "optimization"+                         }) mediumKVI) [sq|+****+| compiler | debug |    0 |    1 |    3 | <- optimization++----------+-------+------+------+------++|     gcc7 |    no |  bad | good |      |+|     gcc7 |   yes | good |      | ugly |+|     gcc8 |   yes | good |  bad | true |+|   clang6 |   yes |   ok |      |      |+|   clang7 |    no | good | good | good |+|   clang7 |   yes |      |      | good |+|  clang10 |    no |      |      | good |+|  clang10 |   yes |      |      | good |+****|]++    , testCase "medium, sorted, !blank, !row rpt, rgrp compiler colstk optimization" $+      cmpTables "medium table s 0blnk colstk=optimization rowgrp=compiler"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.hideBlankRows = True+                         , KTR.colStackAt  = Just "optimization"+                         , KTR.rowRepeat   = False+                         , KTR.rowGroup    = [ "compiler" ]+                         }) mediumKVI) [sq|+****+| compiler | debug |    0 |    1 |    3 | <- optimization++----------+-------+------+------+------++|     gcc7 |    no |  bad | good |      |+|          |   yes | good |      | ugly |++----------+-------+------+------+------++|     gcc8 |   yes | good |  bad | true |++----------+-------+------+------+------++|   clang6 |   yes |   ok |      |      |++----------+-------+------+------+------++|   clang7 |    no | good | good | good |+|          |   yes |      |      | good |++----------+-------+------+------+------++|  clang10 |    no |      |      | good |+|          |   yes |      |      | good |++----------+-------+------+------+------++****|]++    , testCase "medium sized table render, sorted, !blank, equisize, colstack debug" $+      cmpTables "medium table s 0blnk equisize colstk=debug"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = True+                           , KTR.colStackAt  = Just "debug"+                           }) mediumKVI) [sq|+****+| compiler | _______ no _______ | ______ yes _______ | <- debug+|          |    0 |    1 |    3 |    0 |    1 |    3 | <- optimization++----------+------+------+------+------+------+------++|     gcc7 |  bad | good |      | good |      | ugly |+|     gcc8 |      |      |      | good |  bad | true |+|   clang6 |      |      |      |   ok |      |      |+|   clang7 | good | good | good |      |      | good |+|  clang10 |      |      | good |      |      | good |+****|]++    , testCase "medium sized table render, sorted, !blank, fitsize, colstack debug" $+      cmpTables "medium table s 0blnk fitsize colstk=debug"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt  = Just "debug"+                           }) mediumKVI) [sq|+****+| compiler | _______ no _______ | ______ yes ______ | <- debug+|          |    0 |    1 |    3 |    0 |   1 |    3 | <- optimization++----------+------+------+------+------+-----+------++|     gcc7 |  bad | good |      | good |     | ugly |+|     gcc8 |      |      |      | good | bad | true |+|   clang6 |      |      |      |   ok |     |      |+|   clang7 | good | good | good |      |     | good |+|  clang10 |      |      | good |      |     | good |+****|]++    , testCaseSteps "small table right aligned" $ \step ->+      let tbl = foldl KVI.foldlInsert+                (mempty & KVI.valueColName .~ "name" :: KVI.KVITable Text) inp+          inp = [ ([("id", "2")], "Layla")+                , ([("id", "3")], "Jack Gabriel")+                , ([("id", "1")], "Sam")+                ]+      in do step "rows"  -- generally in insert order (foldr pushes from end)+            KVI.rows tbl @?= [ (["1"], "Sam")+                             , (["3"], "Jack Gabriel")+                             , (["2"], "Layla")+                             ]+            step "ASCII rendering unsorted"+            cmpTables "small table right aligned unsorted"+              (KTRA.render (cfg0 { KTR.sortKeyVals = False }) tbl) [sq|+****+| id |         name |++----+--------------++|  1 |          Sam |+|  3 | Jack Gabriel |+|  2 |        Layla |+****|]+            step "ASCII rendering sorted"+            cmpTables "small table right aligned sorted"+              (KTRA.render (cfg0 { KTR.sortKeyVals = True }) tbl) [sq|+****+| id |         name |++----+--------------++|  1 |          Sam |+|  2 |        Layla |+|  3 | Jack Gabriel |+****|]++    , testCase "small multi-column float value table rendering" $+      let tbl = foldl KVI.foldlInsert+                (mempty & KVI.valueColName .~ "Annual Rainfall" :: KVI.KVITable Float) inp+          inp = [ ([("City name", "Adelaide"), ("Area", "1295"), ("Population", "1158259")],  600.5)+                , ([("City name", "Brisbane"), ("Area", "5905"), ("Population", "1857594")], 1146.4)+                , ([("City name", "Darwin"),   ("Area", "112"),  ("Population", "120900")],  1714.7)+                , ([("City name", "Hobart"),   ("Area", "1357"), ("Population", "205556")],   619.5)+                , ([("City name", "Melbourne"),("Area", "1566"), ("Population", "3806092")],  646.9)+                , ([("City name", "Perth"),    ("Area", "5386"), ("Population", "1554769")],  869.4)+                , ([("City name", "Sydney"),   ("Area", "2058"), ("Population", "4336374")], 1214.8)+                ]+      in cmpTables "small table float value table"+         (KTRA.render (cfg0 { KTR.sortKeyVals = True }) tbl) [sq|+****+| City name | Area | Population | Annual Rainfall |++-----------+------+------------+-----------------++|  Adelaide | 1295 |    1158259 |           600.5 |+|  Brisbane | 5905 |    1857594 |          1146.4 |+|    Darwin |  112 |     120900 |          1714.7 |+|    Hobart | 1357 |     205556 |           619.5 |+| Melbourne | 1566 |    3806092 |           646.9 |+|     Perth | 5386 |    1554769 |           869.4 |+|    Sydney | 2058 |    4336374 |          1214.8 |+****|]++    , testCase "big table grouped sorted" $+      cmpTables "big table grouped sorted"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.rowGroup = [ "Location", "Biome", "Category" ]+                         }) zooTable2)+      [uq_f|examples/zoo.md|]++    , testCase "big table grouped sorted no-subtype colstack" $+      let zt = KVI.fromList $+               foldl rmvSubtype [] $+               KVI.toList zooTable2+          rmvSubtype newl (keyspec, v) =+            let ks = filter (("Subtype" /=) . fst) keyspec+            in case lookup ks newl of+                 Nothing -> (ks,v) : newl+                 Just v' -> (ks, v' + v) : filter ((ks /=) . fst) newl+      in cmpTables "big table grouped sorted no-subtype colstack"+         (KTRA.render (cfg0 { KTR.sortKeyVals = True+                            , KTR.rowRepeat = False+                            , KTR.rowGroup = [ "Location", "Biome", "Category" ]+                            , KTR.colStackAt = Just "Name"+                            , KTR.equisizedCols = False+                            }) zt)+         [uq2_f|examples/zoo.md|]++    , testCase "big table grouped sorted equisized" $+      cmpTables "big table grouped sorted equisized"+      (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.hideBlankCols = False+                         , KTR.equisizedCols = True+                         , KTR.rowGroup = [ "Branch" ]+                         , KTR.colStackAt = Just "ghcver"+                         }) testedTable)+      [sq|+        # Note: no seplines under system because it wasn't included in the row_group+****+|        system |     Branch |   Strategy | ___ ghc844 ____ | ___ ghc865 ____ | ___ ghc882 ____ | ___ ghc890 ____ | <- ghcver+|               |            |            |      N |      Y |      N |      Y |      N |      Y |      N |      Y | <- debug++---------------+------------+------------+--------+--------+--------+--------+--------+--------+--------+--------++| x86_64-darwin |    develop |      HEADs |      + |        |        |        |        |        |        |        |+|               |------------+------------+--------+--------+--------+--------+--------+--------+--------+--------++|  x86_64-linux | PR-feature |      HEADs |        |        | FAIL*2 |        | FAIL*1 |        |        |        |+|               |            | submodules |      + |      + |      + |        | FAIL*1 |        |        |        |+|               |------------+------------+--------+--------+--------+--------+--------+--------+--------+--------++|               |    develop |      HEADs |      + |      + |      + |        |        |        |        |        |+|               |            | submodules |      + |        |        |        | FAIL*1 |        |        |        |+|               |------------+------------+--------+--------+--------+--------+--------+--------+--------+--------++|               |     master |      HEADs |        | FAIL*1 | FAIL*1 |        | FAIL*1 |        |        |        |+|               |            | submodules |      + | FAIL*1 | FAIL*1 |        |        |        |        |        |+|               |------------+------------+--------+--------+--------+--------+--------+--------+--------+--------++****|]++    , testCase "big table grouped sorted fitsize colstack=ghcver" $+      cmpTables "big table grouped sorted fitsize colstack=ghcver"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.hideBlankCols = False+                         , KTR.equisizedCols = False+                         , KTR.rowGroup = [ "Branch" ]+                         , KTR.colStackAt = Just "ghcver"+                         }) testedTable)+        [sq|+        # Note: no seplines under system because it wasn't included in the row_group+****+|        system |     Branch |   Strategy | _ ghc844 _ | _ ghc865 _ | _ ghc882 _ | ghc890 | <- ghcver+|               |            |            | N |      Y |      N | Y |      N | Y |  N | Y | <- debug++---------------+------------+------------+---+--------+--------+---+--------+---+----+---++| x86_64-darwin |    develop |      HEADs | + |        |        |   |        |   |    |   |+|               |------------+------------+---+--------+--------+---+--------+---+----+---++|  x86_64-linux | PR-feature |      HEADs |   |        | FAIL*2 |   | FAIL*1 |   |    |   |+|               |            | submodules | + |      + |      + |   | FAIL*1 |   |    |   |+|               |------------+------------+---+--------+--------+---+--------+---+----+---++|               |    develop |      HEADs | + |      + |      + |   |        |   |    |   |+|               |            | submodules | + |        |        |   | FAIL*1 |   |    |   |+|               |------------+------------+---+--------+--------+---+--------+---+----+---++|               |     master |      HEADs |   | FAIL*1 | FAIL*1 |   | FAIL*1 |   |    |   |+|               |            | submodules | + | FAIL*1 | FAIL*1 |   |        |   |    |   |+|               |------------+------------+---+--------+--------+---+--------+---+----+---++****|]++    , testCase "big table grouped sorted fitsize colstack=Strategy" $+      cmpTables "big table grouped sorted fitsize colstack=Strategy"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.hideBlankCols = False+                         , KTR.equisizedCols = False+                         , KTR.rowGroup = [ "Branch" ]+                         , KTR.colStackAt = Just "Strategy"+                         }) testedTable)+        [sq|+        # Note: no seplines under system because it wasn't included in the row_group+****+|        system |     Branch | ___________________ HEADs ___________________ | ________________ submodules _________________ | <- Strategy+|               |            | _ ghc844 _ | _ ghc865 _ | _ ghc882 _ | ghc890 | _ ghc844 _ | _ ghc865 _ | _ ghc882 _ | ghc890 | <- ghcver+|               |            | N |      Y |      N | Y |      N | Y |  N | Y | N |      Y |      N | Y |      N | Y |  N | Y | <- debug++---------------+------------+---+--------+--------+---+--------+---+----+---+---+--------+--------+---+--------+---+----+---++| x86_64-darwin |    develop | + |        |        |   |        |   |    |   |   |        |        |   |        |   |    |   |+|               |------------+---+--------+--------+---+--------+---+----+---+---+--------+--------+---+--------+---+----+---++|  x86_64-linux | PR-feature |   |        | FAIL*2 |   | FAIL*1 |   |    |   | + |      + |      + |   | FAIL*1 |   |    |   |+|               |------------+---+--------+--------+---+--------+---+----+---+---+--------+--------+---+--------+---+----+---++|               |    develop | + |      + |      + |   |        |   |    |   | + |        |        |   | FAIL*1 |   |    |   |+|               |------------+---+--------+--------+---+--------+---+----+---+---+--------+--------+---+--------+---+----+---++|               |     master |   | FAIL*1 | FAIL*1 |   | FAIL*1 |   |    |   | + | FAIL*1 | FAIL*1 |   |        |   |    |   |+|               |------------+---+--------+--------+---+--------+---+----+---+---+--------+--------+---+--------+---+----+---++****|]++    , testCase "nested table hide=blankRows,blankCols colstack=ones" $+      cmpTables "nested table hide=blankRows,blankCols colstack=ones"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "ones"+                           }) nestedTable)+        [sq|+****+| millions | thousands | hundreds | tens |    0 |   1 | <- ones++----------+-----------+----------+------+------+-----++|        0 |         0 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         1 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         2 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|        1 |         0 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         1 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         2 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|        2 |         0 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         1 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+|          |         2 |        1 |    2 | even | odd |+|          |           |        2 |    2 | even | odd |+****|]++    , testCase "nested table colstack=tens" $+      cmpTables "nested table colstack=tens"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "tens"+                           }) nestedTable)+        -- Note: the nestedTable declares a KeyVal of 0 for each key;+        -- without hideBlankCols true, this test would show a 0 value+        -- column for the tens stacked column.+        [sq|+****+| millions | thousands | hundreds | ___ 2 ____ | <- tens+|          |           |          |    0 |   1 | <- ones++----------+-----------+----------+------+-----++|        0 |         0 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         1 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         2 |        1 | even | odd |+|          |           |        2 | even | odd |+|        1 |         0 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         1 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         2 |        1 | even | odd |+|          |           |        2 | even | odd |+|        2 |         0 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         1 |        1 | even | odd |+|          |           |        2 | even | odd |+|          |         2 |        1 | even | odd |+|          |           |        2 | even | odd |+****|]++    , testCase "nested table hide=blankCols,blankRows colstack=hundreds" $+      cmpTables "nested table hide-blankCols,blankRows colstack=hundreds"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "hundreds"+                           }) nestedTable)+        [uq_f|README.md|]++    , testCase "nested table hide=none colstack=hundreds" $+      cmpTables "nested table hide=none colstack=hundreds"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = False+                           , KTR.hideBlankRows = False+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "hundreds"+                           }) nestedTable)+        [uq_f|examples/hundreds_all.md|]++    , testCase "nested table hide=none colstack=hundreds equisized" $+      cmpTables "nested table hide=none colstack=hundreds equisized"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = False+                           , KTR.hideBlankRows = False+                           , KTR.equisizedCols = True+                           , KTR.colStackAt = Just "hundreds"+                           }) nestedTable)+        [uq2_f|examples/hundreds_all.md|]++    , testCase "nested table hideBlank=rol,col colstack=thousands" $+      cmpTables "nested table hideBlank=row,col colstack=thousands"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "thousands"+                           }) nestedTable)+        [uq2_f|README.md|]++    , testCase "nested table hideBlank=rol,col" $+      cmpTables "nested table hideBlank=row,col"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           }) nestedTable)+        [uq3_f|README.md|]++    , testCase "nested table hideBlank=none" $+      cmpTables "nested table hideBlank=none"+        (KTRA.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = False+                           , KTR.hideBlankRows = False+                           , KTR.equisizedCols = False+                           }) nestedTable)+        [sq|+****+| millions | thousands | hundreds | tens | ones | Value |++----------+-----------+----------+------+------+-------++|        0 |         0 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|        1 |         0 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|        2 |         0 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         1 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |         2 |        0 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |        1 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+|          |           |        2 |    0 |    0 |       |+|          |           |          |      |    1 |       |+|          |           |          |    2 |    0 |  even |+|          |           |          |      |    1 |   odd |+****|]++      ]
+ test/HTMLRenderTests.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}++module HTMLRenderTests where++import           Control.Monad ( unless )+import qualified Data.List as L+import           Data.Text ( Text )+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import           Lens.Micro ( (^.), (.~), (%~), (&) )+import           Test.Tasty+import           Test.Tasty.HUnit+import           Text.HTML.Parser ( parseTokens, renderToken+                                  , canonicalizeTokens+                                  , Token( TagOpen, TagSelfClose ) )++import           SampleTables+import           TestQQDefs++import qualified Data.KVITable as KVI+import qualified Data.KVITable.Render as KTR+import qualified Data.KVITable.Render.HTML as KTRH+++cmpTables :: Text -> Text -> Text -> IO ()+cmpTables nm actual expected = do+  let expH = normalize $ parseTokens $ T.concat $ fmap T.strip $ T.lines expected+      actH = normalize $ parseTokens actual+      normalize = fmap sortAttrs . canonicalizeTokens+      sortAttrs (TagOpen n a) = TagOpen n $ L.sort a+      sortAttrs (TagSelfClose n a) = TagSelfClose n $ L.sort a+      sortAttrs t = t+  unless (expH == actH) $ do+    let dl (e,a) = if e == a then db e else de " ↱" e <> "\n    " <> da " ↳" a+        db b = "|        > " <> b+        de m e = "|" <> m <> "expect> " <> e+        da m a = "|" <> m <> "actual> " <> a+        el = fmap (TL.toStrict . renderToken) expH+        al = fmap (TL.toStrict . renderToken) actH+        addnum n l = let nt = T.pack (show n)+                         nl = T.length nt+                     in T.take (4 - nl) "    " <> nt <> l+    let details = ("MISMATCH between " <>+                   T.pack (show $ length el) <> " expected and " <>+                   T.pack (show $ length al) <> " actual for " <> nm) :+                  (fmap (uncurry addnum) $+                   zip [1..] $ concat $+                   -- Highly simplistic "diff" output assumes+                   -- correlated lines: added or removed lines just+                   -- cause everything to shown as different from that+                   -- point forward.+                   [ fmap dl $ zip el al+                   , fmap (de "∌ ") $ drop (length al) el+                   , fmap (da "∹ ") $ drop (length el) al+                   ])+    assertFailure $ T.unpack $ T.unlines details+++testHTMLRendering =+  testGroup "HTML rendering" $+  let kvi0 = mempty :: KVI.KVITable Text+      cfg0 = KTR.defaultRenderConfig+      cfgWBlankRows = cfg0 { KTR.hideBlankRows = False }+  in+    [+      testCase "empty table, hide blank" $+      cmpTables "empty table, hide blank"+      (KTRH.render cfg0 kvi0) [sq|+****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr"><th class="kvitable_th">Value</th></tr>+  </thead>+  <tbody class="kvitable_body">+  </tbody>+</table>+****+|]++    , testCase "empty table, show blank" $+      cmpTables "empty table, show blank"+      (KTRH.render cfgWBlankRows kvi0) [sq|+****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr"><th class="kvitable_th">Value</th></tr>+  </thead>+  <tbody class="kvitable_body">+    <tr class="kvitable_tr">+      <td class="kvitable_td"></td>+    </tr>+  </tbody>+</table>+****+|]++    , testCase "empty table with labels" $+      let kvi = mempty & KVI.keyVals @Float .~ [ ("foo", []), ("dog", []) ]+      in cmpTables "empty table with labels" (KTRH.render cfg0 kvi) [sq|+****+<table class="kvitable">+  <thead class="kvitable_head">+    <tr class="kvitable_tr">+      <th class="kvitable_th">foo</th>+      <th class="kvitable_th">dog</th>+      <th class="kvitable_th">Value</th>+    </tr>+  </thead>+  <tbody class="kvitable_body">+  </tbody>+</table>+****+|]++    , testCase "nested table hideBlank=rows,cols, fitted, colstack=hundreds" $+      cmpTables "nested table hideBlank=rows,cols, fitted, colstack=hundreds"+      (KTRH.render (cfg0 { KTR.sortKeyVals     = True+                         , KTR.rowRepeat     = False+                         , KTR.hideBlankCols = True+                         , KTR.hideBlankRows = True+                         , KTR.equisizedCols = False+                         , KTR.colStackAt    = Just "hundreds"+                         }) nestedTable)+        [sq_f|README.md|]++    , testCase "nested table hide=none, fitted, colstack=hundreds" $+      cmpTables "nested table hide=none, fitted, colstack=hundreds"+      (KTRH.render (cfg0 { KTR.sortKeyVals   = True+                         , KTR.rowRepeat     = False+                         , KTR.hideBlankCols = False+                         , KTR.hideBlankRows = False+                         , KTR.equisizedCols = False+                         , KTR.colStackAt    = Just "hundreds"+                         }) nestedTable)+      [sq_f|examples/hundreds_all.md|]++    , testCase "nested table hideBlank=rol,col colstack=thousands" $+      cmpTables "nested table hideBlank=row,col colstack=thousands"+        (KTRH.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False+                           , KTR.colStackAt = Just "thousands"+                           }) nestedTable)+        [sq2_f|README.md|]++    , testCase "nested table hideBlank=rol,col" $+      cmpTables "nested table hideBlank=row,col"+        (KTRH.render (cfg0 { KTR.sortKeyVals = True+                           , KTR.rowRepeat = False+                           , KTR.hideBlankCols = True+                           , KTR.hideBlankRows = True+                           , KTR.equisizedCols = False++                           }) nestedTable)+        [sq3_f|README.md|]++    , testCase "big table grouped sorted" $+      cmpTables "big table grouped sorted"+      (KTRH.render (cfg0 { KTR.sortKeyVals = True+                         , KTR.rowRepeat = False+                         , KTR.rowGroup = [ "Location", "Biome", "Category" ]+                         }) zooTable2)+      [sq_f|examples/zoo.md|]++    , testCase "big table grouped sorted no-subtype colstack" $+      let zt = KVI.fromList $+               foldl rmvSubtype [] $+               KVI.toList zooTable2+          rmvSubtype newl (keyspec, v) =+            let ks = filter (("Subtype" /=) . fst) keyspec+            in case lookup ks newl of+                 Nothing -> (ks,v) : newl+                 Just v' -> (ks, v' + v) : filter ((ks /=) . fst) newl+      in cmpTables "big table grouped sorted no-subtype colstack"+         (KTRH.render (cfg0 { KTR.sortKeyVals = True+                            , KTR.rowRepeat   = False+                            , KTR.rowGroup    = [ "Location", "Biome", "Category" ]+                            , KTR.colStackAt  = Just "Name"+                            , KTR.equisizedCols = False+                            }) zt)+         [sq2_f|examples/zoo.md|]+++    ]
+ test/SampleTables.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE OverloadedStrings #-}++module SampleTables where++import           Data.KVITable+import           Data.Text ( Text )+import qualified Data.Text as T+import           Lens.Micro ( (^.), (.~), (%~), (&) )+++listing1 :: [ ( [(Key, KeyVal)], Text ) ]+listing1 =+  [ ( [("foo", "bar"), ("moo", "cow")], "one" )+  , ( [("moo", "cow"), ("foo", "bar"), ("goose", "honk")], "two" )+  , ( [("moo", "cow")], "three" )+  , ( [("foo", "baz"), ("moo", "cow")], "four")+  ]+kvi1 = fromList listing1+kvi1_1 = fromList $ take 2 listing1+kvi1_2 = fromList $ drop 2 listing1++mediumKVI = foldl foldlInsert+            (mempty+              & keyVals .~ [ ("compiler", [ "gcc7", "gcc8", "clang6", "clang10", "clang7" ])+                           , ("debug", [ "yes", "no" ])+                           , ("optimization", [ "0", "1", "3" ])+                           ])+            [ ([("compiler", "gcc7"), ("debug", "yes"), ("optimization", "0")], "good")+            , ([("compiler", "gcc7"), ("debug", "no" ), ("optimization", "0")], "bad")+            , ([("compiler", "gcc7"), ("debug", "yes"), ("optimization", "3")], "ugly")+            , ([("compiler", "gcc8"), ("debug", "yes"), ("optimization", "0")], "good")+            , ([("compiler", "clang6"), ("debug", "yes"), ("optimization", "0")], "ok")+            , ([("compiler", "clang7"), ("debug", "no"), ("optimization", "1")], "good")+            , ([("compiler", "clang7"), ("debug", "no"), ("optimization", "3")], "good")+            , ([("compiler", "clang7"), ("debug", "yes"), ("optimization", "3")], "good")+            , ([("compiler", "clang10"), ("debug", "no"), ("optimization", "3")], "good")+            , ([("compiler", "clang10"), ("debug", "yes"), ("optimization", "3")], "good")+            , ([("compiler", "gcc8"), ("debug", "yes"), ("optimization", "3")], "true")+            , ([("compiler", "gcc8"), ("debug", "yes"), ("optimization", "1")], "bad")+            , ([("compiler", "clang7"), ("debug", "no"), ("optimization", "0")], "good")+            , ([("compiler", "gcc7"), ("debug", "no"), ("optimization", "1")], "good")+            ]++-- this table has floating values and keys with spaces+ptable = foldl foldlInsert+         (mempty & valueColName .~ "Annual Rainfall")+         [ ([("City name", "Adelaide"), ("Area", "1295"), ("Population", "1158259")], 600.5)+         , ([("City name", "Brisbane"), ("Area", "5905"), ("Population", "1857594")], 1146.4)+         , ([("City name", "Darwin"), ("Area", "112"), ("Population", "120900")], 1714.7)+         , ([("City name", "Hobart"), ("Area", "1357"), ("Population", "205556")], 619.5)+         , ([("City name", "Melbourne"), ("Area", "1566"), ("Population", "3806092")], 646.9)+         , ([("City name", "Perty"), ("Area", "5386"), ("Population", "1554769")], 869.4)+         , ([("City name", "Sydney"), ("Area", "2058"), ("Population", "4336374")], 1214.8)+         ]++-- big and complicated+zooTable = foldl foldlInsert+           (mempty & valueColName .~ "Count"+             & keyVals .~ [ ("Location", ["San Diego", "LA", "Miami", "New York"])+                          , ("Biome",    ["Savannah", "Jungle", "Polar"])+                          , ("Category", ["Animal", "Reptile", "Bird"])+                          , ("Diet",     ["Herbivore", "Carnivore"])+                          , ("Name",     [])+                          ])+           [ ([ ("Diet", "Carnivore")+              , ("Category", "Animal")+              , ("Biome", "Savannah")+              , ("Name", "Lion")+              , ("Location", "New York")], 3)+           , ([ ("Diet", "Carnivore")+              , ("Category", "Animal")+              , ("Biome", "Savannah")+              , ("Name", "Lion")+              , ("Location", "Miami")], 2)+           , ([ ("Diet", "Carnivore")+              , ("Category", "Animal")+              , ("Biome", "Savannah")+              , ("Name", "Lion")+              , ("Location", "LA")], 4)+           , ([ ("Diet", "Carnivore")+              , ("Category", "Animal")+              , ("Biome", "Savannah")+              , ("Name", "Lion")+              , ("Location", "San Diego")], 8)+           , ([ ("Location", "LA")+              , ("Biome", "Savannah")+              , ("Category", "Animal")+              , ("Name", "Giraffe")+              , ("Diet", "Herbivore")], 2)+           , ([ ("Location", "LA")+              , ("Biome", "Jungle")+              , ("Category", "Animal")+              , ("Name", "Hippo")+              , ("Diet", "Herbivore")], 1)+           , ([ ("Location", "LA")+              , ("Biome", "Savannah")+              , ("Category", "Animal")+              , ("Diet", "Herbivore")+              , ("Name", "Rhino")], 3)+           , ([ ("Location", "Miami")+              , ("Biome", "Polar")+              , ("Category", "Bird")+              , ("Diet", "Carnivore")+              , ("Subtype", "Gentoo")  -- new key+              , ("Name", "Penguin")], 20)+           , ([ ("Location", "San Diego")+              , ("Biome", "Polar")+              , ("Category", "Bird")+              , ("Diet", "Carnivore")+              , ("Subtype", "Emperor")+              , ("Name", "Penguin")], 8)+           , ([ ("Location", "San Diego")+              , ("Biome", "Polar")+              , ("Category", "Bird")+              , ("Diet", "Carnivore")+              , ("Subtype", "Gentoo")+              , ("Name", "Penguin")], 2)+           , ([ ("Location", "Miami")+              , ("Biome", "Savannah")+              , ("Category", "Animal")+              , ("Diet", "Herbivore")+              , ("Name", "Giraffe")+              , ("Subtype", "Reticulated")], 3)+           ]+zooTable2 = insert [ ("Location", "San Diego"), ("Biome", "Plains")+                   , ("Category", "Animal"), ("Subtype", "Black")+                   , ("Name", "Bear"), ("Diet", "Omnivore") ] 1 $+            insert [ ("Location", "San Diego"), ("Biome", "Plains")+                   , ("Category", "Animal"), ("Subtype", "Brown")+                   , ("Name", "Bear"), ("Diet", "Omnivore") ] 1 $+            insert [ ("Location", "San Diego"), ("Biome", "Jungle")+                   , ("Category", "Animal"), ("Subtype", "Sun")+                   , ("Name", "Bear"), ("Diet", "Omnivore") ] 1 $+            insert [ ("Location", "San Diego"), ("Biome", "Polar")+                   , ("Category", "Animal"), ("Subtype", "Polar")+                   , ("Name", "Bear"), ("Diet", "Omnivore") ] 1 $+            adjust succ ([ ("Category", "Animal")+                         , ("Diet", "Carnivore")+                         , ("Biome", "Savannah")+                         , ("Location", "San Diego")+                         , ("Name", "Lion")])+            zooTable++testedTable = foldl foldlInsert+              (mempty+                & keyVals .~ [ ("system",   ["x86_64-linux", "x86_64-darwin"])+                             , ("Branch",   ["master", "develop", "PR-feature"])+                             , ("Strategy", ["submodules", "HEADs"])+                             , ("ghcver",   ["ghc844", "ghc865", "ghc882", "ghc890"])+                             , ("debug",    ["Y", "N"])+                             ])+              $ let ls g b d v = ([ ("system", "x86_64-linux")+                                  , ("Strategy", "submodules")+                                  , ("ghcver", g), ("Branch", b)+                                  , ("debug", d) ], v)+                    lH g b d v = ([ ("system", "x86_64-linux")+                                  , ("Strategy", "HEADs")+                                  , ("ghcver", g), ("Branch", b)+                                  , ("debug", d) ], v)+                    ms g b d v = ([ ("system", "x86_64-darwin")+                                  , ("Strategy", "submodules")+                                  , ("ghcver", g), ("Branch", b)+                                  , ("debug", d) ], v)+                    mH g b d v = ([ ("system", "x86_64-darwin")+                                  , ("Strategy", "HEADs")+                                  , ("ghcver", g), ("Branch", b)+                                  , ("debug", d) ], v)+                in [ ls "ghc844" "PR-feature" "Y" "+"+                   , ls "ghc844" "PR-feature" "N" "+"+                   , lH "ghc865" "PR-feature" "N" "FAIL*2"+                   , ls "ghc882" "develop" "N" "FAIL*1"+                   , lH "ghc865" "develop" "N" "+"+                   , ls "ghc844" "master" "Y" "FAIL*1"+                   , ls "ghc844" "master" "N" "+"+                   , lH "ghc844" "master" "Y" "FAIL*1"+                   , lH "ghc865" "master" "N" "FAIL*1"+                   , lH "ghc882" "master" "N" "FAIL*1"+                   , ls "ghc865" "master" "N" "FAIL*1"+                   , lH "ghc844" "develop" "Y" "+"+                   , lH "ghc844" "develop" "N" "+"+                   , lH "ghc882" "PR-feature" "N" "FAIL*1"+                   , ls "ghc882" "PR-feature" "N" "FAIL*1"+                   , ls "ghc865" "PR-feature" "N" "+"+                   , ls "ghc844" "develop" "N" "+"+                   , lH "ghc844" "develop" "Y" "+"+                   , mH "ghc844" "develop" "N" "+"+                   ]++nestedTable = foldl foldlInsert+              (mempty+                & keyVals .~ [ ("millions",  ["0"])+                             , ("thousands", ["0"])+                             , ("hundreds",  ["0"])+                             , ("tens",      ["0"])+                             , ("ones",      ["0"])+                             ]+              )+              [ let keyvals = [("millions", T.pack $ show m)+                              ,("thousands", T.pack $ show t)+                              ,("hundreds", T.pack $ show h)+                              ,("tens", T.pack $ show d)+                              ,("ones", T.pack $ show o)]+                    value = if (o `rem` 2) == 1 then "odd" else "even"+                in (keyvals, value)+              | m <- [0..2 :: Int]+              , t <- [0..2 :: Int]+              , h <- [1..2 :: Int]+              , d <- [2..2 :: Int]+              , o <- [0..1 :: Int]+              ]
+ test/TestMain.hs view
@@ -0,0 +1,212 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Data.List ( sort )+import           Data.Maybe ( catMaybes )+import           Data.Text ( Text )+import qualified Data.Text as T+import           Lens.Micro ( (^.), (.~), (%~), (&) )+import           Test.Tasty+import           Test.Tasty.HUnit++import           Data.KVITable++import           Prelude hiding ( filter, lookup )++import           AsciiRenderTests+import           HTMLRenderTests+import           SampleTables+++main :: IO ()+main = defaultMain $+       testGroup "kvitable tests" $+       let kvi0 = mempty :: KVITable Text+       in+         [+           testCase "empty table" $ (mempty :: KVITable Bool) @=? (mempty :: KVITable Bool)++         , testCase "to and from" $ length listing1 @=? length (toList kvi1)++         , testCase "length" $ length listing1 @=? length kvi1++         , testCase "mappable" $+           sum (snd <$> (fmap T.length) <$> listing1) @=?+           sum (snd <$> toList (T.length <$> kvi1))++         , testCase "traversable" $+           Just (sort $ snd <$> listing1) @=?+           sort . fmap snd . toList <$> (traverse Just kvi1)++         , testCaseSteps "semigroup" $ \step ->+             do step "empty to full"+                mempty <> kvi1 @?= kvi1+                step "full to empty"+                kvi1 <> mempty @?= kvi1+                step "idempotent"+                kvi1 <> kvi1 @?= kvi1++                step "join parts"+                kvi1_1 <> kvi1_2 @?= kvi1  -- happens to be the right order++                -- note: it is *not* the case that+                --+                -- >   kvi1_2 <> kvi1_1 == kvi1+                --+                -- because the keyvals from a semigroup operation are+                -- built dynamically and this particular kvi1_1 /+                -- kvi1_2 will result in a different key order in+                -- reverse.  However, specifying the keyVals+                -- explicitly can eliminate key detection ordering concerns.++                step "keyed"+                let keyed = mempty & keyVals .~ [ ("foo", ["baz", "", "bar"])+                                                , ("moo", ["cow"])+                                                , ("goose", ["honk", ""])+                                                ]+                keyed <> kvi1_1 <> kvi1_2 @?= kvi1++         , testCase "empty lookup fails" $+           Nothing @=? lookup [("foo", "bar"), ("moo", "cow")] (mempty :: KVITable Bool)++         , testCase "regular lookup" $+           Just "one" @=? lookup [("foo", "bar"), ("moo", "cow")] kvi1++         , testCase "add to empty table" $+           let keyvals = [ ("foo", "bar"), ("moo", "cow") ]+           in [ (keyvals, "hi") ] @=? toList (insert keyvals "hi" kvi0)++         , testCase "getRows on empty table" $ [] @=? rows kvi0++         , testCase "rows in simple table" $+           let keyvals = [ ("foo", "bar"), ("moo", "cow") ]+           in [ (["bar", "cow"], "hi") ] @=? rows (insert keyvals "hi" kvi0)++         , testCaseSteps "non-leaf kvitable insert uses default key value" $ \step ->+             do let keyvals = [ ("foo", "bar"), ("moo", "cow") ]+                    t1 = insert keyvals "hi" kvi0++                step "at start"+                let t2 = insert [ ("moo", "dog") ] "oops" t1+                rows t2 @?= [ ([ "",    "dog" ], "oops")+                            , ([ "bar", "cow" ], "hi")+                            ]++                step "at end"+                let t3 = insert [ ("foo", "dog") ] "oops" t1+                rows t3 @?= [ ([ "dog", ""    ], "oops")+                            , ([ "bar", "cow" ], "hi")+                            ]++                step "in middle"+                let keyvals' = [ ("foo", "bar"), ("moo", "cow"), ("oink", "pig") ]+                    t1' = insert keyvals' "hi" kvi0+                    t4 = insert [ ("foo", "baz"), ("oink", "hog") ] "oops" t1'+                rows t4 @?= [ ([ "baz", "",    "hog" ], "oops")+                            , ([ "bar", "cow", "pig" ], "hi")+                            ]++         , testCaseSteps "valueColName" $ \step ->+             do step "fetch"+                "Value" @=? kvi1 ^. valueColName+                step "set"+                "says" @=? (kvi1 & valueColName .~ "says") ^. valueColName+                step "update"+                "says Value" @=? (kvi1 & valueColName %~ ("says " <>)) ^. valueColName++         , testCase "keyVals fetch" $ [ ("foo", ["baz", "", "bar"])+                                      , ("moo", ["cow"])+                                      , ("goose", ["honk", ""])+                                      ] @=? kvi1 ^. keyVals++         , testCaseSteps "lookup" $ \step ->+             do step "valid #1"+                Just "three" @=? lookup [("moo", "cow")] kvi1+                step "valid #2"+                Just "two"   @=? lookup [("goose", "honk"), ("moo", "cow"), ("foo", "bar")] kvi1+                step "valid #3"+                Just "four"  @=? lookup [("moo", "cow"), ("foo", "baz")] kvi1+                step "valid #4"+                Just "one"   @=? lookup [("moo", "cow"), ("foo", "bar")] kvi1+                step "valid with dups"+                -- Note: ok to duplicate key values with identical entries+                Just "one"   @=? lookup [("moo", "cow"), ("foo", "bar"), ("moo", "cow")] kvi1+                step "invalid #1"+                Nothing @=? lookup [("moo", "bar")] kvi1+                step "invalid #2"+                Nothing @=? lookup [("foo", "moo"), ("cow", "moo")] kvi1+                step "invalid #3"+                Nothing @=? lookup [] kvi1++         , testCase "multiply-specified lookups" $+             -- one of these will work, one will fail, but it's+             -- indeterminate which one.  This is not a recommended+             -- usage, but rather than waste computational resources+             -- to prevent it, this is simply documenting this as a+             -- known behavior and users are discouraged from using+             -- it.+             1 @=? (length $ catMaybes [ lookup [("moo", "sheep"), ("foo", "bar"), ("moo", "cow")] kvi1+                                       , lookup [("moo", "cow"), ("foo", "bar"), ("moo", "sheep")] kvi1+                                       ])++         , testCase "deep add" $+           let t0 = mempty+                    & keyVals .~ [ ("foo", ["bar", "baz"])+                                 , ("moon", ["beam", "pie"])+                                 ]+                    & valueColName .~ "says"+               t1 = insert [ ("foo", "Bill"), ("moon", "Ted"), ("dog", "arf arf") ] "Excellent!" $+                    insert [ ("foo", "baz"), ("moon", "beam"), ("dog", "woof") ] "yo" $+                    insert [ ("foo", "bar"), ("moon", "pie") ] "hi"+                    t0+           in rows t1 @?= [ ([ "Bill", "Ted", "arf arf" ], "Excellent!")+                          , ([ "bar", "pie", "" ], "hi")+                          , ([ "baz", "beam", "woof"], "yo")+                          ]++         , testCase "medium sized table rows" $+           rows mediumKVI @?=+           [ ([ "gcc7", "yes", "0"], "good" )+           , ([ "gcc7", "yes", "3"], "ugly" )+           , ([ "gcc7", "no", "0"], "bad" )+           , ([ "gcc7", "no", "1"], "good" )+           , ([ "gcc8", "yes", "0"], "good" )+           , ([ "gcc8", "yes", "1"], "bad" )+           , ([ "gcc8", "yes", "3"], "true" )+           , ([ "clang6", "yes", "0"], "ok" )+           , ([ "clang10", "yes", "3"], "good" )+           , ([ "clang10", "no", "3"], "good" )+           , ([ "clang7", "yes", "3"], "good" )+           , ([ "clang7", "no", "0"], "good" )+           , ([ "clang7", "no", "1"], "good" )+           , ([ "clang7", "no", "3"], "good" )+           ]++         , testCase "filter" $+           rows (filter (\(spec,val) -> ("compiler", "gcc7") `elem` spec) mediumKVI) @?=+           [ ([ "gcc7", "yes", "0"], "good" )+           , ([ "gcc7", "yes", "3"], "ugly" )+           , ([ "gcc7", "no", "0"], "bad" )+           , ([ "gcc7", "no", "1"], "good" )+           ]++         , testCaseSteps "zoo contents" $ \step -> do+             step "LA Lions"+             Just 4 @=? lookup [ ("Location", "LA"), ("Name", "Lion")+                               , ("Diet", "Carnivore"), ("Category", "Animal")+                               , ("Biome", "Savannah"), ("Subtype", "")+                               ] zooTable2+             step "No polar lions"+             Nothing @=? lookup [ ("Location", "LA"), ("Name", "Lion")+                                , ("Diet", "Carnivore"), ("Category", "Animal")+                                , ("Biome", "Polar"), ("Subtype", "")+                                ] zooTable2++         , testGroup "rendering"+           [+             testAsciiRendering+           , testHTMLRendering+           ]++         ]
+ test/TestQQDefs.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++module TestQQDefs where++import Data.String+import Language.Haskell.TH.Quote+++sq :: QuasiQuoter+sq = qqExtractor 1 "\n****"++sq_f = quoteFile sq+sq2_f = quoteFile $ qqExtractor 2 "\n****"+sq3_f = quoteFile $ qqExtractor 3 "\n****"++uq :: QuasiQuoter+uq = qqExtractor 1 "\n____"++uq_f = quoteFile uq+uq2_f = quoteFile $ qqExtractor 2 "\n____"+uq3_f = quoteFile $ qqExtractor 3 "\n____"++qqExtractor idx sep = QuasiQuoter (extractor idx sep)+                      (error "no patterns supported")+                      (error "no types supported")+                      (error "no declarations supported")++-- extractor :: String -> String -> QState+extractor idx sep s =+  case inSeps idx sep $ filter (/= '\r') s of+    Post a -> [|fromString a|]+    Pre n _ -> error $ "No starting line found for block " <> show n+    MatchLine n -> error $ "Only starting line found for block " <> show n+    Pass n _ -> error $ "No ending line found for block " <> show n++data QState = Pre Int String | MatchLine Int | Pass Int String | Post String++inSeps :: Int -> String -> String -> QState+inSeps idx sep =+  let sepl = length sep+      nxtC :: QState -> Char -> QState+      nxtC (Pre n p) c = let p' = c : p+                             l = length p'+                         in if reverse p' == take l sep+                            then if l == sepl then MatchLine n else Pre n p'+                            else Pre n $ take (sepl-1) p'+      nxtC p@(MatchLine n) c = if '\n' == c then Pass n "" else p+      nxtC (Pass n s) c = let s' = c : s+                              sl = reverse $ take sepl s'+                          in if sl == sep+                             then if n == 1+                                  then Post (reverse $ drop (sepl-1) s')+                                  else Pre (n - 1) ""+                             else Pass n s'+      nxtC (Post s) _ = Post s+    in foldl nxtC (Pre idx "")