kvitable 1.0.3.0 → 1.1.0.0
raw patch · 16 files changed
+1193/−664 lines, 16 filesdep +named-textdep +sayabledep −pretty-showdep −prettyprinterdep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: named-text, sayable
Dependencies removed: pretty-show, prettyprinter
Dependency ranges changed: base
API changes (from Hackage documentation)
- Data.KVITable.Render: sortWithNums :: [KeyVal] -> [KeyVal]
- Data.KVITable.Render.HTML: instance GHC.Show.Show Data.KVITable.Render.HTML.FmtVal
+ Data.KVITable: foldlInsertWith :: (v -> v -> v) -> KVITable v -> (KeySpec, v) -> KVITable v
+ Data.KVITable: insertWith :: (v -> v -> v) -> KeySpec -> v -> KVITable v -> KVITable v
+ Data.KVITable: lookup' :: KeySpec -> KVITable v -> Maybe v
+ Data.KVITable.Internal.Helpers: maxOf :: (Foldable t, Ord e) => e -> t e -> e
+ Data.KVITable.Internal.Helpers: single :: e -> [e]
+ Data.KVITable.Internal.Helpers: snoc :: [a] -> a -> [a]
+ Data.KVITable.Internal.Helpers: type Keys = [Name "Key"]
+ Data.KVITable.Render: [maxCells] :: RenderConfig -> Natural
+ Data.KVITable.Render: [maxCols] :: RenderConfig -> Natural
+ Data.KVITable.Render: sortNumericAlpha :: (KeyVals, KeyVals) -> (KeyVals, KeyVals)
+ Data.KVITable.Render.ASCII: [maxCells] :: RenderConfig -> Natural
+ Data.KVITable.Render.ASCII: [maxCols] :: RenderConfig -> Natural
+ Data.KVITable.Render.HTML: [maxCells] :: RenderConfig -> Natural
+ Data.KVITable.Render.HTML: [maxCols] :: RenderConfig -> Natural
+ Data.KVITable.Render.HTML: instance Lucid.Base.ToHtml (Data.Name.Internal.Named Data.Name.HTMLStyle nameOf)
+ Data.KVITable.Render.Internal: AndMore :: Natural -> TblHdr
+ Data.KVITable.Render.Internal: V :: KeyVal -> TblHdr
+ Data.KVITable.Render.Internal: data TblHdr
+ Data.KVITable.Render.Internal: instance Data.Name.ConvertName Data.Name.UTF8 "Key" "column header"
+ Data.KVITable.Render.Internal: instance Data.Name.ConvertName Data.Name.UTF8 "KeyVal" "column header"
+ Data.KVITable.Render.Internal: nLength :: Foldable t => t a -> Natural
+ Data.KVITable.Render.Internal: renderingKeyVals :: RenderConfig -> KeyVals -> (TblHdrs, TblHdrs)
+ Data.KVITable.Render.Internal: type TblHdrs = [(Key, [TblHdr])]
- Data.KVITable: KVITable :: KeyVals -> (Key -> KeyVal) -> Map KeySpec v -> Text -> KVITable v
+ Data.KVITable: KVITable :: KeyVals -> (Key -> KeyVal) -> Map KeySpec v -> Named HTMLStyle "column header" -> KVITable v
- Data.KVITable: type Key = Text
+ Data.KVITable: type Key = Name "Key"
- Data.KVITable: type KeyVal = Text
+ Data.KVITable: type KeyVal = Name "KeyVal"
- Data.KVITable: valueColName :: Lens' (KVITable v) Text
+ Data.KVITable: valueColName :: Lens' (KVITable v) (Named HTMLStyle "column header")
- Data.KVITable.Render: RenderConfig :: Bool -> Bool -> Bool -> Bool -> Maybe Key -> Bool -> [Key] -> Maybe Text -> RenderConfig
+ Data.KVITable.Render: RenderConfig :: Bool -> Bool -> Bool -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals)) -> Maybe Key -> Bool -> [Key] -> Maybe (Name "caption") -> Natural -> Natural -> RenderConfig
- Data.KVITable.Render: [caption] :: RenderConfig -> Maybe Text
+ Data.KVITable.Render: [caption] :: RenderConfig -> Maybe (Name "caption")
- Data.KVITable.Render: [sortKeyVals] :: RenderConfig -> Bool
+ Data.KVITable.Render: [sortKeyVals] :: RenderConfig -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals))
- Data.KVITable.Render.ASCII: RenderConfig :: Bool -> Bool -> Bool -> Bool -> Maybe Key -> Bool -> [Key] -> Maybe Text -> RenderConfig
+ Data.KVITable.Render.ASCII: RenderConfig :: Bool -> Bool -> Bool -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals)) -> Maybe Key -> Bool -> [Key] -> Maybe (Name "caption") -> Natural -> Natural -> RenderConfig
- Data.KVITable.Render.ASCII: [caption] :: RenderConfig -> Maybe Text
+ Data.KVITable.Render.ASCII: [caption] :: RenderConfig -> Maybe (Name "caption")
- Data.KVITable.Render.ASCII: [sortKeyVals] :: RenderConfig -> Bool
+ Data.KVITable.Render.ASCII: [sortKeyVals] :: RenderConfig -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals))
- Data.KVITable.Render.ASCII: render :: Pretty v => RenderConfig -> KVITable v -> Text
+ Data.KVITable.Render.ASCII: render :: Sayable "normal" v => RenderConfig -> KVITable v -> Text
- Data.KVITable.Render.HTML: RenderConfig :: Bool -> Bool -> Bool -> Bool -> Maybe Key -> Bool -> [Key] -> Maybe Text -> RenderConfig
+ Data.KVITable.Render.HTML: RenderConfig :: Bool -> Bool -> Bool -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals)) -> Maybe Key -> Bool -> [Key] -> Maybe (Name "caption") -> Natural -> Natural -> RenderConfig
- Data.KVITable.Render.HTML: [caption] :: RenderConfig -> Maybe Text
+ Data.KVITable.Render.HTML: [caption] :: RenderConfig -> Maybe (Name "caption")
- Data.KVITable.Render.HTML: [sortKeyVals] :: RenderConfig -> Bool
+ Data.KVITable.Render.HTML: [sortKeyVals] :: RenderConfig -> Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals))
- Data.KVITable.Render.HTML: render :: Pretty v => RenderConfig -> KVITable v -> Text
+ Data.KVITable.Render.HTML: render :: Sayable "html" v => RenderConfig -> KVITable v -> Text
Files
- CHANGELOG.md +9/−1
- README.md +143/−143
- examples/hundreds_all.md +35/−35
- examples/zoo.md +115/−115
- kvitable.cabal +11/−7
- src/Data/KVITable.hs +84/−37
- src/Data/KVITable/Internal/Helpers.hs +18/−0
- src/Data/KVITable/Render.hs +83/−15
- src/Data/KVITable/Render/ASCII.hs +162/−117
- src/Data/KVITable/Render/HTML.hs +220/−149
- src/Data/KVITable/Render/Internal.hs +125/−0
- test/AsciiRenderTests.hs +90/−28
- test/HTMLRenderTests.hs +77/−12
- test/SampleTables.hs +6/−5
- test/TestMain.hs +12/−0
- test/TestQQDefs.hs +3/−0
CHANGELOG.md view
@@ -1,6 +1,14 @@ # Revision history for KVITable -## 1.0 3.0 -- 2024-02-22+## 1.1.0.0 -- 2024-09-18++* The `RenderConfig.sortKeyVals` changed from a `Boolean` to a `Maybe fn`, where+ the `fn` is a sorting function which is provided a `(rowKeyvals, colKeyvals)`+ tuple and should return that tuple, sorted. The `sortNumericAlpha` function+ can be used to obtain the old 'True' behavior.+* Support GHC 9.10.++## 1.0.3.0 -- 2024-02-22 * Support GHC 9.8. * Internal updates to confirm safety and avoid partial functions.
README.md view
@@ -83,87 +83,87 @@ <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 rowspan="3" class="kvitable_th"><div><span>millions</span></div></th>+ <th rowspan="3" class="kvitable_th"><div><span>thousands</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th> <th class="rightlabel kvitable_th"> ←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>+ <th rowspan="3" class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></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>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th class="kvitable_th"><div><span>2</span></div></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>+ <th rowspan="3" class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></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>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th class="kvitable_th"><div><span>2</span></div></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>+ <th rowspan="3" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></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>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th class="kvitable_th"><div><span>2</span></div></th> <td class="kvitable_td">even</td> <td class="kvitable_td">odd</td> <td class="kvitable_td">even</td>@@ -200,49 +200,49 @@ <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 rowspan="4" class="kvitable_th"><div><span>millions</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>0</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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 colspan="2" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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 colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th> <th class="rightlabel kvitable_th"> ←ones</th> </tr> </thead> <tbody class="kvitable_body"> <tr class="kvitable_tr">- <th class="kvitable_th">0</th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> <td class="kvitable_td">odd</td> <td class="kvitable_td">even</td>@@ -257,7 +257,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></th> <td class="kvitable_td">even</td> <td class="kvitable_td">odd</td> <td class="kvitable_td">even</td>@@ -272,7 +272,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">2</th>+ <th class="kvitable_th"><div><span>2</span></div></th> <td class="kvitable_td">even</td> <td class="kvitable_td">odd</td> <td class="kvitable_td">even</td>@@ -347,205 +347,205 @@ <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>+ <th class="kvitable_th"><div><span>millions</span></div></th>+ <th class="kvitable_th"><div><span>thousands</span></div></th>+ <th class="kvitable_th"><div><span>hundreds</span></div></th>+ <th class="kvitable_th"><div><span>tens</span></div></th>+ <th class="kvitable_th"><div><span>ones</span></div></th>+ <th class="kvitable_th"><div><span>Value</span></div></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>+ <th rowspan="12" class="kvitable_th"><div><span>0</span></div></th>+ <th rowspan="4" class="kvitable_th"><div><span>0</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="12" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="4" class="kvitable_th"><div><span>0</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="12" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="4" class="kvitable_th"><div><span>0</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="4" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>1</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></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>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th rowspan="2" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td">even</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></th> <td class="kvitable_td">odd</td> </tr> </tbody>
examples/hundreds_all.md view
@@ -38,42 +38,42 @@ <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 rowspan="3" class="kvitable_th"><div><span>millions</span></div></th>+ <th rowspan="3" class="kvitable_th"><div><span>thousands</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>0</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>1</span></div></th>+ <th colspan="4" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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 colspan="2" class="kvitable_th multicol"><div><span>0</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>0</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>0</span></div></th>+ <th colspan="2" class="kvitable_th multicol"><div><span>2</span></div></th> <th class="rightlabel kvitable_th"> ←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="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>1</span></div></th> <th class="rightlabel kvitable_th"> ←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>+ <th rowspan="3" class="kvitable_th"><div><span>0</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -88,7 +88,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -103,7 +103,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">2</th>+ <th class="kvitable_th"><div><span>2</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -118,8 +118,8 @@ <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>+ <th rowspan="3" class="kvitable_th"><div><span>1</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -134,7 +134,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -149,7 +149,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">2</th>+ <th class="kvitable_th"><div><span>2</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -164,8 +164,8 @@ <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>+ <th rowspan="3" class="kvitable_th"><div><span>2</span></div></th>+ <th class="kvitable_th"><div><span>0</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -180,7 +180,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">1</th>+ <th class="kvitable_th"><div><span>1</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>@@ -195,7 +195,7 @@ <td class="kvitable_td">odd</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th">2</th>+ <th class="kvitable_th"><div><span>2</span></div></th> <td class="kvitable_td"></td> <td class="kvitable_td"></td> <td class="kvitable_td"></td>
examples/zoo.md view
@@ -64,122 +64,122 @@ <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>+ <th class="kvitable_th"><div><span>Location</span></div></th>+ <th class="kvitable_th"><div><span>Biome</span></div></th>+ <th class="kvitable_th"><div><span>Category</span></div></th>+ <th class="kvitable_th"><div><span>Diet</span></div></th>+ <th class="kvitable_th"><div><span>Name</span></div></th>+ <th class="kvitable_th"><div><span>Subtype</span></div></th>+ <th class="kvitable_th"><div><span>Count</span></div></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>+ <th class="kvitable_th last_in_group" rowspan=4><div><span>LA</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Jungle</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Herbivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Hippo</span></div></th>+ <th class="kvitable_th last_in_group"><div><span></span></div></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>+ <th class="kvitable_th last_in_group" rowspan=3><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group" rowspan=3><div><span>Animal</span></div></th>+ <th class="kvitable_th"><div><span>Carnivore</span></div></th>+ <th class="kvitable_th"><div><span>Lion</span></div></th>+ <th class="kvitable_th"><div><span></span></div></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>+ <th class="kvitable_th" rowspan=2><div><span>Herbivore</span></div></th>+ <th class="kvitable_th"><div><span>Giraffe</span></div></th>+ <th class="kvitable_th"><div><span></span></div></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>+ <th class="kvitable_th last_in_group"><div><span>Rhino</span></div></th>+ <th class="kvitable_th last_in_group"><div><span></span></div></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>+ <th class="kvitable_th last_in_group" rowspan=3><div><span>Miami</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Polar</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Bird</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Penguin</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Gentoo</span></div></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>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Animal</span></div></th>+ <th class="kvitable_th"><div><span>Carnivore</span></div></th>+ <th class="kvitable_th"><div><span>Lion</span></div></th>+ <th class="kvitable_th"><div><span></span></div></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>+ <th class="kvitable_th last_in_group"><div><span>Herbivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Giraffe</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Reticulated</span></div></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>+ <th class="kvitable_th last_in_group"><div><span>New York</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Lion</span></div></th>+ <th class="kvitable_th last_in_group"><div><span></span></div></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>+ <th class="kvitable_th last_in_group" rowspan=7><div><span>San Diego</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Jungle</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Omnivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Bear</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Sun</span></div></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>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Plains</span></div></th>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Animal</span></div></th>+ <th class="kvitable_th" rowspan=2><div><span>Omnivore</span></div></th>+ <th class="kvitable_th" rowspan=2><div><span>Bear</span></div></th>+ <th class="kvitable_th"><div><span>Black</span></div></th> <td class="kvitable_td">1</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th last_in_group">Brown</th>+ <th class="kvitable_th last_in_group"><div><span>Brown</span></div></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>+ <th class="kvitable_th last_in_group" rowspan=3><div><span>Polar</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Omnivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Bear</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Polar</span></div></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>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Bird</span></div></th>+ <th class="kvitable_th" rowspan=2><div><span>Carnivore</span></div></th>+ <th class="kvitable_th" rowspan=2><div><span>Penguin</span></div></th>+ <th class="kvitable_th"><div><span>Emperor</span></div></th> <td class="kvitable_td">8</td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th last_in_group">Gentoo</th>+ <th class="kvitable_th last_in_group"><div><span>Gentoo</span></div></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>+ <th class="kvitable_th last_in_group"><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Lion</span></div></th>+ <th class="kvitable_th last_in_group"><div><span></span></div></th> <td class="kvitable_td last_in_group">9</td> </tr> </tbody>@@ -228,25 +228,25 @@ <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="kvitable_th"><div><span>Location</span></div></th>+ <th class="kvitable_th"><div><span>Biome</span></div></th>+ <th class="kvitable_th"><div><span>Category</span></div></th>+ <th class="kvitable_th"><div><span>Diet</span></div></th>+ <th class="kvitable_th"><div><span>Bear</span></div></th>+ <th class="kvitable_th"><div><span>Giraffe</span></div></th>+ <th class="kvitable_th"><div><span>Hippo</span></div></th>+ <th class="kvitable_th"><div><span>Lion</span></div></th>+ <th class="kvitable_th"><div><span>Penguin</span></div></th>+ <th class="kvitable_th"><div><span>Rhino</span></div></th> <th class="rightlabel kvitable_th"> ←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>+ <th class="kvitable_th last_in_group" rowspan=3><div><span>LA</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Jungle</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Herbivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">@@ -260,9 +260,9 @@ </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 last_in_group" rowspan=2><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Animal</span></div></th>+ <th class="kvitable_th"><div><span>Carnivore</span></div></th> <td class="kvitable_td"> </td> <td class="kvitable_td">@@ -276,7 +276,7 @@ </td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th last_in_group">Herbivore</th>+ <th class="kvitable_th last_in_group"><div><span>Herbivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">2</td>@@ -289,10 +289,10 @@ <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" rowspan=3><div><span>Miami</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Polar</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Bird</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">@@ -306,9 +306,9 @@ </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 last_in_group" rowspan=2><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Animal</span></div></th>+ <th class="kvitable_th"><div><span>Carnivore</span></div></th> <td class="kvitable_td"> </td> <td class="kvitable_td">@@ -322,7 +322,7 @@ </td> </tr> <tr class="kvitable_tr">- <th class="kvitable_th last_in_group">Herbivore</th>+ <th class="kvitable_th last_in_group"><div><span>Herbivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">3</td>@@ -336,10 +336,10 @@ </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"><div><span>New York</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">@@ -353,10 +353,10 @@ </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>+ <th class="kvitable_th last_in_group" rowspan=5><div><span>San Diego</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Jungle</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Omnivore</span></div></th> <td class="kvitable_td last_in_group">1</td> <td class="kvitable_td last_in_group"> </td>@@ -370,9 +370,9 @@ </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>+ <th class="kvitable_th last_in_group"><div><span>Plains</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Omnivore</span></div></th> <td class="kvitable_td last_in_group">2</td> <td class="kvitable_td last_in_group"> </td>@@ -386,9 +386,9 @@ </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>+ <th class="kvitable_th last_in_group" rowspan=2><div><span>Polar</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Omnivore</span></div></th> <td class="kvitable_td last_in_group">1</td> <td class="kvitable_td last_in_group"> </td>@@ -402,8 +402,8 @@ </td> </tr> <tr class="kvitable_tr">- <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"><div><span>Bird</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></th> <td class="kvitable_td last_in_group"> </td> <td class="kvitable_td last_in_group">@@ -417,9 +417,9 @@ </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"><div><span>Savannah</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Animal</span></div></th>+ <th class="kvitable_th last_in_group"><div><span>Carnivore</span></div></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>
kvitable.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: kvitable-version: 1.0.3.0+version: 1.1.0.0 synopsis: Key/Value Indexed Table container and formatting library description: .@@ -46,18 +46,23 @@ library hs-source-dirs: src default-language: Haskell2010- GHC-options: -Wall -Wcompat -Wincomplete-uni-patterns -Wpartial-fields -fhide-source-paths- -- other-extensions:+ GHC-options: -Wall+ -Wcompat+ -Wincomplete-uni-patterns+ -Wpartial-fields+ -fhide-source-paths exposed-modules: Data.KVITable+ , Data.KVITable.Internal.Helpers , Data.KVITable.Render , Data.KVITable.Render.ASCII , Data.KVITable.Render.HTML- -- other-modules:- build-depends: base >=4.12 && <4.20+ , Data.KVITable.Render.Internal+ build-depends: base >=4.12 && <4.21 , containers , lucid >= 2.9 && < 2.12 , microlens >= 0.4 && < 0.5- , prettyprinter >= 1.7 && < 1.8+ , named-text >= 1.2 && < 1.3+ , sayable >= 1.2.4.0 && < 1.3 , text test-suite test-kvitable@@ -74,7 +79,6 @@ , html-parse , kvitable , microlens- , pretty-show , tasty , tasty-hunit , template-haskell
src/Data/KVITable.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} @@ -17,11 +18,14 @@ , fromList , toList , Data.KVITable.lookup+ , lookup' , keyVals , keyValGen , valueColName , insert+ , insertWith , foldlInsert+ , foldlInsertWith , Data.KVITable.filter , adjust , adjustWithKey@@ -35,11 +39,13 @@ import Data.Function ( on ) import qualified Data.List as L import qualified Data.Map as Map-import Data.Text ( Text )+import Data.Name import qualified GHC.Exts import Lens.Micro ( Lens' ) +import Data.KVITable.Internal.Helpers + -- | 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.@@ -62,7 +68,7 @@ -- the defaultKeyVal for any keys not explicitly provided for that -- value. - , valuecolName :: Text -- ^ name of the value cells+ , valuecolName :: Named HTMLStyle "column header" -- ^ name of the value cells } instance Eq v => Eq (KVITable v) where@@ -78,11 +84,11 @@ -- | 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+type Key = Name "Key" -- | 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+type KeyVal = Name "KeyVal" -- | The 'KeySpec' is the list of tuples and defines the unique key -- for a value in the 'KVITable'.@@ -150,19 +156,19 @@ 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.+-- | 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; existing entries in the table under the old keys are NOT+-- migrated to the new table entries. 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+ let inKVS spec _val =+ length kvs == length spec+ && and (keymatch <$> zip spec kvs)+ keymatch ((sk,sv), (k,vs)) = sk == k && sv `elem` vs in Map.filterWithKey inKVS (contents t) } ) <$> f (keyvals t)@@ -176,7 +182,7 @@ -- | Fetch or set the column name for the actual value cell in the -- 'KVITable'. -valueColName :: Lens' (KVITable v) Text+valueColName :: Lens' (KVITable v) (Named HTMLStyle "column header") valueColName f t = (\n -> t { valuecolName = n } ) <$> f (valuecolName t) @@ -186,6 +192,13 @@ -- 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.+--+-- The performance of lookup degrades exponentially on a sparse KVITable: an+-- unset KVITable entry is indistinguishable from a non-normalized keyspec, so if+-- the initial query fails, the keyspec is normalized and then the lookup is+-- re-attempted. This may be fine for an individual query, but for large numbers+-- of queries this is excessively slow. To mitigate this, the lookup' can be+-- used, which assumes the use of a normalized key. lookup :: KeySpec -> KVITable v -> Maybe v lookup keyspec t = case Map.lookup keyspec $ contents t of@@ -198,11 +211,20 @@ 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)+ Just v ->+ if v `elem` vs then snoc s (k,v)+ else s -- no level added, so this should never match in the Map+ Nothing -> snoc s (k, keyvalGen t k)+ in foldl keyandval mempty (keyvals t) ++-- | Like 'lookup', but assumes a normalized key (all key elements specified, and+-- in the proper order). Faster than 'lookup', but will return false negatives+-- if not used with a normalized key.++lookup' :: KeySpec -> KVITable v -> Maybe v+lookup' keyspec = Map.lookup keyspec . contents+ -- | Inserts a new cell value into the table at the specified keyspec -- location. The keyspec may be minimally specified and out-of-order. --@@ -210,10 +232,25 @@ -- for the table. In general, insertion is expected to be less -- frequent than lookups so computation costs are biased towards the -- insertion operation.+--+-- This is equivalent to @insertWith const@. insert :: KeySpec -> v -> KVITable v -> KVITable v-insert keyspec val t = endset t val (keyvals t) keyspec [] []+insert = insertWith const +-- | Inserts a new cell value into the table at the specified keyspec+-- location, if no value exists at that location. Otherwise, the old and+-- new value are combined via @f new old@. 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.++insertWith :: (v -> v -> v) -> KeySpec -> v -> KVITable v -> KVITable v+insertWith f keyspec val t = endsetWith f t val (keyvals t) keyspec mempty mempty+ remainingKeyValDefaults :: KVITable v -> [(Key,a)] -> KeySpec remainingKeyValDefaults t = fmap (\(k,_) -> (k, keyvalGen t k)) @@ -222,18 +259,18 @@ then e else (k, keyvalGen t k : vs) -endset :: KVITable v -> v -> KeyVals -> KeySpec -> KeySpec -> KeyVals -> KVITable v-endset t val rkv [] tspec kvbld =+endsetWith :: (v -> v -> v) -> KVITable v -> v -> KeyVals -> KeySpec -> KeySpec -> KeyVals -> KVITable v+endsetWith f 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)+ in t { contents = Map.insertWith f spec val (contents t) , keyvals = kvbld <> (addDefVal t <$> rkv) } -endset t val [] spec tspec kvbld =+endsetWith f 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@@ -241,46 +278,56 @@ -- default values for the new keys in their keyspec. let spec' = tspec <> spec keySpecElemToKeyVals (k,v) = (k, if null curTblList- then [v]+ then single 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+ in t { contents = Map.insertWith f spec' val $ Map.fromList updTblList , keyvals = keyvals' } -endset t val kvs@((k,vs):rkvs) ((sk,sv):srs) tspec kvbld =+endsetWith f 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'+ then snoc kvbld (k, vs)+ else snoc kvbld (k, sv : vs)+ in endsetWith f t val rkvs srs (snoc 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+ then endsetWith f t val kvs (snoc srs (sk,sv)) tspec kvbld else if any (`elem` (fst <$> kvs)) (fst <$> srs)- then endset t val kvs (srs <> [(sk,sv)]) tspec kvbld+ then endsetWith f t val kvs (snoc 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')])+ in endsetWith f t val rkvs ((sk,sv):srs) (snoc tspec (k,defVal)) (snoc kvbld (k,vs')) --- | The foldlInsert is a convenience function that can be specified+-- | 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.+--+-- This is equivalent to @foldlInsertWith const@. foldlInsert :: KVITable v -> (KeySpec, v) -> KVITable v-foldlInsert t (k,v) = insert k v t+foldlInsert = foldlInsertWith const +-- | foldlInsertWith is a convenience function that, when curried with a+-- "combining" function, can be specified as the function argument of a foldl+-- operation over the list form of a KVITable to generate the associated+-- KVITable. +foldlInsertWith :: (v -> v -> v) -> KVITable v -> (KeySpec, v) -> KVITable v+foldlInsertWith f t (k,v) = insertWith f k v t++ -- | Filter 'KVITable' to retain only the elements that satisfy some predicate. filter :: ((KeySpec, v) -> Bool) -> KVITable v -> KVITable v@@ -328,16 +375,16 @@ 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+-- list structure, where each returned 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) []+rows t = go (keyvals t) mempty where go [] spec = let spec' = reverse spec in case Map.lookup spec' (contents t) of- Nothing -> []- Just v -> [ (snd <$> spec', v) ]+ Nothing -> mempty+ Just v -> single (snd <$> spec', v) go ((key, vals):kvs) spec = concatMap (\v -> let spec' = (key,v):spec in go kvs spec') vals
+ src/Data/KVITable/Internal/Helpers.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE DataKinds #-}++module Data.KVITable.Internal.Helpers where++import Data.Name ( Name )+++type Keys = [Name "Key"]+++single :: e -> [e]+single = (:[])++snoc :: [a] -> a -> [a]+snoc l e = l <> [e]++maxOf :: (Foldable t, Ord e) => e -> t e -> e+maxOf v l = if null l then v else max v $ maximum l
src/Data/KVITable/Render.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DataKinds #-}+ -- | Common definitions (and support functions) for rendering a -- 'KVITable'. @@ -5,15 +7,18 @@ ( RenderConfig(..) , defaultRenderConfig- , sortWithNums+ , sortNumericAlpha ) where -import Data.KVITable import qualified Data.List as L-import Data.Text ( Text )+import Data.Name ( Name, nameText, nameLength, nullName ) import qualified Data.Text as T+import Numeric.Natural +import Data.KVITable++ -- | Returns the default rendering configuration, to be used with a -- format-specific @render@ call. @@ -22,16 +27,18 @@ { hideBlankRows = True , hideBlankCols = True , equisizedCols = True- , sortKeyVals = False+ , sortKeyVals = Nothing , colStackAt = Nothing , rowRepeat = True , rowGroup = [] , caption = Nothing+ , maxCells = 40000 -- even this is probably too big+ , maxCols = 40000 -- by default, this should be >= maxCells } -- | 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+-- The 'RenderConfig' is global to all formats, although some of the -- fields in the 'RenderConfig' will be ignored as not-applicable by -- some formats. @@ -50,13 +57,31 @@ -- (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.+ , sortKeyVals :: Maybe ((KeyVals, KeyVals) -> (KeyVals, KeyVals))+ -- ^ Specifies a function to sort the KeyVals (rows and columns,+ -- respectively) for the output. If no function provided, no sorting is+ -- performed. The provided KeyVals are in the order in which the Keys are+ -- declared to the KVITable; the values for each key may be in an arbitrary+ -- order. The usual expectation is that this will only sort the values for+ -- each Key, but it is allowed to re-order the Keys as well.+ --+ -- Sorting is done *prior* to applying maxCells and maxCols limitations; this+ -- provides the unsurprising results for the user but means that this+ -- function may perform extra work to sort rows and columns that will not be+ -- shown.+ --+ -- It is permissible to move entries from the rows to the columns and+ -- vice-versa; this function has control over the shape of the resulting+ -- table, which includes redistributing the rows and columns such that the+ -- result will violate maxCols (but not maxCells).+ --+ -- The columns keyvals will be empty if colStackAt is Nothing. , colStackAt :: Maybe Key- -- ^ Column key to begin stacking keys in columns and sub-columns- -- rather than creating additional sub-rows.+ -- ^ Column key to begin stacking keys in columns and sub-columns rather than+ -- creating additional sub-rows. This is performed on the+ -- originally-specified key order, prior to calling any provided+ -- 'sortKeyVals' function. , rowRepeat :: Bool -- ^ 'True' (default) if an identical 'KeyVal' is to be repeated@@ -66,13 +91,56 @@ -- ^ List of Key names that should by grouped by inserting -- horizontal row lines between KeyVals - , caption :: Maybe Text+ , caption :: Maybe (Name "caption") -- ^ Caption to render for table for backends which support -- captions; otherwise ignored.++ , maxCells :: Natural+ -- ^ The maximum number of cells that will be rendered. The size of the+ -- table is #rows times #cols, so a 100x100 table is 10000 entries. This+ -- value is used to limit the size of the rendered table to provide a+ -- reasonable output in a reasonable amount of time and memory; tables larger+ -- than this limit will return a "table too big" message when rendered.+ --+ -- ASCII: At least 1 character for the cell, plus 3.5 characters for+ -- boundaries means that even on an ultra-wide monitor with a very small+ -- font, the table isn't very readable beyond 30-40 columns. In addition,+ -- the algorithm needs to continually adjust table column widths to+ -- accomodate new values, so there is a great deal of backtracking involved+ -- and the time taken to render grows quite quickly.+ --+ -- HTML: A browser has reasonable constraints on displaying a table: 10000+ -- entries is difficult for the user to comprehend, but the browser is+ -- probably reasonably performant. At 500x500, the browser is likely to be+ -- very sluggish, with visible delays in rendering visible regions during+ -- scrolling.++ , maxCols :: Natural+ -- ^ The maximum number of columns to render. This limit is only useful if+ -- it is set to less than the 'maxCells' value, and it is useful in that case+ -- to ensure that more than one (partial) row is displayed. The 'maxCells'+ -- value takes priority over this value. See the 'maxCells' for more+ -- information. } +-- | Table KeyVals sorting function; alphanumeric with numeric preference. This+-- can be used as the sortKeyVals function for the table.+--+-- 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. ++sortNumericAlpha :: (KeyVals, KeyVals) -> (KeyVals, KeyVals)+sortNumericAlpha (rowkeys, colkeys) = ( fmap sortWithNums <$> rowkeys+ , fmap sortWithNums <$> colkeys+ )++ -- | 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@@ -87,10 +155,10 @@ 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']+ rank e = if (not $ nullName e) &&+ or [ T.head (nameText e) `elem` ['0'..'9']+ , T.last (nameText e) `elem` ['0'..'9'] ]- then T.length e+ then nameLength e else 0 in snd <$> L.sort skvs
src/Data/KVITable/Render/ASCII.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -17,14 +19,20 @@ where import qualified Data.List as L-import Data.Maybe ( fromMaybe, isNothing )+import Data.Maybe ( isNothing )+import Data.Name+import Data.String ( fromString ) import Data.Text ( Text ) import qualified Data.Text as T import Lens.Micro ( (^.) )-import qualified Prettyprinter as PP+import Numeric.Natural+import Text.Sayable -import Data.KVITable+import Data.KVITable ( KVITable, KeySpec, keyVals )+import qualified Data.KVITable as KVIT+import Data.KVITable.Internal.Helpers import Data.KVITable.Render+import Data.KVITable.Render.Internal import Prelude hiding ( lookup ) @@ -32,27 +40,27 @@ -- | Renders the specified table in ASCII format, using the specified -- 'RenderConfig' controls. -render :: PP.Pretty v => RenderConfig -> KVITable v -> Text+render :: Sayable "normal" 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+ let kmap = renderingKeyVals cfg $ t ^. keyVals+ (fmt, hdr) = renderHdrs cfg t kmap+ bdy = renderSeq cfg fmt kmap t in T.unlines $ hdr <> bdy ---------------------------------------------------------------------- -data FmtLine = FmtLine [Int] Sigils Sigils -- last is for sepline+data FmtLine = FmtLine [Natural] Sigils Sigils -- last is for sepline data Sigils = Sigils { sep :: Text, pad :: Text, cap :: Text } -fmtLine :: [Int] -> FmtLine+fmtLine :: [Natural] -> FmtLine fmtLine cols = FmtLine cols Sigils { sep = "|", pad = " ", cap = "_" } Sigils { sep = "+", pad = "-", cap = "_" } -fmtColCnt :: FmtLine -> Int-fmtColCnt (FmtLine cols _ _) = length cols+fmtColCnt :: FmtLine -> Natural+fmtColCnt (FmtLine cols _ _) = nLength cols -perColOvhd :: Int+perColOvhd :: Natural perColOvhd = 2 -- pad chars on either side of each column's entry -- | Formatted width of output, including pad on either side of each@@ -61,18 +69,19 @@ -- 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 -> Natural fmtWidth (FmtLine cols _ _) = let cols' = L.filter (/= 0) cols- in sum cols' + ((perColOvhd + 1) * (length cols' - 1))+ in sum cols' + ((perColOvhd + 1) * (nLength cols' - 1)) fmtEmptyCols :: FmtLine -> Bool fmtEmptyCols (FmtLine cols _ _) = sum cols == 0 -fmtAddColLeft :: Int -> FmtLine -> FmtLine+fmtAddColLeft :: Natural -> FmtLine -> FmtLine fmtAddColLeft leftCol (FmtLine cols s s') = FmtLine (leftCol : cols) s s' data FmtVal = Separator | TxtVal Text | CenterVal Text+ | Overage -- cells for the overflow row/column fmtRender :: FmtLine -> [FmtVal] -> Text fmtRender (FmtLine _cols _sigils _sepsigils) [] = ""@@ -82,20 +91,28 @@ Separator -> f sepsigils TxtVal _ -> f sigils CenterVal _ -> f sigils+ Overage -> f sigils l = sig sep val+ charRepeat n c = T.pack (replicate (fromEnum n) c)+ rightAlign n t = let tl = toEnum $ T.length t+ rt = charRepeat (n - tl) ' ' <> t+ in if tl >= n then t else rt+ centerIn n t = let tl = toEnum $ T.length t+ (w,e) = (n - tl - 2) `divMod` 2+ m = cap sigils+ ls = T.replicate (fromEnum $ w + 0) m+ rs = T.replicate (fromEnum $ w + e) m+ in if tl + 2 >= n+ then rightAlign n t+ else ls <> " " <> t <> " " <> rs 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+ Separator -> charRepeat sz '-'+ TxtVal v -> rightAlign sz v+ CenterVal t -> centerIn sz t+ Overage -> centerIn sz "+" ) <> sig pad fld <> sig sep fld -- KWQ or if next fld is Nothing@@ -109,75 +126,83 @@ data HeaderLine = HdrLine FmtLine HdrVals Trailer type HdrVals = [FmtVal]-type Trailer = Text+type Trailer = Name "column header" hdrFmt :: HeaderLine -> FmtLine hdrFmt (HdrLine fmt _ _) = fmt -renderHdrs :: PP.Pretty v => RenderConfig -> KVITable v -> [Key] -> (FmtLine, [Text])-renderHdrs cfg t keys =+renderHdrs :: Sayable "normal" v+ => RenderConfig -> KVITable v -> (TblHdrs, TblHdrs)+ -> (FmtLine, [Text])+renderHdrs cfg t kmap = ( lastFmt , [ fmtRender fmt hdrvals- <> (if T.null trailer then "" else (" <- " <> trailer))+ <> (if nullName trailer then "" else (" <- " <> nameText trailer)) | (HdrLine fmt hdrvals trailer) <- hrows ] <>- [ fmtRender lastFmt (replicate (fmtColCnt lastFmt) Separator) ])+ (single $ let sz = fromEnum $ fmtColCnt lastFmt+ in fmtRender lastFmt (replicate sz Separator)) ) where- hrows = hdrstep cfg t keys+ hrows = hdrstep cfg t kmap lastFmt = case reverse hrows of- [] -> fmtLine []+ [] -> fmtLine mempty (hrow:_) -> hdrFmt hrow -hdrstep :: PP.Pretty v => RenderConfig -> KVITable v -> [Key] -> [HeaderLine]-hdrstep _cfg t [] =+hdrstep :: Sayable "normal" v+ => RenderConfig+ -> KVITable v+ -> (TblHdrs, TblHdrs)+ -> [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+ let valcoltxt = t ^. KVIT.valueColName+ valcoltsz = nameLength valcoltxt+ valsizes = nLength . sez @"normal" . snd <$> KVIT.toList t+ valwidth = maxOf 0 $ valcoltsz : valsizes+ hdrVal = TxtVal $ nameText valcoltxt+ in single $ HdrLine (fmtLine $ single valwidth) (single hdrVal) ""+hdrstep cfg t ([], colKeyMap) =+ hdrvalstep cfg t colKeyMap mempty -- switch to column-stacking mode+hdrstep cfg t ((key,keyvals) : keys, colKeyMap) =+ let keyw = max (nameLength key)+ $ (maxOf 0 . fmap (nameLength . toHdrText)) keyvals+ mkhdr (hs, v) (HdrLine fmt hdrvals trailer) =+ ( HdrLine (fmtAddColLeft keyw fmt) (TxtVal (nameText v) : hdrvals) trailer : hs , "")+ in reverse $ fst $ foldl mkhdr (mempty, key) $ hdrstep cfg t (keys, colKeyMap)+ -- 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+hdrvalstep :: Sayable "normal" v+ => RenderConfig -> KVITable v -> TblHdrs -> KeySpec+ -> [HeaderLine]+hdrvalstep cfg t ((key,titles) : []) steppath =+ let cvalWidths = \case+ V kv -> fmap (nLength . sez @"normal" . snd)+ $ filter ((L.isSuffixOf (snoc steppath (key, kv))) . fst)+ $ KVIT.toList t+ AndMore _ -> [1] -- always show this column, although it has no contents colWidth kv = let cvw = cvalWidths kv- in if and [ hideBlankCols cfg, sum cvw == 0 ]- then 0- else maximum $ T.length kv : cvw+ in if hideBlankCols cfg && sum cvw == 0+ then 0+ else maxOf (nameLength $ toHdrText kv) cvw cwidths = fmap colWidth titles fmtcols = if equisizedCols cfg- then (replicate (length cwidths) (maximum cwidths))+ then (replicate (length cwidths) (maxOf 0 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)+ tr = convertName key+ hdrTxt = nameText . toHdrText+ in single $ HdrLine (fmtLine $ fmtcols) (TxtVal . hdrTxt <$> titles) tr+hdrvalstep cfg t ((key,vals) : keys) steppath =+ let subhdrsV = \case+ V v -> hdrvalstep cfg t keys (snoc steppath (key,v))+ _ -> mempty+ subTtlHdrs = let subAtVal v = (nameLength $ toHdrText v, subhdrsV v) in fmap subAtVal vals szexts = let subW (hl,sh) = case sh of [] -> (0, 0) -- should never be the case (sh0:_) -> let sv = fmtWidth $ hdrFmt sh0- in if and [ hideBlankCols cfg,- fmtEmptyCols $ hdrFmt sh0- ]+ in if hideBlankCols cfg && (fmtEmptyCols $ hdrFmt sh0) then (0, 0) else (hl, sv) in fmap (uncurry max . subW) subTtlHdrs@@ -185,68 +210,88 @@ L.transpose $ fmap (uncurry rsz_hdrstack) $ zip szhdrs $ fmap snd subTtlHdrs- largest = maximum szexts+ largest = maxOf 0 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))+ numNZCols = nLength nzCols+ pcw = sum nzCols + ((perColOvhd + 1) * (numNZCols - 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+ else max 0 (hw - pcw) `divMod` numNZCols+ c' = fst $ foldl (\(c'',n) w -> (snoc c'' $ n+w, ew)) (mempty,ew+w0) c in HdrLine (FmtLine c' s j) v r- hdrJoin hl = foldl hlJoin (HdrLine (fmtLine []) [] "") hl+ hdrJoin hl = foldl hlJoin (HdrLine (fmtLine mempty) mempty "") 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+ tvals = CenterVal . nameText . toHdrText <$> vals+ in HdrLine (fmtLine szhdrs) tvals (convertName key) : rsz_extsubhdrs+hdrvalstep _ _ [] _ = error "ASCII hdrvalstep with empty keys after matching colStackAt -- impossible" -renderSeq :: PP.Pretty v => RenderConfig -> FmtLine -> [Key] -> KVITable v -> [Text]-renderSeq cfg fmt keys kvitbl = fmtRender fmt . snd <$> asciiRows keys []+toHdrText :: TblHdr -> Name "column header"+toHdrText = \case+ V kv -> convertName kv+ AndMore n -> fromString $ sez @"normal" $ t'"{+" &+ n &+ '}'++----------------------------------------------------------------------++renderSeq :: Sayable "normal" v+ => RenderConfig+ -> FmtLine+ -> (TblHdrs, TblHdrs)+ -> KVITable v+ -> [Text]+renderSeq cfg fmt kmap kvitbl = fmtRender fmt . snd <$> asciiRows kmap mempty where- asciiRows :: [Key] -> KeySpec -> [ (Bool, [FmtVal]) ]- asciiRows [] path =- let v = lookup path kvitbl+ filterBlank = if hideBlankRows cfg+ then L.filter (not . all isNothing . snd)+ else id+ asciiRows :: (TblHdrs, TblHdrs)+ -> KeySpec+ -> [ (Bool, [FmtVal]) ]+ asciiRows ([], []) path =+ let v = KVIT.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 = \case- subs@(sub0:_) | key `elem` rowGroup cfg ->- let subl = [ (True, replicate (length $ snd sub0) Separator) ]- in if fst (last subs)- then init subs <> subl- else subs <> subl- subs -> 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+ in if skip then mempty+ else single $ (False, single $ maybe (TxtVal "") TxtVal (T.pack . sez @"normal" <$> v) )+ asciiRows ([], colKeyMap) path =+ let filterOrDefaultBlankRows = fmap (fmap defaultBlanks) . filterBlank+ defaultBlanks = fmap (\v -> maybe (TxtVal "") TxtVal v)+ in filterOrDefaultBlankRows $ single $ (False, multivalRows colKeyMap path)+ asciiRows ((key, keyvals) : kseq, colKeyMap) path =+ let subrows = \case+ V keyval -> asciiRows (kseq, colKeyMap) $ snoc path (key, keyval)+ _ ->+ let ttlcols = product ( length . snd <$> colKeyMap)+ in [ (False , replicate (ttlcols + length kseq) $ Overage) ]+ grprow = \case+ subs@(sub0:_) | key `elem` rowGroup cfg ->+ let subl = single (True, replicate (length $ snd sub0) Separator)+ in if fst (last subs) then init subs <> subl else subs <> subl+ subs -> subs+ genSubRow keyval = grprow $ fst+ $ foldl leftAdd (mempty, toHdrText keyval) $ subrows keyval+ leftAdd (acc,kv) (b,subrow) =+ (snoc acc (b, TxtVal (nameText kv) : subrow)+ , if rowRepeat cfg then kv else ""+ )+ in concat (genSubRow <$> keyvals)++ multivalRows :: TblHdrs -> KeySpec -> [ Maybe Text ]+ multivalRows ((key, keyvals) : []) path =+ let showEnt = T.pack . sez @"normal"+ in (\case+ V v -> (showEnt <$> (KVIT.lookup' (snoc path (key,v)) kvitbl))+ _ -> Nothing+ ) <$> keyvals+ multivalRows ((key, keyvals) : kseq) path =+ concatMap (\case+ V v -> multivalRows kseq (snoc path (key,v))+ _ -> mempty+ ) keyvals multivalRows [] _ = error "multivalRows cannot be called with no keys!"
src/Data/KVITable/Render/HTML.hs view
@@ -1,6 +1,11 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -fno-warn-orphans #-} -- | This module provides the 'KVITable' 'render' function for -- rendering the table in a HTML table format. The various HTML table@@ -20,16 +25,22 @@ import qualified Data.List as L import Data.List.NonEmpty ( NonEmpty( (:|) ) ) import qualified Data.List.NonEmpty as NEL-import Data.Maybe ( fromMaybe, isNothing )+import Data.Maybe ( isNothing )+import Data.Name ( Named, HTMLStyle, UTF8, convertName+ , convertStyle, nameText )+import Data.String ( fromString ) 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 Numeric.Natural+import Text.Sayable -import Data.KVITable+import Data.KVITable as KVIT+import Data.KVITable.Internal.Helpers import Data.KVITable.Render+import Data.KVITable.Render.Internal import Prelude hiding ( lookup ) @@ -39,20 +50,27 @@ -- definition; it is intended to be embedded in a larger HTML -- document. -render :: PP.Pretty v => RenderConfig -> KVITable v -> Text+render :: Sayable "html" 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+ let kmap = renderingKeyVals cfg $ t ^. keyVals+ (fmt, hdr) = renderHdrs cfg kmap t+ bdy = renderSeq cfg fmt kmap t in TL.toStrict $ renderText $ table_ [ class_ "kvitable" ] $- do maybe mempty (caption_ . toHtml) $ Data.KVITable.Render.caption cfg+ do maybe mempty (caption_ . toHtml . convertStyle @UTF8 @HTMLStyle)+ $ Data.KVITable.Render.caption cfg thead_ [ class_ "kvitable_head" ] hdr tbody_ [ class_ "kvitable_body" ] bdy ++instance ToHtml (Named HTMLStyle nameOf) where+ toHtml = toHtmlRaw . nameText+ -- Note: toHtml uses toHtmlRaw because Named HTMLStyle is already escaped+ toHtmlRaw = toHtmlRaw . nameText+ ---------------------------------------------------------------------- -data FmtLine = FmtLine [Int] -- colspans, length is # columns+data FmtLine = FmtLine [Natural] -- colspans, length is # columns instance Semigroup FmtLine where (FmtLine c1) <> (FmtLine c2) = FmtLine $ c1 <> c2@@ -60,40 +78,56 @@ instance Monoid FmtLine where mempty = FmtLine mempty -fmtAddColLeft :: Int -> FmtLine -> FmtLine+fmtAddColLeft :: Natural -> FmtLine -> FmtLine fmtAddColLeft lspan (FmtLine col) = FmtLine $ lspan : col -data FmtVal = Val Height LastInGroup Text- | Hdr Height LastInGroup Text- deriving Show-type Height = Int+data FmtVal = Val Span LastInGroup Bool Text+ | Hdr Span LastInGroup (Named HTMLStyle "column header")+data Span = Singular | Cols Width | Rows Height+type Height = Natural+type Width = Natural type LastInGroup = Bool-type RightLabel = Text+type RightLabel = Named HTMLStyle "column header" -fmtRender :: FmtLine -> [FmtVal] -> Maybe RightLabel -> Html ()-fmtRender (FmtLine cols) vals mbRLabel = do+fmtRender :: FmtLine -> Maybe RightLabel -> [FmtVal] -> Html ()+fmtRender (FmtLine cols) mbRLabel vals = 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 []+ , case h of+ Singular -> mempty+ Cols n -> if w == 1 && n == 1+ then mempty+ else [ colspan_ $ T.pack $ show (n*w)+ , class_ " multicol"+ ]+ Rows 1 -> mempty+ Rows n -> [ rowspan_ $ T.pack $ show n ]+ , if w == 1+ || case h of+ Cols n | n > 1 -> True+ _ -> False+ then mempty else [ colspan_ $ T.pack $ show w , class_ " multicol" ]- , if l then [ class_ " last_in_group" ] else []+ , if l then [ class_ " last_in_group" ] else mempty ]- in th_ (concat $ reverse a) (toHtml v)- cell (w,Val h l v) =+ in th_ (concat $ reverse a) (div_ $ span_ $ toHtml v)+ cell (w,Val h l i v) = let a = [ [ class_ "kvitable_td" ]- , if h == 1 then []- else [ rowspan_ $ T.pack $ show h ]- , if w == 1 then []+ , case h of+ Singular -> mempty+ Cols 1 -> mempty+ Cols n -> [ colspan_ $ T.pack $ show n ]+ Rows 1 -> mempty+ Rows n -> [ rowspan_ $ T.pack $ show n ]+ , if w == 1 then mempty else [ colspan_ $ T.pack $ show w , class_ " multicol" ]- , if l then [ class_ " last_in_group" ] else []+ , if l then [ class_ " last_in_group" ] else mempty ]- in td_ (concat $ reverse a) (toHtml v)+ in td_ (concat $ reverse a) $ if i then i_ (toHtml v) else toHtml v labelMark = toHtmlRaw (" ←" :: Text) labelHtml = th_ [ class_ "rightlabel kvitable_th" ] . (labelMark <>) .@@ -107,7 +141,7 @@ data HeaderLine = HdrLine FmtLine HdrVals Trailer type HdrVals = [FmtVal]-type Trailer = Maybe Text+type Trailer = Maybe RightLabel instance Semigroup HeaderLine where (HdrLine fmt1 hv1 t1) <> (HdrLine fmt2 hv2 _) =@@ -116,140 +150,177 @@ hdrFmt :: HeaderLine -> FmtLine hdrFmt (HdrLine fmt _ _) = fmt -renderHdrs :: PP.Pretty v- => RenderConfig -> KVITable v -> [Key]+renderHdrs :: Sayable "html" v+ => RenderConfig+ -> (TblHdrs, TblHdrs)+ -> KVITable v -> ( FmtLine, Html () )-renderHdrs cfg t keys = ( rowfmt, sequence_ hdrs )+renderHdrs cfg kmap t = ( rowfmt, sequence_ hdrs ) where hdrs = fmap renderHdr hrows- (hrows, rowfmt) = hdrstep cfg t keys- renderHdr (HdrLine fmt hdrvals trailer) = fmtRender fmt hdrvals trailer+ (hrows, rowfmt) = hdrstep cfg t kmap+ renderHdr (HdrLine fmt hdrvals trailer) = fmtRender fmt trailer hdrvals -hdrstep :: PP.Pretty v- => RenderConfig -> KVITable v -> [Key]+hdrstep :: Sayable "html" v+ => RenderConfig+ -> KVITable v+ -> (TblHdrs, TblHdrs) -> (NEL.NonEmpty 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 (nexthdr0 :| nexthdrs, lowestfmt) = hdrstep cfg t keys- (HdrLine fmt vals tr) = nexthdr0- fmt' = fmtAddColLeft 1 fmt- val = Hdr (length nexthdrs + 1) False key- in ( (HdrLine fmt' (val : vals) tr) :| nexthdrs- , fmtAddColLeft 1 lowestfmt- )+hdrstep _cfg t ([], []) =+ let hdr = Hdr Singular False $ t ^. valueColName+ one = single 1+ in ( HdrLine (FmtLine one) (single hdr) Nothing :| mempty+ , FmtLine one+ )+hdrstep cfg t ([], colKeys) =+ hdrvalstep cfg t colKeys mempty -- switch to column stacking mode+hdrstep cfg t ((key,_) : keys, colKeys) =+ let (nexthdr0 :| nexthdrs, lowestfmt) = hdrstep cfg t (keys, colKeys)+ (HdrLine fmt vals tr) = nexthdr0+ fmt' = fmtAddColLeft 1 fmt+ val = Hdr (Rows $ nLength nexthdrs + 1) False+ $ convertStyle $ convertName key+ in ( (HdrLine fmt' (val : vals) tr) :| nexthdrs+ , fmtAddColLeft 1 lowestfmt+ ) -hdrvalstep :: PP.Pretty v- => RenderConfig -> KVITable v -> KeySpec -> [Key]+hdrvalstep :: Sayable "html" v+ => RenderConfig+ -> KVITable v+ -> TblHdrs+ -> KeySpec -> (NEL.NonEmpty 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 ordering = if sortKeyVals cfg then sortWithNums else id- in case ordering $ fromMaybe [] $ L.lookup key $ t ^. keyVals of- [] -> error "cannot happen"- (ttl:ttls) ->- let- titles = ttl :| ttls- subhdrsV v = hdrvalstep cfg t (steppath <> [(key,v)]) keys- subTtlHdrs :: NEL.NonEmpty (NEL.NonEmpty HeaderLine, FmtLine)- subTtlHdrs = subhdrsV <$> titles- subhdrs :: NEL.NonEmpty (NEL.NonEmpty HeaderLine, FmtLine)- subhdrs = if hideBlankCols cfg- then subTtlHdrs- else- -- Want to repeat the first element of subTtlHdrs to get a- -- NonEmpty the same length as titles. Both titles and- -- subTtlHdrs are NonEmpty, but NonEmpty has no replicate- -- function.- let n = length titles -- >= 1 because titles is NonEmpty- e = NEL.head subTtlHdrs- tail' = NEL.take (n-1) $ NEL.repeat e- in e :| tail'- subhdr_rollup = joinHdrs <$> NEL.transpose (fst <$> subhdrs)- joinHdrs :: NEL.NonEmpty HeaderLine -> HeaderLine- joinHdrs (hl0 :| hls) = foldl (<>) hl0 hls- superFmt :: (NEL.NonEmpty HeaderLine, FmtLine) -> Int- superFmt sub = let FmtLine subcols = hdrFmt $ NEL.last $ fst sub- in if sum subcols == 0- then 0- else length $ L.filter (/= 0) subcols- topfmt = FmtLine $ NEL.toList (superFmt <$> subhdrs)- tophdr = HdrLine topfmt (NEL.toList (Hdr 1 False <$> titles)) $ Just key- in ( NEL.cons tophdr subhdr_rollup, F.fold (snd <$> subTtlHdrs))+hdrvalstep _ _ [] _ = error "HTML hdrvalstep with empty keys after matching colStackAt -- impossible"+hdrvalstep cfg t ((key, titles) : []) steppath =+ let cvalWidths kv = fmap (length . sez @"html" . snd) $+ L.filter ((L.isSuffixOf (snoc steppath (key, kv))) . fst)+ $ KVIT.toList t+ cwidth = \case+ V c -> if hideBlankCols cfg && 0 == (sum $ cvalWidths c) then 0 else 1+ AndMore _ -> 1+ fmt = FmtLine (cwidth <$> titles)+ hdr = Hdr Singular False . toHdrText <$> titles+ k = convertStyle @UTF8 @HTMLStyle $ convertName key+ in ( HdrLine fmt hdr (Just k) :| mempty, fmt)+hdrvalstep _cfg _t ((_key, []) : _keys) _steppath = error "cannot happen"+hdrvalstep cfg t ((key, ttl:ttls) : keys) steppath =+ let+ titles = ttl :| ttls+ subhdrsV v = hdrvalstep cfg t keys (case v of+ V kv -> snoc steppath (key,kv)+ _ -> steppath+ )+ subTtlHdrs :: NEL.NonEmpty (NEL.NonEmpty HeaderLine, FmtLine)+ subTtlHdrs = subhdrsV <$> titles+ subhdrs :: NEL.NonEmpty (NEL.NonEmpty HeaderLine, FmtLine)+ subhdrs = if hideBlankCols cfg+ then subTtlHdrs+ else+ -- Want to repeat the first element of subTtlHdrs to get a+ -- NonEmpty the same length as titles. Both titles and+ -- subTtlHdrs are NonEmpty, but NonEmpty has no replicate+ -- function.+ let n = length titles -- >= 1 because titles is NonEmpty+ e = NEL.head subTtlHdrs+ tail' = NEL.take (n-1) $ NEL.repeat e+ in e :| tail'+ subhdr_rollup = joinHdrs <$> NEL.transpose (fst <$> subhdrs)+ joinHdrs :: NEL.NonEmpty HeaderLine -> HeaderLine+ joinHdrs (hl0 :| hls) = foldl (<>) hl0 hls+ superFmt :: (NEL.NonEmpty HeaderLine, FmtLine) -> Natural+ superFmt sub = let FmtLine subcols = hdrFmt $ NEL.last $ fst sub+ in if sum subcols == 0+ then 0+ else nLength $ L.filter (/= 0) subcols+ topfmt = FmtLine $ NEL.toList (superFmt <$> subhdrs)+ tophdr = let h = Hdr Singular False . toHdrText <$> titles+ tr = convertStyle @UTF8 @HTMLStyle $ convertName key+ in HdrLine topfmt (NEL.toList h) $ Just tr+ in ( NEL.cons tophdr subhdr_rollup, F.fold (snd <$> subTtlHdrs)) ++toHdrText :: TblHdr -> Named HTMLStyle "column header"+toHdrText th = case toHdrText' th of+ Right t -> t+ Left n -> fromString $ sez @"html" $ t'"{+" &+ n &+ '}'++toHdrText' :: TblHdr -> Either Natural (Named HTMLStyle "column header")+toHdrText' = \case+ V kv -> Right $ convertStyle @UTF8 @HTMLStyle $ convertName kv+ AndMore n -> Left n+ ---------------------------------------------------------------------- -renderSeq :: PP.Pretty v- => RenderConfig -> FmtLine -> [Key] -> KVITable v -> Html ()-renderSeq cfg fmt keys t =- mapM_ (flip (fmtRender fmt) Nothing) $ htmlRows keys []+renderSeq :: Sayable "html" v+ => RenderConfig -> FmtLine+ -> (TblHdrs, TblHdrs)+ -> KVITable v+ -> Html ()+renderSeq cfg fmt kmap t =+ let lst = htmlRows kmap mempty+ rndr = fmtRender fmt Nothing+ in sequence_ (each rndr lst) where- mkVal = Val 1 False . T.pack . show . PP.pretty- htmlRows :: [Key] -> KeySpec -> [ [FmtVal] ]- htmlRows [] path =- let v = lookup path t+ each = map++ filterBlank = if hideBlankRows cfg+ then L.filter (not . all isNothing)+ else id++ mkVal = Val Singular False False . T.pack . sez @"html"++ htmlRows :: (TblHdrs, TblHdrs) -> 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 =+ row = maybe (Val Singular False False "") mkVal v+ in if skip then mempty else single $ single row+ htmlRows ([], colKeyMap) path = 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-+ fmap (fmap (maybe (Val Singular False False "") id)) . filterBlank+ in filterOrDefaultBlankRows $ single $ multivalRows colKeyMap path+ htmlRows ((key,keyvals) : kseq, colKeyMap) path =+ let subrows = \case+ V keyval -> htmlRows (kseq, colKeyMap) $ snoc path (key,keyval)+ _ -> [ [Val (Cols nCols) True True "more"] ]+ nCols = product ( nLength . snd <$> colKeyMap )+ nHdrs = nLength kseq + 1+ endOfGroup = key `elem` rowGroup cfg+ genSubrows keyval =+ let sr = subrows keyval+ kv = toHdrText' keyval+ in fst+ $ foldl (leftAdd (nLength sr)) (mempty, Just kv)+ $ 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' i v) = Val h (g || g') i v+ setValGrouping g (Hdr h g' v) = Hdr h (g || g') v+ in ( snoc acc+ (case mb'kv of+ Nothing -> sr+ Just (Right kv) ->+ let w = if rowRepeat cfg then 1 else nrows+ in Hdr (Rows w) endOfGroup kv : sr+ Just (Left n) ->+ let m = fromString $ sez @"html" $ t'"{+" &+ n &+ '}'+ in Hdr (Cols nHdrs) True m : sr+ )+ , if rowRepeat cfg then mb'kv else Nothing)+ in concat $ each genSubrows 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+ multivalRows ((key, keyvals) : []) path =+ (\case+ V v -> mkVal <$> lookup' (snoc path (key,v)) t+ _ -> Just $ Val Singular False True "... -->"+ ) <$> keyvals+ multivalRows ((key, keyvals) : kseq) path =+ concatMap (\case+ V v -> multivalRows kseq (snoc path (key,v))+ _ -> mempty+ ) keyvals
+ src/Data/KVITable/Render/Internal.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Data.KVITable.Render.Internal where++import qualified Data.List as L+import Data.Name ( ConvertName, UTF8 )+import Data.String ( fromString )+import Numeric.Natural++import Data.KVITable+import Data.KVITable.Internal.Helpers+import Data.KVITable.Render+++-- TODO: to allow for hideBlankCols, the KVITable should keep track of what the+-- pre-declared vals for each key are v.s. what additional vals may be set by+-- setting a value in that region. Then there is enough information here+-- (without re-scanning the table) to properly compute the keyvals that should be+-- displayed (which would also mean that the hideCols/hideRows determinations in+-- the rendering functions below are no longer needed.++data TblHdr = V (KeyVal) | AndMore Natural++type TblHdrs = [ (Key, [TblHdr]) ]++-- | Returns the rows and columns KeyVals, with appropriate application of+-- RenderConfig specifications: colStackAt, maxCells, maxCols. Does not collapse+-- empty rows or columns.++renderingKeyVals :: RenderConfig+ -> KeyVals+ -> (TblHdrs, TblHdrs)+renderingKeyVals cfg inpKvs =+ let maxNumKeys = maxCells cfg+ origNumKeys = toEnum $ length inpKvs+ maxNumCols = min (maxCells cfg) (maxCols cfg)++ in case colStackAt cfg of+ Nothing ->+ -- width is just keys, height is combination of keys and values+ let okKvs = if origNumKeys > maxNumKeys+ then snoc (take (fromEnum maxNumKeys) (fst kvs))+ (fromString+ $ "{+ " <> show (origNumKeys - maxNumKeys) <> " MORE}"+ , mempty+ )+ else (fst kvs)+ -- n.b. maxCols is not really useful here, since all but the last+ -- column are headers and values are only shown in that last column.+ in (snd $ trimStacked True 1 maxNumKeys okKvs, [])+ Just _c ->+ let (kvsRows, kvsCols) = kvs+ numRegularColKvs = let v = length inpKvs - length kvsCols+ in if v < 0 then error "BAD1" else toEnum v+ numStackedCols = countStacked kvsCols+ origNumCols = numRegularColKvs + numStackedCols+ allowedNumCols = subOrDef 1 maxNumCols numRegularColKvs+ okKvsCols = if origNumCols > maxNumCols+ then if numStackedCols <= maxNumCols+ then kvsCols+ else snd $ trimStacked False 1 allowedNumCols kvsCols+ else kvsCols+ allowedNumRows = subOrDef 1 (maxCells cfg)+ (if origNumCols > maxNumCols+ then if numStackedCols <= maxNumCols+ then numStackedCols+ else allowedNumCols+ else numStackedCols+ )+ eachRowCols = min maxNumCols numStackedCols+ okKvsRows = snd $ trimStacked False eachRowCols allowedNumRows kvsRows+ in (okKvsRows, okKvsCols)++ where++ -- subtracting Naturals must be done carefully to not allow the result to be+ -- < 0; a post-subtraction max is not enough to protect against the initial+ -- value.+ subOrDef d a b = if a < b then d else a - b++ kvs = let kvs' = case colStackAt cfg of+ Nothing -> (inpKvs, mempty)+ Just c -> span ((/= c) . fst) inpKvs+ ksrt = case sortKeyVals cfg of+ Nothing -> kvs'+ Just fn -> fn kvs'+ in ( fmap (fmap V) <$> fst ksrt+ , fmap (fmap V) <$> snd ksrt+ )++ countStacked = \case -- does not allow for hiddenCols+ [] -> 1+ ((_,vs):r) -> toEnum (L.length vs) * countStacked r+ trimStacked _ each n [] = ((n,each), [])+ trimStacked _mulSubs each n ((k,vs):[]) =+ let lvs = toEnum $ length vs+ mvs = foldl (\a b -> if b * each < n then b else a) 1 $ [0..lvs]+ tvs = snoc (take (fromEnum mvs) vs) $ AndMore $ lvs - mvs+ rvs = if mvs < lvs then tvs else vs+ in ((subOrDef 0 n mvs, mvs * each), [(k,rvs)])+ trimStacked mulSubs each n ((k,vs):rkvs) =+ let lvs = toEnum $ length vs+ ((n',w), kvs') = trimStacked mulSubs each n rkvs+ mvs = foldl (\a b -> if b * w < n then b else a) 1 $ [0..lvs]+ tvs = snoc (take (fromEnum mvs) vs) $ AndMore $ remcnt (lvs - mvs) rkvs+ rvs = if mvs < lvs then tvs else vs+ in ((subOrDef 0 n' mvs, mvs * w), (k,rvs):kvs')++ remcnt n [] = n+ remcnt n (rkv:rkvs) = n * remcnt (toEnum $ length (snd rkv)) rkvs+++instance ConvertName UTF8 "Key" "column header"+instance ConvertName UTF8 "KeyVal" "column header"+++nLength :: Foldable t => t a -> Natural+nLength = toEnum . length
test/AsciiRenderTests.hs view
@@ -5,6 +5,7 @@ module AsciiRenderTests where import Control.Monad ( unless )+import Data.String ( fromString ) import Data.Text ( Text ) import qualified Data.Text as T import Lens.Micro ( (^.), (.~), (%~), (&) )@@ -44,6 +45,7 @@ , fmap (de "∌ ") $ drop (length al) el , fmap (da "∹ ") $ drop (length el) al ])+ -- writeFile "test.txt" $ T.unpack $ T.unlines al assertFailure $ T.unpack $ T.unlines details @@ -120,7 +122,7 @@ , testCase "medium sized table render, sorted" $ cmpTables "medium table"- (KTRA.render (cfg0 { KTR.sortKeyVals = True }) mediumKVI) [sq|+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha }) mediumKVI) [sq| **** | compiler | debug | optimization | Value | +----------+-------+--------------+-------+@@ -142,7 +144,7 @@ , testCase "medium sized table render, unsorted" $ cmpTables "medium table"- (KTRA.render (cfg0 { KTR.sortKeyVals = False }) mediumKVI) [sq|+ (KTRA.render (cfg0 { KTR.sortKeyVals = Nothing }) mediumKVI) [sq| **** | compiler | debug | optimization | Value | +----------+-------+--------------+-------+@@ -164,7 +166,7 @@ , testCase "medium table, sorted, blank, row skip, no repeats" $ cmpTables "medium table"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False }) mediumKVI) [sq| **** | compiler | debug | optimization | Value |@@ -187,7 +189,7 @@ , testCase "medium table, sorted, blank row skip, no repeats, group unknown" $ cmpTables "medium table s 0brow 0rep [unknown]"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = ["unknown"] }) mediumKVI) [sq|@@ -212,7 +214,7 @@ , testCase "medium table, sorted, blank row skip, no repeats, group compiler" $ cmpTables "medium table s 0brow 0rep [unknown]"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = ["compiler"] }) mediumKVI) [sq|@@ -242,7 +244,7 @@ , 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+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = ["unknown", "compiler", "unk", "debug", "huh"] }) mediumKVI) [sq|@@ -275,7 +277,7 @@ , testCase "medium table, sorted, blank" $ cmpTables "medium table"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.hideBlankRows = False }) mediumKVI) [sq| **** | compiler | debug | optimization | Value |@@ -314,7 +316,7 @@ , testCase "medium sized table render, sorted, no blank, colstack unknown" $ cmpTables "medium table s 0blnk colstk=unknown"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.colStackAt = Just "unknown" }) mediumKVI) [sq| ****@@ -338,7 +340,7 @@ , testCase "medium sized table render, sorted, !blank, colstk optimization" $ cmpTables "medium table s 0blnk colstk=optimization"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.hideBlankRows = True , KTR.colStackAt = Just "optimization" }) mediumKVI) [sq|@@ -357,7 +359,7 @@ , 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+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.hideBlankRows = True , KTR.colStackAt = Just "optimization" , KTR.rowRepeat = False@@ -383,7 +385,7 @@ , testCase "medium sized table render, sorted, !blank, equisize, colstack debug" $ cmpTables "medium table s 0blnk equisize colstk=debug"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.hideBlankRows = True , KTR.equisizedCols = True , KTR.colStackAt = Just "debug"@@ -401,7 +403,7 @@ , testCase "medium sized table render, sorted, !blank, fitsize, colstack debug" $ cmpTables "medium table s 0blnk fitsize colstk=debug"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.hideBlankRows = True , KTR.equisizedCols = False , KTR.colStackAt = Just "debug"@@ -431,7 +433,7 @@ ] step "ASCII rendering unsorted" cmpTables "small table right aligned unsorted"- (KTRA.render (cfg0 { KTR.sortKeyVals = False }) tbl) [sq|+ (KTRA.render (cfg0 { KTR.sortKeyVals = Nothing }) tbl) [sq| **** | id | name | +----+--------------+@@ -441,7 +443,7 @@ ****|] step "ASCII rendering sorted" cmpTables "small table right aligned sorted"- (KTRA.render (cfg0 { KTR.sortKeyVals = True }) tbl) [sq|+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha }) tbl) [sq| **** | id | name | +----+--------------+@@ -462,7 +464,7 @@ , ([("City name", "Sydney"), ("Area", "2058"), ("Population", "4336374")], 1214.8) ] in cmpTables "small table float value table"- (KTRA.render (cfg0 { KTR.sortKeyVals = True }) tbl) [sq|+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha }) tbl) [sq| **** | City name | Area | Population | Annual Rainfall | +-----------+------+------------+-----------------+@@ -477,7 +479,7 @@ , testCase "big table grouped sorted" $ cmpTables "big table grouped sorted"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = [ "Location", "Biome", "Category" ] }) zooTable2)@@ -493,7 +495,7 @@ 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+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = [ "Location", "Biome", "Category" ] , KTR.colStackAt = Just "Name"@@ -503,7 +505,7 @@ , testCase "big table grouped sorted equisized" $ cmpTables "big table grouped sorted equisized"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.equisizedCols = True@@ -531,7 +533,7 @@ , testCase "big table grouped sorted fitsize colstack=ghcver" $ cmpTables "big table grouped sorted fitsize colstack=ghcver"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.equisizedCols = False@@ -559,7 +561,7 @@ , testCase "big table grouped sorted fitsize colstack=Strategy" $ cmpTables "big table grouped sorted fitsize colstack=Strategy"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.equisizedCols = False@@ -585,7 +587,7 @@ , testCase "nested table hide=blankRows,blankCols colstack=ones" $ cmpTables "nested table hide=blankRows,blankCols colstack=ones"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -618,7 +620,7 @@ , testCase "nested table colstack=tens" $ cmpTables "nested table colstack=tens"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -655,7 +657,7 @@ , testCase "nested table hide=blankCols,blankRows colstack=hundreds" $ cmpTables "nested table hide-blankCols,blankRows colstack=hundreds"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -666,7 +668,7 @@ , testCase "nested table hide=none colstack=hundreds" $ cmpTables "nested table hide=none colstack=hundreds"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.hideBlankRows = False@@ -675,9 +677,32 @@ }) nestedTable) [uq_f|examples/hundreds_all.md|] + , testCase "nested table hide=none colstack=hundreds testfile" $+ cmpTables "nested table hide=none colstack=hundreds testfile"+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = False+ , KTR.hideBlankRows = False+ , KTR.equisizedCols = False+ , KTR.colStackAt = Just "hundreds"+ }) nestedTable)+ [sq_f|test/evenodd.md|]++ , testCase "nested table hide=none colstack=hundreds, maxCells=60" $+ cmpTables "nested table hide=none colstack=hundreds, maxCells=60"+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = False+ , KTR.hideBlankRows = False+ , KTR.equisizedCols = False+ , KTR.colStackAt = Just "hundreds"+ , KTR.maxCells = 60+ }) nestedTable)+ [sq3_f|test/evenodd.md|]+ , testCase "nested table hide=none colstack=hundreds equisized" $ cmpTables "nested table hide=none colstack=hundreds equisized"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.hideBlankRows = False@@ -688,7 +713,7 @@ , testCase "nested table hideBlank=rol,col colstack=thousands" $ cmpTables "nested table hideBlank=row,col colstack=thousands"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -699,7 +724,7 @@ , testCase "nested table hideBlank=rol,col" $ cmpTables "nested table hideBlank=row,col"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -707,9 +732,20 @@ }) nestedTable) [uq3_f|README.md|] + , testCase "nested table hideBlank=rol,col, maxCells=60" $+ cmpTables "nested table hideBlank=row,col, maxCells=60"+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = True+ , KTR.hideBlankRows = True+ , KTR.equisizedCols = False+ , KTR.maxCells = 60+ }) nestedTable)+ [sq5_f|test/evenodd.md|]+ , testCase "nested table hideBlank=none" $ cmpTables "nested table hideBlank=none"- (KTRA.render (cfg0 { KTR.sortKeyVals = True+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.hideBlankRows = False@@ -829,4 +865,30 @@ | | | | | 1 | odd | ****|] ++ , testCase "big square table, numCols and numCells limited" $+ let tbl = foldl KVI.foldlInsert+ (mempty & KVI.valueColName .~ "Hits"+ & KVI.keyVals+ .~ [ ("Pitcher", [ fromString ("P" <> show n)+ | n <- [0..100::Int] ])+ , ("Batter", [ fromString ("B" <> show n)+ | n <- [0..100::Int] ])+ ])+ [ ([ ("Pitcher", fromString ("P" <> show p))+ , ("Batter", fromString ("B" <> show b))+ ], p * b)+ | p <- [0..100::Int]+ , b <- [0..100::Int]+ ]+ in cmpTables "big table grouped sorted no-subtype colstack"+ (KTRA.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.rowGroup = [ "Location", "Biome", "Category" ]+ , KTR.colStackAt = Just "Batter"+ , KTR.equisizedCols = False+ , KTR.maxCols = 20+ , KTR.maxCells = 600+ }) tbl)+ [sq_f|test/bigsquare.md|] ]
test/HTMLRenderTests.hs view
@@ -6,6 +6,7 @@ import Control.Monad ( unless ) import qualified Data.List as L+import Data.String ( fromString ) import Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.Lazy as TL@@ -55,6 +56,7 @@ , fmap (de "∌ ") $ drop (length al) el , fmap (da "∹ ") $ drop (length el) al ])+ -- writeFile "test.html" $ T.unpack $ T.unlines al assertFailure $ T.unpack $ T.unlines details @@ -71,7 +73,7 @@ **** <table class="kvitable"> <thead class="kvitable_head">- <tr class="kvitable_tr"><th class="kvitable_th">Value</th></tr>+ <tr class="kvitable_tr"><th class="kvitable_th"><div><span>Value</span></div></th></tr> </thead> <tbody class="kvitable_body"> </tbody>@@ -85,7 +87,7 @@ **** <table class="kvitable"> <thead class="kvitable_head">- <tr class="kvitable_tr"><th class="kvitable_th">Value</th></tr>+ <tr class="kvitable_tr"><th class="kvitable_th"><div><span>Value</span></div></th></tr> </thead> <tbody class="kvitable_body"> <tr class="kvitable_tr">@@ -103,9 +105,9 @@ <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>+ <th class="kvitable_th"><div><span>foo</span></div></th>+ <th class="kvitable_th"><div><span>dog</span></div></th>+ <th class="kvitable_th"><div><span>Value</span></div></th> </tr> </thead> <tbody class="kvitable_body">@@ -116,7 +118,7 @@ , testCase "nested table hideBlank=rows,cols, fitted, colstack=hundreds" $ cmpTables "nested table hideBlank=rows,cols, fitted, colstack=hundreds"- (KTRH.render (cfg0 { KTR.sortKeyVals = True+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -127,7 +129,7 @@ , testCase "nested table hide=none, fitted, colstack=hundreds" $ cmpTables "nested table hide=none, fitted, colstack=hundreds"- (KTRH.render (cfg0 { KTR.sortKeyVals = True+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = False , KTR.hideBlankRows = False@@ -136,9 +138,48 @@ }) nestedTable) [sq_f|examples/hundreds_all.md|] + -- duplication of the above in test/evenodd.md+ , testCase "nested table hide=none, fitted, colstack=hundreds" $+ do+ cmpTables "nested table hide=none, fitted, colstack=hundreds"+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = False+ , KTR.hideBlankRows = False+ , KTR.equisizedCols = False+ , KTR.colStackAt = Just "hundreds"+ }) nestedTable)+ [sq2_f|test/evenodd.md|]++ , testCase "nested table hide=none, fitted, colstack=hundreds, maxCells=60" $+ do+ cmpTables "nested table hide=none, fitted, colstack=hundreds, maxCells=60"+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = False+ , KTR.hideBlankRows = False+ , KTR.equisizedCols = False+ , KTR.colStackAt = Just "hundreds"+ , KTR.maxCells = 60+ }) nestedTable)+ [sq4_f|test/evenodd.md|]++ , testCase "nested table hide=none, fitted, no colstack, maxCells=60" $+ do+ cmpTables "nested table hide=none, fitted, no colstack, maxCells=60"+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.hideBlankCols = False+ , KTR.hideBlankRows = False+ , KTR.equisizedCols = False+ , KTR.colStackAt = Nothing+ , KTR.maxCells = 60+ }) nestedTable)+ [sq6_f|test/evenodd.md|]+ , testCase "nested table hideBlank=rol,col colstack=thousands" $ cmpTables "nested table hideBlank=row,col colstack=thousands"- (KTRH.render (cfg0 { KTR.sortKeyVals = True+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -149,7 +190,7 @@ , testCase "nested table hideBlank=rol,col" $ cmpTables "nested table hideBlank=row,col"- (KTRH.render (cfg0 { KTR.sortKeyVals = True+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.hideBlankCols = True , KTR.hideBlankRows = True@@ -160,7 +201,7 @@ , testCase "big table grouped sorted" $ cmpTables "big table grouped sorted"- (KTRH.render (cfg0 { KTR.sortKeyVals = True+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = [ "Location", "Biome", "Category" ] }) zooTable2)@@ -176,7 +217,7 @@ 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+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha , KTR.rowRepeat = False , KTR.rowGroup = [ "Location", "Biome", "Category" ] , KTR.colStackAt = Just "Name"@@ -184,5 +225,29 @@ }) zt) [sq2_f|examples/zoo.md|] -+ , testCase "big square table, numCols and numCells limited" $+ let tbl = foldl KVI.foldlInsert+ (mempty & KVI.valueColName .~ "Hits"+ & KVI.keyVals+ .~ [ ("Pitcher", [ fromString ("P" <> show n)+ | n <- [0..100::Int] ])+ , ("Batter", [ fromString ("B" <> show n)+ | n <- [0..100::Int] ])+ ])+ [ ([ ("Pitcher", fromString ("P" <> show p))+ , ("Batter", fromString ("B" <> show b))+ ], p * b)+ | p <- [0..100::Int]+ , b <- [0..100::Int]+ ]+ in cmpTables "big table grouped sorted no-subtype colstack"+ (KTRH.render (cfg0 { KTR.sortKeyVals = Just KTR.sortNumericAlpha+ , KTR.rowRepeat = False+ , KTR.rowGroup = [ "Location", "Biome", "Category" ]+ , KTR.colStackAt = Just "Batter"+ , KTR.equisizedCols = False+ , KTR.maxCols = 20+ , KTR.maxCells = 600+ }) tbl)+ [sq2_f|test/bigsquare.md|] ]
test/SampleTables.hs view
@@ -3,6 +3,7 @@ module SampleTables where import Data.KVITable+import Data.String ( fromString ) import Data.Text ( Text ) import qualified Data.Text as T import Lens.Micro ( (^.), (.~), (%~), (&) )@@ -195,11 +196,11 @@ , ("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)]+ [ let keyvals = [("millions", fromString $ show m)+ ,("thousands", fromString $ show t)+ ,("hundreds", fromString $ show h)+ ,("tens", fromString $ show d)+ ,("ones", fromString $ show o)] value = if (o `rem` 2) == 1 then "odd" else "even" in (keyvals, value) | m <- [0..2 :: Int]
test/TestMain.hs view
@@ -165,6 +165,18 @@ , ([ "baz", "beam", "woof"], "yo") ] + , testCaseSteps "insertWith" $ \step ->+ do+ let keyvals = [ ("foo", "bar"), ("moo", "cow") ]++ step "inserting a new value"+ let t1 = insertWith (<>) keyvals "old" kvi0+ Just "old" @=? lookup keyvals t1++ step "combining with an existing value"+ let t2 = insertWith (<>) keyvals "new" t1+ Just "newold" @=? lookup keyvals t2+ , testCase "medium sized table rows" $ rows mediumKVI @?= [ ([ "gcc7", "yes", "0"], "good" )
test/TestQQDefs.hs view
@@ -13,6 +13,9 @@ sq_f = quoteFile sq sq2_f = quoteFile $ qqExtractor 2 "\n****" sq3_f = quoteFile $ qqExtractor 3 "\n****"+sq4_f = quoteFile $ qqExtractor 4 "\n****"+sq5_f = quoteFile $ qqExtractor 5 "\n****"+sq6_f = quoteFile $ qqExtractor 6 "\n****" uq :: QuasiQuoter uq = qqExtractor 1 "\n____"